@fragments-sdk/ui 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/assets/ui.css +25 -18
- package/dist/blocks/AccountSettings.block.d.ts +1 -1
- package/dist/blocks/ActivityFeed.block.d.ts +1 -1
- package/dist/blocks/ActivityFeedSkeleton.block.d.ts +1 -1
- package/dist/blocks/BlogEditor.block.d.ts +1 -1
- package/dist/blocks/ChatInterface.block.d.ts +1 -1
- package/dist/blocks/ChatMessages.block.d.ts +1 -1
- package/dist/blocks/CheckoutForm.block.d.ts +1 -1
- package/dist/blocks/CommandPalette.block.d.ts +1 -1
- package/dist/blocks/ContactForm.block.d.ts +1 -1
- package/dist/blocks/DashboardLayout.block.d.ts +1 -1
- package/dist/blocks/DashboardPage.block.d.ts +1 -1
- package/dist/blocks/DashboardSkeleton.block.d.ts +1 -1
- package/dist/blocks/DataTable.block.d.ts +1 -1
- package/dist/blocks/EmptyState.block.d.ts +1 -1
- package/dist/blocks/FAQSection.block.d.ts +1 -1
- package/dist/blocks/FeatureGrid.block.d.ts +1 -1
- package/dist/blocks/HeroSection.block.d.ts +1 -1
- package/dist/blocks/LoginForm.block.d.ts +1 -1
- package/dist/blocks/NavigationHeader.block.d.ts +1 -1
- package/dist/blocks/PaginatedTable.block.d.ts +1 -1
- package/dist/blocks/PricingComparison.block.d.ts +1 -1
- package/dist/blocks/ProductCard.block.d.ts +1 -1
- package/dist/blocks/RegistrationForm.block.d.ts +1 -1
- package/dist/blocks/SettingsDrawer.block.d.ts +1 -1
- package/dist/blocks/SettingsPanel.block.d.ts +1 -1
- package/dist/blocks/ShoppingCart.block.d.ts +1 -1
- package/dist/blocks/StatsCard.block.d.ts +1 -1
- package/dist/blocks/StatsCardSkeleton.block.d.ts +1 -1
- package/dist/blocks/TableSkeleton.block.d.ts +1 -1
- package/dist/blocks/ThinkingStates.block.d.ts +1 -1
- package/dist/codeblock.cjs +7 -1
- package/dist/codeblock.cjs.map +1 -1
- package/dist/codeblock.js +7 -1
- package/dist/codeblock.js.map +1 -1
- package/dist/components/Accordion/index.cjs +11 -4
- package/dist/components/Accordion/index.cjs.map +1 -1
- package/dist/components/Accordion/index.d.ts +3 -3
- package/dist/components/Accordion/index.d.ts.map +1 -1
- package/dist/components/Accordion/index.js +11 -4
- package/dist/components/Accordion/index.js.map +1 -1
- package/dist/components/Alert/index.cjs.map +1 -1
- package/dist/components/Alert/index.d.ts +7 -0
- package/dist/components/Alert/index.d.ts.map +1 -1
- package/dist/components/Alert/index.js.map +1 -1
- package/dist/components/Avatar/index.cjs.map +1 -1
- package/dist/components/Avatar/index.d.ts +4 -0
- package/dist/components/Avatar/index.d.ts.map +1 -1
- package/dist/components/Avatar/index.js.map +1 -1
- package/dist/components/Badge/index.cjs.map +1 -1
- package/dist/components/Badge/index.d.ts +12 -0
- package/dist/components/Badge/index.d.ts.map +1 -1
- package/dist/components/Badge/index.js.map +1 -1
- package/dist/components/Button/index.cjs +9 -1
- package/dist/components/Button/index.cjs.map +1 -1
- package/dist/components/Button/index.d.ts +14 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Button/index.js +9 -1
- package/dist/components/Button/index.js.map +1 -1
- package/dist/components/Card/index.cjs +2 -1
- package/dist/components/Card/index.cjs.map +1 -1
- package/dist/components/Card/index.d.ts +12 -2
- package/dist/components/Card/index.d.ts.map +1 -1
- package/dist/components/Card/index.js +2 -1
- package/dist/components/Card/index.js.map +1 -1
- package/dist/components/Checkbox/index.cjs.map +1 -1
- package/dist/components/Checkbox/index.d.ts +6 -1
- package/dist/components/Checkbox/index.d.ts.map +1 -1
- package/dist/components/Checkbox/index.js.map +1 -1
- package/dist/components/Chip/index.cjs +2 -1
- package/dist/components/Chip/index.cjs.map +1 -1
- package/dist/components/Chip/index.d.ts +10 -3
- package/dist/components/Chip/index.d.ts.map +1 -1
- package/dist/components/Chip/index.js +2 -1
- package/dist/components/Chip/index.js.map +1 -1
- package/dist/components/CodeBlock/index.d.ts +1 -1
- package/dist/components/CodeBlock/index.d.ts.map +1 -1
- package/dist/components/Collapsible/index.cjs +45 -10
- package/dist/components/Collapsible/index.cjs.map +1 -1
- package/dist/components/Collapsible/index.d.ts +6 -12
- package/dist/components/Collapsible/index.d.ts.map +1 -1
- package/dist/components/Collapsible/index.js +45 -10
- package/dist/components/Collapsible/index.js.map +1 -1
- package/dist/components/Combobox/index.cjs +18 -9
- package/dist/components/Combobox/index.cjs.map +1 -1
- package/dist/components/Combobox/index.d.ts +8 -12
- package/dist/components/Combobox/index.d.ts.map +1 -1
- package/dist/components/Combobox/index.js +18 -9
- package/dist/components/Combobox/index.js.map +1 -1
- package/dist/components/Command/index.cjs +54 -21
- package/dist/components/Command/index.cjs.map +1 -1
- package/dist/components/Command/index.d.ts +2 -2
- package/dist/components/Command/index.d.ts.map +1 -1
- package/dist/components/Command/index.js +54 -21
- package/dist/components/Command/index.js.map +1 -1
- package/dist/components/DataTable/index.cjs +13 -1
- package/dist/components/DataTable/index.cjs.map +1 -1
- package/dist/components/DataTable/index.d.ts.map +1 -1
- package/dist/components/DataTable/index.js +13 -1
- package/dist/components/DataTable/index.js.map +1 -1
- package/dist/components/DatePicker/index.d.ts +2 -3
- package/dist/components/DatePicker/index.d.ts.map +1 -1
- package/dist/components/Dialog/index.cjs +12 -9
- package/dist/components/Dialog/index.cjs.map +1 -1
- package/dist/components/Dialog/index.d.ts +20 -12
- package/dist/components/Dialog/index.d.ts.map +1 -1
- package/dist/components/Dialog/index.js +12 -9
- package/dist/components/Dialog/index.js.map +1 -1
- package/dist/components/Drawer/index.cjs +12 -9
- package/dist/components/Drawer/index.cjs.map +1 -1
- package/dist/components/Drawer/index.d.ts +22 -12
- package/dist/components/Drawer/index.d.ts.map +1 -1
- package/dist/components/Drawer/index.js +12 -9
- package/dist/components/Drawer/index.js.map +1 -1
- package/dist/components/Grid/index.cjs +4 -1
- package/dist/components/Grid/index.cjs.map +1 -1
- package/dist/components/Grid/index.d.ts +6 -2
- package/dist/components/Grid/index.d.ts.map +1 -1
- package/dist/components/Grid/index.js +4 -1
- package/dist/components/Grid/index.js.map +1 -1
- package/dist/components/Input/index.cjs.map +1 -1
- package/dist/components/Input/index.d.ts +15 -1
- package/dist/components/Input/index.d.ts.map +1 -1
- package/dist/components/Input/index.js.map +1 -1
- package/dist/components/Menu/index.cjs +30 -16
- package/dist/components/Menu/index.cjs.map +1 -1
- package/dist/components/Menu/index.d.ts +17 -25
- package/dist/components/Menu/index.d.ts.map +1 -1
- package/dist/components/Menu/index.js +30 -16
- package/dist/components/Menu/index.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenuContext.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenuContext.d.ts +1 -0
- package/dist/components/NavigationMenu/NavigationMenuContext.d.ts.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenuContext.js.map +1 -1
- package/dist/components/NavigationMenu/index.cjs +43 -11
- package/dist/components/NavigationMenu/index.cjs.map +1 -1
- package/dist/components/NavigationMenu/index.d.ts.map +1 -1
- package/dist/components/NavigationMenu/index.js +43 -11
- package/dist/components/NavigationMenu/index.js.map +1 -1
- package/dist/components/NavigationMenu/useNavigationMenu.cjs +2 -0
- package/dist/components/NavigationMenu/useNavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/useNavigationMenu.d.ts +1 -0
- package/dist/components/NavigationMenu/useNavigationMenu.d.ts.map +1 -1
- package/dist/components/NavigationMenu/useNavigationMenu.js +2 -0
- package/dist/components/NavigationMenu/useNavigationMenu.js.map +1 -1
- package/dist/components/Popover/index.cjs +11 -10
- package/dist/components/Popover/index.cjs.map +1 -1
- package/dist/components/Popover/index.d.ts +17 -12
- package/dist/components/Popover/index.d.ts.map +1 -1
- package/dist/components/Popover/index.js +11 -10
- package/dist/components/Popover/index.js.map +1 -1
- package/dist/components/RadioGroup/index.cjs.map +1 -1
- package/dist/components/RadioGroup/index.d.ts +4 -0
- package/dist/components/RadioGroup/index.d.ts.map +1 -1
- package/dist/components/RadioGroup/index.js.map +1 -1
- package/dist/components/Select/index.cjs +7 -6
- package/dist/components/Select/index.cjs.map +1 -1
- package/dist/components/Select/index.d.ts +20 -9
- package/dist/components/Select/index.d.ts.map +1 -1
- package/dist/components/Select/index.js +7 -6
- package/dist/components/Select/index.js.map +1 -1
- package/dist/components/Sidebar/index.cjs +71 -24
- package/dist/components/Sidebar/index.cjs.map +1 -1
- package/dist/components/Sidebar/index.d.ts +21 -33
- package/dist/components/Sidebar/index.d.ts.map +1 -1
- package/dist/components/Sidebar/index.js +71 -24
- package/dist/components/Sidebar/index.js.map +1 -1
- package/dist/components/Slider/index.cjs +3 -1
- package/dist/components/Slider/index.cjs.map +1 -1
- package/dist/components/Slider/index.d.ts +10 -0
- package/dist/components/Slider/index.d.ts.map +1 -1
- package/dist/components/Slider/index.js +3 -1
- package/dist/components/Slider/index.js.map +1 -1
- package/dist/components/Stack/index.cjs +6 -0
- package/dist/components/Stack/index.cjs.map +1 -1
- package/dist/components/Stack/index.d.ts +12 -6
- package/dist/components/Stack/index.d.ts.map +1 -1
- package/dist/components/Stack/index.js +6 -0
- package/dist/components/Stack/index.js.map +1 -1
- package/dist/components/Tabs/index.cjs.map +1 -1
- package/dist/components/Tabs/index.d.ts +13 -1
- package/dist/components/Tabs/index.d.ts.map +1 -1
- package/dist/components/Tabs/index.js.map +1 -1
- package/dist/components/Text/Text.module.scss.cjs +44 -32
- package/dist/components/Text/Text.module.scss.cjs.map +1 -1
- package/dist/components/Text/Text.module.scss.js +44 -32
- package/dist/components/Text/Text.module.scss.js.map +1 -1
- package/dist/components/Text/index.cjs.map +1 -1
- package/dist/components/Text/index.d.ts +18 -3
- package/dist/components/Text/index.d.ts.map +1 -1
- package/dist/components/Text/index.js.map +1 -1
- package/dist/components/Theme/index.cjs.map +1 -1
- package/dist/components/Theme/index.d.ts +12 -0
- package/dist/components/Theme/index.d.ts.map +1 -1
- package/dist/components/Theme/index.js.map +1 -1
- package/dist/components/Toggle/index.cjs +2 -1
- package/dist/components/Toggle/index.cjs.map +1 -1
- package/dist/components/Toggle/index.d.ts +9 -0
- package/dist/components/Toggle/index.d.ts.map +1 -1
- package/dist/components/Toggle/index.js +2 -1
- package/dist/components/Toggle/index.js.map +1 -1
- package/dist/components/ToggleGroup/index.cjs +4 -1
- package/dist/components/ToggleGroup/index.cjs.map +1 -1
- package/dist/components/ToggleGroup/index.d.ts +13 -4
- package/dist/components/ToggleGroup/index.d.ts.map +1 -1
- package/dist/components/ToggleGroup/index.js +4 -1
- package/dist/components/ToggleGroup/index.js.map +1 -1
- package/dist/components/Tooltip/index.cjs +20 -10
- package/dist/components/Tooltip/index.cjs.map +1 -1
- package/dist/components/Tooltip/index.d.ts +5 -1
- package/dist/components/Tooltip/index.d.ts.map +1 -1
- package/dist/components/Tooltip/index.js +20 -10
- package/dist/components/Tooltip/index.js.map +1 -1
- package/dist/datepicker.cjs +24 -10
- package/dist/datepicker.cjs.map +1 -1
- package/dist/datepicker.js +24 -10
- package/dist/datepicker.js.map +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/css-warning.cjs +18 -0
- package/dist/utils/css-warning.cjs.map +1 -0
- package/dist/utils/css-warning.d.ts +2 -0
- package/dist/utils/css-warning.d.ts.map +1 -0
- package/dist/utils/css-warning.js +18 -0
- package/dist/utils/css-warning.js.map +1 -0
- package/fragments.json +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.test.tsx +33 -0
- package/src/components/Accordion/index.tsx +10 -3
- package/src/components/Alert/index.tsx +7 -0
- package/src/components/Avatar/index.tsx +4 -0
- package/src/components/Badge/Badge.fragment.tsx +10 -2
- package/src/components/Badge/index.tsx +12 -0
- package/src/components/Button/Button.fragment.tsx +12 -2
- package/src/components/Button/Button.test.tsx +16 -0
- package/src/components/Button/index.tsx +27 -2
- package/src/components/Card/Card.fragment.tsx +14 -2
- package/src/components/Card/Card.test.tsx +5 -0
- package/src/components/Card/index.tsx +15 -2
- package/src/components/Checkbox/index.tsx +6 -1
- package/src/components/Chip/Chip.fragment.tsx +12 -2
- package/src/components/Chip/Chip.test.tsx +5 -0
- package/src/components/Chip/index.tsx +14 -4
- package/src/components/CodeBlock/index.tsx +13 -2
- package/src/components/Collapsible/Collapsible.test.tsx +41 -0
- package/src/components/Collapsible/index.tsx +53 -16
- package/src/components/Combobox/Combobox.test.tsx +55 -0
- package/src/components/Combobox/index.tsx +23 -17
- package/src/components/Command/Command.test.tsx +93 -0
- package/src/components/Command/index.tsx +61 -18
- package/src/components/DataTable/DataTable.test.tsx +11 -2
- package/src/components/DataTable/index.tsx +22 -2
- package/src/components/DatePicker/DatePicker.test.tsx +79 -0
- package/src/components/DatePicker/index.tsx +29 -14
- package/src/components/Dialog/Dialog.test.tsx +23 -0
- package/src/components/Dialog/index.tsx +27 -16
- package/src/components/Drawer/Drawer.test.tsx +27 -0
- package/src/components/Drawer/index.tsx +29 -16
- package/src/components/Grid/Grid.fragment.tsx +14 -2
- package/src/components/Grid/Grid.test.tsx +6 -0
- package/src/components/Grid/index.tsx +12 -3
- package/src/components/Input/index.tsx +15 -1
- package/src/components/Menu/index.tsx +35 -30
- package/src/components/NavigationMenu/NavigationMenu.fragment.tsx +1 -1
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +40 -4
- package/src/components/NavigationMenu/NavigationMenuContext.ts +3 -0
- package/src/components/NavigationMenu/index.tsx +49 -13
- package/src/components/NavigationMenu/useNavigationMenu.ts +4 -0
- package/src/components/Popover/Popover.test.tsx +23 -0
- package/src/components/Popover/index.tsx +24 -18
- package/src/components/RadioGroup/index.tsx +4 -0
- package/src/components/Select/Select.test.tsx +41 -0
- package/src/components/Select/index.tsx +24 -12
- package/src/components/Sidebar/Sidebar.test.tsx +83 -4
- package/src/components/Sidebar/index.tsx +87 -45
- package/src/components/Slider/Slider.fragment.tsx +5 -1
- package/src/components/Slider/Slider.test.tsx +6 -0
- package/src/components/Slider/index.tsx +13 -1
- package/src/components/Stack/Stack.fragment.tsx +22 -2
- package/src/components/Stack/Stack.test.tsx +6 -0
- package/src/components/Stack/index.tsx +20 -6
- package/src/components/Tabs/index.tsx +13 -1
- package/src/components/Text/Text.fragment.tsx +10 -8
- package/src/components/Text/Text.module.scss +8 -2
- package/src/components/Text/Text.test.tsx +15 -0
- package/src/components/Text/index.tsx +18 -3
- package/src/components/Theme/index.tsx +12 -0
- package/src/components/Toggle/Toggle.fragment.tsx +5 -1
- package/src/components/Toggle/Toggle.test.tsx +19 -0
- package/src/components/Toggle/index.tsx +11 -1
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +5 -2
- package/src/components/ToggleGroup/ToggleGroup.test.tsx +20 -0
- package/src/components/ToggleGroup/index.tsx +15 -4
- package/src/components/Tooltip/Tooltip.test.tsx +17 -0
- package/src/components/Tooltip/index.tsx +58 -34
- package/src/index.ts +6 -0
- package/src/tokens/_seeds.scss +5 -3
- package/src/tokens/_variables.scss +2 -0
- package/src/utils/css-warning.ts +29 -0
|
@@ -16,17 +16,31 @@ export interface SelectOption {
|
|
|
16
16
|
disabled?: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Select dropdown for choosing from a list of options.
|
|
21
|
+
* @see https://usefragments.com/components/select
|
|
22
|
+
*/
|
|
19
23
|
export interface SelectProps {
|
|
20
24
|
children: React.ReactNode;
|
|
25
|
+
/** Controlled selected value */
|
|
21
26
|
value?: SelectValue | null;
|
|
27
|
+
/** Default value for uncontrolled usage */
|
|
22
28
|
defaultValue?: SelectValue;
|
|
29
|
+
/** Called when selection changes */
|
|
23
30
|
onValueChange?: (value: SelectValue | null) => void;
|
|
31
|
+
/** Controlled open state */
|
|
24
32
|
open?: boolean;
|
|
33
|
+
/** Default open state */
|
|
25
34
|
defaultOpen?: boolean;
|
|
35
|
+
/** Called when open state changes */
|
|
26
36
|
onOpenChange?: (open: boolean) => void;
|
|
37
|
+
/** Whether the select is non-interactive */
|
|
27
38
|
disabled?: boolean;
|
|
39
|
+
/** Whether a selection is required */
|
|
28
40
|
required?: boolean;
|
|
41
|
+
/** Form field name */
|
|
29
42
|
name?: string;
|
|
43
|
+
/** Placeholder text when no value is selected */
|
|
30
44
|
placeholder?: string;
|
|
31
45
|
}
|
|
32
46
|
|
|
@@ -43,21 +57,18 @@ export interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
43
57
|
maxVisibleItems?: number;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
export interface SelectItemProps {
|
|
60
|
+
export interface SelectItemProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
|
|
47
61
|
children: React.ReactNode;
|
|
48
62
|
value: SelectValue;
|
|
49
63
|
disabled?: boolean;
|
|
50
|
-
className?: string;
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
export interface SelectGroupProps {
|
|
66
|
+
export interface SelectGroupProps extends React.HTMLAttributes<HTMLElement> {
|
|
54
67
|
children: React.ReactNode;
|
|
55
|
-
className?: string;
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
export interface SelectGroupLabelProps {
|
|
70
|
+
export interface SelectGroupLabelProps extends React.HTMLAttributes<HTMLElement> {
|
|
59
71
|
children: React.ReactNode;
|
|
60
|
-
className?: string;
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
// ============================================
|
|
@@ -262,7 +273,7 @@ function SelectContent({
|
|
|
262
273
|
);
|
|
263
274
|
}
|
|
264
275
|
|
|
265
|
-
function SelectItem({ children, value, disabled, className }: SelectItemProps) {
|
|
276
|
+
function SelectItem({ children, value, disabled, className, ...htmlProps }: SelectItemProps) {
|
|
266
277
|
const { itemsRef, incrementItemsVersion } = React.useContext(SelectContext);
|
|
267
278
|
const classes = [styles.item, className].filter(Boolean).join(' ');
|
|
268
279
|
|
|
@@ -274,11 +285,12 @@ function SelectItem({ children, value, disabled, className }: SelectItemProps) {
|
|
|
274
285
|
incrementItemsVersion();
|
|
275
286
|
return () => {
|
|
276
287
|
items.delete(value);
|
|
288
|
+
incrementItemsVersion();
|
|
277
289
|
};
|
|
278
290
|
}, [itemsRef, incrementItemsVersion, value, children]);
|
|
279
291
|
|
|
280
292
|
return (
|
|
281
|
-
<BaseSelect.Item value={value} disabled={disabled} className={classes}>
|
|
293
|
+
<BaseSelect.Item {...htmlProps} value={value} disabled={disabled} className={classes}>
|
|
282
294
|
<BaseSelect.ItemText>{children}</BaseSelect.ItemText>
|
|
283
295
|
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
|
284
296
|
<CheckIcon />
|
|
@@ -287,14 +299,14 @@ function SelectItem({ children, value, disabled, className }: SelectItemProps) {
|
|
|
287
299
|
);
|
|
288
300
|
}
|
|
289
301
|
|
|
290
|
-
function SelectGroup({ children, className }: SelectGroupProps) {
|
|
302
|
+
function SelectGroup({ children, className, ...htmlProps }: SelectGroupProps) {
|
|
291
303
|
const classes = [styles.group, className].filter(Boolean).join(' ');
|
|
292
|
-
return <BaseSelect.Group className={classes}>{children}</BaseSelect.Group>;
|
|
304
|
+
return <BaseSelect.Group {...htmlProps} className={classes}>{children}</BaseSelect.Group>;
|
|
293
305
|
}
|
|
294
306
|
|
|
295
|
-
function SelectGroupLabel({ children, className }: SelectGroupLabelProps) {
|
|
307
|
+
function SelectGroupLabel({ children, className, ...htmlProps }: SelectGroupLabelProps) {
|
|
296
308
|
const classes = [styles.groupLabel, className].filter(Boolean).join(' ');
|
|
297
|
-
return <BaseSelect.GroupLabel className={classes}>{children}</BaseSelect.GroupLabel>;
|
|
309
|
+
return <BaseSelect.GroupLabel {...htmlProps} className={classes}>{children}</BaseSelect.GroupLabel>;
|
|
298
310
|
}
|
|
299
311
|
|
|
300
312
|
// ============================================
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
|
2
|
-
import { render, screen, expectNoA11yViolations } from '../../test/utils';
|
|
2
|
+
import { render, screen, userEvent, expectNoA11yViolations } from '../../test/utils';
|
|
3
3
|
import { Sidebar } from './index';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
beforeAll(() => {
|
|
5
|
+
function mockMatchMedia(matches: boolean) {
|
|
7
6
|
Object.defineProperty(window, 'matchMedia', {
|
|
8
7
|
writable: true,
|
|
9
8
|
value: vi.fn().mockImplementation((query: string) => ({
|
|
10
|
-
matches
|
|
9
|
+
matches,
|
|
11
10
|
media: query,
|
|
12
11
|
onchange: null,
|
|
13
12
|
addListener: vi.fn(),
|
|
@@ -17,6 +16,11 @@ beforeAll(() => {
|
|
|
17
16
|
dispatchEvent: vi.fn(),
|
|
18
17
|
})),
|
|
19
18
|
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Mock matchMedia for jsdom
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
mockMatchMedia(false);
|
|
20
24
|
});
|
|
21
25
|
|
|
22
26
|
function renderSidebar(props: Partial<React.ComponentProps<typeof Sidebar>> = {}) {
|
|
@@ -113,6 +117,81 @@ describe('Sidebar', () => {
|
|
|
113
117
|
expect(screen.getByRole('button', { name: /expand sidebar/i })).toBeInTheDocument();
|
|
114
118
|
});
|
|
115
119
|
|
|
120
|
+
it('composes child click handler in Sidebar.Item asChild mode', async () => {
|
|
121
|
+
const user = userEvent.setup();
|
|
122
|
+
const childClick = vi.fn();
|
|
123
|
+
const onItemClick = vi.fn();
|
|
124
|
+
|
|
125
|
+
render(
|
|
126
|
+
<Sidebar aria-label="Test sidebar">
|
|
127
|
+
<Sidebar.Nav aria-label="Main">
|
|
128
|
+
<Sidebar.Section label="Section One">
|
|
129
|
+
<Sidebar.Item asChild icon={<span>I</span>} onClick={onItemClick}>
|
|
130
|
+
<a href="#dashboard" onClick={childClick}>Dashboard</a>
|
|
131
|
+
</Sidebar.Item>
|
|
132
|
+
</Sidebar.Section>
|
|
133
|
+
</Sidebar.Nav>
|
|
134
|
+
</Sidebar>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
await user.click(screen.getByRole('link', { name: /dashboard/i }));
|
|
138
|
+
|
|
139
|
+
expect(childClick).toHaveBeenCalled();
|
|
140
|
+
expect(onItemClick).toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('forwards html props to desktop compound parts', () => {
|
|
144
|
+
render(
|
|
145
|
+
<Sidebar aria-label="Test sidebar">
|
|
146
|
+
<Sidebar.Header data-testid="header" data-part="header">Header</Sidebar.Header>
|
|
147
|
+
<Sidebar.Nav aria-label="Main" data-testid="nav" data-part="nav">
|
|
148
|
+
<Sidebar.Section data-testid="section" data-part="section" label="Section One">
|
|
149
|
+
<Sidebar.Item icon={<span>I</span>}>Dashboard</Sidebar.Item>
|
|
150
|
+
</Sidebar.Section>
|
|
151
|
+
</Sidebar.Nav>
|
|
152
|
+
<Sidebar.Footer data-testid="footer" data-part="footer">Footer</Sidebar.Footer>
|
|
153
|
+
<Sidebar.CollapseToggle data-testid="collapse-toggle" data-part="collapse-toggle" />
|
|
154
|
+
<Sidebar.Rail data-testid="rail" data-part="rail" />
|
|
155
|
+
</Sidebar>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(screen.getByTestId('header')).toHaveAttribute('data-part', 'header');
|
|
159
|
+
expect(screen.getByTestId('nav')).toHaveAttribute('data-part', 'nav');
|
|
160
|
+
expect(screen.getByTestId('section')).toHaveAttribute('data-part', 'section');
|
|
161
|
+
expect(screen.getByTestId('footer')).toHaveAttribute('data-part', 'footer');
|
|
162
|
+
expect(screen.getByTestId('collapse-toggle')).toHaveAttribute('data-part', 'collapse-toggle');
|
|
163
|
+
expect(screen.getByTestId('rail')).toHaveAttribute('data-part', 'rail');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('forwards props to mobile Trigger/Overlay and composes overlay click', async () => {
|
|
167
|
+
mockMatchMedia(true);
|
|
168
|
+
const user = userEvent.setup();
|
|
169
|
+
const overlayClick = vi.fn();
|
|
170
|
+
|
|
171
|
+
render(
|
|
172
|
+
<Sidebar defaultOpen aria-label="Mobile sidebar">
|
|
173
|
+
<Sidebar.Trigger data-testid="trigger" data-part="trigger" />
|
|
174
|
+
<Sidebar.Overlay data-testid="overlay" data-part="overlay" onClick={overlayClick} />
|
|
175
|
+
<Sidebar.Nav aria-label="Main">
|
|
176
|
+
<Sidebar.Section label="Section One">
|
|
177
|
+
<Sidebar.Item icon={<span>I</span>}>Dashboard</Sidebar.Item>
|
|
178
|
+
</Sidebar.Section>
|
|
179
|
+
</Sidebar.Nav>
|
|
180
|
+
</Sidebar>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const trigger = await screen.findByTestId('trigger');
|
|
184
|
+
const overlay = await screen.findByTestId('overlay');
|
|
185
|
+
|
|
186
|
+
expect(trigger).toHaveAttribute('data-part', 'trigger');
|
|
187
|
+
expect(overlay).toHaveAttribute('data-part', 'overlay');
|
|
188
|
+
|
|
189
|
+
await user.click(overlay);
|
|
190
|
+
expect(overlayClick).toHaveBeenCalled();
|
|
191
|
+
|
|
192
|
+
mockMatchMedia(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
116
195
|
it('has no accessibility violations', async () => {
|
|
117
196
|
const { container } = renderSidebar();
|
|
118
197
|
await expectNoA11yViolations(container);
|
|
@@ -9,6 +9,17 @@ import { ScrollArea } from '../ScrollArea';
|
|
|
9
9
|
import { useFocusTrap } from '../../utils/a11y';
|
|
10
10
|
import { useKeyboardShortcut } from '../../utils/keyboard-shortcuts';
|
|
11
11
|
|
|
12
|
+
function composeEventHandlers<E extends { defaultPrevented: boolean }>(
|
|
13
|
+
userHandler: ((event: E) => void) | undefined,
|
|
14
|
+
internalHandler: (event: E) => void
|
|
15
|
+
) {
|
|
16
|
+
return (event: E) => {
|
|
17
|
+
userHandler?.(event);
|
|
18
|
+
if (event.defaultPrevented) return;
|
|
19
|
+
internalHandler(event);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
// ============================================
|
|
13
24
|
// Types
|
|
14
25
|
// ============================================
|
|
@@ -66,21 +77,19 @@ export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
66
77
|
collapsible?: SidebarCollapsible;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
export interface SidebarHeaderProps {
|
|
80
|
+
export interface SidebarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
70
81
|
children: React.ReactNode;
|
|
71
82
|
/** Content to show when sidebar is collapsed (e.g., just logo icon) */
|
|
72
83
|
collapsedContent?: React.ReactNode;
|
|
73
|
-
className?: string;
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
export interface SidebarNavProps {
|
|
86
|
+
export interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
|
77
87
|
children: React.ReactNode;
|
|
78
88
|
/** Accessible label for navigation */
|
|
79
89
|
'aria-label'?: string;
|
|
80
|
-
className?: string;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
export interface SidebarSectionProps {
|
|
92
|
+
export interface SidebarSectionProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
84
93
|
children: React.ReactNode;
|
|
85
94
|
/** Optional section label */
|
|
86
95
|
label?: string;
|
|
@@ -90,16 +99,14 @@ export interface SidebarSectionProps {
|
|
|
90
99
|
collapsible?: boolean;
|
|
91
100
|
/** Default expanded state (only applies when collapsible is true) */
|
|
92
101
|
defaultOpen?: boolean;
|
|
93
|
-
className?: string;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
export interface SidebarSectionActionProps {
|
|
104
|
+
export interface SidebarSectionActionProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
|
|
97
105
|
children: React.ReactNode;
|
|
98
106
|
/** Click handler */
|
|
99
|
-
onClick?: () => void;
|
|
107
|
+
onClick?: (event?: React.MouseEvent<HTMLButtonElement>) => void;
|
|
100
108
|
/** Accessible label */
|
|
101
109
|
'aria-label'?: string;
|
|
102
|
-
className?: string;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
export interface SidebarItemProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onClick'> {
|
|
@@ -143,39 +150,31 @@ export interface SidebarSubItemProps extends Omit<React.HTMLAttributes<HTMLEleme
|
|
|
143
150
|
onClick?: () => void;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
export interface SidebarFooterProps {
|
|
153
|
+
export interface SidebarFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
147
154
|
children: React.ReactNode;
|
|
148
|
-
className?: string;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
export interface SidebarTriggerProps {
|
|
157
|
+
export interface SidebarTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
152
158
|
/** Custom trigger element (uses render prop pattern) */
|
|
153
159
|
children?: React.ReactNode;
|
|
154
160
|
/** Accessible label */
|
|
155
161
|
'aria-label'?: string;
|
|
156
|
-
className?: string;
|
|
157
162
|
}
|
|
158
163
|
|
|
159
|
-
export
|
|
160
|
-
className?: string;
|
|
161
|
-
}
|
|
164
|
+
export type SidebarOverlayProps = React.HTMLAttributes<HTMLDivElement>;
|
|
162
165
|
|
|
163
|
-
export interface SidebarCollapseToggleProps {
|
|
166
|
+
export interface SidebarCollapseToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
164
167
|
/** Accessible label */
|
|
165
168
|
'aria-label'?: string;
|
|
166
|
-
className?: string;
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
export
|
|
170
|
-
className?: string;
|
|
171
|
-
}
|
|
171
|
+
export type SidebarRailProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
172
172
|
|
|
173
|
-
export interface SidebarMenuSkeletonProps {
|
|
173
|
+
export interface SidebarMenuSkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
174
174
|
/** Number of skeleton items to render */
|
|
175
175
|
count?: number;
|
|
176
176
|
/** Show icons in skeleton items */
|
|
177
177
|
showIcon?: boolean;
|
|
178
|
-
className?: string;
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
// ============================================
|
|
@@ -632,7 +631,12 @@ function SidebarRoot({
|
|
|
632
631
|
);
|
|
633
632
|
}
|
|
634
633
|
|
|
635
|
-
function SidebarHeader({
|
|
634
|
+
function SidebarHeader({
|
|
635
|
+
children,
|
|
636
|
+
collapsedContent,
|
|
637
|
+
className,
|
|
638
|
+
...htmlProps
|
|
639
|
+
}: SidebarHeaderProps) {
|
|
636
640
|
const { collapsed, isMobile } = useSidebarContext();
|
|
637
641
|
const isCollapsed = collapsed && !isMobile;
|
|
638
642
|
const classes = [styles.header, className].filter(Boolean).join(' ');
|
|
@@ -640,13 +644,18 @@ function SidebarHeader({ children, collapsedContent, className }: SidebarHeaderP
|
|
|
640
644
|
// Show collapsed content when sidebar is collapsed (and we have it), otherwise show children
|
|
641
645
|
const content = isCollapsed && collapsedContent ? collapsedContent : children;
|
|
642
646
|
|
|
643
|
-
return <div className={classes}>{content}</div>;
|
|
647
|
+
return <div {...htmlProps} className={classes}>{content}</div>;
|
|
644
648
|
}
|
|
645
649
|
|
|
646
|
-
function SidebarNav({
|
|
650
|
+
function SidebarNav({
|
|
651
|
+
children,
|
|
652
|
+
'aria-label': ariaLabel = 'Main navigation',
|
|
653
|
+
className,
|
|
654
|
+
...htmlProps
|
|
655
|
+
}: SidebarNavProps) {
|
|
647
656
|
const classes = [styles.nav, className].filter(Boolean).join(' ');
|
|
648
657
|
return (
|
|
649
|
-
<nav className={classes} aria-label={ariaLabel}>
|
|
658
|
+
<nav {...htmlProps} className={classes} aria-label={ariaLabel}>
|
|
650
659
|
<ScrollArea orientation="vertical" showFades className={styles.navScrollArea}>
|
|
651
660
|
{children}
|
|
652
661
|
</ScrollArea>
|
|
@@ -660,7 +669,8 @@ function SidebarSection({
|
|
|
660
669
|
action,
|
|
661
670
|
collapsible: isCollapsibleProp = false,
|
|
662
671
|
defaultOpen = true,
|
|
663
|
-
className
|
|
672
|
+
className,
|
|
673
|
+
...htmlProps
|
|
664
674
|
}: SidebarSectionProps) {
|
|
665
675
|
const { collapsed, isMobile } = useSidebarContext();
|
|
666
676
|
|
|
@@ -676,7 +686,7 @@ function SidebarSection({
|
|
|
676
686
|
// Non-collapsible section
|
|
677
687
|
if (!isCollapsible) {
|
|
678
688
|
return (
|
|
679
|
-
<div className={classes} role="group" aria-label={label}>
|
|
689
|
+
<div {...htmlProps} className={classes} role="group" aria-label={label}>
|
|
680
690
|
{(showLabel || showAction) && (
|
|
681
691
|
<div className={styles.sectionHeader}>
|
|
682
692
|
{showLabel && <div className={styles.sectionLabel}>{label}</div>}
|
|
@@ -692,7 +702,7 @@ function SidebarSection({
|
|
|
692
702
|
|
|
693
703
|
// Collapsible section using Collapsible component
|
|
694
704
|
return (
|
|
695
|
-
<div className={classes} role="group" aria-label={label}>
|
|
705
|
+
<div {...htmlProps} className={classes} role="group" aria-label={label}>
|
|
696
706
|
<Collapsible defaultOpen={defaultOpen} className={styles.sectionCollapsible}>
|
|
697
707
|
<div className={styles.sectionHeader}>
|
|
698
708
|
<Collapsible.Trigger
|
|
@@ -718,14 +728,16 @@ function SidebarSectionAction({
|
|
|
718
728
|
onClick,
|
|
719
729
|
'aria-label': ariaLabel,
|
|
720
730
|
className,
|
|
731
|
+
...htmlProps
|
|
721
732
|
}: SidebarSectionActionProps) {
|
|
722
733
|
const classes = [styles.sectionAction, className].filter(Boolean).join(' ');
|
|
723
734
|
|
|
724
735
|
return (
|
|
725
736
|
<button
|
|
726
737
|
type="button"
|
|
738
|
+
{...htmlProps}
|
|
727
739
|
className={classes}
|
|
728
|
-
onClick={onClick}
|
|
740
|
+
onClick={(event) => onClick?.(event)}
|
|
729
741
|
aria-label={ariaLabel}
|
|
730
742
|
>
|
|
731
743
|
{children}
|
|
@@ -817,11 +829,19 @@ function SidebarItem({
|
|
|
817
829
|
|
|
818
830
|
if (asChild && React.isValidElement(children)) {
|
|
819
831
|
// Clone the child element and merge props
|
|
832
|
+
const childProps = children.props as {
|
|
833
|
+
className?: string;
|
|
834
|
+
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
|
835
|
+
};
|
|
820
836
|
itemElement = React.cloneElement(children, {
|
|
821
837
|
...itemProps,
|
|
822
838
|
...rest,
|
|
839
|
+
onClick: composeEventHandlers(
|
|
840
|
+
childProps.onClick,
|
|
841
|
+
(event: React.MouseEvent<HTMLElement>) => handleClick(event)
|
|
842
|
+
),
|
|
823
843
|
// Merge classNames
|
|
824
|
-
className: [classes,
|
|
844
|
+
className: [classes, childProps.className].filter(Boolean).join(' '),
|
|
825
845
|
children: itemContent,
|
|
826
846
|
} as React.HTMLAttributes<HTMLElement>);
|
|
827
847
|
} else if (href) {
|
|
@@ -917,12 +937,18 @@ function SidebarSubmenu({ children }: { children: React.ReactNode }) {
|
|
|
917
937
|
);
|
|
918
938
|
}
|
|
919
939
|
|
|
920
|
-
function SidebarFooter({ children, className }: SidebarFooterProps) {
|
|
940
|
+
function SidebarFooter({ children, className, ...htmlProps }: SidebarFooterProps) {
|
|
921
941
|
const classes = [styles.footer, className].filter(Boolean).join(' ');
|
|
922
|
-
return <div className={classes}>{children}</div>;
|
|
942
|
+
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
923
943
|
}
|
|
924
944
|
|
|
925
|
-
function SidebarTrigger({
|
|
945
|
+
function SidebarTrigger({
|
|
946
|
+
children,
|
|
947
|
+
'aria-label': ariaLabel = 'Toggle navigation',
|
|
948
|
+
className,
|
|
949
|
+
onClick,
|
|
950
|
+
...htmlProps
|
|
951
|
+
}: SidebarTriggerProps) {
|
|
926
952
|
const { open, setOpen, isMobile, sidebarId } = useSidebarContext();
|
|
927
953
|
|
|
928
954
|
// Only render trigger on mobile
|
|
@@ -934,9 +960,10 @@ function SidebarTrigger({ children, 'aria-label': ariaLabel = 'Toggle navigation
|
|
|
934
960
|
|
|
935
961
|
return (
|
|
936
962
|
<button
|
|
963
|
+
{...htmlProps}
|
|
937
964
|
type="button"
|
|
938
965
|
className={classes}
|
|
939
|
-
onClick={() => setOpen(!open)}
|
|
966
|
+
onClick={composeEventHandlers(onClick, () => setOpen(!open))}
|
|
940
967
|
aria-label={ariaLabel}
|
|
941
968
|
aria-expanded={open}
|
|
942
969
|
aria-controls={sidebarId}
|
|
@@ -946,7 +973,7 @@ function SidebarTrigger({ children, 'aria-label': ariaLabel = 'Toggle navigation
|
|
|
946
973
|
);
|
|
947
974
|
}
|
|
948
975
|
|
|
949
|
-
function SidebarOverlay({ className }: SidebarOverlayProps) {
|
|
976
|
+
function SidebarOverlay({ className, onClick, ...htmlProps }: SidebarOverlayProps) {
|
|
950
977
|
const { open, setOpen, isMobile } = useSidebarContext();
|
|
951
978
|
|
|
952
979
|
// Only render overlay on mobile when open
|
|
@@ -958,15 +985,21 @@ function SidebarOverlay({ className }: SidebarOverlayProps) {
|
|
|
958
985
|
|
|
959
986
|
return (
|
|
960
987
|
<div
|
|
988
|
+
{...htmlProps}
|
|
961
989
|
className={classes}
|
|
962
|
-
onClick={() => setOpen(false)}
|
|
990
|
+
onClick={composeEventHandlers(onClick, () => setOpen(false))}
|
|
963
991
|
aria-hidden="true"
|
|
964
992
|
data-state={open ? 'open' : 'closed'}
|
|
965
993
|
/>
|
|
966
994
|
);
|
|
967
995
|
}
|
|
968
996
|
|
|
969
|
-
function SidebarCollapseToggle({
|
|
997
|
+
function SidebarCollapseToggle({
|
|
998
|
+
'aria-label': ariaLabel,
|
|
999
|
+
className,
|
|
1000
|
+
onClick,
|
|
1001
|
+
...htmlProps
|
|
1002
|
+
}: SidebarCollapseToggleProps) {
|
|
970
1003
|
const { collapsed, setCollapsed, isMobile, collapsible, hasIcons } = useSidebarContext();
|
|
971
1004
|
|
|
972
1005
|
// Don't show on mobile or when collapsing is disabled
|
|
@@ -987,9 +1020,10 @@ function SidebarCollapseToggle({ 'aria-label': ariaLabel, className }: SidebarCo
|
|
|
987
1020
|
|
|
988
1021
|
return (
|
|
989
1022
|
<button
|
|
1023
|
+
{...htmlProps}
|
|
990
1024
|
type="button"
|
|
991
1025
|
className={classes}
|
|
992
|
-
onClick={() => setCollapsed(!collapsed)}
|
|
1026
|
+
onClick={composeEventHandlers(onClick, () => setCollapsed(!collapsed))}
|
|
993
1027
|
aria-label={label}
|
|
994
1028
|
>
|
|
995
1029
|
<CollapsePanelIcon />
|
|
@@ -997,7 +1031,13 @@ function SidebarCollapseToggle({ 'aria-label': ariaLabel, className }: SidebarCo
|
|
|
997
1031
|
);
|
|
998
1032
|
}
|
|
999
1033
|
|
|
1000
|
-
function SidebarRail({
|
|
1034
|
+
function SidebarRail({
|
|
1035
|
+
className,
|
|
1036
|
+
onClick,
|
|
1037
|
+
title,
|
|
1038
|
+
'aria-label': ariaLabel,
|
|
1039
|
+
...htmlProps
|
|
1040
|
+
}: SidebarRailProps) {
|
|
1001
1041
|
const { collapsed, setCollapsed, isMobile, collapsible } = useSidebarContext();
|
|
1002
1042
|
|
|
1003
1043
|
// Don't show on mobile or when collapsing is disabled
|
|
@@ -1013,11 +1053,12 @@ function SidebarRail({ className }: SidebarRailProps) {
|
|
|
1013
1053
|
|
|
1014
1054
|
return (
|
|
1015
1055
|
<button
|
|
1056
|
+
{...htmlProps}
|
|
1016
1057
|
type="button"
|
|
1017
1058
|
className={classes}
|
|
1018
|
-
onClick={() => setCollapsed(!collapsed)}
|
|
1019
|
-
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
1020
|
-
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
1059
|
+
onClick={composeEventHandlers(onClick, () => setCollapsed(!collapsed))}
|
|
1060
|
+
aria-label={ariaLabel ?? (collapsed ? 'Expand sidebar' : 'Collapse sidebar')}
|
|
1061
|
+
title={title ?? (collapsed ? 'Expand sidebar' : 'Collapse sidebar')}
|
|
1021
1062
|
/>
|
|
1022
1063
|
);
|
|
1023
1064
|
}
|
|
@@ -1026,6 +1067,7 @@ function SidebarMenuSkeleton({
|
|
|
1026
1067
|
count = 5,
|
|
1027
1068
|
showIcon = true,
|
|
1028
1069
|
className,
|
|
1070
|
+
...htmlProps
|
|
1029
1071
|
}: SidebarMenuSkeletonProps) {
|
|
1030
1072
|
const { collapsed, isMobile } = useSidebarContext();
|
|
1031
1073
|
const isCollapsed = collapsed && !isMobile;
|
|
@@ -1034,7 +1076,7 @@ function SidebarMenuSkeleton({
|
|
|
1034
1076
|
const labelWidths = ['64%', '72%', '68%', '79%', '74%', '66%', '83%', '70%'];
|
|
1035
1077
|
|
|
1036
1078
|
return (
|
|
1037
|
-
<div className={classes} aria-hidden="true">
|
|
1079
|
+
<div {...htmlProps} className={classes} aria-hidden="true">
|
|
1038
1080
|
{Array.from({ length: count }).map((_, i) => (
|
|
1039
1081
|
<div key={i} className={styles.skeletonItem}>
|
|
1040
1082
|
{showIcon && <Skeleton variant="avatar" size="sm" />}
|
|
@@ -71,6 +71,10 @@ export default defineFragment({
|
|
|
71
71
|
type: 'function',
|
|
72
72
|
description: 'Called with new value when changed',
|
|
73
73
|
},
|
|
74
|
+
onValueChange: {
|
|
75
|
+
type: 'function',
|
|
76
|
+
description: 'Alias for onChange (Radix convention): (value: number) => void',
|
|
77
|
+
},
|
|
74
78
|
min: {
|
|
75
79
|
type: 'number',
|
|
76
80
|
description: 'Minimum value',
|
|
@@ -115,7 +119,7 @@ export default defineFragment({
|
|
|
115
119
|
propsSummary: [
|
|
116
120
|
'value: number - controlled value',
|
|
117
121
|
'defaultValue: number - initial value',
|
|
118
|
-
'onChange: (value: number) => void - change handler',
|
|
122
|
+
'onChange: (value: number) => void - change handler (or onValueChange)',
|
|
119
123
|
'min/max: number - range bounds',
|
|
120
124
|
'step: number - increment size',
|
|
121
125
|
'label: string - field label',
|
|
@@ -44,6 +44,12 @@ describe('Slider', () => {
|
|
|
44
44
|
expect(screen.getByText('75')).toBeInTheDocument();
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it('accepts onValueChange alias for onChange', () => {
|
|
48
|
+
const handleChange = vi.fn();
|
|
49
|
+
render(<Slider aria-label="Volume" value={50} onValueChange={handleChange} />);
|
|
50
|
+
expect(screen.getByRole('slider')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
47
53
|
it('has no accessibility violations', async () => {
|
|
48
54
|
const { container } = render(<Slider label="Accessible slider" defaultValue={50} />);
|
|
49
55
|
await expectNoA11yViolations(container);
|
|
@@ -5,11 +5,21 @@ import { Field } from '@base-ui/react/field';
|
|
|
5
5
|
import { Slider as BaseSlider } from '@base-ui/react/slider';
|
|
6
6
|
import styles from './Slider.module.scss';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Range slider for selecting a numeric value within a defined range.
|
|
10
|
+
* @see https://usefragments.com/components/slider
|
|
11
|
+
*/
|
|
8
12
|
export interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'> {
|
|
13
|
+
/** Visible label text */
|
|
9
14
|
label?: string;
|
|
15
|
+
/** Controlled value */
|
|
10
16
|
value?: number;
|
|
17
|
+
/** Default value for uncontrolled usage */
|
|
11
18
|
defaultValue?: number;
|
|
19
|
+
/** Called when the slider value changes */
|
|
12
20
|
onChange?: (value: number) => void;
|
|
21
|
+
/** Alias for onChange (Radix convention) */
|
|
22
|
+
onValueChange?: (value: number) => void;
|
|
13
23
|
min?: number;
|
|
14
24
|
max?: number;
|
|
15
25
|
step?: number;
|
|
@@ -32,6 +42,7 @@ const SliderRoot = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
|
32
42
|
value,
|
|
33
43
|
defaultValue,
|
|
34
44
|
onChange,
|
|
45
|
+
onValueChange,
|
|
35
46
|
min = 0,
|
|
36
47
|
max = 100,
|
|
37
48
|
step = 1,
|
|
@@ -52,10 +63,11 @@ const SliderRoot = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
|
52
63
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? min);
|
|
53
64
|
const displayValue = value !== undefined ? value : internalValue;
|
|
54
65
|
|
|
66
|
+
const resolvedOnChange = onChange ?? onValueChange;
|
|
55
67
|
const handleChange = (newValue: number | number[]) => {
|
|
56
68
|
const val = Array.isArray(newValue) ? newValue[0] : newValue;
|
|
57
69
|
setInternalValue(val);
|
|
58
|
-
|
|
70
|
+
resolvedOnChange?.(val);
|
|
59
71
|
};
|
|
60
72
|
|
|
61
73
|
return (
|
|
@@ -54,7 +54,7 @@ export default defineFragment({
|
|
|
54
54
|
},
|
|
55
55
|
gap: {
|
|
56
56
|
type: 'union',
|
|
57
|
-
description: 'Spacing between items: "none"
|
|
57
|
+
description: 'Spacing between items: "none"|"xs"|"sm"|"md"|"lg"|"xl", a number (1-8) for space scale, or responsive object',
|
|
58
58
|
default: 'md',
|
|
59
59
|
},
|
|
60
60
|
align: {
|
|
@@ -93,7 +93,7 @@ export default defineFragment({
|
|
|
93
93
|
contract: {
|
|
94
94
|
propsSummary: [
|
|
95
95
|
'direction: row|column|{responsive} - stack direction',
|
|
96
|
-
'gap: none|xs|sm|md|lg|xl|{responsive} - spacing',
|
|
96
|
+
'gap: none|xs|sm|md|lg|xl|number|{responsive} - spacing (number maps to space scale)',
|
|
97
97
|
'align: start|center|end|stretch|baseline - cross-axis',
|
|
98
98
|
'justify: start|center|end|between - main-axis',
|
|
99
99
|
'wrap: boolean - allow wrapping',
|
|
@@ -155,6 +155,26 @@ export default defineFragment({
|
|
|
155
155
|
</Stack>
|
|
156
156
|
),
|
|
157
157
|
},
|
|
158
|
+
{
|
|
159
|
+
name: 'Numeric Gap',
|
|
160
|
+
description: 'Using number values (1-8) mapped to the spacing scale',
|
|
161
|
+
render: () => (
|
|
162
|
+
<Stack gap="lg">
|
|
163
|
+
<Stack direction="row" gap={2}>
|
|
164
|
+
<Badge variant="info">Gap 2</Badge>
|
|
165
|
+
<Badge variant="info">Gap 2</Badge>
|
|
166
|
+
</Stack>
|
|
167
|
+
<Stack direction="row" gap={4}>
|
|
168
|
+
<Badge variant="info">Gap 4</Badge>
|
|
169
|
+
<Badge variant="info">Gap 4</Badge>
|
|
170
|
+
</Stack>
|
|
171
|
+
<Stack direction="row" gap={6}>
|
|
172
|
+
<Badge variant="info">Gap 6</Badge>
|
|
173
|
+
<Badge variant="info">Gap 6</Badge>
|
|
174
|
+
</Stack>
|
|
175
|
+
</Stack>
|
|
176
|
+
),
|
|
177
|
+
},
|
|
158
178
|
{
|
|
159
179
|
name: 'Alignment',
|
|
160
180
|
description: 'Cross-axis and main-axis alignment',
|
|
@@ -40,6 +40,12 @@ describe('Stack', () => {
|
|
|
40
40
|
expect(ref).toHaveBeenCalled();
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
it('applies inline style for numeric gap values', () => {
|
|
44
|
+
const { container } = render(<Stack gap={4}><span>A</span></Stack>);
|
|
45
|
+
const el = container.firstChild as HTMLElement;
|
|
46
|
+
expect(el.style.gap).toBe('var(--fui-space-4)');
|
|
47
|
+
});
|
|
48
|
+
|
|
43
49
|
it('has no accessibility violations', async () => {
|
|
44
50
|
const { container } = render(<Stack><span>A</span><span>B</span></Stack>);
|
|
45
51
|
await expectNoA11yViolations(container);
|