@trackunit/react-components 2.5.4 → 2.6.2-alpha-770d9994af7.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,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.menuListRenameToMenuContent = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const ts = tslib_1.__importStar(require("typescript"));
6
+ const jsx_utils_1 = require("../utils/jsx-utils");
7
+ const PACKAGE_NAME = "@trackunit/react-components";
8
+ const RENAMES = [
9
+ { from: "MenuList", to: "MenuContent" },
10
+ { from: "MenuListProps", to: "MenuContentProps" },
11
+ ];
12
+ /**
13
+ * Every `Identifier` node in the file matching `name`. Safe to use as a
14
+ * blanket rename once we've confirmed (via `getImportedAliases`) that `name`
15
+ * is genuinely imported from the target package with no local alias — in
16
+ * that case the import specifier, JSX tag references, and type references
17
+ * all share the same identifier text, so one pass covers every site.
18
+ */
19
+ const collectIdentifierEdits = (sourceFile, name, replacement) => {
20
+ const edits = [];
21
+ const visit = (node) => {
22
+ if (ts.isIdentifier(node) && node.text === name) {
23
+ edits.push({ start: node.getStart(sourceFile), end: node.getEnd(), text: replacement });
24
+ }
25
+ ts.forEachChild(node, visit);
26
+ };
27
+ visit(sourceFile);
28
+ return edits;
29
+ };
30
+ /**
31
+ * Locates the `propertyName` of an aliased named import specifier
32
+ * (`{ MenuList as Foo }`) for `importedName`. Only that part needs
33
+ * rewriting — the local binding `Foo` (and therefore every usage site in
34
+ * the file) stays exactly as the author wrote it.
35
+ */
36
+ const findAliasedImportSpecifierEdit = (sourceFile, packageName, importedName, replacement) => {
37
+ for (const stmt of sourceFile.statements) {
38
+ if (!ts.isImportDeclaration(stmt))
39
+ continue;
40
+ const moduleSpecifier = stmt.moduleSpecifier;
41
+ if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== packageName)
42
+ continue;
43
+ const namedBindings = stmt.importClause?.namedBindings;
44
+ if (namedBindings === undefined || !ts.isNamedImports(namedBindings))
45
+ continue;
46
+ for (const element of namedBindings.elements) {
47
+ if (element.propertyName === undefined)
48
+ continue;
49
+ if (element.propertyName.text !== importedName)
50
+ continue;
51
+ return {
52
+ start: element.propertyName.getStart(sourceFile),
53
+ end: element.propertyName.getEnd(),
54
+ text: replacement,
55
+ };
56
+ }
57
+ }
58
+ return null;
59
+ };
60
+ const transformMenuListUsage = (filePath, content) => {
61
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, filePath);
62
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, PACKAGE_NAME);
63
+ if (aliases === null)
64
+ return null;
65
+ const edits = [];
66
+ for (const { from, to } of RENAMES) {
67
+ const localAlias = Object.entries(aliases).find(([, original]) => original === from)?.[0];
68
+ if (localAlias === undefined)
69
+ continue;
70
+ if (localAlias === from) {
71
+ edits.push(...collectIdentifierEdits(sourceFile, from, to));
72
+ }
73
+ else {
74
+ const edit = findAliasedImportSpecifierEdit(sourceFile, PACKAGE_NAME, from, to);
75
+ if (edit !== null)
76
+ edits.push(edit);
77
+ }
78
+ }
79
+ if (edits.length === 0)
80
+ return null;
81
+ edits.sort((a, b) => b.start - a.start);
82
+ let updated = content;
83
+ for (const { start, end, text } of edits) {
84
+ updated = updated.slice(0, start) + text + updated.slice(end);
85
+ }
86
+ return updated;
87
+ };
88
+ /**
89
+ * Renames `MenuList`/`MenuListProps` to `MenuContent`/`MenuContentProps`
90
+ * everywhere they're imported from `@trackunit/react-components` — the
91
+ * import specifier, every JSX tag usage, and every type reference (e.g.
92
+ * `Omit<MenuListProps, "children">`).
93
+ *
94
+ * Aliased imports (`{ MenuList as Foo }`) only need the imported name
95
+ * rewritten; the local alias and its usage sites are left untouched.
96
+ * Unrelated same-prefix exports like `cvaMenuList`/`cvaMenuListItem` are
97
+ * never matched since identifier comparisons are exact, not substring.
98
+ */
99
+ const menuListRenameToMenuContent = (tree) => {
100
+ const touched = (0, jsx_utils_1.visitTsxFiles)(tree, "MenuList", transformMenuListUsage);
101
+ (0, jsx_utils_1.logSummary)("menulist-rename-to-menucontent", touched);
102
+ };
103
+ exports.menuListRenameToMenuContent = menuListRenameToMenuContent;
104
+ exports.default = exports.menuListRenameToMenuContent;
105
+ //# sourceMappingURL=menulist-rename-to-menucontent.js.map
package/migrations.json CHANGED
@@ -39,6 +39,11 @@
39
39
  "version": "2.0.0",
40
40
  "description": "Remove the no-longer-supported style property from inline ToggleGroup list items.",
41
41
  "implementation": "./migrations/v2-0-0/togglegroup-remove-item-style"
42
+ },
43
+ "v3-0-0-menulist-rename-to-menucontent": {
44
+ "version": "3.0.0",
45
+ "description": "Rename MenuList/MenuListProps to MenuContent/MenuContentProps (import specifiers, JSX tags, and type references).",
46
+ "implementation": "./migrations/v3-0-0/menulist-rename-to-menucontent"
42
47
  }
43
48
  }
44
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "2.5.4",
3
+ "version": "2.6.2-alpha-770d9994af7.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "migrations": "./migrations.json",
@@ -14,17 +14,17 @@
14
14
  "@floating-ui/react": "^0.26.25",
15
15
  "string-ts": "^2.0.0",
16
16
  "tailwind-merge": "^2.0.0",
17
- "@trackunit/ui-design-tokens": "1.14.1",
18
- "@trackunit/css-class-variance-utilities": "1.14.1",
19
- "@trackunit/shared-utils": "1.16.1",
20
- "@trackunit/ui-icons": "1.14.1",
17
+ "@trackunit/ui-design-tokens": "1.14.3-alpha-770d9994af7.0",
18
+ "@trackunit/css-class-variance-utilities": "1.14.3-alpha-770d9994af7.0",
19
+ "@trackunit/shared-utils": "1.16.3-alpha-770d9994af7.0",
20
+ "@trackunit/ui-icons": "1.14.3-alpha-770d9994af7.0",
21
21
  "es-toolkit": "^1.39.10",
22
22
  "@tanstack/react-virtual": "^3.14.3",
23
23
  "dequal": "^2.0.3",
24
24
  "fflate": "^0.8.2",
25
25
  "superjson": "^2.2.6",
26
26
  "zod": "^3.25.76",
27
- "@trackunit/i18n-library-translation": "2.3.1"
27
+ "@trackunit/i18n-library-translation": "2.3.4-alpha-770d9994af7.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "react": "^19.0.0",
@@ -0,0 +1,96 @@
1
+ import { MouseEventHandler, ReactElement, ReactNode } from "react";
2
+ import { CommonProps } from "../../../common/CommonProps";
3
+ import { Refable } from "../../../common/Refable";
4
+ import type { Styleable } from "../../../common/Styleable";
5
+ export interface MenuContentProps extends CommonProps, Styleable, Refable<HTMLDivElement> {
6
+ /**
7
+ * List of menu items to be rendered.
8
+ */
9
+ children: ReactNode;
10
+ /**
11
+ * Optional class names for the inner scrollable list container (`cvaMenuList`).
12
+ * Use this when the outer menu shell and inner list need different layout constraints
13
+ * (for example, filter submenus with a sticky header/body/footer grid).
14
+ */
15
+ listClassName?: string;
16
+ /** Click handler - typically used to trigger close action. when propagation is NOT prevented on menuItem */
17
+ onClick?: MouseEventHandler<HTMLDivElement>;
18
+ /**
19
+ * Enable multi-selection in the menu content.
20
+ *
21
+ * @default false
22
+ */
23
+ isMulti?: boolean;
24
+ /**
25
+ * Array of IDs representing the currently selected items.
26
+ */
27
+ selectedItems?: Array<string>;
28
+ /**
29
+ * Callback triggered when selected items change.
30
+ */
31
+ onSelectionChange?: (selected: Array<string>) => void;
32
+ }
33
+ /**
34
+ * MenuContent (formerly MenuList) is a popover menu that appears above all other content on the page. It offers a
35
+ * list of actions or functions that a user can access by clicking on a trigger, with full keyboard support:
36
+ * roving-tabindex Up/Down navigation (wrapping), Home/End, typeahead, and — via `MenuItem`'s `submenu` prop —
37
+ * nested submenu entry/exit.
38
+ *
39
+ * Typically rendered inside a `Popover` (directly, or as `PopoverContent`'s children), in which case it reads
40
+ * the popover's floating context to power its keyboard navigation. Also works standalone (e.g. inside a
41
+ * `Collapse`, with no ambient `Popover`), falling back to its own local, always-open floating context.
42
+ *
43
+ * **When to use**
44
+ * - Use the MenuContent if you have limited space and need to display overflow actions in a list.
45
+ * - Use the MenuContent for actions that are not essential to completing workflows.
46
+ * - Don't use the MenuContent to display single or multi-select items within form components. For dropdowns within select components, use SelectDropdown (component not available yet).
47
+ *
48
+ * @example MenuContent with action items
49
+ * ```tsx
50
+ * import { MenuContent, MenuItem, MoreMenu, Icon } from "@trackunit/react-components";
51
+ *
52
+ * const ActionsMenu = () => (
53
+ * <MoreMenu>
54
+ * {(close) => (
55
+ * <MenuContent onClick={close}>
56
+ * <MenuItem id="edit" prefix={<Icon name="PencilSquare" size="small" />}>
57
+ * Edit
58
+ * </MenuItem>
59
+ * <MenuItem id="duplicate" prefix={<Icon name="DocumentDuplicate" size="small" />}>
60
+ * Duplicate
61
+ * </MenuItem>
62
+ * <MenuItem id="delete" prefix={<Icon name="Trash" size="small" />} destructive>
63
+ * Delete
64
+ * </MenuItem>
65
+ * </MenuContent>
66
+ * )}
67
+ * </MoreMenu>
68
+ * );
69
+ * ```
70
+ * @example Multi-select MenuContent
71
+ * ```tsx
72
+ * import { MenuContent, MenuItem, MoreMenu } from "@trackunit/react-components";
73
+ * import { useState } from "react";
74
+ *
75
+ * const FilterMenu = () => {
76
+ * const [selected, setSelected] = useState<string[]>(["active"]);
77
+ *
78
+ * return (
79
+ * <MoreMenu label="Filter by status">
80
+ * <MenuContent
81
+ * isMulti
82
+ * selectedItems={selected}
83
+ * onSelectionChange={setSelected}
84
+ * >
85
+ * <MenuItem id="active">Active</MenuItem>
86
+ * <MenuItem id="idle">Idle</MenuItem>
87
+ * <MenuItem id="offline">Offline</MenuItem>
88
+ * </MenuContent>
89
+ * </MoreMenu>
90
+ * );
91
+ * };
92
+ * ```
93
+ * @param {MenuContentProps} props - The props for the MenuContent component
94
+ * @returns {ReactElement} MenuContent component
95
+ */
96
+ export declare const MenuContent: ({ "data-testid": dataTestId, className, listClassName, children, isMulti, selectedItems: controlledSelectedItems, onSelectionChange, style, ref, ...args }: MenuContentProps) => ReactElement;
@@ -4,25 +4,25 @@ import { Refable } from "../../../common/Refable";
4
4
  import type { Styleable } from "../../../common/Styleable";
5
5
  export type MenuDividerProps = CommonProps & Styleable & Refable<HTMLDivElement>;
6
6
  /**
7
- * MenuDivider renders a horizontal line to visually separate groups of items within a MenuList.
7
+ * MenuDivider renders a horizontal line to visually separate groups of items within a MenuContent.
8
8
  *
9
9
  * ### When to use
10
10
  * Use MenuDivider between groups of related `MenuItem` elements to create logical sections within a menu.
11
11
  *
12
12
  * ### When not to use
13
- * Do not use MenuDivider outside of a `MenuList`. For general-purpose dividers, use `Spacer` with `border`.
13
+ * Do not use MenuDivider outside of a `MenuContent`. For general-purpose dividers, use `Spacer` with `border`.
14
14
  *
15
15
  * @example Menu with grouped sections
16
16
  * ```tsx
17
- * import { MenuList, MenuItem, MenuDivider, Icon } from "@trackunit/react-components";
17
+ * import { MenuContent, MenuItem, MenuDivider, Icon } from "@trackunit/react-components";
18
18
  *
19
19
  * const GroupedMenu = () => (
20
- * <MenuList>
20
+ * <MenuContent>
21
21
  * <MenuItem id="edit" label="Edit" prefix={<Icon name="PencilSquare" size="small" />} />
22
22
  * <MenuItem id="duplicate" label="Duplicate" prefix={<Icon name="DocumentDuplicate" size="small" />} />
23
23
  * <MenuDivider />
24
24
  * <MenuItem id="delete" label="Delete" variant="danger" prefix={<Icon name="Trash" size="small" />} />
25
- * </MenuList>
25
+ * </MenuContent>
26
26
  * );
27
27
  * ```
28
28
  * @returns {ReactElement} MenuDivider component
@@ -1,8 +1,9 @@
1
1
  import { VariantProps } from "@trackunit/css-class-variance-utilities";
2
- import { MouseEventHandler, ReactElement, ReactNode } from "react";
2
+ import { FocusEventHandler, MouseEventHandler, PointerEventHandler, ReactElement, ReactNode } from "react";
3
3
  import { CommonProps } from "../../../common/CommonProps";
4
- import type { Styleable } from "../../../common/Styleable";
5
4
  import { Refable } from "../../../common/Refable";
5
+ import type { Styleable } from "../../../common/Styleable";
6
+ import { PopoverSizing } from "../../Popover/PopoverTypes";
6
7
  import { cvaMenuItemStyle } from "./MenuItem.variants";
7
8
  export type MenuItemVariant = "primary" | "danger";
8
9
  export interface MenuItemProps extends CommonProps, Styleable, Refable<HTMLDivElement> {
@@ -85,23 +86,51 @@ export interface MenuItemProps extends CommonProps, Styleable, Refable<HTMLDivEl
85
86
  * @memberof MenuItemProps
86
87
  */
87
88
  fieldSize?: VariantProps<typeof cvaMenuItemStyle>["fieldSize"];
89
+ /**
90
+ * Renders a nested menu that opens when this item is activated (click, `Enter`/`Space`, or
91
+ * `ArrowRight`). When present, `MenuItem` internally wraps itself in a `Popover` joined to the
92
+ * nearest `MenuTree`, and auto-renders a chevron affordance as `suffix` unless one is explicitly
93
+ * provided. `ArrowLeft` inside the submenu closes it and returns focus to this item.
94
+ */
95
+ submenu?: ReactNode;
96
+ /**
97
+ * Size constraints forwarded to the internal `Popover` that renders `submenu`. See `PopoverSizing`.
98
+ * Has no effect when `submenu` is not provided.
99
+ */
100
+ submenuSizing?: PopoverSizing | ((defaultSizing: PopoverSizing) => PopoverSizing);
101
+ /**
102
+ * Forwarded to the root element. Wired in by `MenuContent` to keep real DOM focus in sync with
103
+ * roving-tabindex keyboard navigation -- not typically set directly by consumers.
104
+ */
105
+ onFocus?: FocusEventHandler<HTMLDivElement>;
106
+ /**
107
+ * Forwarded to the root element. Wired in by `MenuContent` so hovering an item synchronizes
108
+ * keyboard-navigation focus -- not typically set directly by consumers.
109
+ */
110
+ onMouseMove?: MouseEventHandler<HTMLDivElement>;
111
+ /**
112
+ * Forwarded to the root element. Wired in by `MenuContent` to clear keyboard-navigation focus
113
+ * when the pointer leaves the list -- not typically set directly by consumers.
114
+ */
115
+ onPointerLeave?: PointerEventHandler<HTMLDivElement>;
88
116
  }
89
117
  /**
90
- * MenuItem represents a single actionable item within a MenuList.
91
- * It supports labels, icons (prefix/suffix), selected and focused states, and danger variants.
118
+ * MenuItem represents a single actionable item within a MenuContent.
119
+ * It supports labels, icons (prefix/suffix), selected and focused states, danger variants, and via the
120
+ * `submenu` prop — nested submenus.
92
121
  *
93
122
  * ### When to use
94
- * Use MenuItem inside a `MenuList` for individual actions (edit, delete, duplicate) or selectable options.
123
+ * Use MenuItem inside a `MenuContent` for individual actions (edit, delete, duplicate) or selectable options.
95
124
  *
96
125
  * ### When not to use
97
- * Do not use MenuItem outside of a `MenuList` context. For standalone clickable items, use `Button` or `ListItem`.
126
+ * Do not use MenuItem outside of a `MenuContent` context. For standalone clickable items, use `Button` or `ListItem`.
98
127
  *
99
128
  * @example MenuItem with icon prefix
100
129
  * ```tsx
101
- * import { MenuList, MenuItem, Icon } from "@trackunit/react-components";
130
+ * import { MenuContent, MenuItem, Icon } from "@trackunit/react-components";
102
131
  *
103
132
  * const ActionMenu = () => (
104
- * <MenuList>
133
+ * <MenuContent>
105
134
  * <MenuItem
106
135
  * id="edit"
107
136
  * label="Edit asset"
@@ -115,10 +144,28 @@ export interface MenuItemProps extends CommonProps, Styleable, Refable<HTMLDivEl
115
144
  * variant="danger"
116
145
  * onClick={() => console.log("Delete clicked")}
117
146
  * />
118
- * </MenuList>
147
+ * </MenuContent>
148
+ * );
149
+ * ```
150
+ * @example MenuItem with a submenu
151
+ * ```tsx
152
+ * import { MenuContent, MenuItem } from "@trackunit/react-components";
153
+ *
154
+ * const StatusMenu = () => (
155
+ * <MenuContent>
156
+ * <MenuItem
157
+ * label="Status"
158
+ * submenu={
159
+ * <MenuContent>
160
+ * <MenuItem id="active" label="Active" />
161
+ * <MenuItem id="idle" label="Idle" />
162
+ * </MenuContent>
163
+ * }
164
+ * />
165
+ * </MenuContent>
119
166
  * );
120
167
  * ```
121
168
  * @param {MenuItemProps} props - The props for the MenuItem component
122
169
  * @returns {ReactElement} MenuItem component
123
170
  */
124
- export declare const MenuItem: ({ className, "data-testid": dataTestId, label, children, selected, focused, prefix, suffix, disabled, onClick, stopPropagation, id, tabIndex, optionLabelDescription, optionPrefix, fieldSize, variant, style, ref, }: MenuItemProps) => ReactElement;
171
+ export declare const MenuItem: ({ className, "data-testid": dataTestId, label, children, selected, focused, prefix, suffix, disabled, onClick, stopPropagation, id, tabIndex, optionLabelDescription, optionPrefix, fieldSize, variant, style, ref, submenu, submenuSizing, onFocus, onMouseMove, onPointerLeave, }: MenuItemProps) => ReactElement;
@@ -43,7 +43,7 @@ export interface MoreMenuProps extends CommonProps, Styleable, Refable<HTMLDivEl
43
43
  }
44
44
  /**
45
45
  * MoreMenu (kebab menu) renders a three-dot button that opens a popover with a list of actions.
46
- * It is typically filled with a MenuList containing MenuItem elements.
46
+ * It is typically filled with a MenuContent containing MenuItem elements.
47
47
  *
48
48
  * ### When to use
49
49
  * Use MoreMenu when you have overflow actions that don't fit in the main UI. Common for row-level actions in tables, card headers, or list items.
@@ -53,30 +53,30 @@ export interface MoreMenuProps extends CommonProps, Styleable, Refable<HTMLDivEl
53
53
  *
54
54
  * @example Action items with render prop — Pass a function as `children` to receive the `close` callback and dismiss the menu after an action.
55
55
  * ```tsx
56
- * import { MoreMenu, MenuList, MenuItem, Icon } from "@trackunit/react-components";
56
+ * import { MoreMenu, MenuContent, MenuItem, Icon } from "@trackunit/react-components";
57
57
  *
58
58
  * const AssetActions = () => (
59
59
  * <MoreMenu>
60
60
  * {(close) => (
61
- * <MenuList onClick={close}>
61
+ * <MenuContent onClick={close}>
62
62
  * <MenuItem id="edit" label="Edit" prefix={<Icon name="PencilSquare" size="small" />} />
63
63
  * <MenuItem id="delete" label="Delete" variant="danger" prefix={<Icon name="Trash" size="small" />} />
64
- * </MenuList>
64
+ * </MenuContent>
65
65
  * )}
66
66
  * </MoreMenu>
67
67
  * );
68
68
  * ```
69
69
  * @example Custom trigger button — Use `customButton` to replace the default kebab icon with any element, like a labeled Button.
70
70
  * ```tsx
71
- * import { MoreMenu, MenuList, MenuItem, Button } from "@trackunit/react-components";
71
+ * import { MoreMenu, MenuContent, MenuItem, Button } from "@trackunit/react-components";
72
72
  *
73
73
  * const CustomTriggerMenu = () => (
74
74
  * <MoreMenu customButton={<Button variant="secondary" size="small">Actions</Button>}>
75
75
  * {(close) => (
76
- * <MenuList onClick={close}>
76
+ * <MenuContent onClick={close}>
77
77
  * <MenuItem id="export" label="Export" />
78
78
  * <MenuItem id="archive" label="Archive" />
79
- * </MenuList>
79
+ * </MenuContent>
80
80
  * )}
81
81
  * </MoreMenu>
82
82
  * );
@@ -0,0 +1,130 @@
1
+ import { FloatingTreeType, OpenChangeReason } from "@floating-ui/react";
2
+ import { ReactElement, ReactNode } from "react";
3
+ /**
4
+ * Emitted on the tree's event emitter whenever a node transitions to open, so `MenuTree` can close
5
+ * that node's siblings. See `usePopover`'s emit side and `MenuTree`'s listener below.
6
+ */
7
+ export declare const MENU_TREE_OPEN_EVENT = "menuopen";
8
+ /**
9
+ * Emitted whenever a node transitions to closed, so siblings can tell when a `reason: "click"` open
10
+ * (see `MenuTreeOpenEvent`) has been dismissed and stop treating that branch as click-pinned. See
11
+ * `usePopover`'s emit side and its `openSiblingPinned` listener.
12
+ */
13
+ export declare const MENU_TREE_CLOSE_EVENT = "menuclose";
14
+ export type MenuTreeOpenEvent = {
15
+ nodeId: string;
16
+ parentId: string | null;
17
+ /**
18
+ * How this node came to open, from Floating UI's `onOpenChange` (`undefined` for a
19
+ * non-interaction-driven open, e.g. the `ArrowRight` keyboard shortcut setting state directly).
20
+ * Siblings use this to tell a deliberate `"click"` open (or any other explicit reason) -- which
21
+ * should not be casually overridden by hovering past it -- apart from a `"hover"` open, which
22
+ * already only happened because the user was passing through the row anyway.
23
+ */
24
+ reason?: OpenChangeReason;
25
+ /**
26
+ * Whether the opening node is itself a delayed-hover submenu row (`MenuItem`'s
27
+ * `hover: { delayed: true }` pattern) -- as opposed to some unrelated `Popover` that merely
28
+ * happens to share a parent in the tree, e.g. a `Tooltip` on the same row or on the tree's own
29
+ * root trigger. `hasOpenSibling`/`openSiblingPinned` tracking (see `usePopover`) only cares about
30
+ * other menu rows, since a stray tooltip opening or closing elsewhere isn't a signal about
31
+ * whether hovering should switch between rows.
32
+ */
33
+ isMenuRow: boolean;
34
+ };
35
+ export type MenuTreeCloseEvent = {
36
+ nodeId: string;
37
+ parentId: string | null;
38
+ /** See `MenuTreeOpenEvent.isMenuRow`. */
39
+ isMenuRow: boolean;
40
+ };
41
+ export type UseMenuTreeType = {
42
+ /**
43
+ * This node's id within the tree. Stable for the lifetime of the component that called the hook.
44
+ */
45
+ nodeId: string;
46
+ /**
47
+ * The nearest ancestor node's id, or `null` at the root of a `MenuTree` (or outside any `MenuTree`).
48
+ */
49
+ parentId: string | null;
50
+ /**
51
+ * Whether this node has an ancestor node, i.e. it is not the root of its `MenuTree`.
52
+ */
53
+ isNested: boolean;
54
+ /**
55
+ * The underlying Floating UI tree, or `null` when called outside a `<MenuTree>`.
56
+ */
57
+ tree: FloatingTreeType | null;
58
+ };
59
+ /**
60
+ * Returns whether a pointer event landed on any `MenuTree` member's trigger or floating surface.
61
+ * Used by nested `Popover`s to avoid treating a click on a sibling row (or the parent panel) as an
62
+ * outside press that dismisses the whole branch.
63
+ */
64
+ export declare const isPressInsideMenuTree: (event: Event, tree: FloatingTreeType) => boolean;
65
+ /**
66
+ * Reads (and joins) the nearest `MenuTree` boundary, if any.
67
+ *
68
+ * Returns explicit tree state so fully custom nested-menu UI can be built without going through
69
+ * `Popover`/`PopoverTrigger`/`PopoverContent`'s implicit prop-cloning. `Popover` is one consumer of
70
+ * this hook, not the only way to participate in a `MenuTree`.
71
+ *
72
+ * Safe to call outside a `<MenuTree>`: `parentId`/`tree` are `null` and `isNested` is `false`.
73
+ *
74
+ * @returns {UseMenuTreeType} Explicit tree state for the calling node
75
+ */
76
+ export declare const useMenuTree: () => UseMenuTreeType;
77
+ /**
78
+ * Opt-in tree-coordination boundary for a family of `Popover`s: sibling-close-on-open, dismiss
79
+ * bubbling (Escape closes one level at a time; outside-press is scoped per node so sibling rows
80
+ * don't collapse the root), and hover safe-polygon between nested rows. Every `Popover` rendered
81
+ * automatically joins it — there is no per-instance flag to set. A `Popover` outside any `MenuTree`
82
+ * is unaffected and behaves exactly as it does today.
83
+ *
84
+ * Only the immediate siblings of a newly-opened node (nodes sharing the same parent) are closed.
85
+ * Closing a node cascades to close its own open subtree as a structural consequence of
86
+ * `PopoverContent` unmounting when its `Popover` closes, not extra logic here.
87
+ *
88
+ * ### When to use
89
+ * Wrap a family of nested `Popover`s (e.g. a menu bar or a filter bar's flyouts) that should
90
+ * coordinate open/close state with each other.
91
+ *
92
+ * ### When not to use
93
+ * Do not wrap unrelated `Popover`s that merely happen to render inside each other's React tree —
94
+ * they will incorrectly join tree coordination and can be closed by sibling opens.
95
+ *
96
+ * ### Click-opened rows vs. sibling hovers
97
+ * `hover: { delayed: true }` on a nested `Popover` (used below, instead of a plain `hover: true`)
98
+ * keeps a row opened by a click open while the cursor passes over its siblings, matching what
99
+ * `MenuItem`'s `submenu` prop already does internally. See the "Advanced" section of the
100
+ * `Components/Menu/Nested Menu` docs page for the full explanation.
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * import { MenuTree, Popover, PopoverContent, PopoverTrigger } from "@trackunit/react-components";
105
+ *
106
+ * const NestedMenu = () => (
107
+ * <MenuTree>
108
+ * <Popover placement="bottom-start">
109
+ * <PopoverTrigger>Open</PopoverTrigger>
110
+ * <PopoverContent>
111
+ * <Popover activation={{ click: true, hover: { delayed: true } }} placement="right-start">
112
+ * <PopoverTrigger>Row A</PopoverTrigger>
113
+ * <PopoverContent>Row A's flyout</PopoverContent>
114
+ * </Popover>
115
+ * <Popover activation={{ click: true, hover: { delayed: true } }} placement="right-start">
116
+ * <PopoverTrigger>Row B</PopoverTrigger>
117
+ * <PopoverContent>Row B's flyout</PopoverContent>
118
+ * </Popover>
119
+ * </PopoverContent>
120
+ * </Popover>
121
+ * </MenuTree>
122
+ * );
123
+ * ```
124
+ * @param {object} props The props for MenuTree
125
+ * @param {ReactNode} props.children The `Popover`s (and anything else) to coordinate
126
+ * @returns {ReactElement} A `FloatingTree` provider wired up for sibling-close coordination
127
+ */
128
+ export declare const MenuTree: ({ children }: {
129
+ children: ReactNode;
130
+ }) => ReactElement;
@@ -1,6 +1,14 @@
1
1
  import { ReactElement, ReactNode } from "react";
2
2
  import { PopoverPlacement, PopoverProps, UsePopoverType } from "./PopoverTypes";
3
3
  export type ContextType = UsePopoverType | null;
4
+ /**
5
+ * Like `usePopoverContext`, but returns `null` instead of throwing when called outside a `<Popover />`.
6
+ * Used by components (e.g. `MenuContent`) that support being rendered either inside a `Popover` or fully
7
+ * standalone.
8
+ *
9
+ * @returns {ContextType} The popover context, or `null` if there is no ancestor `Popover`
10
+ */
11
+ export declare const useOptionalPopoverContext: () => ContextType;
4
12
  /**
5
13
  * A hook to get the popover context.
6
14
  * It should only be used by the Popover components.
@@ -44,18 +44,23 @@ export interface PopoverContentProps extends Omit<HTMLProps<HTMLDivElement>, "ch
44
44
  */
45
45
  initialFocus?: number | MutableRefObject<HTMLElement | null>;
46
46
  /**
47
- * Controls whether focus is returned to the previously focused element when the popover closes.
47
+ * Controls whether -- and where -- focus is returned when the popover closes.
48
48
  *
49
- * Floating UI tracks the most recently focused tabbable element **across the whole app** (not
50
- * just this popover instance) as the return target, so disabling this is the only reliable way
51
- * to guarantee a popover never affects focus on close.
52
- *
53
- * Set to `false` for non-modal, non-interactive overlays (like `Tooltip`) that should never
54
- * manage focus in either direction.
49
+ * - **boolean** (default `true`): Floating UI tracks the most recently focused tabbable element
50
+ * **across the whole app** (not just this popover instance) as the return target. `false` is
51
+ * the only reliable way to guarantee a popover never affects focus on close -- use it for
52
+ * non-modal, non-interactive overlays (like `Tooltip`) that should never manage focus in
53
+ * either direction.
54
+ * - **ref**: returns focus to this specific element instead of Floating UI's own app-wide
55
+ * tracking, checked for visibility right at close time -- if the target has scrolled out of
56
+ * its own scrollable ancestor (or is otherwise clipped/off-viewport), focus automatically
57
+ * falls back to Floating UI's own hidden, off-screen element instead (which has no scroll
58
+ * side effect) rather than focusing something the user can't see. No need to null the ref out
59
+ * by hand for a scrolled-out close -- just keep it pointed at the normal target.
55
60
  *
56
61
  * @default true
57
62
  */
58
- returnFocus?: boolean;
63
+ returnFocus?: boolean | MutableRefObject<HTMLElement | null>;
59
64
  /**
60
65
  * A ref for the component
61
66
  */
@@ -0,0 +1,56 @@
1
+ import { FloatingTreeType } from "@floating-ui/react";
2
+ /**
3
+ * The parent Popover's own floating panel element, found via the `MenuTree`'s Floating UI tree
4
+ * bookkeeping rather than by walking the DOM -- this is the exact element `PopoverContent` set as
5
+ * `context.refs.floating.current` when it rendered, so its rect is the parent panel's true visual
6
+ * boundary (border, shadow, and any internal scrollbar all already baked in), regardless of how
7
+ * deeply the current node's own trigger row is nested inside it.
8
+ *
9
+ * @param tree The `MenuTree`'s Floating UI tree, or `null` outside a `MenuTree`
10
+ * @param parentId The nearest ancestor node's id from `useMenuTree`, or `null` at the tree's root
11
+ * @returns {HTMLElement | null} The parent's floating panel element, or `null` if there isn't one
12
+ */
13
+ export declare const getParentPanelElement: (tree: FloatingTreeType | null, parentId: string | null) => HTMLElement | null;
14
+ /**
15
+ * Extra horizontal offset (in pixels) a `right`/`left`-placed submenu needs so it clears its parent
16
+ * Popover panel's true edge, rather than sitting flush with (or overlapping) whatever is at the edge
17
+ * of the specific row that triggered it.
18
+ *
19
+ * A submenu's `elements.reference` is the trigger row it opened from, not the parent panel itself --
20
+ * and that row's own edge is inset from the panel's true visual boundary by the panel's padding and,
21
+ * when the panel's list happens to be scrolling, by its scrollbar too. Measuring against the parent
22
+ * panel's actual rendered edge (via `getParentPanelElement` above) gets both of those for free and
23
+ * keeps the visual gap identical whether or not the parent list happens to be scrolling -- rather
24
+ * than re-deriving "how much padding/scrollbar is in the way" and landing on a different visual gap
25
+ * depending on the parent's internal layout.
26
+ *
27
+ * @param parentPanelElement The parent Popover's own floating panel element
28
+ * @param referenceElement The trigger element the submenu is anchored to
29
+ * @param resolvedPlacement Floating UI's resolved placement (e.g. `"right-start"`)
30
+ * @returns {number} Additional pixels to add to the base `offset()` `mainAxis` value
31
+ */
32
+ export declare const getNestedSubmenuClearance: (parentPanelElement: HTMLElement, referenceElement: HTMLElement, resolvedPlacement: string) => number;
33
+ type Rect = {
34
+ x: number;
35
+ y: number;
36
+ width: number;
37
+ height: number;
38
+ };
39
+ type YToClearReferenceParams = {
40
+ floatingX: number;
41
+ floatingY: number;
42
+ floatingWidth: number;
43
+ floatingHeight: number;
44
+ reference: Rect;
45
+ gap: number;
46
+ viewportHeight: number;
47
+ padding: number;
48
+ };
49
+ /**
50
+ * When a nested submenu has shifted horizontally over its parent (cascade-with-overlap), it must
51
+ * not cover the trigger row that opened it. Returns a new `y` that places the floating panel
52
+ * entirely below or above the reference -- whichever side has more room -- or `null` when the
53
+ * panels do not overlap and no adjustment is needed.
54
+ */
55
+ export declare const getYToClearReference: ({ floatingX, floatingY, floatingWidth, floatingHeight, reference, gap, viewportHeight, padding, }: YToClearReferenceParams) => number | null;
56
+ export {};