najm-kit 2.11.7 → 2.11.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.11.8 - 2026-08-28
4
+
5
+ - Fixed `NContextMenu` leaving keyboard focus on its opener after the menu
6
+ appeared. The first enabled action now receives focus, disabled actions are
7
+ skipped, Arrow Up/Down wrap, Home/End jump to the bounds, Tab dismisses the
8
+ menu, and Escape restores focus to the opener. Focused actions also receive
9
+ the same visual treatment as hovered actions.
10
+
3
11
  ## 2.11.7 - 2026-08-17
4
12
 
5
13
  - Fixed date, combobox, and multiselect `FormInput` controls reaching the
package/dist/index.mjs CHANGED
@@ -9879,6 +9879,7 @@ function NViewBody({ children, className, variant = "table" }) {
9879
9879
  function NContextMenu({ x, y, items, onAction, onClose, className }) {
9880
9880
  const [openSub, setOpenSub] = useState(null);
9881
9881
  const menuRef = useRef(null);
9882
+ const returnFocusRef = useRef(null);
9882
9883
  const [pos, setPos] = useState({ left: x, top: y });
9883
9884
  useLayoutEffect(() => {
9884
9885
  if (!menuRef.current) return;
@@ -9895,13 +9896,54 @@ function NContextMenu({ x, y, items, onAction, onClose, className }) {
9895
9896
  (prev) => prev.left === next.left && prev.top === next.top ? prev : next
9896
9897
  );
9897
9898
  }, [x, y]);
9899
+ useLayoutEffect(() => {
9900
+ const activeElement = document.activeElement;
9901
+ if (activeElement instanceof HTMLElement && !menuRef.current?.contains(activeElement)) {
9902
+ returnFocusRef.current = activeElement;
9903
+ }
9904
+ menuRef.current?.querySelector("[data-context-menu-item]:not(:disabled)")?.focus();
9905
+ }, []);
9898
9906
  useEffect(() => {
9899
9907
  const onKey = (e) => {
9900
- if (e.key === "Escape") onClose();
9908
+ if (e.key !== "Escape") return;
9909
+ e.preventDefault();
9910
+ onClose();
9911
+ queueMicrotask(() => returnFocusRef.current?.focus());
9901
9912
  };
9902
9913
  window.addEventListener("keydown", onKey);
9903
9914
  return () => window.removeEventListener("keydown", onKey);
9904
9915
  }, [onClose]);
9916
+ const onMenuKeyDown = (e) => {
9917
+ const enabledItems = Array.from(
9918
+ menuRef.current?.querySelectorAll(
9919
+ "[data-context-menu-item]:not(:disabled)"
9920
+ ) ?? []
9921
+ );
9922
+ if (!enabledItems.length) return;
9923
+ const currentIndex = enabledItems.indexOf(document.activeElement);
9924
+ let nextIndex = null;
9925
+ switch (e.key) {
9926
+ case "ArrowDown":
9927
+ nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % enabledItems.length;
9928
+ break;
9929
+ case "ArrowUp":
9930
+ nextIndex = currentIndex < 0 ? enabledItems.length - 1 : (currentIndex - 1 + enabledItems.length) % enabledItems.length;
9931
+ break;
9932
+ case "Home":
9933
+ nextIndex = 0;
9934
+ break;
9935
+ case "End":
9936
+ nextIndex = enabledItems.length - 1;
9937
+ break;
9938
+ case "Tab":
9939
+ onClose();
9940
+ return;
9941
+ default:
9942
+ return;
9943
+ }
9944
+ e.preventDefault();
9945
+ enabledItems[nextIndex].focus();
9946
+ };
9905
9947
  useEffect(() => {
9906
9948
  const isInsideMenu = (target) => target instanceof Node && !!menuRef.current?.contains(target);
9907
9949
  const onMouseDown = (e) => {
@@ -9930,6 +9972,7 @@ function NContextMenu({ x, y, items, onAction, onClose, className }) {
9930
9972
  ),
9931
9973
  style: { left: pos.left, top: pos.top },
9932
9974
  onContextMenu: (e) => e.preventDefault(),
9975
+ onKeyDown: onMenuKeyDown,
9933
9976
  children: items.map((item) => {
9934
9977
  const Icon2 = item.icon;
9935
9978
  const hasSub = !!item.submenu?.length;
@@ -9947,6 +9990,8 @@ function NContextMenu({ x, y, items, onAction, onClose, className }) {
9947
9990
  {
9948
9991
  type: "button",
9949
9992
  role: "menuitem",
9993
+ tabIndex: -1,
9994
+ "data-context-menu-item": true,
9950
9995
  disabled: item.disabled,
9951
9996
  onClick: () => {
9952
9997
  if (hasSub) return;
@@ -9954,8 +9999,8 @@ function NContextMenu({ x, y, items, onAction, onClose, className }) {
9954
9999
  onClose();
9955
10000
  },
9956
10001
  className: cn(
9957
- "flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-1.5 text-xs hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-40",
9958
- item.danger && "text-destructive hover:text-destructive"
10002
+ "flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-1.5 text-xs hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-40",
10003
+ item.danger && "text-destructive hover:text-destructive focus:text-destructive"
9959
10004
  ),
9960
10005
  children: [
9961
10006
  /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
@@ -9974,14 +10019,15 @@ function NContextMenu({ x, y, items, onAction, onClose, className }) {
9974
10019
  {
9975
10020
  type: "button",
9976
10021
  role: "menuitem",
10022
+ tabIndex: -1,
9977
10023
  disabled: sub.disabled,
9978
10024
  onClick: () => {
9979
10025
  onAction(sub.id);
9980
10026
  onClose();
9981
10027
  },
9982
10028
  className: cn(
9983
- "flex w-full cursor-pointer items-center gap-2 px-3 py-1.5 text-xs hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:opacity-40",
9984
- sub.danger && "text-destructive hover:text-destructive"
10029
+ "flex w-full cursor-pointer items-center gap-2 px-3 py-1.5 text-xs hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-40",
10030
+ sub.danger && "text-destructive hover:text-destructive focus:text-destructive"
9985
10031
  ),
9986
10032
  children: [
9987
10033
  SubIcon && /* @__PURE__ */ jsx(SubIcon, { size: 14 }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-kit",
3
- "version": "2.11.7",
3
+ "version": "2.11.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Reusable React UI component package for Najm framework",