Skip to content

Commit

Permalink
Add icon name (#6411)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 authored Mar 4, 2024
1 parent 93b8d95 commit 579d2e4
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 50 deletions.
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

0 comments on commit 579d2e4

Please sign in to comment.