@enonic/ui 0.48.0 → 0.50.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.
@@ -0,0 +1,63 @@
1
+ import { RefObject } from 'react';
2
+ export type UseActiveOnOpenConfig = {
3
+ /**
4
+ * Whether the dropdown is open. Pass `true` for always-open contexts
5
+ * like a standalone Listbox — the rising edge fires once on mount.
6
+ */
7
+ open: boolean;
8
+ /**
9
+ * Current selection set. The hook picks the first selected item in
10
+ * DOM order (according to `getItems()`) as the activation target.
11
+ */
12
+ selection: ReadonlySet<string>;
13
+ /** Setter for active item. */
14
+ setActive: (id: string | undefined) => void;
15
+ /** Returns DOM-ordered IDs from the item registry. */
16
+ getItems: () => readonly string[];
17
+ /** Returns whether an item is disabled. */
18
+ isItemDisabled: (id: string) => boolean;
19
+ /**
20
+ * Container element to scroll. The hook centers the activated selection
21
+ * inside this container by querying for the option element and calling
22
+ * `scrollIntoView({ block: 'center' })`. When omitted, only state is
23
+ * updated (useful for tests or virtualized consumers handling scroll
24
+ * through their own adapter).
25
+ */
26
+ containerRef?: RefObject<HTMLElement> | null;
27
+ /**
28
+ * Build the option element's DOM id from a registry id. Defaults to
29
+ * identity. Match the consumer's `buildElementId` used elsewhere.
30
+ */
31
+ buildElementId?: (id: string) => string;
32
+ /**
33
+ * When `false`, the hook does nothing. Used by Combobox when `active`
34
+ * is controlled externally — the consumer owns activation.
35
+ * @default true
36
+ */
37
+ enabled?: boolean;
38
+ };
39
+ /**
40
+ * Activates the selected item (or first enabled fallback) when a dropdown
41
+ * opens, and centers the activated selection in the viewport.
42
+ *
43
+ * Trigger: `open` rising edge. On rising edge, the hook unconditionally
44
+ * rewrites active even if a stale value persists from the previous open.
45
+ * If the item registry is empty at that moment (popup mounts items lazily,
46
+ * common with Portals), the hook re-runs on subsequent renders until items
47
+ * appear — `getItems` callback identity is coupled to the registry version,
48
+ * so listing it as a dep covers lazy-mount cases.
49
+ *
50
+ * Behavior:
51
+ * - Selection has any enabled item present in the registry → activate it
52
+ * and call `scrollIntoView({ block: 'center' })` on the option element.
53
+ * - Otherwise → set active to first enabled item; no scroll (the default
54
+ * `'nearest'` from `useScrollActiveIntoView` will apply on next nav).
55
+ * - No items registered yet → wait; the next `getItems` identity change
56
+ * re-runs the effect (covers the lazy-mount case).
57
+ *
58
+ * The imperative scroll is owned by this hook so the open-transition center
59
+ * behavior is independent of whether `setActive` actually changed the value
60
+ * — if `active` already pointed at the target, `useScrollActiveIntoView`
61
+ * would not re-fire, and a coordination-via-ref scheme would silently fail.
62
+ */
63
+ export declare function useActiveOnOpen({ open, selection, setActive, getItems, isItemDisabled, containerRef, buildElementId, enabled, }: UseActiveOnOpenConfig): void;
@@ -13,6 +13,8 @@ export type UseClickOutsideConfig = {
13
13
  onPointerDownOutside?: (event: PointerEvent) => void;
14
14
  /** Callback when any interaction occurs outside */
15
15
  onInteractOutside?: (event: Event) => void;
16
+ /** Which event should trigger `onClose`. Outside callbacks still fire on pointerdown. Defaults to pointerdown. */
17
+ closeOn?: 'pointerdown' | 'click';
16
18
  /**
17
19
  * Callback to close/hide the content.
18
20
  * Only called if event.defaultPrevented is false.
@@ -29,6 +31,8 @@ export type UseClickOutsideConfig = {
29
31
  * - Ignores clicks inside the content element
30
32
  * - Supports excluding additional elements (e.g., trigger buttons)
31
33
  * - Ignores clicks inside elements with `data-click-outside-ignore` attribute
34
+ * - Can close immediately on outside pointerdown, or wait for the matching click
35
+ * - Waiting for click helps avoid mobile tap clicks reaching elements behind a closing overlay
32
36
  * - Respects event.defaultPrevented to allow custom handling
33
37
  * - Only active when enabled
34
38
  * - Properly cleans up event listeners
@@ -97,4 +101,4 @@ export type UseClickOutsideConfig = {
97
101
  * }
98
102
  * ```
99
103
  */
100
- export declare function useClickOutside({ enabled, contentRef, excludeRefs, onPointerDownOutside, onInteractOutside, onClose, }: UseClickOutsideConfig): void;
104
+ export declare function useClickOutside({ enabled, contentRef, excludeRefs, onPointerDownOutside, onInteractOutside, closeOn, onClose, }: UseClickOutsideConfig): void;
@@ -21,66 +21,8 @@ export type UseScrollActiveIntoViewConfig = {
21
21
  buildElementId?: (activeId: string) => string;
22
22
  };
23
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
- * // Memoize to keep the effect dependency stable across re-renders
73
- * const buildElementId = useCallback((id: string) => `${baseId}-listbox-option-${id}`, [baseId]);
74
- *
75
- * useScrollActiveIntoView({
76
- * containerRef: contentRef,
77
- * activeId: active,
78
- * orientation: 'vertical',
79
- * buildElementId,
80
- * });
81
- *
82
- * return <div ref={contentRef}>...</div>;
83
- * }
84
- * ```
24
+ * Reactive scroll-into-view for keyboard navigation. Uses `block: 'nearest'`
25
+ * for minimal scroll travel. For the open-transition "center on selected"
26
+ * behavior, see `useActiveOnOpen` which performs an imperative center-scroll.
85
27
  */
86
28
  export declare function useScrollActiveIntoView({ containerRef, activeId, orientation, buildElementId, }: UseScrollActiveIntoViewConfig): void;
@@ -0,0 +1,42 @@
1
+ import { RefObject } from 'react';
2
+ type VirtuosoLikeHandle = {
3
+ scrollToIndex: (location: number | {
4
+ index: number;
5
+ align?: 'start' | 'center' | 'end';
6
+ behavior?: 'auto' | 'smooth';
7
+ }) => void;
8
+ };
9
+ export type UseScrollSelectedToVirtuosoConfig<TItem extends {
10
+ id: string;
11
+ }> = {
12
+ /** Whether the dropdown is open. The hook fires on the rising edge. */
13
+ open: boolean;
14
+ /** The single selected ID to center on open, or undefined to skip. */
15
+ selectedId: string | undefined;
16
+ /**
17
+ * Setter called with `selectedId` on the rising edge so navigation
18
+ * resumes from there. Pass the active setter from the wrapped list
19
+ * (e.g., `VirtualizedTreeList`'s `onActiveChange` consumer).
20
+ */
21
+ setActive: (id: string) => void;
22
+ /** Flat items in render order. Used to look up the index for `scrollToIndex`. */
23
+ items: readonly TItem[];
24
+ /** Ref to the Virtuoso instance for scroll operations. */
25
+ virtuosoRef: RefObject<VirtuosoLikeHandle | null> | undefined;
26
+ };
27
+ /**
28
+ * Centers the selected item in a Virtuoso-backed virtualized list when its
29
+ * containing dropdown opens, and sets the consumer's active state to match.
30
+ *
31
+ * Retries on `requestAnimationFrame` while the Virtuoso ref is still null
32
+ * (Virtuoso typically attaches within a frame of mount). Bails after a small
33
+ * cap to avoid infinite loops.
34
+ *
35
+ * Use inside a consumer that wraps `VirtualizedTreeList` in `Combobox.TreeContent`
36
+ * (or any other open/close container) when the same UX as `Combobox.ListContent`
37
+ * is desired: open with selection → centered, arrow keys → nearest.
38
+ */
39
+ export declare function useScrollSelectedToVirtuoso<TItem extends {
40
+ id: string;
41
+ }>({ open, selectedId, setActive, items, virtuosoRef, }: UseScrollSelectedToVirtuosoConfig<TItem>): void;
42
+ export {};
@@ -29,6 +29,12 @@ export type ComboboxContextValue = {
29
29
  isItemDisabled: (id: string) => boolean;
30
30
  hasValue: boolean;
31
31
  setHasValue: (has: boolean) => void;
32
+ /**
33
+ * Whether the auto-active-on-open behavior should run inside Listbox-backed
34
+ * content. Set to `false` when active state is consumer-controlled. Tree
35
+ * content doesn't use this — see `useScrollSelectedToVirtuoso` instead.
36
+ */
37
+ autoActiveOnOpen: boolean;
32
38
  };
33
39
  export type ComboboxProviderProps = {
34
40
  value: ComboboxContextValue;
@@ -26,6 +26,11 @@ export type ListboxContextValue = {
26
26
  unregisterItem: (id: string) => void;
27
27
  getItems: () => string[];
28
28
  isItemDisabled: (id: string) => boolean;
29
+ /**
30
+ * When `false`, `useActiveOnOpen` in `Listbox.Content` is skipped. Set
31
+ * by hosts (e.g., Combobox) when the consumer controls active state.
32
+ */
33
+ autoActiveOnOpen: boolean;
29
34
  };
30
35
  export type ListboxProviderProps = {
31
36
  value: ListboxContextValue;
@@ -9,6 +9,7 @@ export type VirtualizedTreeListContextValue = {
9
9
  toggleSelection: (id: string, index: number) => void;
10
10
  selectionMode: 'single' | 'multiple' | 'none';
11
11
  isFocused: boolean;
12
+ keyboardActive: boolean;
12
13
  items: readonly FlatNodeBase[];
13
14
  getItemIndex: (id: string) => number;
14
15
  scrollToIndex: (index: number) => void;
@@ -2,4 +2,5 @@ export * from './aria';
2
2
  export * from './cn';
3
3
  export * from './mobile';
4
4
  export * from './ref';
5
+ export * from './scroll';
5
6
  export * from './unwrap';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Scrolls an element to the center of its nearest scrollable ancestor without
3
+ * disturbing outer scroll containers (page, dialog, etc.).
4
+ *
5
+ * - Chromium 134+ uses `scrollIntoView({ block: 'center', container: 'nearest' })`
6
+ * and centers the element within the closest scroll container only.
7
+ * - Other browsers fall back to `scrollIntoView({ block: 'nearest' })` — the
8
+ * element lands at the closest edge instead of the center, but ancestors
9
+ * that already show it stay put.
10
+ *
11
+ * Useful for dropdowns: bring the selected option into view on open without
12
+ * scrolling the page underneath.
13
+ */
14
+ export declare function scrollIntoCenter(element: HTMLElement): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enonic/ui",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "Enonic UI Component Library",
5
5
  "author": "Enonic",
6
6
  "license": "MIT",