@tangible/ui 0.0.1
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.
- package/README.md +100 -0
- package/components/Accordion/Accordion.d.ts +22 -0
- package/components/Accordion/Accordion.js +192 -0
- package/components/Accordion/AccordionContext.d.ts +5 -0
- package/components/Accordion/AccordionContext.js +23 -0
- package/components/Accordion/index.d.ts +2 -0
- package/components/Accordion/index.js +1 -0
- package/components/Accordion/types.d.ts +61 -0
- package/components/Accordion/types.js +1 -0
- package/components/Avatar/Avatar.d.ts +11 -0
- package/components/Avatar/Avatar.js +67 -0
- package/components/Avatar/AvatarGroup.d.ts +11 -0
- package/components/Avatar/AvatarGroup.js +45 -0
- package/components/Avatar/index.d.ts +9 -0
- package/components/Avatar/index.js +7 -0
- package/components/Avatar/types.d.ts +44 -0
- package/components/Avatar/types.js +12 -0
- package/components/Button/Button.d.ts +4 -0
- package/components/Button/Button.js +33 -0
- package/components/Button/index.d.ts +2 -0
- package/components/Button/index.js +1 -0
- package/components/Button/types.d.ts +127 -0
- package/components/Button/types.js +1 -0
- package/components/Card/Card.d.ts +29 -0
- package/components/Card/Card.js +47 -0
- package/components/Card/index.d.ts +2 -0
- package/components/Card/index.js +1 -0
- package/components/Chip/Chip.d.ts +24 -0
- package/components/Chip/Chip.js +37 -0
- package/components/Chip/index.d.ts +2 -0
- package/components/Chip/index.js +1 -0
- package/components/Chips/Chips.d.ts +31 -0
- package/components/Chips/Chips.js +21 -0
- package/components/Chips/index.d.ts +2 -0
- package/components/Chips/index.js +1 -0
- package/components/ContentIndicator/ContentIndicator.d.ts +2 -0
- package/components/ContentIndicator/ContentIndicator.js +21 -0
- package/components/ContentIndicator/index.d.ts +2 -0
- package/components/ContentIndicator/index.js +1 -0
- package/components/ContentIndicator/types.d.ts +57 -0
- package/components/ContentIndicator/types.js +1 -0
- package/components/Dropdown/Dropdown.d.ts +31 -0
- package/components/Dropdown/Dropdown.js +219 -0
- package/components/Dropdown/DropdownContext.d.ts +3 -0
- package/components/Dropdown/DropdownContext.js +9 -0
- package/components/Dropdown/index.d.ts +2 -0
- package/components/Dropdown/index.js +1 -0
- package/components/Dropdown/types.d.ts +102 -0
- package/components/Dropdown/types.js +8 -0
- package/components/Icon/Icon.d.ts +22 -0
- package/components/Icon/Icon.js +24 -0
- package/components/Icon/index.d.ts +2 -0
- package/components/Icon/index.js +1 -0
- package/components/IconButton/IconButton.d.ts +2 -0
- package/components/IconButton/IconButton.js +50 -0
- package/components/IconButton/index.d.ts +2 -0
- package/components/IconButton/index.js +1 -0
- package/components/IconButton/types.d.ts +79 -0
- package/components/IconButton/types.js +1 -0
- package/components/Modal/Modal.d.ts +52 -0
- package/components/Modal/Modal.js +133 -0
- package/components/Modal/context.d.ts +6 -0
- package/components/Modal/context.js +9 -0
- package/components/Modal/index.d.ts +2 -0
- package/components/Modal/index.js +1 -0
- package/components/Notice/Notice.d.ts +93 -0
- package/components/Notice/Notice.js +144 -0
- package/components/Notice/index.d.ts +2 -0
- package/components/Notice/index.js +1 -0
- package/components/OverlapStack/OverlapStack.d.ts +44 -0
- package/components/OverlapStack/OverlapStack.js +41 -0
- package/components/OverlapStack/index.d.ts +2 -0
- package/components/OverlapStack/index.js +1 -0
- package/components/Pager/Pager.d.ts +26 -0
- package/components/Pager/Pager.js +151 -0
- package/components/Pager/index.d.ts +2 -0
- package/components/Pager/index.js +1 -0
- package/components/Progress/Progress.d.ts +2 -0
- package/components/Progress/Progress.js +100 -0
- package/components/Progress/index.d.ts +4 -0
- package/components/Progress/index.js +2 -0
- package/components/Progress/types.d.ts +251 -0
- package/components/Progress/types.js +1 -0
- package/components/Progress/useProgressSegments.d.ts +40 -0
- package/components/Progress/useProgressSegments.js +42 -0
- package/components/Rating/Rating.d.ts +32 -0
- package/components/Rating/Rating.js +74 -0
- package/components/Rating/index.d.ts +2 -0
- package/components/Rating/index.js +1 -0
- package/components/SegmentedControl/SegmentedControl.d.ts +10 -0
- package/components/SegmentedControl/SegmentedControl.js +183 -0
- package/components/SegmentedControl/SegmentedControlContext.d.ts +3 -0
- package/components/SegmentedControl/SegmentedControlContext.js +9 -0
- package/components/SegmentedControl/index.d.ts +2 -0
- package/components/SegmentedControl/index.js +1 -0
- package/components/SegmentedControl/types.d.ts +63 -0
- package/components/SegmentedControl/types.js +1 -0
- package/components/Sidebar/Sidebar.d.ts +17 -0
- package/components/Sidebar/Sidebar.js +107 -0
- package/components/Sidebar/index.d.ts +2 -0
- package/components/Sidebar/index.js +1 -0
- package/components/Sidebar/types.d.ts +65 -0
- package/components/Sidebar/types.js +4 -0
- package/components/StepIndicator/StepIndicator.d.ts +2 -0
- package/components/StepIndicator/StepIndicator.js +64 -0
- package/components/StepIndicator/index.d.ts +2 -0
- package/components/StepIndicator/index.js +1 -0
- package/components/StepIndicator/types.d.ts +68 -0
- package/components/StepIndicator/types.js +1 -0
- package/components/StepList/StepList.d.ts +12 -0
- package/components/StepList/StepList.js +59 -0
- package/components/StepList/StepListContext.d.ts +3 -0
- package/components/StepList/StepListContext.js +9 -0
- package/components/StepList/index.d.ts +2 -0
- package/components/StepList/index.js +1 -0
- package/components/StepList/types.d.ts +91 -0
- package/components/StepList/types.js +4 -0
- package/components/Table/BulkActionsBar.d.ts +12 -0
- package/components/Table/BulkActionsBar.js +9 -0
- package/components/Table/DataTable.d.ts +35 -0
- package/components/Table/DataTable.js +184 -0
- package/components/Table/Pagination.d.ts +13 -0
- package/components/Table/Pagination.js +13 -0
- package/components/Table/index.d.ts +2 -0
- package/components/Table/index.js +1 -0
- package/components/Tabs/Tabs.d.ts +23 -0
- package/components/Tabs/Tabs.js +309 -0
- package/components/Tabs/TabsContext.d.ts +3 -0
- package/components/Tabs/TabsContext.js +12 -0
- package/components/Tabs/index.d.ts +2 -0
- package/components/Tabs/index.js +1 -0
- package/components/Tabs/types.d.ts +75 -0
- package/components/Tabs/types.js +1 -0
- package/components/Toolbar/Toolbar.d.ts +18 -0
- package/components/Toolbar/Toolbar.js +241 -0
- package/components/Toolbar/index.d.ts +2 -0
- package/components/Toolbar/index.js +1 -0
- package/components/Toolbar/types.d.ts +28 -0
- package/components/Toolbar/types.js +1 -0
- package/components/Tooltip/Tooltip.d.ts +15 -0
- package/components/Tooltip/Tooltip.js +166 -0
- package/components/Tooltip/TooltipContext.d.ts +15 -0
- package/components/Tooltip/TooltipContext.js +25 -0
- package/components/Tooltip/index.d.ts +2 -0
- package/components/Tooltip/index.js +1 -0
- package/components/Tooltip/types.d.ts +85 -0
- package/components/Tooltip/types.js +8 -0
- package/components/index.d.ts +52 -0
- package/components/index.js +26 -0
- package/constants.d.ts +16 -0
- package/constants.js +16 -0
- package/icons/cred/index.d.ts +31 -0
- package/icons/cred/index.js +136 -0
- package/icons/icons.svg +155 -0
- package/icons/lms/index.d.ts +21 -0
- package/icons/lms/index.js +81 -0
- package/icons/manifest.json +1226 -0
- package/icons/player/index.d.ts +55 -0
- package/icons/player/index.js +268 -0
- package/icons/reaction/index.d.ts +79 -0
- package/icons/reaction/index.js +400 -0
- package/icons/registry.d.ts +316 -0
- package/icons/registry.js +163 -0
- package/icons/system/index.d.ts +155 -0
- package/icons/system/index.js +818 -0
- package/package.json +121 -0
- package/styles/all.css +1 -0
- package/styles/all.expanded.css +4137 -0
- package/styles/all.expanded.unlayered.css +4137 -0
- package/styles/all.unlayered.css +1 -0
- package/styles/components/_bundle.scss +51 -0
- package/styles/components/index.scss +1 -0
- package/styles/components/input/index.scss +248 -0
- package/styles/index.scss +71 -0
- package/styles/system/_constants.scss +12 -0
- package/styles/system/_motion.scss +47 -0
- package/styles/system/_palette-fns.scss +10 -0
- package/styles/system/_palettes.scss +80 -0
- package/styles/system/_tokens.scss +249 -0
- package/styles/system/index.scss +4 -0
- package/styles/utilities/_index.scss +373 -0
- package/tui-manifest.json +1858 -0
- package/types/index.d.ts +2 -0
- package/types/index.js +1 -0
- package/types/index.ts +2 -0
- package/types/sizes.d.ts +17 -0
- package/types/sizes.js +10 -0
- package/types/sizes.ts +21 -0
- package/types/svg.d.ts +5 -0
- package/types/themes.d.ts +14 -0
- package/types/themes.js +9 -0
- package/types/themes.ts +17 -0
- package/utils/color/contrast.d.ts +33 -0
- package/utils/color/contrast.js +88 -0
- package/utils/color-scheme.d.ts +25 -0
- package/utils/color-scheme.js +55 -0
- package/utils/compose-refs.d.ts +17 -0
- package/utils/compose-refs.js +38 -0
- package/utils/cx.d.ts +12 -0
- package/utils/cx.js +14 -0
- package/utils/focus-trap.d.ts +40 -0
- package/utils/focus-trap.js +93 -0
- package/utils/index.d.ts +10 -0
- package/utils/index.js +16 -0
- package/utils/math.d.ts +4 -0
- package/utils/math.js +19 -0
- package/utils/merge-props.d.ts +25 -0
- package/utils/merge-props.js +60 -0
- package/utils/polymorphic.d.ts +28 -0
- package/utils/polymorphic.js +44 -0
- package/utils/portal.d.ts +11 -0
- package/utils/portal.js +105 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { Icon } from '../Icon/index.js';
|
|
5
|
+
import { TabsContext, useTabsContext } from './TabsContext.js';
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Utility: Sanitize ID
|
|
8
|
+
// =============================================================================
|
|
9
|
+
const sanitizeId = (str) => str.replace(/[^a-zA-Z0-9-_]/g, '-');
|
|
10
|
+
function TabsRoot({ variant = 'underline', activationMode = 'auto', value: controlledValue, defaultValue, onValueChange, orientation = 'horizontal', className, children, }) {
|
|
11
|
+
const baseId = useId();
|
|
12
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
13
|
+
const [focusedValue, setFocusedValue] = useState(undefined);
|
|
14
|
+
const [registryVersion, setRegistryVersion] = useState(0);
|
|
15
|
+
const tabsRef = useRef(new Map());
|
|
16
|
+
const panelsRef = useRef(new Set());
|
|
17
|
+
const mountCounterRef = useRef(0);
|
|
18
|
+
const prevDisabledRef = useRef(new Map());
|
|
19
|
+
const isControlled = controlledValue !== undefined;
|
|
20
|
+
const activeValue = isControlled ? controlledValue : internalValue;
|
|
21
|
+
// ID generators
|
|
22
|
+
const getTabId = useCallback((value) => `${baseId}-tab-${sanitizeId(value)}`, [baseId]);
|
|
23
|
+
const getPanelId = useCallback((value) => `${baseId}-panel-${sanitizeId(value)}`, [baseId]);
|
|
24
|
+
// Tab registration
|
|
25
|
+
const registerTab = useCallback((record) => {
|
|
26
|
+
const existing = tabsRef.current.get(record.value);
|
|
27
|
+
tabsRef.current.set(record.value, {
|
|
28
|
+
...record,
|
|
29
|
+
mountIndex: existing?.mountIndex ?? mountCounterRef.current++,
|
|
30
|
+
});
|
|
31
|
+
setRegistryVersion((v) => v + 1);
|
|
32
|
+
}, []);
|
|
33
|
+
const unregisterTab = useCallback((value) => {
|
|
34
|
+
tabsRef.current.delete(value);
|
|
35
|
+
setRegistryVersion((v) => v + 1);
|
|
36
|
+
}, []);
|
|
37
|
+
// Panel registration
|
|
38
|
+
const registerPanel = useCallback((value) => {
|
|
39
|
+
panelsRef.current.add(value);
|
|
40
|
+
setRegistryVersion((v) => v + 1);
|
|
41
|
+
}, []);
|
|
42
|
+
const unregisterPanel = useCallback((value) => {
|
|
43
|
+
panelsRef.current.delete(value);
|
|
44
|
+
setRegistryVersion((v) => v + 1);
|
|
45
|
+
}, []);
|
|
46
|
+
// Get tabs sorted by DOM order, falling back to mount order for disconnected nodes
|
|
47
|
+
const getOrderedTabs = useCallback(() => {
|
|
48
|
+
const tabs = Array.from(tabsRef.current.values());
|
|
49
|
+
return tabs.sort((a, b) => {
|
|
50
|
+
const position = a.element.compareDocumentPosition(b.element);
|
|
51
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
52
|
+
return -1;
|
|
53
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING)
|
|
54
|
+
return 1;
|
|
55
|
+
// Disconnected or same node: fall back to stable mount order
|
|
56
|
+
return a.mountIndex - b.mountIndex;
|
|
57
|
+
});
|
|
58
|
+
}, []);
|
|
59
|
+
// Selection handler
|
|
60
|
+
const onSelect = useCallback((newValue) => {
|
|
61
|
+
if (isControlled) {
|
|
62
|
+
// Controlled: ONLY call callback, never touch internal state
|
|
63
|
+
onValueChange?.(newValue);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Uncontrolled: update internal state, then notify
|
|
67
|
+
setInternalValue(newValue);
|
|
68
|
+
onValueChange?.(newValue);
|
|
69
|
+
}
|
|
70
|
+
}, [isControlled, onValueChange]);
|
|
71
|
+
// Handle dynamic disable: if active or focused tab becomes disabled, move selection/focus
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const enabledTabs = getOrderedTabs().filter((t) => !t.disabled);
|
|
74
|
+
if (enabledTabs.length === 0)
|
|
75
|
+
return; // Edge case: all disabled
|
|
76
|
+
// 1. If ACTIVE tab became disabled, move selection
|
|
77
|
+
const activeTab = tabsRef.current.get(activeValue ?? '');
|
|
78
|
+
const activeWasDisabled = prevDisabledRef.current.get(activeValue ?? '');
|
|
79
|
+
if (activeTab?.disabled && !activeWasDisabled) {
|
|
80
|
+
const firstEnabled = enabledTabs[0];
|
|
81
|
+
if (isControlled) {
|
|
82
|
+
onValueChange?.(firstEnabled.value);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
setInternalValue(firstEnabled.value);
|
|
86
|
+
onValueChange?.(firstEnabled.value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// 2. If FOCUSED tab became disabled, move focus to nearest enabled
|
|
90
|
+
const focusedTab = tabsRef.current.get(focusedValue ?? '');
|
|
91
|
+
const focusedWasDisabled = prevDisabledRef.current.get(focusedValue ?? '');
|
|
92
|
+
if (focusedTab?.disabled && !focusedWasDisabled) {
|
|
93
|
+
// Find next enabled tab after current, or wrap to first
|
|
94
|
+
const currentIdx = enabledTabs.findIndex((t) => t.mountIndex > focusedTab.mountIndex);
|
|
95
|
+
const nextEnabled = enabledTabs[currentIdx >= 0 ? currentIdx : 0];
|
|
96
|
+
setFocusedValue(nextEnabled.value);
|
|
97
|
+
nextEnabled.element.focus();
|
|
98
|
+
}
|
|
99
|
+
// Update tracking
|
|
100
|
+
tabsRef.current.forEach((record, val) => {
|
|
101
|
+
prevDisabledRef.current.set(val, record.disabled);
|
|
102
|
+
});
|
|
103
|
+
}, [registryVersion, activeValue, focusedValue, isControlled, onValueChange, getOrderedTabs]);
|
|
104
|
+
// Dev-only: Tab-Panel pairing validation
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (import.meta.env.DEV) {
|
|
107
|
+
// Defer validation to next microtask so all tabs/panels have registered
|
|
108
|
+
queueMicrotask(() => {
|
|
109
|
+
const tabValues = new Set(Array.from(tabsRef.current.keys()));
|
|
110
|
+
const panelValues = panelsRef.current;
|
|
111
|
+
// Tabs without panels
|
|
112
|
+
tabValues.forEach((v) => {
|
|
113
|
+
if (!panelValues.has(v)) {
|
|
114
|
+
console.warn(`Tabs: Tab "${v}" has no matching Panel.`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// Panels without tabs
|
|
118
|
+
panelValues.forEach((v) => {
|
|
119
|
+
if (!tabValues.has(v)) {
|
|
120
|
+
console.warn(`Tabs: Panel "${v}" has no matching Tab.`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}, [registryVersion]);
|
|
126
|
+
const contextValue = useMemo(() => ({
|
|
127
|
+
variant,
|
|
128
|
+
orientation,
|
|
129
|
+
activationMode,
|
|
130
|
+
activeValue,
|
|
131
|
+
focusedValue,
|
|
132
|
+
baseId,
|
|
133
|
+
registerTab,
|
|
134
|
+
unregisterTab,
|
|
135
|
+
registerPanel,
|
|
136
|
+
unregisterPanel,
|
|
137
|
+
getOrderedTabs,
|
|
138
|
+
onSelect,
|
|
139
|
+
setFocusedValue,
|
|
140
|
+
getTabId,
|
|
141
|
+
getPanelId,
|
|
142
|
+
tabsRef,
|
|
143
|
+
}), [
|
|
144
|
+
variant,
|
|
145
|
+
orientation,
|
|
146
|
+
activationMode,
|
|
147
|
+
activeValue,
|
|
148
|
+
focusedValue,
|
|
149
|
+
baseId,
|
|
150
|
+
registerTab,
|
|
151
|
+
unregisterTab,
|
|
152
|
+
registerPanel,
|
|
153
|
+
unregisterPanel,
|
|
154
|
+
getOrderedTabs,
|
|
155
|
+
onSelect,
|
|
156
|
+
setFocusedValue,
|
|
157
|
+
getTabId,
|
|
158
|
+
getPanelId,
|
|
159
|
+
]);
|
|
160
|
+
return (_jsx(TabsContext.Provider, { value: contextValue, children: _jsx("div", { className: cx('tui-tabs', className), "data-variant": variant, "data-orientation": orientation, children: children }) }));
|
|
161
|
+
}
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// Tabs.List
|
|
164
|
+
// =============================================================================
|
|
165
|
+
function TabsList({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }) {
|
|
166
|
+
const { orientation, activationMode, activeValue, focusedValue, getOrderedTabs, onSelect, setFocusedValue, tabsRef, } = useTabsContext();
|
|
167
|
+
// Dev-only: Warn if missing accessible name
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (import.meta.env.DEV) {
|
|
170
|
+
if (!ariaLabel && !ariaLabelledBy) {
|
|
171
|
+
console.warn('Tabs.List: Missing accessible name. Provide aria-label or aria-labelledby.');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, [ariaLabel, ariaLabelledBy]);
|
|
175
|
+
// Focus tracking via real focus events (O(1) lookup via data attribute)
|
|
176
|
+
const handleFocusIn = (event) => {
|
|
177
|
+
const tab = event.target.closest('[role="tab"]');
|
|
178
|
+
if (!tab)
|
|
179
|
+
return;
|
|
180
|
+
// O(1) lookup via data attribute
|
|
181
|
+
const value = tab.getAttribute('data-tui-tab-value');
|
|
182
|
+
if (!value)
|
|
183
|
+
return;
|
|
184
|
+
const record = tabsRef.current.get(value);
|
|
185
|
+
if (record && !record.disabled) {
|
|
186
|
+
setFocusedValue(value);
|
|
187
|
+
// In auto mode, select on focus
|
|
188
|
+
if (activationMode === 'auto') {
|
|
189
|
+
onSelect(value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
// Keyboard navigation
|
|
194
|
+
const handleKeyDown = (event) => {
|
|
195
|
+
const tabs = getOrderedTabs().filter((t) => !t.disabled);
|
|
196
|
+
// Guard: no enabled tabs
|
|
197
|
+
if (tabs.length === 0)
|
|
198
|
+
return;
|
|
199
|
+
// Find current index; if focusedValue unset or not found, start from first/active tab
|
|
200
|
+
let currentIndex = tabs.findIndex((t) => t.value === focusedValue);
|
|
201
|
+
if (currentIndex === -1) {
|
|
202
|
+
// Fall back to active tab, or first tab
|
|
203
|
+
currentIndex = tabs.findIndex((t) => t.value === activeValue);
|
|
204
|
+
if (currentIndex === -1)
|
|
205
|
+
currentIndex = 0;
|
|
206
|
+
}
|
|
207
|
+
const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
|
|
208
|
+
const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
|
|
209
|
+
let targetIndex = null;
|
|
210
|
+
switch (event.key) {
|
|
211
|
+
case nextKey:
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
targetIndex = (currentIndex + 1) % tabs.length;
|
|
214
|
+
break;
|
|
215
|
+
case prevKey:
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
targetIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
218
|
+
break;
|
|
219
|
+
case 'Home':
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
targetIndex = 0;
|
|
222
|
+
break;
|
|
223
|
+
case 'End':
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
targetIndex = tabs.length - 1;
|
|
226
|
+
break;
|
|
227
|
+
case 'Enter':
|
|
228
|
+
case ' ':
|
|
229
|
+
if (activationMode === 'manual' && focusedValue) {
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
onSelect(focusedValue);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (targetIndex !== null) {
|
|
236
|
+
const targetTab = tabs[targetIndex];
|
|
237
|
+
targetTab.element.focus();
|
|
238
|
+
// Focus event handler will update focusedValue and (in auto mode) call onSelect
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
return (_jsx("div", { role: "tablist", className: cx('tui-tabs__list', className), "aria-orientation": orientation, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, onKeyDown: handleKeyDown, onFocusCapture: handleFocusIn, children: children }));
|
|
242
|
+
}
|
|
243
|
+
// =============================================================================
|
|
244
|
+
// Tabs.Tab
|
|
245
|
+
// =============================================================================
|
|
246
|
+
function Tab({ value, icon, disabled = false, className, children }) {
|
|
247
|
+
const { activationMode, activeValue, focusedValue, registerTab, unregisterTab, onSelect, getTabId, getPanelId, } = useTabsContext();
|
|
248
|
+
const tabId = getTabId(value);
|
|
249
|
+
const panelId = getPanelId(value);
|
|
250
|
+
const isActive = activeValue === value;
|
|
251
|
+
// Determine which tab gets tabIndex={0}
|
|
252
|
+
const getFocusableValue = () => {
|
|
253
|
+
if (activationMode === 'auto') {
|
|
254
|
+
return activeValue;
|
|
255
|
+
}
|
|
256
|
+
// Manual: focused tab gets tabIndex=0, or fall back to active
|
|
257
|
+
return focusedValue ?? activeValue;
|
|
258
|
+
};
|
|
259
|
+
const isFocusable = value === getFocusableValue();
|
|
260
|
+
// Callback ref for registration (handles null during unmount)
|
|
261
|
+
const callbackRef = useCallback((node) => {
|
|
262
|
+
if (node) {
|
|
263
|
+
registerTab({ value, element: node, disabled, tabId, panelId });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
unregisterTab(value);
|
|
267
|
+
}
|
|
268
|
+
}, [value, disabled, tabId, panelId, registerTab, unregisterTab]);
|
|
269
|
+
// Click handler
|
|
270
|
+
const handleClick = () => {
|
|
271
|
+
if (disabled)
|
|
272
|
+
return;
|
|
273
|
+
// In manual mode, explicitly select. In auto mode, focus handler will select.
|
|
274
|
+
if (activationMode === 'manual') {
|
|
275
|
+
onSelect(value);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
return (_jsxs("button", { ref: callbackRef, type: "button", role: "tab", id: tabId, className: cx('tui-tabs__tab', className), "data-tui-tab-value": value, "data-state": isActive ? 'active' : 'inactive', "aria-selected": isActive, "aria-controls": panelId, tabIndex: disabled ? -1 : isFocusable ? 0 : -1, disabled: disabled, onClick: handleClick, children: [icon && _jsx(Icon, { name: icon, size: "sm", "aria-hidden": "true" }), children && _jsx("span", { className: "tui-tabs__tab-label", children: children })] }));
|
|
279
|
+
}
|
|
280
|
+
// =============================================================================
|
|
281
|
+
// Tabs.Panel
|
|
282
|
+
// =============================================================================
|
|
283
|
+
function TabPanel({ value, className, children }) {
|
|
284
|
+
const { activeValue, registerPanel, unregisterPanel, getTabId, getPanelId } = useTabsContext();
|
|
285
|
+
const tabId = getTabId(value);
|
|
286
|
+
const panelId = getPanelId(value);
|
|
287
|
+
const isActive = activeValue === value;
|
|
288
|
+
// Register panel on mount
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
registerPanel(value);
|
|
291
|
+
return () => unregisterPanel(value);
|
|
292
|
+
}, [value, registerPanel, unregisterPanel]);
|
|
293
|
+
return (_jsx("div", { role: "tabpanel", id: panelId, className: cx('tui-tabs__panel', className), "data-state": isActive ? 'active' : 'inactive', "aria-labelledby": tabId, "aria-hidden": !isActive,
|
|
294
|
+
// tabIndex={0} allows direct focus on panel for screen reader navigation
|
|
295
|
+
tabIndex: isActive ? 0 : undefined,
|
|
296
|
+
// inert prevents Tab navigation into hidden panel content
|
|
297
|
+
inert: !isActive || undefined, children: children }));
|
|
298
|
+
}
|
|
299
|
+
// =============================================================================
|
|
300
|
+
// Compound Component Export
|
|
301
|
+
// =============================================================================
|
|
302
|
+
TabsList.displayName = 'Tabs.List';
|
|
303
|
+
Tab.displayName = 'Tabs.Tab';
|
|
304
|
+
TabPanel.displayName = 'Tabs.Panel';
|
|
305
|
+
export const Tabs = TabsRoot;
|
|
306
|
+
Tabs.displayName = 'Tabs';
|
|
307
|
+
Tabs.List = TabsList;
|
|
308
|
+
Tabs.Tab = Tab;
|
|
309
|
+
Tabs.Panel = TabPanel;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Tabs Context
|
|
4
|
+
// =============================================================================
|
|
5
|
+
export const TabsContext = createContext(null);
|
|
6
|
+
export function useTabsContext() {
|
|
7
|
+
const context = useContext(TabsContext);
|
|
8
|
+
if (!context) {
|
|
9
|
+
throw new Error('Tabs compound components must be used within a Tabs component');
|
|
10
|
+
}
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Tabs } from './Tabs.js';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { IconName } from '../../icons/registry';
|
|
3
|
+
export type TabsVariant = 'pill' | 'underline' | 'outline';
|
|
4
|
+
export type TabsActivationMode = 'auto' | 'manual';
|
|
5
|
+
export type TabsOrientation = 'horizontal' | 'vertical';
|
|
6
|
+
export type TabsProps = {
|
|
7
|
+
/** Visual style */
|
|
8
|
+
variant?: TabsVariant;
|
|
9
|
+
/** Auto: select on focus. Manual: arrow keys move focus only, Enter/Space selects. */
|
|
10
|
+
activationMode?: TabsActivationMode;
|
|
11
|
+
/** Controlled active tab */
|
|
12
|
+
value?: string;
|
|
13
|
+
/** Uncontrolled initial tab */
|
|
14
|
+
defaultValue?: string;
|
|
15
|
+
/** Callback when active tab changes */
|
|
16
|
+
onValueChange?: (value: string) => void;
|
|
17
|
+
/** Affects arrow key mapping and aria-orientation */
|
|
18
|
+
orientation?: TabsOrientation;
|
|
19
|
+
/** Additional classes */
|
|
20
|
+
className?: string;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
};
|
|
23
|
+
export type TabsListProps = {
|
|
24
|
+
/** Accessible label (required) */
|
|
25
|
+
'aria-label'?: string;
|
|
26
|
+
/** Alternative to aria-label */
|
|
27
|
+
'aria-labelledby'?: string;
|
|
28
|
+
/** Additional classes */
|
|
29
|
+
className?: string;
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
};
|
|
32
|
+
export type TabProps = {
|
|
33
|
+
/** Unique identifier, must match a Panel */
|
|
34
|
+
value: string;
|
|
35
|
+
/** Icon name from registry */
|
|
36
|
+
icon?: IconName;
|
|
37
|
+
/** Disable tab (skipped in keyboard nav) */
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** Additional classes */
|
|
40
|
+
className?: string;
|
|
41
|
+
children?: ReactNode;
|
|
42
|
+
};
|
|
43
|
+
export type TabPanelProps = {
|
|
44
|
+
/** Must match a Tab value */
|
|
45
|
+
value: string;
|
|
46
|
+
/** Additional classes */
|
|
47
|
+
className?: string;
|
|
48
|
+
children?: ReactNode;
|
|
49
|
+
};
|
|
50
|
+
export type TabRecord = {
|
|
51
|
+
value: string;
|
|
52
|
+
element: HTMLButtonElement;
|
|
53
|
+
disabled: boolean;
|
|
54
|
+
tabId: string;
|
|
55
|
+
panelId: string;
|
|
56
|
+
mountIndex: number;
|
|
57
|
+
};
|
|
58
|
+
export type TabsContextValue = {
|
|
59
|
+
variant: TabsVariant;
|
|
60
|
+
orientation: TabsOrientation;
|
|
61
|
+
activationMode: TabsActivationMode;
|
|
62
|
+
activeValue: string | undefined;
|
|
63
|
+
focusedValue: string | undefined;
|
|
64
|
+
baseId: string;
|
|
65
|
+
registerTab: (record: Omit<TabRecord, 'mountIndex'>) => void;
|
|
66
|
+
unregisterTab: (value: string) => void;
|
|
67
|
+
registerPanel: (value: string) => void;
|
|
68
|
+
unregisterPanel: (value: string) => void;
|
|
69
|
+
getOrderedTabs: () => TabRecord[];
|
|
70
|
+
onSelect: (value: string) => void;
|
|
71
|
+
setFocusedValue: (value: string | undefined) => void;
|
|
72
|
+
getTabId: (value: string) => string;
|
|
73
|
+
getPanelId: (value: string) => string;
|
|
74
|
+
tabsRef: React.MutableRefObject<Map<string, TabRecord>>;
|
|
75
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ToolbarProps, ToolbarGroupProps, ToolbarItemProps } from './types';
|
|
3
|
+
type ToolbarCompound = {
|
|
4
|
+
(props: ToolbarProps): React.JSX.Element;
|
|
5
|
+
displayName?: string;
|
|
6
|
+
Group: typeof ToolbarGroup;
|
|
7
|
+
Item: typeof ToolbarItem;
|
|
8
|
+
};
|
|
9
|
+
declare function ToolbarGroup({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, className, }: ToolbarGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
declare namespace ToolbarGroup {
|
|
11
|
+
var displayName: string;
|
|
12
|
+
}
|
|
13
|
+
declare function ToolbarItem({ children }: ToolbarItemProps): React.ReactElement<Record<string, unknown>, string | React.JSXElementConstructor<any>> | null;
|
|
14
|
+
declare namespace ToolbarItem {
|
|
15
|
+
var displayName: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const Toolbar: ToolbarCompound;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useCallback, useEffect, useRef, cloneElement, isValidElement, } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Toolbar Component
|
|
6
|
+
// =============================================================================
|
|
7
|
+
//
|
|
8
|
+
// WAI-ARIA toolbar pattern with roving tabindex. Only elements marked with
|
|
9
|
+
// data-tui-toolbar-item participate in keyboard navigation - this prevents
|
|
10
|
+
// hijacking nested focusables (inputs, menus, etc.).
|
|
11
|
+
//
|
|
12
|
+
// Per APG: Use toolbars for groups of 3+ controls. For 1-2 controls, standard
|
|
13
|
+
// button groups without role="toolbar" avoid unnecessary keyboard complexity.
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// <Toolbar aria-label="Actions">
|
|
17
|
+
// <Toolbar.Group aria-label="Navigation">
|
|
18
|
+
// <Toolbar.Item>
|
|
19
|
+
// <IconButton icon="system/chevron-left" label="Previous" />
|
|
20
|
+
// </Toolbar.Item>
|
|
21
|
+
// </Toolbar.Group>
|
|
22
|
+
// </Toolbar>
|
|
23
|
+
//
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Selectors for toolbar items
|
|
26
|
+
const ITEM_SELECTOR = '[data-tui-toolbar-item]';
|
|
27
|
+
const DISABLED_SELECTOR = ':disabled, [aria-disabled="true"]';
|
|
28
|
+
// Elements that should not have arrow keys intercepted (composite widgets per APG)
|
|
29
|
+
const ARROW_EXEMPT_SELECTOR = 'input, textarea, select, [contenteditable="true"], ' +
|
|
30
|
+
'[role="textbox"], [role="combobox"], [role="listbox"], ' +
|
|
31
|
+
'[role="menu"], [role="tree"], [role="grid"], [role="spinbutton"], [role="slider"], ' +
|
|
32
|
+
'[role="radiogroup"]';
|
|
33
|
+
// -----------------------------------------------------------------------------
|
|
34
|
+
// Helper: Find next/prev enabled item
|
|
35
|
+
// -----------------------------------------------------------------------------
|
|
36
|
+
function findNextItem(items, currentIndex, direction, loop) {
|
|
37
|
+
const len = items.length;
|
|
38
|
+
if (len === 0)
|
|
39
|
+
return null;
|
|
40
|
+
let index = currentIndex;
|
|
41
|
+
for (let i = 0; i < len; i++) {
|
|
42
|
+
index += direction;
|
|
43
|
+
// Handle boundaries
|
|
44
|
+
if (index >= len) {
|
|
45
|
+
if (!loop)
|
|
46
|
+
return null;
|
|
47
|
+
index = 0;
|
|
48
|
+
}
|
|
49
|
+
else if (index < 0) {
|
|
50
|
+
if (!loop)
|
|
51
|
+
return null;
|
|
52
|
+
index = len - 1;
|
|
53
|
+
}
|
|
54
|
+
// Skip disabled items
|
|
55
|
+
const item = items[index];
|
|
56
|
+
if (!item.matches(DISABLED_SELECTOR)) {
|
|
57
|
+
return index;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null; // All items disabled
|
|
61
|
+
}
|
|
62
|
+
function ToolbarRoot({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, orientation = 'horizontal', loop = true, children, className, style, }) {
|
|
63
|
+
const containerRef = useRef(null);
|
|
64
|
+
const focusedIndexRef = useRef(0);
|
|
65
|
+
// Instance-scoped map for storing original tabindex values
|
|
66
|
+
const originalTabIndexMapRef = useRef(new WeakMap());
|
|
67
|
+
// Dev warning for missing accessible name
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (import.meta.env?.DEV && !ariaLabel && !ariaLabelledBy) {
|
|
70
|
+
console.warn('[Toolbar] aria-label or aria-labelledby is required for accessibility.');
|
|
71
|
+
}
|
|
72
|
+
}, [ariaLabel, ariaLabelledBy]);
|
|
73
|
+
// -------------------------------------------------------------------------
|
|
74
|
+
// Item discovery
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
const getItems = useCallback(() => {
|
|
77
|
+
if (!containerRef.current)
|
|
78
|
+
return [];
|
|
79
|
+
return Array.from(containerRef.current.querySelectorAll(ITEM_SELECTOR));
|
|
80
|
+
}, []);
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
// Roving tabindex management
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
const updateTabIndices = useCallback((items, focusIdx) => {
|
|
85
|
+
const map = originalTabIndexMapRef.current;
|
|
86
|
+
items.forEach((item, i) => {
|
|
87
|
+
// Store original tabindex if not already stored
|
|
88
|
+
if (!map.has(item)) {
|
|
89
|
+
map.set(item, item.hasAttribute('tabindex') ? item.tabIndex : null);
|
|
90
|
+
}
|
|
91
|
+
// Disabled items get tabIndex=-1 (not in Tab order)
|
|
92
|
+
const isDisabled = item.matches(DISABLED_SELECTOR);
|
|
93
|
+
if (isDisabled) {
|
|
94
|
+
item.setAttribute('tabindex', '-1');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
item.setAttribute('tabindex', i === focusIdx ? '0' : '-1');
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}, []);
|
|
101
|
+
// Initialize tabindex and observe for dynamic item changes
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const container = containerRef.current;
|
|
104
|
+
if (!container)
|
|
105
|
+
return;
|
|
106
|
+
const initializeTabIndices = () => {
|
|
107
|
+
const items = getItems();
|
|
108
|
+
if (items.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
// If current focused item is still valid, keep it
|
|
111
|
+
const currentItems = getItems();
|
|
112
|
+
const currentFocusedItem = currentItems[focusedIndexRef.current];
|
|
113
|
+
if (currentFocusedItem && !currentFocusedItem.matches(DISABLED_SELECTOR)) {
|
|
114
|
+
updateTabIndices(items, focusedIndexRef.current);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Otherwise find first enabled item
|
|
118
|
+
const firstEnabled = items.findIndex((el) => !el.matches(DISABLED_SELECTOR));
|
|
119
|
+
const newIdx = Math.max(0, firstEnabled);
|
|
120
|
+
focusedIndexRef.current = newIdx;
|
|
121
|
+
updateTabIndices(items, newIdx);
|
|
122
|
+
};
|
|
123
|
+
// Initial setup
|
|
124
|
+
initializeTabIndices();
|
|
125
|
+
// Watch for items being added/removed
|
|
126
|
+
const observer = new MutationObserver((mutations) => {
|
|
127
|
+
// Only re-init if toolbar items changed
|
|
128
|
+
const relevantChange = mutations.some((m) => m.type === 'childList' ||
|
|
129
|
+
(m.type === 'attributes' && m.attributeName === 'data-tui-toolbar-item'));
|
|
130
|
+
if (relevantChange) {
|
|
131
|
+
initializeTabIndices();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
observer.observe(container, {
|
|
135
|
+
childList: true,
|
|
136
|
+
subtree: true,
|
|
137
|
+
attributes: true,
|
|
138
|
+
attributeFilter: ['data-tui-toolbar-item', 'disabled', 'aria-disabled'],
|
|
139
|
+
});
|
|
140
|
+
return () => observer.disconnect();
|
|
141
|
+
}, [getItems, updateTabIndices]);
|
|
142
|
+
// -------------------------------------------------------------------------
|
|
143
|
+
// Focus sync - DOM is source of truth
|
|
144
|
+
// -------------------------------------------------------------------------
|
|
145
|
+
const handleFocusIn = useCallback((e) => {
|
|
146
|
+
// Target may be inner element (SVG, span) - find closest toolbar item
|
|
147
|
+
const item = e.target.closest(ITEM_SELECTOR);
|
|
148
|
+
if (!item)
|
|
149
|
+
return;
|
|
150
|
+
const items = getItems();
|
|
151
|
+
const idx = items.indexOf(item);
|
|
152
|
+
if (idx !== -1) {
|
|
153
|
+
focusedIndexRef.current = idx;
|
|
154
|
+
// Defer DOM mutation to avoid React hydration mismatches in SSR contexts
|
|
155
|
+
queueMicrotask(() => updateTabIndices(items, idx));
|
|
156
|
+
}
|
|
157
|
+
}, [getItems, updateTabIndices]);
|
|
158
|
+
// -------------------------------------------------------------------------
|
|
159
|
+
// Keyboard navigation
|
|
160
|
+
// -------------------------------------------------------------------------
|
|
161
|
+
const handleKeyDown = useCallback((e) => {
|
|
162
|
+
// Bail if modifier keys held
|
|
163
|
+
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
|
|
164
|
+
return;
|
|
165
|
+
// Bail if target is or is inside exempt element (input, textarea, composite widget)
|
|
166
|
+
if (e.target.closest(ARROW_EXEMPT_SELECTOR))
|
|
167
|
+
return;
|
|
168
|
+
const items = getItems();
|
|
169
|
+
if (items.length === 0)
|
|
170
|
+
return;
|
|
171
|
+
// Find which item currently has focus (using closest for inner elements)
|
|
172
|
+
const activeItem = document.activeElement?.closest(ITEM_SELECTOR);
|
|
173
|
+
if (!activeItem)
|
|
174
|
+
return;
|
|
175
|
+
const currentIdx = items.indexOf(activeItem);
|
|
176
|
+
if (currentIdx === -1)
|
|
177
|
+
return;
|
|
178
|
+
const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
|
|
179
|
+
const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
|
|
180
|
+
let targetIdx = null;
|
|
181
|
+
switch (e.key) {
|
|
182
|
+
case nextKey:
|
|
183
|
+
targetIdx = findNextItem(items, currentIdx, 1, loop);
|
|
184
|
+
break;
|
|
185
|
+
case prevKey:
|
|
186
|
+
targetIdx = findNextItem(items, currentIdx, -1, loop);
|
|
187
|
+
break;
|
|
188
|
+
case 'Home':
|
|
189
|
+
targetIdx = findNextItem(items, -1, 1, false);
|
|
190
|
+
break;
|
|
191
|
+
case 'End':
|
|
192
|
+
targetIdx = findNextItem(items, items.length, -1, false);
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (targetIdx !== null && targetIdx !== currentIdx) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
items[targetIdx].focus();
|
|
200
|
+
// focusin handler will update state
|
|
201
|
+
}
|
|
202
|
+
}, [getItems, orientation, loop]);
|
|
203
|
+
return (_jsx("div", { ref: containerRef, role: "toolbar", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-orientation": orientation, className: cx('tui-toolbar', className), style: style,
|
|
204
|
+
// Capture phase catches focus on inner elements (SVG icons, spans) before
|
|
205
|
+
// they handle it, allowing us to update roving tabindex correctly
|
|
206
|
+
onFocusCapture: handleFocusIn, onKeyDown: handleKeyDown, children: children }));
|
|
207
|
+
}
|
|
208
|
+
// =============================================================================
|
|
209
|
+
// Toolbar Group
|
|
210
|
+
// =============================================================================
|
|
211
|
+
function ToolbarGroup({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, className, }) {
|
|
212
|
+
// Only add role="group" if accessible name is provided
|
|
213
|
+
// Unnamed groups are worse than no group
|
|
214
|
+
const hasLabel = ariaLabel || ariaLabelledBy;
|
|
215
|
+
return (_jsx("div", { role: hasLabel ? 'group' : undefined, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: cx('tui-toolbar__group', className), children: children }));
|
|
216
|
+
}
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// Toolbar Item
|
|
219
|
+
// =============================================================================
|
|
220
|
+
function ToolbarItem({ children }) {
|
|
221
|
+
if (!isValidElement(children)) {
|
|
222
|
+
console.warn('[Toolbar.Item] Expected a single valid React element as child');
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const child = children;
|
|
226
|
+
// cloneElement preserves the child's ref automatically - don't touch it
|
|
227
|
+
// Only add the data attribute for toolbar item registration
|
|
228
|
+
return cloneElement(child, {
|
|
229
|
+
...child.props,
|
|
230
|
+
'data-tui-toolbar-item': '',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Export Compound Component
|
|
235
|
+
// =============================================================================
|
|
236
|
+
ToolbarGroup.displayName = 'Toolbar.Group';
|
|
237
|
+
ToolbarItem.displayName = 'Toolbar.Item';
|
|
238
|
+
export const Toolbar = ToolbarRoot;
|
|
239
|
+
Toolbar.displayName = 'Toolbar';
|
|
240
|
+
Toolbar.Group = ToolbarGroup;
|
|
241
|
+
Toolbar.Item = ToolbarItem;
|