@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.
- package/index.cjs.js +1237 -133
- package/index.esm.js +1237 -136
- package/migrations/v3-0-0/menulist-rename-to-menucontent.js +105 -0
- package/migrations.json +5 -0
- package/package.json +6 -6
- package/src/components/Menu/MenuContent/MenuContent.d.ts +96 -0
- package/src/components/Menu/MenuDivider/MenuDivider.d.ts +5 -5
- package/src/components/Menu/MenuItem/MenuItem.d.ts +57 -10
- package/src/components/Menu/MoreMenu/MoreMenu.d.ts +7 -7
- package/src/components/Popover/MenuTree.d.ts +130 -0
- package/src/components/Popover/Popover.d.ts +8 -0
- package/src/components/Popover/PopoverContent.d.ts +13 -8
- package/src/components/Popover/PopoverNestedClearance.d.ts +56 -0
- package/src/components/Popover/PopoverSizing.d.ts +54 -2
- package/src/components/Popover/PopoverTypes.d.ts +21 -0
- package/src/components/Popover/hoverScrollGuard.d.ts +85 -0
- package/src/components/Popover/isElementVisible.d.ts +15 -0
- package/src/components/Popover/useHoverScrollGuard.d.ts +54 -0
- package/src/components/Popover/usePopover.d.ts +1 -1
- package/src/components/Sidebar/Sidebar.d.ts +3 -3
- package/src/hooks/getScrollbarWidth.d.ts +13 -0
- package/src/index.d.ts +4 -2
- package/src/components/Menu/MenuList/MenuList.d.ts +0 -83
- /package/src/components/Menu/{MenuList/MenuList.variants.d.ts → MenuContent/MenuContent.variants.d.ts} +0 -0
|
@@ -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.
|
|
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.
|
|
18
|
-
"@trackunit/css-class-variance-utilities": "1.14.
|
|
19
|
-
"@trackunit/shared-utils": "1.16.
|
|
20
|
-
"@trackunit/ui-icons": "1.14.
|
|
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.
|
|
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
|
|
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 `
|
|
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 {
|
|
17
|
+
* import { MenuContent, MenuItem, MenuDivider, Icon } from "@trackunit/react-components";
|
|
18
18
|
*
|
|
19
19
|
* const GroupedMenu = () => (
|
|
20
|
-
* <
|
|
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
|
-
* </
|
|
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
|
|
91
|
-
* It supports labels, icons (prefix/suffix), selected and focused states, and
|
|
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 `
|
|
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 `
|
|
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 {
|
|
130
|
+
* import { MenuContent, MenuItem, Icon } from "@trackunit/react-components";
|
|
102
131
|
*
|
|
103
132
|
* const ActionMenu = () => (
|
|
104
|
-
* <
|
|
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
|
-
* </
|
|
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
|
|
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,
|
|
56
|
+
* import { MoreMenu, MenuContent, MenuItem, Icon } from "@trackunit/react-components";
|
|
57
57
|
*
|
|
58
58
|
* const AssetActions = () => (
|
|
59
59
|
* <MoreMenu>
|
|
60
60
|
* {(close) => (
|
|
61
|
-
* <
|
|
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
|
-
* </
|
|
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,
|
|
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
|
-
* <
|
|
76
|
+
* <MenuContent onClick={close}>
|
|
77
77
|
* <MenuItem id="export" label="Export" />
|
|
78
78
|
* <MenuItem id="archive" label="Archive" />
|
|
79
|
-
* </
|
|
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
|
|
47
|
+
* Controls whether -- and where -- focus is returned when the popover closes.
|
|
48
48
|
*
|
|
49
|
-
* Floating UI tracks the most recently focused tabbable element
|
|
50
|
-
* just this popover instance) as the return target
|
|
51
|
-
* to guarantee a popover never affects focus on close
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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 {};
|