Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `shiny run` now takes a `--reload-dir <DIR>` argument that indicates a directory `--reload` should (recursively) monitor for changes, in addition to the app's parent directory. Can be used more than once. (#353)
* The default theme has been updated to use Bootstrap 5 with custom Shiny style enhancements. (#624)
* Added experimental UI `tooltip()`, `update_tooltip()`, and `toggle_tooltip()` methods for easy creation (and server-side updating) of [Bootstrap tooltips](https://getbootstrap.com/docs/5.2/components/tooltips/). (A way to display additional information when focusing (or hovering over) a UI element). (#629)


### Bug fixes
Expand Down
32 changes: 32 additions & 0 deletions shiny/experimental/api-examples/tooltip/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

import shiny.experimental as x
from shiny import App, ui

# https://icons.getbootstrap.com/icons/question-circle-fill/
question_circle_fill = ui.HTML(
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-circle-fill mb-1" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.496 6.033h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286a.237.237 0 0 0 .241.247zm2.325 6.443c.61 0 1.029-.394 1.029-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94 0 .533.425.927 1.01.927z"/></svg>'
)

app_ui = ui.page_fluid(
x.ui.tooltip(
ui.input_action_button("btn", "A button", class_="mt-3"),
"A message",
id="btn_tooltip",
),
ui.hr(),
x.ui.card(
x.ui.card_header(
x.ui.tooltip(
ui.span("Card title ", question_circle_fill),
"Additional info",
placement="right",
id="card_tooltip",
),
),
"Card body content...",
),
)


app = App(app_ui, server=None)
46 changes: 46 additions & 0 deletions shiny/experimental/api-examples/tooltip_toggle/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import annotations

import shiny.experimental as x
from shiny import App, Inputs, Outputs, Session, reactive, req, ui

app_ui = ui.page_fluid(
ui.input_action_button("btn_show", "Show tooltip", class_="mt-3 me-3"),
ui.input_action_button("btn_close", "Close tooltip", class_="mt-3 me-3"),
ui.br(),
ui.input_action_button("btn_toggle", "Toggle tooltip", class_="mt-3 me-3"),
ui.br(),
ui.br(),
x.ui.tooltip(
ui.input_action_button("btn_w_tooltip", "A button w/ a tooltip", class_="mt-3"),
"A message",
id="tooltip_id",
),
)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.Effect
def _():
req(input.btn_show())

x.ui.tooltip_toggle("tooltip_id", show=True)

@reactive.Effect
def _():
req(input.btn_close())

x.ui.tooltip_toggle("tooltip_id", show=False)

@reactive.Effect
def _():
req(input.btn_toggle())

x.ui.tooltip_toggle("tooltip_id")

@reactive.Effect
def _():
req(input.btn_w_tooltip())
ui.notification_show("Button clicked!", duration=3, type="message")


app = App(app_ui, server=server)
41 changes: 41 additions & 0 deletions shiny/experimental/api-examples/update_tooltip/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

import shiny.experimental as x
from shiny import App, Inputs, Outputs, Session, reactive, req, ui

app_ui = ui.page_fluid(
ui.input_action_button("btn_update", "Update tooltip phrase", class_="mt-3 me-3"),
ui.br(),
ui.br(),
x.ui.tooltip(
ui.input_action_button("btn_w_tooltip", "A button w/ a tooltip", class_="mt-3"),
"A message",
id="tooltip_id",
),
)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.Effect
def _():
# Immediately display tooltip
x.ui.tooltip_toggle("tooltip_id", show=True)

@reactive.Effect
def _():
req(input.btn_update())

content = (
"A " + " ".join(["NEW" for _ in range(input.btn_update())]) + " message"
)

x.ui.update_tooltip("tooltip_id", content)
x.ui.tooltip_toggle("tooltip_id", show=True)

@reactive.Effect
def _():
req(input.btn_w_tooltip())
ui.notification_show("Button clicked!", duration=3, type="message")


app = App(app_ui, server=server)
5 changes: 5 additions & 0 deletions shiny/experimental/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
sidebar,
sidebar_toggle,
)
from ._tooltip import tooltip, tooltip_toggle, update_tooltip
from ._valuebox import showcase_left_center, showcase_top_right, value_box

__all__ = (
Expand Down Expand Up @@ -84,6 +85,10 @@
"card_footer",
# Layout
"layout_column_wrap",
# Tooltip
"tooltip",
"tooltip_toggle",
"update_tooltip",
# ValueBox
"value_box",
"showcase_left_center",
Expand Down
1 change: 1 addition & 0 deletions shiny/experimental/ui/_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def card(
min_height=as_css_unit(min_height),
),
"data-bslib-card-init": True,
"data-full-screen": "false" if full_screen else None,
},
*children,
attrs,
Expand Down
20 changes: 19 additions & 1 deletion shiny/experimental/ui/_htmldeps.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from __future__ import annotations

from pathlib import PurePath
from typing import Literal

from htmltools import HTMLDependency

from ... import __version__ as shiny_version
from ..._typing_extensions import NotRequired, TypedDict
from ..._versions import bslib as bslib_version
from ..._versions import htmltools as htmltools_version

Expand Down Expand Up @@ -33,20 +35,32 @@ def _htmltools_dep(
)


class _ScriptItemDict(TypedDict):
src: str
type: NotRequired[Literal["module"]]


def _bslib_component_dep(
name: str,
script: bool = False,
stylesheet: bool = False,
all_files: bool = True,
script_is_module: bool = False,
) -> HTMLDependency:
script_val: _ScriptItemDict | None = None
if script:
script_val = {"src": f"{name}.min.js"}
if script_is_module:
script_val["type"] = "module"

return HTMLDependency(
name=f"bslib-{name}",
version=bslib_version,
source={
"package": "shiny",
"subdir": str(_x_components_path / name),
},
script={"src": f"{name}.min.js"} if script else None,
script=script_val, # type: ignore # https://github.com/rstudio/py-htmltools/issues/59
stylesheet={"href": f"{name}.css"} if stylesheet else None,
all_files=all_files,
)
Expand Down Expand Up @@ -100,6 +114,10 @@ def value_box_dependency() -> HTMLDependency:
return _bslib_component_dep("value_box", stylesheet=True)


def web_component_dependency() -> HTMLDependency:
return _bslib_component_dep("webComponents", script=True, script_is_module=True)


# -- Experimental ------------------


Expand Down
141 changes: 141 additions & 0 deletions shiny/experimental/ui/_tooltip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from __future__ import annotations

import json
from typing import Literal, Optional

from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, TagList, div

from ... import Session
from ..._utils import drop_none
from ...session import require_active_session

# from ._color import get_color_contrast
from ._utils import consolidate_attrs
from ._web_component import web_component


def tooltip(
trigger: TagChild,
*args: TagChild | TagAttrs,
id: Optional[str] = None,
placement: Literal["auto", "top", "right", "bottom", "left"] = "auto",
options: Optional[dict[str, object]] = None,
**kwargs: TagAttrValue,
) -> Tag:
"""
Add a tooltip to a UI element

Display additional information when focusing (or hovering over) a UI element.

Parameters
----------
trigger
A UI element (i.e., :class:`~htmltools.Tag`) to serve as the tooltips trigger.
It's good practice for this element to be a keyboard-focusable and interactive
element (e.g., :func:`~shiny.ui.input_action_button`,
:func:`~shiny.ui.input_action_link`, etc.) so that the tooltip is accessible to
keyboard and assistive technology users.
*args
Contents to the tooltip's body. Or tag attributes that are supplied to the
resolved :class:`~htmltools.Tag` object.
id
A character string. Required to re-actively respond to the visibility of the
tooltip (via the `input[id]` value) and/or update the visibility/contents of the
tooltip.
placement
The placement of the tooltip relative to its trigger.
options
A list of additional [Bootstrap
options](https://getbootstrap.com/docs/5.2/components/tooltips/#options).

Details
-------

If `trigger` yields multiple HTML elements (e.g., a :class:`~htmltools.TagList` or
complex [`shinywidgets`](https://github.com/rstudio/py-shinywidgets) object), the
last HTML element is used as the trigger. If the `trigger` should contain all of
those elements, wrap the object in a :func:`~htmltools.div` or :func:`~htmltools.span`.

See Also
--------

* [Bootstrap tooltips documentation](https://getbootstrap.com/docs/5.2/components/tooltips/)
"""
attrs, children = consolidate_attrs(*args, **kwargs)

if len(children) == 0:
raise RuntimeError("At least one value must be provided to `*args: TagChild`")

res = web_component(
"bslib-tooltip",
{
"id": id,
"placement": placement,
"options": json.dumps(options) if options else None,
},
attrs,
# Use display:none instead of <template> since shiny.js
# doesn't bind to the contents of the latter
div(*children, {"style": "display:none;"}),
trigger,
)

return res


def _session_on_flush_send_msg(
id: str, session: Session | None, msg: dict[str, object]
) -> None:
session = require_active_session(session)
session.on_flush(lambda: session.send_input_message(id, msg), once=True)


def tooltip_toggle(
id: str, show: Optional[bool] = None, session: Optional[Session] = None
) -> None:
"""
Programmatically show/hide a tooltip

Parameters
----------
id
A character string that matches an existing tooltip id.
show
Whether to show (`True`) or hide (`False`) the tooltip. The default (`None`)
will show if currently hidden and hide if currently shown. Note that a tooltip
will not be shown if the trigger is not visible (e.g., it's hidden behind a
tab).
session
A Shiny session object (the default should almost always be used).
"""
_session_on_flush_send_msg(
id,
session,
{
"method": "toggle",
"value": _normalize_show_value(show),
},
)


# @describeIn tooltip Update the contents of a tooltip.
# @export
def update_tooltip(id: str, *args: TagChild, session: Optional[Session] = None) -> None:
_session_on_flush_send_msg(
id,
session,
drop_none(
{
"method": "update",
"title": require_active_session(session)._process_ui(TagList(*args))
if len(args) > 0
else None,
}
),
)


def _normalize_show_value(show: bool | None) -> Literal["toggle", "show", "hide"]:
if show is None:
return "toggle"
return "show" if show else "hide"
19 changes: 19 additions & 0 deletions shiny/experimental/ui/_web_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

from htmltools import Tag, TagAttrs, TagAttrValue, TagChild

from ._htmldeps import web_component_dependency


def web_component(
tag_name: str,
*args: TagChild | TagAttrs,
**kwargs: TagAttrValue,
) -> Tag:
return Tag(
tag_name,
web_component_dependency(),
*args,
_add_ws=False,
**kwargs,
)
2 changes: 1 addition & 1 deletion shiny/experimental/www/bslib/_version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"note!": "This file is auto-generated by scripts/htmlDependencies.R",
"package": "bslib",
"version": "Github (rstudio/bslib@175ad4624b9a1ede4130ebb92042f8da12eb7d70)"
"version": "Github (rstudio/bslib@1e604150048d185633b8f8883b4acf26d070f1ff)"
}
2 changes: 1 addition & 1 deletion shiny/experimental/www/htmltools/_version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"note!": "This file is auto-generated by scripts/htmlDependencies.R",
"package": "htmltools",
"version": "Github (rstudio/htmltools@6f082f843cd5d897ecae91f2154c5b9523637659)"
"version": "Github (rstudio/htmltools@758552e58113b844e0767daa5b2071513fc9bb56)"
}
Loading