@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.
@@ -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: readonly string[];
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,4 @@
1
+ export * from './aria';
1
2
  export * from './cn';
2
3
  export * from './ref';
3
4
  export * from './unwrap';
@@ -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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enonic/ui",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Enonic UI Component Library",
5
5
  "author": "Enonic",
6
6
  "license": "MIT",