@crisp-ui-kit/crisp 0.14.0 → 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,6 +36,7 @@ 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.
@@ -63,16 +64,19 @@ function ContextMenu({ children, items, menuLabel = "Context menu", className, }
63
64
  returnFocusTo.current?.focus?.();
64
65
  returnFocusTo.current = null;
65
66
  }, []);
66
- /** The items a roving focus may land on: not separators, not disabled. */
67
- const enabled = React.useCallback(() => Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]:not([disabled])') ?? []), []);
68
67
  // `role="menu"` puts a screen reader in application mode, where the arrow
69
- // keys are the only navigation there is — so opening has to put focus on an
70
- // item, or the role is a promise the component does not keep (#188).
71
- React.useEffect(() => {
72
- if (!pos)
73
- return;
74
- enabled()[0]?.focus();
75
- }, [pos, enabled]);
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
+ });
76
80
  React.useEffect(() => {
77
81
  if (!pos)
78
82
  return;
@@ -88,29 +92,6 @@ function ContextMenu({ children, items, menuLabel = "Context menu", className, }
88
92
  document.removeEventListener("keydown", onKey);
89
93
  };
90
94
  }, [pos, close]);
91
- const onMenuKeyDown = (e) => {
92
- // a menu is not a focus trap: Tab leaves, and leaving means closing
93
- if (e.key === "Tab") {
94
- close(true);
95
- return;
96
- }
97
- const step = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0;
98
- const items = enabled();
99
- if (!items.length)
100
- return;
101
- const at = items.indexOf(document.activeElement);
102
- let next = null;
103
- if (step)
104
- next = (at + step + items.length) % items.length;
105
- else if (e.key === "Home")
106
- next = 0;
107
- else if (e.key === "End")
108
- next = items.length - 1;
109
- if (next === null)
110
- return;
111
- e.preventDefault();
112
- items[next]?.focus();
113
- };
114
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
115
96
  ? "contextmenu-item contextmenu-item--danger"
116
97
  : "contextmenu-item", tabIndex: -1, onClick: () => {
@@ -1,5 +1,6 @@
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.
@@ -27,16 +28,19 @@ export function ContextMenu({ children, items, menuLabel = "Context menu", class
27
28
  returnFocusTo.current?.focus?.();
28
29
  returnFocusTo.current = null;
29
30
  }, []);
30
- /** The items a roving focus may land on: not separators, not disabled. */
31
- const enabled = React.useCallback(() => Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]:not([disabled])') ?? []), []);
32
31
  // `role="menu"` puts a screen reader in application mode, where the arrow
33
- // keys are the only navigation there is — so opening has to put focus on an
34
- // item, or the role is a promise the component does not keep (#188).
35
- React.useEffect(() => {
36
- if (!pos)
37
- return;
38
- enabled()[0]?.focus();
39
- }, [pos, enabled]);
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
+ });
40
44
  React.useEffect(() => {
41
45
  if (!pos)
42
46
  return;
@@ -52,29 +56,6 @@ export function ContextMenu({ children, items, menuLabel = "Context menu", class
52
56
  document.removeEventListener("keydown", onKey);
53
57
  };
54
58
  }, [pos, close]);
55
- const onMenuKeyDown = (e) => {
56
- // a menu is not a focus trap: Tab leaves, and leaving means closing
57
- if (e.key === "Tab") {
58
- close(true);
59
- return;
60
- }
61
- const step = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0;
62
- const items = enabled();
63
- if (!items.length)
64
- return;
65
- const at = items.indexOf(document.activeElement);
66
- let next = null;
67
- if (step)
68
- next = (at + step + items.length) % items.length;
69
- else if (e.key === "Home")
70
- next = 0;
71
- else if (e.key === "End")
72
- next = items.length - 1;
73
- if (next === null)
74
- return;
75
- e.preventDefault();
76
- items[next]?.focus();
77
- };
78
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
79
60
  ? "contextmenu-item contextmenu-item--danger"
80
61
  : "contextmenu-item", tabIndex: -1, onClick: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.14.0",
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",