diff --git a/CHANGELOG.md b/CHANGELOG.md index 25256d205..a22f3f3bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] +### New features + ### Improvements +* `selectize`, `remove_button`, and `options` parameters of `ui.input_select()` have been deprecated; use `ui.input_selectize()` instead. (Thanks, @ErdaradunGaztea!) (#1947) + * Improved the styling and readability of markdown tables rendered by `ui.Chat()` and `ui.MarkdownStream()`. (#1973) +### Bug fixes + + ## [1.4.0] - 2025-04-08 ## New features diff --git a/shiny/ui/_input_select.py b/shiny/ui/_input_select.py index 3a0ec75ba..4d144b3ca 100644 --- a/shiny/ui/_input_select.py +++ b/shiny/ui/_input_select.py @@ -2,7 +2,8 @@ from __future__ import annotations -from ..types import Jsonifiable +from .._deprecated import warn_deprecated +from ..types import DEPRECATED, MISSING_TYPE, Jsonifiable __all__ = ( "input_select", @@ -73,8 +74,9 @@ def input_selectize( An input label. choices Either a list of choices or a dictionary mapping choice values to labels. Note - that if a dictionary is provided, the keys are used as the (input) values so - that the dictionary values can hold HTML labels. A dictionary of dictionaries is + that if a dictionary is provided, the keys are used as the (input) values and + the values are labels displayed to the user. It is not recommended to use + anything other than a string for these labels. A dictionary of dictionaries is also supported, and in that case, the top-level keys are treated as ```` labels. selected @@ -113,7 +115,7 @@ def input_selectize( """ resolved_id = resolve_id(id) - x = input_select( + x = _input_select_impl( id=resolved_id, label=label, choices=restore_input(resolved_id, choices), @@ -136,11 +138,11 @@ def input_select( *, selected: Optional[str | list[str]] = None, multiple: bool = False, - selectize: bool = False, + selectize: bool | MISSING_TYPE = DEPRECATED, width: Optional[str] = None, size: Optional[str] = None, - remove_button: Optional[bool] = None, - options: Optional[dict[str, Jsonifiable | JSEval]] = None, + remove_button: Optional[bool] | MISSING_TYPE = DEPRECATED, + options: Optional[dict[str, Jsonifiable | JSEval]] | MISSING_TYPE = DEPRECATED, ) -> Tag: """ Create a select list that can be used to choose a single or multiple items from a @@ -154,8 +156,9 @@ def input_select( An input label. choices Either a list of choices or a dictionary mapping choice values to labels. Note - that if a dictionary is provided, the keys are used as the (input) values so - that the dictionary values can hold HTML labels. A dictionary of dictionaries is + that if a dictionary is provided, the keys are used as the (input) values and + the values are labels displayed to the user. It is not recommended to use + anything other than a string for these labels. A dictionary of dictionaries is also supported, and in that case, the top-level keys are treated as ```` labels. selected @@ -163,7 +166,7 @@ def input_select( multiple Is selection of multiple items allowed? selectize - Whether to use selectize.js or not. + Deprecated. Use ``input_selectize()`` instead of passing ``selectize=True``. width The CSS width, e.g. '400px', or '100%' size @@ -192,6 +195,61 @@ def input_select( * :func:`~shiny.ui.input_radio_buttons` * :func:`~shiny.ui.input_checkbox_group` """ + if isinstance(selectize, MISSING_TYPE): + selectize = False + else: + warn_deprecated( + "`selectize` parameter of `input_select()` is deprecated. " + "Use `input_selectize()` instead of passing `selectize=True`." + ) + + if isinstance(remove_button, MISSING_TYPE): + remove_button = None + else: + warn_deprecated( + "`remove_button` parameter of `input_select()` is deprecated. " + "Use `input_selectize()` instead." + ) + + if isinstance(options, MISSING_TYPE): + options = None + else: + warn_deprecated( + "`options` parameter of `input_select()` is deprecated. " + "Use `input_selectize()` instead." + ) + + resolved_id = resolve_id(id) + + x = _input_select_impl( + id=resolved_id, + label=label, + choices=choices, + selected=selected, + multiple=multiple, + selectize=selectize, + width=width, + size=size, + remove_button=remove_button, + options=options, + ) + + return x + + +def _input_select_impl( + id: str, + label: TagChild, + choices: SelectChoicesArg, + *, + selected: Optional[str | list[str]] = None, + multiple: bool = False, + selectize: bool = False, + width: Optional[str] = None, + size: Optional[str] = None, + remove_button: Optional[bool] = None, + options: Optional[dict[str, Jsonifiable | JSEval]] = None, +) -> Tag: if options is not None and selectize is False: raise Exception("Options can only be set when selectize is `True`.") @@ -201,6 +259,12 @@ def input_select( choices_ = _normalize_choices(choices) + if _contains_html(choices_): + warn_deprecated( + "Passing anything other than a string to `choices` parameter of " + "`input_select()` and `input_selectize()` is deprecated." + ) + selected = restore_input(resolved_id, selected) if selected is None and not multiple: selected = _find_first_option(choices_) @@ -282,6 +346,19 @@ def _normalize_choices(x: SelectChoicesArg) -> _SelectChoices: return x +def _contains_html(x: _SelectChoices) -> bool: + for v in x.values(): + if isinstance(v, Mapping): + # Check the `_Choices` values of `_OptGrpChoices` + for vv in v.values(): + if not isinstance(vv, str): + return True + else: + if not isinstance(v, str): + return True + return False + + def _render_choices( x: _SelectChoices, selected: Optional[str | list[str]] = None ) -> TagList: