najm-kit 2.11.7 → 2.11.9

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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.11.9 - 2026-08-30
4
+
5
+ - Fixed card grids that paginate client-side rendering their entire dataset on
6
+ one page. `useTable` pinned an uncontrolled paged card list to `data.length`,
7
+ a branch that predates the container measurement added in 2.2.0 and was never
8
+ reconciled with it: `calculatedCardPageSize` was measured and published, but
9
+ only ever applied to a server-paginated grid. A 500-row list rendered 500
10
+ cards on page one, reported a single page, and showed 500 as the current
11
+ Rows/page value. The measured size is now applied to an uncontrolled card grid
12
+ too, through the guards that already keep a server-paginated one stable —
13
+ geometry bucketing, the per-container report budget, and the deferral until
14
+ real cards have been measured. That loop is a property of the measurement
15
+ rather than of fetching: page size decides which cards render, and rendered
16
+ cards are what card height is measured from. Rendering every supplied row
17
+ stays the behavior wherever there is nothing to measure against, so
18
+ `dynamicHeight={false}` and a container that has not yet reported a layout are
19
+ unchanged, and a controlled `pagination` prop or an explicit Rows/page choice
20
+ still wins.
21
+
22
+ ## 2.11.8 - 2026-08-28
23
+
24
+ - Fixed `NContextMenu` leaving keyboard focus on its opener after the menu
25
+ appeared. The first enabled action now receives focus, disabled actions are
26
+ skipped, Arrow Up/Down wrap, Home/End jump to the bounds, Tab dismisses the
27
+ menu, and Escape restores focus to the opener. Focused actions also receive
28
+ the same visual treatment as hovered actions.
29
+
3
30
  ## 2.11.7 - 2026-08-17
4
31
 
5
32
  - 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 }),
@@ -12161,15 +12207,17 @@ function useTable(effectiveViewModeOverride) {
12161
12207
  useLayoutEffect(() => {
12162
12208
  if (manualPagination || isPaginationControlled || isPageSizeUserSelected) return;
12163
12209
  if (dynamicHeight && renderedMode === "table") table.setPageSize(calculatedPageSize);
12164
- if (viewMode === "cards" && cardPagination.mode === "paged") {
12210
+ const cardsAreUnmeasured = !dynamicHeight || !hasMeasuredLayout;
12211
+ if (viewMode === "cards" && cardPagination.mode === "paged" && cardsAreUnmeasured) {
12165
12212
  table.setPageSize(data.length || 9999);
12166
12213
  }
12167
- }, [calculatedPageSize, dynamicHeight, renderedMode, viewMode, cardPagination.mode, table, data.length, manualPagination, isPaginationControlled, isPageSizeUserSelected]);
12214
+ }, [calculatedPageSize, dynamicHeight, hasMeasuredLayout, renderedMode, viewMode, cardPagination.mode, table, data.length, manualPagination, isPaginationControlled, isPageSizeUserSelected]);
12168
12215
  const dynamicPageSizeTarget = renderedMode === "cards" ? calculatedCardPageSize : calculatedPageSize;
12169
12216
  const geometryKey = `${Math.round(measuredBodyWidth / GEOMETRY_BUCKET_PX)}x${Math.round(measuredBodyHeight / GEOMETRY_BUCKET_PX)}`;
12170
12217
  const reportedGeometryRef = useRef(null);
12171
12218
  useLayoutEffect(() => {
12172
- if (!manualPagination || !dynamicHeight || isPageSizeUserSelected) return;
12219
+ if (!dynamicHeight || isPageSizeUserSelected) return;
12220
+ if (!manualPagination && (isPaginationControlled || renderedMode !== "cards")) return;
12173
12221
  if (cardPagination.mode !== "paged") return;
12174
12222
  if (!hasMeasuredLayout || measuredBodyHeight <= 0) return;
12175
12223
  if (!dynamicPageSizeTarget || dynamicPageSizeTarget < 1) return;
@@ -12191,6 +12239,7 @@ function useTable(effectiveViewModeOverride) {
12191
12239
  return () => clearTimeout(timer);
12192
12240
  }, [
12193
12241
  manualPagination,
12242
+ isPaginationControlled,
12194
12243
  dynamicHeight,
12195
12244
  isPageSizeUserSelected,
12196
12245
  cardPagination.mode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-kit",
3
- "version": "2.11.7",
3
+ "version": "2.11.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Reusable React UI component package for Najm framework",