@crisp-ui-kit/crisp 0.13.1 → 0.15.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.
|
@@ -36,23 +36,47 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.ContextMenu = ContextMenu;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const React = __importStar(require("react"));
|
|
39
|
+
const menu_1 = require("../menu");
|
|
39
40
|
const contextmenu_recipe_1 = require("./contextmenu.recipe");
|
|
40
41
|
/**
|
|
41
42
|
* A right-click context menu — the popover surface positioned at the cursor.
|
|
42
43
|
* Wrap the target; right-clicking it opens the menu (flipping at the viewport
|
|
43
44
|
* edges), closing on select, outside pointer-down, and Escape.
|
|
44
45
|
*/
|
|
45
|
-
function ContextMenu({ children, items, className }) {
|
|
46
|
+
function ContextMenu({ children, items, menuLabel = "Context menu", className, }) {
|
|
46
47
|
const rootRef = React.useRef(null);
|
|
48
|
+
const menuRef = React.useRef(null);
|
|
49
|
+
const returnFocusTo = React.useRef(null);
|
|
47
50
|
const [pos, setPos] = React.useState(null);
|
|
48
51
|
const open = (e) => {
|
|
49
52
|
e.preventDefault();
|
|
53
|
+
// where focus goes back to on close. The menu takes focus to be operable
|
|
54
|
+
// at all, so something has to remember what had it.
|
|
55
|
+
returnFocusTo.current = document.activeElement;
|
|
50
56
|
// flip so the menu stays on-screen near the right/bottom edges
|
|
51
57
|
const x = Math.min(e.clientX, window.innerWidth - 220);
|
|
52
58
|
const y = Math.min(e.clientY, window.innerHeight - 12 - items.length * 34);
|
|
53
59
|
setPos({ x: Math.max(8, x), y: Math.max(8, y) });
|
|
54
60
|
};
|
|
55
|
-
const close = React.useCallback(() =>
|
|
61
|
+
const close = React.useCallback((restoreFocus = false) => {
|
|
62
|
+
setPos(null);
|
|
63
|
+
if (restoreFocus)
|
|
64
|
+
returnFocusTo.current?.focus?.();
|
|
65
|
+
returnFocusTo.current = null;
|
|
66
|
+
}, []);
|
|
67
|
+
// `role="menu"` puts a screen reader in application mode, where the arrow
|
|
68
|
+
// keys are the only navigation there is (#188). The model itself is crisp's
|
|
69
|
+
// shared one (#192) — Menu and all three RecordsTable surfaces run the same
|
|
70
|
+
// hook, and a fourth hand-written copy is how typeahead, menuitemcheckbox
|
|
71
|
+
// and the `[data-disabled]` spelling get lost one surface at a time.
|
|
72
|
+
const { onKeyDown: onMenuKeyDown } = (0, menu_1.useMenuKeyboard)({
|
|
73
|
+
open: !!pos,
|
|
74
|
+
close: () => close(true),
|
|
75
|
+
// Tab: the browser is already moving focus somewhere, so pulling it back
|
|
76
|
+
// to the trigger first would land the user in the wrong place
|
|
77
|
+
dismiss: () => close(),
|
|
78
|
+
contentRef: menuRef,
|
|
79
|
+
});
|
|
56
80
|
React.useEffect(() => {
|
|
57
81
|
if (!pos)
|
|
58
82
|
return;
|
|
@@ -60,7 +84,7 @@ function ContextMenu({ children, items, className }) {
|
|
|
60
84
|
if (!rootRef.current?.contains(e.target))
|
|
61
85
|
close();
|
|
62
86
|
};
|
|
63
|
-
const onKey = (e) => e.key === "Escape" && close();
|
|
87
|
+
const onKey = (e) => e.key === "Escape" && close(true);
|
|
64
88
|
document.addEventListener("pointerdown", onDown);
|
|
65
89
|
document.addEventListener("keydown", onKey);
|
|
66
90
|
return () => {
|
|
@@ -68,11 +92,11 @@ function ContextMenu({ children, items, className }) {
|
|
|
68
92
|
document.removeEventListener("keydown", onKey);
|
|
69
93
|
};
|
|
70
94
|
}, [pos, close]);
|
|
71
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: [(0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }), pos && ((0, jsx_runtime_1.jsx)("div", { className: "contextmenu-content", role: "menu", style: { left: pos.x, top: pos.y }, children: items.map((item, i) => item.separator ? ((0, jsx_runtime_1.jsx)("hr", { className: "contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
|
|
95
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: [(0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }), pos && ((0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? ((0, jsx_runtime_1.jsx)("hr", { className: "contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
|
|
72
96
|
? "contextmenu-item contextmenu-item--danger"
|
|
73
|
-
: "contextmenu-item", onClick: () => {
|
|
97
|
+
: "contextmenu-item", tabIndex: -1, onClick: () => {
|
|
74
98
|
item.onSelect?.();
|
|
75
|
-
close();
|
|
99
|
+
close(true);
|
|
76
100
|
}, children: [item.icon, item.label, item.shortcut && ((0, jsx_runtime_1.jsx)("span", { className: "contextmenu-shortcut", children: item.shortcut }))] }, i))) }))] }));
|
|
77
101
|
}
|
|
78
102
|
ContextMenu.displayName = "ContextMenu";
|
|
@@ -1,22 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { useMenuKeyboard } from "../menu/index.js";
|
|
3
4
|
import { contextmenuRecipe } from "./contextmenu.recipe.js";
|
|
4
5
|
/**
|
|
5
6
|
* A right-click context menu — the popover surface positioned at the cursor.
|
|
6
7
|
* Wrap the target; right-clicking it opens the menu (flipping at the viewport
|
|
7
8
|
* edges), closing on select, outside pointer-down, and Escape.
|
|
8
9
|
*/
|
|
9
|
-
export function ContextMenu({ children, items, className }) {
|
|
10
|
+
export function ContextMenu({ children, items, menuLabel = "Context menu", className, }) {
|
|
10
11
|
const rootRef = React.useRef(null);
|
|
12
|
+
const menuRef = React.useRef(null);
|
|
13
|
+
const returnFocusTo = React.useRef(null);
|
|
11
14
|
const [pos, setPos] = React.useState(null);
|
|
12
15
|
const open = (e) => {
|
|
13
16
|
e.preventDefault();
|
|
17
|
+
// where focus goes back to on close. The menu takes focus to be operable
|
|
18
|
+
// at all, so something has to remember what had it.
|
|
19
|
+
returnFocusTo.current = document.activeElement;
|
|
14
20
|
// flip so the menu stays on-screen near the right/bottom edges
|
|
15
21
|
const x = Math.min(e.clientX, window.innerWidth - 220);
|
|
16
22
|
const y = Math.min(e.clientY, window.innerHeight - 12 - items.length * 34);
|
|
17
23
|
setPos({ x: Math.max(8, x), y: Math.max(8, y) });
|
|
18
24
|
};
|
|
19
|
-
const close = React.useCallback(() =>
|
|
25
|
+
const close = React.useCallback((restoreFocus = false) => {
|
|
26
|
+
setPos(null);
|
|
27
|
+
if (restoreFocus)
|
|
28
|
+
returnFocusTo.current?.focus?.();
|
|
29
|
+
returnFocusTo.current = null;
|
|
30
|
+
}, []);
|
|
31
|
+
// `role="menu"` puts a screen reader in application mode, where the arrow
|
|
32
|
+
// keys are the only navigation there is (#188). The model itself is crisp's
|
|
33
|
+
// shared one (#192) — Menu and all three RecordsTable surfaces run the same
|
|
34
|
+
// hook, and a fourth hand-written copy is how typeahead, menuitemcheckbox
|
|
35
|
+
// and the `[data-disabled]` spelling get lost one surface at a time.
|
|
36
|
+
const { onKeyDown: onMenuKeyDown } = useMenuKeyboard({
|
|
37
|
+
open: !!pos,
|
|
38
|
+
close: () => close(true),
|
|
39
|
+
// Tab: the browser is already moving focus somewhere, so pulling it back
|
|
40
|
+
// to the trigger first would land the user in the wrong place
|
|
41
|
+
dismiss: () => close(),
|
|
42
|
+
contentRef: menuRef,
|
|
43
|
+
});
|
|
20
44
|
React.useEffect(() => {
|
|
21
45
|
if (!pos)
|
|
22
46
|
return;
|
|
@@ -24,7 +48,7 @@ export function ContextMenu({ children, items, className }) {
|
|
|
24
48
|
if (!rootRef.current?.contains(e.target))
|
|
25
49
|
close();
|
|
26
50
|
};
|
|
27
|
-
const onKey = (e) => e.key === "Escape" && close();
|
|
51
|
+
const onKey = (e) => e.key === "Escape" && close(true);
|
|
28
52
|
document.addEventListener("pointerdown", onDown);
|
|
29
53
|
document.addEventListener("keydown", onKey);
|
|
30
54
|
return () => {
|
|
@@ -32,11 +56,11 @@ export function ContextMenu({ children, items, className }) {
|
|
|
32
56
|
document.removeEventListener("keydown", onKey);
|
|
33
57
|
};
|
|
34
58
|
}, [pos, close]);
|
|
35
|
-
return (_jsxs("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: [_jsx("div", { onContextMenu: open, children: children }), pos && (_jsx("div", { className: "contextmenu-content", role: "menu", style: { left: pos.x, top: pos.y }, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
|
|
59
|
+
return (_jsxs("div", { ref: rootRef, className: contextmenuRecipe({ className }), children: [_jsx("div", { onContextMenu: open, children: children }), pos && (_jsx("div", { ref: menuRef, className: "contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? (_jsx("hr", { className: "contextmenu-sep" }, i)) : (_jsxs("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
|
|
36
60
|
? "contextmenu-item contextmenu-item--danger"
|
|
37
|
-
: "contextmenu-item", onClick: () => {
|
|
61
|
+
: "contextmenu-item", tabIndex: -1, onClick: () => {
|
|
38
62
|
item.onSelect?.();
|
|
39
|
-
close();
|
|
63
|
+
close(true);
|
|
40
64
|
}, children: [item.icon, item.label, item.shortcut && (_jsx("span", { className: "contextmenu-shortcut", children: item.shortcut }))] }, i))) }))] }));
|
|
41
65
|
}
|
|
42
66
|
ContextMenu.displayName = "ContextMenu";
|
|
@@ -14,6 +14,8 @@ export type ContextMenuProps = {
|
|
|
14
14
|
/** The area that opens the menu on right-click. */
|
|
15
15
|
children: React.ReactNode;
|
|
16
16
|
items: ContextMenuOption[];
|
|
17
|
+
/** Accessible name for the menu surface. */
|
|
18
|
+
menuLabel?: string;
|
|
17
19
|
className?: string;
|
|
18
20
|
};
|
|
19
21
|
/**
|
|
@@ -21,7 +23,7 @@ export type ContextMenuProps = {
|
|
|
21
23
|
* Wrap the target; right-clicking it opens the menu (flipping at the viewport
|
|
22
24
|
* edges), closing on select, outside pointer-down, and Escape.
|
|
23
25
|
*/
|
|
24
|
-
export declare function ContextMenu({ children, items, className }: ContextMenuProps): React.JSX.Element;
|
|
26
|
+
export declare function ContextMenu({ children, items, menuLabel, className, }: ContextMenuProps): React.JSX.Element;
|
|
25
27
|
export declare namespace ContextMenu {
|
|
26
28
|
var displayName: string;
|
|
27
29
|
}
|