@enonic/ui 0.47.12 → 0.49.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.
@@ -1,10 +1,11 @@
1
1
  import { RefObject } from 'react';
2
+ export type FloatingSide = 'top' | 'bottom' | 'left' | 'right';
2
3
  export type FloatingPosition = {
3
4
  top: number;
4
5
  left?: number;
5
6
  right?: number;
6
- /** Which side the popup is positioned relative to the anchor */
7
- side: 'top' | 'bottom';
7
+ /** Which side the popup is positioned relative to the anchor (post-flip) */
8
+ side: FloatingSide;
8
9
  };
9
10
  export type UseFloatingPositionConfig = {
10
11
  /** Whether the floating element is open/visible */
@@ -13,48 +14,29 @@ export type UseFloatingPositionConfig = {
13
14
  anchorRef: RefObject<HTMLElement> | null;
14
15
  /** Reference to the floating content element */
15
16
  contentRef: RefObject<HTMLElement> | null;
16
- /** Horizontal alignment relative to anchor */
17
+ /**
18
+ * Preferred side relative to the anchor. Flips to the opposite side when
19
+ * the content would overflow the viewport.
20
+ * @default 'bottom'
21
+ */
22
+ side?: FloatingSide;
23
+ /**
24
+ * Alignment along the perpendicular axis:
25
+ * - For `side` top/bottom: horizontal alignment ('start' = left, 'end' = right)
26
+ * - For `side` left/right: vertical alignment ('start' = top, 'end' = bottom)
27
+ * @default 'start'
28
+ */
17
29
  align?: 'start' | 'end';
18
30
  };
19
31
  /**
20
- * Calculates optimal position for floating elements (menus, dropdowns) relative to an anchor,
21
- * with automatic viewport collision detection and flip behavior.
32
+ * Calculates optimal position for floating elements (menus, dropdowns, submenus) relative
33
+ * to an anchor, with automatic viewport collision detection and flip behavior.
22
34
  *
23
- * This hook handles:
24
- * - Positioning below anchor with configurable alignment
25
- * - Vertical flipping when overflowing bottom edge
26
- * - Horizontal adjustment to stay within viewport bounds
27
- * - Automatic repositioning on window resize and scroll
35
+ * Supports four preferred sides (top/bottom/left/right). When the content would overflow
36
+ * the viewport on the preferred side, it flips to the opposite side. Perpendicular-axis
37
+ * alignment is adjusted to keep the content within the viewport.
28
38
  *
29
39
  * @param config - Configuration object for positioning behavior
30
40
  * @returns Position object with top and left/right coordinates, or null if not yet calculated
31
- *
32
- * @example
33
- * ```tsx
34
- * function Menu() {
35
- * const anchorRef = useRef<HTMLButtonElement>(null);
36
- * const contentRef = useRef<HTMLDivElement>(null);
37
- * const position = useFloatingPosition({
38
- * enabled: open,
39
- * anchorRef,
40
- * contentRef,
41
- * align: 'start'
42
- * });
43
- *
44
- * return (
45
- * <div
46
- * ref={contentRef}
47
- * style={{
48
- * position: 'fixed',
49
- * top: position ? `${position.top}px` : '0',
50
- * left: position?.left !== undefined ? `${position.left}px` : undefined,
51
- * right: position?.right !== undefined ? `${position.right}px` : undefined,
52
- * }}
53
- * >
54
- * Menu content
55
- * </div>
56
- * );
57
- * }
58
- * ```
59
41
  */
60
- export declare function useFloatingPosition({ enabled, anchorRef, contentRef, align, }: UseFloatingPositionConfig): FloatingPosition | null;
42
+ export declare function useFloatingPosition({ enabled, anchorRef, contentRef, side: preferredSide, align, }: UseFloatingPositionConfig): FloatingPosition | null;
@@ -0,0 +1,38 @@
1
+ import { RefObject } from 'react';
2
+ export type SafeAreaSide = 'left' | 'right';
3
+ export type UseSubmenuSafeAreaConfig = {
4
+ /** Whether the submenu is open (tracking is active) */
5
+ enabled: boolean;
6
+ /** Reference to the submenu content element — used to compute its near edge */
7
+ submenuRef: RefObject<HTMLElement>;
8
+ /**
9
+ * Which side the submenu sits on relative to its trigger.
10
+ * - 'right': near edge is the submenu's LEFT edge; pointer travels right toward it
11
+ * - 'left': near edge is the submenu's RIGHT edge; pointer travels left toward it
12
+ */
13
+ side: SafeAreaSide;
14
+ /**
15
+ * How long (ms) after the submenu opens the safe zone remains valid without
16
+ * a confirming pointer sample. Prevents a stale history from locking the zone.
17
+ * @default 300
18
+ */
19
+ graceMs?: number;
20
+ };
21
+ export type UseSubmenuSafeAreaReturn = {
22
+ /**
23
+ * Returns true if the pointer at (x, y) is currently traveling toward the submenu's
24
+ * near edge — i.e. inside the triangle formed by the previous pointer sample and
25
+ * the two corners of that edge. Callers should skip sibling hover handling while
26
+ * this is true.
27
+ */
28
+ isInSafeArea: (x: number, y: number) => boolean;
29
+ };
30
+ /**
31
+ * Tracks pointer history while a submenu is open and computes whether the pointer
32
+ * is inside the "safe triangle" — the region between the previous pointer position
33
+ * and the submenu's near edge. Callers use this to suppress sibling-item hover
34
+ * handling while the user is diagonally moving toward the submenu.
35
+ *
36
+ * Implements the classic Amazon.com menu heuristic popularized by Radix UI.
37
+ */
38
+ export declare function useSubmenuSafeArea({ enabled, submenuRef, side, graceMs, }: UseSubmenuSafeAreaConfig): UseSubmenuSafeAreaReturn;
@@ -0,0 +1,29 @@
1
+ import { MutableRefObject, ReactElement, ReactNode } from 'react';
2
+ /**
3
+ * Navigation-related state scoped to a single `ContextMenu.Content` (or `SubContent`).
4
+ *
5
+ * Each menu level owns its own item registry and active-item state so nested submenus
6
+ * don't share navigation state with their parent. Selection-related state like `setOpen`
7
+ * (which closes the entire menu) still lives on the root `ContextMenuContextValue`.
8
+ */
9
+ export type ContextMenuContentContextValue = {
10
+ active: string | undefined;
11
+ setActive: (id: string | undefined) => void;
12
+ registerItem: (id: string, disabled?: boolean, element?: HTMLElement | null) => void;
13
+ unregisterItem: (id: string) => void;
14
+ getItems: () => string[];
15
+ isItemDisabled: (id: string) => boolean;
16
+ /**
17
+ * Safe-triangle check registered by a currently-open child `Sub`. When non-null,
18
+ * the Content's pointer-move capture handler consults it to decide whether to
19
+ * suppress sibling hover activation while the user is traveling toward the submenu.
20
+ * Null when no submenu is open at this level.
21
+ */
22
+ safeAreaCheckRef: MutableRefObject<((x: number, y: number) => boolean) | null>;
23
+ };
24
+ export type ContextMenuContentProviderProps = {
25
+ value: ContextMenuContentContextValue;
26
+ children?: ReactNode;
27
+ };
28
+ export declare const ContextMenuContentProvider: ({ value, children }: ContextMenuContentProviderProps) => ReactElement;
29
+ export declare const useContextMenuContent: () => ContextMenuContentContextValue;
@@ -1,14 +1,16 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
+ /**
3
+ * Root-level context for ContextMenu. Holds menu-wide state that every level
4
+ * (top content, submenus) needs access to.
5
+ *
6
+ * Navigation state (active item, item registry) lives in a per-level
7
+ * `ContextMenuContentContextValue` — each Content and SubContent owns its own.
8
+ */
2
9
  export type ContextMenuContextValue = {
3
10
  baseId: string;
4
11
  open: boolean;
5
12
  setOpen: (open: boolean) => void;
6
- active: string | undefined;
7
- setActive: (id: string | undefined) => void;
8
- registerItem: (id: string, disabled?: boolean, element?: HTMLElement | null) => void;
9
- unregisterItem: (id: string) => void;
10
- getItems: () => string[];
11
- isItemDisabled: (id: string) => boolean;
13
+ /** Pointer coordinates where the menu was opened (set by Trigger onContextMenu) */
12
14
  position: {
13
15
  x: number;
14
16
  y: number;
@@ -0,0 +1,28 @@
1
+ import { RefObject } from 'react';
2
+ import { ContextMenuContentContextValue } from './context-menu-content-provider';
3
+ /**
4
+ * Context value for a single `ContextMenu.Sub` wrapper.
5
+ *
6
+ * Coordinates between the submenu's local open state and the parent content's
7
+ * navigation state (so ArrowLeft from a submenu can restore active on its trigger).
8
+ */
9
+ export type ContextMenuSubContextValue = {
10
+ /** Unique ID used by the SubTrigger (also the registry key in the parent content) */
11
+ subId: string;
12
+ /** ID of the submenu content element (for aria-controls) */
13
+ contentId: string;
14
+ /** Whether the submenu is currently open */
15
+ open: boolean;
16
+ /** Open/close the submenu */
17
+ setOpen: (open: boolean) => void;
18
+ /** Ref to the SubTrigger DOM node (for ArrowLeft refocus and collision anchoring) */
19
+ triggerRef: RefObject<HTMLDivElement> | null;
20
+ /**
21
+ * Snapshot of the parent content context captured at mount time.
22
+ * Used by SubContent's ArrowLeft handler to set parent `active` back to the trigger.
23
+ */
24
+ parentContent: ContextMenuContentContextValue;
25
+ };
26
+ export declare const ContextMenuSubProvider: import('preact').Provider<ContextMenuSubContextValue | undefined>;
27
+ export declare const useContextMenuSub: () => ContextMenuSubContextValue;
28
+ export declare const useContextMenuSubOptional: () => ContextMenuSubContextValue | undefined;
@@ -1,6 +1,8 @@
1
1
  export * from './avatar-provider';
2
2
  export * from './combobox-provider';
3
+ export * from './context-menu-content-provider';
3
4
  export * from './context-menu-provider';
5
+ export * from './context-menu-sub-provider';
4
6
  export * from './date-picker-provider';
5
7
  export * from './dialog-provider';
6
8
  export * from './focus-container-provider';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enonic/ui",
3
- "version": "0.47.12",
3
+ "version": "0.49.0",
4
4
  "description": "Enonic UI Component Library",
5
5
  "author": "Enonic",
6
6
  "license": "MIT",