@enonic/ui 0.13.0 → 0.13.1
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 -6
- package/dist/enonic-ui.es.js +369 -340
- package/dist/types/components/combobox/combobox.d.ts +8 -2
- package/dist/types/components/listbox/listbox.d.ts +9 -3
- package/dist/types/hooks/index.d.ts +3 -0
- package/dist/types/hooks/use-controlled-state.d.ts +28 -0
- package/dist/types/hooks/use-item-registry.d.ts +57 -0
- package/dist/types/hooks/use-keyboard-navigation.d.ts +82 -0
- package/dist/types/providers/combobox-provider.d.ts +1 -1
- package/dist/types/providers/listbox-provider.d.ts +4 -0
- package/dist/types/utils/aria.d.ts +69 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/ref.d.ts +1 -1
- package/package.json +1 -1
|
@@ -2,13 +2,19 @@ import { ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react';
|
|
|
2
2
|
export type ComboboxRootProps = {
|
|
3
3
|
children?: ReactNode;
|
|
4
4
|
open?: boolean;
|
|
5
|
+
defaultOpen?: boolean;
|
|
5
6
|
onOpenChange?: (open: boolean) => void;
|
|
6
7
|
closeOnBlur?: boolean;
|
|
7
8
|
value?: string;
|
|
9
|
+
defaultValue?: string;
|
|
8
10
|
onChange?: (value: string | undefined) => void;
|
|
9
11
|
selectionMode?: 'single' | 'multiple';
|
|
10
12
|
selection?: readonly string[];
|
|
13
|
+
defaultSelection?: readonly string[];
|
|
11
14
|
onSelectionChange?: (selection: readonly string[]) => void;
|
|
15
|
+
active?: string;
|
|
16
|
+
defaultActive?: string;
|
|
17
|
+
setActive?: (active: string | undefined) => void;
|
|
12
18
|
disabled?: boolean;
|
|
13
19
|
error?: boolean;
|
|
14
20
|
};
|
|
@@ -32,11 +38,11 @@ export type ComboboxPopupProps = {
|
|
|
32
38
|
className?: string;
|
|
33
39
|
};
|
|
34
40
|
export declare const Combobox: {
|
|
35
|
-
({ children, open: controlledOpen, onOpenChange, closeOnBlur, value, onChange, disabled, error, selectionMode, selection: controlledSelection, onSelectionChange, }: ComboboxRootProps): ReactElement;
|
|
41
|
+
({ children, open: controlledOpen, defaultOpen, onOpenChange, closeOnBlur, value, defaultValue, onChange, disabled, error, selectionMode, selection: controlledSelection, defaultSelection, onSelectionChange, active: controlledActive, defaultActive, setActive, }: ComboboxRootProps): ReactElement;
|
|
36
42
|
displayName: string;
|
|
37
43
|
} & {
|
|
38
44
|
Root: {
|
|
39
|
-
({ children, open: controlledOpen, onOpenChange, closeOnBlur, value, onChange, disabled, error, selectionMode, selection: controlledSelection, onSelectionChange, }: ComboboxRootProps): ReactElement;
|
|
45
|
+
({ children, open: controlledOpen, defaultOpen, onOpenChange, closeOnBlur, value, defaultValue, onChange, disabled, error, selectionMode, selection: controlledSelection, defaultSelection, onSelectionChange, active: controlledActive, defaultActive, setActive, }: ComboboxRootProps): ReactElement;
|
|
40
46
|
displayName: string;
|
|
41
47
|
};
|
|
42
48
|
Content: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<ComboboxContentProps> & {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UseItemRegistryReturn } from '../../hooks';
|
|
1
2
|
import { ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react';
|
|
2
3
|
export type ListboxRootProps = {
|
|
3
4
|
baseId?: string;
|
|
@@ -12,6 +13,10 @@ export type ListboxRootProps = {
|
|
|
12
13
|
focusable?: boolean;
|
|
13
14
|
children?: ReactNode;
|
|
14
15
|
keyHandler?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
16
|
+
registerItem?: UseItemRegistryReturn['registerItem'];
|
|
17
|
+
unregisterItem?: UseItemRegistryReturn['unregisterItem'];
|
|
18
|
+
getItems?: UseItemRegistryReturn['getItems'];
|
|
19
|
+
isItemDisabled?: UseItemRegistryReturn['isItemDisabled'];
|
|
15
20
|
};
|
|
16
21
|
export type ListboxContentProps = {
|
|
17
22
|
className?: string;
|
|
@@ -20,22 +25,23 @@ export type ListboxContentProps = {
|
|
|
20
25
|
} & ComponentPropsWithoutRef<'div'>;
|
|
21
26
|
export type ListboxItemProps = {
|
|
22
27
|
value: string;
|
|
28
|
+
disabled?: boolean;
|
|
23
29
|
children: ReactNode;
|
|
24
30
|
className?: string;
|
|
25
31
|
} & ComponentPropsWithoutRef<'div'>;
|
|
26
32
|
export declare const Listbox: {
|
|
27
|
-
({ baseId, selection: controlledSelection, defaultSelection, onSelectionChange, active: controlledActive, defaultActive, setActive, selectionMode, focusable, disabled, children, keyHandler, }: ListboxRootProps): ReactElement;
|
|
33
|
+
({ baseId, selection: controlledSelection, defaultSelection, onSelectionChange, active: controlledActive, defaultActive, setActive, selectionMode, focusable, disabled, children, keyHandler, registerItem: externalRegisterItem, unregisterItem: externalUnregisterItem, getItems: externalGetItems, isItemDisabled: externalIsItemDisabled, }: ListboxRootProps): ReactElement;
|
|
28
34
|
displayName: string;
|
|
29
35
|
} & {
|
|
30
36
|
Root: {
|
|
31
|
-
({ baseId, selection: controlledSelection, defaultSelection, onSelectionChange, active: controlledActive, defaultActive, setActive, selectionMode, focusable, disabled, children, keyHandler, }: ListboxRootProps): ReactElement;
|
|
37
|
+
({ baseId, selection: controlledSelection, defaultSelection, onSelectionChange, active: controlledActive, defaultActive, setActive, selectionMode, focusable, disabled, children, keyHandler, registerItem: externalRegisterItem, unregisterItem: externalUnregisterItem, getItems: externalGetItems, isItemDisabled: externalIsItemDisabled, }: ListboxRootProps): ReactElement;
|
|
32
38
|
displayName: string;
|
|
33
39
|
};
|
|
34
40
|
Content: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<ListboxContentProps> & {
|
|
35
41
|
ref?: import('preact').Ref<HTMLDivElement> | undefined;
|
|
36
42
|
}>;
|
|
37
43
|
Item: {
|
|
38
|
-
({ value, children, className, ...props }: ListboxItemProps): ReactElement;
|
|
44
|
+
({ value, disabled, children, className, ...props }: ListboxItemProps): ReactElement;
|
|
39
45
|
displayName: string;
|
|
40
46
|
};
|
|
41
47
|
};
|
|
@@ -1 +1,4 @@
|
|
|
1
1
|
export { useScrollLock } from './use-scroll-lock';
|
|
2
|
+
export { useControlledState } from './use-controlled-state';
|
|
3
|
+
export { useItemRegistry, type ItemMetadata, type UseItemRegistryReturn } from './use-item-registry';
|
|
4
|
+
export { useKeyboardNavigation, type KeyboardNavigationConfig, type UseKeyboardNavigationReturn, } from './use-keyboard-navigation';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages state that can be either controlled or uncontrolled.
|
|
3
|
+
* Follows the pattern for controlled/uncontrolled state management.
|
|
4
|
+
*
|
|
5
|
+
* @template T - The type of the state value
|
|
6
|
+
*
|
|
7
|
+
* @param controlledValue - The controlled value from props (e.g., `value`, `open`, `checked`)
|
|
8
|
+
* @param defaultValue - The default value for uncontrolled mode (e.g., `defaultValue`, `defaultOpen`)
|
|
9
|
+
* @param onChange - Callback invoked when the value changes
|
|
10
|
+
*
|
|
11
|
+
* @returns A tuple containing the current value and a setter function
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* // Uncontrolled usage
|
|
16
|
+
* function Component({ defaultOpen = false, onOpenChange }) {
|
|
17
|
+
* const [open, setOpen] = useControlledState(undefined, defaultOpen, onOpenChange);
|
|
18
|
+
* return <div>{open ? 'Open' : 'Closed'}</div>;
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* // Controlled usage
|
|
22
|
+
* function Component({ open, onOpenChange }) {
|
|
23
|
+
* const [value, setValue] = useControlledState(open, false, onOpenChange);
|
|
24
|
+
* return <div>{value ? 'Open' : 'Closed'}</div>;
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function useControlledState<T>(controlledValue: T | undefined, defaultValue: T, onChange?: (value: T) => void): [T, (value: T) => void];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type ItemMetadata = {
|
|
2
|
+
disabled: boolean;
|
|
3
|
+
};
|
|
4
|
+
export type UseItemRegistryReturn = {
|
|
5
|
+
/**
|
|
6
|
+
* Registers an item with the registry.
|
|
7
|
+
* @param id - Unique identifier for the item
|
|
8
|
+
* @param disabled - Whether the item is disabled
|
|
9
|
+
*/
|
|
10
|
+
registerItem: (id: string, disabled?: boolean) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Unregisters an item from the registry.
|
|
13
|
+
* @param id - Unique identifier for the item to remove
|
|
14
|
+
*/
|
|
15
|
+
unregisterItem: (id: string) => void;
|
|
16
|
+
/**
|
|
17
|
+
* Gets all registered item IDs in insertion order.
|
|
18
|
+
* @returns Array of item IDs
|
|
19
|
+
*/
|
|
20
|
+
getItems: () => string[];
|
|
21
|
+
/**
|
|
22
|
+
* Checks if an item is disabled.
|
|
23
|
+
* @param id - Item ID to check
|
|
24
|
+
* @returns True if the item is disabled
|
|
25
|
+
*/
|
|
26
|
+
isItemDisabled: (id: string) => boolean;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Hook for managing a registry of items (menu items, listbox options, etc.).
|
|
30
|
+
* Provides a more reliable alternative to DOM queries for item discovery.
|
|
31
|
+
*
|
|
32
|
+
* This pattern is superior to `querySelectorAll` because:
|
|
33
|
+
* - No stale queries from DOM changes
|
|
34
|
+
* - Type-safe item metadata
|
|
35
|
+
* - Better performance (no DOM traversal)
|
|
36
|
+
* - Consistent insertion order
|
|
37
|
+
*
|
|
38
|
+
* @returns Object with registry methods
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* function MyList() {
|
|
43
|
+
* const { registerItem, unregisterItem, getItems, isItemDisabled } = useItemRegistry();
|
|
44
|
+
*
|
|
45
|
+
* // In child components:
|
|
46
|
+
* useEffect(() => {
|
|
47
|
+
* registerItem(id, disabled);
|
|
48
|
+
* return () => unregisterItem(id);
|
|
49
|
+
* }, [id, disabled]);
|
|
50
|
+
*
|
|
51
|
+
* // Navigate through items
|
|
52
|
+
* const items = getItems();
|
|
53
|
+
* const enabledItems = items.filter(id => !isItemDisabled(id));
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function useItemRegistry(): UseItemRegistryReturn;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type KeyboardNavigationConfig = {
|
|
2
|
+
/**
|
|
3
|
+
* Get all item IDs in order
|
|
4
|
+
*/
|
|
5
|
+
getItems: () => string[];
|
|
6
|
+
/**
|
|
7
|
+
* Check if an item is disabled
|
|
8
|
+
*/
|
|
9
|
+
isItemDisabled: (id: string) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Currently active item ID
|
|
12
|
+
*/
|
|
13
|
+
active: string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Set the active item ID
|
|
16
|
+
*/
|
|
17
|
+
setActive: (id: string | undefined) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Whether to loop navigation (wrap around at start/end)
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
loop?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Orientation of navigation
|
|
25
|
+
* @default 'vertical'
|
|
26
|
+
*/
|
|
27
|
+
orientation?: 'vertical' | 'horizontal';
|
|
28
|
+
/**
|
|
29
|
+
* Called when user requests selection (Enter/Space)
|
|
30
|
+
* Receives the active item ID
|
|
31
|
+
*/
|
|
32
|
+
onSelect?: (id: string) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Called when user presses Escape
|
|
35
|
+
*/
|
|
36
|
+
onEscape?: () => void;
|
|
37
|
+
};
|
|
38
|
+
export type UseKeyboardNavigationReturn = {
|
|
39
|
+
/**
|
|
40
|
+
* Move active item by delta (1 for next, -1 for previous)
|
|
41
|
+
*/
|
|
42
|
+
moveActive: (delta: number) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Keyboard event handler to attach to the container
|
|
45
|
+
*/
|
|
46
|
+
handleKeyDown: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Hook for keyboard navigation through a list of items.
|
|
50
|
+
* Handles arrow keys, Home/End, Enter/Space, and Escape.
|
|
51
|
+
*
|
|
52
|
+
* Supports:
|
|
53
|
+
* - ArrowUp/ArrowDown (or ArrowLeft/ArrowRight for horizontal)
|
|
54
|
+
* - Home/End keys
|
|
55
|
+
* - Loop navigation (optional)
|
|
56
|
+
* - Disabled item skipping
|
|
57
|
+
* - Enter/Space for selection
|
|
58
|
+
* - Escape key handling
|
|
59
|
+
*
|
|
60
|
+
* @param config - Configuration object
|
|
61
|
+
* @returns Object with navigation methods
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* function MyList() {
|
|
66
|
+
* const { getItems, isItemDisabled } = useItemRegistry();
|
|
67
|
+
* const [active, setActive] = useState<string>();
|
|
68
|
+
*
|
|
69
|
+
* const { handleKeyDown } = useKeyboardNavigation({
|
|
70
|
+
* getItems,
|
|
71
|
+
* isItemDisabled,
|
|
72
|
+
* active,
|
|
73
|
+
* setActive,
|
|
74
|
+
* loop: true,
|
|
75
|
+
* onSelect: (id) => console.log('Selected:', id),
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* return <div onKeyDown={handleKeyDown}>...</div>;
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function useKeyboardNavigation(config: KeyboardNavigationConfig): UseKeyboardNavigationReturn;
|
|
@@ -6,7 +6,7 @@ export type ComboboxContextValue = {
|
|
|
6
6
|
closeOnBlur: boolean;
|
|
7
7
|
inputValue: string;
|
|
8
8
|
setInputValue: (value: string) => void;
|
|
9
|
-
selection:
|
|
9
|
+
selection: ReadonlySet<string>;
|
|
10
10
|
active?: string;
|
|
11
11
|
keyHandler: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
12
12
|
disabled?: boolean;
|
|
@@ -9,6 +9,10 @@ export type ListboxContextValue = {
|
|
|
9
9
|
setActive: (id?: string) => void;
|
|
10
10
|
toggleValue: (value: string) => void;
|
|
11
11
|
keyHandler?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
12
|
+
registerItem: (id: string, disabled?: boolean) => void;
|
|
13
|
+
unregisterItem: (id: string) => void;
|
|
14
|
+
getItems: () => string[];
|
|
15
|
+
isItemDisabled: (id: string) => boolean;
|
|
12
16
|
};
|
|
13
17
|
export type ListboxProviderProps = {
|
|
14
18
|
value: ListboxContextValue;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates ARIA-compliant IDs for component parts.
|
|
3
|
+
* Helps maintain consistent ID patterns across components.
|
|
4
|
+
*
|
|
5
|
+
* @param baseId - The base ID for the component
|
|
6
|
+
* @param suffix - The suffix to append (e.g., 'trigger', 'content', 'option')
|
|
7
|
+
* @returns The generated ID
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const baseId = 'menu-1';
|
|
12
|
+
* const triggerId = generateAriaId(baseId, 'trigger'); // 'menu-1-trigger'
|
|
13
|
+
* const contentId = generateAriaId(baseId, 'content'); // 'menu-1-content'
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateAriaId(baseId: string, suffix: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Generates multiple ARIA IDs at once from a base ID.
|
|
19
|
+
* Useful for components with multiple parts.
|
|
20
|
+
*
|
|
21
|
+
* @param baseId - The base ID for the component
|
|
22
|
+
* @param parts - Array of part names
|
|
23
|
+
* @returns Object mapping part names to generated IDs
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const ids = generateAriaIds('menu-1', ['trigger', 'content', 'item']);
|
|
28
|
+
* // { trigger: 'menu-1-trigger', content: 'menu-1-content', item: 'menu-1-item' }
|
|
29
|
+
*
|
|
30
|
+
* <button id={ids.trigger} aria-controls={ids.content}>Menu</button>
|
|
31
|
+
* <div id={ids.content} role="menu">...</div>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateAriaIds<T extends readonly string[]>(baseId: string, parts: T): Record<T[number], string>;
|
|
35
|
+
/**
|
|
36
|
+
* Generates an ARIA ID for an item within a list (menu, listbox, etc.).
|
|
37
|
+
*
|
|
38
|
+
* @param baseId - The base ID for the component
|
|
39
|
+
* @param componentType - The type of component ('menu', 'listbox', etc.)
|
|
40
|
+
* @param itemValue - The value/ID of the specific item
|
|
41
|
+
* @returns The generated item ID
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const itemId = generateItemId('menu-1', 'menu', 'file-open');
|
|
46
|
+
* // 'menu-1-menu-item-file-open'
|
|
47
|
+
*
|
|
48
|
+
* <div id={itemId} role="menuitem">Open File</div>
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateItemId(baseId: string, componentType: string, itemValue: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Generates aria-activedescendant ID based on component configuration.
|
|
54
|
+
* Returns undefined if no active item.
|
|
55
|
+
*
|
|
56
|
+
* @param baseId - The base ID for the component
|
|
57
|
+
* @param componentType - The type of component ('menu', 'listbox', etc.)
|
|
58
|
+
* @param activeValue - The currently active item value (undefined if none)
|
|
59
|
+
* @returns The active descendant ID or undefined
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* const activeId = getActiveDescendantId('listbox-1', 'listbox', 'option-2');
|
|
64
|
+
* // 'listbox-1-listbox-item-option-2'
|
|
65
|
+
*
|
|
66
|
+
* <div role="listbox" aria-activedescendant={activeId}>...</div>
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function getActiveDescendantId(baseId: string, componentType: string, activeValue: string | undefined): string | undefined;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ForwardedRef, Ref } from 'react';
|
|
2
|
-
export declare function setRef<T>(ref: Ref<T> | undefined, value: T | null): void;
|
|
2
|
+
export declare function setRef<T>(ref: Ref<T> | undefined | null, value: T | null): void;
|
|
3
3
|
export declare function useComposedRefs<T>(...refs: (Ref<T> | undefined | null)[]): ForwardedRef<T>;
|