From 1039d452ce9160cb8426e13846d48fc44387a9a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 13 Nov 2023 10:44:31 +0100 Subject: [PATCH 1/5] Add delay to tooltip for buttons --- panel/models/button.ts | 6 ++++-- panel/models/checkbox_button_group.ts | 6 ++++-- panel/models/radio_button_group.ts | 6 ++++-- panel/models/widgets.py | 15 +++++++++++++++ panel/widgets/_mixin.py | 8 +++++++- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/panel/models/button.ts b/panel/models/button.ts index 69c141475e..9569b91574 100644 --- a/panel/models/button.ts +++ b/panel/models/button.ts @@ -37,7 +37,7 @@ export class ButtonView extends BkButtonView { }) } this.el.addEventListener("mouseenter", () => { - toggle(true) + setTimeout(() => toggle(true), this.model.tooltip_delay) }) this.el.addEventListener("mouseleave", () => { toggle(false) @@ -51,6 +51,7 @@ export namespace Button { export type Props = BkButton.Props & { tooltip: p.Property + tooltip_delay: p.Property } } @@ -69,8 +70,9 @@ export class Button extends BkButton { static { this.prototype.default_view = ButtonView - this.define(({Nullable, Ref}) => ({ + this.define(({Nullable, Ref, Number}) => ({ tooltip: [ Nullable(Ref(Tooltip)), null ], + tooltip_delay: [ Number, 500], })) } } diff --git a/panel/models/checkbox_button_group.ts b/panel/models/checkbox_button_group.ts index 76d56bbb4a..3eec0356d9 100644 --- a/panel/models/checkbox_button_group.ts +++ b/panel/models/checkbox_button_group.ts @@ -40,7 +40,7 @@ export class CheckboxButtonGroupView extends bkCheckboxButtonGroupView { }) } this.el.addEventListener("mouseenter", () => { - toggle(true) + setTimeout(() => toggle(true), this.model.tooltip_delay) }) this.el.addEventListener("mouseleave", () => { toggle(false) @@ -54,6 +54,7 @@ export namespace CheckboxButtonGroup { export type Props = bkCheckboxButtonGroup.Props & { tooltip: p.Property + tooltip_delay: p.Property } } @@ -72,8 +73,9 @@ export class CheckboxButtonGroup extends bkCheckboxButtonGroup { static { this.prototype.default_view = CheckboxButtonGroupView - this.define(({Nullable, Ref}) => ({ + this.define(({Nullable, Ref, Number}) => ({ tooltip: [ Nullable(Ref(Tooltip)), null ], + tooltip_delay: [ Number, 500], })) } } diff --git a/panel/models/radio_button_group.ts b/panel/models/radio_button_group.ts index 1d250c93bc..ccef24a43e 100644 --- a/panel/models/radio_button_group.ts +++ b/panel/models/radio_button_group.ts @@ -40,7 +40,7 @@ export class RadioButtonGroupView extends bkRadioButtonGroupView { }) } this.el.addEventListener("mouseenter", () => { - toggle(true) + setTimeout(() => toggle(true), this.model.tooltip_delay) }) this.el.addEventListener("mouseleave", () => { toggle(false) @@ -54,6 +54,7 @@ export namespace RadioButtonGroup { export type Props = bkRadioButtonGroup.Props & { tooltip: p.Property + tooltip_delay: p.Property } } @@ -72,8 +73,9 @@ export class RadioButtonGroup extends bkRadioButtonGroup { static { this.prototype.default_view = RadioButtonGroupView - this.define(({Nullable, Ref}) => ({ + this.define(({Nullable, Ref, Number}) => ({ tooltip: [ Nullable(Ref(Tooltip)), null ], + tooltip_delay: [ Number, 500], })) } } diff --git a/panel/models/widgets.py b/panel/models/widgets.py index d700a9d429..ae58077dec 100644 --- a/panel/models/widgets.py +++ b/panel/models/widgets.py @@ -223,6 +223,11 @@ class Button(bkButton): description of a widget's or component's function. """) + tooltip_delay = Int(500, help=""" + Delay (in milliseconds) to display the tooltip after the cursor has + hovered over the Button, default is 500ms. + """) + class CheckboxButtonGroup(bkCheckboxButtonGroup): @@ -231,6 +236,11 @@ class CheckboxButtonGroup(bkCheckboxButtonGroup): description of a widget's or component's function. """) + tooltip_delay = Int(500, help=""" + Delay (in milliseconds) to display the tooltip after the cursor has + hovered over the Button, default is 500ms. + """) + class RadioButtonGroup(bkRadioButtonGroup): @@ -238,3 +248,8 @@ class RadioButtonGroup(bkRadioButtonGroup): A tooltip with plain text or rich HTML contents, providing general help or description of a widget's or component's function. """) + + tooltip_delay = Int(500, help=""" + Delay (in milliseconds) to display the tooltip after the cursor has + hovered over the Button, default is 500ms. + """) diff --git a/panel/widgets/_mixin.py b/panel/widgets/_mixin.py index f47607639d..8eb8747aa2 100644 --- a/panel/widgets/_mixin.py +++ b/panel/widgets/_mixin.py @@ -17,7 +17,13 @@ class TooltipMixin(Widget): description = param.ClassSelector(default=None, class_=(str, BkTooltip, TooltipIcon), doc=""" The description in the tooltip.""") - _rename: ClassVar[Mapping[str, str | None]] = {'description': 'tooltip'} + description_delay = param.Integer(default=500, doc=""" + Delay (in milliseconds) to display the tooltip after the cursor has + hovered over the Button, default is 500ms.""") + + _rename: ClassVar[Mapping[str, str | None]] = { + 'description': 'tooltip', 'description_delay': 'tooltip_delay' + } def _process_param_change(self, params) -> dict[str, Any]: desc = params.get('description') From e8e58e635a7724a7d4c80e9f3ff69d2296420c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 13 Nov 2023 10:44:43 +0100 Subject: [PATCH 2/5] Update tests --- panel/tests/ui/widgets/test_button.py | 31 +++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/panel/tests/ui/widgets/test_button.py b/panel/tests/ui/widgets/test_button.py index ccb5ef8b56..0629b541ca 100644 --- a/panel/tests/ui/widgets/test_button.py +++ b/panel/tests/ui/widgets/test_button.py @@ -3,7 +3,7 @@ pytest.importorskip("playwright") from bokeh.models import Tooltip -from playwright.sync_api import expect +from playwright.sync_api import Expect, expect from panel.tests.util import serve_component, wait_until from panel.widgets import ( @@ -29,7 +29,7 @@ def cb(event): @pytest.mark.parametrize( - "tooltip", + "description", ["Test", Tooltip(content="Test", position="right"), TooltipIcon(value="Test")], ids=["str", "Tooltip", "TooltipIcon"], ) @@ -42,8 +42,8 @@ def cb(event): ], ids=["Button", "CheckButtonGroup", "RadioButtonGroup"], ) -def test_button_tooltip(page, button_fn, button_locator, tooltip): - pn_button = button_fn(name="test", description="Test") +def test_button_tooltip(page, button_fn, button_locator, description): + pn_button = button_fn(name="test", description=description, description_delay=0) serve_component(page, pn_button) @@ -62,3 +62,26 @@ def test_button_tooltip(page, button_fn, button_locator, tooltip): page.hover("body") tooltip = page.locator(".bk-tooltip-content") expect(tooltip).to_have_count(0) + + +def test_button_tooltip_with_delay(page): + pn_button = Button(name="test", description="Test", description_delay=300) + + exp = Expect() + exp.set_options(timeout=200) + + serve_component(page, pn_button) + + button = page.locator(".bk-btn") + expect(button).to_have_count(1) + tooltip = page.locator(".bk-tooltip-content") + expect(tooltip).to_have_count(0) + + # Hovering over the button should not show the tooltip + page.hover(".bk-btn") + tooltip = page.locator(".bk-tooltip-content") + exp(tooltip).to_have_count(0) + + # After 100 ms the tooltip should be visible + page.wait_for_timeout(200) + exp(tooltip).to_have_count(1) From 03aa2b05792fee994bd67a3d6987be6a1efd174a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 13 Nov 2023 10:48:44 +0100 Subject: [PATCH 3/5] Add clear timeout --- panel/models/button.ts | 4 +++- panel/models/checkbox_button_group.ts | 4 +++- panel/models/radio_button_group.ts | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/panel/models/button.ts b/panel/models/button.ts index 9569b91574..b834f471e8 100644 --- a/panel/models/button.ts +++ b/panel/models/button.ts @@ -36,10 +36,12 @@ export class ButtonView extends BkButtonView { visible, }) } + let timer: number this.el.addEventListener("mouseenter", () => { - setTimeout(() => toggle(true), this.model.tooltip_delay) + timer = setTimeout(() => toggle(true), this.model.tooltip_delay) }) this.el.addEventListener("mouseleave", () => { + clearTimeout(timer) toggle(false) }) } diff --git a/panel/models/checkbox_button_group.ts b/panel/models/checkbox_button_group.ts index 3eec0356d9..b2033576a3 100644 --- a/panel/models/checkbox_button_group.ts +++ b/panel/models/checkbox_button_group.ts @@ -39,10 +39,12 @@ export class CheckboxButtonGroupView extends bkCheckboxButtonGroupView { visible, }) } + let timer: number this.el.addEventListener("mouseenter", () => { - setTimeout(() => toggle(true), this.model.tooltip_delay) + timer = setTimeout(() => toggle(true), this.model.tooltip_delay) }) this.el.addEventListener("mouseleave", () => { + clearTimeout(timer) toggle(false) }) } diff --git a/panel/models/radio_button_group.ts b/panel/models/radio_button_group.ts index ccef24a43e..af5091e92f 100644 --- a/panel/models/radio_button_group.ts +++ b/panel/models/radio_button_group.ts @@ -39,10 +39,12 @@ export class RadioButtonGroupView extends bkRadioButtonGroupView { visible, }) } + let timer: number this.el.addEventListener("mouseenter", () => { - setTimeout(() => toggle(true), this.model.tooltip_delay) + timer = setTimeout(() => toggle(true), this.model.tooltip_delay) }) this.el.addEventListener("mouseleave", () => { + clearTimeout(timer) toggle(false) }) } From dff882db4d323f21d882560be8ff07d266a66b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 13 Nov 2023 10:52:56 +0100 Subject: [PATCH 4/5] Add test for clear timeout --- panel/tests/ui/widgets/test_button.py | 29 +++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/panel/tests/ui/widgets/test_button.py b/panel/tests/ui/widgets/test_button.py index 0629b541ca..4af149aac0 100644 --- a/panel/tests/ui/widgets/test_button.py +++ b/panel/tests/ui/widgets/test_button.py @@ -64,24 +64,45 @@ def test_button_tooltip(page, button_fn, button_locator, description): expect(tooltip).to_have_count(0) -def test_button_tooltip_with_delay(page): - pn_button = Button(name="test", description="Test", description_delay=300) +@pytest.mark.parametrize( + "button_fn,button_locator", + [ + (lambda **kw: Button(**kw), ".bk-btn"), + (lambda **kw: CheckButtonGroup(options=["A", "B"], **kw), ".bk-btn-group"), + (lambda **kw: RadioButtonGroup(options=["A", "B"], **kw), ".bk-btn-group"), + ], + ids=["Button", "CheckButtonGroup", "RadioButtonGroup"], +) +def test_button_tooltip_with_delay(page, button_fn, button_locator): + pn_button = button_fn(name="test", description="Test", description_delay=300) exp = Expect() exp.set_options(timeout=200) serve_component(page, pn_button) - button = page.locator(".bk-btn") + button = page.locator(button_locator) expect(button).to_have_count(1) tooltip = page.locator(".bk-tooltip-content") expect(tooltip).to_have_count(0) # Hovering over the button should not show the tooltip - page.hover(".bk-btn") + page.hover(button_locator) tooltip = page.locator(".bk-tooltip-content") exp(tooltip).to_have_count(0) # After 100 ms the tooltip should be visible page.wait_for_timeout(200) exp(tooltip).to_have_count(1) + + # Removing hover should hide the tooltip + page.hover("body") + tooltip = page.locator(".bk-tooltip-content") + exp(tooltip).to_have_count(0) + + # Hovering over the button for a short time should not show the tooltip + page.hover(button_locator) + page.wait_for_timeout(50) + page.hover("body") + page.wait_for_timeout(300) + exp(tooltip).to_have_count(0) From 1edd15c4224b7d8be2dbe6fdc316d816bf56d3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 13 Nov 2023 11:52:03 +0100 Subject: [PATCH 5/5] Add TooltipMixin._rename to buttons _rename --- panel/widgets/button.py | 2 +- panel/widgets/select.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/panel/widgets/button.py b/panel/widgets/button.py index 145387cf96..2aa7cb76a9 100644 --- a/panel/widgets/button.py +++ b/panel/widgets/button.py @@ -181,7 +181,7 @@ class Button(_ClickButton, TooltipMixin): Toggles from False to True while the event is being processed.""") _rename: ClassVar[Mapping[str, str | None]] = { - 'clicks': None, 'name': 'label', 'value': None, + **TooltipMixin._rename, 'clicks': None, 'name': 'label', 'value': None, } _source_transforms: ClassVar[Mapping[str, str | None]] = { diff --git a/panel/widgets/select.py b/panel/widgets/select.py index d968ead704..40d857f02d 100644 --- a/panel/widgets/select.py +++ b/panel/widgets/select.py @@ -656,6 +656,8 @@ class RadioButtonGroup(_RadioGroupBase, _ButtonBase, TooltipMixin): objects=['horizontal', 'vertical'], doc=""" Button group orientation, either 'horizontal' (default) or 'vertical'.""") + _rename: ClassVar[Mapping[str, str | None]] = {**_RadioGroupBase._rename, **TooltipMixin._rename} + _source_transforms = { 'value': "source.labels[value]", 'button_style': None, 'description': None } @@ -755,6 +757,8 @@ class CheckButtonGroup(_CheckGroupBase, _ButtonBase, TooltipMixin): objects=['horizontal', 'vertical'], doc=""" Button group orientation, either 'horizontal' (default) or 'vertical'.""") + _rename: ClassVar[Mapping[str, str | None]] = {**_CheckGroupBase._rename, **TooltipMixin._rename} + _source_transforms = { 'value': "value.map((index) => source.labels[index])", 'button_style': None, 'description': None