@enonic/ui 0.15.0 → 0.16.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/enonic-ui.cjs +5 -5
- package/dist/enonic-ui.es.js +1012 -115
- package/dist/styles/style.css +1 -1
- package/dist/types/components/combobox/combobox.d.ts +9 -3
- package/dist/types/components/index.d.ts +1 -0
- package/dist/types/components/listbox/listbox.d.ts +5 -2
- package/dist/types/components/menu/menu.d.ts +3 -4
- package/dist/types/components/menubar/index.d.ts +2 -0
- package/dist/types/components/menubar/menubar.d.ts +346 -0
- package/dist/types/hooks/index.d.ts +1 -0
- package/dist/types/hooks/use-controlled-state-with-null.d.ts +47 -0
- package/dist/types/hooks/use-controlled-state.d.ts +20 -2
- package/dist/types/providers/index.d.ts +2 -0
- package/dist/types/providers/listbox-provider.d.ts +9 -1
- package/dist/types/providers/menubar-menu-provider.d.ts +36 -0
- package/dist/types/providers/menubar-provider.d.ts +75 -0
- package/package.json +1 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Root component that provides context for the Menubar.
|
|
4
|
+
*
|
|
5
|
+
* Implements the ARIA menubar pattern for horizontal navigation.
|
|
6
|
+
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/menubar/}
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <Menubar.Root>
|
|
11
|
+
* <Menubar.Content aria-label="Main navigation">
|
|
12
|
+
* <Menubar.Button>New</Menubar.Button>
|
|
13
|
+
* <Menubar.Button>Save</Menubar.Button>
|
|
14
|
+
* </Menubar.Content>
|
|
15
|
+
* </Menubar.Root>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export type MenubarRootProps = {
|
|
19
|
+
/** Default active item ID on initial render */
|
|
20
|
+
defaultActive?: string;
|
|
21
|
+
/** Callback when active item changes */
|
|
22
|
+
onActiveChange?: (active: string | undefined) => void;
|
|
23
|
+
/** Base ID for generating unique IDs */
|
|
24
|
+
id?: string;
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Container component that renders the menubar element with keyboard navigation.
|
|
29
|
+
*
|
|
30
|
+
* Implements horizontal arrow key navigation (ArrowLeft/Right), Home/End keys,
|
|
31
|
+
* and Tab key to exit the menubar.
|
|
32
|
+
*
|
|
33
|
+
* This is exported as part of the Menubar API but used for the horizontal menubar container,
|
|
34
|
+
* not the dropdown menu content (which is also named MenubarContent but context-aware).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <MenubarNav aria-label="Main menu" loop>
|
|
39
|
+
* <Menubar.Button>File</Menubar.Button>
|
|
40
|
+
* <Menubar.Button>Edit</Menubar.Button>
|
|
41
|
+
* </MenubarNav>
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export type MenubarNavProps = {
|
|
45
|
+
/** Accessible label for the menubar (required for screen readers) */
|
|
46
|
+
'aria-label': string;
|
|
47
|
+
/** Whether navigation should loop from last to first item */
|
|
48
|
+
loop?: boolean;
|
|
49
|
+
className?: string;
|
|
50
|
+
children?: ReactNode;
|
|
51
|
+
} & ComponentPropsWithoutRef<'div'>;
|
|
52
|
+
/**
|
|
53
|
+
* A button item within the menubar.
|
|
54
|
+
*
|
|
55
|
+
* Implements ARIA menuitem role and integrates with menubar keyboard navigation.
|
|
56
|
+
* Supports active state animations via Tailwind transitions.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* <Menubar.Button onSelect={() => console.log('clicked')}>
|
|
61
|
+
* Save
|
|
62
|
+
* </Menubar.Button>
|
|
63
|
+
*
|
|
64
|
+
* <Menubar.Button disabled>Export</Menubar.Button>
|
|
65
|
+
*
|
|
66
|
+
* <Menubar.Button asChild>
|
|
67
|
+
* <a href="/new">New</a>
|
|
68
|
+
* </Menubar.Button>
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export type MenubarButtonProps = {
|
|
72
|
+
/** Unique ID for this button (auto-generated if not provided) */
|
|
73
|
+
id?: string;
|
|
74
|
+
/** Whether the button is disabled */
|
|
75
|
+
disabled?: boolean;
|
|
76
|
+
/** Callback when the button is selected (clicked or activated via keyboard) */
|
|
77
|
+
onSelect?: (event: Event) => void;
|
|
78
|
+
/** Render as a child element using Radix UI Slot */
|
|
79
|
+
asChild?: boolean;
|
|
80
|
+
className?: string;
|
|
81
|
+
children: ReactNode;
|
|
82
|
+
} & Omit<ComponentPropsWithoutRef<'button'>, 'id' | 'children'>;
|
|
83
|
+
/**
|
|
84
|
+
* A visual separator component that adapts based on context.
|
|
85
|
+
*
|
|
86
|
+
* - Within Menubar.Content (menubar): Renders as vertical separator between buttons
|
|
87
|
+
* - Within Menubar.Menu Content (dropdown): Renders as horizontal separator between items
|
|
88
|
+
*
|
|
89
|
+
* Does not participate in keyboard navigation (purely decorative).
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```tsx
|
|
93
|
+
* // Vertical separator between menubar buttons
|
|
94
|
+
* <Menubar.Content aria-label="Actions">
|
|
95
|
+
* <Menubar.Button>File</Menubar.Button>
|
|
96
|
+
* <Menubar.Separator />
|
|
97
|
+
* <Menubar.Button>Edit</Menubar.Button>
|
|
98
|
+
* </Menubar.Content>
|
|
99
|
+
*
|
|
100
|
+
* // Horizontal separator between menu items
|
|
101
|
+
* <Menubar.Menu>
|
|
102
|
+
* <Menubar.Trigger>File</Menubar.Trigger>
|
|
103
|
+
* <Menubar.Portal>
|
|
104
|
+
* <Menubar.Content>
|
|
105
|
+
* <Menubar.Item>New</Menubar.Item>
|
|
106
|
+
* <Menubar.Separator />
|
|
107
|
+
* <Menubar.Item>Exit</Menubar.Item>
|
|
108
|
+
* </Menubar.Content>
|
|
109
|
+
* </Menubar.Portal>
|
|
110
|
+
* </Menubar.Menu>
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export type MenubarSeparatorProps = {
|
|
114
|
+
className?: string;
|
|
115
|
+
} & ComponentPropsWithoutRef<'div'>;
|
|
116
|
+
/**
|
|
117
|
+
* Wrapper component that integrates dropdown menus within a Menubar.
|
|
118
|
+
*
|
|
119
|
+
* Coordinates between the menubar's horizontal navigation and the menu's
|
|
120
|
+
* vertical navigation, implementing the ARIA menubar pattern with submenus.
|
|
121
|
+
*
|
|
122
|
+
* Features:
|
|
123
|
+
* - Registers menu trigger as a menubar item for keyboard navigation
|
|
124
|
+
* - ArrowDown opens the menu from the menubar
|
|
125
|
+
* - ArrowLeft/Right navigate between menus (closing current, opening adjacent)
|
|
126
|
+
* - Automatic conditional hover (hover opens menu when another menu is open)
|
|
127
|
+
* - Tab closes menu and exits menubar entirely
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```tsx
|
|
131
|
+
* <Menubar.Menu>
|
|
132
|
+
* <Menubar.Trigger>File</Menubar.Trigger>
|
|
133
|
+
* <Menubar.Portal>
|
|
134
|
+
* <Menubar.Content>
|
|
135
|
+
* <Menubar.Item>New</Menubar.Item>
|
|
136
|
+
* <Menubar.Item>Open</Menubar.Item>
|
|
137
|
+
* </Menubar.Content>
|
|
138
|
+
* </Menubar.Portal>
|
|
139
|
+
* </Menubar.Menu>
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export type MenubarMenuProps = {
|
|
143
|
+
/** Unique ID for this menu within the menubar */
|
|
144
|
+
id?: string;
|
|
145
|
+
/** Whether the menu is disabled (cannot be opened) */
|
|
146
|
+
disabled?: boolean;
|
|
147
|
+
children?: ReactNode;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Trigger button for a menubar dropdown menu.
|
|
151
|
+
*
|
|
152
|
+
* Opens the menu on click, Enter, Space, or ArrowDown.
|
|
153
|
+
* Implements conditional hover: when another menu is open, hovering this
|
|
154
|
+
* trigger automatically opens its menu and closes the other.
|
|
155
|
+
*
|
|
156
|
+
* Must be used within Menubar.Menu.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```tsx
|
|
160
|
+
* <Menubar.Menu>
|
|
161
|
+
* <Menubar.Trigger>File</Menubar.Trigger>
|
|
162
|
+
* <Menubar.Portal>
|
|
163
|
+
* <Menubar.Content>...</Menubar.Content>
|
|
164
|
+
* </Menubar.Portal>
|
|
165
|
+
* </Menubar.Menu>
|
|
166
|
+
*
|
|
167
|
+
* <Menubar.Menu>
|
|
168
|
+
* <Menubar.Trigger asChild>
|
|
169
|
+
* <Button variant="text">Edit</Button>
|
|
170
|
+
* </Menubar.Trigger>
|
|
171
|
+
* <Menubar.Portal>
|
|
172
|
+
* <Menubar.Content>...</Menubar.Content>
|
|
173
|
+
* </Menubar.Portal>
|
|
174
|
+
* </Menubar.Menu>
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export type MenubarTriggerProps = {
|
|
178
|
+
/** Render as a child element using Radix UI Slot */
|
|
179
|
+
asChild?: boolean;
|
|
180
|
+
className?: string;
|
|
181
|
+
children: ReactNode;
|
|
182
|
+
} & Omit<ComponentPropsWithoutRef<'button'>, 'children'>;
|
|
183
|
+
/**
|
|
184
|
+
* Portal component that renders menubar dropdown content to document.body.
|
|
185
|
+
*
|
|
186
|
+
* Prevents z-index stacking issues by rendering outside the DOM hierarchy.
|
|
187
|
+
* Only renders when the menu is open unless `forceMount` is true.
|
|
188
|
+
*
|
|
189
|
+
* Must be used within Menubar.Menu.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```tsx
|
|
193
|
+
* <Menubar.Menu>
|
|
194
|
+
* <Menubar.Trigger>File</Menubar.Trigger>
|
|
195
|
+
* <Menubar.Portal>
|
|
196
|
+
* <Menubar.Content>
|
|
197
|
+
* <Menubar.Item>New</Menubar.Item>
|
|
198
|
+
* </Menubar.Content>
|
|
199
|
+
* </Menubar.Portal>
|
|
200
|
+
* </Menubar.Menu>
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export type MenubarPortalProps = {
|
|
204
|
+
/** Custom container element (defaults to document.body) */
|
|
205
|
+
container?: HTMLElement | null;
|
|
206
|
+
/** Keep content mounted in DOM even when menu is closed */
|
|
207
|
+
forceMount?: boolean;
|
|
208
|
+
children?: ReactNode;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Dropdown content container for a menubar menu.
|
|
212
|
+
*
|
|
213
|
+
* Positioned below the trigger with automatic viewport collision detection.
|
|
214
|
+
* Implements vertical keyboard navigation (ArrowUp/Down) within the menu,
|
|
215
|
+
* plus special ArrowLeft/Right handling to navigate between menus.
|
|
216
|
+
*
|
|
217
|
+
* Must be used within Menubar.Portal and Menubar.Menu.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```tsx
|
|
221
|
+
* <Menubar.Menu>
|
|
222
|
+
* <Menubar.Trigger>File</Menubar.Trigger>
|
|
223
|
+
* <Menubar.Portal>
|
|
224
|
+
* <Menubar.Content align="start" loop>
|
|
225
|
+
* <Menubar.Item>New File</Menubar.Item>
|
|
226
|
+
* <Menubar.Item>Open File</Menubar.Item>
|
|
227
|
+
* <Menubar.Separator />
|
|
228
|
+
* <Menubar.Item>Exit</Menubar.Item>
|
|
229
|
+
* </Menubar.Content>
|
|
230
|
+
* </Menubar.Portal>
|
|
231
|
+
* </Menubar.Menu>
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export type MenubarContentProps = {
|
|
235
|
+
/** Horizontal alignment relative to trigger */
|
|
236
|
+
align?: 'start' | 'end';
|
|
237
|
+
/** Whether navigation should loop from last to first item */
|
|
238
|
+
loop?: boolean;
|
|
239
|
+
/** Keep content mounted in DOM even when menu is closed */
|
|
240
|
+
forceMount?: boolean;
|
|
241
|
+
/** Callback when Escape key is pressed */
|
|
242
|
+
onEscapeKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
|
|
243
|
+
/** Callback when pointer clicks outside the menu */
|
|
244
|
+
onPointerDownOutside?: (event: PointerEvent) => void;
|
|
245
|
+
/** Callback when any outside interaction occurs */
|
|
246
|
+
onInteractOutside?: (event: Event) => void;
|
|
247
|
+
className?: string;
|
|
248
|
+
children?: ReactNode;
|
|
249
|
+
} & ComponentPropsWithoutRef<'div'>;
|
|
250
|
+
/**
|
|
251
|
+
* An interactive item within a menubar dropdown menu.
|
|
252
|
+
*
|
|
253
|
+
* Supports keyboard navigation, hover effects, and the onSelect callback.
|
|
254
|
+
* Automatically closes the menu when selected.
|
|
255
|
+
*
|
|
256
|
+
* Must be used within Menubar.Content.
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```tsx
|
|
260
|
+
* <Menubar.Content>
|
|
261
|
+
* <Menubar.Item onSelect={() => console.log('New file')}>
|
|
262
|
+
* New File
|
|
263
|
+
* </Menubar.Item>
|
|
264
|
+
* <Menubar.Item disabled>
|
|
265
|
+
* Save (disabled)
|
|
266
|
+
* </Menubar.Item>
|
|
267
|
+
* <Menubar.Item asChild>
|
|
268
|
+
* <a href="/export">Export</a>
|
|
269
|
+
* </Menubar.Item>
|
|
270
|
+
* </Menubar.Content>
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export type MenubarItemProps = {
|
|
274
|
+
/** Unique ID for this item (auto-generated if not provided) */
|
|
275
|
+
id?: string;
|
|
276
|
+
/** Whether the item is disabled */
|
|
277
|
+
disabled?: boolean;
|
|
278
|
+
/** Callback when the item is selected (clicked or activated via keyboard) */
|
|
279
|
+
onSelect?: (event: Event) => void;
|
|
280
|
+
/** Render as a child element using Radix UI Slot */
|
|
281
|
+
asChild?: boolean;
|
|
282
|
+
className?: string;
|
|
283
|
+
children: ReactNode;
|
|
284
|
+
} & Omit<ComponentPropsWithoutRef<'div'>, 'id' | 'children'>;
|
|
285
|
+
/**
|
|
286
|
+
* A non-interactive label for grouping items within a menubar dropdown.
|
|
287
|
+
*
|
|
288
|
+
* Used to provide section headings or context within the menu.
|
|
289
|
+
* Does not participate in keyboard navigation.
|
|
290
|
+
*
|
|
291
|
+
* Must be used within Menubar.Content.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```tsx
|
|
295
|
+
* <Menubar.Content>
|
|
296
|
+
* <Menubar.Label>File Operations</Menubar.Label>
|
|
297
|
+
* <Menubar.Item>New</Menubar.Item>
|
|
298
|
+
* <Menubar.Item>Open</Menubar.Item>
|
|
299
|
+
* <Menubar.Separator />
|
|
300
|
+
* <Menubar.Label>Recent Files</Menubar.Label>
|
|
301
|
+
* <Menubar.Item>Document.txt</Menubar.Item>
|
|
302
|
+
* </Menubar.Content>
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
export type MenubarLabelProps = {
|
|
306
|
+
className?: string;
|
|
307
|
+
children?: ReactNode;
|
|
308
|
+
} & ComponentPropsWithoutRef<'div'>;
|
|
309
|
+
export declare const Menubar: {
|
|
310
|
+
({ defaultActive, onActiveChange, id, children }: MenubarRootProps): ReactElement;
|
|
311
|
+
displayName: string;
|
|
312
|
+
} & {
|
|
313
|
+
Root: {
|
|
314
|
+
({ defaultActive, onActiveChange, id, children }: MenubarRootProps): ReactElement;
|
|
315
|
+
displayName: string;
|
|
316
|
+
};
|
|
317
|
+
Nav: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<MenubarNavProps> & {
|
|
318
|
+
ref?: import('preact').Ref<HTMLDivElement> | undefined;
|
|
319
|
+
}>;
|
|
320
|
+
Content: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<MenubarContentProps> & {
|
|
321
|
+
ref?: import('preact').Ref<HTMLDivElement> | undefined;
|
|
322
|
+
}>;
|
|
323
|
+
Button: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<MenubarButtonProps> & {
|
|
324
|
+
ref?: import('preact').Ref<HTMLButtonElement> | undefined;
|
|
325
|
+
}>;
|
|
326
|
+
Separator: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<MenubarSeparatorProps> & {
|
|
327
|
+
ref?: import('preact').Ref<HTMLDivElement> | undefined;
|
|
328
|
+
}>;
|
|
329
|
+
Menu: {
|
|
330
|
+
({ id: providedId, disabled, children }: MenubarMenuProps): ReactElement;
|
|
331
|
+
displayName: string;
|
|
332
|
+
};
|
|
333
|
+
Trigger: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<MenubarTriggerProps> & {
|
|
334
|
+
ref?: import('preact').Ref<HTMLButtonElement> | undefined;
|
|
335
|
+
}>;
|
|
336
|
+
Portal: {
|
|
337
|
+
({ container, forceMount, children }: MenubarPortalProps): ReactElement | null;
|
|
338
|
+
displayName: string;
|
|
339
|
+
};
|
|
340
|
+
Item: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<MenubarItemProps> & {
|
|
341
|
+
ref?: import('preact').Ref<HTMLDivElement> | undefined;
|
|
342
|
+
}>;
|
|
343
|
+
Label: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<MenubarLabelProps> & {
|
|
344
|
+
ref?: import('preact').Ref<HTMLDivElement> | undefined;
|
|
345
|
+
}>;
|
|
346
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useScrollLock } from './use-scroll-lock';
|
|
2
2
|
export { useControlledState } from './use-controlled-state';
|
|
3
|
+
export { useControlledStateWithNull } from './use-controlled-state-with-null';
|
|
3
4
|
export { useItemRegistry, type ItemMetadata, type UseItemRegistryReturn } from './use-item-registry';
|
|
4
5
|
export { useKeyboardNavigation, type KeyboardNavigationConfig, type UseKeyboardNavigationReturn, } from './use-keyboard-navigation';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A variant of `useControlledState` that handles `null` as "no value" in controlled mode.
|
|
3
|
+
*
|
|
4
|
+
* This hook provides a clean external API where `null` represents "no value" while
|
|
5
|
+
* internally using `undefined` for consistency with React/Preact patterns.
|
|
6
|
+
*
|
|
7
|
+
* **External API (Component Props):**
|
|
8
|
+
* - `undefined` = uncontrolled mode (prop not provided)
|
|
9
|
+
* - `null` = controlled mode with no value
|
|
10
|
+
* - `value` = controlled mode with value
|
|
11
|
+
*
|
|
12
|
+
* **Internal State:**
|
|
13
|
+
* - `undefined` = no value (converted from external `null`)
|
|
14
|
+
* - `value` = has value
|
|
15
|
+
*
|
|
16
|
+
* @template T - The type of the state value (typically `string`)
|
|
17
|
+
*
|
|
18
|
+
* @param controlledValue - The controlled value from props (e.g., `active`)
|
|
19
|
+
* @param defaultValue - The default value for uncontrolled mode
|
|
20
|
+
* @param onChange - Callback invoked when the value changes (receives `null` for no value)
|
|
21
|
+
*
|
|
22
|
+
* @returns A tuple containing the current value and a setter function
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // Component with controlled "no active" support
|
|
27
|
+
* function Listbox({ active, defaultActive, setActive }: {
|
|
28
|
+
* active?: string | null;
|
|
29
|
+
* defaultActive?: string;
|
|
30
|
+
* setActive?: (active: string | null) => void;
|
|
31
|
+
* }) {
|
|
32
|
+
* const [activeInternal, setActiveInternal] = useControlledStateWithNull(
|
|
33
|
+
* active,
|
|
34
|
+
* defaultActive,
|
|
35
|
+
* setActive
|
|
36
|
+
* );
|
|
37
|
+
*
|
|
38
|
+
* // activeInternal is string | undefined internally
|
|
39
|
+
* // But external API uses string | null
|
|
40
|
+
*
|
|
41
|
+
* // active={null} → activeInternal = undefined
|
|
42
|
+
* // active="item" → activeInternal = "item"
|
|
43
|
+
* // active not provided → activeInternal from defaultActive
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function useControlledStateWithNull<T extends string>(controlledValue: T | null | undefined, defaultValue: T | undefined, onChange?: (value: T | null) => void): [T | undefined, (value: T | null | undefined) => void];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Manages state that can be either controlled or uncontrolled.
|
|
3
|
-
* Follows the pattern for controlled/uncontrolled state management.
|
|
3
|
+
* Follows the React pattern for controlled/uncontrolled state management.
|
|
4
4
|
*
|
|
5
|
-
* @template T - The type of the state value
|
|
5
|
+
* @template T - The type of the state value (can include `null` for "no value" state)
|
|
6
6
|
*
|
|
7
7
|
* @param controlledValue - The controlled value from props (e.g., `value`, `open`, `checked`)
|
|
8
8
|
* @param defaultValue - The default value for uncontrolled mode (e.g., `defaultValue`, `defaultOpen`)
|
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @returns A tuple containing the current value and a setter function
|
|
12
12
|
*
|
|
13
|
+
* **Controlled vs Uncontrolled Detection:**
|
|
14
|
+
* - `undefined` = uncontrolled mode (prop not provided)
|
|
15
|
+
* - Any other value (including `null`) = controlled mode
|
|
16
|
+
*
|
|
17
|
+
* **Supporting "No Value" in Controlled Mode:**
|
|
18
|
+
* Use `null` to represent "no value" in controlled mode:
|
|
19
|
+
* - `active={null}` → controlled with no active item
|
|
20
|
+
* - `active="item-1"` → controlled with active item
|
|
21
|
+
* - `active` not provided → uncontrolled mode
|
|
22
|
+
*
|
|
13
23
|
* @example
|
|
14
24
|
* ```tsx
|
|
15
25
|
* // Uncontrolled usage
|
|
@@ -23,6 +33,14 @@
|
|
|
23
33
|
* const [value, setValue] = useControlledState(open, false, onOpenChange);
|
|
24
34
|
* return <div>{value ? 'Open' : 'Closed'}</div>;
|
|
25
35
|
* }
|
|
36
|
+
*
|
|
37
|
+
* // Controlled with null support (for "no value" state)
|
|
38
|
+
* function Component({ active, onActiveChange }: { active?: string | null }) {
|
|
39
|
+
* const [value, setValue] = useControlledState(active, undefined, onActiveChange);
|
|
40
|
+
* // active={null} → controlled, value = null
|
|
41
|
+
* // active="item" → controlled, value = "item"
|
|
42
|
+
* // active not provided → uncontrolled, value from defaultActive
|
|
43
|
+
* }
|
|
26
44
|
* ```
|
|
27
45
|
*/
|
|
28
46
|
export declare function useControlledState<T>(controlledValue: T | undefined, defaultValue: T, onChange?: (value: T) => void): [T, (value: T) => void];
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { ReactElement, ReactNode } from 'react';
|
|
2
2
|
export type ListboxContextValue = {
|
|
3
3
|
baseId: string;
|
|
4
|
+
/**
|
|
5
|
+
* Active item ID (`undefined` when no item is active).
|
|
6
|
+
* Note: Never `null` - the hook converts `null` to `undefined` internally.
|
|
7
|
+
*/
|
|
4
8
|
active?: string;
|
|
5
9
|
selection: ReadonlySet<string>;
|
|
6
10
|
selectionMode: 'single' | 'multiple';
|
|
7
11
|
disabled?: boolean;
|
|
8
12
|
focusable?: boolean;
|
|
9
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Set active item.
|
|
15
|
+
* Accepts `null` for compatibility with controlled prop API, but converts it to `undefined` internally.
|
|
16
|
+
*/
|
|
17
|
+
setActive: (id?: string | null) => void;
|
|
10
18
|
toggleValue: (value: string) => void;
|
|
11
19
|
keyHandler?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
12
20
|
registerItem: (id: string, disabled?: boolean) => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { MenubarContextValue } from './menubar-provider';
|
|
3
|
+
/**
|
|
4
|
+
* Context value for individual menu within a menubar.
|
|
5
|
+
*
|
|
6
|
+
* Coordinates between the menu's state and the parent menubar's state.
|
|
7
|
+
*/
|
|
8
|
+
export type MenubarMenuContextValue = {
|
|
9
|
+
/** Unique ID for this menu */
|
|
10
|
+
menuId: string;
|
|
11
|
+
/** ID for the menu content element */
|
|
12
|
+
contentId: string;
|
|
13
|
+
/** Whether this menu is currently open */
|
|
14
|
+
open: boolean;
|
|
15
|
+
/** Open/close this menu */
|
|
16
|
+
setOpen: (open: boolean) => void;
|
|
17
|
+
/** Reference to the parent menubar context */
|
|
18
|
+
menubarContext: MenubarContextValue;
|
|
19
|
+
/** Reference to the trigger element */
|
|
20
|
+
triggerRef: RefObject<HTMLButtonElement> | null;
|
|
21
|
+
};
|
|
22
|
+
export declare const MenubarMenuContext: import('preact').Context<MenubarMenuContextValue | undefined>;
|
|
23
|
+
export declare const MenubarMenuProvider: import('preact').Provider<MenubarMenuContextValue | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* Access the menubar menu context.
|
|
26
|
+
*
|
|
27
|
+
* @throws Error if used outside MenubarMenuProvider
|
|
28
|
+
*/
|
|
29
|
+
export declare function useMenubarMenu(): MenubarMenuContextValue;
|
|
30
|
+
/**
|
|
31
|
+
* Optionally access the menubar menu context.
|
|
32
|
+
*
|
|
33
|
+
* Returns undefined if used outside MenubarMenuProvider.
|
|
34
|
+
* Useful for components that can work both inside and outside a menubar.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useMenubarMenuOptional(): MenubarMenuContextValue | undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ReactElement, ReactNode, RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Context value for Menubar component.
|
|
4
|
+
* Manages horizontal navigation between menubar items (buttons and menu triggers).
|
|
5
|
+
*
|
|
6
|
+
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/menubar/} - ARIA Menubar Pattern
|
|
7
|
+
*/
|
|
8
|
+
export type MenubarContextValue = {
|
|
9
|
+
/**
|
|
10
|
+
* Currently active menubar item ID (keyboard navigation focus).
|
|
11
|
+
* This tracks which button or menu trigger is currently highlighted.
|
|
12
|
+
* `undefined` when no item is active.
|
|
13
|
+
*/
|
|
14
|
+
active: string | undefined;
|
|
15
|
+
setActive: (id: string | undefined) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Registry for menubar items to enable keyboard navigation.
|
|
18
|
+
* Items are tracked in insertion order for left/right arrow navigation.
|
|
19
|
+
*/
|
|
20
|
+
registerItem: (id: string, disabled?: boolean) => void;
|
|
21
|
+
unregisterItem: (id: string) => void;
|
|
22
|
+
getItems: () => string[];
|
|
23
|
+
isItemDisabled: (id: string) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* ID of the currently open menu (if any).
|
|
26
|
+
* Used in Phase 2 to track which menu is expanded in the menubar.
|
|
27
|
+
*
|
|
28
|
+
* @phase2
|
|
29
|
+
*/
|
|
30
|
+
openMenuId: string | undefined;
|
|
31
|
+
setOpenMenuId: (id: string | undefined) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Base ID for the menubar container.
|
|
34
|
+
*/
|
|
35
|
+
menubarId: string;
|
|
36
|
+
/**
|
|
37
|
+
* Ref to the menubar container element for focus management.
|
|
38
|
+
*/
|
|
39
|
+
menubarRef: RefObject<HTMLDivElement> | null;
|
|
40
|
+
};
|
|
41
|
+
export type MenubarProviderProps = {
|
|
42
|
+
value: MenubarContextValue;
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Provider component that supplies menubar context to child components.
|
|
47
|
+
* Should wrap all Menubar.* components.
|
|
48
|
+
*/
|
|
49
|
+
export declare const MenubarProvider: {
|
|
50
|
+
({ value, children }: MenubarProviderProps): ReactElement;
|
|
51
|
+
displayName: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Hook to access menubar context.
|
|
55
|
+
* Must be used within a MenubarProvider.
|
|
56
|
+
*
|
|
57
|
+
* @throws {Error} If used outside of MenubarProvider
|
|
58
|
+
* @returns {MenubarContextValue} The menubar context
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* const { active, setActive, registerItem } = useMenubar();
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare const useMenubar: () => MenubarContextValue;
|
|
66
|
+
/**
|
|
67
|
+
* Hook to optionally access menubar context.
|
|
68
|
+
* Returns undefined if not within a MenubarProvider.
|
|
69
|
+
*
|
|
70
|
+
* Useful for components that can work both inside and outside a menubar (e.g., Menu.Trigger).
|
|
71
|
+
*
|
|
72
|
+
* @phase2 Used by Menu components to detect if they're within a Menubar
|
|
73
|
+
* @returns {MenubarContextValue | undefined} The menubar context or undefined
|
|
74
|
+
*/
|
|
75
|
+
export declare const useMenubarOptional: () => MenubarContextValue | undefined;
|