@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
@@ -38,24 +38,21 @@ export interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement>
38
38
  backdrop?: boolean;
39
39
  }
40
40
 
41
- export interface DrawerTriggerProps {
41
+ export interface DrawerTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
42
42
  children: React.ReactNode;
43
43
  asChild?: boolean;
44
- className?: string;
45
44
  }
46
45
 
47
46
  export interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
48
47
  children: React.ReactNode;
49
48
  }
50
49
 
51
- export interface DrawerTitleProps {
50
+ export interface DrawerTitleProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
52
51
  children: React.ReactNode;
53
- className?: string;
54
52
  }
55
53
 
56
- export interface DrawerDescriptionProps {
54
+ export interface DrawerDescriptionProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
57
55
  children: React.ReactNode;
58
- className?: string;
59
56
  }
60
57
 
61
58
  export interface DrawerBodyProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -66,10 +63,9 @@ export interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement>
66
63
  children: React.ReactNode;
67
64
  }
68
65
 
69
- export interface DrawerCloseProps {
66
+ export interface DrawerCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
70
67
  children?: React.ReactNode;
71
68
  asChild?: boolean;
72
- className?: string;
73
69
  }
74
70
 
75
71
  // ============================================
@@ -123,17 +119,18 @@ function DrawerTrigger({
123
119
  children,
124
120
  asChild,
125
121
  className,
122
+ ...htmlProps
126
123
  }: DrawerTriggerProps) {
127
124
  if (asChild) {
128
125
  return (
129
- <BaseDialog.Trigger className={className} render={children as React.ReactElement}>
126
+ <BaseDialog.Trigger {...htmlProps} className={className} render={children as React.ReactElement}>
130
127
  {null}
131
128
  </BaseDialog.Trigger>
132
129
  );
133
130
  }
134
131
 
135
132
  return (
136
- <BaseDialog.Trigger className={className}>
133
+ <BaseDialog.Trigger {...htmlProps} className={className}>
137
134
  {children}
138
135
  </BaseDialog.Trigger>
139
136
  );
@@ -171,15 +168,15 @@ function DrawerHeader({ children, className, ...htmlProps }: DrawerHeaderProps)
171
168
  return <div {...htmlProps} className={classes}>{children}</div>;
172
169
  }
173
170
 
174
- function DrawerTitle({ children, className }: DrawerTitleProps) {
171
+ function DrawerTitle({ children, className, ...htmlProps }: DrawerTitleProps) {
175
172
  const classes = [styles.title, className].filter(Boolean).join(' ');
176
- return <BaseDialog.Title className={classes}>{children}</BaseDialog.Title>;
173
+ return <BaseDialog.Title {...htmlProps} className={classes}>{children}</BaseDialog.Title>;
177
174
  }
178
175
 
179
- function DrawerDescription({ children, className }: DrawerDescriptionProps) {
176
+ function DrawerDescription({ children, className, ...htmlProps }: DrawerDescriptionProps) {
180
177
  const classes = [styles.description, className].filter(Boolean).join(' ');
181
178
  return (
182
- <BaseDialog.Description className={classes}>
179
+ <BaseDialog.Description {...htmlProps} className={classes}>
183
180
  {children}
184
181
  </BaseDialog.Description>
185
182
  );
@@ -195,10 +192,11 @@ function DrawerFooter({ children, className, ...htmlProps }: DrawerFooterProps)
195
192
  return <div {...htmlProps} className={classes}>{children}</div>;
196
193
  }
197
194
 
198
- function DrawerClose({ children, asChild, className }: DrawerCloseProps) {
195
+ function DrawerClose({ children, asChild, className, ...htmlProps }: DrawerCloseProps) {
199
196
  if (!children) {
200
197
  return (
201
198
  <BaseDialog.Close
199
+ {...htmlProps}
202
200
  data-drawer-close
203
201
  aria-label="Close drawer"
204
202
  className={[styles.close, className].filter(Boolean).join(' ')}
@@ -211,6 +209,7 @@ function DrawerClose({ children, asChild, className }: DrawerCloseProps) {
211
209
  if (asChild) {
212
210
  return (
213
211
  <BaseDialog.Close
212
+ {...htmlProps}
214
213
  data-drawer-close
215
214
  className={className}
216
215
  render={children as React.ReactElement}
@@ -221,7 +220,7 @@ function DrawerClose({ children, asChild, className }: DrawerCloseProps) {
221
220
  }
222
221
 
223
222
  return (
224
- <BaseDialog.Close data-drawer-close className={className}>
223
+ <BaseDialog.Close {...htmlProps} data-drawer-close className={className}>
225
224
  {children}
226
225
  </BaseDialog.Close>
227
226
  );
@@ -16,10 +16,9 @@ export interface MenuProps {
16
16
  modal?: boolean;
17
17
  }
18
18
 
19
- export interface MenuTriggerProps {
19
+ export interface MenuTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
20
20
  children: React.ReactNode;
21
21
  asChild?: boolean;
22
- className?: string;
23
22
  }
24
23
 
25
24
  export interface MenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -29,25 +28,23 @@ export interface MenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
29
28
  sideOffset?: number;
30
29
  }
31
30
 
32
- export interface MenuItemProps {
31
+ export interface MenuItemProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children' | 'onSelect'> {
33
32
  children: React.ReactNode;
34
33
  disabled?: boolean;
35
34
  danger?: boolean;
36
- onSelect?: () => void;
37
- className?: string;
35
+ onSelect?: (...args: any[]) => void;
38
36
  icon?: React.ReactNode;
39
37
  shortcut?: string;
40
38
  /** When passed, renders a check indicator. `true` shows a checkmark, `false` reserves space. */
41
39
  checked?: boolean;
42
40
  }
43
41
 
44
- export interface MenuCheckboxItemProps {
42
+ export interface MenuCheckboxItemProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children' | 'onChange'> {
45
43
  children: React.ReactNode;
46
44
  checked?: boolean;
47
45
  defaultChecked?: boolean;
48
46
  onCheckedChange?: (checked: boolean) => void;
49
47
  disabled?: boolean;
50
- className?: string;
51
48
  }
52
49
 
53
50
  export interface MenuRadioGroupProps {
@@ -57,26 +54,21 @@ export interface MenuRadioGroupProps {
57
54
  onValueChange?: (value: string) => void;
58
55
  }
59
56
 
60
- export interface MenuRadioItemProps {
57
+ export interface MenuRadioItemProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
61
58
  children: React.ReactNode;
62
59
  value: string;
63
60
  disabled?: boolean;
64
- className?: string;
65
61
  }
66
62
 
67
- export interface MenuGroupProps {
63
+ export interface MenuGroupProps extends React.HTMLAttributes<HTMLElement> {
68
64
  children: React.ReactNode;
69
- className?: string;
70
65
  }
71
66
 
72
- export interface MenuGroupLabelProps {
67
+ export interface MenuGroupLabelProps extends React.HTMLAttributes<HTMLElement> {
73
68
  children: React.ReactNode;
74
- className?: string;
75
69
  }
76
70
 
77
- export interface MenuSeparatorProps {
78
- className?: string;
79
- }
71
+ export interface MenuSeparatorProps extends React.HTMLAttributes<HTMLElement> {}
80
72
 
81
73
  export interface MenuSubmenuProps {
82
74
  children: React.ReactNode;
@@ -85,10 +77,9 @@ export interface MenuSubmenuProps {
85
77
  onOpenChange?: (open: boolean) => void;
86
78
  }
87
79
 
88
- export interface MenuSubmenuTriggerProps {
80
+ export interface MenuSubmenuTriggerProps extends React.HTMLAttributes<HTMLElement> {
89
81
  children: React.ReactNode;
90
82
  disabled?: boolean;
91
- className?: string;
92
83
  icon?: React.ReactNode;
93
84
  }
94
85
 
@@ -153,17 +144,17 @@ function MenuRoot({
153
144
  );
154
145
  }
155
146
 
156
- function MenuTrigger({ children, asChild, className }: MenuTriggerProps) {
147
+ function MenuTrigger({ children, asChild, className, ...htmlProps }: MenuTriggerProps) {
157
148
  if (asChild) {
158
149
  return (
159
- <BaseMenu.Trigger className={className} render={children as React.ReactElement}>
150
+ <BaseMenu.Trigger {...htmlProps} className={className} render={children as React.ReactElement}>
160
151
  {null}
161
152
  </BaseMenu.Trigger>
162
153
  );
163
154
  }
164
155
 
165
156
  return (
166
- <BaseMenu.Trigger className={className}>
157
+ <BaseMenu.Trigger {...htmlProps} className={className}>
167
158
  {children}
168
159
  </BaseMenu.Trigger>
169
160
  );
@@ -204,7 +195,16 @@ function MenuItem({
204
195
  icon,
205
196
  shortcut,
206
197
  checked,
198
+ ...htmlProps
207
199
  }: MenuItemProps) {
200
+ const handleClick = React.useCallback(
201
+ (event: React.MouseEvent<HTMLElement>) => {
202
+ (htmlProps.onClick as React.MouseEventHandler<HTMLElement> | undefined)?.(event);
203
+ onSelect?.(event);
204
+ },
205
+ [htmlProps, onSelect],
206
+ );
207
+
208
208
  const hasChecked = checked !== undefined;
209
209
  const classes = [
210
210
  styles.item,
@@ -214,8 +214,9 @@ function MenuItem({
214
214
 
215
215
  return (
216
216
  <BaseMenu.Item
217
+ {...htmlProps}
217
218
  disabled={disabled}
218
- onClick={onSelect}
219
+ onClick={handleClick as any}
219
220
  className={classes}
220
221
  >
221
222
  {hasChecked && (
@@ -237,6 +238,7 @@ function MenuCheckboxItem({
237
238
  onCheckedChange,
238
239
  disabled,
239
240
  className,
241
+ ...htmlProps
240
242
  }: MenuCheckboxItemProps) {
241
243
  const isControlled = checkedProp !== undefined;
242
244
  const [internalChecked, setInternalChecked] = React.useState(defaultChecked ?? false);
@@ -256,6 +258,7 @@ function MenuCheckboxItem({
256
258
 
257
259
  return (
258
260
  <BaseMenu.CheckboxItem
261
+ {...htmlProps}
259
262
  checked={checkedProp}
260
263
  defaultChecked={defaultChecked}
261
264
  onCheckedChange={handleCheckedChange}
@@ -292,13 +295,14 @@ function MenuRadioItem({
292
295
  value,
293
296
  disabled,
294
297
  className,
298
+ ...htmlProps
295
299
  }: MenuRadioItemProps) {
296
300
  const classes = [styles.item, styles.radioItem, className]
297
301
  .filter(Boolean)
298
302
  .join(' ');
299
303
 
300
304
  return (
301
- <BaseMenu.RadioItem value={value} disabled={disabled} className={classes}>
305
+ <BaseMenu.RadioItem {...htmlProps} value={value} disabled={disabled} className={classes}>
302
306
  <span className={styles.radioIndicator}>
303
307
  <DotIcon />
304
308
  </span>
@@ -307,19 +311,19 @@ function MenuRadioItem({
307
311
  );
308
312
  }
309
313
 
310
- function MenuGroup({ children, className }: MenuGroupProps) {
314
+ function MenuGroup({ children, className, ...htmlProps }: MenuGroupProps) {
311
315
  const classes = [styles.group, className].filter(Boolean).join(' ');
312
- return <BaseMenu.Group className={classes}>{children}</BaseMenu.Group>;
316
+ return <BaseMenu.Group {...htmlProps} className={classes}>{children}</BaseMenu.Group>;
313
317
  }
314
318
 
315
- function MenuGroupLabel({ children, className }: MenuGroupLabelProps) {
319
+ function MenuGroupLabel({ children, className, ...htmlProps }: MenuGroupLabelProps) {
316
320
  const classes = [styles.groupLabel, className].filter(Boolean).join(' ');
317
- return <BaseMenu.GroupLabel className={classes}>{children}</BaseMenu.GroupLabel>;
321
+ return <BaseMenu.GroupLabel {...htmlProps} className={classes}>{children}</BaseMenu.GroupLabel>;
318
322
  }
319
323
 
320
- function MenuSeparator({ className }: MenuSeparatorProps) {
324
+ function MenuSeparator({ className, ...htmlProps }: MenuSeparatorProps) {
321
325
  const classes = [styles.separator, className].filter(Boolean).join(' ');
322
- return <BaseMenu.Separator className={classes} />;
326
+ return <BaseMenu.Separator {...htmlProps} className={classes} />;
323
327
  }
324
328
 
325
329
  function MenuSubmenu({
@@ -344,13 +348,14 @@ function MenuSubmenuTrigger({
344
348
  disabled,
345
349
  className,
346
350
  icon,
351
+ ...htmlProps
347
352
  }: MenuSubmenuTriggerProps) {
348
353
  const classes = [styles.item, styles.submenuTrigger, className]
349
354
  .filter(Boolean)
350
355
  .join(' ');
351
356
 
352
357
  return (
353
- <BaseMenu.SubmenuTrigger disabled={disabled} className={classes}>
358
+ <BaseMenu.SubmenuTrigger {...htmlProps} disabled={disabled} className={classes}>
354
359
  {icon && <span className={styles.itemIcon}>{icon}</span>}
355
360
  <span className={styles.itemLabel}>{children}</span>
356
361
  </BaseMenu.SubmenuTrigger>
@@ -107,7 +107,7 @@ export default defineFragment({
107
107
 
108
108
  ai: {
109
109
  compositionPattern: 'compound',
110
- subComponents: ['List', 'Item', 'Trigger', 'Content', 'Link', 'Indicator', 'Viewport', 'MobileContent', 'MobileSection'],
110
+ subComponents: ['List', 'Item', 'Trigger', 'Content', 'Link', 'Indicator', 'Viewport', 'MobileBrand', 'MobileContent', 'MobileSection'],
111
111
  requiredChildren: ['List'],
112
112
  commonPatterns: [
113
113
  '<NavigationMenu><NavigationMenu.List><NavigationMenu.Item value="docs"><NavigationMenu.Trigger>Docs</NavigationMenu.Trigger><NavigationMenu.Content><NavigationMenu.Link href="/guides" title="Guides" description="Learn the basics" /></NavigationMenu.Content></NavigationMenu.Item></NavigationMenu.List><NavigationMenu.Viewport /></NavigationMenu>',
@@ -1,15 +1,14 @@
1
1
  import React from 'react';
2
2
  import { render, screen, userEvent, expectNoA11yViolations } from '../../test/utils';
3
- import { fireEvent, act } from '@testing-library/react';
3
+ import { fireEvent, act, within } from '@testing-library/react';
4
4
  import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest';
5
5
  import { NavigationMenu } from '.';
6
6
 
7
- // Mock matchMedia for jsdom
8
- beforeAll(() => {
7
+ function setMatchMedia(matches: boolean) {
9
8
  Object.defineProperty(window, 'matchMedia', {
10
9
  writable: true,
11
10
  value: vi.fn().mockImplementation((query: string) => ({
12
- matches: false,
11
+ matches,
13
12
  media: query,
14
13
  onchange: null,
15
14
  addListener: vi.fn(),
@@ -19,6 +18,11 @@ beforeAll(() => {
19
18
  dispatchEvent: vi.fn(),
20
19
  })),
21
20
  });
21
+ }
22
+
23
+ // Mock matchMedia for jsdom
24
+ beforeAll(() => {
25
+ setMatchMedia(false);
22
26
  });
23
27
 
24
28
  // ============================================
@@ -254,6 +258,17 @@ describe('NavigationMenu', () => {
254
258
  expect(document.activeElement).toBe(communityTrigger);
255
259
  });
256
260
 
261
+ it('navigates based on focused trigger even when another item is open', async () => {
262
+ renderBasicMenu();
263
+ const learnTrigger = screen.getByText('Learn');
264
+ const communityTrigger = screen.getByText('Community');
265
+
266
+ await userEvent.click(learnTrigger);
267
+ communityTrigger.focus();
268
+ fireEvent.keyDown(communityTrigger.closest('ul')!, { key: 'ArrowRight' });
269
+ expect(document.activeElement).toBe(learnTrigger);
270
+ });
271
+
257
272
  it('navigates to first trigger on Home', () => {
258
273
  renderBasicMenu();
259
274
  const communityTrigger = screen.getByText('Community');
@@ -454,4 +469,25 @@ describe('NavigationMenu', () => {
454
469
  expect(screen.queryByText('Extra')).not.toBeInTheDocument();
455
470
  });
456
471
  });
472
+
473
+ describe('Mobile drawer', () => {
474
+ beforeEach(() => {
475
+ setMatchMedia(true);
476
+ });
477
+
478
+ afterEach(() => {
479
+ setMatchMedia(false);
480
+ });
481
+
482
+ it('includes direct link items in auto-converted drawer navigation', async () => {
483
+ renderBasicMenu();
484
+
485
+ const toggle = await screen.findByLabelText('Toggle navigation');
486
+ await userEvent.click(toggle);
487
+
488
+ const drawer = await screen.findByRole('dialog', { name: 'Navigation' });
489
+ const blogLink = within(drawer).getByRole('link', { name: 'Blog' });
490
+ expect(blogLink).toHaveAttribute('href', '/blog');
491
+ });
492
+ });
457
493
  });
@@ -34,6 +34,9 @@ export interface NavigationMenuContextValue {
34
34
  triggerRefs: React.MutableRefObject<Map<string, HTMLButtonElement>>;
35
35
  triggerOrder: React.MutableRefObject<string[]>;
36
36
 
37
+ // Full item order (includes items without triggers)
38
+ itemOrder: React.MutableRefObject<string[]>;
39
+
37
40
  // Item info registry (for mobile drawer)
38
41
  itemInfoMap: React.MutableRefObject<Map<string, NavigationMenuItemInfo>>;
39
42
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react';
4
4
  import { createPortal } from 'react-dom';
5
- import { CaretDown, CaretRight, List, X } from '@phosphor-icons/react';
5
+ import { CaretDown, List, X } from '@phosphor-icons/react';
6
6
  import { handleArrowNavigation, useFocusTrap } from '../../utils/a11y';
7
7
  import { Collapsible } from '../Collapsible';
8
8
  import { ScrollArea } from '../ScrollArea';
@@ -183,7 +183,9 @@ function NavigationMenuList({ children, className }: NavigationMenuListProps) {
183
183
 
184
184
  const handleKeyDown = (e: React.KeyboardEvent) => {
185
185
  const order = triggerOrder.current;
186
- const currentIdx = order.indexOf(value);
186
+ const focusedValue = (document.activeElement as HTMLElement | null)?.getAttribute('data-navmenu-value');
187
+ const currentValue = focusedValue || value;
188
+ const currentIdx = order.indexOf(currentValue);
187
189
 
188
190
  const newIdx = handleArrowNavigation(e, order, currentIdx >= 0 ? currentIdx : 0, {
189
191
  orientation: orientation === 'horizontal' ? 'horizontal' : 'vertical',
@@ -220,14 +222,33 @@ function NavigationMenuList({ children, className }: NavigationMenuListProps) {
220
222
  // Item
221
223
  // ============================================
222
224
 
223
- let itemCounter = 0;
224
-
225
225
  function NavigationMenuItem({ children, value: valueProp, className }: NavigationMenuItemProps) {
226
226
  const rootCtx = useNavigationMenuContext();
227
- const [autoValue] = React.useState(() => valueProp || `navmenu-item-${++itemCounter}`);
227
+ const generatedValue = React.useId();
228
+ const autoValue = valueProp || `navmenu-item-${generatedValue}`;
228
229
  const triggerId = `${rootCtx.rootId}-trigger-${autoValue}`;
229
230
  const contentId = `${rootCtx.rootId}-content-${autoValue}`;
230
231
 
232
+ React.useEffect(() => {
233
+ if (!rootCtx.itemOrder.current.includes(autoValue)) {
234
+ rootCtx.itemOrder.current.push(autoValue);
235
+ }
236
+
237
+ const existing = rootCtx.itemInfoMap.current.get(autoValue);
238
+ if (!existing) {
239
+ rootCtx.itemInfoMap.current.set(autoValue, {
240
+ value: autoValue,
241
+ triggerLabel: '',
242
+ contentChildren: null,
243
+ });
244
+ }
245
+
246
+ return () => {
247
+ rootCtx.itemOrder.current = rootCtx.itemOrder.current.filter(v => v !== autoValue);
248
+ rootCtx.itemInfoMap.current.delete(autoValue);
249
+ };
250
+ }, [autoValue, rootCtx.itemInfoMap, rootCtx.itemOrder]);
251
+
231
252
  const itemCtx = React.useMemo(
232
253
  () => ({
233
254
  value: autoValue,
@@ -335,6 +356,7 @@ function NavigationMenuTrigger({ children, className }: NavigationMenuTriggerPro
335
356
  type="button"
336
357
  id={itemCtx.triggerId}
337
358
  className={classes}
359
+ data-navmenu-value={itemCtx.value}
338
360
  aria-expanded={isOpen}
339
361
  aria-controls={itemCtx.contentId}
340
362
  data-state={isOpen ? 'open' : 'closed'}
@@ -472,8 +494,25 @@ function NavigationMenuLink({
472
494
  ...htmlProps
473
495
  }: NavigationMenuLinkProps) {
474
496
  const ctx = React.useContext(NavigationMenuContext);
497
+ const itemCtx = React.useContext(NavigationMenuItemContext);
475
498
  const isStructured = !!(title || description || icon);
476
499
 
500
+ React.useEffect(() => {
501
+ if (!ctx || !itemCtx) return;
502
+
503
+ const existing = ctx.itemInfoMap.current.get(itemCtx.value);
504
+ const fallbackLabel = typeof children === 'string' ? children : title || '';
505
+ const resolvedHref = typeof href === 'string' ? href : existing?.linkHref;
506
+
507
+ ctx.itemInfoMap.current.set(itemCtx.value, {
508
+ ...existing,
509
+ value: itemCtx.value,
510
+ triggerLabel: existing?.triggerLabel || fallbackLabel || '',
511
+ contentChildren: existing?.contentChildren ?? null,
512
+ linkHref: resolvedHref,
513
+ });
514
+ }, [ctx, itemCtx, children, title, href]);
515
+
477
516
  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
478
517
  onClick?.(e);
479
518
  // Close mobile drawer on link click
@@ -745,14 +784,11 @@ function MobileDrawer() {
745
784
  }, [ctx]);
746
785
 
747
786
  // Build auto-converted nav items from item info registry
748
- const autoItems = React.useMemo(() => {
749
- const items: NavigationMenuItemInfo[] = [];
750
- for (const value of ctx.triggerOrder.current) {
751
- const info = ctx.itemInfoMap.current.get(value);
752
- if (info) items.push(info);
753
- }
754
- return items;
755
- }, [ctx.triggerOrder, ctx.itemInfoMap]);
787
+ const autoItems: NavigationMenuItemInfo[] = [];
788
+ for (const value of ctx.itemOrder.current) {
789
+ const info = ctx.itemInfoMap.current.get(value);
790
+ if (info) autoItems.push(info);
791
+ }
756
792
 
757
793
  const handleLinkClick = () => {
758
794
  ctx.setMobileOpen(false);
@@ -44,6 +44,9 @@ export function useNavigationMenu({
44
44
  const triggerRefs = React.useRef<Map<string, HTMLButtonElement>>(new Map());
45
45
  const triggerOrder = React.useRef<string[]>([]);
46
46
 
47
+ // Full item order registry (includes link-only items)
48
+ const itemOrder = React.useRef<string[]>([]);
49
+
47
50
  // Item info registry for mobile drawer
48
51
  const itemInfoMap = React.useRef<Map<string, NavigationMenuItemInfo>>(new Map());
49
52
 
@@ -79,6 +82,7 @@ export function useNavigationMenu({
79
82
  skipDelayTimerRef,
80
83
  triggerRefs,
81
84
  triggerOrder,
85
+ itemOrder,
82
86
  itemInfoMap,
83
87
  viewportSize,
84
88
  setViewportSize,
@@ -43,6 +43,29 @@ describe('Popover', () => {
43
43
  });
44
44
  });
45
45
 
46
+ it('forwards html props to trigger, title, description, and close', async () => {
47
+ const user = userEvent.setup();
48
+ render(
49
+ <Popover>
50
+ <Popover.Trigger id="popover-trigger">Open</Popover.Trigger>
51
+ <Popover.Content>
52
+ <Popover.Title id="popover-title">Popover Title</Popover.Title>
53
+ <Popover.Description id="popover-description">Popover Description</Popover.Description>
54
+ <Popover.Close data-testid="popover-close" />
55
+ </Popover.Content>
56
+ </Popover>
57
+ );
58
+
59
+ expect(screen.getByRole('button', { name: /open/i })).toHaveAttribute('id', 'popover-trigger');
60
+ await user.click(screen.getByRole('button', { name: /open/i }));
61
+
62
+ await waitFor(() => {
63
+ expect(screen.getByText('Popover Title')).toHaveAttribute('id', 'popover-title');
64
+ expect(screen.getByText('Popover Description')).toHaveAttribute('id', 'popover-description');
65
+ expect(screen.getByTestId('popover-close')).toBeInTheDocument();
66
+ });
67
+ });
68
+
46
69
  it('has a close button with aria-label', async () => {
47
70
  const user = userEvent.setup();
48
71
  renderPopover();
@@ -25,10 +25,9 @@ export interface PopoverProps {
25
25
  modal?: boolean;
26
26
  }
27
27
 
28
- export interface PopoverTriggerProps {
28
+ export interface PopoverTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
29
29
  children: React.ReactNode;
30
30
  asChild?: boolean;
31
- className?: string;
32
31
  }
33
32
 
34
33
  export interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -40,14 +39,12 @@ export interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement
40
39
  arrow?: boolean;
41
40
  }
42
41
 
43
- export interface PopoverTitleProps {
42
+ export interface PopoverTitleProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
44
43
  children: React.ReactNode;
45
- className?: string;
46
44
  }
47
45
 
48
- export interface PopoverDescriptionProps {
46
+ export interface PopoverDescriptionProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
49
47
  children: React.ReactNode;
50
- className?: string;
51
48
  }
52
49
 
53
50
  export interface PopoverBodyProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -58,10 +55,9 @@ export interface PopoverFooterProps extends React.HTMLAttributes<HTMLDivElement>
58
55
  children: React.ReactNode;
59
56
  }
60
57
 
61
- export interface PopoverCloseProps {
58
+ export interface PopoverCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
62
59
  children?: React.ReactNode;
63
60
  asChild?: boolean;
64
- className?: string;
65
61
  }
66
62
 
67
63
  // ============================================
@@ -111,17 +107,17 @@ function PopoverRoot({
111
107
  );
112
108
  }
113
109
 
114
- function PopoverTrigger({ children, asChild, className }: PopoverTriggerProps) {
110
+ function PopoverTrigger({ children, asChild, className, ...htmlProps }: PopoverTriggerProps) {
115
111
  if (asChild) {
116
112
  return (
117
- <BasePopover.Trigger className={className} render={children as React.ReactElement}>
113
+ <BasePopover.Trigger {...htmlProps} className={className} render={children as React.ReactElement}>
118
114
  {null}
119
115
  </BasePopover.Trigger>
120
116
  );
121
117
  }
122
118
 
123
119
  return (
124
- <BasePopover.Trigger className={className}>
120
+ <BasePopover.Trigger {...htmlProps} className={className}>
125
121
  {children}
126
122
  </BasePopover.Trigger>
127
123
  );
@@ -160,15 +156,15 @@ function PopoverContent({
160
156
  );
161
157
  }
162
158
 
163
- function PopoverTitle({ children, className }: PopoverTitleProps) {
159
+ function PopoverTitle({ children, className, ...htmlProps }: PopoverTitleProps) {
164
160
  const classes = [styles.title, className].filter(Boolean).join(' ');
165
- return <BasePopover.Title className={classes}>{children}</BasePopover.Title>;
161
+ return <BasePopover.Title {...htmlProps} className={classes}>{children}</BasePopover.Title>;
166
162
  }
167
163
 
168
- function PopoverDescription({ children, className }: PopoverDescriptionProps) {
164
+ function PopoverDescription({ children, className, ...htmlProps }: PopoverDescriptionProps) {
169
165
  const classes = [styles.description, className].filter(Boolean).join(' ');
170
166
  return (
171
- <BasePopover.Description className={classes}>
167
+ <BasePopover.Description {...htmlProps} className={classes}>
172
168
  {children}
173
169
  </BasePopover.Description>
174
170
  );
@@ -184,11 +180,12 @@ function PopoverFooter({ children, className, ...htmlProps }: PopoverFooterProps
184
180
  return <div {...htmlProps} className={classes}>{children}</div>;
185
181
  }
186
182
 
187
- function PopoverClose({ children, asChild, className }: PopoverCloseProps) {
183
+ function PopoverClose({ children, asChild, className, ...htmlProps }: PopoverCloseProps) {
188
184
  // Default close button (X icon)
189
185
  if (!children) {
190
186
  return (
191
187
  <BasePopover.Close
188
+ {...htmlProps}
192
189
  aria-label="Close popover"
193
190
  className={[styles.close, className].filter(Boolean).join(' ')}
194
191
  >
@@ -199,14 +196,14 @@ function PopoverClose({ children, asChild, className }: PopoverCloseProps) {
199
196
 
200
197
  if (asChild) {
201
198
  return (
202
- <BasePopover.Close className={className} render={children as React.ReactElement}>
199
+ <BasePopover.Close {...htmlProps} className={className} render={children as React.ReactElement}>
203
200
  {null}
204
201
  </BasePopover.Close>
205
202
  );
206
203
  }
207
204
 
208
205
  return (
209
- <BasePopover.Close className={className}>
206
+ <BasePopover.Close {...htmlProps} className={className}>
210
207
  {children}
211
208
  </BasePopover.Close>
212
209
  );