From b6451fdbc97531aa4717540873ab4d41921d843a Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Dec 2023 13:24:42 -0600 Subject: [PATCH] Change `express.layout` to `express.ui` (#904) --- docs/_quartodoc.yml | 57 +- examples/express/accordion_app.py | 10 +- examples/express/basic_app.py | 4 +- examples/express/column_wrap_app.py | 12 +- examples/express/nav_app.py | 18 +- examples/express/shared_app.py | 4 +- examples/express/sidebar_app.py | 6 +- shiny/express/__init__.py | 4 +- shiny/express/_recall_context.py | 15 +- shiny/express/layout.py | 908 +-------- shiny/express/ui/__init__.py | 305 +++ shiny/express/ui/_cm_components.py | 1691 +++++++++++++++++ shiny/ui/_navs.py | 2 +- .../apps/shiny-express-accordion/app.py | 10 +- .../apps/shiny-express-page-default/app.py | 58 +- .../apps/shiny-express-page-fillable/app.py | 8 +- .../apps/shiny-express-page-fluid/app.py | 8 +- .../apps/shiny-express-page-sidebar/app.py | 10 +- .../shiny/shiny-express/accordion/app.py | 10 +- .../shiny/shiny-express/page_default/app.py | 58 +- .../shiny/shiny-express/page_fillable/app.py | 8 +- .../shiny/shiny-express/page_fluid/app.py | 8 +- .../shiny/shiny-express/page_sidebar/app.py | 10 +- tests/playwright/utils/express_utils.py | 4 +- tests/pytest/test_express_ui.py | 32 + 25 files changed, 2204 insertions(+), 1056 deletions(-) create mode 100644 shiny/express/ui/__init__.py create mode 100644 shiny/express/ui/_cm_components.py diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 96cf24eea..86b0c6738 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -283,26 +283,43 @@ quartodoc: - title: Shiny Express desc: Functions for Shiny Express applications contents: - - express.layout.set_page - - express.layout.p - - express.layout.div - - express.layout.span - - express.layout.pre - - express.layout.sidebar - - express.layout.layout_columns - - express.layout.layout_column_wrap - - express.layout.column - - express.layout.row - - express.layout.card - - express.layout.accordion - - express.layout.accordion_panel - - express.layout.navset - - express.layout.navset_card - - express.layout.nav_panel - - express.layout.page_fluid - - express.layout.page_fixed - - express.layout.page_fillable - - express.layout.page_sidebar + - kind: page + path: ContextManagerComponents + summary: + name: "Context manager components" + desc: "" + flatten: true + contents: + - express.ui.set_page + - express.ui.sidebar + - express.ui.layout_sidebar + - express.ui.layout_column_wrap + - express.ui.layout_columns + - express.ui.card + - express.ui.accordion + - express.ui.accordion_panel + - express.ui.nav_panel + - express.ui.nav_control + - express.ui.nav_menu + - express.ui.navset_bar + - express.ui.navset_card_pill + - express.ui.navset_card_tab + - express.ui.navset_card_underline + - express.ui.navset_hidden + - express.ui.navset_pill + - express.ui.navset_pill_list + - express.ui.navset_tab + - express.ui.navset_underline + - express.ui.value_box + - express.ui.panel_well + - express.ui.panel_conditional + - express.ui.panel_fixed + - express.ui.panel_absolute + - express.ui.page_fluid + - express.ui.page_fixed + - express.ui.page_fillable + - express.ui.page_sidebar + - express.ui.page_navbar - title: Deprecated desc: "" contents: diff --git a/examples/express/accordion_app.py b/examples/express/accordion_app.py index 7dc6d691f..fdd722c34 100644 --- a/examples/express/accordion_app.py +++ b/examples/express/accordion_app.py @@ -1,14 +1,14 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.accordion(open=["Panel 1", "Panel 2"]): - with layout.accordion_panel("Panel 1"): +with ui.accordion(open=["Panel 1", "Panel 2"]): + with ui.accordion_panel("Panel 1"): ui.input_slider("n", "N", 1, 100, 50) - with layout.accordion_panel("Panel 2"): + with ui.accordion_panel("Panel 2"): @render.text def txt(): diff --git a/examples/express/basic_app.py b/examples/express/basic_app.py index 9c6827839..c81e218aa 100644 --- a/examples/express/basic_app.py +++ b/examples/express/basic_app.py @@ -1,5 +1,5 @@ -from shiny import render, ui -from shiny.express import input +from shiny import render +from shiny.express import input, ui ui.input_slider("n", "N", 1, 100, 50) diff --git a/examples/express/column_wrap_app.py b/examples/express/column_wrap_app.py index 75bf3fd47..5e5c44f06 100644 --- a/examples/express/column_wrap_app.py +++ b/examples/express/column_wrap_app.py @@ -1,14 +1,14 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.layout_column_wrap(width=1 / 2): - with layout.card(): +with ui.layout_column_wrap(width=1 / 2): + with ui.card(): ui.input_slider("n", "N", 1, 100, 50) - with layout.card(): + with ui.card(): @render.plot def histogram(): @@ -16,7 +16,7 @@ def histogram(): x = 100 + 15 * np.random.randn(437) plt.hist(x, input.n(), density=True) - with layout.card(): + with ui.card(): @render.plot def histogram2(): diff --git a/examples/express/nav_app.py b/examples/express/nav_app.py index d4f421ce8..0a72627ff 100644 --- a/examples/express/nav_app.py +++ b/examples/express/nav_app.py @@ -1,15 +1,15 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.layout_column_wrap(width=1 / 2): - with layout.navset(): - with layout.nav_panel(title="One"): +with ui.layout_column_wrap(width=1 / 2): + with ui.navset_underline(): + with ui.nav_panel(title="One"): ui.input_slider("n", "N", 1, 100, 50) - with layout.nav_panel(title="Two"): + with ui.nav_panel(title="Two"): @render.plot def histogram(): @@ -17,11 +17,11 @@ def histogram(): x = 100 + 15 * np.random.randn(437) plt.hist(x, input.n(), density=True) - with layout.navset_card(): - with layout.nav_panel(title="One"): + with ui.navset_card_underline(): + with ui.nav_panel(title="One"): ui.input_slider("n2", "N", 1, 100, 50) - with layout.nav_panel(title="Two"): + with ui.nav_panel(title="Two"): @render.plot def histogram2(): diff --git a/examples/express/shared_app.py b/examples/express/shared_app.py index b16e5005e..d5f874341 100644 --- a/examples/express/shared_app.py +++ b/examples/express/shared_app.py @@ -6,8 +6,8 @@ import numpy as np import shared -from shiny import reactive, render, ui -from shiny.express import input +from shiny import reactive, render +from shiny.express import input, ui @render.plot diff --git a/examples/express/sidebar_app.py b/examples/express/sidebar_app.py index c229051d6..ac1e97844 100644 --- a/examples/express/sidebar_app.py +++ b/examples/express/sidebar_app.py @@ -1,10 +1,10 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.sidebar(): +with ui.sidebar(): ui.input_slider("n", "N", 1, 100, 50) diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index 4cc54de50..9b318c8c6 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -2,7 +2,7 @@ from ..session import Inputs, Outputs, Session from ..session import _utils as _session_utils -from . import app, layout +from . import app, ui from ._is_express import is_express_app from ._output import output_args, suspend_display from ._run import wrap_express_app @@ -17,7 +17,7 @@ "suspend_display", "wrap_express_app", "app", - "layout", + "ui", "display_body", ) diff --git a/shiny/express/_recall_context.py b/shiny/express/_recall_context.py index 0dc55d52a..dc636d6a0 100644 --- a/shiny/express/_recall_context.py +++ b/shiny/express/_recall_context.py @@ -5,7 +5,7 @@ from types import TracebackType from typing import Callable, Generic, Mapping, Optional, Type, TypeVar -from htmltools import Tag, wrap_displayhook_handler +from htmltools import MetadataNode, Tag, TagList, wrap_displayhook_handler from .._typing_extensions import ParamSpec @@ -54,6 +54,19 @@ def __exit__( sys.displayhook(res) return False + def tagify(self) -> Tag | TagList | MetadataNode | str: + res = self.fn(*self.args, **self.kwargs) + + if callable(getattr(res, "tagify", None)): + return res.tagify() # pyright: ignore + if callable(getattr(res, "_repr_html_", None)): + return res._repr_html_() # pyright: ignore + + raise RuntimeError( + "RecallContextManager was used without `with`. When used this way, the " + "result must have a .tagify() or ._repr_html_() method, but it does not." + ) + def wrap_recall_context_manager( fn: Callable[P, R] diff --git a/shiny/express/layout.py b/shiny/express/layout.py index e65a8dc87..a6b72473e 100644 --- a/shiny/express/layout.py +++ b/shiny/express/layout.py @@ -1,904 +1,14 @@ -from __future__ import annotations +import warnings -from typing import Literal, Optional +from . import ui -import htmltools -from htmltools import Tag, TagAttrValue, TagChild, TagList - -from .. import ui -from ..types import MISSING, MISSING_TYPE -from ..ui._accordion import AccordionPanel -from ..ui._layout_columns import BreakpointsUser -from ..ui._navs import NavPanel, NavSet, NavSetCard -from ..ui.css import CssUnit -from . import _run -from ._recall_context import RecallContextManager, wrap_recall_context_manager - -__all__ = ( - "set_page", - "p", - "div", - "span", - "pre", - "sidebar", - "layout_column_wrap", - "layout_columns", - "column", - "row", - "card", - "accordion", - "accordion_panel", - "navset", - "navset_card", - "nav_panel", - "page_fluid", - "page_fixed", - "page_fillable", - "page_sidebar", +warnings.warn( + "shiny.express.layout has been deprecated and renamed to shiny.express.ui. " + "Please import shiny.express.ui instead of shiny.express.layout.", + ImportWarning, + stacklevel=2, ) -# ====================================================================================== -# Page functions -# ====================================================================================== -def set_page(page_fn: RecallContextManager[Tag]) -> None: - """Set the page function for the current Shiny express app.""" - _run.replace_top_level_recall_context_manager(page_fn, force=True) - - -# ====================================================================================== -# htmltools Tag functions -# ====================================================================================== -p = wrap_recall_context_manager(htmltools.p) -div = wrap_recall_context_manager(htmltools.div) -span = wrap_recall_context_manager(htmltools.span) -pre = wrap_recall_context_manager(htmltools.pre) - - -# ====================================================================================== -# Shiny layout components -# ====================================================================================== -def sidebar( - *, - width: CssUnit = 250, - position: Literal["left", "right"] = "left", - open: Literal["desktop", "open", "closed", "always"] = "always", - id: Optional[str] = None, - title: TagChild | str = None, - bg: Optional[str] = None, - fg: Optional[str] = None, - class_: Optional[str] = None, # TODO-future; Consider using `**kwargs` instead - max_height_mobile: Optional[str | float] = "auto", - gap: Optional[CssUnit] = None, - padding: Optional[CssUnit | list[CssUnit]] = None, -) -> RecallContextManager[ui.Sidebar]: - """ - Sidebar element - - Create a collapsing sidebar layout. This function wraps :func:`~shiny.ui.sidebar`. - - Parameters - ---------- - width - A valid CSS unit used for the width of the sidebar. - position - Where the sidebar should appear relative to the main content. - open - The initial state of the sidebar. - - * `"desktop"`: the sidebar starts open on desktop screen, closed on mobile - * `"open"` or `True`: the sidebar starts open - * `"closed"` or `False`: the sidebar starts closed - * `"always"` or `None`: the sidebar is always open and cannot be closed - - In :func:`~shiny.ui.update_sidebar`, `open` indicates the desired state of the - sidebar. Note that :func:`~shiny.ui.update_sidebar` can only open or close the - sidebar, so it does not support the `"desktop"` and `"always"` options. - id - A character string. Required if wanting to re-actively read (or update) the - `collapsible` state in a Shiny app. - title - A character title to be used as the sidebar title, which will be wrapped in a - `
` element with class `sidebar-title`. You can also provide a custom - :class:`~htmltools.Tag` for the title element, in which case you'll - likely want to give this element `class = "sidebar-title"`. - bg,fg - A background or foreground color. - class_ - CSS classes for the sidebar container element, in addition to the fixed - `.sidebar` class. - max_height_mobile - A CSS length unit (passed through :func:`~shiny.ui.css.as_css_unit`) defining - the maximum height of the horizontal sidebar when viewed on mobile devices. Only - applies to always-open sidebars that use `open = "always"`, where by default the - sidebar container is placed below the main content container on mobile devices. - gap - A CSS length unit defining the vertical `gap` (i.e., spacing) between elements - provided to `*args`. - padding - Padding within the sidebar itself. This can be a numeric vector (which will be - interpreted as pixels) or a character vector with valid CSS lengths. `padding` - may be one to four values. - - * If a single value, then that value will be used for all four sides. - * If two, then the first value will be used for the top and bottom, while - the second value will be used for left and right. - * If three values, then the first will be used for top, the second will be left - and right, and the third will be bottom. - * If four, then the values will be interpreted as top, right, bottom, and left - respectively. - - Returns - ------- - : - A :class:`~shiny.ui.Sidebar` object. - """ - return RecallContextManager( - ui.sidebar, - default_page=page_sidebar(), - kwargs=dict( - width=width, - position=position, - open=open, - id=id, - title=title, - bg=bg, - fg=fg, - class_=class_, - max_height_mobile=max_height_mobile, - gap=gap, - padding=padding, - ), - ) - - -def layout_column_wrap( - *, - width: CssUnit | None | MISSING_TYPE = MISSING, - fixed_width: bool = False, - heights_equal: Literal["all", "row"] = "all", - fill: bool = True, - fillable: bool = True, - height: Optional[CssUnit] = None, - height_mobile: Optional[CssUnit] = None, - gap: Optional[CssUnit] = None, - class_: Optional[str] = None, - **kwargs: TagAttrValue, -) -> RecallContextManager[Tag]: - """ - A grid-like, column-first layout - - Wraps a 1d sequence of UI elements into a 2d grid. The number of columns (and rows) - in the grid dependent on the column `width` as well as the size of the display. - This function wraps :func:`~shiny.ui.layout_column_wrap`. - - Parameters - ---------- - width - The desired width of each card. It can be one of the following: - - * A (unit-less) number between 0 and 1, specified as `1/num`, where `num` - represents the number of desired columns. - * A CSS length unit representing either the minimum (when `fixed_width=False`) - or fixed width (`fixed_width=True`). - * `None`, which allows power users to set the `grid-template-columns` CSS - property manually, either via a `style` attribute or a CSS stylesheet. - * If missing, a value of `200px` will be used. - fixed_width - When `width` is greater than 1 or is a CSS length unit, e.g. `"200px"`, - `fixed_width` indicates whether that `width` value represents the absolute size - of each column (`fixed_width=TRUE`) or the minimum size of a column - (`fixed_width=FALSE`). - - When `fixed_width=FALSE`, new columns are added to a row when `width` space is - available and columns will never exceed the container or viewport size. - - When `fixed_width=TRUE`, all columns will be exactly `width` wide, which may - result in columns overflowing the parent container. - heights_equal - If `"all"` (the default), every card in every row of the grid will have the same - height. If `"row"`, then every card in _each_ row of the grid will have the same - height, but heights may vary between rows. - fill - Whether or not to allow the layout to grow/shrink to fit a fillable container - with an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). - fillable - Whether or not each element is wrapped in a fillable container. - height - Any valid CSS unit to use for the height. - height_mobile - Any valid CSS unit to use for the height when on mobile devices (or narrow - windows). - gap - Any valid CSS unit to use for the gap between columns. - class_ - A CSS class to apply to the containing element. - **kwargs - Additional attributes to apply to the containing element. - - Returns - ------- - : - A :class:`~htmltools.Tag` element. - - See Also - -------- - * :func:`~shiny.express.layout.layout_columns` for laying out elements into a - responsive 12-column grid. - """ - return RecallContextManager( - ui.layout_column_wrap, - default_page=page_fillable(), - kwargs=dict( - width=width, - fixed_width=fixed_width, - heights_equal=heights_equal, - fill=fill, - fillable=fillable, - height=height, - height_mobile=height_mobile, - gap=gap, - class_=class_, - **kwargs, - ), - ) - - -def layout_columns( - *, - col_widths: BreakpointsUser[int] = None, - row_heights: BreakpointsUser[CssUnit] = None, - fill: bool = True, - fillable: bool = True, - gap: Optional[CssUnit] = None, - class_: Optional[str] = None, - height: Optional[CssUnit] = None, - **kwargs: TagAttrValue, -) -> RecallContextManager[Tag]: - """ - Create responsive, column-based grid layouts, based on a 12-column grid. - - Parameters - ---------- - col_widths - The widths of the columns, possibly at different breakpoints. Can be one of the - following: - - * `None` (the default): Automatically determines a sensible number of columns - based on the number of children given to the layout. - * A list or tuple of integers between 1 and 12, where each element represents - the number of columns for the relevant UI element. Column widths are recycled - to extend the values in `col_widths` to match the actual number of items in - the layout, and children are wrapped onto the next row when a row exceeds 12 - column units. For example, `col_widths=(4, 8, 12)` allocates 4 columns to the - first element, 8 columns to the second element, and 12 columns to the third - element (which wraps to the next row). Negative values are also allowed, and - are treated as empty columns. For example, `col_widths=(-2, 8, -2)` would - allocate 8 columns to an element (with 2 empty columns on either side). - * A dictionary of column widths at different breakpoints. The keys should be - one of `"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"`, or `"xxl"`, and the values are - either of the above. For example, `col_widths={"sm": (3, 3, 6), "lg": (4)}`. - - row_heights - The heights of the rows, possibly at different breakpoints. Can be one of the - following: - - * A numeric vector, where each value represents the - [fractional unit](https://css-tricks.com/introduction-fr-css-unit/) - (`fr`) height of the relevant row. If there are more rows than values - provided, the pattern will be repeated. For example, `row_heights=(1, 2)` - allows even rows to take up twice as much space as odd rows. - * A list of numeric or CSS length units, where each value represents the height - of the relevant row. If more rows are needed than values provided, the pattern - will repeat. For example, `row_heights=["auto", 1]` allows the height of odd - rows to be driven my it's contents and even rows to be - [`1fr`](https://css-tricks.com/introduction-fr-css-unit/). - * A single string containing CSS length units. In this case, the value is - supplied directly to `grid-auto-rows`. - * A dictionary of row heights at different breakpoints, where each key is a - breakpoint name (one of `"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"`, or `"xxl"`) - and where the values may be any of the above options. - - fill - Whether or not to allow the layout to grow/shrink to fit a fillable container - with an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). - - fillable - Whether or not each element is wrapped in a fillable container. - - gap - Any valid CSS unit to use for the gap between columns. - - class_ - CSS class(es) to apply to the containing element. - - height - Any valid CSS unit to use for the height. - - **kwargs - Additional attributes to apply to the containing element. - - Returns - ------- - : - An :class:`~htmltools.Tag` element. - - See Also - -------- - * :func:`~shiny.express.layout.layout_column_wrap` for laying out elements into a - uniform grid. - - Reference - -------- - * [Bootstrap CSS Grid](https://getbootstrap.com/docs/5.3/layout/grid/) - * [Bootstrap Breakpoints](https://getbootstrap.com/docs/5.3/layout/breakpoints/) - """ - return RecallContextManager( - ui.layout_columns, - kwargs=dict( - col_widths=col_widths, - row_heights=row_heights, - fill=fill, - fillable=fillable, - gap=gap, - class_=class_, - height=height, - **kwargs, - ), - ) - - -def column( - width: int, *, offset: int = 0, **kwargs: TagAttrValue -) -> RecallContextManager[Tag]: - """ - Responsive row-column based layout - - This function wraps :func:`~shiny.ui.column`. See :func:`~shiny.ui.row` for more - information. - - Parameters - ---------- - width - The width of the column (an integer between 1 and 12). - offset - The number of columns to offset this column from the end of the previous column. - **kwargs - Attributes to place on the column tag. - - Returns - ------- - : - A UI element. - - See Also - ------- - :func:`~shiny.ui.row` - """ - return RecallContextManager( - ui.column, - args=(width,), - kwargs=dict( - offset=offset, - **kwargs, - ), - ) - - -def row(**kwargs: TagAttrValue) -> RecallContextManager[Tag]: - """ - Responsive row-column based layout - - This function wraps :func:`~shiny.ui.row`. Layout UI components using Bootstrap's - grid layout system. Use ``row()`` to group elements that should appear on the same - line (if the browser has adequate width) and :func:`~shiny.ui.column` to define how - much horizontal space within a 12-unit wide grid each on of these elements should - occupy. See the [layout guide](https://shiny.posit.co/articles/layout-guide.html>) - for more context and examples. (The article is about Shiny for R, but the general - principles are the same.) - - Parameters - ---------- - **kwargs - Attributes to place on the row tag. - - Returns - ------- - : - A UI element. - - See Also - ------- - :func:`~shiny.ui.column` - """ - return RecallContextManager(ui.row, kwargs=kwargs) - - -def card( - *, - full_screen: bool = False, - height: Optional[CssUnit] = None, - max_height: Optional[CssUnit] = None, - min_height: Optional[CssUnit] = None, - fill: bool = True, - class_: Optional[str] = None, - **kwargs: TagAttrValue, -) -> RecallContextManager[Tag]: - """ - A Bootstrap card component - - This function wraps :func:`~shiny.ui.card`. A general purpose container for grouping - related UI elements together with a border and optional padding. To learn more about - `card()`s, see [this article](https://rstudio.github.io/bslib/articles/cards.html). - - Parameters - ---------- - full_screen - If `True`, an icon will appear when hovering over the card body. Clicking the - icon expands the card to fit viewport size. - height,max_height,min_height - Any valid CSS unit (e.g., `height="200px"`). Doesn't apply when a card is made - `full_screen` (in this case, consider setting a `height` in - :func:`~shiny.experimental.ui.card_body`). - fill - Whether or not to allow the card to grow/shrink to fit a fillable container with - an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). - class_ - Additional CSS classes for the returned Tag. - **kwargs - HTML attributes on the returned Tag. - - Returns - ------- - : - An :func:`~shiny.ui.tags.div` tag. - """ - - # wrapper - # A function (which returns a UI element) to call on unnamed arguments in `*args` - # which are not already card item(s) (like :func:`~shiny.ui.card_header`, - # :func:`~shiny.experimental.ui.card_body`, etc.). Note that non-card items are - # grouped together into one `wrapper` call (e.g. given `card("a", "b", - # card_body("c"), "d")`, `wrapper` would be called twice, once with `"a"` and - # `"b"` and once with `"d"`). - - return RecallContextManager( - ui.card, - kwargs=dict( - full_screen=full_screen, - height=height, - max_height=max_height, - min_height=min_height, - fill=fill, - class_=class_, - **kwargs, - ), - ) - - -def accordion( - *, - id: Optional[str] = None, - open: Optional[bool | str | list[str]] = None, - multiple: bool = True, - class_: Optional[str] = None, - width: Optional[CssUnit] = None, - height: Optional[CssUnit] = None, - **kwargs: TagAttrValue, -) -> RecallContextManager[Tag]: - """ - Create a vertically collapsing accordion. - - This function wraps :func:`~shiny.ui.accordion`. - - Parameters - ---------- - id - If provided, you can use `input.id()` in your server logic to determine which of - the :func:`~shiny.ui.accordion_panel`s are currently active. The - value will correspond to the :func:`~shiny.ui.accordion_panel`'s - `value` argument. - open - A list of :func:`~shiny.ui.accordion_panel` values to open (i.e., - show) by default. The default value of `None` will open the first - :func:`~shiny.ui.accordion_panel`. Use a value of `True` to open - all (or `False` to open none) of the items. It's only possible to open more than - one panel when `multiple=True`. - multiple - Whether multiple :func:`~shiny.ui.accordion_panel` can be open at - once. - class_ - Additional CSS classes to include on the accordion div. - width - Any valid CSS unit; for example, height="100%". - height - Any valid CSS unit; for example, height="100%". - **kwargs - Attributes to this tag. - - Returns - ------- - : - Accordion panel Tag object. - """ - return RecallContextManager( - ui.accordion, - kwargs=dict( - id=id, - open=open, - multiple=multiple, - class_=class_, - width=width, - height=height, - **kwargs, - ), - ) - - -def accordion_panel( - title: TagChild, - *, - value: Optional[str] | MISSING_TYPE = MISSING, - icon: Optional[TagChild] = None, - **kwargs: TagAttrValue, -) -> RecallContextManager[AccordionPanel]: - """ - Single accordion panel. - - This function wraps :func:`~shiny.ui.accordion_panel`. - - Parameters - ---------- - title - A title to appear in the :func:`~shiny.ui.accordion_panel`'s header. - value - A character string that uniquely identifies this panel. If `MISSING`, the - `title` will be used. - icon - A :class:`~htmltools.Tag` which is positioned just before the `title`. - **kwargs - Tag attributes to the `accordion-body` div Tag. - - Returns - ------- - : - `AccordionPanel` object. - """ - return RecallContextManager( - ui.accordion_panel, - args=(title,), - kwargs=dict( - value=value, - icon=icon, - **kwargs, - ), - ) - - -# ====================================================================================== -# Nav components -# ====================================================================================== - - -def navset( - *, - type: Literal["underline", "pill", "tab"] = "underline", - id: Optional[str] = None, - selected: Optional[str] = None, - header: TagChild = None, - footer: TagChild = None, -) -> RecallContextManager[NavSet]: - """ - Render a set of nav items - - Parameters - ---------- - type - The type of navset to render. Can be one of `"underline"`, `"pill"`, or `"tab"`. - id - If provided, will create an input value that holds the currently selected nav - item. - selected - Choose a particular nav item to select by default value (should match it's - ``value``). - header - UI to display above the selected content. - footer - UI to display below the selected content. - """ - # *args - # A collection of nav items (e.g., :func:`shiny.ui.nav`). - - funcs = { - "underline": ui.navset_underline, - "pill": ui.navset_pill, - "tab": ui.navset_tab, - } - - func = funcs.get(type, None) - if func is None: - raise ValueError(f"Invalid navset type: {type!r}") - - return RecallContextManager( - func, - kwargs=dict( - id=id, - selected=selected, - header=header, - footer=footer, - ), - ) - - -def navset_card( - *, - type: Literal["underline", "pill", "tab"] = "underline", - id: Optional[str] = None, - selected: Optional[str] = None, - title: Optional[TagChild] = None, - sidebar: Optional[ui.Sidebar] = None, - header: TagChild = None, - footer: TagChild = None, -) -> RecallContextManager[NavSetCard]: - """ - Render a set of nav items inside a card container. - - Parameters - ---------- - type - The type of navset to render. Can be one of `"underline"`, `"pill"`, or `"tab"`. - id - If provided, will create an input value that holds the currently selected nav - item. - selected - Choose a particular nav item to select by default value (should match it's - ``value``). - sidebar - A :class:`shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav` page. - header - UI to display above the selected content. - footer - UI to display below the selected content. - """ - # *args - # A collection of nav items (e.g., :func:`shiny.ui.nav`). - - funcs = { - "underline": ui.navset_card_underline, - "pill": ui.navset_card_pill, - "tab": ui.navset_card_tab, - } - - func = funcs.get(type, None) - if func is None: - raise ValueError(f"Invalid navset type: {type!r}") - - return RecallContextManager( - func, - kwargs=dict( - id=id, - selected=selected, - title=title, - sidebar=sidebar, - header=header, - footer=footer, - ), - ) - - -def nav_panel( - title: TagChild, - *, - value: Optional[str] = None, - icon: TagChild = None, -) -> RecallContextManager[NavPanel]: - """ - Create a nav item pointing to some internal content. - - This function wraps :func:`~shiny.ui.nav`. - - Parameters - ---------- - title - A title to display. Can be a character string or UI elements (i.e., tags). - value - The value of the item. This is used to determine whether the item is active - (when an ``id`` is provided to the nav container), programmatically select the - item (e.g., :func:`~shiny.ui.update_navs`), and/or be provided to the - ``selected`` argument of the navigation container (e.g., - :func:`~shiny.ui.navset_tab`). - icon - An icon to appear inline with the button/link. - """ - return RecallContextManager( - ui.nav_panel, - args=(title,), - kwargs=dict( - value=value, - icon=icon, - ), - ) - - -# ====================================================================================== -# Page components -# ====================================================================================== -def page_fluid( - *, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: str, -) -> RecallContextManager[Tag]: - """ - Create a fluid page. - - This function wraps :func:`~shiny.ui.page_fluid`. - - Parameters - ---------- - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Attributes on the page level container. - - Returns - ------- - : - A UI element. - """ - return RecallContextManager( - ui.page_fluid, - kwargs=dict( - title=title, - lang=lang, - **kwargs, - ), - ) - - -def page_fixed( - *, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: str, -) -> RecallContextManager[Tag]: - """ - Create a fixed page. - - This function wraps :func:`~shiny.ui.page_fixed`. - - Parameters - ---------- - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Attributes on the page level container. - - Returns - ------- - : - A UI element. - """ - return RecallContextManager( - ui.page_fixed, - kwargs=dict( - title=title, - lang=lang, - **kwargs, - ), - ) - - -def page_fillable( - *, - padding: Optional[CssUnit | list[CssUnit]] = None, - gap: Optional[CssUnit] = None, - fillable_mobile: bool = False, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: TagAttrValue, -) -> RecallContextManager[Tag]: - """ - Creates a fillable page. - - This function wraps :func:`~shiny.ui.page_fillable`. - - Parameters - ---------- - padding - Padding to use for the body. See :func:`~shiny.ui.css_unit.as_css_padding` - for more details. - fillable_mobile - Whether or not the page should fill the viewport's height on mobile devices - (i.e., narrow windows). - gap - A CSS length unit passed through :func:`~shiny.ui.css_unit.as_css_unit` - defining the `gap` (i.e., spacing) between elements provided to `*args`. - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - - Returns - ------- - : - A UI element. - """ - return RecallContextManager( - ui.page_fillable, - kwargs=dict( - padding=padding, - gap=gap, - fillable_mobile=fillable_mobile, - title=title, - lang=lang, - **kwargs, - ), - ) - - -def page_sidebar( - *, - title: Optional[str | Tag | TagList] = None, - fillable: bool = True, - fillable_mobile: bool = False, - window_title: str | MISSING_TYPE = MISSING, - lang: Optional[str] = None, - **kwargs: TagAttrValue, -) -> RecallContextManager[Tag]: - """ - Create a page with a sidebar and a title. - - This function wraps :func:`~shiny.ui.page_sidebar`. - - Parameters - ---------- - title - A title to display at the top of the page. - fillable - Whether or not the main content area should be considered a fillable - (i.e., flexbox) container. - fillable_mobile - Whether or not ``fillable`` should apply on mobile devices. - window_title - The browser's window title (defaults to the host URL of the page). Can also be - set as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Additional attributes passed to :func:`~shiny.ui.layout_sidebar`. - - Returns - ------- - : - A UI element. - """ - # sidebar - # Content to display in the sidebar. - - return RecallContextManager( - ui.page_sidebar, - kwargs=dict( - title=title, - fillable=fillable, - fillable_mobile=fillable_mobile, - window_title=window_title, - lang=lang, - **kwargs, - ), - ) +def __getattr__(name: str) -> object: + return getattr(ui, name) diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py new file mode 100644 index 000000000..f33cab3e2 --- /dev/null +++ b/shiny/express/ui/__init__.py @@ -0,0 +1,305 @@ +from __future__ import annotations + + +from htmltools import ( + TagList, + Tag, + TagChild, + TagAttrs, + TagAttrValue, + tags, + HTML, + head_content, + a, + br, + code, + div, + em, + h1, + h2, + h3, + h4, + h5, + h6, + hr, + img, + p, + pre, + span, + strong, +) + +from ...ui import ( + AccordionPanel, + AnimationOptions, + CardItem, + ShowcaseLayout, + Sidebar, + SliderStepArg, + SliderValueArg, + ValueBoxTheme, + download_button, + download_link, + brush_opts, + click_opts, + dblclick_opts, + help_text, + hover_opts, + include_css, + include_js, + input_action_button, + input_action_link, + input_checkbox, + input_checkbox_group, + input_switch, + input_radio_buttons, + input_date, + input_date_range, + input_file, + input_numeric, + input_password, + input_select, + input_selectize, + input_slider, + input_text, + input_text_area, + insert_accordion_panel, + remove_accordion_panel, + update_accordion, + update_accordion_panel, + update_sidebar, + update_action_button, + update_action_link, + update_checkbox, + update_switch, + update_checkbox_group, + update_radio_buttons, + update_date, + update_date_range, + update_numeric, + update_select, + update_selectize, + update_slider, + update_text, + update_text_area, + update_navs, + update_tooltip, + update_popover, + insert_ui, + remove_ui, + markdown, + modal_button, + modal, + modal_show, + modal_remove, + notification_show, + notification_remove, + nav_spacer, + output_plot, + output_image, + output_text, + output_text_verbatim, + output_table, + output_ui, + Progress, + output_data_frame, + value_box_theme, +) + +from ._cm_components import ( + set_page, + sidebar, + layout_sidebar, + layout_column_wrap, + layout_columns, + card, + card_header, + card_footer, + accordion, + accordion_panel, + nav_panel, + nav_control, + nav_menu, + navset_bar, + navset_card_pill, + navset_card_tab, + navset_card_underline, + navset_hidden, + navset_pill, + navset_pill_list, + navset_tab, + navset_underline, + value_box, + panel_well, + panel_conditional, + panel_fixed, + panel_absolute, + page_fluid, + page_fixed, + page_fillable, + page_sidebar, + page_navbar, +) + +__all__ = ( + # Imports from htmltools + "TagList", + "Tag", + "TagChild", + "TagAttrs", + "TagAttrValue", + "tags", + "HTML", + "head_content", + "a", + "br", + "code", + "div", + "em", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "img", + "p", + "pre", + "span", + "strong", + "tags", + # Imports from ...ui + "AccordionPanel", + "AnimationOptions", + "CardItem", + "ShowcaseLayout", + "Sidebar", + "SliderStepArg", + "SliderValueArg", + "ValueBoxTheme", + "download_button", + "download_link", + "brush_opts", + "click_opts", + "dblclick_opts", + "help_text", + "hover_opts", + "include_css", + "include_js", + "input_action_button", + "input_action_link", + "input_checkbox", + "input_checkbox_group", + "input_switch", + "input_radio_buttons", + "input_date", + "input_date_range", + "input_file", + "input_numeric", + "input_password", + "input_select", + "input_selectize", + "input_slider", + "input_text", + "input_text_area", + "insert_accordion_panel", + "remove_accordion_panel", + "update_accordion", + "update_accordion_panel", + "update_sidebar", + "update_action_button", + "update_action_link", + "update_checkbox", + "update_switch", + "update_checkbox_group", + "update_radio_buttons", + "update_date", + "update_date_range", + "update_numeric", + "update_select", + "update_selectize", + "update_slider", + "update_text", + "update_text_area", + "update_navs", + "update_tooltip", + "update_popover", + "insert_ui", + "remove_ui", + "markdown", + "modal_button", + "modal", + "modal_show", + "modal_remove", + "notification_show", + "notification_remove", + "nav_spacer", + "output_plot", + "output_image", + "output_text", + "output_text_verbatim", + "output_table", + "output_ui", + "Progress", + "output_data_frame", + "value_box_theme", + # Imports from ._cm_components + "set_page", + "sidebar", + "layout_sidebar", + "layout_column_wrap", + "layout_columns", + "card", + "card_header", + "card_footer", + "accordion", + "accordion_panel", + "nav_panel", + "nav_control", + "nav_menu", + "navset_bar", + "navset_card_pill", + "navset_card_tab", + "navset_card_underline", + "navset_hidden", + "navset_pill", + "navset_pill_list", + "navset_tab", + "navset_underline", + "value_box", + "panel_well", + "panel_conditional", + "panel_fixed", + "panel_absolute", + "page_fluid", + "page_fixed", + "page_fillable", + "page_sidebar", + "page_navbar", +) + + +# This is used for unit tests to verify that shiny.ui and shiny.express.ui stay in sync. +_known_missing = { + # Items from shiny.ui that don't have a counterpart in shiny.express.ui + "shiny.ui": ( + "column", # Deprecated in favor of layout_columns + "row", # Deprecated in favor of layout_columns + "nav", # Deprecated in favor of nav_panel + "navset_pill_card", # Deprecated + "navset_tab_card", # Deprecated + "page_bootstrap", + "page_output", + "panel_main", # Deprecated + "panel_sidebar", # Deprecated + "panel_title", + "popover", + "showcase_bottom", + "showcase_left_center", + "showcase_top_right", + "tooltip", + ), + # Items from shiny.express.ui that don't have a counterpart in shiny.ui + "shiny.express.ui": ("set_page",), +} diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py new file mode 100644 index 000000000..4772cae7f --- /dev/null +++ b/shiny/express/ui/_cm_components.py @@ -0,0 +1,1691 @@ +"Context manager components for Shiny Express" + +from __future__ import annotations + +from typing import Literal, Optional + +from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, TagFunction, TagList + +from ... import ui +from ...types import MISSING, MISSING_TYPE +from ...ui._accordion import AccordionPanel +from ...ui._card import CardItem +from ...ui._layout_columns import BreakpointsUser +from ...ui._navs import NavMenu, NavPanel, NavSet, NavSetBar, NavSetCard +from ...ui.css import CssUnit +from .. import _run +from .._recall_context import RecallContextManager + +__all__ = ( + "set_page", + "sidebar", + "layout_sidebar", + "layout_column_wrap", + "layout_columns", + "card", + "card_header", + "card_footer", + "accordion", + "accordion_panel", + "nav_panel", + "nav_control", + "nav_menu", + "panel_well", + "panel_conditional", + "panel_fixed", + "panel_absolute", + "page_fluid", + "page_fixed", + "page_fillable", + "page_sidebar", + "page_navbar", +) + + +# ====================================================================================== +# Page functions +# ====================================================================================== +def set_page(page_fn: RecallContextManager[Tag]) -> None: + """Set the page function for the current Shiny express app.""" + _run.replace_top_level_recall_context_manager(page_fn, force=True) + + +# ====================================================================================== +# Shiny layout components +# ====================================================================================== +def sidebar( + *, + width: CssUnit = 250, + position: Literal["left", "right"] = "left", + open: Literal["desktop", "open", "closed", "always"] = "always", + id: Optional[str] = None, + title: TagChild | str = None, + bg: Optional[str] = None, + fg: Optional[str] = None, + class_: Optional[str] = None, # TODO-future; Consider using `**kwargs` instead + max_height_mobile: Optional[str | float] = "auto", + gap: Optional[CssUnit] = None, + padding: Optional[CssUnit | list[CssUnit]] = None, +) -> RecallContextManager[ui.Sidebar]: + """ + Context manager for sidebar element + + This function wraps :func:`~shiny.ui.sidebar`. + + Parameters + ---------- + width + A valid CSS unit used for the width of the sidebar. + position + Where the sidebar should appear relative to the main content. + open + The initial state of the sidebar. + + * `"desktop"`: the sidebar starts open on desktop screen, closed on mobile + * `"open"` or `True`: the sidebar starts open + * `"closed"` or `False`: the sidebar starts closed + * `"always"` or `None`: the sidebar is always open and cannot be closed + + In :func:`~shiny.ui.update_sidebar`, `open` indicates the desired state of the + sidebar. Note that :func:`~shiny.ui.update_sidebar` can only open or close the + sidebar, so it does not support the `"desktop"` and `"always"` options. + id + A character string. Required if wanting to re-actively read (or update) the + `collapsible` state in a Shiny app. + title + A character title to be used as the sidebar title, which will be wrapped in a + `
` element with class `sidebar-title`. You can also provide a custom + :class:`~htmltools.Tag` for the title element, in which case you'll + likely want to give this element `class = "sidebar-title"`. + bg,fg + A background or foreground color. + class_ + CSS classes for the sidebar container element, in addition to the fixed + `.sidebar` class. + max_height_mobile + A CSS length unit (passed through :func:`~shiny.ui.css.as_css_unit`) defining + the maximum height of the horizontal sidebar when viewed on mobile devices. Only + applies to always-open sidebars that use `open = "always"`, where by default the + sidebar container is placed below the main content container on mobile devices. + gap + A CSS length unit defining the vertical `gap` (i.e., spacing) between elements + provided to `*args`. + padding + Padding within the sidebar itself. This can be a numeric vector (which will be + interpreted as pixels) or a character vector with valid CSS lengths. `padding` + may be one to four values. + + * If a single value, then that value will be used for all four sides. + * If two, then the first value will be used for the top and bottom, while + the second value will be used for left and right. + * If three values, then the first will be used for top, the second will be left + and right, and the third will be bottom. + * If four, then the values will be interpreted as top, right, bottom, and left + respectively. + """ + return RecallContextManager( + ui.sidebar, + default_page=page_sidebar(), + kwargs=dict( + width=width, + position=position, + open=open, + id=id, + title=title, + bg=bg, + fg=fg, + class_=class_, + max_height_mobile=max_height_mobile, + gap=gap, + padding=padding, + ), + ) + + +# TODO: Figure out sidebar arg for ui.layout_sidebar +def layout_sidebar( + *, + fillable: bool = True, + fill: bool = True, + bg: Optional[str] = None, + fg: Optional[str] = None, + border: Optional[bool] = None, + border_radius: Optional[bool] = None, + border_color: Optional[str] = None, + gap: Optional[CssUnit] = None, + padding: Optional[CssUnit | list[CssUnit]] = None, + height: Optional[CssUnit] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[CardItem]: + """ + Context manager for sidebar layout + + This function wraps :func:`~shiny.ui.layout_sidebar`. + + Create a sidebar layout component which can be dropped inside any Shiny UI page + method (e.g. :func:`~shiny.shiny.ui.page_fillable`) or :func:`~shiny.ui.card` + context. + + The first child needs to be of class :class:`~shiny.ui.Sidebar` object created by + :func:`~shiny.express.ui.sidebar`. The remaining arguments will contain the contents + to the main content area. Or tag attributes that are supplied to the resolved + :class:`~htmltools.Tag` object. + + Parameters + ---------- + fillable + Whether or not the main content area should be wrapped in a fillable container. + See :func:`~shiny.ui.as_fillable_container` for details. + fill + Whether or not the sidebar layout should be wrapped in a fillable container. See + :func:`~shiny.ui.as_fill_item` for details. + bg,fg + A background or foreground color. + border + Whether or not to show a border around the sidebar layout. + border_radius + Whether or not to round the corners of the sidebar layout. + border_color + A border color. + gap + A CSS length unit defining the vertical `gap` (i.e., spacing) between elements + provided to `*args`. This value will only be used if `fillable` is `True`. + padding + Padding within the sidebar itself. This can be a numeric vector (which will be + interpreted as pixels) or a character vector with valid CSS lengths. `padding` + may be one to four values. If one, then that value will be used for all four + sides. If two, then the first value will be used for the top and bottom, while + the second value will be used for left and right. If three, then the first will + be used for top, the second will be left and right, and the third will be + bottom. If four, then the values will be interpreted as top, right, bottom, and + left respectively. + height + Any valid CSS unit to use for the height. + """ + return RecallContextManager( + ui.layout_sidebar, + kwargs=dict( + fillable=fillable, + fill=fill, + bg=bg, + fg=fg, + border=border, + border_radius=border_radius, + border_color=border_color, + gap=gap, + padding=padding, + height=height, + **kwargs, + ), + ) + + +def layout_column_wrap( + *, + width: CssUnit | None | MISSING_TYPE = MISSING, + fixed_width: bool = False, + heights_equal: Literal["all", "row"] = "all", + fill: bool = True, + fillable: bool = True, + height: Optional[CssUnit] = None, + height_mobile: Optional[CssUnit] = None, + gap: Optional[CssUnit] = None, + class_: Optional[str] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[Tag]: + """ + Context manager for a grid-like, column-first layout + + This function wraps :func:`~shiny.ui.layout_column_wrap`. + + Wraps a 1d sequence of UI elements into a 2d grid. The number of columns (and rows) + in the grid dependent on the column `width` as well as the size of the display. + + Parameters + ---------- + width + The desired width of each card. It can be one of the following: + + * A (unit-less) number between 0 and 1, specified as `1/num`, where `num` + represents the number of desired columns. + * A CSS length unit representing either the minimum (when `fixed_width=False`) + or fixed width (`fixed_width=True`). + * `None`, which allows power users to set the `grid-template-columns` CSS + property manually, either via a `style` attribute or a CSS stylesheet. + * If missing, a value of `200px` will be used. + fixed_width + When `width` is greater than 1 or is a CSS length unit, e.g. `"200px"`, + `fixed_width` indicates whether that `width` value represents the absolute size + of each column (`fixed_width=TRUE`) or the minimum size of a column + (`fixed_width=FALSE`). + + When `fixed_width=FALSE`, new columns are added to a row when `width` space is + available and columns will never exceed the container or viewport size. + + When `fixed_width=TRUE`, all columns will be exactly `width` wide, which may + result in columns overflowing the parent container. + heights_equal + If `"all"` (the default), every card in every row of the grid will have the same + height. If `"row"`, then every card in _each_ row of the grid will have the same + height, but heights may vary between rows. + fill + Whether or not to allow the layout to grow/shrink to fit a fillable container + with an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). + fillable + Whether or not each element is wrapped in a fillable container. + height + Any valid CSS unit to use for the height. + height_mobile + Any valid CSS unit to use for the height when on mobile devices (or narrow + windows). + gap + Any valid CSS unit to use for the gap between columns. + class_ + A CSS class to apply to the containing element. + **kwargs + Additional attributes to apply to the containing element. + """ + return RecallContextManager( + ui.layout_column_wrap, + default_page=page_fillable(), + kwargs=dict( + width=width, + fixed_width=fixed_width, + heights_equal=heights_equal, + fill=fill, + fillable=fillable, + height=height, + height_mobile=height_mobile, + gap=gap, + class_=class_, + **kwargs, + ), + ) + + +def layout_columns( + *, + col_widths: BreakpointsUser[int] = None, + row_heights: BreakpointsUser[CssUnit] = None, + fill: bool = True, + fillable: bool = True, + gap: Optional[CssUnit] = None, + class_: Optional[str] = None, + height: Optional[CssUnit] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[Tag]: + """ + Context manager for responsive, column-based grid layouts, based on a 12-column + grid. + + This function wraps :func:`~shiny.ui.layout_columns`. + + Parameters + ---------- + col_widths + The widths of the columns, possibly at different breakpoints. Can be one of the + following: + + * `None` (the default): Automatically determines a sensible number of columns + based on the number of children given to the layout. + * A list or tuple of integers between 1 and 12, where each element represents + the number of columns for the relevant UI element. Column widths are recycled + to extend the values in `col_widths` to match the actual number of items in + the layout, and children are wrapped onto the next row when a row exceeds 12 + column units. For example, `col_widths=(4, 8, 12)` allocates 4 columns to the + first element, 8 columns to the second element, and 12 columns to the third + element (which wraps to the next row). Negative values are also allowed, and + are treated as empty columns. For example, `col_widths=(-2, 8, -2)` would + allocate 8 columns to an element (with 2 empty columns on either side). + * A dictionary of column widths at different breakpoints. The keys should be one + of `"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"`, or `"xxl"`, and the values are + either of the above. For example, `col_widths={"sm": (3, 3, 6), "lg": (4)}`. + + row_heights + The heights of the rows, possibly at different breakpoints. Can be one of the + following: + + * A numeric vector, where each value represents the [fractional + unit](https://css-tricks.com/introduction-fr-css-unit/) (`fr`) height of the + relevant row. If there are more rows than values provided, the pattern will be + repeated. For example, `row_heights=(1, 2)` allows even rows to take up twice + as much space as odd rows. + * A list of numeric or CSS length units, where each value represents the height + of the relevant row. If more rows are needed than values provided, the pattern + will repeat. For example, `row_heights=["auto", 1]` allows the height of odd + rows to be driven my it's contents and even rows to be + [`1fr`](https://css-tricks.com/introduction-fr-css-unit/). + * A single string containing CSS length units. In this case, the value is + supplied directly to `grid-auto-rows`. + * A dictionary of row heights at different breakpoints, where each key is a + breakpoint name (one of `"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"`, or `"xxl"`) + and where the values may be any of the above options. + + fill + Whether or not to allow the layout to grow/shrink to fit a fillable container + with an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). + + fillable + Whether or not each element is wrapped in a fillable container. + + gap + Any valid CSS unit to use for the gap between columns. + + class_ + CSS class(es) to apply to the containing element. + + height + Any valid CSS unit to use for the height. + + **kwargs + Additional attributes to apply to the containing element. + + Returns + ------- + : + An :class:`~htmltools.Tag` element. + + See Also + -------- + * :func:`~shiny.express.layout.layout_column_wrap` for laying out elements into a + uniform grid. + + Reference + -------- + * [Bootstrap CSS Grid](https://getbootstrap.com/docs/5.3/layout/grid/) + * [Bootstrap Breakpoints](https://getbootstrap.com/docs/5.3/layout/breakpoints/) + """ + return RecallContextManager( + ui.layout_columns, + kwargs=dict( + col_widths=col_widths, + row_heights=row_heights, + fill=fill, + fillable=fillable, + gap=gap, + class_=class_, + height=height, + **kwargs, + ), + ) + + +def card( + *, + full_screen: bool = False, + height: Optional[CssUnit] = None, + max_height: Optional[CssUnit] = None, + min_height: Optional[CssUnit] = None, + fill: bool = True, + class_: Optional[str] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[Tag]: + """ + Context manager for Bootstrap card component + + This function wraps :func:`~shiny.ui.card`. + + A general purpose container for grouping related UI elements together with a border + and optional padding. To learn more about `card()`s, see [this + article](https://rstudio.github.io/bslib/articles/cards.html). + + Parameters + ---------- + full_screen + If `True`, an icon will appear when hovering over the card body. Clicking the + icon expands the card to fit viewport size. + height,max_height,min_height + Any valid CSS unit (e.g., `height="200px"`). Doesn't apply when a card is made + `full_screen` (in this case, consider setting a `height` in + :func:`~shiny.experimental.ui.card_body`). + fill + Whether or not to allow the card to grow/shrink to fit a fillable container with + an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). + class_ + Additional CSS classes for the returned Tag. + **kwargs + HTML attributes on the returned Tag. + """ + + # wrapper + # A function (which returns a UI element) to call on unnamed arguments in `*args` + # which are not already card item(s) (like :func:`~shiny.ui.card_header`, + # :func:`~shiny.experimental.ui.card_body`, etc.). Note that non-card items are + # grouped together into one `wrapper` call (e.g. given `card("a", "b", + # card_body("c"), "d")`, `wrapper` would be called twice, once with `"a"` and + # `"b"` and once with `"d"`). + + return RecallContextManager( + ui.card, + kwargs=dict( + full_screen=full_screen, + height=height, + max_height=max_height, + min_height=min_height, + fill=fill, + class_=class_, + **kwargs, + ), + ) + + +ui.card_header + + +def card_header( + *args: TagChild | TagAttrs, + container: TagFunction = ui.tags.div, + **kwargs: TagAttrValue, +) -> RecallContextManager[CardItem]: + """ + Context manager for a card header container + + This function wraps :func:`~shiny.ui.card_header`. + + A general container for the "header" of a :func:`~shiny.ui.card`. This component is designed + to be provided as a direct child to :func:`~shiny.ui.card`. + + The header has a different background color and border than the rest of the card. + + Parameters + ---------- + *args + Contents to the header container. Or tag attributes that are supplied to the + resolved :class:`~htmltools.Tag` object. + container + Method for the returned Tag object. Defaults to :func:`~shiny.ui.tags.div`. + **kwargs + Additional HTML attributes for the returned Tag. + """ + return RecallContextManager( + ui.card_header, + args=args, + kwargs=dict( + container=container, + **kwargs, + ), + ) + + +def card_footer( + *args: TagChild | TagAttrs, + **kwargs: TagAttrValue, +) -> RecallContextManager[CardItem]: + """ + Context manager for a card footer container + + This function wraps :func:`~shiny.ui.card_footer`. + + + A general container for the "footer" of a :func:`~shiny.ui.card`. This component is designed + to be provided as a direct child to :func:`~shiny.ui.card`. + + The footer has a different background color and border than the rest of the card. + + Parameters + ---------- + *args + Contents to the footer container. Or tag attributes that are supplied to the + resolved :class:`~htmltools.Tag` object. + **kwargs + Additional HTML attributes for the returned Tag. + + """ + return RecallContextManager( + ui.card_footer, + args=args, + kwargs=kwargs, + ) + + +def accordion( + *, + id: Optional[str] = None, + open: Optional[bool | str | list[str]] = None, + multiple: bool = True, + class_: Optional[str] = None, + width: Optional[CssUnit] = None, + height: Optional[CssUnit] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[Tag]: + """ + Context manager for a vertically collapsing accordion. + + This function wraps :func:`~shiny.ui.accordion`. + + Parameters + ---------- + id + If provided, you can use `input.id()` in your server logic to determine which of + the :func:`~shiny.ui.accordion_panel`s are currently active. The + value will correspond to the :func:`~shiny.ui.accordion_panel`'s + `value` argument. + open + A list of :func:`~shiny.ui.accordion_panel` values to open (i.e., + show) by default. The default value of `None` will open the first + :func:`~shiny.ui.accordion_panel`. Use a value of `True` to open + all (or `False` to open none) of the items. It's only possible to open more than + one panel when `multiple=True`. + multiple + Whether multiple :func:`~shiny.ui.accordion_panel` can be open at + once. + class_ + Additional CSS classes to include on the accordion div. + width + Any valid CSS unit; for example, height="100%". + height + Any valid CSS unit; for example, height="100%". + **kwargs + Attributes to this tag. + """ + return RecallContextManager( + ui.accordion, + kwargs=dict( + id=id, + open=open, + multiple=multiple, + class_=class_, + width=width, + height=height, + **kwargs, + ), + ) + + +def accordion_panel( + title: TagChild, + *, + value: Optional[str] | MISSING_TYPE = MISSING, + icon: Optional[TagChild] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[AccordionPanel]: + """ + Context manager for single accordion panel. + + This function wraps :func:`~shiny.ui.accordion_panel`. + + Parameters + ---------- + title + A title to appear in the :func:`~shiny.ui.accordion_panel`'s header. + value + A character string that uniquely identifies this panel. If `MISSING`, the + `title` will be used. + icon + A :class:`~htmltools.Tag` which is positioned just before the `title`. + **kwargs + Tag attributes to the `accordion-body` div Tag. + """ + return RecallContextManager( + ui.accordion_panel, + args=(title,), + kwargs=dict( + value=value, + icon=icon, + **kwargs, + ), + ) + + +# ====================================================================================== +# Nav components +# ====================================================================================== + + +def navset_tab( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSet]: + """ + Context manager for a set of nav items as a tabset. + + This function wraps :func:`~shiny.ui.navset_tab`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ + return RecallContextManager( + ui.navset_tab, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + ), + ) + + +def navset_pill( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSet]: + """ + Context manager for a set of nav items as a pillset. + + This function wraps :func:`~shiny.ui.navset_pill`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ + return RecallContextManager( + ui.navset_pill, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + ), + ) + + +def navset_underline( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSet]: + """ + Context manager for a set of nav items whose active/focused navigation links are + styled with an underline. + + This function wraps :func:`~shiny.ui.navset_underline`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ + return RecallContextManager( + ui.navset_underline, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + ), + ) + + +def navset_hidden( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSet]: + """ + Context manager for nav contents without the nav items. + + This function wraps :func:`~shiny.ui.navset_hidden`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ + return RecallContextManager( + ui.navset_hidden, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + ), + ) + + +def navset_card_tab( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + title: Optional[TagChild] = None, + sidebar: Optional[ui.Sidebar] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSetCard]: + """ + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_card_tab`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :class:`shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav` page. + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ + return RecallContextManager( + ui.navset_card_tab, + kwargs=dict( + id=id, + selected=selected, + title=title, + sidebar=sidebar, + header=header, + footer=footer, + ), + ) + + +def navset_card_pill( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + title: Optional[TagChild] = None, + sidebar: Optional[ui.Sidebar] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSetCard]: + """ + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_card_pill`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :class:`shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav` page. + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ + return RecallContextManager( + ui.navset_card_pill, + kwargs=dict( + id=id, + selected=selected, + title=title, + sidebar=sidebar, + header=header, + footer=footer, + ), + ) + + +def navset_card_underline( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + title: Optional[TagChild] = None, + sidebar: Optional[ui.Sidebar] = None, + header: TagChild = None, + footer: TagChild = None, + placement: Literal["above", "below"] = "above", +) -> RecallContextManager[NavSetCard]: + """ + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_card_underline`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :class:`shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav` page. + header + UI to display above the selected content. + footer + UI to display below the selected content. + placement + Placement of the nav items relative to the content. + """ + return RecallContextManager( + ui.navset_card_underline, + kwargs=dict( + id=id, + selected=selected, + title=title, + sidebar=sidebar, + header=header, + footer=footer, + placement=placement, + ), + ) + + +def navset_pill_list( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, + well: bool = True, + widths: tuple[int, int] = (4, 8), +) -> RecallContextManager[NavSet]: + """ + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_pill_list`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match its + ``value``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + well + ``True`` to place a well (gray rounded rectangle) around the navigation list. + widths + Column widths of the navigation list and tabset content areas respectively. + """ + return RecallContextManager( + ui.navset_pill_list, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + well=well, + widths=widths, + ), + ) + + +def navset_bar( + *, + title: TagChild, + id: Optional[str] = None, + selected: Optional[str] = None, + sidebar: Optional[ui.Sidebar] = None, + fillable: bool | list[str] = True, + gap: Optional[CssUnit] = None, + padding: Optional[CssUnit | list[CssUnit]] = None, + position: Literal[ + "static-top", "fixed-top", "fixed-bottom", "sticky-top" + ] = "static-top", + header: TagChild = None, + footer: TagChild = None, + bg: Optional[str] = None, + # TODO: default to 'auto', like we have in R (parse color via webcolors?) + inverse: bool = False, + underline: bool = True, + collapsible: bool = True, + fluid: bool = True, +) -> RecallContextManager[NavSetBar]: + """ + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_bar`. + + Parameters + ---------- + title + Title to display in the navbar. + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :class:`~shiny.ui.Sidebar` component to display on every + :func:`~shiny.ui.nav_panel` page. + fillable + Whether or not to allow fill items to grow/shrink to fit the browser window. If + `True`, all `nav()` pages are fillable. A character vector, matching the value + of `nav()`s to be filled, may also be provided. Note that, if a `sidebar` is + provided, `fillable` makes the main content portion fillable. + gap + A CSS length unit defining the gap (i.e., spacing) between elements provided to + `*args`. + padding + Padding to use for the body. This can be a numeric vector (which will be + interpreted as pixels) or a character vector with valid CSS lengths. The length + can be between one and four. If one, then that value will be used for all four + sides. If two, then the first value will be used for the top and bottom, while + the second value will be used for left and right. If three, then the first will + be used for top, the second will be left and right, and the third will be + bottom. If four, then the values will be interpreted as top, right, bottom, and + left respectively. + position + Determines whether the navbar should be displayed at the top of the page with + normal scrolling behavior ("static-top"), pinned at the top ("fixed-top"), or + pinned at the bottom ("fixed-bottom"). Note that using "fixed-top" or + "fixed-bottom" will cause the navbar to overlay your body content, unless you + add padding (e.g., ``tags.style("body {padding-top: 70px;}")``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + bg + Background color of the navbar (a CSS color). + inverse + Either ``True`` for a light text color or ``False`` for a dark text color. + collapsible + ``True`` to automatically collapse the navigation elements into an expandable + menu on mobile devices or narrow window widths. + fluid + ``True`` to use fluid layout; ``False`` to use fixed layout. + """ + return RecallContextManager( + ui.navset_bar, + kwargs=dict( + title=title, + id=id, + selected=selected, + sidebar=sidebar, + fillable=fillable, + gap=gap, + padding=padding, + position=position, + header=header, + footer=footer, + bg=bg, + inverse=inverse, + underline=underline, + collapsible=collapsible, + fluid=fluid, + ), + ) + + +def nav_panel( + title: TagChild, + *, + value: Optional[str] = None, + icon: TagChild = None, +) -> RecallContextManager[NavPanel]: + """ + Context manager for nav item pointing to some internal content. + + This function wraps :func:`~shiny.ui.nav`. + + Parameters + ---------- + title + A title to display. Can be a character string or UI elements (i.e., tags). + value + The value of the item. This is used to determine whether the item is active + (when an ``id`` is provided to the nav container), programmatically select the + item (e.g., :func:`~shiny.ui.update_navs`), and/or be provided to the + ``selected`` argument of the navigation container (e.g., + :func:`~shiny.ui.navset_tab`). + icon + An icon to appear inline with the button/link. + """ + return RecallContextManager( + ui.nav_panel, + args=(title,), + kwargs=dict( + value=value, + icon=icon, + ), + ) + + +def nav_control() -> RecallContextManager[NavPanel]: + """ + Context manager for a control in the navigation container. + + This function wraps :func:`~shiny.ui.nav_control`. + """ + return RecallContextManager(ui.nav_control) + + +def nav_menu( + title: TagChild, + *, + value: Optional[str] = None, + icon: TagChild = None, + align: Literal["left", "right"] = "left", +) -> RecallContextManager[NavMenu]: + """ + Context manager for a menu of nav items. + + This function wraps :func:`~shiny.ui.nav_menu`. + + Parameters + ---------- + title + A title to display. Can be a character string or UI elements (i.e., tags). + value + The value of the item. This is used to determine whether the item is active + (when an ``id`` is provided to the nav container), programmatically select the + item (e.g., :func:`~shiny.ui.update_navs`), and/or be provided to the + ``selected`` argument of the navigation container (e.g., + :func:`~shiny.ui.navset_tab`). + icon + An icon to appear inline with the button/link. + align + Horizontal alignment of the dropdown menu relative to dropdown toggle. + """ + + return RecallContextManager( + ui.nav_menu, + args=(title,), + kwargs=dict( + value=value, + icon=icon, + align=align, + ), + ) + + +# ====================================================================================== +# Value boxes +# ====================================================================================== +def value_box( + title: TagChild, + value: TagChild, + *, + showcase: Optional[TagChild] = None, + showcase_layout: ui._valuebox.SHOWCASE_LAYOUTS_STR + | ui.ShowcaseLayout = "left center", + full_screen: bool = False, + theme: Optional[str | ui.ValueBoxTheme] = None, + height: Optional[CssUnit] = None, + max_height: Optional[CssUnit] = None, + fill: bool = True, + class_: Optional[str] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[Tag]: + """ + Context manager for a value box + + This function wraps :func:`~shiny.ui.value_box`. + + An opinionated (:func:`~shiny.ui.card`-powered) box, designed for + displaying a `value` and `title`. Optionally, a `showcase` can provide for context + for what the `value` represents (for example, it could hold an icon, or even a + :func:`~shiny.ui.output_plot`). + + Parameters + ---------- + title,value + A string, number, or :class:`~htmltools.Tag` child to display as + the title or value of the value box. The `title` appears above the `value`. + showcase + A :class:`~htmltools.Tag` child to showcase (e.g., an icon, a + :func:`~shiny.ui.output_plot`, etc). + showcase_layout + One of `"left center"` (default), `"top right"` or `"bottom"`. Alternatively, + you can customize the showcase layout options with the + :func:`~shiny.ui.showcase_left_center`, :func:`~shiny.ui.showcase_top_right()`, + or :func:`~shiny.ui.showcase_bottom()` functions. Use the options functions when + you want to control the height or width of the showcase area. + theme + The name of a theme (e.g. `"primary"`, `"danger"`, `"purple"`, `"bg-green"`, + `"text-red"`) for the value box, or a theme constructed with + :func:`~shiny.ui.value_box_theme`. The theme names provide a convenient way to + use your app's Bootstrap theme colors as the foreground or background colors of + the value box. For more control, you can create your own theme with + :func:`~shiny.ui.value_box_theme` where you can pass foreground and background + colors directly. Bootstrap supported color themes: `"blue"`, `"purple"`, + `"pink"`, `"red"`, `"orange"`, `"yellow"`, `"green"`, `"teal"`, and `"cyan"`. + These colors can be used with `bg-NAME`, `text-NAME`, and + `bg-gradient-NAME1-NAME2` to change the background, foreground, or use a + background gradient respectively. If a `theme` string does not start with + `text-` or `bg-`, it will be auto prefixed with `bg-`. + full_screen + If `True`, an icon will appear when hovering over the card body. Clicking the + icon expands the card to fit viewport size. + height,max_height + Any valid CSS unit (e.g., `height="200px"`). Doesn't apply when a card is made + `full_screen`. + fill + Whether to allow the value box to grow/shrink to fit a fillable container with + an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). + class_ + Utility classes for customizing the appearance of the summary card. Use `bg-*` + and `text-*` classes (e.g, `"bg-danger"` and `"text-light"`) to customize the + background/foreground colors. + **kwargs + Additional attributes to pass to :func:`~shiny.ui.card`. + """ + return RecallContextManager( + ui.value_box, + args=(title, value), + kwargs=dict( + showcase=showcase, + showcase_layout=showcase_layout, + full_screen=full_screen, + theme=theme, + height=height, + max_height=max_height, + fill=fill, + class_=class_, + **kwargs, + ), + ) + + +# ====================================================================================== +# Panels +# ====================================================================================== + + +def panel_well(**kwargs: TagAttrValue) -> RecallContextManager[Tag]: + """ + Context manager for a well panel + + This function wraps :func:`~shiny.ui.panel_well`. + + A well panel is a simple container with a border and some padding. It's useful for + grouping related content together. + """ + return RecallContextManager( + ui.panel_well, + kwargs=dict( + **kwargs, + ), + ) + + +def panel_conditional( + *, + condition: str, + **kwargs: TagAttrValue, +) -> RecallContextManager[Tag]: + """ + Context manager for a conditional panel + + This function wraps :func:`~shiny.ui.panel_conditional`. + + Show UI elements only if a ``JavaScript`` condition is ``true``. + + Parameters + ---------- + condition + A JavaScript expression that will be evaluated repeatedly to determine whether + the panel should be displayed. + **kwargs + Attributes to place on the panel tag. + + Note + ---- + In the JS expression, you can refer to input and output JavaScript objects that + contain the current values of input and output. For example, if you have an input + with an id of foo, then you can use input.foo to read its value. (Be sure not to + modify the input/output objects, as this may cause unpredictable behavior.) + + You are not recommended to use special JavaScript characters such as a period . in + the input id's, but if you do use them anyway, for example, ``id = "foo.bar"``, you + will have to use ``input["foo.bar"]`` instead of ``input.foo.bar`` to read the input + value. + + Tip + --- + A more powerful (but slower) way to conditionally show UI content is to use + :func:`~shiny.render.ui`. + """ + return RecallContextManager( + ui.panel_conditional, + kwargs=dict( + condition=condition, + **kwargs, + ), + ) + + +def panel_fixed( + *, + top: Optional[str] = None, + left: Optional[str] = None, + right: Optional[str] = None, + bottom: Optional[str] = None, + width: Optional[str] = None, + height: Optional[str] = None, + draggable: bool = False, + cursor: Literal["auto", "move", "default", "inherit"] = "auto", + **kwargs: TagAttrValue, +) -> RecallContextManager[TagList]: + """ + Context manager for a panel of absolutely positioned content. + + This function wraps :func:`~shiny.ui.panel_fixed`. + + This function is equivalent to calling :func:`~shiny.ui.panel_absolute` with + ``fixed=True`` (i.e., the panel does not scroll with the rest of the page). See + :func:`~shiny.ui.panel_absolute` for more information. + + Parameters + ---------- + **kwargs + Arguments passed along to :func:`~shiny.ui.panel_absolute`. + + See Also + ------- + :func:`~shiny.ui.panel_absolute` + """ + return RecallContextManager( + ui.panel_fixed, + kwargs=dict( + top=top, + left=left, + right=right, + bottom=bottom, + width=width, + height=height, + draggable=draggable, + cursor=cursor, + **kwargs, + ), + ) + + +def panel_absolute( + *, + top: Optional[str] = None, + left: Optional[str] = None, + right: Optional[str] = None, + bottom: Optional[str] = None, + width: Optional[str] = None, + height: Optional[str] = None, + draggable: bool = False, + fixed: bool = False, + cursor: Literal["auto", "move", "default", "inherit"] = "auto", + **kwargs: TagAttrValue, +) -> RecallContextManager[TagList]: + """ + Context manager for a panel of absolutely positioned content. + + This function wraps :func:`~shiny.ui.panel_absolute`. + + Creates a ``
`` tag whose CSS position is set to absolute (or fixed if ``fixed = + True``). The way absolute positioning works in HTML is that absolute coordinates are + specified relative to its nearest parent element whose position is not set to static + (which is the default), and if no such parent is found, then relative to the page + borders. If you're not sure what that means, just keep in mind that you may get + strange results if you use this function from inside of certain types of panels. + + Parameters + ---------- + top + Distance between the top of the panel, and the top of the page or parent + container. + left + Distance between the left side of the panel, and the left of the page or parent + container. + right + Distance between the right side of the panel, and the right of the page or + parent container. + bottom + Distance between the bottom of the panel, and the bottom of the page or parent + container. + width + Width of the panel. + height + Height of the panel. + draggable + If ``True``, allows the user to move the panel by clicking and dragging. + fixed + Positions the panel relative to the browser window and prevents it from being + scrolled with the rest of the page. + cursor + The type of cursor that should appear when the user mouses over the panel. Use + ``"move"`` for a north-east-south-west icon, ``"default"`` for the usual cursor + arrow, or ``"inherit"`` for the usual cursor behavior (including changing to an + I-beam when the cursor is over text). The default is ``"auto"``, which is + equivalent to ``"move" if draggable else "inherit"``. + **kwargs + Attributes added to the content's container tag. + + Tip + ---- + The position (``top``, ``left``, ``right``, ``bottom``) and size (``width``, + ``height``) parameters are all optional, but you should specify exactly two of top, + bottom, and height and exactly two of left, right, and width for predictable + results. + + Like most other distance parameters in Shiny, the position and size parameters take + a number (interpreted as pixels) or a valid CSS size string, such as ``"100px"`` + (100 pixels) or ``"25%"``. + + For arcane HTML reasons, to have the panel fill the page or parent you should + specify 0 for ``top``, ``left``, ``right``, and ``bottom`` rather than the more + obvious ``width = "100%"`` and ``height = "100%"``. + """ + return RecallContextManager( + ui.panel_absolute, + kwargs=dict( + top=top, + left=left, + right=right, + bottom=bottom, + width=width, + height=height, + draggable=draggable, + fixed=fixed, + cursor=cursor, + **kwargs, + ), + ) + + +# ====================================================================================== +# Page components +# ====================================================================================== +def page_fluid( + *, + title: Optional[str] = None, + lang: Optional[str] = None, + **kwargs: str, +) -> RecallContextManager[Tag]: + """ + Create a fluid page. + + This function wraps :func:`~shiny.ui.page_fluid`. + + Parameters + ---------- + title + The browser window title (defaults to the host URL of the page). Can also be set + as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + **kwargs + Attributes on the page level container. + """ + return RecallContextManager( + ui.page_fluid, + kwargs=dict( + title=title, + lang=lang, + **kwargs, + ), + ) + + +def page_fixed( + *, + title: Optional[str] = None, + lang: Optional[str] = None, + **kwargs: str, +) -> RecallContextManager[Tag]: + """ + Create a fixed page. + + This function wraps :func:`~shiny.ui.page_fixed`. + + Parameters + ---------- + title + The browser window title (defaults to the host URL of the page). Can also be set + as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + **kwargs + Attributes on the page level container. + """ + return RecallContextManager( + ui.page_fixed, + kwargs=dict( + title=title, + lang=lang, + **kwargs, + ), + ) + + +def page_fillable( + *, + padding: Optional[CssUnit | list[CssUnit]] = None, + gap: Optional[CssUnit] = None, + fillable_mobile: bool = False, + title: Optional[str] = None, + lang: Optional[str] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[Tag]: + """ + Creates a fillable page. + + This function wraps :func:`~shiny.ui.page_fillable`. + + Parameters + ---------- + padding + Padding to use for the body. See :func:`~shiny.ui.css_unit.as_css_padding` + for more details. + fillable_mobile + Whether or not the page should fill the viewport's height on mobile devices + (i.e., narrow windows). + gap + A CSS length unit passed through :func:`~shiny.ui.css_unit.as_css_unit` + defining the `gap` (i.e., spacing) between elements provided to `*args`. + title + The browser window title (defaults to the host URL of the page). Can also be set + as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + + """ + return RecallContextManager( + ui.page_fillable, + kwargs=dict( + padding=padding, + gap=gap, + fillable_mobile=fillable_mobile, + title=title, + lang=lang, + **kwargs, + ), + ) + + +def page_sidebar( + *, + title: Optional[str | Tag | TagList] = None, + fillable: bool = True, + fillable_mobile: bool = False, + window_title: str | MISSING_TYPE = MISSING, + lang: Optional[str] = None, + **kwargs: TagAttrValue, +) -> RecallContextManager[Tag]: + """ + Create a page with a sidebar and a title. + + This function wraps :func:`~shiny.ui.page_sidebar`. + + Parameters + ---------- + title + A title to display at the top of the page. + fillable + Whether or not the main content area should be considered a fillable + (i.e., flexbox) container. + fillable_mobile + Whether or not ``fillable`` should apply on mobile devices. + window_title + The browser's window title (defaults to the host URL of the page). Can also be + set as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + **kwargs + Additional attributes passed to :func:`~shiny.ui.layout_sidebar`. + """ + # sidebar + # Content to display in the sidebar. + + return RecallContextManager( + ui.page_sidebar, + kwargs=dict( + title=title, + fillable=fillable, + fillable_mobile=fillable_mobile, + window_title=window_title, + lang=lang, + **kwargs, + ), + ) + + +# TODO: Figure out sidebar arg for ui.page_navbar +def page_navbar( + *, + title: Optional[str | Tag | TagList] = None, + id: Optional[str] = None, + selected: Optional[str] = None, + fillable: bool | list[str] = True, + fillable_mobile: bool = False, + gap: Optional[CssUnit] = None, + padding: Optional[CssUnit | list[CssUnit]] = None, + position: Literal["static-top", "fixed-top", "fixed-bottom"] = "static-top", + header: Optional[TagChild] = None, + footer: Optional[TagChild] = None, + bg: Optional[str] = None, + inverse: bool = False, + underline: bool = True, + collapsible: bool = True, + fluid: bool = True, + window_title: str | MISSING_TYPE = MISSING, + lang: Optional[str] = None, +) -> RecallContextManager[Tag]: + """ + Create a page with a navbar and a title. + + This function wraps :func:`~shiny.ui.page_navbar`. + + Parameters + ---------- + title + The browser window title (defaults to the host URL of the page). Can also be set + as a side effect via :func:`~shiny.ui.panel_title`. + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + fillable + Whether or not the main content area should be considered a fillable + (i.e., flexbox) container. + fillable_mobile + Whether or not ``fillable`` should apply on mobile devices. + position + Determines whether the navbar should be displayed at the top of the page with + normal scrolling behavior ("static-top"), pinned at the top ("fixed-top"), or + pinned at the bottom ("fixed-bottom"). Note that using "fixed-top" or + "fixed-bottom" will cause the navbar to overlay your body content, unless you + add padding (e.g., ``tags.style("body {padding-top: 70px;}")``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + bg + Background color of the navbar (a CSS color). + inverse + Either ``True`` for a light text color or ``False`` for a dark text color. + collapsible + ``True`` to automatically collapse the elements into an expandable menu on mobile devices or narrow window widths. + fluid + ``True`` to use fluid layout; ``False`` to use fixed layout. + window_title + The browser's window title (defaults to the host URL of the page). Can also be + set as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + + See Also + ------- + * :func:`~shiny.ui.nav` + * :func:`~shiny.ui.nav_menu` + * :func:`~shiny.ui.navset_bar` + * :func:`~shiny.ui.page_fluid` + + Example + ------- + See :func:`~shiny.ui.nav`. + """ + + return RecallContextManager( + ui.page_navbar, + kwargs=dict( + title=title, + id=id, + selected=selected, + fillable=fillable, + fillable_mobile=fillable_mobile, + gap=gap, + padding=padding, + position=position, + header=header, + footer=footer, + bg=bg, + inverse=inverse, + underline=underline, + collapsible=collapsible, + fluid=fluid, + window_title=window_title, + lang=lang, + ), + ) diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 8a883d164..44e686b87 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -923,7 +923,7 @@ def navset_pill_list( If provided, will create an input value that holds the currently selected nav item. selected - Choose a particular nav item to select by default value (should match it's + Choose a particular nav item to select by default value (should match its ``value``). header UI to display above the selected content. diff --git a/tests/playwright/deploys/apps/shiny-express-accordion/app.py b/tests/playwright/deploys/apps/shiny-express-accordion/app.py index 62f95aa23..60d79fc2c 100644 --- a/tests/playwright/deploys/apps/shiny-express-accordion/app.py +++ b/tests/playwright/deploys/apps/shiny-express-accordion/app.py @@ -1,11 +1,11 @@ -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): - with layout.accordion_panel("Panel 1"): +with ui.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): + with ui.accordion_panel("Panel 1"): ui.input_slider("n", "N", 1, 100, 50) - with layout.accordion_panel("Panel 2"): + with ui.accordion_panel("Panel 2"): @render.text def txt(): diff --git a/tests/playwright/deploys/apps/shiny-express-page-default/app.py b/tests/playwright/deploys/apps/shiny-express-page-default/app.py index 6318139ef..de1577c1b 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-default/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-default/app.py @@ -1,5 +1,4 @@ -from shiny import ui -from shiny.express import layout +from shiny.express import ui ui.tags.style( """ @@ -7,41 +6,32 @@ background-color: #00000022} """ ) -with layout.div(id="shell"): - with layout.row(): - with layout.column(width=8): - with layout.row(): - "R1C1R1" - with layout.row(): - with layout.row(): - with layout.column(width=8): - with layout.row(): - "R1C1R2-R1C1R1" - with layout.row(): - "R1C1R2-R1C1R2" +with ui.div(id="shell"): + with ui.layout_columns(col_widths=[8, 4]): + "R1C1R1" + with ui.layout_columns(col_widths=[8, 4]): + with ui.div(): + ui.div("R1C1R2-R1C1R1") - with layout.column(width=4): - "R1C1R2-R1C2" + ui.div("R1C1R2-R1C1R2") - with layout.column(width=4): - "R1C2" + "R1C1R2-R1C2" -with layout.column(width=6): + "R1C2" + +with ui.layout_columns(col_widths=[6, 6]): # check height is below 300px - bounding box - with layout.navset_card(id="express_navset_card_tab", type="tab"): - with layout.nav_panel(title="Two"): + with ui.navset_card_tab(id="express_navset_card_tab"): + with ui.nav_panel(title="Two"): ... - -with layout.column(width=6): - with layout.row(): - with layout.navset(id="express_navset_tab", type="tab"): - for fn_txt, fn in [ - ("pre", layout.pre), - ("div", layout.div), - ("span", layout.span), - ]: - with layout.nav_panel(title=fn_txt): - for i in range(3): - with fn(): - ui.HTML(f"{fn_txt} {i}") + with ui.navset_tab(id="express_navset_tab"): + for fn_txt, fn in [ + ("pre", ui.pre), + ("div", ui.div), + ("span", ui.span), + ]: + with ui.nav_panel(title=fn_txt): + for i in range(3): + with fn(): + ui.HTML(f"{fn_txt} {i}") diff --git a/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py b/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py index ee9653a15..9c2013b04 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py @@ -1,9 +1,9 @@ -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -layout.set_page(layout.page_fillable()) +ui.set_page(ui.page_fillable()) -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py b/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py index 4d664501b..0422113b8 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py @@ -1,9 +1,9 @@ -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -layout.set_page(layout.page_fluid()) +ui.set_page(ui.page_fluid()) -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py index a20d6dd36..fac04a612 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py @@ -1,12 +1,12 @@ -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -layout.set_page(layout.page_sidebar(title="PageTitle")) +ui.set_page(ui.page_sidebar(title="PageTitle")) -with layout.sidebar(id="sidebar", title="SidebarTitle"): +with ui.sidebar(id="sidebar", title="SidebarTitle"): "Sidebar Content" -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/shiny/shiny-express/accordion/app.py b/tests/playwright/shiny/shiny-express/accordion/app.py index 62f95aa23..60d79fc2c 100644 --- a/tests/playwright/shiny/shiny-express/accordion/app.py +++ b/tests/playwright/shiny/shiny-express/accordion/app.py @@ -1,11 +1,11 @@ -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): - with layout.accordion_panel("Panel 1"): +with ui.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): + with ui.accordion_panel("Panel 1"): ui.input_slider("n", "N", 1, 100, 50) - with layout.accordion_panel("Panel 2"): + with ui.accordion_panel("Panel 2"): @render.text def txt(): diff --git a/tests/playwright/shiny/shiny-express/page_default/app.py b/tests/playwright/shiny/shiny-express/page_default/app.py index 6318139ef..de1577c1b 100644 --- a/tests/playwright/shiny/shiny-express/page_default/app.py +++ b/tests/playwright/shiny/shiny-express/page_default/app.py @@ -1,5 +1,4 @@ -from shiny import ui -from shiny.express import layout +from shiny.express import ui ui.tags.style( """ @@ -7,41 +6,32 @@ background-color: #00000022} """ ) -with layout.div(id="shell"): - with layout.row(): - with layout.column(width=8): - with layout.row(): - "R1C1R1" - with layout.row(): - with layout.row(): - with layout.column(width=8): - with layout.row(): - "R1C1R2-R1C1R1" - with layout.row(): - "R1C1R2-R1C1R2" +with ui.div(id="shell"): + with ui.layout_columns(col_widths=[8, 4]): + "R1C1R1" + with ui.layout_columns(col_widths=[8, 4]): + with ui.div(): + ui.div("R1C1R2-R1C1R1") - with layout.column(width=4): - "R1C1R2-R1C2" + ui.div("R1C1R2-R1C1R2") - with layout.column(width=4): - "R1C2" + "R1C1R2-R1C2" -with layout.column(width=6): + "R1C2" + +with ui.layout_columns(col_widths=[6, 6]): # check height is below 300px - bounding box - with layout.navset_card(id="express_navset_card_tab", type="tab"): - with layout.nav_panel(title="Two"): + with ui.navset_card_tab(id="express_navset_card_tab"): + with ui.nav_panel(title="Two"): ... - -with layout.column(width=6): - with layout.row(): - with layout.navset(id="express_navset_tab", type="tab"): - for fn_txt, fn in [ - ("pre", layout.pre), - ("div", layout.div), - ("span", layout.span), - ]: - with layout.nav_panel(title=fn_txt): - for i in range(3): - with fn(): - ui.HTML(f"{fn_txt} {i}") + with ui.navset_tab(id="express_navset_tab"): + for fn_txt, fn in [ + ("pre", ui.pre), + ("div", ui.div), + ("span", ui.span), + ]: + with ui.nav_panel(title=fn_txt): + for i in range(3): + with fn(): + ui.HTML(f"{fn_txt} {i}") diff --git a/tests/playwright/shiny/shiny-express/page_fillable/app.py b/tests/playwright/shiny/shiny-express/page_fillable/app.py index ee9653a15..9c2013b04 100644 --- a/tests/playwright/shiny/shiny-express/page_fillable/app.py +++ b/tests/playwright/shiny/shiny-express/page_fillable/app.py @@ -1,9 +1,9 @@ -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -layout.set_page(layout.page_fillable()) +ui.set_page(ui.page_fillable()) -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/shiny/shiny-express/page_fluid/app.py b/tests/playwright/shiny/shiny-express/page_fluid/app.py index 4d664501b..0422113b8 100644 --- a/tests/playwright/shiny/shiny-express/page_fluid/app.py +++ b/tests/playwright/shiny/shiny-express/page_fluid/app.py @@ -1,9 +1,9 @@ -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -layout.set_page(layout.page_fluid()) +ui.set_page(ui.page_fluid()) -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/shiny/shiny-express/page_sidebar/app.py b/tests/playwright/shiny/shiny-express/page_sidebar/app.py index a20d6dd36..fac04a612 100644 --- a/tests/playwright/shiny/shiny-express/page_sidebar/app.py +++ b/tests/playwright/shiny/shiny-express/page_sidebar/app.py @@ -1,12 +1,12 @@ -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -layout.set_page(layout.page_sidebar(title="PageTitle")) +ui.set_page(ui.page_sidebar(title="PageTitle")) -with layout.sidebar(id="sidebar", title="SidebarTitle"): +with ui.sidebar(id="sidebar", title="SidebarTitle"): "Sidebar Content" -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/utils/express_utils.py b/tests/playwright/utils/express_utils.py index 961500efa..5c039b6b5 100644 --- a/tests/playwright/utils/express_utils.py +++ b/tests/playwright/utils/express_utils.py @@ -6,7 +6,7 @@ from playwright.sync_api import Page from shiny import ui -from shiny.express import layout +from shiny.express import ui as xui def verify_express_accordion(page: Page) -> None: @@ -82,4 +82,4 @@ def verify_express_page_sidebar(page: Page) -> None: sidebar.expect_text("SidebarTitle Sidebar Content") output_txt = OutputTextVerbatim(page, "txt") output_txt.expect_value("50") - compare_annotations(ui.sidebar, layout.sidebar) + compare_annotations(ui.sidebar, xui.sidebar) diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index b06fb3972..ee99effdc 100644 --- a/tests/pytest/test_express_ui.py +++ b/tests/pytest/test_express_ui.py @@ -7,6 +7,38 @@ from shiny.express import output_args, suspend_display +def test_express_ui_is_complete(): + """ + Make sure shiny.express.ui covers everything that shiny.ui does, or explicitly lists + the item in _known_missing. + """ + + from shiny import ui + from shiny.express import ui as xui + + ui_all = set(ui.__all__) + xui_all = set(xui.__all__) + ui_known_missing = set(xui._known_missing["shiny.ui"]) + xui_known_missing = set(xui._known_missing["shiny.express.ui"]) + + # Make sure that there's no overlap between shiny.express.ui.__all__ and + # _known_missing["shiny.ui"]; and same for other combinations. Note that the use of + # `.intersection() == set()` instead of `disjoint()` is intentional, because if the + # test fails, the first form provides an error message that shows the difference, + # while the second form does note. + assert xui_all.intersection(ui_known_missing) == set() + assert ui_all.intersection(xui_known_missing) == set() + + # Similar to above, use .difference() instead of .issubset() to get better error + # messages. + assert xui_known_missing.difference(xui_all) == set() + assert ui_known_missing.difference(ui_all) == set() + + # Make sure that everything from shiny.ui is either exported by shiny.express.ui, or + # explicitly listed in shiny.express.ui._known_missing. + assert ui_all.union(xui_known_missing) == xui_all.union(ui_known_missing) + + def test_render_output_controls(): @render.text def text1():