@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,69 @@
|
|
|
1
|
+
export type UseRovingTabIndexConfig = {
|
|
2
|
+
/** The ID of this item */
|
|
3
|
+
id: string;
|
|
4
|
+
/** Currently active item ID (undefined if no item is active) */
|
|
5
|
+
active: string | undefined;
|
|
6
|
+
/** Whether this item is disabled */
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
/** Function to get all registered item IDs */
|
|
9
|
+
getItems: () => string[];
|
|
10
|
+
/** Function to check if a specific item is disabled */
|
|
11
|
+
isItemDisabled: (id: string) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Focus mode for the component
|
|
14
|
+
* - 'roving-tabindex': Items are individually focusable (default)
|
|
15
|
+
* - 'activedescendant': Container manages focus via aria-activedescendant
|
|
16
|
+
*/
|
|
17
|
+
focusMode?: 'roving-tabindex' | 'activedescendant';
|
|
18
|
+
};
|
|
19
|
+
export type UseRovingTabIndexReturn = {
|
|
20
|
+
/** Whether this item should be focusable (tabIndex=0) */
|
|
21
|
+
isFocusable: boolean;
|
|
22
|
+
/** The tabIndex value to apply to this item */
|
|
23
|
+
tabIndex: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Implements roving tabindex pattern for keyboard navigation.
|
|
27
|
+
*
|
|
28
|
+
* In roving tabindex, only one item in a list is focusable at a time (tabIndex=0),
|
|
29
|
+
* while all other items have tabIndex=-1. This allows users to:
|
|
30
|
+
* - Tab into the list (focuses the active or first available item)
|
|
31
|
+
* - Use arrow keys to navigate within the list
|
|
32
|
+
* - Tab out of the list
|
|
33
|
+
*
|
|
34
|
+
* The hook automatically determines which item should be focusable based on:
|
|
35
|
+
* 1. The currently active item (if set and not disabled)
|
|
36
|
+
* 2. The first non-disabled item (fallback)
|
|
37
|
+
* 3. The first item in the list (if all items are disabled)
|
|
38
|
+
* 4. This item itself (final fallback if no other items exist)
|
|
39
|
+
*
|
|
40
|
+
* @param config - Configuration object for roving tabindex behavior
|
|
41
|
+
* @returns Object containing isFocusable flag and tabIndex value
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* function MenuItem({ id, disabled }: MenuItemProps) {
|
|
46
|
+
* const { active, getItems, isItemDisabled } = useMenu();
|
|
47
|
+
* const { isFocusable, tabIndex } = useRovingTabIndex({
|
|
48
|
+
* id,
|
|
49
|
+
* active,
|
|
50
|
+
* disabled,
|
|
51
|
+
* getItems,
|
|
52
|
+
* isItemDisabled,
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* return (
|
|
56
|
+
* <div
|
|
57
|
+
* role="menuitem"
|
|
58
|
+
* tabIndex={tabIndex}
|
|
59
|
+
* data-focusable={isFocusable || undefined}
|
|
60
|
+
* >
|
|
61
|
+
* {children}
|
|
62
|
+
* </div>
|
|
63
|
+
* );
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @see {@link https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex}
|
|
68
|
+
*/
|
|
69
|
+
export declare function useRovingTabIndex({ id, active, disabled, getItems, isItemDisabled, focusMode, }: UseRovingTabIndexConfig): UseRovingTabIndexReturn;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export type UseScrollActiveIntoViewConfig = {
|
|
3
|
+
/** Reference to the container element */
|
|
4
|
+
containerRef: RefObject<HTMLElement> | null;
|
|
5
|
+
/** ID of the currently active item (undefined if no active item) */
|
|
6
|
+
activeId: string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Scroll orientation:
|
|
9
|
+
* - 'vertical': Only scroll vertically (block: 'nearest')
|
|
10
|
+
* - 'horizontal': Scroll both horizontally and vertically (block + inline: 'nearest')
|
|
11
|
+
*/
|
|
12
|
+
orientation?: 'vertical' | 'horizontal';
|
|
13
|
+
/**
|
|
14
|
+
* Optional function to construct the element ID from the active ID.
|
|
15
|
+
* If not provided, uses activeId directly.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // For Listbox, which prefixes IDs:
|
|
19
|
+
* buildElementId: (id) => `${baseId}-listbox-option-${id}`
|
|
20
|
+
*/
|
|
21
|
+
buildElementId?: (activeId: string) => string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Automatically scrolls the active item into view within a scrollable container.
|
|
25
|
+
*
|
|
26
|
+
* This hook is commonly used in keyboard-navigable lists, menus, and navigation
|
|
27
|
+
* components to ensure the currently active item is visible when it changes.
|
|
28
|
+
*
|
|
29
|
+
* Features:
|
|
30
|
+
* - Smooth scrolling behavior with 'nearest' algorithm (minimal scroll distance)
|
|
31
|
+
* - Supports both vertical and horizontal scrolling
|
|
32
|
+
* - Supports custom ID construction for prefixed/namespaced IDs
|
|
33
|
+
* - Only scrolls when activeId changes
|
|
34
|
+
*
|
|
35
|
+
* @param config - Configuration object for scroll behavior
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* // Vertical list (Menu)
|
|
40
|
+
* function MenuContent() {
|
|
41
|
+
* const contentRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
* const { active } = useMenu();
|
|
43
|
+
*
|
|
44
|
+
* useScrollActiveIntoView({
|
|
45
|
+
* containerRef: contentRef,
|
|
46
|
+
* activeId: active,
|
|
47
|
+
* orientation: 'vertical',
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* return <div ref={contentRef}>...</div>;
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* // Horizontal navigation (Menubar)
|
|
54
|
+
* function Menubar() {
|
|
55
|
+
* const menubarRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
* const { active } = useMenubar();
|
|
57
|
+
*
|
|
58
|
+
* useScrollActiveIntoView({
|
|
59
|
+
* containerRef: menubarRef,
|
|
60
|
+
* activeId: active,
|
|
61
|
+
* orientation: 'horizontal',
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* return <div ref={menubarRef}>...</div>;
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
67
|
+
* // With ID prefix (Listbox)
|
|
68
|
+
* function ListboxContent({ baseId }: { baseId: string }) {
|
|
69
|
+
* const contentRef = useRef<HTMLDivElement>(null);
|
|
70
|
+
* const { active } = useListbox();
|
|
71
|
+
*
|
|
72
|
+
* useScrollActiveIntoView({
|
|
73
|
+
* containerRef: contentRef,
|
|
74
|
+
* activeId: active,
|
|
75
|
+
* orientation: 'vertical',
|
|
76
|
+
* buildElementId: (id) => `${baseId}-listbox-option-${id}`,
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* return <div ref={contentRef}>...</div>;
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function useScrollActiveIntoView({ containerRef, activeId, orientation, buildElementId, }: UseScrollActiveIntoViewConfig): void;
|
|
@@ -1,12 +1,25 @@
|
|
|
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
|
-
|
|
9
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Focus management mode:
|
|
14
|
+
* - 'roving-tabindex': Items are individually focusable (default)
|
|
15
|
+
* - 'activedescendant': Container manages focus via aria-activedescendant
|
|
16
|
+
*/
|
|
17
|
+
focusMode?: 'roving-tabindex' | 'activedescendant';
|
|
18
|
+
/**
|
|
19
|
+
* Set active item.
|
|
20
|
+
* Accepts `null` for compatibility with controlled prop API, but converts it to `undefined` internally.
|
|
21
|
+
*/
|
|
22
|
+
setActive: (id?: string | null) => void;
|
|
10
23
|
toggleValue: (value: string) => void;
|
|
11
24
|
keyHandler?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
12
25
|
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,72 @@
|
|
|
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
|
+
* Tracks which menu is expanded in the menubar for conditional hover behavior.
|
|
27
|
+
*/
|
|
28
|
+
openMenuId: string | undefined;
|
|
29
|
+
setOpenMenuId: (id: string | undefined) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Base ID for the menubar container.
|
|
32
|
+
*/
|
|
33
|
+
menubarId: string;
|
|
34
|
+
/**
|
|
35
|
+
* Ref to the menubar container element for focus management.
|
|
36
|
+
*/
|
|
37
|
+
menubarRef: RefObject<HTMLDivElement> | null;
|
|
38
|
+
};
|
|
39
|
+
export type MenubarProviderProps = {
|
|
40
|
+
value: MenubarContextValue;
|
|
41
|
+
children?: ReactNode;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Provider component that supplies menubar context to child components.
|
|
45
|
+
* Should wrap all Menubar.* components.
|
|
46
|
+
*/
|
|
47
|
+
export declare const MenubarProvider: {
|
|
48
|
+
({ value, children }: MenubarProviderProps): ReactElement;
|
|
49
|
+
displayName: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Hook to access menubar context.
|
|
53
|
+
* Must be used within a MenubarProvider.
|
|
54
|
+
*
|
|
55
|
+
* @throws {Error} If used outside of MenubarProvider
|
|
56
|
+
* @returns {MenubarContextValue} The menubar context
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```tsx
|
|
60
|
+
* const { active, setActive, registerItem } = useMenubar();
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare const useMenubar: () => MenubarContextValue;
|
|
64
|
+
/**
|
|
65
|
+
* Hook to optionally access menubar context.
|
|
66
|
+
* Returns undefined if not within a MenubarProvider.
|
|
67
|
+
*
|
|
68
|
+
* Useful for components that can work both inside and outside a menubar.
|
|
69
|
+
*
|
|
70
|
+
* @returns {MenubarContextValue | undefined} The menubar context or undefined
|
|
71
|
+
*/
|
|
72
|
+
export declare const useMenubarOptional: () => MenubarContextValue | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enonic/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Enonic UI Component Library",
|
|
5
5
|
"author": "Enonic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"class-variance-authority": "~0.7.1",
|
|
48
48
|
"clsx": "~2.1.1",
|
|
49
|
-
"tailwind-merge": "~3.
|
|
49
|
+
"tailwind-merge": "~3.4.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@radix-ui/react-slot": "^1.2.0",
|
|
@@ -68,44 +68,44 @@
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@chromatic-com/storybook": "~4.1.
|
|
72
|
-
"@eslint/js": "~9.
|
|
71
|
+
"@chromatic-com/storybook": "~4.1.2",
|
|
72
|
+
"@eslint/js": "~9.39.1",
|
|
73
73
|
"@preact/preset-vite": "~2.10.2",
|
|
74
74
|
"@size-limit/file": "~11.2.0",
|
|
75
75
|
"@size-limit/preset-small-lib": "~11.2.0",
|
|
76
|
-
"@storybook/addon-a11y": "~
|
|
77
|
-
"@storybook/addon-docs": "~
|
|
78
|
-
"@storybook/addon-links": "~
|
|
79
|
-
"@storybook/addon-themes": "~
|
|
80
|
-
"@storybook/preact-vite": "~
|
|
81
|
-
"@tailwindcss/vite": "~4.1.
|
|
82
|
-
"@trivago/prettier-plugin-sort-imports": "~
|
|
83
|
-
"@types/node": "~24.
|
|
84
|
-
"@types/react": "~19.2.
|
|
85
|
-
"@types/react-dom": "~19.2.
|
|
86
|
-
"@typescript-eslint/eslint-plugin": "~8.46.
|
|
87
|
-
"@typescript-eslint/parser": "~8.46.
|
|
88
|
-
"eslint": "~9.
|
|
76
|
+
"@storybook/addon-a11y": "~10.0.7",
|
|
77
|
+
"@storybook/addon-docs": "~10.0.7",
|
|
78
|
+
"@storybook/addon-links": "~10.0.7",
|
|
79
|
+
"@storybook/addon-themes": "~10.0.7",
|
|
80
|
+
"@storybook/preact-vite": "~10.0.7",
|
|
81
|
+
"@tailwindcss/vite": "~4.1.17",
|
|
82
|
+
"@trivago/prettier-plugin-sort-imports": "~6.0.0",
|
|
83
|
+
"@types/node": "~24.10.0",
|
|
84
|
+
"@types/react": "~19.2.3",
|
|
85
|
+
"@types/react-dom": "~19.2.2",
|
|
86
|
+
"@typescript-eslint/eslint-plugin": "~8.46.4",
|
|
87
|
+
"@typescript-eslint/parser": "~8.46.4",
|
|
88
|
+
"eslint": "~9.39.1",
|
|
89
89
|
"eslint-import-resolver-typescript": "~4.4.4",
|
|
90
90
|
"eslint-plugin-import": "~2.32.0",
|
|
91
91
|
"eslint-plugin-jsx-a11y": "~6.10.2",
|
|
92
92
|
"eslint-plugin-react": "~7.37.5",
|
|
93
|
-
"eslint-plugin-react-hooks": "~7.0.
|
|
93
|
+
"eslint-plugin-react-hooks": "~7.0.1",
|
|
94
94
|
"husky": "~9.1.7",
|
|
95
|
-
"lint-staged": "~16.2.
|
|
96
|
-
"lucide-preact": "~0.
|
|
97
|
-
"lucide-react": "~0.
|
|
95
|
+
"lint-staged": "~16.2.6",
|
|
96
|
+
"lucide-preact": "~0.553.0",
|
|
97
|
+
"lucide-react": "~0.553.0",
|
|
98
98
|
"postcss": "~8.5.6",
|
|
99
99
|
"prettier": "~3.6.2",
|
|
100
|
-
"rollup-plugin-visualizer": "~6.0.
|
|
100
|
+
"rollup-plugin-visualizer": "~6.0.5",
|
|
101
101
|
"size-limit": "~11.2.0",
|
|
102
|
-
"storybook": "~
|
|
103
|
-
"tailwindcss": "~4.1.
|
|
104
|
-
"terser": "~5.44.
|
|
102
|
+
"storybook": "~10.0.7",
|
|
103
|
+
"tailwindcss": "~4.1.17",
|
|
104
|
+
"terser": "~5.44.1",
|
|
105
105
|
"tw-animate-css": "~1.4.0",
|
|
106
106
|
"typescript": "~5.9.3",
|
|
107
|
-
"typescript-eslint": "~8.46.
|
|
108
|
-
"vite": "~7.
|
|
107
|
+
"typescript-eslint": "~8.46.4",
|
|
108
|
+
"vite": "~7.2.2",
|
|
109
109
|
"vite-plugin-dts": "~4.5.4",
|
|
110
110
|
"vite-plugin-environment": "~1.1.3"
|
|
111
111
|
},
|