Skip to content

Commit

Permalink
Add delay for tooltip to show up for buttons (#5860)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro authored Nov 14, 2023
1 parent 7ab311b commit 34df686
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 12 deletions.
8 changes: 6 additions & 2 deletions panel/models/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ export class ButtonView extends BkButtonView {
visible,
})
}
let timer: number
this.el.addEventListener("mouseenter", () => {
toggle(true)
timer = setTimeout(() => toggle(true), this.model.tooltip_delay)
})
this.el.addEventListener("mouseleave", () => {
clearTimeout(timer)
toggle(false)
})
}
Expand All @@ -51,6 +53,7 @@ export namespace Button {

export type Props = BkButton.Props & {
tooltip: p.Property<Tooltip | null>
tooltip_delay: p.Property<number>
}
}

Expand All @@ -69,8 +72,9 @@ export class Button extends BkButton {
static {
this.prototype.default_view = ButtonView

this.define<Button.Props>(({Nullable, Ref}) => ({
this.define<Button.Props>(({Nullable, Ref, Number}) => ({
tooltip: [ Nullable(Ref(Tooltip)), null ],
tooltip_delay: [ Number, 500],
}))
}
}
8 changes: 6 additions & 2 deletions panel/models/checkbox_button_group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ export class CheckboxButtonGroupView extends bkCheckboxButtonGroupView {
visible,
})
}
let timer: number
this.el.addEventListener("mouseenter", () => {
toggle(true)
timer = setTimeout(() => toggle(true), this.model.tooltip_delay)
})
this.el.addEventListener("mouseleave", () => {
clearTimeout(timer)
toggle(false)
})
}
Expand All @@ -54,6 +56,7 @@ export namespace CheckboxButtonGroup {

export type Props = bkCheckboxButtonGroup.Props & {
tooltip: p.Property<Tooltip | null>
tooltip_delay: p.Property<number>
}
}

Expand All @@ -72,8 +75,9 @@ export class CheckboxButtonGroup extends bkCheckboxButtonGroup {
static {
this.prototype.default_view = CheckboxButtonGroupView

this.define<CheckboxButtonGroup.Props>(({Nullable, Ref}) => ({
this.define<CheckboxButtonGroup.Props>(({Nullable, Ref, Number}) => ({
tooltip: [ Nullable(Ref(Tooltip)), null ],
tooltip_delay: [ Number, 500],
}))
}
}
8 changes: 6 additions & 2 deletions panel/models/radio_button_group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ export class RadioButtonGroupView extends bkRadioButtonGroupView {
visible,
})
}
let timer: number
this.el.addEventListener("mouseenter", () => {
toggle(true)
timer = setTimeout(() => toggle(true), this.model.tooltip_delay)
})
this.el.addEventListener("mouseleave", () => {
clearTimeout(timer)
toggle(false)
})
}
Expand All @@ -54,6 +56,7 @@ export namespace RadioButtonGroup {

export type Props = bkRadioButtonGroup.Props & {
tooltip: p.Property<Tooltip | null>
tooltip_delay: p.Property<number>
}
}

Expand All @@ -72,8 +75,9 @@ export class RadioButtonGroup extends bkRadioButtonGroup {
static {
this.prototype.default_view = RadioButtonGroupView

this.define<RadioButtonGroup.Props>(({Nullable, Ref}) => ({
this.define<RadioButtonGroup.Props>(({Nullable, Ref, Number}) => ({
tooltip: [ Nullable(Ref(Tooltip)), null ],
tooltip_delay: [ Number, 500],
}))
}
}
15 changes: 15 additions & 0 deletions panel/models/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand All @@ -231,10 +236,20 @@ 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):

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.
""")

tooltip_delay = Int(500, help="""
Delay (in milliseconds) to display the tooltip after the cursor has
hovered over the Button, default is 500ms.
""")
52 changes: 48 additions & 4 deletions panel/tests/ui/widgets/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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"],
)
Expand All @@ -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)

Expand All @@ -62,3 +62,47 @@ 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)


@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(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(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)
8 changes: 7 additions & 1 deletion panel/widgets/_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion panel/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = {
Expand Down
4 changes: 4 additions & 0 deletions panel/widgets/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 34df686

Please sign in to comment.