@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.
Files changed (123) hide show
  1. package/dist/components/Accordion/index.cjs +11 -4
  2. package/dist/components/Accordion/index.cjs.map +1 -1
  3. package/dist/components/Accordion/index.d.ts +3 -3
  4. package/dist/components/Accordion/index.d.ts.map +1 -1
  5. package/dist/components/Accordion/index.js +11 -4
  6. package/dist/components/Accordion/index.js.map +1 -1
  7. package/dist/components/Collapsible/index.cjs +45 -10
  8. package/dist/components/Collapsible/index.cjs.map +1 -1
  9. package/dist/components/Collapsible/index.d.ts +6 -12
  10. package/dist/components/Collapsible/index.d.ts.map +1 -1
  11. package/dist/components/Collapsible/index.js +45 -10
  12. package/dist/components/Collapsible/index.js.map +1 -1
  13. package/dist/components/Combobox/index.cjs +18 -9
  14. package/dist/components/Combobox/index.cjs.map +1 -1
  15. package/dist/components/Combobox/index.d.ts +8 -12
  16. package/dist/components/Combobox/index.d.ts.map +1 -1
  17. package/dist/components/Combobox/index.js +18 -9
  18. package/dist/components/Combobox/index.js.map +1 -1
  19. package/dist/components/Command/index.cjs +54 -21
  20. package/dist/components/Command/index.cjs.map +1 -1
  21. package/dist/components/Command/index.d.ts +2 -2
  22. package/dist/components/Command/index.d.ts.map +1 -1
  23. package/dist/components/Command/index.js +54 -21
  24. package/dist/components/Command/index.js.map +1 -1
  25. package/dist/components/DataTable/index.cjs +13 -1
  26. package/dist/components/DataTable/index.cjs.map +1 -1
  27. package/dist/components/DataTable/index.d.ts.map +1 -1
  28. package/dist/components/DataTable/index.js +13 -1
  29. package/dist/components/DataTable/index.js.map +1 -1
  30. package/dist/components/DatePicker/index.d.ts +2 -3
  31. package/dist/components/DatePicker/index.d.ts.map +1 -1
  32. package/dist/components/Dialog/index.cjs +12 -9
  33. package/dist/components/Dialog/index.cjs.map +1 -1
  34. package/dist/components/Dialog/index.d.ts +8 -12
  35. package/dist/components/Dialog/index.d.ts.map +1 -1
  36. package/dist/components/Dialog/index.js +12 -9
  37. package/dist/components/Dialog/index.js.map +1 -1
  38. package/dist/components/Drawer/index.cjs +12 -9
  39. package/dist/components/Drawer/index.cjs.map +1 -1
  40. package/dist/components/Drawer/index.d.ts +8 -12
  41. package/dist/components/Drawer/index.d.ts.map +1 -1
  42. package/dist/components/Drawer/index.js +12 -9
  43. package/dist/components/Drawer/index.js.map +1 -1
  44. package/dist/components/Menu/index.cjs +30 -16
  45. package/dist/components/Menu/index.cjs.map +1 -1
  46. package/dist/components/Menu/index.d.ts +17 -25
  47. package/dist/components/Menu/index.d.ts.map +1 -1
  48. package/dist/components/Menu/index.js +30 -16
  49. package/dist/components/Menu/index.js.map +1 -1
  50. package/dist/components/NavigationMenu/NavigationMenuContext.cjs.map +1 -1
  51. package/dist/components/NavigationMenu/NavigationMenuContext.d.ts +1 -0
  52. package/dist/components/NavigationMenu/NavigationMenuContext.d.ts.map +1 -1
  53. package/dist/components/NavigationMenu/NavigationMenuContext.js.map +1 -1
  54. package/dist/components/NavigationMenu/index.cjs +43 -11
  55. package/dist/components/NavigationMenu/index.cjs.map +1 -1
  56. package/dist/components/NavigationMenu/index.d.ts.map +1 -1
  57. package/dist/components/NavigationMenu/index.js +43 -11
  58. package/dist/components/NavigationMenu/index.js.map +1 -1
  59. package/dist/components/NavigationMenu/useNavigationMenu.cjs +2 -0
  60. package/dist/components/NavigationMenu/useNavigationMenu.cjs.map +1 -1
  61. package/dist/components/NavigationMenu/useNavigationMenu.d.ts +1 -0
  62. package/dist/components/NavigationMenu/useNavigationMenu.d.ts.map +1 -1
  63. package/dist/components/NavigationMenu/useNavigationMenu.js +2 -0
  64. package/dist/components/NavigationMenu/useNavigationMenu.js.map +1 -1
  65. package/dist/components/Popover/index.cjs +11 -10
  66. package/dist/components/Popover/index.cjs.map +1 -1
  67. package/dist/components/Popover/index.d.ts +8 -12
  68. package/dist/components/Popover/index.d.ts.map +1 -1
  69. package/dist/components/Popover/index.js +11 -10
  70. package/dist/components/Popover/index.js.map +1 -1
  71. package/dist/components/Select/index.cjs +7 -6
  72. package/dist/components/Select/index.cjs.map +1 -1
  73. package/dist/components/Select/index.d.ts +6 -9
  74. package/dist/components/Select/index.d.ts.map +1 -1
  75. package/dist/components/Select/index.js +7 -6
  76. package/dist/components/Select/index.js.map +1 -1
  77. package/dist/components/Sidebar/index.cjs +71 -24
  78. package/dist/components/Sidebar/index.cjs.map +1 -1
  79. package/dist/components/Sidebar/index.d.ts +21 -33
  80. package/dist/components/Sidebar/index.d.ts.map +1 -1
  81. package/dist/components/Sidebar/index.js +71 -24
  82. package/dist/components/Sidebar/index.js.map +1 -1
  83. package/dist/components/Tooltip/index.cjs +12 -6
  84. package/dist/components/Tooltip/index.cjs.map +1 -1
  85. package/dist/components/Tooltip/index.d.ts.map +1 -1
  86. package/dist/components/Tooltip/index.js +12 -6
  87. package/dist/components/Tooltip/index.js.map +1 -1
  88. package/dist/datepicker.cjs +24 -10
  89. package/dist/datepicker.cjs.map +1 -1
  90. package/dist/datepicker.js +24 -10
  91. package/dist/datepicker.js.map +1 -1
  92. package/fragments.json +1 -1
  93. package/package.json +2 -2
  94. package/src/components/Accordion/Accordion.test.tsx +33 -0
  95. package/src/components/Accordion/index.tsx +10 -3
  96. package/src/components/Collapsible/Collapsible.test.tsx +41 -0
  97. package/src/components/Collapsible/index.tsx +53 -16
  98. package/src/components/Combobox/Combobox.test.tsx +55 -0
  99. package/src/components/Combobox/index.tsx +23 -17
  100. package/src/components/Command/Command.test.tsx +93 -0
  101. package/src/components/Command/index.tsx +61 -18
  102. package/src/components/DataTable/DataTable.test.tsx +11 -2
  103. package/src/components/DataTable/index.tsx +22 -2
  104. package/src/components/DatePicker/DatePicker.test.tsx +79 -0
  105. package/src/components/DatePicker/index.tsx +29 -14
  106. package/src/components/Dialog/Dialog.test.tsx +23 -0
  107. package/src/components/Dialog/index.tsx +15 -16
  108. package/src/components/Drawer/Drawer.test.tsx +27 -0
  109. package/src/components/Drawer/index.tsx +15 -16
  110. package/src/components/Menu/index.tsx +35 -30
  111. package/src/components/NavigationMenu/NavigationMenu.fragment.tsx +1 -1
  112. package/src/components/NavigationMenu/NavigationMenu.test.tsx +40 -4
  113. package/src/components/NavigationMenu/NavigationMenuContext.ts +3 -0
  114. package/src/components/NavigationMenu/index.tsx +49 -13
  115. package/src/components/NavigationMenu/useNavigationMenu.ts +4 -0
  116. package/src/components/Popover/Popover.test.tsx +23 -0
  117. package/src/components/Popover/index.tsx +15 -18
  118. package/src/components/Select/Select.test.tsx +41 -0
  119. package/src/components/Select/index.tsx +10 -12
  120. package/src/components/Sidebar/Sidebar.test.tsx +83 -4
  121. package/src/components/Sidebar/index.tsx +87 -45
  122. package/src/components/Tooltip/Tooltip.test.tsx +17 -0
  123. 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
- // Mock matchMedia for jsdom
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: false,
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 interface SidebarOverlayProps {
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 interface SidebarRailProps {
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({ children, collapsedContent, className }: SidebarHeaderProps) {
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({ children, 'aria-label': ariaLabel = 'Main navigation', className }: SidebarNavProps) {
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, (children.props as { className?: string }).className].filter(Boolean).join(' '),
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({ children, 'aria-label': ariaLabel = 'Toggle navigation', className }: SidebarTriggerProps) {
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({ 'aria-label': ariaLabel, className }: SidebarCollapseToggleProps) {
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({ className }: SidebarRailProps) {
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}>