nicegui-bootstrap-components 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nicegui_bootstrap_components/__init__.py +7 -0
- nicegui_bootstrap_components/_base.py +590 -0
- nicegui_bootstrap_components/_host.py +259 -0
- nicegui_bootstrap_components/assets.py +371 -0
- nicegui_bootstrap_components/bs/__init__.py +267 -0
- nicegui_bootstrap_components/bs/_actions.py +232 -0
- nicegui_bootstrap_components/bs/_alert_spinner.py +423 -0
- nicegui_bootstrap_components/bs/_carousel.py +312 -0
- nicegui_bootstrap_components/bs/_choices.py +779 -0
- nicegui_bootstrap_components/bs/_content.py +1343 -0
- nicegui_bootstrap_components/bs/_forms.py +378 -0
- nicegui_bootstrap_components/bs/_inputs.py +560 -0
- nicegui_bootstrap_components/bs/_layout.py +341 -0
- nicegui_bootstrap_components/bs/_nav.py +650 -0
- nicegui_bootstrap_components/bs/_navbar.py +445 -0
- nicegui_bootstrap_components/bs/_overlay_root.py +73 -0
- nicegui_bootstrap_components/bs/_overlays.py +2633 -0
- nicegui_bootstrap_components/bs/_scope.py +28 -0
- nicegui_bootstrap_components/bs/_tabs.py +605 -0
- nicegui_bootstrap_components/bs/_toast.py +137 -0
- nicegui_bootstrap_components/bs/_tooltip_popover.py +537 -0
- nicegui_bootstrap_components/contracts/dbc-2.0.4.json +16811 -0
- nicegui_bootstrap_components/dbc/__init__.py +108 -0
- nicegui_bootstrap_components/icons.py +29 -0
- nicegui_bootstrap_components/persist.py +201 -0
- nicegui_bootstrap_components/py.typed +0 -0
- nicegui_bootstrap_components/static/dist/fonts/bootstrap-icons.woff +0 -0
- nicegui_bootstrap_components/static/dist/fonts/bootstrap-icons.woff2 +0 -0
- nicegui_bootstrap_components/static/dist/ngbs-bootstrap-unscoped.css +12047 -0
- nicegui_bootstrap_components/static/dist/ngbs-bootstrap.css +12057 -0
- nicegui_bootstrap_components/static/dist/ngbs-cerulean-unscoped.css +12338 -0
- nicegui_bootstrap_components/static/dist/ngbs-cerulean.css +12348 -0
- nicegui_bootstrap_components/static/dist/ngbs-compat-unscoped.css +12 -0
- nicegui_bootstrap_components/static/dist/ngbs-compat.css +12 -0
- nicegui_bootstrap_components/static/dist/ngbs-cosmo-unscoped.css +11825 -0
- nicegui_bootstrap_components/static/dist/ngbs-cosmo.css +11835 -0
- nicegui_bootstrap_components/static/dist/ngbs-cyborg-unscoped.css +12213 -0
- nicegui_bootstrap_components/static/dist/ngbs-cyborg.css +12223 -0
- nicegui_bootstrap_components/static/dist/ngbs-darkly-unscoped.css +12143 -0
- nicegui_bootstrap_components/static/dist/ngbs-darkly.css +12153 -0
- nicegui_bootstrap_components/static/dist/ngbs-flatly-unscoped.css +12129 -0
- nicegui_bootstrap_components/static/dist/ngbs-flatly.css +12139 -0
- nicegui_bootstrap_components/static/dist/ngbs-grid-unscoped.css +4084 -0
- nicegui_bootstrap_components/static/dist/ngbs-grid.css +4094 -0
- nicegui_bootstrap_components/static/dist/ngbs-host-unscoped.css +9 -0
- nicegui_bootstrap_components/static/dist/ngbs-host.css +9 -0
- nicegui_bootstrap_components/static/dist/ngbs-icons-bootstrap.css +5 -0
- nicegui_bootstrap_components/static/dist/ngbs-icons-fontawesome.css +9 -0
- nicegui_bootstrap_components/static/dist/ngbs-journal-unscoped.css +12112 -0
- nicegui_bootstrap_components/static/dist/ngbs-journal.css +12122 -0
- nicegui_bootstrap_components/static/dist/ngbs-litera-unscoped.css +12214 -0
- nicegui_bootstrap_components/static/dist/ngbs-litera.css +12224 -0
- nicegui_bootstrap_components/static/dist/ngbs-lumen-unscoped.css +12377 -0
- nicegui_bootstrap_components/static/dist/ngbs-lumen.css +12387 -0
- nicegui_bootstrap_components/static/dist/ngbs-lux-unscoped.css +12048 -0
- nicegui_bootstrap_components/static/dist/ngbs-lux.css +12058 -0
- nicegui_bootstrap_components/static/dist/ngbs-materia-unscoped.css +13197 -0
- nicegui_bootstrap_components/static/dist/ngbs-materia.css +13207 -0
- nicegui_bootstrap_components/static/dist/ngbs-minty-unscoped.css +12184 -0
- nicegui_bootstrap_components/static/dist/ngbs-minty.css +12194 -0
- nicegui_bootstrap_components/static/dist/ngbs-morph-unscoped.css +12829 -0
- nicegui_bootstrap_components/static/dist/ngbs-morph.css +12839 -0
- nicegui_bootstrap_components/static/dist/ngbs-pulse-unscoped.css +11901 -0
- nicegui_bootstrap_components/static/dist/ngbs-pulse.css +11911 -0
- nicegui_bootstrap_components/static/dist/ngbs-quartz-unscoped.css +12625 -0
- nicegui_bootstrap_components/static/dist/ngbs-quartz.css +12635 -0
- nicegui_bootstrap_components/static/dist/ngbs-sandstone-unscoped.css +12214 -0
- nicegui_bootstrap_components/static/dist/ngbs-sandstone.css +12224 -0
- nicegui_bootstrap_components/static/dist/ngbs-simplex-unscoped.css +12199 -0
- nicegui_bootstrap_components/static/dist/ngbs-simplex.css +12209 -0
- nicegui_bootstrap_components/static/dist/ngbs-sketchy-unscoped.css +12518 -0
- nicegui_bootstrap_components/static/dist/ngbs-sketchy.css +12528 -0
- nicegui_bootstrap_components/static/dist/ngbs-slate-unscoped.css +12527 -0
- nicegui_bootstrap_components/static/dist/ngbs-slate.css +12537 -0
- nicegui_bootstrap_components/static/dist/ngbs-solar-unscoped.css +12152 -0
- nicegui_bootstrap_components/static/dist/ngbs-solar.css +12162 -0
- nicegui_bootstrap_components/static/dist/ngbs-spacelab-unscoped.css +12279 -0
- nicegui_bootstrap_components/static/dist/ngbs-spacelab.css +12289 -0
- nicegui_bootstrap_components/static/dist/ngbs-superhero-unscoped.css +12223 -0
- nicegui_bootstrap_components/static/dist/ngbs-superhero.css +12233 -0
- nicegui_bootstrap_components/static/dist/ngbs-united-unscoped.css +12090 -0
- nicegui_bootstrap_components/static/dist/ngbs-united.css +12100 -0
- nicegui_bootstrap_components/static/dist/ngbs-vapor-unscoped.css +12556 -0
- nicegui_bootstrap_components/static/dist/ngbs-vapor.css +12566 -0
- nicegui_bootstrap_components/static/dist/ngbs-yeti-unscoped.css +12333 -0
- nicegui_bootstrap_components/static/dist/ngbs-yeti.css +12343 -0
- nicegui_bootstrap_components/static/dist/ngbs-zephyr-unscoped.css +12296 -0
- nicegui_bootstrap_components/static/dist/ngbs-zephyr.css +12306 -0
- nicegui_bootstrap_components/static/prototype-ngbs.css +541 -0
- nicegui_bootstrap_components/static/webfonts/fa-brands-400.ttf +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-brands-400.woff2 +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-regular-400.ttf +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-regular-400.woff2 +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-solid-900.ttf +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-solid-900.woff2 +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-v4compatibility.ttf +0 -0
- nicegui_bootstrap_components/static/webfonts/fa-v4compatibility.woff2 +0 -0
- nicegui_bootstrap_components/theme.py +238 -0
- nicegui_bootstrap_components/themes.py +131 -0
- nicegui_bootstrap_components-0.1.0.dist-info/METADATA +234 -0
- nicegui_bootstrap_components-0.1.0.dist-info/RECORD +103 -0
- nicegui_bootstrap_components-0.1.0.dist-info/WHEEL +4 -0
- nicegui_bootstrap_components-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Native ``bs.scope`` island wrapper (Mode B Reboot / selector anchor)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .._base import BootstrapElement
|
|
8
|
+
|
|
9
|
+
__all__ = ["Scope", "scope"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Scope(BootstrapElement):
|
|
13
|
+
"""Mode B island / Reboot scope-anchor. Tag ``div``, class ``ngbs``.
|
|
14
|
+
|
|
15
|
+
Individual components do not carry ``ngbs``; Bootstrap selectors are rewritten
|
|
16
|
+
against this ancestor (and the overlay root).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
component_name = "Scope"
|
|
20
|
+
children_kind = "auto"
|
|
21
|
+
|
|
22
|
+
def __init__(self, children: object = None, **props: Any) -> None:
|
|
23
|
+
self._surface = "native"
|
|
24
|
+
self._structural_classes = ("ngbs",)
|
|
25
|
+
super().__init__(children, tag="div", **props)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
scope = Scope
|
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"""Tabs family: Tabs and Tab."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
|
+
from functools import partial
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from nicegui import ui
|
|
10
|
+
from nicegui.element import Element
|
|
11
|
+
|
|
12
|
+
from .._base import (
|
|
13
|
+
BootstrapElement,
|
|
14
|
+
ChildrenError,
|
|
15
|
+
UnsupportedPropError,
|
|
16
|
+
make_surface_classes,
|
|
17
|
+
normalize_style,
|
|
18
|
+
resolve_class_name,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Tabs",
|
|
23
|
+
"DbcTabs",
|
|
24
|
+
"tabs",
|
|
25
|
+
"Tab",
|
|
26
|
+
"DbcTab",
|
|
27
|
+
"tab",
|
|
28
|
+
"assign_tab_ids",
|
|
29
|
+
"first_enabled_tab_id",
|
|
30
|
+
"tab_button_aria",
|
|
31
|
+
"tab_link_classes",
|
|
32
|
+
"tab_pane_aria",
|
|
33
|
+
"tab_pane_classes",
|
|
34
|
+
"tabs_card_body_classes",
|
|
35
|
+
"tabs_card_header_classes",
|
|
36
|
+
"tabs_content_classes",
|
|
37
|
+
"tabs_nav_classes",
|
|
38
|
+
"tabs_nav_item_classes",
|
|
39
|
+
"tabs_structural_classes",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
_PERSIST_VALUES = ("off", "local", "session", "memory")
|
|
43
|
+
_ACTIVE_TAB_MEMORY: dict[str, str] = {}
|
|
44
|
+
|
|
45
|
+
_TABLIST_KEYDOWN = """(e) => {
|
|
46
|
+
const key = e.key;
|
|
47
|
+
if (key !== 'ArrowLeft' && key !== 'ArrowRight' && key !== 'Home' && key !== 'End') return;
|
|
48
|
+
const root = e.currentTarget;
|
|
49
|
+
if (!root || !root.querySelectorAll) return;
|
|
50
|
+
const tabs = Array.from(root.querySelectorAll('[role="tab"]')).filter((el) => {
|
|
51
|
+
return !el.disabled && el.getAttribute('aria-disabled') !== 'true'
|
|
52
|
+
&& !el.classList.contains('disabled');
|
|
53
|
+
});
|
|
54
|
+
if (!tabs.length) return;
|
|
55
|
+
let idx = tabs.indexOf(document.activeElement);
|
|
56
|
+
if (key === 'Home') idx = 0;
|
|
57
|
+
else if (key === 'End') idx = tabs.length - 1;
|
|
58
|
+
else if (key === 'ArrowLeft') idx = idx <= 0 ? tabs.length - 1 : idx - 1;
|
|
59
|
+
else if (key === 'ArrowRight') idx = (idx + 1) % tabs.length;
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
const next = tabs[idx];
|
|
62
|
+
if (next) { next.focus(); next.click(); }
|
|
63
|
+
}"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def tabs_structural_classes(*, card: bool = False) -> list[str]:
|
|
67
|
+
"""Return structural classes for the Tabs root."""
|
|
68
|
+
if card:
|
|
69
|
+
return ["card"]
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def tabs_nav_classes(*, card: bool = False) -> list[str]:
|
|
74
|
+
"""Return Bootstrap ``nav nav-tabs`` classes, with card-header-tabs when card."""
|
|
75
|
+
classes = ["nav", "nav-tabs"]
|
|
76
|
+
if card:
|
|
77
|
+
classes.append("card-header-tabs")
|
|
78
|
+
return classes
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def tabs_nav_item_classes() -> list[str]:
|
|
82
|
+
"""Return Bootstrap classes for a tab list item."""
|
|
83
|
+
return ["nav-item"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def tabs_card_header_classes() -> list[str]:
|
|
87
|
+
"""Return Bootstrap card header classes for card tabs."""
|
|
88
|
+
return ["card-header"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def tabs_card_body_classes() -> list[str]:
|
|
92
|
+
"""Return Bootstrap card body classes for card tabs."""
|
|
93
|
+
return ["card-body"]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def tabs_content_classes() -> list[str]:
|
|
97
|
+
"""Return Bootstrap tab content wrapper classes."""
|
|
98
|
+
return ["tab-content"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def tab_link_classes(*, active: bool = False, disabled: bool = False) -> list[str]:
|
|
102
|
+
"""Return Bootstrap ``nav-link`` classes for a tab button."""
|
|
103
|
+
classes = ["nav-link"]
|
|
104
|
+
if active:
|
|
105
|
+
classes.append("active")
|
|
106
|
+
if disabled:
|
|
107
|
+
classes.append("disabled")
|
|
108
|
+
return classes
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def tab_pane_classes(*, active: bool = False) -> list[str]:
|
|
112
|
+
"""Return Bootstrap ``tab-pane`` classes."""
|
|
113
|
+
classes = ["tab-pane"]
|
|
114
|
+
if active:
|
|
115
|
+
classes.append("active")
|
|
116
|
+
return classes
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def assign_tab_ids(tab_ids: Sequence[str | None]) -> list[str]:
|
|
120
|
+
"""Return stable tab ids for the given child order.
|
|
121
|
+
|
|
122
|
+
Missing ids become ``tab-0``, ``tab-1``, ... using the child index when free,
|
|
123
|
+
then the lowest unused ``tab-n`` if that index collides with an explicit id.
|
|
124
|
+
Existing ids are preserved.
|
|
125
|
+
"""
|
|
126
|
+
used = {item for item in tab_ids if item}
|
|
127
|
+
result: list[str] = []
|
|
128
|
+
for index, current in enumerate(tab_ids):
|
|
129
|
+
if current:
|
|
130
|
+
result.append(current)
|
|
131
|
+
continue
|
|
132
|
+
candidate = f"tab-{index}"
|
|
133
|
+
if candidate in used:
|
|
134
|
+
n = 0
|
|
135
|
+
while f"tab-{n}" in used:
|
|
136
|
+
n += 1
|
|
137
|
+
candidate = f"tab-{n}"
|
|
138
|
+
used.add(candidate)
|
|
139
|
+
result.append(candidate)
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def first_enabled_tab_id(tab_ids: Sequence[str], disabled: Sequence[bool]) -> str | None:
|
|
144
|
+
"""Return the first tab id whose matching disabled flag is false."""
|
|
145
|
+
for tab_id, is_disabled in zip(tab_ids, disabled, strict=True):
|
|
146
|
+
if not is_disabled:
|
|
147
|
+
return tab_id
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def tab_button_aria(
|
|
152
|
+
*,
|
|
153
|
+
tab_id: str,
|
|
154
|
+
pane_id: str,
|
|
155
|
+
selected: bool,
|
|
156
|
+
disabled: bool = False,
|
|
157
|
+
) -> dict[str, str]:
|
|
158
|
+
"""Return ARIA attributes for a tab button."""
|
|
159
|
+
attrs = {
|
|
160
|
+
"role": "tab",
|
|
161
|
+
"id": f"{tab_id}-tab",
|
|
162
|
+
"aria-controls": pane_id,
|
|
163
|
+
"aria-selected": "true" if selected else "false",
|
|
164
|
+
"tabindex": "0" if selected and not disabled else "-1",
|
|
165
|
+
}
|
|
166
|
+
if disabled:
|
|
167
|
+
attrs["aria-disabled"] = "true"
|
|
168
|
+
return attrs
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def tab_pane_aria(*, tab_id: str, pane_id: str, selected: bool) -> dict[str, str]:
|
|
172
|
+
"""Return ARIA attributes for a tab pane."""
|
|
173
|
+
return {
|
|
174
|
+
"role": "tabpanel",
|
|
175
|
+
"id": pane_id,
|
|
176
|
+
"aria-labelledby": f"{tab_id}-tab",
|
|
177
|
+
"aria-hidden": "false" if selected else "true",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _is_tab_element(value: object) -> bool:
|
|
182
|
+
return getattr(type(value), "component_name", None) == "Tab"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _element_children(element: Any) -> list[Any]:
|
|
186
|
+
slot = getattr(element, "default_slot", None)
|
|
187
|
+
if slot is None:
|
|
188
|
+
return []
|
|
189
|
+
children = getattr(slot, "children", None)
|
|
190
|
+
if not children:
|
|
191
|
+
return []
|
|
192
|
+
return list(children)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _coerce_tab_children(children: object) -> list[Any]:
|
|
196
|
+
if children is None:
|
|
197
|
+
return []
|
|
198
|
+
if _is_tab_element(children):
|
|
199
|
+
return [children]
|
|
200
|
+
if isinstance(children, (str, bytes, bytearray)):
|
|
201
|
+
raise ChildrenError("Tabs does not accept text children; wrap text in a Tab component.")
|
|
202
|
+
if isinstance(children, Sequence):
|
|
203
|
+
items = list(children)
|
|
204
|
+
else:
|
|
205
|
+
raise ChildrenError(
|
|
206
|
+
"Tabs only accepts Tab children; wrap other content in a Tab component."
|
|
207
|
+
)
|
|
208
|
+
result: list[Any] = []
|
|
209
|
+
for child in items:
|
|
210
|
+
if isinstance(child, str):
|
|
211
|
+
raise ChildrenError("Tabs does not accept text children; wrap text in a Tab component.")
|
|
212
|
+
if not _is_tab_element(child):
|
|
213
|
+
raise ChildrenError(
|
|
214
|
+
"Tabs only accepts Tab children; wrap other content in a Tab component."
|
|
215
|
+
)
|
|
216
|
+
result.append(child)
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _set_class(element: Any, name: str, present: bool) -> None:
|
|
221
|
+
if present:
|
|
222
|
+
element.classes(add=name)
|
|
223
|
+
else:
|
|
224
|
+
element.classes(remove=name)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _apply_style(element: Any, style: dict[str, Any] | str | None) -> None:
|
|
228
|
+
if style is None:
|
|
229
|
+
return
|
|
230
|
+
if isinstance(style, dict):
|
|
231
|
+
element.style(normalize_style(style))
|
|
232
|
+
return
|
|
233
|
+
element.style(str(style))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _pane_dom_id(tab: Any) -> str:
|
|
237
|
+
current = tab._props.get("id")
|
|
238
|
+
if isinstance(current, str) and current:
|
|
239
|
+
return current
|
|
240
|
+
assigned = tab.tab_id or "tab"
|
|
241
|
+
tab._props["id"] = assigned
|
|
242
|
+
return assigned
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class _TabImpl(BootstrapElement):
|
|
246
|
+
"""Private Tab implementation. Renders as a pane; the parent Tabs owns the tab button."""
|
|
247
|
+
|
|
248
|
+
component_name = "Tab"
|
|
249
|
+
children_kind = "auto"
|
|
250
|
+
reserved_classes = ("tab-pane",)
|
|
251
|
+
|
|
252
|
+
def __init__(
|
|
253
|
+
self,
|
|
254
|
+
children: object = None,
|
|
255
|
+
*,
|
|
256
|
+
_surface: str,
|
|
257
|
+
label: str | None = None,
|
|
258
|
+
tab_id: str | None = None,
|
|
259
|
+
disabled: bool = False,
|
|
260
|
+
label_class_name: str | None = None,
|
|
261
|
+
label_style: dict[str, Any] | str | None = None,
|
|
262
|
+
**kwargs: Any,
|
|
263
|
+
) -> None:
|
|
264
|
+
self._surface = _surface
|
|
265
|
+
label_class_alias = kwargs.pop("labelClassName", None)
|
|
266
|
+
self._label_class_name = resolve_class_name(
|
|
267
|
+
label_class_name, label_class_alias, self._surface
|
|
268
|
+
)
|
|
269
|
+
tab_class_name = kwargs.pop("tab_class_name", None)
|
|
270
|
+
tab_class_alias = kwargs.pop("tabClassName", None)
|
|
271
|
+
self._tab_class_name = resolve_class_name(tab_class_name, tab_class_alias, self._surface)
|
|
272
|
+
self._tab_style = kwargs.pop("tab_style", None)
|
|
273
|
+
kwargs.pop("tabStyle", None)
|
|
274
|
+
for extra in (
|
|
275
|
+
"key",
|
|
276
|
+
"loading_state",
|
|
277
|
+
"active_label_style",
|
|
278
|
+
"active_label_class_name",
|
|
279
|
+
"active_labelClassName",
|
|
280
|
+
"active_tab_class_name",
|
|
281
|
+
"active_tab_style",
|
|
282
|
+
"active_tabClassName",
|
|
283
|
+
"active_tab_className",
|
|
284
|
+
):
|
|
285
|
+
kwargs.pop(extra, None)
|
|
286
|
+
self._label = label
|
|
287
|
+
self._explicit_tab_id = tab_id if tab_id else None
|
|
288
|
+
self._tab_id = self._explicit_tab_id
|
|
289
|
+
self._disabled = bool(disabled)
|
|
290
|
+
self._label_style = label_style
|
|
291
|
+
self._structural_classes = tuple(tab_pane_classes(active=False))
|
|
292
|
+
super().__init__(children, tag="div", **kwargs)
|
|
293
|
+
self._props["role"] = "tabpanel"
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def tab_id(self) -> str | None:
|
|
297
|
+
return self._tab_id
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def label(self) -> str | None:
|
|
301
|
+
return self._label
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def disabled(self) -> bool:
|
|
305
|
+
return self._disabled
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class _TabsImpl(BootstrapElement):
|
|
309
|
+
"""Private Tabs implementation. Tab children become a tablist plus panes."""
|
|
310
|
+
|
|
311
|
+
component_name = "Tabs"
|
|
312
|
+
children_kind = "grouping"
|
|
313
|
+
reserved_classes: tuple[str, ...] = ()
|
|
314
|
+
|
|
315
|
+
def __init__(
|
|
316
|
+
self,
|
|
317
|
+
children: object = None,
|
|
318
|
+
*,
|
|
319
|
+
_surface: str,
|
|
320
|
+
active_tab: str | None = None,
|
|
321
|
+
card: bool = False,
|
|
322
|
+
**kwargs: Any,
|
|
323
|
+
) -> None:
|
|
324
|
+
self._surface = _surface
|
|
325
|
+
persist, lazy, on_change = self._consume_surface_kwargs(kwargs)
|
|
326
|
+
parsed = _coerce_tab_children(children)
|
|
327
|
+
self._card = bool(card)
|
|
328
|
+
self._lazy = lazy
|
|
329
|
+
self._persist = persist
|
|
330
|
+
self._on_change = on_change
|
|
331
|
+
self._requested_active = active_tab
|
|
332
|
+
self._active_tab = active_tab
|
|
333
|
+
self._tabs: list[Any] = []
|
|
334
|
+
self._buttons: dict[str, Element] = {}
|
|
335
|
+
self._nav_items: dict[str, Element] = {}
|
|
336
|
+
self._lazy_held: list[Any] = []
|
|
337
|
+
self._header: Element | None = None
|
|
338
|
+
self._body: Element | None = None
|
|
339
|
+
self._structural_classes = tuple(tabs_structural_classes(card=self._card))
|
|
340
|
+
super().__init__(None, tag="div", **kwargs)
|
|
341
|
+
self._build_chrome()
|
|
342
|
+
self._tabs.extend(parsed)
|
|
343
|
+
assigned = assign_tab_ids([tab._explicit_tab_id for tab in self._tabs])
|
|
344
|
+
for tab, tab_id in zip(self._tabs, assigned, strict=True):
|
|
345
|
+
tab._tab_id = tab_id
|
|
346
|
+
self._restore_persist()
|
|
347
|
+
self._ensure_active()
|
|
348
|
+
for tab in self._tabs:
|
|
349
|
+
self._mount_pane(tab)
|
|
350
|
+
self._create_nav_item(tab)
|
|
351
|
+
self._apply_active_classes()
|
|
352
|
+
self._persist_save()
|
|
353
|
+
|
|
354
|
+
def _consume_surface_kwargs(
|
|
355
|
+
self,
|
|
356
|
+
kwargs: dict[str, Any],
|
|
357
|
+
) -> tuple[str, bool, Callable[..., Any] | None]:
|
|
358
|
+
if self._surface == "compat":
|
|
359
|
+
for native_only in ("lazy", "on_change", "persist"):
|
|
360
|
+
if native_only in kwargs:
|
|
361
|
+
raise UnsupportedPropError(native_only)
|
|
362
|
+
persistence = kwargs.pop("persistence", None)
|
|
363
|
+
kwargs.pop("persisted_props", None)
|
|
364
|
+
persistence_type = kwargs.pop("persistence_type", None)
|
|
365
|
+
kwargs.pop("key", None)
|
|
366
|
+
kwargs.pop("loading_state", None)
|
|
367
|
+
persist = "off"
|
|
368
|
+
if persistence:
|
|
369
|
+
if persistence_type in {"local", "session", "memory"}:
|
|
370
|
+
persist = str(persistence_type)
|
|
371
|
+
else:
|
|
372
|
+
persist = "local"
|
|
373
|
+
return persist, False, None
|
|
374
|
+
persist = kwargs.pop("persist", "off")
|
|
375
|
+
if persist not in _PERSIST_VALUES:
|
|
376
|
+
allowed = ", ".join(repr(item) for item in _PERSIST_VALUES)
|
|
377
|
+
raise ValueError(f"persist must be one of {allowed}, got {persist!r}")
|
|
378
|
+
lazy = bool(kwargs.pop("lazy", False))
|
|
379
|
+
on_change = kwargs.pop("on_change", None)
|
|
380
|
+
return str(persist), lazy, on_change
|
|
381
|
+
|
|
382
|
+
def _build_chrome(self) -> None:
|
|
383
|
+
with self:
|
|
384
|
+
if self._card:
|
|
385
|
+
self._header = ui.element("div")
|
|
386
|
+
self._header.classes(" ".join(tabs_card_header_classes()))
|
|
387
|
+
with self._header:
|
|
388
|
+
self._nav = ui.element("ul")
|
|
389
|
+
self._nav.classes(" ".join(tabs_nav_classes(card=True)))
|
|
390
|
+
self._body = ui.element("div")
|
|
391
|
+
self._body.classes(" ".join(tabs_card_body_classes()))
|
|
392
|
+
with self._body:
|
|
393
|
+
self._content = ui.element("div")
|
|
394
|
+
self._content.classes(" ".join(tabs_content_classes()))
|
|
395
|
+
else:
|
|
396
|
+
self._nav = ui.element("ul")
|
|
397
|
+
self._nav.classes(" ".join(tabs_nav_classes(card=False)))
|
|
398
|
+
self._content = ui.element("div")
|
|
399
|
+
self._content.classes(" ".join(tabs_content_classes()))
|
|
400
|
+
self._nav._props["role"] = "tablist"
|
|
401
|
+
self._nav.on("keydown", self._on_tablist_keydown, js_handler=_TABLIST_KEYDOWN)
|
|
402
|
+
|
|
403
|
+
def _on_tablist_keydown(self, *_args: Any, **_kwargs: Any) -> None:
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
def __exit__(self, *args: Any) -> Any:
|
|
407
|
+
result = super().__exit__(*args)
|
|
408
|
+
self._sync_tabs()
|
|
409
|
+
return result
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def active_tab(self) -> str | None:
|
|
413
|
+
self._sync_tabs()
|
|
414
|
+
return self._active_tab
|
|
415
|
+
|
|
416
|
+
@active_tab.setter
|
|
417
|
+
def active_tab(self, value: str | None) -> None:
|
|
418
|
+
self._sync_tabs()
|
|
419
|
+
self._requested_active = value
|
|
420
|
+
self._active_tab = value
|
|
421
|
+
if value is None:
|
|
422
|
+
self._ensure_active()
|
|
423
|
+
self._apply_active_classes()
|
|
424
|
+
self._persist_save()
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def tab_ids(self) -> list[str]:
|
|
428
|
+
self._sync_tabs()
|
|
429
|
+
return [tab.tab_id for tab in self._tabs if tab.tab_id is not None]
|
|
430
|
+
|
|
431
|
+
def _persist_key(self) -> str | None:
|
|
432
|
+
public = self._props.get("id")
|
|
433
|
+
if isinstance(public, str) and public:
|
|
434
|
+
return public
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
def _restore_persist(self) -> None:
|
|
438
|
+
if self._persist == "off" or self._requested_active is not None:
|
|
439
|
+
return
|
|
440
|
+
key = self._persist_key()
|
|
441
|
+
if key is None:
|
|
442
|
+
return
|
|
443
|
+
remembered = _ACTIVE_TAB_MEMORY.get(key)
|
|
444
|
+
known = {tab.tab_id for tab in self._tabs}
|
|
445
|
+
if remembered and remembered in known:
|
|
446
|
+
self._active_tab = remembered
|
|
447
|
+
|
|
448
|
+
def _persist_save(self) -> None:
|
|
449
|
+
if self._persist == "off" or self._active_tab is None:
|
|
450
|
+
return
|
|
451
|
+
key = self._persist_key()
|
|
452
|
+
if key is not None:
|
|
453
|
+
_ACTIVE_TAB_MEMORY[key] = self._active_tab
|
|
454
|
+
|
|
455
|
+
def _ensure_active(self) -> None:
|
|
456
|
+
ids = [tab.tab_id for tab in self._tabs if tab.tab_id is not None]
|
|
457
|
+
disabled = [tab.disabled for tab in self._tabs]
|
|
458
|
+
if self._active_tab is not None and self._active_tab in ids:
|
|
459
|
+
return
|
|
460
|
+
self._active_tab = first_enabled_tab_id(ids, disabled)
|
|
461
|
+
|
|
462
|
+
def _find_tab(self, tab_id: str) -> Any | None:
|
|
463
|
+
for tab in self._tabs:
|
|
464
|
+
if tab.tab_id == tab_id:
|
|
465
|
+
return tab
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
def _mount_pane(self, tab: Any) -> None:
|
|
469
|
+
selected = tab.tab_id == self._active_tab
|
|
470
|
+
if self._lazy and not selected:
|
|
471
|
+
if tab not in self._lazy_held:
|
|
472
|
+
self._lazy_held.append(tab)
|
|
473
|
+
return
|
|
474
|
+
if tab in self._lazy_held:
|
|
475
|
+
self._lazy_held.remove(tab)
|
|
476
|
+
tab.move(self._content)
|
|
477
|
+
_set_class(tab, "active", selected)
|
|
478
|
+
|
|
479
|
+
def _create_nav_item(self, tab: Any) -> None:
|
|
480
|
+
tab_id = tab.tab_id
|
|
481
|
+
if tab_id is None or tab_id in self._buttons:
|
|
482
|
+
return
|
|
483
|
+
selected = tab_id == self._active_tab
|
|
484
|
+
with self._nav:
|
|
485
|
+
item = ui.element("li")
|
|
486
|
+
item.classes(" ".join(tabs_nav_item_classes()))
|
|
487
|
+
item._props["role"] = "presentation"
|
|
488
|
+
with item:
|
|
489
|
+
btn = ui.element("button")
|
|
490
|
+
btn.classes(" ".join(tab_link_classes(active=selected, disabled=tab.disabled)))
|
|
491
|
+
btn._props["type"] = "button"
|
|
492
|
+
if tab._tab_class_name:
|
|
493
|
+
btn.classes(tab._tab_class_name)
|
|
494
|
+
if tab._label_class_name:
|
|
495
|
+
btn.classes(tab._label_class_name)
|
|
496
|
+
_apply_style(btn, tab._tab_style)
|
|
497
|
+
_apply_style(btn, tab._label_style)
|
|
498
|
+
if tab.disabled:
|
|
499
|
+
btn._props["disabled"] = True
|
|
500
|
+
with btn:
|
|
501
|
+
ui.label(tab.label or "")
|
|
502
|
+
btn.on("click", partial(self._on_tab_click, tab_id))
|
|
503
|
+
self._nav_items[tab_id] = item
|
|
504
|
+
self._buttons[tab_id] = btn
|
|
505
|
+
|
|
506
|
+
def _on_tab_click(self, tab_id: str, *_args: Any, **_kwargs: Any) -> None:
|
|
507
|
+
target = self._find_tab(tab_id)
|
|
508
|
+
if target is None or target.disabled:
|
|
509
|
+
return
|
|
510
|
+
self._activate(tab_id, from_user=True)
|
|
511
|
+
|
|
512
|
+
def _activate(self, tab_id: str, *, from_user: bool) -> None:
|
|
513
|
+
if tab_id == self._active_tab:
|
|
514
|
+
return
|
|
515
|
+
self._active_tab = tab_id
|
|
516
|
+
if self._lazy:
|
|
517
|
+
tab = self._find_tab(tab_id)
|
|
518
|
+
if tab is not None and tab in self._lazy_held:
|
|
519
|
+
self._mount_pane(tab)
|
|
520
|
+
self._apply_active_classes()
|
|
521
|
+
self._persist_save()
|
|
522
|
+
if from_user and self._on_change is not None:
|
|
523
|
+
self._on_change(tab_id)
|
|
524
|
+
|
|
525
|
+
def _apply_active_classes(self) -> None:
|
|
526
|
+
for tab in self._tabs:
|
|
527
|
+
tab_id = tab.tab_id
|
|
528
|
+
if tab_id is None:
|
|
529
|
+
continue
|
|
530
|
+
selected = tab_id == self._active_tab
|
|
531
|
+
pane_id = _pane_dom_id(tab)
|
|
532
|
+
for key, value in tab_pane_aria(
|
|
533
|
+
tab_id=tab_id,
|
|
534
|
+
pane_id=pane_id,
|
|
535
|
+
selected=selected,
|
|
536
|
+
).items():
|
|
537
|
+
tab._props[key] = value
|
|
538
|
+
_set_class(tab, "active", selected)
|
|
539
|
+
tab.update()
|
|
540
|
+
btn = self._buttons.get(tab_id)
|
|
541
|
+
if btn is None:
|
|
542
|
+
continue
|
|
543
|
+
for key, value in tab_button_aria(
|
|
544
|
+
tab_id=tab_id,
|
|
545
|
+
pane_id=pane_id,
|
|
546
|
+
selected=selected,
|
|
547
|
+
disabled=tab.disabled,
|
|
548
|
+
).items():
|
|
549
|
+
btn._props[key] = value
|
|
550
|
+
_set_class(btn, "active", selected)
|
|
551
|
+
_set_class(btn, "disabled", tab.disabled)
|
|
552
|
+
if tab.disabled:
|
|
553
|
+
btn._props["disabled"] = True
|
|
554
|
+
elif "disabled" in btn._props:
|
|
555
|
+
del btn._props["disabled"]
|
|
556
|
+
btn.update()
|
|
557
|
+
|
|
558
|
+
def _register_new_tab(self, tab: Any) -> None:
|
|
559
|
+
if any(existing is tab for existing in self._tabs):
|
|
560
|
+
return
|
|
561
|
+
incoming = tab.tab_id if tab.tab_id else tab._explicit_tab_id
|
|
562
|
+
assigned = assign_tab_ids([existing.tab_id for existing in self._tabs] + [incoming])
|
|
563
|
+
tab._tab_id = assigned[-1]
|
|
564
|
+
self._tabs.append(tab)
|
|
565
|
+
if self._active_tab is None:
|
|
566
|
+
self._ensure_active()
|
|
567
|
+
self._mount_pane(tab)
|
|
568
|
+
self._create_nav_item(tab)
|
|
569
|
+
self._apply_active_classes()
|
|
570
|
+
|
|
571
|
+
def _forget_tab(self, tab: Any) -> None:
|
|
572
|
+
tab_id = tab.tab_id
|
|
573
|
+
if tab_id is not None:
|
|
574
|
+
item = self._nav_items.pop(tab_id, None)
|
|
575
|
+
self._buttons.pop(tab_id, None)
|
|
576
|
+
if item is not None:
|
|
577
|
+
item.delete()
|
|
578
|
+
if tab in self._lazy_held:
|
|
579
|
+
self._lazy_held.remove(tab)
|
|
580
|
+
if tab in self._tabs:
|
|
581
|
+
self._tabs.remove(tab)
|
|
582
|
+
|
|
583
|
+
def _sync_tabs(self) -> None:
|
|
584
|
+
if getattr(self, "_content", None) is None:
|
|
585
|
+
return
|
|
586
|
+
loose = [child for child in _element_children(self) if _is_tab_element(child)]
|
|
587
|
+
for tab in loose:
|
|
588
|
+
self._register_new_tab(tab)
|
|
589
|
+
present = [child for child in _element_children(self._content) if _is_tab_element(child)]
|
|
590
|
+
present.extend(self._lazy_held)
|
|
591
|
+
present_ids = {id(tab) for tab in present}
|
|
592
|
+
for tab in list(self._tabs):
|
|
593
|
+
if id(tab) not in present_ids:
|
|
594
|
+
self._forget_tab(tab)
|
|
595
|
+
known_ids = {tab.tab_id for tab in self._tabs}
|
|
596
|
+
if self._active_tab not in known_ids:
|
|
597
|
+
self._ensure_active()
|
|
598
|
+
self._apply_active_classes()
|
|
599
|
+
self._persist_save()
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
Tab, DbcTab = make_surface_classes("Tab", globals())
|
|
603
|
+
tab = Tab
|
|
604
|
+
Tabs, DbcTabs = make_surface_classes("Tabs", globals())
|
|
605
|
+
tabs = Tabs
|