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

Standalone Tooltip Icon #4909

Merged
merged 10 commits into from
Jun 5, 2023
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
102 changes: 102 additions & 0 deletions examples/reference/indicators/TooltipIcon.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"pn.extension()"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``TooltipIcon`` is a tooltip indicator providing a Tooltip. The `value` will be the text inside of the tooltip. \n",
"\n",
"#### Parameters:\n",
"\n",
"For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n",
"\n",
"* **``value``** (`str` or `bokeh.models.Tooltip`): The text inside the tooltip.\n",
"\n",
"___"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"The `TooltipIcon` indicator can be instantiated with either a string:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\n",
"pn.widgets.TooltipIcon(value=\"This is a simple tooltip by using a string\")\n",
"\n"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"or as a `bokeh.models.Tooltip`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from bokeh.models import Tooltip\n",
"\n",
"pn.widgets.TooltipIcon(value=Tooltip(content=\"This is a tooltip using a bokeh.models.Tooltip\", position=\"right\"))\n"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {
"tags": [
"parameters"
]
},
"source": [
"The `TooltipIcon` can be used to add more information to a widgets:\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Row(\n",
" pn.widgets.Button(name=\"Click me!\"), \n",
" pn.widgets.TooltipIcon(value=\"Nothing happens when you click the button!\")\n",
")"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
4 changes: 2 additions & 2 deletions panel/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
from .state import State # noqa
from .trend import TrendIndicator # noqa
from .widgets import ( # noqa
Audio, CustomSelect, FileDownload, Player, Progress, SingleSelect, Video,
VideoStream,
Audio, CustomSelect, FileDownload, Player, Progress, SingleSelect,
TooltipIcon, Video, VideoStream,
)
1 change: 1 addition & 0 deletions panel/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {State} from "./state"
export {Tabs} from "./tabs"
export {Terminal} from "./terminal"
export {TextToSpeech} from "./text_to_speech"
export {TooltipIcon} from "./tooltip_icon"
export {TrendIndicator} from "./trend"
export {VegaPlot} from "./vega"
export {Video} from "./video"
Expand Down
123 changes: 123 additions & 0 deletions panel/models/tooltip_icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Control, ControlView } from '@bokehjs/models/widgets/control'
import { Tooltip, TooltipView } from '@bokehjs/models/ui/tooltip'

import { build_view, IterViews } from '@bokehjs/core/build_views'
import { div, label, StyleSheetLike } from '@bokehjs/core/dom'
import * as p from '@bokehjs/core/properties'

import inputs_css, * as inputs from '@bokehjs/styles/widgets/inputs.css'
import icons_css from '@bokehjs/styles/icons.css'

export class TooltipIconView extends ControlView {
declare model: TooltipIcon

protected description: TooltipView

protected desc_el: HTMLElement

public *controls() {}

override *children(): IterViews {
yield* super.children()
yield this.description
}

override async lazy_initialize(): Promise<void> {
await super.lazy_initialize()

const { description } = this.model
this.description = await build_view(description, { parent: this })
}

override remove(): void {
this.description?.remove()
super.remove()
}

override stylesheets(): StyleSheetLike[] {
return [...super.stylesheets(), inputs_css, icons_css]
}

override render(): void {
super.render()

const icon_el = div({ class: inputs.icon })
this.desc_el = div({ class: inputs.description }, icon_el)

const { desc_el, description } = this
description.model.target = desc_el

let persistent = false

const toggle = (visible: boolean) => {
description.model.setv({
visible,
closable: persistent,
})
icon_el.classList.toggle(inputs.opaque, visible && persistent)
}

this.on_change(description.model.properties.visible, () => {
const { visible } = description.model
if (!visible) {
persistent = false
}
toggle(visible)
})
desc_el.addEventListener('mouseenter', () => {
toggle(true)
})
desc_el.addEventListener('mouseleave', () => {
if (!persistent) toggle(false)
})
document.addEventListener('mousedown', (event) => {
const path = event.composedPath()
if (path.includes(description.el)) {
return
} else if (path.includes(desc_el)) {
persistent = !persistent
toggle(persistent)
} else {
persistent = false
toggle(false)
}
})
window.addEventListener('blur', () => {
persistent = false
toggle(false)
})

// Label to get highlight when icon is hovered
this.shadow_el.appendChild(label(this.desc_el))
}

change_input(): void {}
}

export namespace TooltipIcon {
export type Attrs = p.AttrsOf<Props>

export type Props = Control.Props & {
description: p.Property<Tooltip>
}
}

export interface TooltipIcon extends TooltipIcon.Attrs {}

export class TooltipIcon extends Control {
declare properties: TooltipIcon.Props
declare __view_type__: TooltipIconView
static __module__ = 'panel.models.widgets'

constructor(attrs?: Partial<TooltipIcon.Attrs>) {
super(attrs)
}

static {
this.prototype.default_view = TooltipIconView

this.define<TooltipIcon.Props>(({ Ref }) => ({
description: [Ref(Tooltip), new Tooltip()],
}))
}
}
9 changes: 9 additions & 0 deletions panel/models/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Any, Bool, Either, Enum, Float, Instance, Int, List, Nullable, Override,
String, Tuple,
)
from bokeh.models.ui import Tooltip
from bokeh.models.ui.icons import Icon
from bokeh.models.widgets import InputWidget, Select, Widget

Expand Down Expand Up @@ -186,3 +187,11 @@ class CustomSelect(Select):
""")

size = Int(default=1)


class TooltipIcon(Widget):
description = Instance(
Tooltip,
default=Tooltip(content="Help text", position="right"),
help="""The tooltip held by the icon"""
)
6 changes: 3 additions & 3 deletions panel/tests/io/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def test_resolve_custom_path_abs_input_relative_to():
assert str(resolve_custom_path(Button, (PANEL_DIR / 'widgets' / 'button.py'), relative=True)) == 'button.py'

def test_resources_cdn():
resources = Resources(mode='cdn')
resources = Resources(mode='cdn', minified=True)
assert resources.js_raw == ['Bokeh.set_log_level("info");']
assert resources.js_files == [
f'https://cdn.bokeh.org/bokeh/{bk_prefix}/bokeh-{BOKEH_VERSION}.min.js',
Expand All @@ -49,7 +49,7 @@ def test_resources_cdn():
]

def test_resources_server_absolute():
resources = Resources(mode='server', absolute=True)
resources = Resources(mode='server', absolute=True, minified=True)
assert resources.js_raw == ['Bokeh.set_log_level("info");']
assert resources.js_files == [
'http://localhost:5006/static/js/bokeh.min.js',
Expand All @@ -60,7 +60,7 @@ def test_resources_server_absolute():
]

def test_resources_server():
resources = Resources(mode='server')
resources = Resources(mode='server', minified=True)
assert resources.js_raw == ['Bokeh.set_log_level("info");']
assert resources.js_files == [
'static/js/bokeh.min.js',
Expand Down
59 changes: 59 additions & 0 deletions panel/tests/ui/widgets/test_indicators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import time

import pytest

pytestmark = pytest.mark.ui

from bokeh.models import Tooltip

from panel.io.server import serve
from panel.widgets import TooltipIcon

try:
from playwright.sync_api import expect
except ImportError:
pytestmark = pytest.mark.skip("playwright not available")


@pytest.mark.parametrize(
"value", ["Test", Tooltip(content="Test", position="right")], ids=["str", "Tooltip"]
)
def test_plaintext_tooltip(page, port, value):
tooltip_icon = TooltipIcon(value="Test")

serve(tooltip_icon, port=port, threaded=True, show=False)

time.sleep(0.2)

page.goto(f"http://localhost:{port}")

icon = page.locator(".bk-icon")
expect(icon).to_have_count(1)
tooltip = page.locator(".bk-Tooltip")
expect(tooltip).to_have_count(0)

# Hovering over the icon should show the tooltip
page.hover(".bk-icon")
tooltip = page.locator(".bk-Tooltip")
expect(tooltip).to_have_count(1)
assert tooltip.locator("div").first.text_content().strip() == "Test"

# Removing hover should hide the tooltip
page.hover("body")
tooltip = page.locator(".bk-Tooltip")
expect(tooltip).to_have_count(0)

# Clicking the icon should show the tooltip
page.click(".bk-icon")
tooltip = page.locator(".bk-Tooltip")
expect(tooltip).to_have_count(1)

# Removing the hover should keep the tooltip
page.hover("body")
tooltip = page.locator(".bk-Tooltip")
expect(tooltip).to_have_count(1)

# Clicking should remove the tooltip
page.click("body")
tooltip = page.locator(".bk-Tooltip")
expect(tooltip).to_have_count(0)
4 changes: 3 additions & 1 deletion panel/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from .file_selector import FileSelector # noqa
from .indicators import ( # noqa
BooleanStatus, Dial, Gauge, LinearGauge, LoadingSpinner, Number, Progress,
Tqdm, Trend,
TooltipIcon, Tqdm, Trend,
)
from .input import ( # noqa
ArrayInput, Checkbox, ColorPicker, DatePicker, DatetimeInput,
Expand Down Expand Up @@ -125,6 +125,7 @@
"SpeechToText",
"Spinner",
"StaticText",
"Switch",
"Tabulator",
"Terminal",
"TextAreaInput",
Expand All @@ -133,6 +134,7 @@
"TextToSpeech",
"Toggle",
"ToggleGroup",
"TooltipIcon",
"Tqdm",
"Trend",
"Utterance",
Expand Down
Loading