22
33import warnings
44from dataclasses import dataclass , field
5- from typing import TYPE_CHECKING , Literal , Optional , Union , cast
5+ from typing import TYPE_CHECKING , Literal , Optional , cast
66
77from htmltools import (
88 HTML ,
5353* `"always"`: the sidebar is always open and cannot be closed
5454"""
5555
56- SidebarOpenValueMobile = Union [SidebarOpenValue , Literal ["always-above" ]]
57-
58- _always_open_mobile = ("always" , "always-above" )
59-
6056
6157class SidebarOpenSpec (TypedDict ):
6258 desktop : SidebarOpenValue
63- mobile : SidebarOpenValueMobile
59+ mobile : SidebarOpenValue | Literal [ "always-above" ]
6460
6561
6662@dataclass
@@ -72,7 +68,7 @@ class SidebarOpen:
7268 desktop : SidebarOpenValue = "open"
7369 """The initial state of the sidebar on desktop screen sizes."""
7470
75- mobile : SidebarOpenValueMobile = "closed"
71+ mobile : SidebarOpenValue | Literal [ "always-above" ] = "closed"
7672 """The initial state of the sidebar on mobile screen sizes."""
7773
7874 _VALUES : tuple [SidebarOpenValue , ...] = field (
@@ -82,28 +78,26 @@ class SidebarOpen:
8278 compare = False ,
8379 )
8480
85- @staticmethod
86- def _values_str (values : tuple [str , ...] | None = None ) -> str :
87- quoted = [f'"{ x } "' for x in (values or SidebarOpen ._VALUES )]
88- ret = ""
89- for i , value in enumerate (quoted ):
90- if i > 0 and len (quoted ) > 2 :
91- ret += ","
92- if i == len (quoted ) - 1 :
93- ret += " or"
94- if i > 0 :
95- ret += " "
96- ret += value
97- return ret
81+ _VALUES_MOBILE : tuple [SidebarOpenValue | Literal ["always-above" ], ...] = field (
82+ default = ("open" , "closed" , "always" , "always-above" ),
83+ repr = False ,
84+ hash = False ,
85+ compare = False ,
86+ )
9887
9988 def __post_init__ (self ):
10089 if self .desktop not in self ._VALUES :
101- raise ValueError (f"`desktop` must be one of { self ._values_str ()} " )
102- mobileValues = tuple (str (x ) for x in set ((* self ._VALUES , * _always_open_mobile )))
103- if self .mobile not in mobileValues :
104- raise ValueError (
105- f"`mobile` must be one of { self ._values_str (mobileValues )} "
106- )
90+ raise ValueError (f"`desktop` must be one of: { self ._VALUES } ." )
91+ if self .mobile not in self ._VALUES_MOBILE :
92+ raise ValueError (f"`mobile` must be one of: { self ._VALUES_MOBILE } ." )
93+
94+ def _is_always_open (
95+ self , on : Literal ["desktop" , "mobile" , "both" ] = "both"
96+ ) -> bool :
97+ desktop = self .desktop == "always"
98+ mobile = self .mobile in ("always" , "always-above" )
99+ switch = {"desktop" : desktop , "mobile" : mobile , "both" : desktop and mobile }
100+ return switch [on ]
107101
108102 @classmethod
109103 def _from_string (cls , open : str ) -> SidebarOpen :
@@ -130,7 +124,7 @@ def _from_string(cls, open: str) -> SidebarOpen:
130124 A :class:`~shiny.ui.SidebarOpen` object.
131125 """
132126 bad_value = ValueError (
133- f"`open` must be a non-empty string of one of { SidebarOpen ._values_str () } ."
127+ f"`open` must be a string matching one of: { SidebarOpen ._VALUES } ."
134128 )
135129
136130 if not isinstance (open , str ) or len (open ) == 0 :
@@ -155,7 +149,7 @@ def _as_open(
155149 return cls ._from_string (open )
156150
157151 raise ValueError (
158- f"""`open` must be one of { SidebarOpen ._values_str () } , """
152+ f"""`open` must be one of { SidebarOpen ._VALUES } , """
159153 + "or a dictionary with keys `desktop` and `mobile` using these values."
160154 )
161155
@@ -338,10 +332,10 @@ def open(
338332 def max_height_mobile (self ) -> Optional [str ]:
339333 max_height_mobile = self ._max_height_mobile
340334
341- if max_height_mobile is not None and self .open ().mobile in _always_open_mobile :
335+ if max_height_mobile is not None and not self .open ()._is_always_open ( " mobile" ) :
342336 warnings .warn (
343337 "The `shiny.ui.sidebar(max_height_mobile=)` argument only applies to "
344- + f "the sidebar when `open` is { SidebarOpen . _values_str ( _always_open_mobile ) } on mobile, but "
338+ + "the sidebar when `open` is 'always' or 'always-above' on mobile, but "
345339 + f"`open` is `'{ self .open ().mobile } '`. "
346340 + "The `max_height_mobile` argument will be ignored." ,
347341 # `stacklevel=2`: Refers to the caller of `.max_height_mobile` property method
@@ -369,11 +363,10 @@ def _as_open(
369363
370364 return SidebarOpen ._as_open (open )
371365
372- def _is_always_open (self ) -> bool :
373- return (
374- self .open ().desktop == "always"
375- and self .open ().mobile in _always_open_mobile
376- )
366+ def _is_always_open (
367+ self , on : Literal ["desktop" , "mobile" , "both" ] = "both"
368+ ) -> bool :
369+ return self .open ()._is_always_open (on )
377370
378371 def _get_sidebar_id (self ) -> Optional [str ]:
379372 """
@@ -689,10 +682,10 @@ def layout_sidebar(
689682 data_open_desktop = sidebar .open ().desktop ,
690683 data_open_mobile = sidebar .open ().mobile ,
691684 data_collapsible_mobile = (
692- "false" if sidebar .open ().mobile in _always_open_mobile else "true"
685+ "false" if sidebar .open ()._is_always_open ( " mobile" ) else "true"
693686 ),
694687 data_collapsible_desktop = (
695- "false" if sidebar .open ().desktop == "always" else "true"
688+ "false" if sidebar .open ()._is_always_open ( " desktop" ) else "true"
696689 ),
697690 data_bslib_sidebar_border = trinary (border ),
698691 data_bslib_sidebar_border_radius = trinary (border_radius ),
0 commit comments