@enonic/ui 0.15.1 → 0.17.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 +1 -26
- package/dist/enonic-ui.es.js +51 -5806
- package/dist/styles/preset.css +1 -1
- package/dist/styles/style.css +1 -1
- package/dist/styles/tokens.css +1 -1
- package/dist/types/components/combobox/combobox.d.ts +5 -2
- package/dist/types/components/index.d.ts +1 -0
- package/dist/types/components/listbox/listbox.d.ts +13 -5
- 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 +7 -1
- package/dist/types/hooks/use-active-item-focus.d.ts +83 -0
- package/dist/types/hooks/use-click-outside.d.ts +99 -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/hooks/use-floating-position.d.ts +58 -0
- package/dist/types/hooks/use-roving-tabindex.d.ts +69 -0
- package/dist/types/hooks/use-scroll-active-into-view.d.ts +83 -0
- package/dist/types/providers/index.d.ts +2 -0
- package/dist/types/providers/listbox-provider.d.ts +15 -2
- package/dist/types/providers/menubar-menu-provider.d.ts +36 -0
- package/dist/types/providers/menubar-provider.d.ts +72 -0
- package/package.json +27 -27
|
@@ -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,10 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useActiveItemFocus, type UseActiveItemFocusConfig } from './use-active-item-focus';
|
|
2
|
+
export { useClickOutside, type UseClickOutsideConfig } from './use-click-outside';
|
|
2
3
|
export { useControlledState } from './use-controlled-state';
|
|
4
|
+
export { useControlledStateWithNull } from './use-controlled-state-with-null';
|
|
5
|
+
export { useFloatingPosition, type FloatingPosition, type UseFloatingPositionConfig } from './use-floating-position';
|
|
3
6
|
export { useItemRegistry, type ItemMetadata, type UseItemRegistryReturn } from './use-item-registry';
|
|
4
7
|
export { useKeyboardNavigation, type KeyboardNavigationConfig, type UseKeyboardNavigationReturn, } from './use-keyboard-navigation';
|
|
8
|
+
export { useRovingTabIndex, type UseRovingTabIndexConfig, type UseRovingTabIndexReturn } from './use-roving-tabindex';
|
|
9
|
+
export { useScrollActiveIntoView, type UseScrollActiveIntoViewConfig } from './use-scroll-active-into-view';
|
|
10
|
+
export { useScrollLock } from './use-scroll-lock';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export type UseActiveItemFocusConfig = {
|
|
3
|
+
/** Reference to the item element to focus */
|
|
4
|
+
ref: RefObject<HTMLElement> | null;
|
|
5
|
+
/** Whether this item is currently active */
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
/** Whether this item is disabled */
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Focus mode - determines when to auto-focus:
|
|
11
|
+
* - 'roving-tabindex': Auto-focus when active (default)
|
|
12
|
+
* - 'activedescendant': Don't auto-focus (container manages focus)
|
|
13
|
+
*/
|
|
14
|
+
focusMode?: 'roving-tabindex' | 'activedescendant';
|
|
15
|
+
/**
|
|
16
|
+
* When true, only focus if focus is already within the container.
|
|
17
|
+
* This prevents mouse hover from causing focus rings when navigating with keyboard.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* In Listbox, we only want to auto-focus during keyboard navigation,
|
|
21
|
+
* not when the user hovers with their mouse. We check if focus is
|
|
22
|
+
* already within the listbox to determine if keyboard nav is active.
|
|
23
|
+
*/
|
|
24
|
+
checkFocusWithin?: {
|
|
25
|
+
/** Enable the focus-within check */
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
/** ARIA role of the container to check (e.g., 'listbox', 'menu') */
|
|
28
|
+
containerRole: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Automatically focuses an element when it becomes active, typically used
|
|
33
|
+
* in keyboard navigation patterns like roving tabindex.
|
|
34
|
+
*
|
|
35
|
+
* This hook handles the common pattern where an item should receive DOM focus
|
|
36
|
+
* when it becomes the "active" item in a list, menu, or navigation structure.
|
|
37
|
+
* It includes safeguards to:
|
|
38
|
+
* - Only focus when not disabled
|
|
39
|
+
* - Only focus when not already focused (prevents infinite loops)
|
|
40
|
+
* - Optionally check if focus is within the container (prevents hover-induced focus)
|
|
41
|
+
* - Respect focus mode (roving-tabindex vs activedescendant)
|
|
42
|
+
*
|
|
43
|
+
* @param config - Configuration object for auto-focus behavior
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* // Basic usage (Menu item)
|
|
48
|
+
* function MenuItem() {
|
|
49
|
+
* const itemRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
* const { active } = useMenu();
|
|
51
|
+
* const isActive = active === id;
|
|
52
|
+
*
|
|
53
|
+
* useActiveItemFocus({
|
|
54
|
+
* ref: itemRef,
|
|
55
|
+
* isActive,
|
|
56
|
+
* disabled: false,
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* return <div ref={itemRef}>Item</div>;
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* // With focus-within check (Listbox item)
|
|
63
|
+
* function ListboxItem() {
|
|
64
|
+
* const itemRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
* const { active, focusMode } = useListbox();
|
|
66
|
+
* const isActive = active === id;
|
|
67
|
+
*
|
|
68
|
+
* useActiveItemFocus({
|
|
69
|
+
* ref: itemRef,
|
|
70
|
+
* isActive,
|
|
71
|
+
* disabled: false,
|
|
72
|
+
* focusMode,
|
|
73
|
+
* checkFocusWithin: {
|
|
74
|
+
* enabled: true,
|
|
75
|
+
* containerRole: 'listbox',
|
|
76
|
+
* },
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* return <div ref={itemRef}>Item</div>;
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function useActiveItemFocus({ ref, isActive, disabled, focusMode, checkFocusWithin, }: UseActiveItemFocusConfig): void;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export type UseClickOutsideConfig = {
|
|
3
|
+
/** Whether the click outside listener is active */
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
/** Reference to the content element (clicks inside are ignored) */
|
|
6
|
+
contentRef: RefObject<HTMLElement>;
|
|
7
|
+
/**
|
|
8
|
+
* Optional references to elements that should be excluded from "outside" detection.
|
|
9
|
+
* Typically used for trigger buttons that toggle the content visibility.
|
|
10
|
+
*/
|
|
11
|
+
excludeRefs?: (RefObject<HTMLElement> | undefined)[];
|
|
12
|
+
/** Callback when pointer down occurs outside */
|
|
13
|
+
onPointerDownOutside?: (event: PointerEvent) => void;
|
|
14
|
+
/** Callback when any interaction occurs outside */
|
|
15
|
+
onInteractOutside?: (event: Event) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Callback to close/hide the content.
|
|
18
|
+
* Only called if event.defaultPrevented is false.
|
|
19
|
+
*/
|
|
20
|
+
onClose?: () => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Detects clicks/pointer events outside a content element and triggers callbacks.
|
|
24
|
+
*
|
|
25
|
+
* This hook is commonly used for dismissible UI elements like dropdowns, menus,
|
|
26
|
+
* dialogs, and popovers that should close when the user clicks outside.
|
|
27
|
+
*
|
|
28
|
+
* Features:
|
|
29
|
+
* - Ignores clicks inside the content element
|
|
30
|
+
* - Supports excluding additional elements (e.g., trigger buttons)
|
|
31
|
+
* - Respects event.defaultPrevented to allow custom handling
|
|
32
|
+
* - Only active when enabled
|
|
33
|
+
* - Properly cleans up event listeners
|
|
34
|
+
*
|
|
35
|
+
* @param config - Configuration object for click outside detection
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* // Basic usage (Dialog)
|
|
40
|
+
* function Dialog() {
|
|
41
|
+
* const [open, setOpen] = useState(false);
|
|
42
|
+
* const contentRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
*
|
|
44
|
+
* useClickOutside({
|
|
45
|
+
* enabled: open,
|
|
46
|
+
* contentRef,
|
|
47
|
+
* onClose: () => setOpen(false),
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* if (!open) return null;
|
|
51
|
+
* return <div ref={contentRef}>Dialog content</div>;
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* // With trigger exclusion (Menu)
|
|
55
|
+
* function Menu() {
|
|
56
|
+
* const [open, setOpen] = useState(false);
|
|
57
|
+
* const triggerRef = useRef<HTMLButtonElement>(null);
|
|
58
|
+
* const contentRef = useRef<HTMLDivElement>(null);
|
|
59
|
+
*
|
|
60
|
+
* useClickOutside({
|
|
61
|
+
* enabled: open,
|
|
62
|
+
* contentRef,
|
|
63
|
+
* excludeRefs: [triggerRef],
|
|
64
|
+
* onPointerDownOutside: (e) => console.log('Outside click', e),
|
|
65
|
+
* onInteractOutside: (e) => console.log('Outside interaction', e),
|
|
66
|
+
* onClose: () => setOpen(false),
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* return (
|
|
70
|
+
* <>
|
|
71
|
+
* <button ref={triggerRef}>Open Menu</button>
|
|
72
|
+
* {open && <div ref={contentRef}>Menu content</div>}
|
|
73
|
+
* </>
|
|
74
|
+
* );
|
|
75
|
+
* }
|
|
76
|
+
*
|
|
77
|
+
* // With custom close prevention
|
|
78
|
+
* function CustomDialog() {
|
|
79
|
+
* const [open, setOpen] = useState(false);
|
|
80
|
+
* const contentRef = useRef<HTMLDivElement>(null);
|
|
81
|
+
*
|
|
82
|
+
* useClickOutside({
|
|
83
|
+
* enabled: open,
|
|
84
|
+
* contentRef,
|
|
85
|
+
* onPointerDownOutside: (e) => {
|
|
86
|
+
* const shouldClose = confirm('Close dialog?');
|
|
87
|
+
* if (!shouldClose) {
|
|
88
|
+
* e.preventDefault(); // Prevents onClose from being called
|
|
89
|
+
* }
|
|
90
|
+
* },
|
|
91
|
+
* onClose: () => setOpen(false),
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* if (!open) return null;
|
|
95
|
+
* return <div ref={contentRef}>Dialog content</div>;
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare function useClickOutside({ enabled, contentRef, excludeRefs, onPointerDownOutside, onInteractOutside, onClose, }: UseClickOutsideConfig): void;
|
|
@@ -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];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export type FloatingPosition = {
|
|
3
|
+
top: number;
|
|
4
|
+
left?: number;
|
|
5
|
+
right?: number;
|
|
6
|
+
};
|
|
7
|
+
export type UseFloatingPositionConfig = {
|
|
8
|
+
/** Whether the floating element is open/visible */
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
/** Reference to the trigger element */
|
|
11
|
+
triggerRef: RefObject<HTMLElement> | null;
|
|
12
|
+
/** Reference to the floating content element */
|
|
13
|
+
contentRef: RefObject<HTMLElement> | null;
|
|
14
|
+
/** Horizontal alignment relative to trigger */
|
|
15
|
+
align?: 'start' | 'end';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Calculates optimal position for floating elements (menus, dropdowns) relative to a trigger,
|
|
19
|
+
* with automatic viewport collision detection and flip behavior.
|
|
20
|
+
*
|
|
21
|
+
* This hook handles:
|
|
22
|
+
* - Positioning below trigger with configurable alignment
|
|
23
|
+
* - Vertical flipping when overflowing bottom edge
|
|
24
|
+
* - Horizontal adjustment to stay within viewport bounds
|
|
25
|
+
* - Automatic repositioning on window resize and scroll
|
|
26
|
+
*
|
|
27
|
+
* @param config - Configuration object for positioning behavior
|
|
28
|
+
* @returns Position object with top and left/right coordinates, or null if not yet calculated
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* function Menu() {
|
|
33
|
+
* const triggerRef = useRef<HTMLButtonElement>(null);
|
|
34
|
+
* const contentRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
* const position = useFloatingPosition({
|
|
36
|
+
* enabled: open,
|
|
37
|
+
* triggerRef,
|
|
38
|
+
* contentRef,
|
|
39
|
+
* align: 'start'
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* return (
|
|
43
|
+
* <div
|
|
44
|
+
* ref={contentRef}
|
|
45
|
+
* style={{
|
|
46
|
+
* position: 'fixed',
|
|
47
|
+
* top: position ? `${position.top}px` : '0',
|
|
48
|
+
* left: position?.left !== undefined ? `${position.left}px` : undefined,
|
|
49
|
+
* right: position?.right !== undefined ? `${position.right}px` : undefined,
|
|
50
|
+
* }}
|
|
51
|
+
* >
|
|
52
|
+
* Menu content
|
|
53
|
+
* </div>
|
|
54
|
+
* );
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function useFloatingPosition({ enabled, triggerRef, contentRef, align, }: UseFloatingPositionConfig): FloatingPosition | null;
|