@fragments-sdk/ui 0.12.0 → 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/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/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 +8 -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 +8 -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/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 +8 -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/Select/index.cjs +7 -6
- package/dist/components/Select/index.cjs.map +1 -1
- package/dist/components/Select/index.d.ts +6 -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/Tooltip/index.cjs +12 -6
- package/dist/components/Tooltip/index.cjs.map +1 -1
- package/dist/components/Tooltip/index.d.ts.map +1 -1
- package/dist/components/Tooltip/index.js +12 -6
- 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/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/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 +15 -16
- package/src/components/Drawer/Drawer.test.tsx +27 -0
- package/src/components/Drawer/index.tsx +15 -16
- 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 +15 -18
- package/src/components/Select/Select.test.tsx +41 -0
- package/src/components/Select/index.tsx +10 -12
- package/src/components/Sidebar/Sidebar.test.tsx +83 -4
- package/src/components/Sidebar/index.tsx +87 -45
- package/src/components/Tooltip/Tooltip.test.tsx +17 -0
- package/src/components/Tooltip/index.tsx +46 -32
|
@@ -73,6 +73,26 @@ describe('Select', () => {
|
|
|
73
73
|
expect(await screen.findByText('Fruits')).toBeInTheDocument();
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
it('forwards html props to item and group labels', async () => {
|
|
77
|
+
const user = userEvent.setup();
|
|
78
|
+
render(
|
|
79
|
+
<Select placeholder="Pick">
|
|
80
|
+
<Select.Trigger />
|
|
81
|
+
<Select.Content>
|
|
82
|
+
<Select.Group id="fruit-group">
|
|
83
|
+
<Select.GroupLabel id="fruit-group-label">Fruits</Select.GroupLabel>
|
|
84
|
+
<Select.Item id="apple-option" value="apple">Apple</Select.Item>
|
|
85
|
+
</Select.Group>
|
|
86
|
+
</Select.Content>
|
|
87
|
+
</Select>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await user.click(screen.getByRole('combobox'));
|
|
91
|
+
expect(await screen.findByRole('option', { name: 'Apple' })).toHaveAttribute('id', 'apple-option');
|
|
92
|
+
expect(screen.getByText('Fruits')).toHaveAttribute('id', 'fruit-group-label');
|
|
93
|
+
expect(screen.getByText('Fruits').closest('#fruit-group')).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
76
96
|
it('has no accessibility violations', async () => {
|
|
77
97
|
const { container } = render(
|
|
78
98
|
<Select placeholder="Pick one">
|
|
@@ -158,4 +178,25 @@ describe('Select', () => {
|
|
|
158
178
|
expect(cherry).toHaveAttribute('aria-disabled', 'true');
|
|
159
179
|
});
|
|
160
180
|
});
|
|
181
|
+
|
|
182
|
+
it('updates trigger label when selected option is removed', async () => {
|
|
183
|
+
const renderDemo = (showApple: boolean) => (
|
|
184
|
+
<Select value="apple" placeholder="Pick one" defaultOpen>
|
|
185
|
+
<Select.Trigger />
|
|
186
|
+
<Select.Content>
|
|
187
|
+
{showApple && <Select.Item value="apple">Apple</Select.Item>}
|
|
188
|
+
<Select.Item value="banana">Banana</Select.Item>
|
|
189
|
+
</Select.Content>
|
|
190
|
+
</Select>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const { rerender } = render(renderDemo(true));
|
|
194
|
+
|
|
195
|
+
await screen.findByRole('option', { name: 'Apple' });
|
|
196
|
+
expect(screen.getByRole('combobox')).toHaveTextContent('Apple');
|
|
197
|
+
|
|
198
|
+
rerender(renderDemo(false));
|
|
199
|
+
|
|
200
|
+
expect(await screen.findByRole('combobox')).toHaveTextContent('Pick one');
|
|
201
|
+
});
|
|
161
202
|
});
|
|
@@ -57,21 +57,18 @@ export interface SelectContentProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
57
57
|
maxVisibleItems?: number;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export interface SelectItemProps {
|
|
60
|
+
export interface SelectItemProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
|
|
61
61
|
children: React.ReactNode;
|
|
62
62
|
value: SelectValue;
|
|
63
63
|
disabled?: boolean;
|
|
64
|
-
className?: string;
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
export interface SelectGroupProps {
|
|
66
|
+
export interface SelectGroupProps extends React.HTMLAttributes<HTMLElement> {
|
|
68
67
|
children: React.ReactNode;
|
|
69
|
-
className?: string;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
export interface SelectGroupLabelProps {
|
|
70
|
+
export interface SelectGroupLabelProps extends React.HTMLAttributes<HTMLElement> {
|
|
73
71
|
children: React.ReactNode;
|
|
74
|
-
className?: string;
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
// ============================================
|
|
@@ -276,7 +273,7 @@ function SelectContent({
|
|
|
276
273
|
);
|
|
277
274
|
}
|
|
278
275
|
|
|
279
|
-
function SelectItem({ children, value, disabled, className }: SelectItemProps) {
|
|
276
|
+
function SelectItem({ children, value, disabled, className, ...htmlProps }: SelectItemProps) {
|
|
280
277
|
const { itemsRef, incrementItemsVersion } = React.useContext(SelectContext);
|
|
281
278
|
const classes = [styles.item, className].filter(Boolean).join(' ');
|
|
282
279
|
|
|
@@ -288,11 +285,12 @@ function SelectItem({ children, value, disabled, className }: SelectItemProps) {
|
|
|
288
285
|
incrementItemsVersion();
|
|
289
286
|
return () => {
|
|
290
287
|
items.delete(value);
|
|
288
|
+
incrementItemsVersion();
|
|
291
289
|
};
|
|
292
290
|
}, [itemsRef, incrementItemsVersion, value, children]);
|
|
293
291
|
|
|
294
292
|
return (
|
|
295
|
-
<BaseSelect.Item value={value} disabled={disabled} className={classes}>
|
|
293
|
+
<BaseSelect.Item {...htmlProps} value={value} disabled={disabled} className={classes}>
|
|
296
294
|
<BaseSelect.ItemText>{children}</BaseSelect.ItemText>
|
|
297
295
|
<BaseSelect.ItemIndicator className={styles.itemIndicator}>
|
|
298
296
|
<CheckIcon />
|
|
@@ -301,14 +299,14 @@ function SelectItem({ children, value, disabled, className }: SelectItemProps) {
|
|
|
301
299
|
);
|
|
302
300
|
}
|
|
303
301
|
|
|
304
|
-
function SelectGroup({ children, className }: SelectGroupProps) {
|
|
302
|
+
function SelectGroup({ children, className, ...htmlProps }: SelectGroupProps) {
|
|
305
303
|
const classes = [styles.group, className].filter(Boolean).join(' ');
|
|
306
|
-
return <BaseSelect.Group className={classes}>{children}</BaseSelect.Group>;
|
|
304
|
+
return <BaseSelect.Group {...htmlProps} className={classes}>{children}</BaseSelect.Group>;
|
|
307
305
|
}
|
|
308
306
|
|
|
309
|
-
function SelectGroupLabel({ children, className }: SelectGroupLabelProps) {
|
|
307
|
+
function SelectGroupLabel({ children, className, ...htmlProps }: SelectGroupLabelProps) {
|
|
310
308
|
const classes = [styles.groupLabel, className].filter(Boolean).join(' ');
|
|
311
|
-
return <BaseSelect.GroupLabel className={classes}>{children}</BaseSelect.GroupLabel>;
|
|
309
|
+
return <BaseSelect.GroupLabel {...htmlProps} className={classes}>{children}</BaseSelect.GroupLabel>;
|
|
312
310
|
}
|
|
313
311
|
|
|
314
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" />}
|
|
@@ -91,6 +91,23 @@ describe('Tooltip', () => {
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
it('respects shared Tooltip.Provider delay settings', async () => {
|
|
95
|
+
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
|
96
|
+
render(
|
|
97
|
+
<Tooltip.Provider delay={0}>
|
|
98
|
+
<Tooltip content="Provider tooltip">
|
|
99
|
+
<button>Provider Trigger</button>
|
|
100
|
+
</Tooltip>
|
|
101
|
+
</Tooltip.Provider>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
await user.hover(screen.getByRole('button', { name: /provider trigger/i }));
|
|
105
|
+
|
|
106
|
+
await waitFor(() => {
|
|
107
|
+
expect(screen.getByText('Provider tooltip')).toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
94
111
|
it('has no accessibility violations when open', async () => {
|
|
95
112
|
const { container } = render(
|
|
96
113
|
<Tooltip content="Accessible tooltip" open={true} delay={0}>
|