Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add icon name #6411

Merged
merged 4 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions examples/reference/widgets/ButtonIcon.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@
"button"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can also replace the `description` with `name` to have it shown on the side."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"button = pn.widgets.ButtonIcon(icon=\"heart\", size=\"4em\", name=\"favorite\")\n",
"button"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
17 changes: 17 additions & 0 deletions examples/reference/widgets/ToggleIcon.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@
"toggle"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can also replace the `description` with `name` to have it shown on the side."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"toggle = pn.widgets.ToggleIcon(icon=\"heart\", size=\"4em\", name=\"favorite\")\n",
"toggle"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
10 changes: 10 additions & 0 deletions panel/dist/css/icon.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@
opacity: 1;
transform: scale(1.1);
}

.bk-IconLabel {
margin-left: 5px;
}

.bk-IconRow {
display: flex;
align-items: center;
justify-content: center;
}
3 changes: 3 additions & 0 deletions panel/models/icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class _ClickableIcon(Widget):
value = Bool(default=False, help="""
Whether the icon is toggled on or off.""")

title = String(default="", help="""
The title of the icon.""")

tooltip = Nullable(Instance(Tooltip), help="""
A tooltip with plain text or rich HTML contents, providing general help or
description of a widget's or component's function.
Expand Down
109 changes: 61 additions & 48 deletions panel/models/icon.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import type {TooltipView} from "@bokehjs/models/ui/tooltip"
import {Tooltip} from "@bokehjs/models/ui/tooltip"
import type {TablerIconView} from "@bokehjs/models/ui/icons/tabler_icon"
import {TablerIcon} from "@bokehjs/models/ui/icons/tabler_icon"
import type {SVGIconView} from "@bokehjs/models/ui/icons/svg_icon"
import {SVGIcon} from "@bokehjs/models/ui/icons/svg_icon"
import {Control, ControlView} from "@bokehjs/models/widgets/control"
import type {IterViews} from "@bokehjs/core/build_views"
import type * as p from "@bokehjs/core/properties"
import {build_view} from "@bokehjs/core/build_views"
import {ButtonClick} from "@bokehjs/core/bokeh_events"
import type {EventCallback} from "@bokehjs/model"
import { Tooltip, TooltipView } from "@bokehjs/models/ui/tooltip"
import { TablerIcon, TablerIconView } from "@bokehjs/models/ui/icons/tabler_icon";
import { SVGIcon, SVGIconView } from "@bokehjs/models/ui/icons/svg_icon";
import { Control, ControlView } from '@bokehjs/models/widgets/control';
import type { IterViews } from '@bokehjs/core/build_views';
import * as p from "@bokehjs/core/properties";
import { div } from "@bokehjs/core/dom";
import { build_view } from '@bokehjs/core/build_views';
import { ButtonClick } from "@bokehjs/core/bokeh_events"
import type { EventCallback } from "@bokehjs/model"

export class ClickableIconView extends ControlView {
declare model: ClickableIcon

icon_view: TablerIconView | SVGIconView
declare model: ClickableIcon;
icon_view: TablerIconView | SVGIconView;
label_el: HTMLDivElement;
was_svg_icon: boolean
row_div: HTMLDivElement

protected tooltip: TooltipView | null

Expand All @@ -31,11 +30,12 @@ export class ClickableIconView extends ControlView {
await super.lazy_initialize()

this.was_svg_icon = this.is_svg_icon(this.model.icon)
this.icon_view = await this.build_icon_model(this.model.icon, this.was_svg_icon)
const {tooltip} = this.model
if (tooltip != null) {
this.tooltip = await build_view(tooltip, {parent: this})
}
this.label_el = div({ class: 'bk-IconLabel' }, this.model.title);
this.label_el.style.fontSize = this.calculate_size(0.6);
this.icon_view = await this.build_icon_model(this.model.icon, this.was_svg_icon);
const { tooltip } = this.model
if (tooltip != null)
this.tooltip = await build_view(tooltip, { parent: this })
}

override *children(): IterViews {
Expand All @@ -51,19 +51,24 @@ export class ClickableIconView extends ControlView {
}

override connect_signals(): void {
super.connect_signals()
const {icon, active_icon, disabled, value, size} = this.model.properties
this.on_change([active_icon, icon, value], () => this.update_icon())
this.on_change(disabled, () => this.update_cursor())
this.on_change(size, () => this.update_size())
super.connect_signals();
const { icon, active_icon, disabled, value, size } = this.model.properties;
this.on_change([active_icon, icon, value], () => this.update_icon());
this.on_change(this.model.properties.title, () => this.update_label());
this.on_change(disabled, () => this.update_cursor());
this.on_change(size, () => this.update_size());
}

override render(): void {
super.render()
this.icon_view.render()
this.update_icon()
this.update_label()
this.update_cursor()
this.shadow_el.appendChild(this.icon_view.el)
this.row_div = div({
class: 'bk-IconRow'
}, this.icon_view.el, this.label_el)
this.shadow_el.appendChild(this.row_div);

const toggle_tooltip = (visible: boolean) => {
this.tooltip?.model.setv({
Expand All @@ -80,12 +85,17 @@ export class ClickableIconView extends ControlView {
})
}

update_label(): void {
this.label_el.innerText = this.model.title;
}

update_cursor(): void {
this.icon_view.el.style.cursor = this.model.disabled ? "not-allowed" : "pointer"
}

update_size(): void {
this.icon_view.model.size = this.calculate_size()
this.icon_view.model.size = this.calculate_size();
this.label_el.style.fontSize = this.calculate_size(0.6);
}

async build_icon_model(icon: string, is_svg_icon: boolean): Promise<TablerIconView | SVGIconView> {
Expand All @@ -109,15 +119,17 @@ export class ClickableIconView extends ControlView {
if (this.was_svg_icon !== is_svg_icon) {
// If the icon type has changed, we need to rebuild the icon view
// and invalidate the old one.
const icon_view = await this.build_icon_model(icon, is_svg_icon)
icon_view.render()
this.icon_view.remove()
this.icon_view = icon_view
this.was_svg_icon = is_svg_icon
this.update_cursor()
this.shadow_el.appendChild(this.icon_view.el)
} else if (is_svg_icon) {
(this.icon_view as SVGIconView).model.svg = icon
const icon_view = await this.build_icon_model(icon, is_svg_icon);
icon_view.render();
this.icon_view.remove();
this.icon_view = icon_view;
this.was_svg_icon = is_svg_icon;
this.update_cursor();
// We need to re-append the new icon view to the DOM
this.row_div.insertBefore(this.icon_view.el, this.label_el);
}
else if (is_svg_icon) {
(this.icon_view as SVGIconView).model.svg = icon;
} else {
(this.icon_view as TablerIconView).model.icon_name = icon
}
Expand All @@ -128,14 +140,13 @@ export class ClickableIconView extends ControlView {
return this.model.active_icon !== "" ? this.model.active_icon : `${this.model.icon}-filled`
}

calculate_size(): string {
if (this.model.size !== null) {
return this.model.size
}
const maxWidth = this.model.width ?? 15
const maxHeight = this.model.height ?? 15
const size = Math.max(maxWidth, maxHeight)
return `${size}px`
calculate_size(factor: number = 1): string {
if (this.model.size !== null)
return `calc(${this.model.size} * ${factor})`;
const maxWidth = this.model.width ?? 15;
const maxHeight = this.model.height ?? 15;
const size = Math.max(maxWidth, maxHeight) * factor;
return `${size}px`;
}

click(): void {
Expand All @@ -146,10 +157,11 @@ export class ClickableIconView extends ControlView {
export namespace ClickableIcon {
export type Attrs = p.AttrsOf<Props>
export type Props = Control.Props & {
active_icon: p.Property<string>
icon: p.Property<string>
size: p.Property<string | null>
value: p.Property<boolean>
active_icon: p.Property<string>;
icon: p.Property<string>;
size: p.Property<string | null>;
value: p.Property<boolean>;
title: p.Property<string>;
tooltip: p.Property<Tooltip | null>
tooltip_delay: p.Property<number>
}
Expand All @@ -174,6 +186,7 @@ export class ClickableIcon extends Control {
icon: [Str, "heart"],
size: [Nullable(Str), null],
value: [Bool, false],
title: [Str, ""],
tooltip: [Nullable(Ref(Tooltip)), null],
tooltip_delay: [Float, 500],
}))
Expand Down
26 changes: 26 additions & 0 deletions panel/tests/ui/widgets/test_icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,29 @@ def test_button_icon_tooltip(page):
page.hover(".bk-TablerIcon")
wait_until(lambda: page.locator(".bk-tooltip-content") is not None, page)
assert page.locator(".bk-tooltip-content").text_content() == "Click me"


def test_button_icon_name(page):
button = ButtonIcon(name="Like")
serve_component(page, button)

assert button.name == "Like"
assert page.locator(".bk-IconLabel").text_content() == "Like"


def test_button_icon_name_dynamically(page):
button = ButtonIcon(name="Like")
serve_component(page, button)

assert button.name == "Like"
assert page.locator(".bk-IconLabel").text_content() == "Like"

button.name = "Dislike"
assert button.name == "Dislike"
assert page.locator(".bk-IconLabel").text_content() == "Dislike"

assert page.locator(".bk-IconLabel").bounding_box()["width"] < 40

button.size = "2em"
# check size
assert page.locator(".bk-IconLabel").bounding_box()["width"] >= 40
5 changes: 3 additions & 2 deletions panel/widgets/icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@


class _ClickableIcon(Widget):

active_icon = param.String(default='', doc="""
The name of the icon to display when toggled from
[tabler-icons.io](https://tabler-icons.io)/ or an SVG.""")
Expand All @@ -34,7 +35,7 @@ class _ClickableIcon(Widget):
_widget_type = _PnClickableIcon

_rename: ClassVar[Mapping[str, str | None]] = {
**TooltipMixin._rename, 'name': 'name',
**TooltipMixin._rename, 'name': 'title',
}

_source_transforms: ClassVar[Mapping[str, str | None]] = {
Expand Down Expand Up @@ -78,7 +79,7 @@ class ButtonIcon(_ClickableIcon, _ClickButton, TooltipMixin):
_widget_type = _PnButtonIcon

_rename: ClassVar[Mapping[str, str | None]] = {
**TooltipMixin._rename, 'name': 'name', 'clicks': None,
**TooltipMixin._rename, 'name': 'title', 'clicks': None,
}

_target_transforms: ClassVar[Mapping[str, str | None]] = {
Expand Down
Loading