@flikk/ui 1.0.0-beta.23 → 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/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 +13 -8
- 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/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.js +25 -34
- package/dist/components/core/Tree/Tree.js +178 -8
- 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 +14 -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 +2 -29
- package/dist/components/forms/TimePicker/TimePickerTrigger.js +2 -2
- package/dist/components/forms/index.js +1 -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 +1 -57
- package/dist/shadcn-compat.css +80 -0
- package/dist/styles.css +1 -1
- package/dist/utils/stateMachine.d.ts +4 -4
- package/package.json +11 -10
- package/tailwind.preset.cjs +32 -3
- /package/{src/theme-plugin.css → dist/theme.css} +0 -0
|
@@ -35,7 +35,7 @@ const PROGRESS_SPRING_TRANSITION = {
|
|
|
35
35
|
* // Indeterminate (loading)
|
|
36
36
|
* <Progress />
|
|
37
37
|
*/
|
|
38
|
-
const Progress = React__default.forwardRef(({ value, size = "md", showLabel = false, labelPosition = "right", formatLabel, animated = true, wrapperClassName, className, theme: customTheme, ...props }, ref) => {
|
|
38
|
+
const Progress = React__default.forwardRef(({ value, size = "md", showLabel = false, labelPosition = "right", formatLabel, animated = true, wrapperClassName, className, theme: customTheme, 'aria-label': consumerAriaLabel, ...props }, ref) => {
|
|
39
39
|
const shouldReduceMotion = useReducedMotion();
|
|
40
40
|
// Merge custom theme with default theme
|
|
41
41
|
const theme = useMemo(() => {
|
|
@@ -98,11 +98,11 @@ const Progress = React__default.forwardRef(({ value, size = "md", showLabel = fa
|
|
|
98
98
|
const renderTrack = () => (jsxs("div", { className: cn(theme.trackStyle, theme.sizes[size], "relative", wrapperClassName), children: [isIndeterminate ? (
|
|
99
99
|
// Indeterminate animation
|
|
100
100
|
jsx("div", { className: cn(theme.fillStyle, "absolute", theme.indeterminateStyle, className) })) : shouldAnimate ? (
|
|
101
|
-
// Animated determinate progress with spring animation
|
|
102
|
-
jsx(motion.div, { className: cn(theme.fillStyle, className),
|
|
101
|
+
// Animated determinate progress with spring animation (scaleX avoids layout recalculation)
|
|
102
|
+
jsx(motion.div, { className: cn(theme.fillStyle, className), style: { width: '100%', transformOrigin: 'left' }, initial: { scaleX: 0 }, animate: { scaleX: percentage / 100 }, transition: PROGRESS_SPRING_TRANSITION })) : (
|
|
103
103
|
// Static determinate progress
|
|
104
104
|
jsx("div", { className: cn(theme.fillStyle, className), style: { width: `${percentage}%` } })), showLabel && labelPosition === "inside" && size === "lg" && !isIndeterminate && (jsx("span", { className: theme.insideLabelStyle, children: label }))] }));
|
|
105
|
-
return (jsxs("div", { ref: ref, className: cn(theme.containerStyle, containerLayoutClass), role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": isIndeterminate ? undefined : percentage, "aria-label": isIndeterminate ? "Loading" : `${percentage}% complete
|
|
105
|
+
return (jsxs("div", { ref: ref, className: cn(theme.containerStyle, containerLayoutClass), role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": isIndeterminate ? undefined : percentage, "aria-label": consumerAriaLabel !== null && consumerAriaLabel !== void 0 ? consumerAriaLabel : (isIndeterminate ? "Loading" : `${percentage}% complete`), ...props, children: [labelPosition === "top" && renderLabel(), labelPosition === "right" ? (jsxs(Fragment, { children: [jsx("div", { className: "flex-1", children: renderTrack() }), renderLabel()] })) : (renderTrack()), labelPosition === "bottom" && renderLabel()] }));
|
|
106
106
|
});
|
|
107
107
|
Progress.displayName = "Progress";
|
|
108
108
|
|
|
@@ -160,7 +160,7 @@ const Rating = React__default.memo(React__default.forwardRef(({ value: controlle
|
|
|
160
160
|
return (jsx("span", { className: "inline-flex items-center justify-center", children: renderStarIcon() }, starIndex));
|
|
161
161
|
}
|
|
162
162
|
// Interactive: render as a button
|
|
163
|
-
return (jsx("button", { type: "button", className: cn(theme.starStyle, "hover:scale-110 active:scale-95"), onClick: (e) => handleStarClick(starIndex, e), onMouseMove: (e) => handleStarMouseMove(starIndex, e), "aria-label": `Rate ${starIndex} out of ${max}`, tabIndex: -1, children: renderStarIcon() }, starIndex));
|
|
163
|
+
return (jsx("button", { type: "button", className: cn(theme.starStyle, "hover:scale-110 active:scale-95"), onClick: (e) => handleStarClick(starIndex, e), onMouseMove: (e) => handleStarMouseMove(starIndex, e), "aria-label": `Rate ${starIndex} out of ${max}`, tabIndex: -1, "aria-hidden": "true", children: renderStarIcon() }, starIndex));
|
|
164
164
|
};
|
|
165
165
|
// Build array of star indices (1-based)
|
|
166
166
|
const stars = useMemo(() => Array.from({ length: max }, (_, i) => i + 1), [max]);
|
|
@@ -50,8 +50,9 @@ const ScrollArea = React__default.forwardRef(({ children, type = 'hover', size =
|
|
|
50
50
|
const smoothAPI = useSmoothScroll(smooth ? internalRef : null, smoothScrollConfig, smoothOrientation);
|
|
51
51
|
// Expose extended API via ref
|
|
52
52
|
useImperativeHandle(ref, () => {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// internalRef.current is always set by the time the imperative handle is
|
|
54
|
+
// accessed (refs are resolved after mount). The non-null assertion avoids
|
|
55
|
+
// the unsafe `null as unknown as ScrollAreaRef` double-cast.
|
|
55
56
|
const element = internalRef.current;
|
|
56
57
|
// Create extended ref object
|
|
57
58
|
const extendedRef = element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import React__default, { useRef, useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import React__default, { useRef, useState, useCallback, useEffect, useMemo } from 'react';
|
|
3
3
|
import { cn } from '../../../utils/cn.js';
|
|
4
4
|
import { debounce } from '../../../utils/debounce.js';
|
|
5
5
|
import { segmentedTheme } from './Segmented.theme.js';
|
|
@@ -10,11 +10,11 @@ import { SegmentedItem } from './SegmentedItem.js';
|
|
|
10
10
|
* Segmented component for selecting one option from a set of segments
|
|
11
11
|
* Uses compound component pattern with Segmented.Item
|
|
12
12
|
*/
|
|
13
|
-
const SegmentedComponent = React__default.forwardRef(({ value, onChange, size = "md", disabled = false, block = false, children, className, slidingBackgroundClassName, theme, ...props }, ref) => {
|
|
13
|
+
const SegmentedComponent = React__default.forwardRef(({ value, onChange, size = "md", disabled = false, block = false, children, className, slidingBackgroundClassName, theme, "aria-label": ariaLabel, ...props }, ref) => {
|
|
14
14
|
var _a;
|
|
15
15
|
const containerRef = useRef(null);
|
|
16
16
|
const [activeStyle, setActiveStyle] = useState({});
|
|
17
|
-
const updateActiveStyle = () => {
|
|
17
|
+
const updateActiveStyle = useCallback(() => {
|
|
18
18
|
if (!containerRef.current || !value)
|
|
19
19
|
return;
|
|
20
20
|
const activeButton = containerRef.current.querySelector(`[data-value="${value}"]`);
|
|
@@ -28,10 +28,10 @@ const SegmentedComponent = React__default.forwardRef(({ value, onChange, size =
|
|
|
28
28
|
transform: `translateX(${buttonRect.left - containerRect.left}px)`,
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
};
|
|
31
|
+
}, [value]);
|
|
32
32
|
useEffect(() => {
|
|
33
33
|
updateActiveStyle();
|
|
34
|
-
}, [value]);
|
|
34
|
+
}, [value, updateActiveStyle]);
|
|
35
35
|
// Debounced resize handler for better performance
|
|
36
36
|
const debouncedUpdateActiveStyle = useMemo(() => debounce(updateActiveStyle, 100), [updateActiveStyle]);
|
|
37
37
|
useEffect(() => {
|
|
@@ -52,7 +52,7 @@ const SegmentedComponent = React__default.forwardRef(({ value, onChange, size =
|
|
|
52
52
|
else if (ref) {
|
|
53
53
|
ref.current = node;
|
|
54
54
|
}
|
|
55
|
-
}, role: "
|
|
55
|
+
}, role: "radiogroup", "aria-label": ariaLabel, className: cn((theme === null || theme === void 0 ? void 0 : theme.baseStyle) || segmentedTheme.baseStyle, ((_a = theme === null || theme === void 0 ? void 0 : theme.sizes) === null || _a === void 0 ? void 0 : _a[size]) || segmentedTheme.sizes[size], block && ((theme === null || theme === void 0 ? void 0 : theme.blockStyle) || segmentedTheme.blockStyle), disabled && ((theme === null || theme === void 0 ? void 0 : theme.disabledStyle) || segmentedTheme.disabledStyle), className), "data-size": size, "data-disabled": disabled, "data-block": block, ...props, children: [value && (jsx("div", { className: cn((theme === null || theme === void 0 ? void 0 : theme.slidingBackgroundStyle) ||
|
|
56
56
|
segmentedTheme.slidingBackgroundStyle, slidingBackgroundClassName), style: activeStyle })), children] }) }));
|
|
57
57
|
});
|
|
58
58
|
// Set display name for dev tools and Storybook
|
|
@@ -18,10 +18,36 @@ const SegmentedItem = React__default.forwardRef(({ value, disabled = false, chil
|
|
|
18
18
|
onChange === null || onChange === void 0 ? void 0 : onChange(value);
|
|
19
19
|
onClick === null || onClick === void 0 ? void 0 : onClick(event);
|
|
20
20
|
};
|
|
21
|
-
|
|
21
|
+
const handleKeyDown = (event) => {
|
|
22
|
+
if (isDisabled)
|
|
23
|
+
return;
|
|
24
|
+
const radioGroup = event.currentTarget.parentElement;
|
|
25
|
+
if (!radioGroup)
|
|
26
|
+
return;
|
|
27
|
+
const radios = Array.from(radioGroup.querySelectorAll('[role="radio"]:not([disabled])'));
|
|
28
|
+
const currentIndex = radios.indexOf(event.currentTarget);
|
|
29
|
+
let targetRadio;
|
|
30
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
targetRadio = radios[(currentIndex + 1) % radios.length];
|
|
33
|
+
}
|
|
34
|
+
else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
targetRadio = radios[(currentIndex - 1 + radios.length) % radios.length];
|
|
37
|
+
}
|
|
38
|
+
if (targetRadio) {
|
|
39
|
+
targetRadio.focus();
|
|
40
|
+
// Read the value from the data attribute and trigger selection
|
|
41
|
+
const targetValue = targetRadio.getAttribute('data-value');
|
|
42
|
+
if (targetValue) {
|
|
43
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(targetValue);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return (jsx("button", { ref: ref, type: "button", role: "radio", "aria-checked": isSelected, tabIndex: isSelected ? 0 : -1, disabled: isDisabled, className: cn(segmentedItemTheme.baseStyle, segmentedItemTheme.sizes[size], isSelected
|
|
22
48
|
? segmentedItemTheme.selectedStyle
|
|
23
49
|
: segmentedItemTheme.unselectedStyle, isDisabled && segmentedItemTheme.disabledStyle, className // User overrides take precedence
|
|
24
|
-
), onClick: handleClick, "data-value": value, "data-selected": isSelected, "data-disabled": isDisabled, ...props, children: children }));
|
|
50
|
+
), onClick: handleClick, onKeyDown: handleKeyDown, "data-value": value, "data-selected": isSelected, "data-disabled": isDisabled, ...props, children: children }));
|
|
25
51
|
});
|
|
26
52
|
// Set display name for dev tools and Storybook
|
|
27
53
|
SegmentedItem.displayName = "Segmented.Item";
|
|
@@ -50,11 +50,11 @@ const Skeleton = React__default.forwardRef(({ variant = 'rectangle', width, heig
|
|
|
50
50
|
// Make last line 75% width for more natural look
|
|
51
51
|
const isLastLine = index === lines - 1;
|
|
52
52
|
const lineWidth = isLastLine && !width ? '75%' : computedWidth;
|
|
53
|
-
return (jsx("div", { className: baseClasses, style: { ...computedStyle, width: lineWidth } }, index));
|
|
53
|
+
return (jsx("div", { className: baseClasses, style: { ...computedStyle, width: lineWidth }, "aria-hidden": "true" }, index));
|
|
54
54
|
}) }));
|
|
55
55
|
}
|
|
56
56
|
// Single skeleton
|
|
57
|
-
return (jsx("div", { ref: ref, className: baseClasses, style: computedStyle, "aria-
|
|
57
|
+
return (jsx("div", { ref: ref, className: baseClasses, style: computedStyle, "aria-hidden": "true", ...props }));
|
|
58
58
|
});
|
|
59
59
|
Skeleton.displayName = 'Skeleton';
|
|
60
60
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { SortableProps, SortableItemProps } from './Sortable.types';
|
|
3
3
|
export declare const Sortable: React.ForwardRefExoticComponent<SortableProps & React.RefAttributes<HTMLUListElement>>;
|
|
4
|
-
export declare const SortableItem: React.ForwardRefExoticComponent<SortableItemProps & React.RefAttributes<
|
|
4
|
+
export declare const SortableItem: React.ForwardRefExoticComponent<SortableItemProps & React.RefAttributes<HTMLLIElement>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import React__default, { createContext, useState, useRef, useMemo, useCallback, useEffect, useContext } from 'react';
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import React__default, { createContext, useState, useRef, useId, useMemo, useCallback, useEffect, useContext } from 'react';
|
|
3
3
|
import { Reorder, useDragControls, motion } from 'motion/react';
|
|
4
4
|
import { LockClosedIcon, Bars3Icon } from '@heroicons/react/24/outline';
|
|
5
5
|
import { cn } from '../../../utils/cn.js';
|
|
@@ -19,6 +19,8 @@ const Sortable = React__default.forwardRef(({ children, onReorder, className, th
|
|
|
19
19
|
const [dropPosition, setDropPosition] = useState(null);
|
|
20
20
|
const itemRefs = useRef(new Map());
|
|
21
21
|
const [draggedItemContent, setDraggedItemContent] = useState(null);
|
|
22
|
+
const [liveMessage, setLiveMessage] = useState('');
|
|
23
|
+
const liveRegionId = useId();
|
|
22
24
|
const theme = {
|
|
23
25
|
...sortableTheme,
|
|
24
26
|
...customTheme,
|
|
@@ -63,8 +65,19 @@ const Sortable = React__default.forwardRef(({ children, onReorder, className, th
|
|
|
63
65
|
itemRefs,
|
|
64
66
|
draggedItemContent,
|
|
65
67
|
setDraggedItemContent,
|
|
68
|
+
setLiveMessage,
|
|
66
69
|
}), [direction, showDragHandle, theme, onReorder, onSortDragStart, onSortDragEnd, draggedItem, dropPosition, draggedItemContent]);
|
|
67
|
-
return (
|
|
70
|
+
return (jsxs(SortableContext.Provider, { value: contextValue, children: [jsx("div", { id: liveRegionId, "aria-live": "assertive", "aria-atomic": "true", style: {
|
|
71
|
+
position: 'absolute',
|
|
72
|
+
width: '1px',
|
|
73
|
+
height: '1px',
|
|
74
|
+
padding: 0,
|
|
75
|
+
margin: '-1px',
|
|
76
|
+
overflow: 'hidden',
|
|
77
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
78
|
+
whiteSpace: 'nowrap',
|
|
79
|
+
border: 0,
|
|
80
|
+
}, children: liveMessage }), jsx(Reorder.Group, { ref: ref, axis: direction === 'horizontal' ? 'x' : 'y', values: internalValues, onReorder: handleReorder, className: cn(theme.containerStyle, direction === 'horizontal' ? 'flex flex-row' : 'flex flex-col', className), as: "ul", children: children })] }));
|
|
68
81
|
});
|
|
69
82
|
Sortable.displayName = 'Sortable';
|
|
70
83
|
const SortableItem = React__default.forwardRef(({ id, children, className, theme: customTheme = {}, disabled = false, value, }, forwardedRef) => {
|
|
@@ -136,6 +149,9 @@ const SortableItem = React__default.forwardRef(({ id, children, className, theme
|
|
|
136
149
|
}
|
|
137
150
|
if (targetIndex !== currentIndex) {
|
|
138
151
|
context.onReorder(currentIndex, targetIndex);
|
|
152
|
+
// Announce the reorder to screen readers via the live region
|
|
153
|
+
const itemLabel = typeof value === 'string' ? value : id;
|
|
154
|
+
context.setLiveMessage(`${itemLabel} moved to position ${targetIndex + 1} of ${items.length}`);
|
|
139
155
|
// Focus the item at new position after reorder
|
|
140
156
|
setTimeout(() => {
|
|
141
157
|
const targetItem = items[targetIndex];
|
|
@@ -185,7 +201,7 @@ const SortableItem = React__default.forwardRef(({ id, children, className, theme
|
|
|
185
201
|
dragControls.start(event);
|
|
186
202
|
}, [disabled, dragControls]);
|
|
187
203
|
const isBeingDragged = context.draggedItem === id;
|
|
188
|
-
return (jsxs(Reorder.Item, { ref: (node) => {
|
|
204
|
+
return (jsxs(Reorder.Item, { as: "li", ref: (node) => {
|
|
189
205
|
itemRef.current = node;
|
|
190
206
|
if (typeof forwardedRef === 'function') {
|
|
191
207
|
forwardedRef(node);
|
|
@@ -70,4 +70,6 @@ export interface SortableContextValue {
|
|
|
70
70
|
itemRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
|
71
71
|
draggedItemContent: React.ReactNode | null;
|
|
72
72
|
setDraggedItemContent: (content: React.ReactNode | null) => void;
|
|
73
|
+
/** Update the visually-hidden live region with a reorder announcement */
|
|
74
|
+
setLiveMessage: (message: string) => void;
|
|
73
75
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
|
-
import React__default, { useState, useRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import React__default, { useState, useId, useRef, useCallback, useMemo } from 'react';
|
|
3
3
|
import { cn } from '../../../utils/cn.js';
|
|
4
4
|
import { tabsTheme } from './Tabs.theme.js';
|
|
5
5
|
import { TabsContext } from './TabsContext.js';
|
|
@@ -14,6 +14,8 @@ import { TabsBody } from './TabsBody.js';
|
|
|
14
14
|
*/
|
|
15
15
|
const TabsComponent = React__default.forwardRef(({ value, defaultValue, onValueChange, orientation = "horizontal", size = "md", disabled = false, children, className, darkMode = false, ...props }, ref) => {
|
|
16
16
|
const [currentValue, setCurrentValue] = useState(value || defaultValue || "");
|
|
17
|
+
// Unique instance prefix to scope all DOM IDs to this Tabs instance
|
|
18
|
+
const instanceId = useId();
|
|
17
19
|
// Track trigger elements for underline positioning
|
|
18
20
|
const triggerRefs = useRef(new Map());
|
|
19
21
|
// Handle controlled vs uncontrolled
|
|
@@ -68,15 +70,15 @@ const TabsComponent = React__default.forwardRef(({ value, defaultValue, onValueC
|
|
|
68
70
|
disabled,
|
|
69
71
|
registerTrigger,
|
|
70
72
|
unregisterTrigger,
|
|
71
|
-
|
|
73
|
+
instanceId,
|
|
74
|
+
}), [activeValue, handleValueChange, orientation, size, disabled, registerTrigger, unregisterTrigger, instanceId]);
|
|
72
75
|
// Memoize theme classes for performance
|
|
73
76
|
const themeClasses = useMemo(() => ({
|
|
74
77
|
baseStyle: tabsTheme.baseStyle,
|
|
75
78
|
disabledStyle: disabled ? "opacity-50 cursor-not-allowed pointer-events-none" : "",
|
|
76
79
|
}), [disabled]);
|
|
77
80
|
return (jsx(TabsContext.Provider, { value: contextValue, children: jsx("div", { ref: ref, className: cn(themeClasses.baseStyle, themeClasses.disabledStyle, darkMode && "dark", className // User overrides take precedence
|
|
78
|
-
), "data-orientation": orientation, "data-size": size, "data-disabled": disabled,
|
|
79
|
-
, "aria-orientation": orientation, ...props, children: children }) }));
|
|
81
|
+
), "data-orientation": orientation, "data-size": size, "data-disabled": disabled, ...props, children: children }) }));
|
|
80
82
|
});
|
|
81
83
|
// Set display name for dev tools and Storybook
|
|
82
84
|
TabsComponent.displayName = "Tabs";
|
|
@@ -139,4 +139,6 @@ export interface TabsContextValue {
|
|
|
139
139
|
registerTrigger: (value: string, element: HTMLButtonElement) => void;
|
|
140
140
|
/** Unregister a tab trigger when it unmounts */
|
|
141
141
|
unregisterTrigger: (value: string) => void;
|
|
142
|
+
/** Unique prefix scoping DOM IDs to this Tabs instance */
|
|
143
|
+
instanceId: string;
|
|
142
144
|
}
|
|
@@ -10,7 +10,7 @@ import { getTabsBodyVariants } from './Tabs.animations.js';
|
|
|
10
10
|
* TabsBody component displays content panels for tabs with directional fade animations
|
|
11
11
|
*/
|
|
12
12
|
const TabsBody = React__default.forwardRef(({ value, forceMount = false, children, className, ...props }, ref) => {
|
|
13
|
-
const { value: selectedValue, orientation } = useTabsContext();
|
|
13
|
+
const { value: selectedValue, orientation, instanceId } = useTabsContext();
|
|
14
14
|
const shouldReduceMotion = useReducedMotion();
|
|
15
15
|
// Check if we're in Storybook and should disable animations
|
|
16
16
|
const isInStorybook = typeof window !== 'undefined' && window.__STORYBOOK_DISABLE_ANIMATIONS__;
|
|
@@ -21,7 +21,7 @@ const TabsBody = React__default.forwardRef(({ value, forceMount = false, childre
|
|
|
21
21
|
const contentVariants = useMemo(() => getTabsBodyVariants(shouldDisableAnimations), [shouldDisableAnimations]);
|
|
22
22
|
// For forceMount, we handle visibility differently to preserve DOM
|
|
23
23
|
if (forceMount) {
|
|
24
|
-
return (jsx(motion.div, { ref: ref, role: "tabpanel", "aria-labelledby": `${value}-tab`, id: `${value}-content`, tabIndex: 0, className: cn(tabsTheme.bodyStyle, !isSelected && "hidden", // Hide when not selected but force mounted
|
|
24
|
+
return (jsx(motion.div, { ref: ref, role: "tabpanel", "aria-labelledby": `${instanceId}-${value}-tab`, id: `${instanceId}-${value}-content`, tabIndex: 0, className: cn(tabsTheme.bodyStyle, !isSelected && "hidden", // Hide when not selected but force mounted
|
|
25
25
|
className // User overrides take precedence
|
|
26
26
|
), "data-state": isSelected ? "active" : "inactive", "data-orientation": orientation, style: {
|
|
27
27
|
// Ensure proper stacking context for animations
|
|
@@ -39,7 +39,7 @@ const TabsBody = React__default.forwardRef(({ value, forceMount = false, childre
|
|
|
39
39
|
exit: "exit",
|
|
40
40
|
variants: contentVariants,
|
|
41
41
|
};
|
|
42
|
-
return (jsx(AnimatePresence, { mode: "wait", children: isSelected && (jsx(motion.div, { ref: ref, role: "tabpanel", "aria-labelledby": `${value}-tab`, id: `${value}-content`, tabIndex: 0, className: cn(tabsTheme.bodyStyle, className // User overrides take precedence
|
|
42
|
+
return (jsx(AnimatePresence, { mode: "wait", children: isSelected && (jsx(motion.div, { ref: ref, role: "tabpanel", "aria-labelledby": `${instanceId}-${value}-tab`, id: `${instanceId}-${value}-content`, tabIndex: 0, className: cn(tabsTheme.bodyStyle, className // User overrides take precedence
|
|
43
43
|
), "data-state": "active", "data-orientation": orientation, style: {
|
|
44
44
|
// Ensure proper stacking context for animations
|
|
45
45
|
position: "relative",
|
|
@@ -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,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,36 +7,8 @@ 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
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);
|
|
@@ -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
|