@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(() => setPos(null), []);
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(() => setPos(null), []);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",