@flikk/ui 1.0.0-beta.22 → 1.0.0-beta.24
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/dist/components/ai/PromptInput/PromptInput.js +139 -7
- package/dist/components/ai/PromptInput/VoiceRecorder.js +84 -0
- package/dist/components/charts/AreaChart/AreaChart.types.d.ts +2 -2
- package/dist/components/charts/BarChart/BarChart.types.d.ts +2 -2
- package/dist/components/charts/ComboChart/ComboChart.types.d.ts +2 -2
- package/dist/components/charts/LineChart/LineChart.types.d.ts +2 -2
- package/dist/components/charts/ScatterPlot/ScatterPlot.types.d.ts +2 -2
- package/dist/components/charts/StackedBarChart/StackedBarChart.types.d.ts +2 -2
- package/dist/components/core/Accordion/AccordionTrigger.js +28 -1
- package/dist/components/core/Alert/Alert.js +2 -1
- package/dist/components/core/AlertDialog/AlertDialog.js +1 -1
- package/dist/components/core/Badge/Badge.js +6 -17
- package/dist/components/core/Breadcrumbs/Breadcrumbs.js +17 -28
- package/dist/components/core/Calendar/Calendar.js +1 -1
- package/dist/components/core/Carousel/CarouselBody.js +6 -1
- package/dist/components/core/ContextMenu/ContextMenuBody.js +6 -3
- package/dist/components/core/ContextMenu/ContextMenuSubBody.js +6 -3
- package/dist/components/core/DragDrop/DragDrop.js +170 -17
- package/dist/components/core/DragDrop/DragDrop.types.d.ts +6 -0
- package/dist/components/core/Drawer/Drawer.d.ts +1 -1
- package/dist/components/core/Drawer/Drawer.js +2 -0
- package/dist/components/core/Drawer/Drawer.types.d.ts +2 -0
- package/dist/components/core/Drawer/DrawerTitle.d.ts +1 -1
- package/dist/components/core/Drawer/DrawerTitle.js +2 -2
- package/dist/components/core/Dropdown/DropdownMenu.js +2 -30
- package/dist/components/core/Masonry/Masonry.js +7 -2
- package/dist/components/core/Modal/Modal.d.ts +1 -1
- package/dist/components/core/Modal/Modal.js +3 -3
- package/dist/components/core/Modal/Modal.types.d.ts +2 -0
- package/dist/components/core/Modal/ModalTitle.d.ts +1 -1
- package/dist/components/core/Modal/ModalTitle.js +2 -2
- package/dist/components/core/NavItem/NavItem.js +21 -6
- package/dist/components/core/NavItem/NavItem.theme.js +2 -2
- package/dist/components/core/Pagination/Pagination.js +1 -1
- package/dist/components/core/Popover/PopoverBody.js +3 -27
- package/dist/components/core/Popover/PopoverTrigger.js +1 -1
- package/dist/components/core/Progress/Progress.js +4 -4
- package/dist/components/core/Rating/Rating.js +1 -1
- package/dist/components/core/ScrollArea/ScrollArea.js +3 -2
- package/dist/components/core/Segmented/Segmented.js +6 -6
- package/dist/components/core/Segmented/SegmentedItem.js +28 -2
- package/dist/components/core/Sidebar/Sidebar.d.ts +1 -0
- package/dist/components/core/Sidebar/Sidebar.js +5 -3
- package/dist/components/core/Sidebar/Sidebar.types.d.ts +7 -1
- package/dist/components/core/Sidebar/SidebarContent.js +1 -1
- package/dist/components/core/Sidebar/SidebarFooter.js +1 -1
- package/dist/components/core/Sidebar/SidebarHeader.js +5 -2
- package/dist/components/core/Sidebar/SidebarLogo.d.ts +3 -0
- package/dist/components/core/Sidebar/SidebarLogo.js +17 -0
- package/dist/components/core/Sidebar/SidebarNavGroup.js +2 -1
- package/dist/components/core/Sidebar/SidebarToggle.js +2 -4
- package/dist/components/core/Sidebar/SidebarUserProfile.js +3 -6
- package/dist/components/core/Sidebar/index.d.ts +2 -1
- package/dist/components/core/Skeleton/Skeleton.js +2 -2
- package/dist/components/core/Sortable/Sortable.d.ts +1 -1
- package/dist/components/core/Sortable/Sortable.js +20 -4
- package/dist/components/core/Sortable/Sortable.types.d.ts +2 -0
- package/dist/components/core/Tabs/Tabs.js +6 -4
- package/dist/components/core/Tabs/Tabs.types.d.ts +2 -0
- package/dist/components/core/Tabs/TabsBody.js +3 -3
- package/dist/components/core/Tabs/TabsList.js +19 -21
- package/dist/components/core/Tabs/TabsTrigger.js +47 -28
- package/dist/components/core/Toast/Toast.js +1 -1
- package/dist/components/core/Toast/ToastProvider.js +3 -2
- package/dist/components/core/Tooltip/Tooltip.d.ts +1 -1
- package/dist/components/core/Tooltip/Tooltip.js +26 -35
- package/dist/components/core/Tooltip/Tooltip.types.d.ts +5 -1
- package/dist/components/core/Tree/Tree.js +178 -8
- package/dist/components/core/index.js +1 -0
- package/dist/components/data-display/GanttChart/GanttSplitter.js +15 -1
- package/dist/components/data-display/Table/Table.js +2 -2
- package/dist/components/data-display/Table/Table.types.d.ts +2 -0
- package/dist/components/data-display/Table/TableFilter.js +21 -18
- package/dist/components/effects/CustomCursor/CustomCursor.js +3 -7
- package/dist/components/effects/Overlay/Overlay.js +4 -9
- package/dist/components/effects/PageTransition/PageTransition.js +17 -6
- package/dist/components/effects/SpotlightBorder/SpotlightBorder.js +3 -4
- package/dist/components/forms/Checkbox/Checkbox.js +11 -1
- package/dist/components/forms/Checkbox/Checkbox.theme.js +4 -1
- package/dist/components/forms/Checkbox/Checkbox.types.d.ts +7 -5
- package/dist/components/forms/Checkbox/CheckboxGroup.js +9 -4
- package/dist/components/forms/ColorPicker/ColorPicker.js +7 -4
- package/dist/components/forms/ColorPicker/ColorPicker.types.d.ts +2 -2
- package/dist/components/forms/ColorPicker/ColorPickerBody.js +5 -3
- package/dist/components/forms/ColorPicker/ColorPickerEyeDropper.js +3 -3
- package/dist/components/forms/ColorPicker/ColorPickerGradient.js +8 -8
- package/dist/components/forms/ColorPicker/ColorPickerInput.js +1 -1
- package/dist/components/forms/ColorPicker/index.d.ts +1 -1
- package/dist/components/forms/Combobox/Combobox.js +20 -45
- package/dist/components/forms/Combobox/Combobox.types.d.ts +4 -2
- package/dist/components/forms/CronInput/CronInput.js +10 -4
- package/dist/components/forms/CronInput/CronInput.types.d.ts +12 -1
- package/dist/components/forms/DatePicker/DatePicker.js +2 -2
- package/dist/components/forms/DatePicker/DatePicker.types.d.ts +4 -2
- package/dist/components/forms/DatePicker/DatePickerBody.js +1 -29
- package/dist/components/forms/DatePicker/DatePickerTrigger.js +2 -2
- package/dist/components/forms/DateRangePicker/DateRangePicker.js +3 -3
- package/dist/components/forms/DateRangePicker/DateRangePicker.types.d.ts +4 -2
- package/dist/components/forms/DateRangePicker/DateRangePickerBody.js +1 -28
- package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +2 -2
- package/dist/components/forms/FileUpload/FileUpload.js +99 -10
- package/dist/components/forms/FileUpload/FileUpload.types.d.ts +15 -0
- package/dist/components/forms/FormLabel/FormLabel.js +2 -2
- package/dist/components/forms/Input/Input.js +5 -3
- package/dist/components/forms/Input/Input.types.d.ts +2 -0
- package/dist/components/forms/InputAddress/InputAddress.d.ts +1 -1
- package/dist/components/forms/InputAddress/InputAddress.js +8 -9
- package/dist/components/forms/InputCounter/InputCounter.js +25 -18
- package/dist/components/forms/InputCounter/InputCounter.types.d.ts +8 -1
- package/dist/components/forms/InputCreditCard/InputCreditCard.d.ts +1 -1
- package/dist/components/forms/InputCreditCard/InputCreditCard.js +27 -9
- package/dist/components/forms/InputCreditCard/InputCreditCard.types.d.ts +5 -0
- package/dist/components/forms/InputOTP/InputOTP.js +26 -17
- package/dist/components/forms/InputOTP/InputOTP.types.d.ts +21 -4
- package/dist/components/forms/InputTag/InputTag.js +12 -39
- package/dist/components/forms/InputTag/InputTag.types.d.ts +2 -2
- package/dist/components/forms/Mention/Mention.js +13 -6
- package/dist/components/forms/Mention/Mention.types.d.ts +9 -0
- package/dist/components/forms/Radio/Radio.js +12 -2
- package/dist/components/forms/Radio/Radio.theme.js +5 -1
- package/dist/components/forms/Radio/Radio.types.d.ts +7 -5
- package/dist/components/forms/Radio/RadioGroup.js +8 -3
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +21 -21
- package/dist/components/forms/RichTextEditor/RichTextEditor.types.d.ts +2 -2
- package/dist/components/forms/Select/Select.d.ts +2 -0
- package/dist/components/forms/Select/Select.js +26 -56
- package/dist/components/forms/Select/Select.types.d.ts +14 -0
- package/dist/components/forms/Signature/Signature.js +1 -1
- package/dist/components/forms/Signature/Signature.types.d.ts +3 -3
- package/dist/components/forms/Slider/Slider.js +18 -4
- package/dist/components/forms/Slider/Slider.types.d.ts +2 -2
- package/dist/components/forms/Switch/Switch.js +12 -19
- package/dist/components/forms/Switch/Switch.theme.js +12 -2
- package/dist/components/forms/Switch/Switch.types.d.ts +20 -2
- package/dist/components/forms/Textarea/Textarea.js +8 -3
- package/dist/components/forms/Textarea/Textarea.types.d.ts +2 -0
- package/dist/components/forms/TimePicker/TimePicker.js +1 -1
- package/dist/components/forms/TimePicker/TimePicker.types.d.ts +6 -2
- package/dist/components/forms/TimePicker/TimePickerContent.js +88 -30
- package/dist/components/forms/TimePicker/TimePickerTrigger.js +2 -2
- package/dist/components/forms/index.js +86 -1
- package/dist/components/layout/FormLayout/FormLayout.js +87 -1
- package/dist/components/layout/index.js +4 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useClickOutside.d.ts +10 -4
- package/dist/hooks/useClickOutside.js +44 -24
- package/dist/hooks/useDarkMode.d.ts +1 -0
- package/dist/hooks/useDarkMode.js +42 -0
- package/dist/hooks/useFormStateMachine.d.ts +1 -1
- package/dist/hooks/useNetworkStatus.js +1 -1
- package/dist/hooks/useOverlay.js +4 -0
- package/dist/hooks/useScaleBackground.js +5 -2
- package/dist/hooks/useSelectPortal.js +14 -5
- package/dist/index.d.ts +0 -2
- package/dist/index.js +174 -223
- package/dist/shadcn-compat.css +80 -0
- package/dist/styles.css +1 -1
- package/dist/utils/stateMachine.d.ts +4 -4
- package/package.json +12 -10
- package/tailwind.preset.cjs +32 -3
- /package/{src/theme-plugin.css → dist/theme.css} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import React__default, { useState, useRef,
|
|
2
|
+
import React__default, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
3
3
|
import { useReducedMotion, motion } from 'motion/react';
|
|
4
4
|
import { cn } from '../../../utils/cn.js';
|
|
5
5
|
import { tabsTheme } from './Tabs.theme.js';
|
|
@@ -18,26 +18,24 @@ const TabsList = React__default.forwardRef(({ children, className, ...props }, r
|
|
|
18
18
|
// Ref to access the list element for calculations
|
|
19
19
|
const listRef = useRef(null);
|
|
20
20
|
// Calculate underline position based on active tab
|
|
21
|
-
const updateUnderlinePosition =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
setShowUnderline(true);
|
|
40
|
-
};
|
|
21
|
+
const updateUnderlinePosition = useCallback(() => {
|
|
22
|
+
if (!activeValue || !listRef.current) {
|
|
23
|
+
setShowUnderline(false);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Find the active trigger element
|
|
27
|
+
const activeTrigger = listRef.current.querySelector(`[data-value="${activeValue}"]`);
|
|
28
|
+
if (!activeTrigger) {
|
|
29
|
+
setShowUnderline(false);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Get the position relative to the list container
|
|
33
|
+
const listRect = listRef.current.getBoundingClientRect();
|
|
34
|
+
const triggerRect = activeTrigger.getBoundingClientRect();
|
|
35
|
+
const left = triggerRect.left - listRect.left;
|
|
36
|
+
const width = triggerRect.width;
|
|
37
|
+
setUnderlinePosition({ left, width });
|
|
38
|
+
setShowUnderline(true);
|
|
41
39
|
}, [activeValue]);
|
|
42
40
|
// Update underline position when active value changes or on mount
|
|
43
41
|
useEffect(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
|
-
import React__default, { useMemo, useCallback
|
|
2
|
+
import React__default, { useRef, useEffect, useMemo, useCallback } from 'react';
|
|
3
3
|
import { useReducedMotion, motion } from 'motion/react';
|
|
4
4
|
import { cn } from '../../../utils/cn.js';
|
|
5
5
|
import { tabsTheme } from './Tabs.theme.js';
|
|
@@ -10,11 +10,14 @@ import { getTabsTriggerVariants } from './Tabs.animations.js';
|
|
|
10
10
|
* TabsTrigger component displays individual tab headers with optional animations
|
|
11
11
|
*/
|
|
12
12
|
const TabsTrigger = React__default.forwardRef(({ value, disabled = false, children, className, onClick, onKeyDown, ...props }, ref) => {
|
|
13
|
-
const { value: selectedValue, onValueChange, orientation, size, disabled: contextDisabled, registerTrigger, unregisterTrigger } = useTabsContext();
|
|
13
|
+
const { value: selectedValue, onValueChange, orientation, size, disabled: contextDisabled, registerTrigger, unregisterTrigger, instanceId } = useTabsContext();
|
|
14
14
|
const shouldReduceMotion = useReducedMotion();
|
|
15
|
-
// Check if we're in Storybook
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
// Check if we're in Storybook — evaluated after mount to avoid SSR hydration mismatch
|
|
16
|
+
const isInStorybookRef = useRef(false);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
isInStorybookRef.current = typeof window !== 'undefined' && !!window.__STORYBOOK_DISABLE_ANIMATIONS__;
|
|
19
|
+
}, []);
|
|
20
|
+
const shouldDisableAnimations = !!shouldReduceMotion || !!isInStorybookRef.current;
|
|
18
21
|
// Memoize derived state flags for performance
|
|
19
22
|
const derivedState = useMemo(() => ({
|
|
20
23
|
isSelected: value === selectedValue,
|
|
@@ -51,7 +54,6 @@ const TabsTrigger = React__default.forwardRef(({ value, disabled = false, childr
|
|
|
51
54
|
}, [isDisabled, onValueChange, value, onClick]);
|
|
52
55
|
// Enhanced keyboard navigation handler with error handling
|
|
53
56
|
const handleKeyDown = useCallback((event) => {
|
|
54
|
-
var _a, _b;
|
|
55
57
|
// Handle keyboard navigation for tabs
|
|
56
58
|
if (!isDisabled) {
|
|
57
59
|
try {
|
|
@@ -78,20 +80,32 @@ const TabsTrigger = React__default.forwardRef(({ value, disabled = false, childr
|
|
|
78
80
|
break;
|
|
79
81
|
case "Home":
|
|
80
82
|
event.preventDefault();
|
|
81
|
-
// Move to first tab
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
// Move to first tab — use role="tab" to skip the sliding underline element
|
|
84
|
+
{
|
|
85
|
+
const tabList = event.currentTarget.parentElement;
|
|
86
|
+
const allTabs = tabList
|
|
87
|
+
? Array.from(tabList.querySelectorAll('[role="tab"]:not([disabled])'))
|
|
88
|
+
: [];
|
|
89
|
+
const firstTrigger = allTabs[0];
|
|
90
|
+
if (firstTrigger) {
|
|
91
|
+
firstTrigger.focus();
|
|
92
|
+
firstTrigger.click();
|
|
93
|
+
}
|
|
86
94
|
}
|
|
87
95
|
break;
|
|
88
96
|
case "End":
|
|
89
97
|
event.preventDefault();
|
|
90
|
-
// Move to last tab
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
// Move to last tab — use role="tab" to skip the sliding underline element
|
|
99
|
+
{
|
|
100
|
+
const tabList = event.currentTarget.parentElement;
|
|
101
|
+
const allTabs = tabList
|
|
102
|
+
? Array.from(tabList.querySelectorAll('[role="tab"]:not([disabled])'))
|
|
103
|
+
: [];
|
|
104
|
+
const lastTrigger = allTabs[allTabs.length - 1];
|
|
105
|
+
if (lastTrigger) {
|
|
106
|
+
lastTrigger.focus();
|
|
107
|
+
lastTrigger.click();
|
|
108
|
+
}
|
|
95
109
|
}
|
|
96
110
|
break;
|
|
97
111
|
}
|
|
@@ -114,18 +128,23 @@ const TabsTrigger = React__default.forwardRef(({ value, disabled = false, childr
|
|
|
114
128
|
}, [size]);
|
|
115
129
|
// Generate default aria-label for icon-only triggers if not provided (fallback only)
|
|
116
130
|
const ariaLabel = props["aria-label"] || undefined;
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return () => {
|
|
126
|
-
clearTimeout(timeoutId);
|
|
131
|
+
// Callback ref that registers/unregisters the trigger for underline positioning.
|
|
132
|
+
// Using a callback ref ensures registration fires whenever the DOM node mounts,
|
|
133
|
+
// not just when the ref object identity changes.
|
|
134
|
+
const triggerRef = useCallback((node) => {
|
|
135
|
+
if (node) {
|
|
136
|
+
registerTrigger(value, node);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
127
139
|
unregisterTrigger(value);
|
|
128
|
-
}
|
|
140
|
+
}
|
|
141
|
+
// Forward to the outer ref passed by the consumer, if any
|
|
142
|
+
if (typeof ref === 'function') {
|
|
143
|
+
ref(node);
|
|
144
|
+
}
|
|
145
|
+
else if (ref) {
|
|
146
|
+
ref.current = node;
|
|
147
|
+
}
|
|
129
148
|
}, [value, registerTrigger, unregisterTrigger, ref]);
|
|
130
149
|
// Determine animation props (user props will override due to spread order)
|
|
131
150
|
const animationProps = shouldDisableAnimations ? {} : {
|
|
@@ -133,7 +152,7 @@ const TabsTrigger = React__default.forwardRef(({ value, disabled = false, childr
|
|
|
133
152
|
whileTap: !isDisabled ? "tap" : undefined,
|
|
134
153
|
variants: triggerVariants,
|
|
135
154
|
};
|
|
136
|
-
return (jsx(motion.button, { ref:
|
|
155
|
+
return (jsx(motion.button, { ref: triggerRef, type: "button", role: "tab", "aria-selected": isSelected, "aria-controls": `${instanceId}-${value}-content`, id: `${instanceId}-${value}-tab`, tabIndex: isSelected ? 0 : -1, disabled: isDisabled, className: cn(themeClasses.triggerStyle, themeClasses.sizeStyle, className // User overrides take precedence
|
|
137
156
|
), onClick: handleClick, onKeyDown: handleKeyDown, "data-value": value, "data-state": isSelected ? "active" : "inactive", "data-disabled": isDisabled, "data-orientation": orientation, "aria-disabled": isDisabled || undefined, "aria-label": ariaLabel, ...animationProps, ...props, children: children }));
|
|
138
157
|
});
|
|
139
158
|
TabsTrigger.displayName = "TabsTrigger";
|
|
@@ -84,7 +84,7 @@ const Toast = React__default.forwardRef(({ toast, className, onDismiss, index =
|
|
|
84
84
|
}, [stackingStyle.zIndex, index]);
|
|
85
85
|
const toastNode = (jsxs(motion.div, { ref: setRefs, layout: true, initial: shouldReduceMotion ? false : toastAnimations.initial, animate: animateValue, exit: shouldReduceMotion
|
|
86
86
|
? { ...toastAnimations.exit, transition: { duration: 0 } }
|
|
87
|
-
: toastAnimations.exit, transition: shouldReduceMotion ? { duration: 0 } : toastAnimations.transition, className: cn(toastTheme.baseStyle, isTextOnly && "items-center", ((_a = toastTheme.variants) === null || _a === void 0 ? void 0 : _a[variant]) || "", "pointer-events-auto", index > 0 && !isExpanded && "absolute top-0 left-0", className), style: styleValue, role: "
|
|
87
|
+
: toastAnimations.exit, transition: shouldReduceMotion ? { duration: 0 } : toastAnimations.transition, className: cn(toastTheme.baseStyle, isTextOnly && "items-center", ((_a = toastTheme.variants) === null || _a === void 0 ? void 0 : _a[variant]) || "", "pointer-events-auto", index > 0 && !isExpanded && "absolute top-0 left-0", className), style: styleValue, role: "status", "aria-live": "polite", children: [jsx("div", { className: cn(toastTheme.iconStyle, !isTextOnly && "mt-0.5"), children: resolvedIcon }), jsxs("div", { className: "flex-1 min-w-0", children: [title &&
|
|
88
88
|
(typeof title === "string" ? (jsx("div", { className: cn(toastTheme.titleStyle, !isTextOnly && "font-semibold"), children: title })) : (title)), subtitle &&
|
|
89
89
|
(typeof subtitle === "string" ? (jsx("div", { className: toastTheme.subtitleStyle, children: subtitle })) : (subtitle)), action && (jsx("div", { className: "mt-4", children: isToastAction(action) ? (jsx(Button, { variant: "link", color: "primary", onClick: handleActionClick, children: action.label })) : (action) })), children] }), dismissible && (jsx("button", { type: "button", className: cn(toastTheme.closeButtonStyle, !isTextOnly && "mt-0.5"), onClick: handleDismiss, "aria-label": "Dismiss notification", children: jsx(XMarkIcon, { className: "size-5" }) }))] }));
|
|
90
90
|
if (darkMode) {
|
|
@@ -24,7 +24,7 @@ const ToastProvider = ({ children, position = 'top-right', maxToasts = 5, defaul
|
|
|
24
24
|
const hoverTimeoutRef = React__default.useRef(null);
|
|
25
25
|
const addToast = useCallback((toastData) => {
|
|
26
26
|
var _a;
|
|
27
|
-
const id =
|
|
27
|
+
const id = crypto.randomUUID();
|
|
28
28
|
const newToast = {
|
|
29
29
|
...toastData,
|
|
30
30
|
id,
|
|
@@ -91,7 +91,8 @@ const ToastProvider = ({ children, position = 'top-right', maxToasts = 5, defaul
|
|
|
91
91
|
removeToast,
|
|
92
92
|
removeAllToasts,
|
|
93
93
|
}), [toasts, addToast, removeToast, removeAllToasts]);
|
|
94
|
-
|
|
94
|
+
const portalTarget = isClient ? document.body : null;
|
|
95
|
+
return (jsxs(ToastContext.Provider, { value: contextValue, children: [children, portalTarget ? createPortal(jsx("div", { className: cn(toastTheme.containerStyle, positionStyles[position]), "aria-live": "polite", "aria-label": "Notifications", children: jsx("div", { className: "relative w-80", style: { paddingBottom: isHovered && toasts.length > 1 ? totalExpandedHeight : undefined }, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: jsx(AnimatePresence, { mode: "popLayout", children: toasts.map((toast, index) => (jsx(Toast, { toast: toast, onDismiss: removeToast, index: index, isExpanded: isHovered, totalToasts: toasts.length, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onHeightChange: handleToastHeight, expandedOffset: getExpandedOffset(index), darkMode: darkMode }, toast.id))) }) }) }), portalTarget) : null] }));
|
|
95
96
|
};
|
|
96
97
|
const useToastContext = () => {
|
|
97
98
|
const context = useContext(ToastContext);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TooltipProps } from "./Tooltip.types";
|
|
2
2
|
declare const Tooltip: {
|
|
3
|
-
({ content, children, placement, delay, disabled, className, maxWidth, minWidth, showOnFocus, wrapText, id, theme: themeOverrides, "aria-label": ariaLabel, role, offset, darkMode, }: TooltipProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
({ content, children, placement, delay, disabled, className, triggerClassName, maxWidth, minWidth, showOnFocus, wrapText, id, theme: themeOverrides, "aria-label": ariaLabel, role, offset, darkMode, }: TooltipProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
displayName: string;
|
|
5
5
|
};
|
|
6
6
|
export { Tooltip };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import React__default, { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
3
|
import { useReducedMotion, AnimatePresence, motion } from 'motion/react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { cn } from '../../../utils/cn.js';
|
|
@@ -7,37 +7,9 @@ import { tooltipTheme } from './Tooltip.theme.js';
|
|
|
7
7
|
import { getTooltipVariants } from './Tooltip.animations.js';
|
|
8
8
|
import { useSelectPortal } from '../../../hooks/useSelectPortal.js';
|
|
9
9
|
import { useIsClient } from '../../../hooks/useIsClient.js';
|
|
10
|
+
import { useDarkMode } from '../../../hooks/useDarkMode.js';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
* Hook to detect dark mode state from document
|
|
13
|
-
*/
|
|
14
|
-
const useDarkMode = () => {
|
|
15
|
-
const [isDark, setIsDark] = useState(false);
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (typeof document === "undefined")
|
|
18
|
-
return;
|
|
19
|
-
const checkDarkMode = () => {
|
|
20
|
-
const hasDarkClass = document.documentElement.classList.contains("dark") ||
|
|
21
|
-
document.body.classList.contains("dark") ||
|
|
22
|
-
document.querySelector(".dark") !== null;
|
|
23
|
-
setIsDark(hasDarkClass);
|
|
24
|
-
};
|
|
25
|
-
checkDarkMode();
|
|
26
|
-
// Observe class changes on html and body
|
|
27
|
-
const observer = new MutationObserver(checkDarkMode);
|
|
28
|
-
observer.observe(document.documentElement, {
|
|
29
|
-
attributes: true,
|
|
30
|
-
attributeFilter: ["class"],
|
|
31
|
-
});
|
|
32
|
-
observer.observe(document.body, {
|
|
33
|
-
attributes: true,
|
|
34
|
-
attributeFilter: ["class"],
|
|
35
|
-
});
|
|
36
|
-
return () => observer.disconnect();
|
|
37
|
-
}, []);
|
|
38
|
-
return isDark;
|
|
39
|
-
};
|
|
40
|
-
const Tooltip = ({ content, children, placement = "top", delay = 300, disabled = false, className, maxWidth = "360px", minWidth = "fit-content", showOnFocus = true, wrapText = false, id, theme: themeOverrides, "aria-label": ariaLabel, role = "tooltip", offset = 8, darkMode = false, }) => {
|
|
12
|
+
const Tooltip = ({ content, children, placement = "top", delay = 300, disabled = false, className, triggerClassName, maxWidth = "360px", minWidth = "fit-content", showOnFocus = true, wrapText = false, id, theme: themeOverrides, "aria-label": ariaLabel, role = "tooltip", offset = 8, darkMode = false, }) => {
|
|
41
13
|
const [isVisible, setIsVisible] = useState(false);
|
|
42
14
|
const tooltipRef = useRef(null);
|
|
43
15
|
const triggerRef = useRef(null);
|
|
@@ -91,11 +63,14 @@ const Tooltip = ({ content, children, placement = "top", delay = 300, disabled =
|
|
|
91
63
|
handleHideTooltip();
|
|
92
64
|
}
|
|
93
65
|
}, [handleHideTooltip]);
|
|
94
|
-
//
|
|
66
|
+
// Determine whether the child can receive focus events via cloneElement.
|
|
67
|
+
// If so, focus/blur are forwarded to the child; otherwise fall back to the wrapper.
|
|
68
|
+
const childCanReceiveFocus = showOnFocus && React__default.isValidElement(children);
|
|
69
|
+
// Event handlers for the wrapper div (focus/blur only go here as fallback)
|
|
95
70
|
const triggerHandlers = {
|
|
96
71
|
onMouseEnter: handleShowTooltip,
|
|
97
72
|
onMouseLeave: handleHideTooltip,
|
|
98
|
-
...(showOnFocus && {
|
|
73
|
+
...(showOnFocus && !childCanReceiveFocus && {
|
|
99
74
|
onFocus: handleShowTooltip,
|
|
100
75
|
onBlur: handleHideTooltip,
|
|
101
76
|
}),
|
|
@@ -122,8 +97,24 @@ const Tooltip = ({ content, children, placement = "top", delay = 300, disabled =
|
|
|
122
97
|
...cssVariables,
|
|
123
98
|
top: `${position.top}px`,
|
|
124
99
|
left: `${position.left}px`,
|
|
125
|
-
}, className: cn(theme.baseStyle, widthClasses, className), "aria-hidden": !isVisible, "aria-label": ariaLabel,
|
|
126
|
-
|
|
100
|
+
}, className: cn(theme.baseStyle, widthClasses, className), "aria-hidden": !isVisible, "aria-label": ariaLabel, variants: shouldReduceMotion ? {} : getTooltipVariants(placement), initial: shouldReduceMotion ? undefined : "initial", animate: shouldReduceMotion ? undefined : "animate", exit: shouldReduceMotion ? undefined : "exit", children: content })) }));
|
|
101
|
+
// When the child is a valid element, forward focus/blur events directly onto it
|
|
102
|
+
// instead of adding tabIndex to the wrapper (which would create a double Tab stop).
|
|
103
|
+
const childWithFocusHandlers = childCanReceiveFocus
|
|
104
|
+
? React__default.cloneElement(children, {
|
|
105
|
+
onFocus: (e) => {
|
|
106
|
+
var _a, _b;
|
|
107
|
+
handleShowTooltip();
|
|
108
|
+
(_b = (_a = children.props).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
109
|
+
},
|
|
110
|
+
onBlur: (e) => {
|
|
111
|
+
var _a, _b;
|
|
112
|
+
handleHideTooltip();
|
|
113
|
+
(_b = (_a = children.props).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
: children;
|
|
117
|
+
return (jsxs("div", { className: cn("relative inline-block", triggerClassName), ref: triggerRef, ...triggerHandlers, "aria-describedby": isVisible && id ? id : undefined, children: [childWithFocusHandlers, isClient &&
|
|
127
118
|
createPortal(jsx("div", { className: darkMode || isDarkMode ? "dark" : undefined, children: tooltipContent }), document.body)] }));
|
|
128
119
|
};
|
|
129
120
|
// Set display name for dev tools and Storybook
|
|
@@ -38,9 +38,13 @@ export interface TooltipProps {
|
|
|
38
38
|
*/
|
|
39
39
|
disabled?: boolean;
|
|
40
40
|
/**
|
|
41
|
-
* Additional classes for the tooltip container
|
|
41
|
+
* Additional classes for the tooltip container (the tooltip bubble)
|
|
42
42
|
*/
|
|
43
43
|
className?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Additional classes for the trigger wrapper element
|
|
46
|
+
*/
|
|
47
|
+
triggerClassName?: string;
|
|
44
48
|
/**
|
|
45
49
|
* Max width for the tooltip content
|
|
46
50
|
* @default "360px"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
-
import React__default, { useCallback, useState, useMemo } from 'react';
|
|
2
|
+
import React__default, { useCallback, useState, useRef, useMemo } from 'react';
|
|
3
3
|
import { motion, AnimatePresence, useReducedMotion } from 'motion/react';
|
|
4
4
|
import { cn } from '../../../utils/cn.js';
|
|
5
5
|
import { treeTheme } from './Tree.theme.js';
|
|
@@ -7,6 +7,32 @@ import { Checkbox } from '../../forms/Checkbox/Checkbox.js';
|
|
|
7
7
|
import { ChevronRightIcon, DocumentIcon } from '@heroicons/react/24/outline';
|
|
8
8
|
import { getReducedMotionVariants, treeNodeVariants, expandIconVariants, childNodeVariants, childrenStagger } from './Tree.animations.js';
|
|
9
9
|
|
|
10
|
+
// Build a flat ordered list of visible (non-collapsed) node ids via depth-first traversal.
|
|
11
|
+
// Disabled nodes are included in the list so that ArrowDown/Up skip over them in the
|
|
12
|
+
// keyboard handler rather than here — keeping the traversal pure.
|
|
13
|
+
const buildVisibleNodeList = (nodes, expandedSet) => {
|
|
14
|
+
const result = [];
|
|
15
|
+
const traverse = (currentNodes) => {
|
|
16
|
+
for (const node of currentNodes) {
|
|
17
|
+
result.push(node.id);
|
|
18
|
+
if (node.children && node.children.length > 0 && expandedSet.has(node.id)) {
|
|
19
|
+
traverse(node.children);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
traverse(nodes);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
// Build a map from node id → parent node id for ArrowLeft "move to parent" navigation.
|
|
27
|
+
const buildParentMap = (nodes, parentId = null, acc = new Map()) => {
|
|
28
|
+
for (const node of nodes) {
|
|
29
|
+
acc.set(node.id, parentId);
|
|
30
|
+
if (node.children && node.children.length > 0) {
|
|
31
|
+
buildParentMap(node.children, node.id, acc);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return acc;
|
|
35
|
+
};
|
|
10
36
|
// Simplified selection utilities (merged from Tree.utils.ts)
|
|
11
37
|
const getDescendantIds = (node) => {
|
|
12
38
|
const descendants = [];
|
|
@@ -72,7 +98,7 @@ const calculateSelection = (nodeId, selectedKeys, data) => {
|
|
|
72
98
|
indeterminateKeys
|
|
73
99
|
};
|
|
74
100
|
};
|
|
75
|
-
const InternalTreeNode = React__default.memo(({ node, level, expanded, selected, isIndeterminate, selectable, checkable, hierarchicalSelection, expandable, expandedKeysSet, selectedKeysSet, indeterminateKeysSet, onNodeClick, onToggleExpand, theme, className, shouldReduceMotion, }) => {
|
|
101
|
+
const InternalTreeNode = React__default.memo(({ node, level, expanded, selected, isIndeterminate, selectable, checkable, hierarchicalSelection, expandable, expandedKeysSet, selectedKeysSet, indeterminateKeysSet, onNodeClick, onToggleExpand, onKeyboardNavigate, nodeRefs, focusedNodeId, onFocusNode, theme, className, shouldReduceMotion, }) => {
|
|
76
102
|
var _a;
|
|
77
103
|
const hasChildren = node.children && node.children.length > 0;
|
|
78
104
|
const isExpandable = expandable && hasChildren;
|
|
@@ -80,20 +106,40 @@ const InternalTreeNode = React__default.memo(({ node, level, expanded, selected,
|
|
|
80
106
|
const nodeVariants = shouldReduceMotion ? getReducedMotionVariants(treeNodeVariants) : treeNodeVariants;
|
|
81
107
|
const iconVariants = shouldReduceMotion ? getReducedMotionVariants(expandIconVariants) : expandIconVariants;
|
|
82
108
|
const childVariants = shouldReduceMotion ? getReducedMotionVariants(childNodeVariants) : childNodeVariants;
|
|
109
|
+
// Register/unregister this node's DOM element in the shared refs map.
|
|
110
|
+
const itemRef = useCallback((el) => {
|
|
111
|
+
if (el) {
|
|
112
|
+
nodeRefs.current.set(node.id, el);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
nodeRefs.current.delete(node.id);
|
|
116
|
+
}
|
|
117
|
+
}, [node.id, nodeRefs]);
|
|
118
|
+
// Roving tabIndex: only the focused node is reachable via Tab.
|
|
119
|
+
// If no node is focused yet, the first root node (level 0) gets tabIndex 0 as
|
|
120
|
+
// a sensible initial entry point — this is handled at the Tree root.
|
|
121
|
+
const isFocused = focusedNodeId === node.id;
|
|
122
|
+
const tabIndexValue = node.disabled ? -1 : (isFocused ? 0 : -1);
|
|
83
123
|
const handleNodeClick = useCallback((e) => {
|
|
84
124
|
e.stopPropagation();
|
|
85
125
|
if (node.disabled)
|
|
86
126
|
return;
|
|
127
|
+
onFocusNode(node.id);
|
|
87
128
|
if (selectable) {
|
|
88
129
|
onNodeClick(node);
|
|
89
130
|
}
|
|
90
|
-
}, [node, selectable, onNodeClick]);
|
|
131
|
+
}, [node, selectable, onNodeClick, onFocusNode]);
|
|
91
132
|
const handleExpandClick = useCallback((e) => {
|
|
92
133
|
e.stopPropagation();
|
|
93
134
|
if (isExpandable && !node.disabled) {
|
|
94
135
|
onToggleExpand(node);
|
|
95
136
|
}
|
|
96
137
|
}, [isExpandable, node, onToggleExpand]);
|
|
138
|
+
const handleFocus = useCallback(() => {
|
|
139
|
+
if (!node.disabled) {
|
|
140
|
+
onFocusNode(node.id);
|
|
141
|
+
}
|
|
142
|
+
}, [node.id, node.disabled, onFocusNode]);
|
|
97
143
|
const handleKeyDown = (e) => {
|
|
98
144
|
if (node.disabled)
|
|
99
145
|
return;
|
|
@@ -106,16 +152,43 @@ const InternalTreeNode = React__default.memo(({ node, level, expanded, selected,
|
|
|
106
152
|
}
|
|
107
153
|
break;
|
|
108
154
|
case 'ArrowRight':
|
|
155
|
+
e.preventDefault();
|
|
109
156
|
if (isExpandable && !expanded) {
|
|
110
|
-
|
|
157
|
+
// Collapsed node with children: expand it
|
|
111
158
|
onToggleExpand(node);
|
|
112
159
|
}
|
|
160
|
+
else if (isExpandable && expanded) {
|
|
161
|
+
// Expanded node with children: move focus to first child
|
|
162
|
+
onKeyboardNavigate(node.id, 'ArrowRight');
|
|
163
|
+
}
|
|
164
|
+
// Leaf node: do nothing (APG spec)
|
|
113
165
|
break;
|
|
114
166
|
case 'ArrowLeft':
|
|
167
|
+
e.preventDefault();
|
|
115
168
|
if (isExpandable && expanded) {
|
|
116
|
-
|
|
169
|
+
// Expanded node: collapse it
|
|
117
170
|
onToggleExpand(node);
|
|
118
171
|
}
|
|
172
|
+
else {
|
|
173
|
+
// Collapsed node or leaf: move to parent
|
|
174
|
+
onKeyboardNavigate(node.id, 'ArrowLeft');
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
case 'ArrowDown':
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
onKeyboardNavigate(node.id, 'ArrowDown');
|
|
180
|
+
break;
|
|
181
|
+
case 'ArrowUp':
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
onKeyboardNavigate(node.id, 'ArrowUp');
|
|
184
|
+
break;
|
|
185
|
+
case 'Home':
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
onKeyboardNavigate(node.id, 'Home');
|
|
188
|
+
break;
|
|
189
|
+
case 'End':
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
onKeyboardNavigate(node.id, 'End');
|
|
119
192
|
break;
|
|
120
193
|
}
|
|
121
194
|
};
|
|
@@ -126,10 +199,10 @@ const InternalTreeNode = React__default.memo(({ node, level, expanded, selected,
|
|
|
126
199
|
const labelStyle = cn(theme.labelStyle, {
|
|
127
200
|
'text-[var(--color-text-muted)]': node.disabled,
|
|
128
201
|
});
|
|
129
|
-
return (jsxs("div", { children: [jsxs("div", { className: nodeStyle, style: { paddingLeft: `calc(${level} * ${theme.indentSize} + 0.25rem)` }, onClick: handleNodeClick, onKeyDown: handleKeyDown,
|
|
202
|
+
return (jsxs("div", { children: [jsxs("div", { ref: itemRef, className: nodeStyle, style: { paddingLeft: `calc(${level} * ${theme.indentSize} + 0.25rem)` }, onClick: handleNodeClick, onKeyDown: handleKeyDown, onFocus: handleFocus, tabIndex: tabIndexValue, role: "treeitem", "aria-expanded": isExpandable ? expanded : undefined, "aria-selected": selectable ? selected : undefined, "aria-disabled": node.disabled, children: [isExpandable ? (jsx(motion.span, { className: cn(theme.expandIconStyle), variants: iconVariants, initial: "collapsed", animate: expanded ? "expanded" : "collapsed", onClick: handleExpandClick, "aria-label": expanded ? 'Collapse' : 'Expand', tabIndex: -1, children: jsx(ChevronRightIcon, { className: "size-4", strokeWidth: 2 }) })) : (jsx("span", { className: "w-4 mr-1" })), checkable && (jsx(Checkbox, { size: 'sm', id: `tree-checkbox-${node.id}`, name: `tree-checkbox-${node.id}`, value: node.id, checked: selected, indeterminate: hierarchicalSelection && isIndeterminate, onChange: () => onNodeClick(node), state: node.disabled ? "disabled" : "default", className: "flex-shrink-0", onClick: (e) => e.stopPropagation() })), (node.icon || !isExpandable) && (jsx("span", { className: cn(theme.iconStyle), children: node.icon || jsx(DocumentIcon, { className: "w-4 h-4" }) })), jsx("span", { className: labelStyle, title: node.label, children: node.label })] }), hasChildren && (jsx(AnimatePresence, { initial: false, children: expanded && (jsx(motion.div, { variants: nodeVariants, initial: "collapsed", animate: "expanded", exit: "collapsed", style: {
|
|
130
203
|
overflow: 'hidden',
|
|
131
204
|
transformOrigin: 'top'
|
|
132
|
-
}, transition: shouldReduceMotion ? { duration: 0.01 } : childrenStagger, children: (_a = node.children) === null || _a === void 0 ? void 0 : _a.map((childNode, index) => (jsx(motion.div, { variants: childVariants, initial: "hidden", animate: "visible", exit: "hidden", custom: index, children: jsx(InternalTreeNode, { node: childNode, level: level + 1, expanded: expandedKeysSet.has(childNode.id), selected: selectedKeysSet.has(childNode.id), isIndeterminate: indeterminateKeysSet.has(childNode.id), selectable: selectable, checkable: checkable, hierarchicalSelection: hierarchicalSelection, expandable: expandable, expandedKeysSet: expandedKeysSet, selectedKeysSet: selectedKeysSet, indeterminateKeysSet: indeterminateKeysSet, onNodeClick: onNodeClick, onToggleExpand: onToggleExpand, theme: theme, shouldReduceMotion: shouldReduceMotion }) }, childNode.id))) }, `${node.id}-children`)) }))] }));
|
|
205
|
+
}, transition: shouldReduceMotion ? { duration: 0.01 } : childrenStagger, children: (_a = node.children) === null || _a === void 0 ? void 0 : _a.map((childNode, index) => (jsx(motion.div, { variants: childVariants, initial: "hidden", animate: "visible", exit: "hidden", custom: index, children: jsx(InternalTreeNode, { node: childNode, level: level + 1, expanded: expandedKeysSet.has(childNode.id), selected: selectedKeysSet.has(childNode.id), isIndeterminate: indeterminateKeysSet.has(childNode.id), selectable: selectable, checkable: checkable, hierarchicalSelection: hierarchicalSelection, expandable: expandable, expandedKeysSet: expandedKeysSet, selectedKeysSet: selectedKeysSet, indeterminateKeysSet: indeterminateKeysSet, onNodeClick: onNodeClick, onToggleExpand: onToggleExpand, onKeyboardNavigate: onKeyboardNavigate, nodeRefs: nodeRefs, focusedNodeId: focusedNodeId, onFocusNode: onFocusNode, theme: theme, shouldReduceMotion: shouldReduceMotion }) }, childNode.id))) }, `${node.id}-children`)) }))] }));
|
|
133
206
|
});
|
|
134
207
|
const TreeRoot = React__default.forwardRef(({ data, selectable = false, checkable = false, hierarchicalSelection, expandable = true, defaultExpandedKeys = [], defaultSelectedKeys = [], onSelectionChange, onExpand, className, theme: customTheme = {}, ...htmlProps }, ref) => {
|
|
135
208
|
var _a;
|
|
@@ -144,9 +217,106 @@ const TreeRoot = React__default.forwardRef(({ data, selectable = false, checkabl
|
|
|
144
217
|
const [expandedKeys, setExpandedKeys] = useState(defaultExpandedKeys);
|
|
145
218
|
const [selectedKeys, setSelectedKeys] = useState(defaultSelectedKeys);
|
|
146
219
|
const [indeterminateKeys, setIndeterminateKeys] = useState([]);
|
|
220
|
+
// Roving tabIndex: track which node currently owns tabIndex=0.
|
|
221
|
+
// Initialise to the first root node (if any) so the tree is keyboard-enterable.
|
|
222
|
+
const [focusedNodeId, setFocusedNodeId] = useState(data.length > 0 ? data[0].id : null);
|
|
223
|
+
// Map from node id → DOM element, populated by each InternalTreeNode on mount.
|
|
224
|
+
const nodeRefs = useRef(new Map());
|
|
147
225
|
const expandedKeysSet = useMemo(() => new Set(expandedKeys), [expandedKeys]);
|
|
148
226
|
const selectedKeysSet = useMemo(() => new Set(selectedKeys), [selectedKeys]);
|
|
149
227
|
const indeterminateKeysSet = useMemo(() => new Set(indeterminateKeys), [indeterminateKeys]);
|
|
228
|
+
// Parent map for ArrowLeft navigation (move to parent node).
|
|
229
|
+
const parentMap = useMemo(() => buildParentMap(data), [data]);
|
|
230
|
+
// Called by InternalTreeNode when it receives focus (mouse or keyboard).
|
|
231
|
+
const handleFocusNode = useCallback((nodeId) => {
|
|
232
|
+
setFocusedNodeId(nodeId);
|
|
233
|
+
}, []);
|
|
234
|
+
// Central keyboard navigation handler: moves focus within the tree.
|
|
235
|
+
const handleKeyboardNavigate = useCallback((nodeId, key) => {
|
|
236
|
+
const visibleIds = buildVisibleNodeList(data, expandedKeysSet);
|
|
237
|
+
const focusById = (targetId) => {
|
|
238
|
+
const el = nodeRefs.current.get(targetId);
|
|
239
|
+
if (el) {
|
|
240
|
+
setFocusedNodeId(targetId);
|
|
241
|
+
el.focus();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const currentIndex = visibleIds.indexOf(nodeId);
|
|
245
|
+
switch (key) {
|
|
246
|
+
case 'ArrowDown': {
|
|
247
|
+
// Move to next visible node, skipping disabled ones.
|
|
248
|
+
for (let i = currentIndex + 1; i < visibleIds.length; i++) {
|
|
249
|
+
const candidateId = visibleIds[i];
|
|
250
|
+
const candidateEl = nodeRefs.current.get(candidateId);
|
|
251
|
+
if (candidateEl && candidateEl.getAttribute('aria-disabled') !== 'true') {
|
|
252
|
+
focusById(candidateId);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'ArrowUp': {
|
|
259
|
+
// Move to previous visible node, skipping disabled ones.
|
|
260
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
261
|
+
const candidateId = visibleIds[i];
|
|
262
|
+
const candidateEl = nodeRefs.current.get(candidateId);
|
|
263
|
+
if (candidateEl && candidateEl.getAttribute('aria-disabled') !== 'true') {
|
|
264
|
+
focusById(candidateId);
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case 'Home': {
|
|
271
|
+
// Move to the first non-disabled visible node.
|
|
272
|
+
for (let i = 0; i < visibleIds.length; i++) {
|
|
273
|
+
const candidateId = visibleIds[i];
|
|
274
|
+
const candidateEl = nodeRefs.current.get(candidateId);
|
|
275
|
+
if (candidateEl && candidateEl.getAttribute('aria-disabled') !== 'true') {
|
|
276
|
+
focusById(candidateId);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case 'End': {
|
|
283
|
+
// Move to the last non-disabled visible node.
|
|
284
|
+
for (let i = visibleIds.length - 1; i >= 0; i--) {
|
|
285
|
+
const candidateId = visibleIds[i];
|
|
286
|
+
const candidateEl = nodeRefs.current.get(candidateId);
|
|
287
|
+
if (candidateEl && candidateEl.getAttribute('aria-disabled') !== 'true') {
|
|
288
|
+
focusById(candidateId);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case 'ArrowRight': {
|
|
295
|
+
// Move focus to the first child of an already-expanded node.
|
|
296
|
+
// (Expand-when-collapsed is handled directly in InternalTreeNode.)
|
|
297
|
+
const firstChildIndex = currentIndex + 1;
|
|
298
|
+
if (firstChildIndex < visibleIds.length) {
|
|
299
|
+
const firstChildId = visibleIds[firstChildIndex];
|
|
300
|
+
const firstChildEl = nodeRefs.current.get(firstChildId);
|
|
301
|
+
if (firstChildEl && firstChildEl.getAttribute('aria-disabled') !== 'true') {
|
|
302
|
+
focusById(firstChildId);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case 'ArrowLeft': {
|
|
308
|
+
// Move focus to the parent node (when current node is collapsed or a leaf).
|
|
309
|
+
const parentId = parentMap.get(nodeId);
|
|
310
|
+
if (parentId) {
|
|
311
|
+
const parentEl = nodeRefs.current.get(parentId);
|
|
312
|
+
if (parentEl && parentEl.getAttribute('aria-disabled') !== 'true') {
|
|
313
|
+
focusById(parentId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}, [data, expandedKeysSet, parentMap]);
|
|
150
320
|
// Handle node selection
|
|
151
321
|
const handleNodeSelect = useCallback((node) => {
|
|
152
322
|
if (!selectable || node.disabled)
|
|
@@ -194,7 +364,7 @@ const TreeRoot = React__default.forwardRef(({ data, selectable = false, checkabl
|
|
|
194
364
|
setExpandedKeys(newExpandedKeys);
|
|
195
365
|
onExpand === null || onExpand === void 0 ? void 0 : onExpand(newExpandedKeys, node, !isExpanded);
|
|
196
366
|
}, [expandedKeys, expandable, onExpand]);
|
|
197
|
-
return (jsx("div", { ref: ref, className: cn(theme.baseStyle, 'tree-container', className), role: "tree", "aria-multiselectable": checkable, ...htmlProps, children: data.map((node) => (jsx(InternalTreeNode, { node: node, level: 0, expanded: expandedKeysSet.has(node.id), selected: selectedKeysSet.has(node.id), isIndeterminate: indeterminateKeysSet.has(node.id), selectable: selectable, checkable: checkable, hierarchicalSelection: enableHierarchicalSelection, expandable: expandable, expandedKeysSet: expandedKeysSet, selectedKeysSet: selectedKeysSet, indeterminateKeysSet: indeterminateKeysSet, onNodeClick: handleNodeSelect, onToggleExpand: handleNodeToggleExpand, theme: theme, shouldReduceMotion: shouldReduceMotion }, node.id))) }));
|
|
367
|
+
return (jsx("div", { ref: ref, className: cn(theme.baseStyle, 'tree-container', className), role: "tree", "aria-multiselectable": checkable, ...htmlProps, children: data.map((node) => (jsx(InternalTreeNode, { node: node, level: 0, expanded: expandedKeysSet.has(node.id), selected: selectedKeysSet.has(node.id), isIndeterminate: indeterminateKeysSet.has(node.id), selectable: selectable, checkable: checkable, hierarchicalSelection: enableHierarchicalSelection, expandable: expandable, expandedKeysSet: expandedKeysSet, selectedKeysSet: selectedKeysSet, indeterminateKeysSet: indeterminateKeysSet, onNodeClick: handleNodeSelect, onToggleExpand: handleNodeToggleExpand, onKeyboardNavigate: handleKeyboardNavigate, nodeRefs: nodeRefs, focusedNodeId: focusedNodeId, onFocusNode: handleFocusNode, theme: theme, shouldReduceMotion: shouldReduceMotion }, node.id))) }));
|
|
198
368
|
});
|
|
199
369
|
TreeRoot.displayName = 'Tree';
|
|
200
370
|
// Simplified compound component
|
|
@@ -100,6 +100,7 @@ export { SidebarSubmenu } from './Sidebar/SidebarSubmenu.js';
|
|
|
100
100
|
export { SidebarToggle } from './Sidebar/SidebarToggle.js';
|
|
101
101
|
export { SidebarMobileTrigger } from './Sidebar/SidebarMobileTrigger.js';
|
|
102
102
|
export { SidebarUserProfile } from './Sidebar/SidebarUserProfile.js';
|
|
103
|
+
export { SidebarLogo } from './Sidebar/SidebarLogo.js';
|
|
103
104
|
export { useSidebarContext } from './Sidebar/SidebarContext.js';
|
|
104
105
|
export { OfflineIndicator } from './OfflineIndicator/OfflineIndicator.js';
|
|
105
106
|
export { offlineIndicatorTheme } from './OfflineIndicator/OfflineIndicator.theme.js';
|