@trackunit/react-components 2.11.2 → 2.11.6

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.
Files changed (43) hide show
  1. package/index.cjs.js +152 -11
  2. package/index.esm.js +152 -12
  3. package/package.json +6 -6
  4. package/src/components/Menu/MenuContent/MenuContent.d.ts +14 -1
  5. package/src/components/Menu/MenuContent/MenuContentContext.d.ts +11 -0
  6. package/src/components/Menu/MenuContent/compactFloatingListRefs.d.ts +4 -3
  7. package/src/components/Menu/MenuContent/useMenuContentState.d.ts +2 -1
  8. package/src/components/Menu/MenuContent/useMenuSearchField.d.ts +61 -0
  9. package/src/index.d.ts +1 -0
  10. package/translation.cjs.js +2 -0
  11. package/translation.esm.js +2 -0
  12. package/translation10.cjs.js +2 -0
  13. package/translation10.esm.js +2 -0
  14. package/translation11.cjs.js +2 -0
  15. package/translation11.esm.js +2 -0
  16. package/translation12.cjs.js +2 -0
  17. package/translation12.esm.js +2 -0
  18. package/translation13.cjs.js +2 -0
  19. package/translation13.esm.js +2 -0
  20. package/translation14.cjs.js +2 -0
  21. package/translation14.esm.js +2 -0
  22. package/translation15.cjs.js +2 -0
  23. package/translation15.esm.js +2 -0
  24. package/translation16.cjs.js +2 -0
  25. package/translation16.esm.js +2 -0
  26. package/translation17.cjs.js +2 -0
  27. package/translation17.esm.js +2 -0
  28. package/translation2.cjs.js +2 -0
  29. package/translation2.esm.js +2 -0
  30. package/translation3.cjs.js +2 -0
  31. package/translation3.esm.js +2 -0
  32. package/translation4.cjs.js +2 -0
  33. package/translation4.esm.js +2 -0
  34. package/translation5.cjs.js +2 -0
  35. package/translation5.esm.js +2 -0
  36. package/translation6.cjs.js +2 -0
  37. package/translation6.esm.js +2 -0
  38. package/translation7.cjs.js +2 -0
  39. package/translation7.esm.js +2 -0
  40. package/translation8.cjs.js +2 -0
  41. package/translation8.esm.js +2 -0
  42. package/translation9.cjs.js +2 -0
  43. package/translation9.esm.js +2 -0
package/index.cjs.js CHANGED
@@ -7364,15 +7364,16 @@ const compactFloatingListRefs = (elementsRef, labelsRef) => {
7364
7364
  labels.length = lastLive + 1;
7365
7365
  };
7366
7366
  /**
7367
- * Index of the first registered item that is not `aria-disabled`. Used as the initial tab stop
7367
+ * Index of the first registered entry that is not `aria-disabled`. Used as the initial tab stop
7368
7368
  * when `activeIndex` is still null.
7369
7369
  *
7370
7370
  * @param elementsRef The FloatingList elements array
7371
- * @returns {number} The first enabled index, or `0` when the list has no enabled item
7371
+ * @param skipIndex Registered index to ignore, used to look past a search field to the first real item
7372
+ * @returns {number} The first enabled index, or `0` when the list has no enabled entry
7372
7373
  */
7373
- const firstEnabledListIndex = (elementsRef) => {
7374
- const index = elementsRef.current.findIndex(element => {
7375
- if (!element) {
7374
+ const firstEnabledListIndex = (elementsRef, skipIndex) => {
7375
+ const index = elementsRef.current.findIndex((element, elementIndex) => {
7376
+ if (!element || elementIndex === skipIndex) {
7376
7377
  return false;
7377
7378
  }
7378
7379
  return element.getAttribute("aria-disabled") !== "true";
@@ -7385,7 +7386,7 @@ const UNMANAGED_SELECTION = [];
7385
7386
  * Owns MenuContent's list registration, selection, and typeable-child key handling so the
7386
7387
  * component file stays the shell + JSX.
7387
7388
  */
7388
- const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelectedItems, onSelectionChange, ref, }) => {
7389
+ const useMenuContentState = ({ children, isMulti, autoFocus, selectedItems: controlledSelectedItems, onSelectionChange, ref, }) => {
7389
7390
  const selectionManaged = controlledSelectedItems !== undefined || onSelectionChange !== undefined;
7390
7391
  const [internalSelectedItems, setInternalSelectedItems] = react.useState(controlledSelectedItems ?? []);
7391
7392
  const selectedItems = selectionManaged ? (controlledSelectedItems ?? internalSelectedItems) : UNMANAGED_SELECTION;
@@ -7393,6 +7394,10 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7393
7394
  const [firstEnabledIndex, setFirstEnabledIndex] = react.useState(0);
7394
7395
  const listRef = react.useRef([]);
7395
7396
  const labelsRef = react.useRef([]);
7397
+ const searchFieldIndexRef = react.useRef(null);
7398
+ const registerSearchField = react.useCallback((index) => {
7399
+ searchFieldIndexRef.current = index;
7400
+ }, []);
7396
7401
  const ambientPopover = useOptionalPopoverContext();
7397
7402
  // Falls back to a local, always-open floating context when there is no ambient `Popover` (e.g. `MenuContent`
7398
7403
  // rendered standalone inside a `Collapse`) -- `useFloating` must still be called unconditionally per the
@@ -7416,6 +7421,10 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7416
7421
  onNavigate: setActiveIndex,
7417
7422
  loop: true,
7418
7423
  nested: isNested,
7424
+ // Floating UI's own entry focus, which waits for the list to finish registering before it
7425
+ // picks the first enabled index. Off by default: most menus open from a trigger the user
7426
+ // should stay on.
7427
+ focusItemOnOpen: autoFocus,
7419
7428
  });
7420
7429
  const typeahead = react$1.useTypeahead(context, {
7421
7430
  listRef: labelsRef,
@@ -7426,6 +7435,10 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7426
7435
  const { getFloatingProps, getItemProps } = react$1.useInteractions([listNavigation, typeahead]);
7427
7436
  const syncRegisteredList = react.useCallback(() => {
7428
7437
  compactFloatingListRefs(listRef, labelsRef);
7438
+ // Deliberately *not* skipping the search field. `firstEnabledIndex` is the menu's single
7439
+ // roving tab stop, and a registered field should hold it: that is what keeps every row's
7440
+ // `tabIndex` at -1 while the user is typing, so Tab leaves for the menu's chrome instead of
7441
+ // stopping on a row. Only Home (below) looks past the field, because Home addresses items.
7429
7442
  const nextFirstEnabled = firstEnabledListIndex(listRef);
7430
7443
  setFirstEnabledIndex(current => (current === nextFirstEnabled ? current : nextFirstEnabled));
7431
7444
  setActiveIndex(current => {
@@ -7471,24 +7484,62 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7471
7484
  selectedItems,
7472
7485
  handleItemClick,
7473
7486
  syncRegisteredList,
7474
- }), [activeIndex, firstEnabledIndex, getItemProps, isMulti, selectedItems, handleItemClick, syncRegisteredList]);
7487
+ setActiveIndex,
7488
+ registerSearchField,
7489
+ }), [
7490
+ activeIndex,
7491
+ firstEnabledIndex,
7492
+ getItemProps,
7493
+ isMulti,
7494
+ selectedItems,
7495
+ handleItemClick,
7496
+ syncRegisteredList,
7497
+ registerSearchField,
7498
+ ]);
7475
7499
  // Printable characters and Home/End/Left/Right stay native in nested inputs (e.g. FilterBar
7476
7500
  // Search). ArrowDown is left alone so a typeable child can enter the registered list.
7477
7501
  const handleKeyDownCapture = react.useCallback((event) => {
7478
7502
  if (!utils.isTypeableElement(event.target)) {
7503
+ // Home/End address the *items*. A registered search field is a navigation stop but not an
7504
+ // item, and it renders above them, so Floating UI's Home -- the list's minimum index -- would
7505
+ // land in the input. Redirect to the first item instead, by moving `activeIndex` rather than
7506
+ // calling `focus()`, so list navigation stays the only thing that moves focus. End needs no
7507
+ // counterpart: the field is never the maximum index.
7508
+ //
7509
+ // The containment check is not redundant: a `MenuItem` submenu renders its own `MenuContent`
7510
+ // into a portal, and React still routes that subtree's synthetic events through the React
7511
+ // tree, so this handler sees a submenu's Home first and would claim it before the submenu
7512
+ // ever ran its own.
7513
+ if (event.key === "Home" &&
7514
+ searchFieldIndexRef.current !== null &&
7515
+ event.target instanceof Node &&
7516
+ event.currentTarget.contains(event.target)) {
7517
+ event.preventDefault();
7518
+ event.stopPropagation();
7519
+ const firstItemIndex = firstEnabledListIndex(listRef, searchFieldIndexRef.current);
7520
+ // `firstEnabledListIndex` falls back to 0 when nothing qualifies, which is the field's own
7521
+ // index once a query has filtered every row away. Leave the highlight alone rather than
7522
+ // pulling it into the input.
7523
+ if (firstItemIndex !== searchFieldIndexRef.current) {
7524
+ setActiveIndex(firstItemIndex);
7525
+ }
7526
+ }
7479
7527
  return;
7480
7528
  }
7481
7529
  if (event.key === "ArrowDown") {
7482
7530
  return;
7483
7531
  }
7484
- // ArrowUp is stopped so wrapping does not jump to the last item while the user is in Search.
7485
- // Phase 2 (FilterBar) adds the reverse path: ArrowUp on the first row returns to Search.
7532
+ // ArrowUp is swallowed only for an *unregistered* typeable child, where a wrap has no sensible
7533
+ // destination: the input is not in the list, so list navigation would move the menu's roving
7534
+ // tab stop to the last item behind the user's back. A field registered via `useMenuSearchField`
7535
+ // *is* in the list, at the index above the first row, so its ArrowUp is an ordinary wrap to the
7536
+ // last item -- symmetric with the ArrowDown wrap that already lands back in the field.
7486
7537
  if (event.key.length === 1 ||
7487
7538
  event.key === "Home" ||
7488
7539
  event.key === "End" ||
7489
7540
  event.key === "ArrowLeft" ||
7490
7541
  event.key === "ArrowRight" ||
7491
- event.key === "ArrowUp") {
7542
+ (event.key === "ArrowUp" && searchFieldIndexRef.current === null)) {
7492
7543
  event.stopPropagation();
7493
7544
  }
7494
7545
  }, []);
@@ -7568,10 +7619,11 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7568
7619
  * @param {MenuContentProps} props - The props for the MenuContent component
7569
7620
  * @returns {ReactElement} MenuContent component
7570
7621
  */
7571
- const MenuContent = ({ "data-testid": dataTestId, className, listClassName, children, isMulti = false, selectedItems, onSelectionChange, style, ref, "aria-label": ariaLabel, ...args }) => {
7622
+ const MenuContent = ({ "data-testid": dataTestId, className, listClassName, children, isMulti = false, autoFocus = false, selectedItems, onSelectionChange, style, ref, "aria-label": ariaLabel, ...args }) => {
7572
7623
  const { floatingRef, getFloatingProps, handleKeyDownCapture, listRef, labelsRef, menuContext } = useMenuContentState({
7573
7624
  children,
7574
7625
  isMulti,
7626
+ autoFocus,
7575
7627
  selectedItems,
7576
7628
  onSelectionChange,
7577
7629
  ref,
@@ -7579,6 +7631,94 @@ const MenuContent = ({ "data-testid": dataTestId, className, listClassName, chil
7579
7631
  return (jsxRuntime.jsx("div", { "aria-label": ariaLabel, className: cvaMenu({ className, limitWidth: true }), "data-testid": dataTestId ? `${dataTestId}-menu-list` : "menu-list", onKeyDownCapture: handleKeyDownCapture, ref: floatingRef, role: "menu", style: style, tabIndex: -1, ...getFloatingProps({ onClick: args.onClick }), children: jsxRuntime.jsx("div", { className: cvaMenuList({ className: listClassName }), children: jsxRuntime.jsx(MenuContentContext.Provider, { value: menuContext, children: jsxRuntime.jsx(react$1.FloatingList, { elementsRef: listRef, labelsRef: labelsRef, children: children }) }) }) }));
7580
7632
  };
7581
7633
 
7634
+ /**
7635
+ * Joins a search input to the nearest `MenuContent` as the stop *above* the first item, so Floating
7636
+ * UI's list navigation moves focus in and out of the list on its own: ArrowDown from the field enters
7637
+ * the list, and ArrowUp from the first item comes back to the field. The field is a full member of
7638
+ * the loop, so ArrowUp from it wraps to the last item and ArrowDown from the last item wraps back.
7639
+ *
7640
+ * Focus then has a single owner. Moving between an unregistered field and the list means calling
7641
+ * `.focus()` from a key handler, which races the `requestAnimationFrame` Floating UI asserts item
7642
+ * focus from: the field wins or loses depending on how far apart the two key presses land. That is
7643
+ * a flake rather than a behaviour, and it cannot be fixed from the outside (ADR-0005).
7644
+ *
7645
+ * Registering also keeps the rows off the sequential Tab path while the field holds focus: the menu's
7646
+ * single roving tab stop is the field itself, so Tab continues to the menu's chrome (reset buttons,
7647
+ * footers) instead of stopping on a row.
7648
+ *
7649
+ * Call it from a component rendered *inside* `MenuContent`: registration goes through the
7650
+ * `FloatingList` context that `MenuContent` opens around its children, so a caller that also renders
7651
+ * the `MenuContent` sits outside that context and registers nothing.
7652
+ *
7653
+ * Product code should render `Search` from `@trackunit/react-form-components` here, as
7654
+ * `FiltersMenuSearchField` does -- the native `<input>` below (and in this package's story and specs)
7655
+ * is only there because `@trackunit/react-components` cannot depend on `@trackunit/react-form-components`
7656
+ * without a dependency cycle.
7657
+ *
7658
+ * @returns {UseMenuSearchFieldResult} A getter for the search input's props
7659
+ * @example
7660
+ * ```tsx
7661
+ * const MenuSearch = () => {
7662
+ * const { getSearchFieldProps } = useMenuSearchField();
7663
+ * return <input aria-label="Search" type="text" {...getSearchFieldProps()} />;
7664
+ * };
7665
+ *
7666
+ * const MenuWithSearch = () => (
7667
+ * <MenuContent autoFocus>
7668
+ * <MenuSearch />
7669
+ * <MenuItem id="alpha" label="Alpha" />
7670
+ * </MenuContent>
7671
+ * );
7672
+ * ```
7673
+ */
7674
+ const useMenuSearchField = () => {
7675
+ const menu = useOptionalMenuContentContext();
7676
+ // `label: null` keeps the field out of typeahead: it is a navigation stop, not a match target.
7677
+ const { ref: listItemRef, index } = react$1.useListItem({ label: null });
7678
+ const syncRegisteredList = menu?.syncRegisteredList;
7679
+ const registerSearchField = menu?.registerSearchField;
7680
+ const setActiveIndex = menu?.setActiveIndex;
7681
+ react.useLayoutEffect(() => {
7682
+ if (index < 0 || registerSearchField === undefined) {
7683
+ return;
7684
+ }
7685
+ registerSearchField(index);
7686
+ syncRegisteredList?.();
7687
+ return () => {
7688
+ registerSearchField(null);
7689
+ };
7690
+ }, [index, registerSearchField, syncRegisteredList]);
7691
+ // A consumer's ref only arrives when the getter is called, so it is held here and forwarded
7692
+ // through one callback whose identity never changes. Floating UI's `useMergeRefs` memoizes on the
7693
+ // refs themselves, which matters: `useListItem`'s ref changes identity when it learns its index,
7694
+ // and only re-running it writes the input into `elementsRef` at that index.
7695
+ const userRef = react.useRef(undefined);
7696
+ const forwardUserRef = react.useCallback(node => {
7697
+ const target = userRef.current;
7698
+ if (typeof target === "function") {
7699
+ target(node);
7700
+ return;
7701
+ }
7702
+ if (target !== null && target !== undefined) {
7703
+ target.current = node;
7704
+ }
7705
+ }, []);
7706
+ const ref = react$1.useMergeRefs([listItemRef, forwardUserRef]);
7707
+ const getSearchFieldProps = react.useCallback((userProps) => {
7708
+ userRef.current = userProps?.ref;
7709
+ return {
7710
+ ref,
7711
+ onFocus: (event) => {
7712
+ if (index >= 0) {
7713
+ setActiveIndex?.(index);
7714
+ }
7715
+ userProps?.onFocus?.(event);
7716
+ },
7717
+ };
7718
+ }, [index, ref, setActiveIndex]);
7719
+ return react.useMemo(() => ({ getSearchFieldProps }), [getSearchFieldProps]);
7720
+ };
7721
+
7582
7722
  /**
7583
7723
  * MenuDivider renders a horizontal line to visually separate groups of items within a MenuContent.
7584
7724
  *
@@ -14922,6 +15062,7 @@ exports.useListItemHeight = useListItemHeight;
14922
15062
  exports.useLocalStorage = useLocalStorage;
14923
15063
  exports.useLocalStorageReducer = useLocalStorageReducer;
14924
15064
  exports.useMeasure = useMeasure;
15065
+ exports.useMenuSearchField = useMenuSearchField;
14925
15066
  exports.useMenuTree = useMenuTree;
14926
15067
  exports.useMergeRefs = useMergeRefs;
14927
15068
  exports.useModifierKey = useModifierKey;
package/index.esm.js CHANGED
@@ -7363,15 +7363,16 @@ const compactFloatingListRefs = (elementsRef, labelsRef) => {
7363
7363
  labels.length = lastLive + 1;
7364
7364
  };
7365
7365
  /**
7366
- * Index of the first registered item that is not `aria-disabled`. Used as the initial tab stop
7366
+ * Index of the first registered entry that is not `aria-disabled`. Used as the initial tab stop
7367
7367
  * when `activeIndex` is still null.
7368
7368
  *
7369
7369
  * @param elementsRef The FloatingList elements array
7370
- * @returns {number} The first enabled index, or `0` when the list has no enabled item
7370
+ * @param skipIndex Registered index to ignore, used to look past a search field to the first real item
7371
+ * @returns {number} The first enabled index, or `0` when the list has no enabled entry
7371
7372
  */
7372
- const firstEnabledListIndex = (elementsRef) => {
7373
- const index = elementsRef.current.findIndex(element => {
7374
- if (!element) {
7373
+ const firstEnabledListIndex = (elementsRef, skipIndex) => {
7374
+ const index = elementsRef.current.findIndex((element, elementIndex) => {
7375
+ if (!element || elementIndex === skipIndex) {
7375
7376
  return false;
7376
7377
  }
7377
7378
  return element.getAttribute("aria-disabled") !== "true";
@@ -7384,7 +7385,7 @@ const UNMANAGED_SELECTION = [];
7384
7385
  * Owns MenuContent's list registration, selection, and typeable-child key handling so the
7385
7386
  * component file stays the shell + JSX.
7386
7387
  */
7387
- const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelectedItems, onSelectionChange, ref, }) => {
7388
+ const useMenuContentState = ({ children, isMulti, autoFocus, selectedItems: controlledSelectedItems, onSelectionChange, ref, }) => {
7388
7389
  const selectionManaged = controlledSelectedItems !== undefined || onSelectionChange !== undefined;
7389
7390
  const [internalSelectedItems, setInternalSelectedItems] = useState(controlledSelectedItems ?? []);
7390
7391
  const selectedItems = selectionManaged ? (controlledSelectedItems ?? internalSelectedItems) : UNMANAGED_SELECTION;
@@ -7392,6 +7393,10 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7392
7393
  const [firstEnabledIndex, setFirstEnabledIndex] = useState(0);
7393
7394
  const listRef = useRef([]);
7394
7395
  const labelsRef = useRef([]);
7396
+ const searchFieldIndexRef = useRef(null);
7397
+ const registerSearchField = useCallback((index) => {
7398
+ searchFieldIndexRef.current = index;
7399
+ }, []);
7395
7400
  const ambientPopover = useOptionalPopoverContext();
7396
7401
  // Falls back to a local, always-open floating context when there is no ambient `Popover` (e.g. `MenuContent`
7397
7402
  // rendered standalone inside a `Collapse`) -- `useFloating` must still be called unconditionally per the
@@ -7415,6 +7420,10 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7415
7420
  onNavigate: setActiveIndex,
7416
7421
  loop: true,
7417
7422
  nested: isNested,
7423
+ // Floating UI's own entry focus, which waits for the list to finish registering before it
7424
+ // picks the first enabled index. Off by default: most menus open from a trigger the user
7425
+ // should stay on.
7426
+ focusItemOnOpen: autoFocus,
7418
7427
  });
7419
7428
  const typeahead = useTypeahead(context, {
7420
7429
  listRef: labelsRef,
@@ -7425,6 +7434,10 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7425
7434
  const { getFloatingProps, getItemProps } = useInteractions([listNavigation, typeahead]);
7426
7435
  const syncRegisteredList = useCallback(() => {
7427
7436
  compactFloatingListRefs(listRef, labelsRef);
7437
+ // Deliberately *not* skipping the search field. `firstEnabledIndex` is the menu's single
7438
+ // roving tab stop, and a registered field should hold it: that is what keeps every row's
7439
+ // `tabIndex` at -1 while the user is typing, so Tab leaves for the menu's chrome instead of
7440
+ // stopping on a row. Only Home (below) looks past the field, because Home addresses items.
7428
7441
  const nextFirstEnabled = firstEnabledListIndex(listRef);
7429
7442
  setFirstEnabledIndex(current => (current === nextFirstEnabled ? current : nextFirstEnabled));
7430
7443
  setActiveIndex(current => {
@@ -7470,24 +7483,62 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7470
7483
  selectedItems,
7471
7484
  handleItemClick,
7472
7485
  syncRegisteredList,
7473
- }), [activeIndex, firstEnabledIndex, getItemProps, isMulti, selectedItems, handleItemClick, syncRegisteredList]);
7486
+ setActiveIndex,
7487
+ registerSearchField,
7488
+ }), [
7489
+ activeIndex,
7490
+ firstEnabledIndex,
7491
+ getItemProps,
7492
+ isMulti,
7493
+ selectedItems,
7494
+ handleItemClick,
7495
+ syncRegisteredList,
7496
+ registerSearchField,
7497
+ ]);
7474
7498
  // Printable characters and Home/End/Left/Right stay native in nested inputs (e.g. FilterBar
7475
7499
  // Search). ArrowDown is left alone so a typeable child can enter the registered list.
7476
7500
  const handleKeyDownCapture = useCallback((event) => {
7477
7501
  if (!isTypeableElement(event.target)) {
7502
+ // Home/End address the *items*. A registered search field is a navigation stop but not an
7503
+ // item, and it renders above them, so Floating UI's Home -- the list's minimum index -- would
7504
+ // land in the input. Redirect to the first item instead, by moving `activeIndex` rather than
7505
+ // calling `focus()`, so list navigation stays the only thing that moves focus. End needs no
7506
+ // counterpart: the field is never the maximum index.
7507
+ //
7508
+ // The containment check is not redundant: a `MenuItem` submenu renders its own `MenuContent`
7509
+ // into a portal, and React still routes that subtree's synthetic events through the React
7510
+ // tree, so this handler sees a submenu's Home first and would claim it before the submenu
7511
+ // ever ran its own.
7512
+ if (event.key === "Home" &&
7513
+ searchFieldIndexRef.current !== null &&
7514
+ event.target instanceof Node &&
7515
+ event.currentTarget.contains(event.target)) {
7516
+ event.preventDefault();
7517
+ event.stopPropagation();
7518
+ const firstItemIndex = firstEnabledListIndex(listRef, searchFieldIndexRef.current);
7519
+ // `firstEnabledListIndex` falls back to 0 when nothing qualifies, which is the field's own
7520
+ // index once a query has filtered every row away. Leave the highlight alone rather than
7521
+ // pulling it into the input.
7522
+ if (firstItemIndex !== searchFieldIndexRef.current) {
7523
+ setActiveIndex(firstItemIndex);
7524
+ }
7525
+ }
7478
7526
  return;
7479
7527
  }
7480
7528
  if (event.key === "ArrowDown") {
7481
7529
  return;
7482
7530
  }
7483
- // ArrowUp is stopped so wrapping does not jump to the last item while the user is in Search.
7484
- // Phase 2 (FilterBar) adds the reverse path: ArrowUp on the first row returns to Search.
7531
+ // ArrowUp is swallowed only for an *unregistered* typeable child, where a wrap has no sensible
7532
+ // destination: the input is not in the list, so list navigation would move the menu's roving
7533
+ // tab stop to the last item behind the user's back. A field registered via `useMenuSearchField`
7534
+ // *is* in the list, at the index above the first row, so its ArrowUp is an ordinary wrap to the
7535
+ // last item -- symmetric with the ArrowDown wrap that already lands back in the field.
7485
7536
  if (event.key.length === 1 ||
7486
7537
  event.key === "Home" ||
7487
7538
  event.key === "End" ||
7488
7539
  event.key === "ArrowLeft" ||
7489
7540
  event.key === "ArrowRight" ||
7490
- event.key === "ArrowUp") {
7541
+ (event.key === "ArrowUp" && searchFieldIndexRef.current === null)) {
7491
7542
  event.stopPropagation();
7492
7543
  }
7493
7544
  }, []);
@@ -7567,10 +7618,11 @@ const useMenuContentState = ({ children, isMulti, selectedItems: controlledSelec
7567
7618
  * @param {MenuContentProps} props - The props for the MenuContent component
7568
7619
  * @returns {ReactElement} MenuContent component
7569
7620
  */
7570
- const MenuContent = ({ "data-testid": dataTestId, className, listClassName, children, isMulti = false, selectedItems, onSelectionChange, style, ref, "aria-label": ariaLabel, ...args }) => {
7621
+ const MenuContent = ({ "data-testid": dataTestId, className, listClassName, children, isMulti = false, autoFocus = false, selectedItems, onSelectionChange, style, ref, "aria-label": ariaLabel, ...args }) => {
7571
7622
  const { floatingRef, getFloatingProps, handleKeyDownCapture, listRef, labelsRef, menuContext } = useMenuContentState({
7572
7623
  children,
7573
7624
  isMulti,
7625
+ autoFocus,
7574
7626
  selectedItems,
7575
7627
  onSelectionChange,
7576
7628
  ref,
@@ -7578,6 +7630,94 @@ const MenuContent = ({ "data-testid": dataTestId, className, listClassName, chil
7578
7630
  return (jsx("div", { "aria-label": ariaLabel, className: cvaMenu({ className, limitWidth: true }), "data-testid": dataTestId ? `${dataTestId}-menu-list` : "menu-list", onKeyDownCapture: handleKeyDownCapture, ref: floatingRef, role: "menu", style: style, tabIndex: -1, ...getFloatingProps({ onClick: args.onClick }), children: jsx("div", { className: cvaMenuList({ className: listClassName }), children: jsx(MenuContentContext.Provider, { value: menuContext, children: jsx(FloatingList, { elementsRef: listRef, labelsRef: labelsRef, children: children }) }) }) }));
7579
7631
  };
7580
7632
 
7633
+ /**
7634
+ * Joins a search input to the nearest `MenuContent` as the stop *above* the first item, so Floating
7635
+ * UI's list navigation moves focus in and out of the list on its own: ArrowDown from the field enters
7636
+ * the list, and ArrowUp from the first item comes back to the field. The field is a full member of
7637
+ * the loop, so ArrowUp from it wraps to the last item and ArrowDown from the last item wraps back.
7638
+ *
7639
+ * Focus then has a single owner. Moving between an unregistered field and the list means calling
7640
+ * `.focus()` from a key handler, which races the `requestAnimationFrame` Floating UI asserts item
7641
+ * focus from: the field wins or loses depending on how far apart the two key presses land. That is
7642
+ * a flake rather than a behaviour, and it cannot be fixed from the outside (ADR-0005).
7643
+ *
7644
+ * Registering also keeps the rows off the sequential Tab path while the field holds focus: the menu's
7645
+ * single roving tab stop is the field itself, so Tab continues to the menu's chrome (reset buttons,
7646
+ * footers) instead of stopping on a row.
7647
+ *
7648
+ * Call it from a component rendered *inside* `MenuContent`: registration goes through the
7649
+ * `FloatingList` context that `MenuContent` opens around its children, so a caller that also renders
7650
+ * the `MenuContent` sits outside that context and registers nothing.
7651
+ *
7652
+ * Product code should render `Search` from `@trackunit/react-form-components` here, as
7653
+ * `FiltersMenuSearchField` does -- the native `<input>` below (and in this package's story and specs)
7654
+ * is only there because `@trackunit/react-components` cannot depend on `@trackunit/react-form-components`
7655
+ * without a dependency cycle.
7656
+ *
7657
+ * @returns {UseMenuSearchFieldResult} A getter for the search input's props
7658
+ * @example
7659
+ * ```tsx
7660
+ * const MenuSearch = () => {
7661
+ * const { getSearchFieldProps } = useMenuSearchField();
7662
+ * return <input aria-label="Search" type="text" {...getSearchFieldProps()} />;
7663
+ * };
7664
+ *
7665
+ * const MenuWithSearch = () => (
7666
+ * <MenuContent autoFocus>
7667
+ * <MenuSearch />
7668
+ * <MenuItem id="alpha" label="Alpha" />
7669
+ * </MenuContent>
7670
+ * );
7671
+ * ```
7672
+ */
7673
+ const useMenuSearchField = () => {
7674
+ const menu = useOptionalMenuContentContext();
7675
+ // `label: null` keeps the field out of typeahead: it is a navigation stop, not a match target.
7676
+ const { ref: listItemRef, index } = useListItem({ label: null });
7677
+ const syncRegisteredList = menu?.syncRegisteredList;
7678
+ const registerSearchField = menu?.registerSearchField;
7679
+ const setActiveIndex = menu?.setActiveIndex;
7680
+ useLayoutEffect(() => {
7681
+ if (index < 0 || registerSearchField === undefined) {
7682
+ return;
7683
+ }
7684
+ registerSearchField(index);
7685
+ syncRegisteredList?.();
7686
+ return () => {
7687
+ registerSearchField(null);
7688
+ };
7689
+ }, [index, registerSearchField, syncRegisteredList]);
7690
+ // A consumer's ref only arrives when the getter is called, so it is held here and forwarded
7691
+ // through one callback whose identity never changes. Floating UI's `useMergeRefs` memoizes on the
7692
+ // refs themselves, which matters: `useListItem`'s ref changes identity when it learns its index,
7693
+ // and only re-running it writes the input into `elementsRef` at that index.
7694
+ const userRef = useRef(undefined);
7695
+ const forwardUserRef = useCallback(node => {
7696
+ const target = userRef.current;
7697
+ if (typeof target === "function") {
7698
+ target(node);
7699
+ return;
7700
+ }
7701
+ if (target !== null && target !== undefined) {
7702
+ target.current = node;
7703
+ }
7704
+ }, []);
7705
+ const ref = useMergeRefs$1([listItemRef, forwardUserRef]);
7706
+ const getSearchFieldProps = useCallback((userProps) => {
7707
+ userRef.current = userProps?.ref;
7708
+ return {
7709
+ ref,
7710
+ onFocus: (event) => {
7711
+ if (index >= 0) {
7712
+ setActiveIndex?.(index);
7713
+ }
7714
+ userProps?.onFocus?.(event);
7715
+ },
7716
+ };
7717
+ }, [index, ref, setActiveIndex]);
7718
+ return useMemo(() => ({ getSearchFieldProps }), [getSearchFieldProps]);
7719
+ };
7720
+
7581
7721
  /**
7582
7722
  * MenuDivider renders a horizontal line to visually separate groups of items within a MenuContent.
7583
7723
  *
@@ -14756,4 +14896,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
14756
14896
  */
14757
14897
  setupLibraryTranslations();
14758
14898
 
14759
- export { Alert, Badge, Breadcrumb, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyButton, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, LabeledValue, LabeledValueList, List, ListItem, MAX_HASH_LENGTH, MAX_URL_LENGTH, MenuContent, MenuDivider, MenuItem, MenuTree, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SHEET_TRANSITION_DURATION, SHEET_TRANSITION_DURATION_MS, SHEET_TRANSITION_EASING, SectionHeader, SegmentedValueBar, Sheet, Sidebar, SidebarContentLayout, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHashParamSync, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMenuTree, useMergeRefs, useModifierKey, useOptionalPopoverContext, useOverflowBorder, useOverflowItems, usePersistedState, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSearchParamSync, useSelfUpdatingRef, useSessionStorage, useSessionStorageReducer, useSheet, useSheetSnap, useStorageKey, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
14899
+ export { Alert, Badge, Breadcrumb, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyButton, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, LabeledValue, LabeledValueList, List, ListItem, MAX_HASH_LENGTH, MAX_URL_LENGTH, MenuContent, MenuDivider, MenuItem, MenuTree, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SHEET_TRANSITION_DURATION, SHEET_TRANSITION_DURATION_MS, SHEET_TRANSITION_EASING, SectionHeader, SegmentedValueBar, Sheet, Sidebar, SidebarContentLayout, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHashParamSync, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMenuSearchField, useMenuTree, useMergeRefs, useModifierKey, useOptionalPopoverContext, useOverflowBorder, useOverflowItems, usePersistedState, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSearchParamSync, useSelfUpdatingRef, useSessionStorage, useSessionStorageReducer, useSheet, useSheetSnap, useStorageKey, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "2.11.2",
3
+ "version": "2.11.6",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "migrations": "./migrations.json",
@@ -14,16 +14,16 @@
14
14
  "@floating-ui/react": "^0.26.25",
15
15
  "string-ts": "^2.0.0",
16
16
  "tailwind-merge": "^2.0.0",
17
- "@trackunit/ui-design-tokens": "1.15.15",
18
- "@trackunit/css-class-variance-utilities": "1.14.25",
19
- "@trackunit/shared-utils": "1.16.28",
20
- "@trackunit/ui-icons": "1.14.24",
17
+ "@trackunit/ui-design-tokens": "1.15.17",
18
+ "@trackunit/css-class-variance-utilities": "1.14.27",
19
+ "@trackunit/shared-utils": "1.16.30",
20
+ "@trackunit/ui-icons": "1.14.26",
21
21
  "es-toolkit": "^1.39.10",
22
22
  "@tanstack/react-virtual": "^3.14.3",
23
23
  "dequal": "^2.0.3",
24
24
  "fflate": "^0.8.2",
25
25
  "zod": "^3.25.76",
26
- "@trackunit/i18n-library-translation": "2.4.36"
26
+ "@trackunit/i18n-library-translation": "2.4.38"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19.0.0",
@@ -29,6 +29,19 @@ export interface MenuContentProps extends CommonProps, Styleable, Refable<HTMLDi
29
29
  * Callback triggered when selected items change.
30
30
  */
31
31
  onSelectionChange?: (selected: Array<string>) => void;
32
+ /**
33
+ * Move focus into the menu as soon as it opens, rather than leaving it on whatever opened it.
34
+ *
35
+ * The menu decides where "into the menu" is: a search field registered with `useMenuSearchField`
36
+ * if there is one, otherwise the first enabled item. Chrome that merely happens to come first in
37
+ * the DOM -- a reset button, a header link -- is never the entry point.
38
+ *
39
+ * Use it for menus the keyboard should land inside immediately (a filter panel the user opened to
40
+ * type in). Leave it off for action menus opened from a trigger the user should stay on.
41
+ *
42
+ * @default false
43
+ */
44
+ autoFocus?: boolean;
32
45
  /**
33
46
  * Accessible name for the `role="menu"` node. Prefer this (or `aria-labelledby` via the trigger)
34
47
  * when the menu is not named by surrounding chrome.
@@ -101,4 +114,4 @@ export interface MenuContentProps extends CommonProps, Styleable, Refable<HTMLDi
101
114
  * @param {MenuContentProps} props - The props for the MenuContent component
102
115
  * @returns {ReactElement} MenuContent component
103
116
  */
104
- export declare const MenuContent: ({ "data-testid": dataTestId, className, listClassName, children, isMulti, selectedItems, onSelectionChange, style, ref, "aria-label": ariaLabel, ...args }: MenuContentProps) => ReactElement;
117
+ export declare const MenuContent: ({ "data-testid": dataTestId, className, listClassName, children, isMulti, autoFocus, selectedItems, onSelectionChange, style, ref, "aria-label": ariaLabel, ...args }: MenuContentProps) => ReactElement;
@@ -11,6 +11,17 @@ export interface MenuContentContextValue {
11
11
  selectedItems: Array<string>;
12
12
  handleItemClick: (id: string) => void;
13
13
  syncRegisteredList: () => void;
14
+ /**
15
+ * Moves the menu's highlight to a registered index. Rows get this through `getItemProps`;
16
+ * a registered search field (`useMenuSearchField`) needs it directly, because it must not
17
+ * take Floating UI's hover props -- those pull focus out of the input on pointer leave.
18
+ */
19
+ setActiveIndex: (index: number | null) => void;
20
+ /**
21
+ * Reports the registered index of the menu's search field, or `null` when it unregisters. The
22
+ * field is a navigation stop but not an item, which is the difference Home/End have to respect.
23
+ */
24
+ registerSearchField: (index: number | null) => void;
14
25
  }
15
26
  declare const MenuContentContext: import("react").Context<MenuContentContextValue | null>;
16
27
  /**
@@ -9,10 +9,11 @@ import { MutableRefObject } from "react";
9
9
  */
10
10
  export declare const compactFloatingListRefs: (elementsRef: MutableRefObject<Array<HTMLElement | null>>, labelsRef: MutableRefObject<Array<string | null>>) => void;
11
11
  /**
12
- * Index of the first registered item that is not `aria-disabled`. Used as the initial tab stop
12
+ * Index of the first registered entry that is not `aria-disabled`. Used as the initial tab stop
13
13
  * when `activeIndex` is still null.
14
14
  *
15
15
  * @param elementsRef The FloatingList elements array
16
- * @returns {number} The first enabled index, or `0` when the list has no enabled item
16
+ * @param skipIndex Registered index to ignore, used to look past a search field to the first real item
17
+ * @returns {number} The first enabled index, or `0` when the list has no enabled entry
17
18
  */
18
- export declare const firstEnabledListIndex: (elementsRef: MutableRefObject<Array<HTMLElement | null>>) => number;
19
+ export declare const firstEnabledListIndex: (elementsRef: MutableRefObject<Array<HTMLElement | null>>, skipIndex?: number | null) => number;
@@ -4,6 +4,7 @@ import { MenuContentContextValue } from "./MenuContentContext";
4
4
  export interface UseMenuContentStateProps {
5
5
  children: ReactNode;
6
6
  isMulti: boolean;
7
+ autoFocus: boolean;
7
8
  selectedItems?: Array<string>;
8
9
  onSelectionChange?: (selected: Array<string>) => void;
9
10
  ref?: Ref<HTMLDivElement>;
@@ -20,4 +21,4 @@ export interface UseMenuContentStateResult {
20
21
  * Owns MenuContent's list registration, selection, and typeable-child key handling so the
21
22
  * component file stays the shell + JSX.
22
23
  */
23
- export declare const useMenuContentState: ({ children, isMulti, selectedItems: controlledSelectedItems, onSelectionChange, ref, }: UseMenuContentStateProps) => UseMenuContentStateResult;
24
+ export declare const useMenuContentState: ({ children, isMulti, autoFocus, selectedItems: controlledSelectedItems, onSelectionChange, ref, }: UseMenuContentStateProps) => UseMenuContentStateResult;
@@ -0,0 +1,61 @@
1
+ import { FocusEventHandler, Ref, RefCallback } from "react";
2
+ export interface MenuSearchFieldUserProps {
3
+ /** The consumer's own ref, kept alongside the one that registers the input. */
4
+ ref?: Ref<HTMLElement>;
5
+ /** The consumer's own focus handler, called after the menu has moved its highlight. */
6
+ onFocus?: FocusEventHandler<HTMLElement>;
7
+ }
8
+ export interface MenuSearchFieldProps {
9
+ /** Puts the input in its place in the menu's keyboard order. */
10
+ ref: RefCallback<HTMLElement> | null;
11
+ /** Moves the menu's highlight off the items when the input is clicked or tabbed into. */
12
+ onFocus: FocusEventHandler<HTMLElement>;
13
+ }
14
+ export interface UseMenuSearchFieldResult {
15
+ /**
16
+ * Props for the search input. Pass your own `ref` / `onFocus` in and they are merged rather
17
+ * than replaced, so spread order cannot silently take the menu's registration away.
18
+ */
19
+ getSearchFieldProps: (userProps?: MenuSearchFieldUserProps) => MenuSearchFieldProps;
20
+ }
21
+ /**
22
+ * Joins a search input to the nearest `MenuContent` as the stop *above* the first item, so Floating
23
+ * UI's list navigation moves focus in and out of the list on its own: ArrowDown from the field enters
24
+ * the list, and ArrowUp from the first item comes back to the field. The field is a full member of
25
+ * the loop, so ArrowUp from it wraps to the last item and ArrowDown from the last item wraps back.
26
+ *
27
+ * Focus then has a single owner. Moving between an unregistered field and the list means calling
28
+ * `.focus()` from a key handler, which races the `requestAnimationFrame` Floating UI asserts item
29
+ * focus from: the field wins or loses depending on how far apart the two key presses land. That is
30
+ * a flake rather than a behaviour, and it cannot be fixed from the outside (ADR-0005).
31
+ *
32
+ * Registering also keeps the rows off the sequential Tab path while the field holds focus: the menu's
33
+ * single roving tab stop is the field itself, so Tab continues to the menu's chrome (reset buttons,
34
+ * footers) instead of stopping on a row.
35
+ *
36
+ * Call it from a component rendered *inside* `MenuContent`: registration goes through the
37
+ * `FloatingList` context that `MenuContent` opens around its children, so a caller that also renders
38
+ * the `MenuContent` sits outside that context and registers nothing.
39
+ *
40
+ * Product code should render `Search` from `@trackunit/react-form-components` here, as
41
+ * `FiltersMenuSearchField` does -- the native `<input>` below (and in this package's story and specs)
42
+ * is only there because `@trackunit/react-components` cannot depend on `@trackunit/react-form-components`
43
+ * without a dependency cycle.
44
+ *
45
+ * @returns {UseMenuSearchFieldResult} A getter for the search input's props
46
+ * @example
47
+ * ```tsx
48
+ * const MenuSearch = () => {
49
+ * const { getSearchFieldProps } = useMenuSearchField();
50
+ * return <input aria-label="Search" type="text" {...getSearchFieldProps()} />;
51
+ * };
52
+ *
53
+ * const MenuWithSearch = () => (
54
+ * <MenuContent autoFocus>
55
+ * <MenuSearch />
56
+ * <MenuItem id="alpha" label="Alpha" />
57
+ * </MenuContent>
58
+ * );
59
+ * ```
60
+ */
61
+ export declare const useMenuSearchField: () => UseMenuSearchFieldResult;
package/src/index.d.ts CHANGED
@@ -55,6 +55,7 @@ export * from "./components/ListItem/ListItem";
55
55
  export * from "./components/ListItem/useListItemHeight";
56
56
  export * from "./components/Menu/MenuContent/MenuContent";
57
57
  export * from "./components/Menu/MenuContent/MenuContent.variants";
58
+ export * from "./components/Menu/MenuContent/useMenuSearchField";
58
59
  export * from "./components/Menu/MenuDivider/MenuDivider";
59
60
  export * from "./components/Menu/MenuItem/MenuItem";
60
61
  export * from "./components/Menu/MenuItem/MenuItem.variants";
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "zurück",
5
5
  "breadcrumb.expandButton": "ausklappen",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Mehr anzeigen"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "zurück",
3
3
  "breadcrumb.expandButton": "ausklappen",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Mehr anzeigen"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "powrót",
5
5
  "breadcrumb.expandButton": "rozwiń",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Pokaż więcej"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "powrót",
3
3
  "breadcrumb.expandButton": "rozwiń",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Pokaż więcej"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "voltar",
5
5
  "breadcrumb.expandButton": "expandir",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Mostrar mais"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "voltar",
3
3
  "breadcrumb.expandButton": "expandir",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Mostrar mais"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "назад",
5
5
  "breadcrumb.expandButton": "развернуть",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Показать больше"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "назад",
3
3
  "breadcrumb.expandButton": "развернуть",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Показать больше"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "înapoi",
5
5
  "breadcrumb.expandButton": "extindere",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Afișare mai mult"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "înapoi",
3
3
  "breadcrumb.expandButton": "extindere",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Afișare mai mult"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "atrás",
5
5
  "breadcrumb.expandButton": "expandir",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Mostrar más"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "atrás",
3
3
  "breadcrumb.expandButton": "expandir",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Mostrar más"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "tillbaka",
5
5
  "breadcrumb.expandButton": "expandera",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Visa mer"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "tillbaka",
3
3
  "breadcrumb.expandButton": "expandera",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Visa mer"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "戻る",
5
5
  "breadcrumb.expandButton": "展開",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "さらに表示"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "戻る",
3
3
  "breadcrumb.expandButton": "展開",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "さらに表示"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "ย้อนกลับ",
5
5
  "breadcrumb.expandButton": "ขยาย",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "แสดงเพิ่มเติม"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "ย้อนกลับ",
3
3
  "breadcrumb.expandButton": "ขยาย",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "แสดงเพิ่มเติม"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "tilbage",
5
5
  "breadcrumb.expandButton": "udvid",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Vis mere"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "tilbage",
3
3
  "breadcrumb.expandButton": "udvid",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Vis mere"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "zpět",
5
5
  "breadcrumb.expandButton": "rozbalit",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Zobrazit více"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "zpět",
3
3
  "breadcrumb.expandButton": "rozbalit",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Zobrazit více"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "terug",
5
5
  "breadcrumb.expandButton": "uitklappen",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Meer weergeven"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "terug",
3
3
  "breadcrumb.expandButton": "uitklappen",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Meer weergeven"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "retour",
5
5
  "breadcrumb.expandButton": "développer",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Afficher plus"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "retour",
3
3
  "breadcrumb.expandButton": "développer",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Afficher plus"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "takaisin",
5
5
  "breadcrumb.expandButton": "laajenna",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Näytä lisää"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "takaisin",
3
3
  "breadcrumb.expandButton": "laajenna",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Näytä lisää"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "vissza",
5
5
  "breadcrumb.expandButton": "kibontás",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Több megjelenítése"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "vissza",
3
3
  "breadcrumb.expandButton": "kibontás",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Több megjelenítése"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "indietro",
5
5
  "breadcrumb.expandButton": "espandi",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Mostra di più"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "indietro",
3
3
  "breadcrumb.expandButton": "espandi",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Mostra di più"
5
7
  };
6
8
 
@@ -3,6 +3,8 @@
3
3
  var translation = {
4
4
  "breadcrumb.backButton": "tilbake",
5
5
  "breadcrumb.expandButton": "utvid",
6
+ "copyButton.copied": "Copied!",
7
+ "copyButton.copy": "Copy",
6
8
  "moreMenu.buttonLabel": "Vis mer"
7
9
  };
8
10
 
@@ -1,6 +1,8 @@
1
1
  var translation = {
2
2
  "breadcrumb.backButton": "tilbake",
3
3
  "breadcrumb.expandButton": "utvid",
4
+ "copyButton.copied": "Copied!",
5
+ "copyButton.copy": "Copy",
4
6
  "moreMenu.buttonLabel": "Vis mer"
5
7
  };
6
8