@trackunit/react-form-components 2.2.0 → 2.2.1

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/index.cjs.js CHANGED
@@ -1638,7 +1638,11 @@ const cvaSelectSingleValue = cssClassVarianceUtilities.cvaMerge([
1638
1638
  "text-ellipsis",
1639
1639
  "whitespace-nowrap",
1640
1640
  ]);
1641
- const cvaSelectMenu = cssClassVarianceUtilities.cvaMerge([reactComponents.cvaMenu({ limitWidth: false }), "absolute", "w-full"], {
1641
+ // No `w-full`: useCustomComponents.tsx's custom `Menu` applies edge-aware min/max-width and
1642
+ // left/right alignment as an inline style (see utils/computeMenuGeometry.ts), so a
1643
+ // static Tailwind width class here would just compete with that. The menu sizes intrinsically
1644
+ // (shrink-to-fit, bounded by that inline min/max-width) as a result.
1645
+ const cvaSelectMenu = cssClassVarianceUtilities.cvaMerge([reactComponents.cvaMenu({ limitWidth: false }), "absolute"], {
1642
1646
  variants: {
1643
1647
  placement: {
1644
1648
  bottom: "top-[calc(100%+var(--spacing-1))]",
@@ -2394,7 +2398,7 @@ const getVisibleCountFromGeometries = ({ geometries, availableWidth, containerX,
2394
2398
  */
2395
2399
  const useCustomComponents = ({ disabled, readOnly, "data-testid": dataTestId, prefix, hasError, fieldSize = "medium", getOptionLabelDescription, getOptionPrefix, className, isMulti, //prefer using the component prop (ala. selectValueContainer.isMulti) inside of customComponents instead of this one.
2396
2400
  autoComplete, // see https://github.com/JedWatson/react-select/issues/758
2397
- formatOptionLabel, }) => {
2401
+ formatOptionLabel, getMenuGeometry, }) => {
2398
2402
  const [t] = useTranslation();
2399
2403
  const { setValueContainerRef, setCounterRef, setFakeCounterRef, setGeometryRef, setMenuRef, getVisibleCount, getTotalCount, getCounterWidth, getIsComplete, } = useMultiValueOverflow({ skip: !isMulti });
2400
2404
  const interactable = react.useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
@@ -2432,10 +2436,13 @@ formatOptionLabel, }) => {
2432
2436
  Control: selectControl => {
2433
2437
  return (jsxRuntime.jsxs(ReactSelect.components.Control, { ...selectControl, className: cvaSelectControl({
2434
2438
  className: selectControl.className,
2435
- }), getStyles: getNoStyles, innerProps: Boolean(readOnly)
2436
- ? // We omit the onMouseDown and onTouchEnd events to allow text selection
2437
- esToolkit.omit(selectControl.innerProps, ["onMouseDown", "onTouchEnd"])
2438
- : selectControl.innerProps, children: [prefix !== undefined ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, selectControl.children, typeof disabled === "object" ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-disabled-locked` : null, children: jsxRuntime.jsx(InputLockReasonTooltip, { ...disabled }) })) : null, typeof readOnly === "object" && !Boolean(disabled) ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-readonly-locked` : null, children: jsxRuntime.jsx(InputLockReasonTooltip, { ...readOnly }) })) : null] }));
2439
+ }), getStyles: getNoStyles, innerProps: {
2440
+ ...(Boolean(readOnly)
2441
+ ? // We omit the onMouseDown and onTouchEnd events to allow text selection
2442
+ esToolkit.omit(selectControl.innerProps, ["onMouseDown", "onTouchEnd"])
2443
+ : selectControl.innerProps),
2444
+ ...(dataTestId ? { "data-testid": `${dataTestId}-control` } : {}),
2445
+ }, children: [prefix !== undefined ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, selectControl.children, typeof disabled === "object" ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-disabled-locked` : null, children: jsxRuntime.jsx(InputLockReasonTooltip, { ...disabled }) })) : null, typeof readOnly === "object" && !Boolean(disabled) ? (jsxRuntime.jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-readonly-locked` : null, children: jsxRuntime.jsx(InputLockReasonTooltip, { ...readOnly }) })) : null] }));
2439
2446
  },
2440
2447
  Placeholder: selectPlaceholder => {
2441
2448
  return (jsxRuntime.jsx(ReactSelect.components.Placeholder, { ...selectPlaceholder, className: cvaSelectPlaceholder({ className: selectPlaceholder.className }), getStyles: getNoStyles, children: selectPlaceholder.children }));
@@ -2507,11 +2514,22 @@ formatOptionLabel, }) => {
2507
2514
  Menu: selectMenu => {
2508
2515
  return (jsxRuntime.jsx(ReactSelect.components.Menu, { ...selectMenu, className: cvaSelectMenu({ className: selectMenu.className, placement: selectMenu.placement }), getStyles: getNoStyles, innerProps: {
2509
2516
  ...selectMenu.innerProps,
2510
- ref: reactComponents.useMergeRefs([setMenuRef, selectMenu.innerProps.ref]),
2511
- } }));
2517
+ ...(dataTestId ? { "data-testid": `${dataTestId}-menu` } : {}),
2518
+ style: getMenuGeometryStyle(getMenuGeometry()),
2519
+ },
2520
+ // `innerRef` (not `innerProps.ref`) is the actual DOM ref react-select's `MenuPlacer` reads
2521
+ // to measure available space and decide whether to flip the menu open upward -- merging our
2522
+ // own ref into `innerProps.ref` instead used to silently overwrite that ref (since
2523
+ // `components.Menu` applies `innerProps` after `innerRef` when building the element's props),
2524
+ // leaving `MenuPlacer`'s measurement permanently null and its placement decision stuck at
2525
+ // the "bottom" default regardless of available space.
2526
+ innerRef: reactComponents.useMergeRefs([setMenuRef, selectMenu.innerRef]) }));
2512
2527
  },
2513
2528
  MenuPortal: selectMenuPortal => {
2514
- return (jsxRuntime.jsx(ReactSelect.components.MenuPortal, { ...selectMenuPortal, className: "!z-overlay", children: selectMenuPortal.children }));
2529
+ return (jsxRuntime.jsx(ReactSelect.components.MenuPortal, { ...selectMenuPortal, className: "!z-popover", innerProps: {
2530
+ ...selectMenuPortal.innerProps,
2531
+ ...(dataTestId ? { "data-testid": `${dataTestId}-menu-portal` } : {}),
2532
+ }, children: selectMenuPortal.children }));
2515
2533
  },
2516
2534
  MenuList: selectMenuList => {
2517
2535
  return (jsxRuntime.jsx(ReactSelect.components.MenuList, { className: cvaSelectMenuList({
@@ -2571,6 +2589,7 @@ formatOptionLabel, }) => {
2571
2589
  t,
2572
2590
  setGeometryRef,
2573
2591
  setMenuRef,
2592
+ getMenuGeometry,
2574
2593
  ]);
2575
2594
  return customComponents;
2576
2595
  };
@@ -2578,6 +2597,22 @@ const getNoStyles = () => {
2578
2597
  // To prevent the default styles from being applied from react-select.
2579
2598
  return {};
2580
2599
  };
2600
+ /**
2601
+ * Translates the computed geometry into the menu's positioning/sizing inline style: grows/shrinks
2602
+ * to fit content between `minWidth` (the control's own width) and `maxWidth` (the available space
2603
+ * on the anchored side), flipping to right-aligned ("poking out" left of the control) when needed.
2604
+ */
2605
+ const getMenuGeometryStyle = (geometry) => {
2606
+ if (!geometry) {
2607
+ return undefined;
2608
+ }
2609
+ return {
2610
+ left: geometry.alignRight ? undefined : 0,
2611
+ right: geometry.alignRight ? 0 : undefined,
2612
+ minWidth: geometry.minWidth,
2613
+ maxWidth: geometry.maxWidth,
2614
+ };
2615
+ };
2581
2616
  const getInputComponent = (children) => {
2582
2617
  return Array.isArray(children)
2583
2618
  ? children.find(child => child !== null && child !== undefined && /react-select-\d+-input/.test(child?.props?.id))
@@ -2596,6 +2631,104 @@ const getPlaceholderElement = (children) => {
2596
2631
  : null;
2597
2632
  };
2598
2633
 
2634
+ // Mirrors Popover's PADDING (libs/react/components/src/components/Popover/PopoverSizing.ts) so the
2635
+ // dropdown keeps the same edge gap as other floating design-system elements.
2636
+ const EDGE_PADDING = 12;
2637
+ /**
2638
+ * Derives the dropdown's alignment/sizing from the control's rect and the viewport:
2639
+ * - flips to right-aligned ("poking out" left of the control) only when the control no
2640
+ * longer fits to the right AND flipping actually buys more room
2641
+ * - the width ceiling on the anchored side (bounded by the viewport edge, minus padding)
2642
+ *
2643
+ * It's a bare function rather than a hook because it's a pure, synchronous transform with no
2644
+ * React state of its own, called from inside `useSelect.ts`'s `styles.menuPortal` (react-select's
2645
+ * own extension point, not ours) -- there's nowhere in that call chain to invoke a hook.
2646
+ *
2647
+ * Its result is applied as an inline style on the actual menu box by useCustomComponents.tsx's
2648
+ * custom `Menu` component (via a `getMenuGeometry` getter), not on the portal itself: `MenuPortal`
2649
+ * is a zero-size, out-of-flow wrapper, and an absolutely-positioned child (the menu) doesn't
2650
+ * contribute to its shrink-to-fit sizing, so styling the portal can't drive the visible menu's
2651
+ * width or position.
2652
+ */
2653
+ const computeMenuGeometry = (rect) => {
2654
+ const controlRight = rect.left + rect.width;
2655
+ const spaceRight = window.innerWidth - rect.left - EDGE_PADDING;
2656
+ const spaceLeft = controlRight - EDGE_PADDING;
2657
+ const alignRight = spaceRight < rect.width && spaceLeft > spaceRight;
2658
+ return {
2659
+ alignRight,
2660
+ minWidth: rect.width,
2661
+ maxWidth: alignRight ? spaceLeft : spaceRight,
2662
+ };
2663
+ };
2664
+
2665
+ // Exported purely so tests can advance a real timer by an exact, non-guessed amount -- see this
2666
+ // hook's own comment for why a trailing debounce (not a per-frame rAF throttle) is what's needed.
2667
+ const MENU_PLACEMENT_RECOMPUTE_DEBOUNCE_MS = 120;
2668
+ /**
2669
+ * react-select's own `MenuPlacer` (internal to the library) decides the escaped dropdown's
2670
+ * up/down flip exactly once per open, inside a `useLayoutEffect` gated on `[maxMenuHeight,
2671
+ * menuPlacement, menuPosition, menuShouldScrollIntoView, minMenuHeight, controlHeight]` --
2672
+ * none of which change on their own when the control's position shifts relative to the viewport
2673
+ * (e.g. scrolling the modal/drawer/page the select sits in without closing the menu). A menu that
2674
+ * correctly opened "up" (or "down") can end up clipped once the layout around it moves.
2675
+ *
2676
+ * `useSelect.ts` feeds the returned `tick` into `maxMenuHeight` -- the one prop in that dependency
2677
+ * list safe to toggle purely to force a fresh measurement, with no other side effect: the
2678
+ * resulting `maxHeight` react-select computes is never consumed on our end (the custom
2679
+ * `Menu`/`MenuList` in useCustomComponents.tsx disable react-select's per-slot styling via
2680
+ * `getStyles={getNoStyles}` and constrain height with Tailwind's `cvaMenuList` instead) -- only
2681
+ * the *change* in value, not the value itself, matters. `minMenuHeight` must stay untouched:
2682
+ * passing any finite number there (instead of `undefined`) would make react-select treat "the
2683
+ * menu barely fits, if constrained" as true almost everywhere, short-circuiting past the actual
2684
+ * flip check.
2685
+ *
2686
+ * Deliberately a trailing *debounce*, not a per-frame (rAF) throttle: `resize` (and, to a lesser
2687
+ * extent, `scroll`) fires continuously while the user is actively dragging, so a control that
2688
+ * happens to sit right at the fits-below/doesn't-fit-below threshold would otherwise have its
2689
+ * flip decision -- and therefore the whole menu -- recomputed and visibly toggle top/bottom on
2690
+ * practically every frame of the drag. Waiting until the gesture actually settles means the menu
2691
+ * still only flips (at most) once, to whatever's correct for the final size/position.
2692
+ */
2693
+ const useMenuPlacementRecompute = () => {
2694
+ const [tick, setTick] = react.useState(0);
2695
+ const cleanupRef = react.useRef(null);
2696
+ const onMenuOpen = react.useCallback(() => {
2697
+ if (cleanupRef.current) {
2698
+ return; // already watching
2699
+ }
2700
+ let timeoutId = null;
2701
+ const handleReposition = () => {
2702
+ if (timeoutId !== null) {
2703
+ clearTimeout(timeoutId);
2704
+ }
2705
+ timeoutId = setTimeout(() => {
2706
+ timeoutId = null;
2707
+ setTick(previousTick => previousTick + 1);
2708
+ }, MENU_PLACEMENT_RECOMPUTE_DEBOUNCE_MS);
2709
+ };
2710
+ // `capture: true` is required to observe scroll events from nested scrollable ancestors (e.g.
2711
+ // a Modal/Drawer body) -- "scroll" doesn't bubble, but it's still dispatched through the
2712
+ // capture phase on every ancestor, including window.
2713
+ window.addEventListener("scroll", handleReposition, { capture: true, passive: true });
2714
+ window.addEventListener("resize", handleReposition, { passive: true });
2715
+ cleanupRef.current = () => {
2716
+ window.removeEventListener("scroll", handleReposition, { capture: true });
2717
+ window.removeEventListener("resize", handleReposition);
2718
+ if (timeoutId !== null) {
2719
+ clearTimeout(timeoutId);
2720
+ }
2721
+ };
2722
+ }, []);
2723
+ const onMenuClose = react.useCallback(() => {
2724
+ cleanupRef.current?.();
2725
+ cleanupRef.current = null;
2726
+ }, []);
2727
+ // Stop watching if the component unmounts while the menu is still open.
2728
+ react.useEffect(() => onMenuClose, [onMenuClose]);
2729
+ return react.useMemo(() => ({ tick, onMenuOpen, onMenuClose }), [tick, onMenuOpen, onMenuClose]);
2730
+ };
2731
+
2599
2732
  /**
2600
2733
  * A hook used by selects to share the common code
2601
2734
  *
@@ -2629,6 +2762,19 @@ const useSelect = (props) => {
2629
2762
  });
2630
2763
  }
2631
2764
  }, [restProps, stableRestProps]);
2765
+ // `styles.menuPortal` is the only place react-select exposes the control's rect. The custom
2766
+ // `Menu` component (useCustomComponents.tsx) is what actually renders the visible box, but it
2767
+ // disables react-select's own `styles.menu` callback (`getStyles={getNoStyles}`) in favor of
2768
+ // Tailwind classes, so the geometry can't be delivered through `styles` -- this ref hands it to
2769
+ // `getMenuGeometry` below instead, read directly as inline styles by that custom component.
2770
+ const menuGeometryRef = react.useRef(null);
2771
+ const menuPortalStyles = react.useMemo(() => ({
2772
+ menuPortal: (base, { rect }) => {
2773
+ menuGeometryRef.current = computeMenuGeometry(rect);
2774
+ return base;
2775
+ },
2776
+ }), []);
2777
+ const getMenuGeometry = react.useCallback(() => menuGeometryRef.current, []);
2632
2778
  const customComponents = useCustomComponents({
2633
2779
  disabled, // intentionally not evaluated as boolean, since it can be object too!
2634
2780
  readOnly: readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
@@ -2642,6 +2788,7 @@ const useSelect = (props) => {
2642
2788
  className: stableRestProps.className,
2643
2789
  autoComplete: stableRestProps.autoComplete,
2644
2790
  formatOptionLabel,
2791
+ getMenuGeometry,
2645
2792
  });
2646
2793
  const interactable = react.useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
2647
2794
  // Determine the portal target for the menu
@@ -2649,6 +2796,12 @@ const useSelect = (props) => {
2649
2796
  // Use custom scroll blocking hook to prevent layout shifts
2650
2797
  // Pass the portal target so we only block scroll when menu is portaled to document.body
2651
2798
  const { blockScroll, restoreScroll } = reactComponents.useScrollBlock(portalTarget);
2799
+ // Forces react-select to redo its up/down flip decision on scroll/resize while the menu stays
2800
+ // open -- see useMenuPlacementRecompute's own comment for why this is otherwise stuck at the
2801
+ // decision made when the menu was first opened. `onMenuOpen`/`onMenuClose` are stable
2802
+ // (useCallback with no deps), so destructuring them here doesn't affect handleMenuOpen/
2803
+ // handleMenuClose's own stability below -- only `tick` (a primitive) is expected to change.
2804
+ const { tick: menuPlacementRecomputeTick, onMenuOpen: startMenuRepositionWatcher, onMenuClose: stopMenuRepositionWatcher, } = useMenuPlacementRecompute();
2652
2805
  // Store only the wrapped callbacks in refs to keep them stable
2653
2806
  // We only do this for onMenuOpen and onMenuClose because we wrap them with additional logic
2654
2807
  // Other callbacks are passed through directly and should trigger recalculation when they change
@@ -2666,13 +2819,15 @@ const useSelect = (props) => {
2666
2819
  // See comment next to menuShouldBlockScroll below for more
2667
2820
  const handleMenuOpen = react.useCallback(() => {
2668
2821
  blockScroll();
2822
+ startMenuRepositionWatcher();
2669
2823
  onMenuOpenRef.current?.();
2670
- }, [blockScroll]);
2824
+ }, [blockScroll, startMenuRepositionWatcher]);
2671
2825
  // Wrap user's onMenuClose callback to restore scrolling
2672
2826
  const handleMenuClose = react.useCallback(() => {
2673
2827
  restoreScroll();
2828
+ stopMenuRepositionWatcher();
2674
2829
  onMenuCloseRef.current?.();
2675
- }, [restoreScroll]);
2830
+ }, [restoreScroll, stopMenuRepositionWatcher]);
2676
2831
  // Final memoized props object for react-select
2677
2832
  // Stability strategy:
2678
2833
  // 1. stableRestProps - deep-equality memoized to prevent reference changes
@@ -2751,11 +2906,14 @@ const useSelect = (props) => {
2751
2906
  onMenuOpen: handleMenuOpen,
2752
2907
  onMenuClose: handleMenuClose,
2753
2908
  // 👇 putting these here to avoid them _accidentally_ being overwritten in the future👇
2754
- maxMenuHeight: undefined, // controlled custom components styling
2909
+ // Not "controlled custom components styling" like `minMenuHeight` below -- this value is
2910
+ // otherwise inert (see useMenuPlacementRecompute), it only exists to force react-select to
2911
+ // redo its up/down flip decision whenever it changes.
2912
+ maxMenuHeight: menuPlacementRecomputeTick,
2755
2913
  minMenuHeight: undefined, // controlled custom components styling
2756
2914
  theme: undefined,
2757
2915
  classNames: undefined,
2758
- styles: undefined,
2916
+ styles: menuPortalStyles,
2759
2917
  };
2760
2918
  }, [
2761
2919
  stableRestProps,
@@ -2807,6 +2965,8 @@ const useSelect = (props) => {
2807
2965
  onMenuScrollToBottom,
2808
2966
  handleMenuOpen,
2809
2967
  handleMenuClose,
2968
+ menuPortalStyles,
2969
+ menuPlacementRecomputeTick,
2810
2970
  ]);
2811
2971
  };
2812
2972
 
@@ -5704,7 +5864,6 @@ exports.toISODateStringUTC = toISODateStringUTC;
5704
5864
  exports.useCreatableSelect = useCreatableSelect;
5705
5865
  exports.useCreateInputBlurEvent = useCreateInputBlurEvent;
5706
5866
  exports.useCreateInputChangeEvent = useCreateInputChangeEvent;
5707
- exports.useCustomComponents = useCustomComponents;
5708
5867
  exports.useGetPhoneValidationRules = useGetPhoneValidationRules;
5709
5868
  exports.usePhoneInput = usePhoneInput;
5710
5869
  exports.useRadioItemChecked = useRadioItemChecked;
package/index.esm.js CHANGED
@@ -1637,7 +1637,11 @@ const cvaSelectSingleValue = cvaMerge([
1637
1637
  "text-ellipsis",
1638
1638
  "whitespace-nowrap",
1639
1639
  ]);
1640
- const cvaSelectMenu = cvaMerge([cvaMenu({ limitWidth: false }), "absolute", "w-full"], {
1640
+ // No `w-full`: useCustomComponents.tsx's custom `Menu` applies edge-aware min/max-width and
1641
+ // left/right alignment as an inline style (see utils/computeMenuGeometry.ts), so a
1642
+ // static Tailwind width class here would just compete with that. The menu sizes intrinsically
1643
+ // (shrink-to-fit, bounded by that inline min/max-width) as a result.
1644
+ const cvaSelectMenu = cvaMerge([cvaMenu({ limitWidth: false }), "absolute"], {
1641
1645
  variants: {
1642
1646
  placement: {
1643
1647
  bottom: "top-[calc(100%+var(--spacing-1))]",
@@ -2393,7 +2397,7 @@ const getVisibleCountFromGeometries = ({ geometries, availableWidth, containerX,
2393
2397
  */
2394
2398
  const useCustomComponents = ({ disabled, readOnly, "data-testid": dataTestId, prefix, hasError, fieldSize = "medium", getOptionLabelDescription, getOptionPrefix, className, isMulti, //prefer using the component prop (ala. selectValueContainer.isMulti) inside of customComponents instead of this one.
2395
2399
  autoComplete, // see https://github.com/JedWatson/react-select/issues/758
2396
- formatOptionLabel, }) => {
2400
+ formatOptionLabel, getMenuGeometry, }) => {
2397
2401
  const [t] = useTranslation();
2398
2402
  const { setValueContainerRef, setCounterRef, setFakeCounterRef, setGeometryRef, setMenuRef, getVisibleCount, getTotalCount, getCounterWidth, getIsComplete, } = useMultiValueOverflow({ skip: !isMulti });
2399
2403
  const interactable = useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
@@ -2431,10 +2435,13 @@ formatOptionLabel, }) => {
2431
2435
  Control: selectControl => {
2432
2436
  return (jsxs(components.Control, { ...selectControl, className: cvaSelectControl({
2433
2437
  className: selectControl.className,
2434
- }), getStyles: getNoStyles, innerProps: Boolean(readOnly)
2435
- ? // We omit the onMouseDown and onTouchEnd events to allow text selection
2436
- omit(selectControl.innerProps, ["onMouseDown", "onTouchEnd"])
2437
- : selectControl.innerProps, children: [prefix !== undefined ? (jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, selectControl.children, typeof disabled === "object" ? (jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-disabled-locked` : null, children: jsx(InputLockReasonTooltip, { ...disabled }) })) : null, typeof readOnly === "object" && !Boolean(disabled) ? (jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-readonly-locked` : null, children: jsx(InputLockReasonTooltip, { ...readOnly }) })) : null] }));
2438
+ }), getStyles: getNoStyles, innerProps: {
2439
+ ...(Boolean(readOnly)
2440
+ ? // We omit the onMouseDown and onTouchEnd events to allow text selection
2441
+ omit(selectControl.innerProps, ["onMouseDown", "onTouchEnd"])
2442
+ : selectControl.innerProps),
2443
+ ...(dataTestId ? { "data-testid": `${dataTestId}-control` } : {}),
2444
+ }, children: [prefix !== undefined ? (jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-prefix` : null, children: prefix })) : null, selectControl.children, typeof disabled === "object" ? (jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-disabled-locked` : null, children: jsx(InputLockReasonTooltip, { ...disabled }) })) : null, typeof readOnly === "object" && !Boolean(disabled) ? (jsx("div", { className: cvaSelectPrefixSuffix(), "data-testid": dataTestId ? `${dataTestId}-readonly-locked` : null, children: jsx(InputLockReasonTooltip, { ...readOnly }) })) : null] }));
2438
2445
  },
2439
2446
  Placeholder: selectPlaceholder => {
2440
2447
  return (jsx(components.Placeholder, { ...selectPlaceholder, className: cvaSelectPlaceholder({ className: selectPlaceholder.className }), getStyles: getNoStyles, children: selectPlaceholder.children }));
@@ -2506,11 +2513,22 @@ formatOptionLabel, }) => {
2506
2513
  Menu: selectMenu => {
2507
2514
  return (jsx(components.Menu, { ...selectMenu, className: cvaSelectMenu({ className: selectMenu.className, placement: selectMenu.placement }), getStyles: getNoStyles, innerProps: {
2508
2515
  ...selectMenu.innerProps,
2509
- ref: useMergeRefs([setMenuRef, selectMenu.innerProps.ref]),
2510
- } }));
2516
+ ...(dataTestId ? { "data-testid": `${dataTestId}-menu` } : {}),
2517
+ style: getMenuGeometryStyle(getMenuGeometry()),
2518
+ },
2519
+ // `innerRef` (not `innerProps.ref`) is the actual DOM ref react-select's `MenuPlacer` reads
2520
+ // to measure available space and decide whether to flip the menu open upward -- merging our
2521
+ // own ref into `innerProps.ref` instead used to silently overwrite that ref (since
2522
+ // `components.Menu` applies `innerProps` after `innerRef` when building the element's props),
2523
+ // leaving `MenuPlacer`'s measurement permanently null and its placement decision stuck at
2524
+ // the "bottom" default regardless of available space.
2525
+ innerRef: useMergeRefs([setMenuRef, selectMenu.innerRef]) }));
2511
2526
  },
2512
2527
  MenuPortal: selectMenuPortal => {
2513
- return (jsx(components.MenuPortal, { ...selectMenuPortal, className: "!z-overlay", children: selectMenuPortal.children }));
2528
+ return (jsx(components.MenuPortal, { ...selectMenuPortal, className: "!z-popover", innerProps: {
2529
+ ...selectMenuPortal.innerProps,
2530
+ ...(dataTestId ? { "data-testid": `${dataTestId}-menu-portal` } : {}),
2531
+ }, children: selectMenuPortal.children }));
2514
2532
  },
2515
2533
  MenuList: selectMenuList => {
2516
2534
  return (jsx(components.MenuList, { className: cvaSelectMenuList({
@@ -2570,6 +2588,7 @@ formatOptionLabel, }) => {
2570
2588
  t,
2571
2589
  setGeometryRef,
2572
2590
  setMenuRef,
2591
+ getMenuGeometry,
2573
2592
  ]);
2574
2593
  return customComponents;
2575
2594
  };
@@ -2577,6 +2596,22 @@ const getNoStyles = () => {
2577
2596
  // To prevent the default styles from being applied from react-select.
2578
2597
  return {};
2579
2598
  };
2599
+ /**
2600
+ * Translates the computed geometry into the menu's positioning/sizing inline style: grows/shrinks
2601
+ * to fit content between `minWidth` (the control's own width) and `maxWidth` (the available space
2602
+ * on the anchored side), flipping to right-aligned ("poking out" left of the control) when needed.
2603
+ */
2604
+ const getMenuGeometryStyle = (geometry) => {
2605
+ if (!geometry) {
2606
+ return undefined;
2607
+ }
2608
+ return {
2609
+ left: geometry.alignRight ? undefined : 0,
2610
+ right: geometry.alignRight ? 0 : undefined,
2611
+ minWidth: geometry.minWidth,
2612
+ maxWidth: geometry.maxWidth,
2613
+ };
2614
+ };
2580
2615
  const getInputComponent = (children) => {
2581
2616
  return Array.isArray(children)
2582
2617
  ? children.find(child => child !== null && child !== undefined && /react-select-\d+-input/.test(child?.props?.id))
@@ -2595,6 +2630,104 @@ const getPlaceholderElement = (children) => {
2595
2630
  : null;
2596
2631
  };
2597
2632
 
2633
+ // Mirrors Popover's PADDING (libs/react/components/src/components/Popover/PopoverSizing.ts) so the
2634
+ // dropdown keeps the same edge gap as other floating design-system elements.
2635
+ const EDGE_PADDING = 12;
2636
+ /**
2637
+ * Derives the dropdown's alignment/sizing from the control's rect and the viewport:
2638
+ * - flips to right-aligned ("poking out" left of the control) only when the control no
2639
+ * longer fits to the right AND flipping actually buys more room
2640
+ * - the width ceiling on the anchored side (bounded by the viewport edge, minus padding)
2641
+ *
2642
+ * It's a bare function rather than a hook because it's a pure, synchronous transform with no
2643
+ * React state of its own, called from inside `useSelect.ts`'s `styles.menuPortal` (react-select's
2644
+ * own extension point, not ours) -- there's nowhere in that call chain to invoke a hook.
2645
+ *
2646
+ * Its result is applied as an inline style on the actual menu box by useCustomComponents.tsx's
2647
+ * custom `Menu` component (via a `getMenuGeometry` getter), not on the portal itself: `MenuPortal`
2648
+ * is a zero-size, out-of-flow wrapper, and an absolutely-positioned child (the menu) doesn't
2649
+ * contribute to its shrink-to-fit sizing, so styling the portal can't drive the visible menu's
2650
+ * width or position.
2651
+ */
2652
+ const computeMenuGeometry = (rect) => {
2653
+ const controlRight = rect.left + rect.width;
2654
+ const spaceRight = window.innerWidth - rect.left - EDGE_PADDING;
2655
+ const spaceLeft = controlRight - EDGE_PADDING;
2656
+ const alignRight = spaceRight < rect.width && spaceLeft > spaceRight;
2657
+ return {
2658
+ alignRight,
2659
+ minWidth: rect.width,
2660
+ maxWidth: alignRight ? spaceLeft : spaceRight,
2661
+ };
2662
+ };
2663
+
2664
+ // Exported purely so tests can advance a real timer by an exact, non-guessed amount -- see this
2665
+ // hook's own comment for why a trailing debounce (not a per-frame rAF throttle) is what's needed.
2666
+ const MENU_PLACEMENT_RECOMPUTE_DEBOUNCE_MS = 120;
2667
+ /**
2668
+ * react-select's own `MenuPlacer` (internal to the library) decides the escaped dropdown's
2669
+ * up/down flip exactly once per open, inside a `useLayoutEffect` gated on `[maxMenuHeight,
2670
+ * menuPlacement, menuPosition, menuShouldScrollIntoView, minMenuHeight, controlHeight]` --
2671
+ * none of which change on their own when the control's position shifts relative to the viewport
2672
+ * (e.g. scrolling the modal/drawer/page the select sits in without closing the menu). A menu that
2673
+ * correctly opened "up" (or "down") can end up clipped once the layout around it moves.
2674
+ *
2675
+ * `useSelect.ts` feeds the returned `tick` into `maxMenuHeight` -- the one prop in that dependency
2676
+ * list safe to toggle purely to force a fresh measurement, with no other side effect: the
2677
+ * resulting `maxHeight` react-select computes is never consumed on our end (the custom
2678
+ * `Menu`/`MenuList` in useCustomComponents.tsx disable react-select's per-slot styling via
2679
+ * `getStyles={getNoStyles}` and constrain height with Tailwind's `cvaMenuList` instead) -- only
2680
+ * the *change* in value, not the value itself, matters. `minMenuHeight` must stay untouched:
2681
+ * passing any finite number there (instead of `undefined`) would make react-select treat "the
2682
+ * menu barely fits, if constrained" as true almost everywhere, short-circuiting past the actual
2683
+ * flip check.
2684
+ *
2685
+ * Deliberately a trailing *debounce*, not a per-frame (rAF) throttle: `resize` (and, to a lesser
2686
+ * extent, `scroll`) fires continuously while the user is actively dragging, so a control that
2687
+ * happens to sit right at the fits-below/doesn't-fit-below threshold would otherwise have its
2688
+ * flip decision -- and therefore the whole menu -- recomputed and visibly toggle top/bottom on
2689
+ * practically every frame of the drag. Waiting until the gesture actually settles means the menu
2690
+ * still only flips (at most) once, to whatever's correct for the final size/position.
2691
+ */
2692
+ const useMenuPlacementRecompute = () => {
2693
+ const [tick, setTick] = useState(0);
2694
+ const cleanupRef = useRef(null);
2695
+ const onMenuOpen = useCallback(() => {
2696
+ if (cleanupRef.current) {
2697
+ return; // already watching
2698
+ }
2699
+ let timeoutId = null;
2700
+ const handleReposition = () => {
2701
+ if (timeoutId !== null) {
2702
+ clearTimeout(timeoutId);
2703
+ }
2704
+ timeoutId = setTimeout(() => {
2705
+ timeoutId = null;
2706
+ setTick(previousTick => previousTick + 1);
2707
+ }, MENU_PLACEMENT_RECOMPUTE_DEBOUNCE_MS);
2708
+ };
2709
+ // `capture: true` is required to observe scroll events from nested scrollable ancestors (e.g.
2710
+ // a Modal/Drawer body) -- "scroll" doesn't bubble, but it's still dispatched through the
2711
+ // capture phase on every ancestor, including window.
2712
+ window.addEventListener("scroll", handleReposition, { capture: true, passive: true });
2713
+ window.addEventListener("resize", handleReposition, { passive: true });
2714
+ cleanupRef.current = () => {
2715
+ window.removeEventListener("scroll", handleReposition, { capture: true });
2716
+ window.removeEventListener("resize", handleReposition);
2717
+ if (timeoutId !== null) {
2718
+ clearTimeout(timeoutId);
2719
+ }
2720
+ };
2721
+ }, []);
2722
+ const onMenuClose = useCallback(() => {
2723
+ cleanupRef.current?.();
2724
+ cleanupRef.current = null;
2725
+ }, []);
2726
+ // Stop watching if the component unmounts while the menu is still open.
2727
+ useEffect(() => onMenuClose, [onMenuClose]);
2728
+ return useMemo(() => ({ tick, onMenuOpen, onMenuClose }), [tick, onMenuOpen, onMenuClose]);
2729
+ };
2730
+
2598
2731
  /**
2599
2732
  * A hook used by selects to share the common code
2600
2733
  *
@@ -2628,6 +2761,19 @@ const useSelect = (props) => {
2628
2761
  });
2629
2762
  }
2630
2763
  }, [restProps, stableRestProps]);
2764
+ // `styles.menuPortal` is the only place react-select exposes the control's rect. The custom
2765
+ // `Menu` component (useCustomComponents.tsx) is what actually renders the visible box, but it
2766
+ // disables react-select's own `styles.menu` callback (`getStyles={getNoStyles}`) in favor of
2767
+ // Tailwind classes, so the geometry can't be delivered through `styles` -- this ref hands it to
2768
+ // `getMenuGeometry` below instead, read directly as inline styles by that custom component.
2769
+ const menuGeometryRef = useRef(null);
2770
+ const menuPortalStyles = useMemo(() => ({
2771
+ menuPortal: (base, { rect }) => {
2772
+ menuGeometryRef.current = computeMenuGeometry(rect);
2773
+ return base;
2774
+ },
2775
+ }), []);
2776
+ const getMenuGeometry = useCallback(() => menuGeometryRef.current, []);
2631
2777
  const customComponents = useCustomComponents({
2632
2778
  disabled, // intentionally not evaluated as boolean, since it can be object too!
2633
2779
  readOnly: readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
@@ -2641,6 +2787,7 @@ const useSelect = (props) => {
2641
2787
  className: stableRestProps.className,
2642
2788
  autoComplete: stableRestProps.autoComplete,
2643
2789
  formatOptionLabel,
2790
+ getMenuGeometry,
2644
2791
  });
2645
2792
  const interactable = useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
2646
2793
  // Determine the portal target for the menu
@@ -2648,6 +2795,12 @@ const useSelect = (props) => {
2648
2795
  // Use custom scroll blocking hook to prevent layout shifts
2649
2796
  // Pass the portal target so we only block scroll when menu is portaled to document.body
2650
2797
  const { blockScroll, restoreScroll } = useScrollBlock(portalTarget);
2798
+ // Forces react-select to redo its up/down flip decision on scroll/resize while the menu stays
2799
+ // open -- see useMenuPlacementRecompute's own comment for why this is otherwise stuck at the
2800
+ // decision made when the menu was first opened. `onMenuOpen`/`onMenuClose` are stable
2801
+ // (useCallback with no deps), so destructuring them here doesn't affect handleMenuOpen/
2802
+ // handleMenuClose's own stability below -- only `tick` (a primitive) is expected to change.
2803
+ const { tick: menuPlacementRecomputeTick, onMenuOpen: startMenuRepositionWatcher, onMenuClose: stopMenuRepositionWatcher, } = useMenuPlacementRecompute();
2651
2804
  // Store only the wrapped callbacks in refs to keep them stable
2652
2805
  // We only do this for onMenuOpen and onMenuClose because we wrap them with additional logic
2653
2806
  // Other callbacks are passed through directly and should trigger recalculation when they change
@@ -2665,13 +2818,15 @@ const useSelect = (props) => {
2665
2818
  // See comment next to menuShouldBlockScroll below for more
2666
2819
  const handleMenuOpen = useCallback(() => {
2667
2820
  blockScroll();
2821
+ startMenuRepositionWatcher();
2668
2822
  onMenuOpenRef.current?.();
2669
- }, [blockScroll]);
2823
+ }, [blockScroll, startMenuRepositionWatcher]);
2670
2824
  // Wrap user's onMenuClose callback to restore scrolling
2671
2825
  const handleMenuClose = useCallback(() => {
2672
2826
  restoreScroll();
2827
+ stopMenuRepositionWatcher();
2673
2828
  onMenuCloseRef.current?.();
2674
- }, [restoreScroll]);
2829
+ }, [restoreScroll, stopMenuRepositionWatcher]);
2675
2830
  // Final memoized props object for react-select
2676
2831
  // Stability strategy:
2677
2832
  // 1. stableRestProps - deep-equality memoized to prevent reference changes
@@ -2750,11 +2905,14 @@ const useSelect = (props) => {
2750
2905
  onMenuOpen: handleMenuOpen,
2751
2906
  onMenuClose: handleMenuClose,
2752
2907
  // 👇 putting these here to avoid them _accidentally_ being overwritten in the future👇
2753
- maxMenuHeight: undefined, // controlled custom components styling
2908
+ // Not "controlled custom components styling" like `minMenuHeight` below -- this value is
2909
+ // otherwise inert (see useMenuPlacementRecompute), it only exists to force react-select to
2910
+ // redo its up/down flip decision whenever it changes.
2911
+ maxMenuHeight: menuPlacementRecomputeTick,
2754
2912
  minMenuHeight: undefined, // controlled custom components styling
2755
2913
  theme: undefined,
2756
2914
  classNames: undefined,
2757
- styles: undefined,
2915
+ styles: menuPortalStyles,
2758
2916
  };
2759
2917
  }, [
2760
2918
  stableRestProps,
@@ -2806,6 +2964,8 @@ const useSelect = (props) => {
2806
2964
  onMenuScrollToBottom,
2807
2965
  handleMenuOpen,
2808
2966
  handleMenuClose,
2967
+ menuPortalStyles,
2968
+ menuPlacementRecomputeTick,
2809
2969
  ]);
2810
2970
  };
2811
2971
 
@@ -5610,4 +5770,4 @@ const useZodValidators = () => {
5610
5770
  */
5611
5771
  setupLibraryTranslations();
5612
5772
 
5613
- export { ActionButton, BaseInput, BaseSelect, Checkbox, CheckboxField, ColorField, CreatableSelect, CreatableSelectField, DEFAULT_TIME, DateBaseInput, DateField, DropZone, DropZoneDefaultLabel, EMAIL_REGEX, EmailField, FormFieldSelectAdapter, FormGroup, Label, MultiSelectField, NumberBaseInput, NumberField, OptionCard, PasswordBaseInput, PasswordField, PhoneBaseInput, PhoneField, PhoneFieldWithController, RadioGroup, RadioGroupContext, RadioItem, Schedule, ScheduleVariant, Search, SelectField, TextAreaBaseInput, TextAreaField, TextBaseInput, TextField, TimeRange, TimeRangeField, ToggleSwitch, ToggleSwitchOption, UploadField, UploadInput, UrlField, checkIfPhoneNumberHasPlus, countryCodeToFlagEmoji, cvaAccessoriesContainer, cvaActionButton, cvaActionContainer, cvaInput$1 as cvaInput, cvaInputAddon, cvaInputBase, cvaInputBaseDisabled, cvaInputBaseInvalid, cvaInputBaseReadOnly, cvaInputBaseSize, cvaInputElement, cvaInputGroup, cvaInputItemPlacementManager, cvaInputPrefix, cvaInputSuffix, cvaLabel, cvaRadioItem, cvaSelectClearIndicator, cvaSelectContainer, cvaSelectControl, cvaSelectDropdownIconContainer, cvaSelectDropdownIndicator, cvaSelectIndicatorsContainer, cvaSelectLoadingMessage, cvaSelectMenu, cvaSelectMenuList, cvaSelectMultiValue, cvaSelectNoOptionsMessage, cvaSelectPlaceholder, cvaSelectPrefixSuffix, cvaSelectSingleValue, cvaSelectValueContainer, dateToISODateUTC, getCountryAbbreviation, getPhoneNumberWithPlus, isInvalidCountryCode, isInvalidPhoneNumber, isValidHEXColor, parseDateFieldValue, parseSchedule, phoneErrorMessage, serializeSchedule, toISODateStringUTC, useCreatableSelect, useCreateInputBlurEvent, useCreateInputChangeEvent, useCustomComponents, useGetPhoneValidationRules, usePhoneInput, useRadioItemChecked, useSelect, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };
5773
+ export { ActionButton, BaseInput, BaseSelect, Checkbox, CheckboxField, ColorField, CreatableSelect, CreatableSelectField, DEFAULT_TIME, DateBaseInput, DateField, DropZone, DropZoneDefaultLabel, EMAIL_REGEX, EmailField, FormFieldSelectAdapter, FormGroup, Label, MultiSelectField, NumberBaseInput, NumberField, OptionCard, PasswordBaseInput, PasswordField, PhoneBaseInput, PhoneField, PhoneFieldWithController, RadioGroup, RadioGroupContext, RadioItem, Schedule, ScheduleVariant, Search, SelectField, TextAreaBaseInput, TextAreaField, TextBaseInput, TextField, TimeRange, TimeRangeField, ToggleSwitch, ToggleSwitchOption, UploadField, UploadInput, UrlField, checkIfPhoneNumberHasPlus, countryCodeToFlagEmoji, cvaAccessoriesContainer, cvaActionButton, cvaActionContainer, cvaInput$1 as cvaInput, cvaInputAddon, cvaInputBase, cvaInputBaseDisabled, cvaInputBaseInvalid, cvaInputBaseReadOnly, cvaInputBaseSize, cvaInputElement, cvaInputGroup, cvaInputItemPlacementManager, cvaInputPrefix, cvaInputSuffix, cvaLabel, cvaRadioItem, cvaSelectClearIndicator, cvaSelectContainer, cvaSelectControl, cvaSelectDropdownIconContainer, cvaSelectDropdownIndicator, cvaSelectIndicatorsContainer, cvaSelectLoadingMessage, cvaSelectMenu, cvaSelectMenuList, cvaSelectMultiValue, cvaSelectNoOptionsMessage, cvaSelectPlaceholder, cvaSelectPrefixSuffix, cvaSelectSingleValue, cvaSelectValueContainer, dateToISODateUTC, getCountryAbbreviation, getPhoneNumberWithPlus, isInvalidCountryCode, isInvalidPhoneNumber, isValidHEXColor, parseDateFieldValue, parseSchedule, phoneErrorMessage, serializeSchedule, toISODateStringUTC, useCreatableSelect, useCreateInputBlurEvent, useCreateInputChangeEvent, useGetPhoneValidationRules, usePhoneInput, useRadioItemChecked, useSelect, useZodValidators, validateEmailAddress, validatePhoneNumber, weekDay };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/form-components/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["// Migration entry point for @nx/js:tsc build target.\n// Migrations are registered in ../migrations.json and resolved by NX at runtime.\nexport {};\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-utils.js","sourceRoot":"","sources":["../../../../../../libs/react/form-components/migrations/utils/jsx-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { logger, type Tree, visitNotIgnoredFiles } from \"@nx/devkit\";\nimport { tsquery } from \"@phenomnomnominal/tsquery\";\nimport * as ts from \"typescript\";\n\nconst TSX_EXTENSIONS = [\".tsx\", \".jsx\"] as const;\nconst TS_EXTENSIONS = [\".ts\", \".tsx\", \".jsx\"] as const;\n\nconst isTsxFile = (filePath: string): boolean => TSX_EXTENSIONS.some(ext => filePath.endsWith(ext));\nconst isTsFile = (filePath: string): boolean => TS_EXTENSIONS.some(ext => filePath.endsWith(ext));\n\nconst isUnderNodeModules = (filePath: string): boolean =>\n filePath.includes(\"/node_modules/\") || filePath.startsWith(\"node_modules/\");\n\nconst isUnderDist = (filePath: string): boolean =>\n filePath.includes(\"/dist/\") || filePath.startsWith(\"dist/\") || filePath.includes(\"/.nx/\");\n\n/**\n * Visits every `.tsx`/`.jsx` file under the workspace root and invokes the\n * callback with the file path and its current contents. Files that don't\n * include the marker substring are skipped, which makes large workspaces\n * cheap to scan.\n */\nexport const visitTsxFiles = (\n tree: Tree,\n marker: string,\n callback: (filePath: string, content: string) => string | null | undefined\n): number => {\n let touched = 0;\n visitNotIgnoredFiles(tree, \"/\", filePath => {\n if (!isTsxFile(filePath)) return;\n if (isUnderNodeModules(filePath) || isUnderDist(filePath)) return;\n const content = tree.read(filePath, \"utf-8\");\n if (content === null) return;\n if (!content.includes(marker)) return;\n const updated = callback(filePath, content);\n if (updated !== null && updated !== undefined && updated !== content) {\n tree.write(filePath, updated);\n touched += 1;\n }\n });\n return touched;\n};\n\n/**\n * Same as {@link visitTsxFiles} but also includes plain `.ts` files. Use this\n * when the codemod also rewrites non-JSX files such as helpers or hooks.\n */\nexport const visitTsFiles = (\n tree: Tree,\n marker: string,\n callback: (filePath: string, content: string) => string | null | undefined\n): number => {\n let touched = 0;\n visitNotIgnoredFiles(tree, \"/\", filePath => {\n if (!isTsFile(filePath)) return;\n if (isUnderNodeModules(filePath) || isUnderDist(filePath)) return;\n const content = tree.read(filePath, \"utf-8\");\n if (content === null) return;\n if (!content.includes(marker)) return;\n const updated = callback(filePath, content);\n if (updated !== null && updated !== undefined && updated !== content) {\n tree.write(filePath, updated);\n touched += 1;\n }\n });\n return touched;\n};\n\n/**\n * Mapping of imported component aliases to their original names for a given\n * package. For `import { IconButton as Btn } from \"@trackunit/react-components\"`\n * this returns `{ Btn: \"IconButton\" }`.\n *\n * Returns `null` when the file imports nothing from the package, which lets\n * callers short-circuit before parsing JSX.\n */\nexport const getImportedAliases = (sourceFile: ts.SourceFile, packageName: string): Record<string, string> | null => {\n const result: Record<string, string> = {};\n let found = false;\n\n for (const stmt of sourceFile.statements) {\n if (!ts.isImportDeclaration(stmt)) continue;\n const moduleSpecifier = stmt.moduleSpecifier;\n if (!ts.isStringLiteral(moduleSpecifier)) continue;\n if (moduleSpecifier.text !== packageName) continue;\n\n const namedBindings = stmt.importClause?.namedBindings;\n if (namedBindings === undefined || !ts.isNamedImports(namedBindings)) continue;\n\n for (const element of namedBindings.elements) {\n const localName = element.name.text;\n const importedName = element.propertyName?.text ?? localName;\n result[localName] = importedName;\n found = true;\n }\n }\n\n return found ? result : null;\n};\n\n/**\n * Returns the local alias used in this file for `originalName` when imported\n * from `packageName`, or `null` if the component is not imported.\n */\nexport const getLocalAliasFor = (\n sourceFile: ts.SourceFile,\n packageName: string,\n originalName: string\n): string | null => {\n const aliases = getImportedAliases(sourceFile, packageName);\n if (aliases === null) return null;\n for (const [local, original] of Object.entries(aliases)) {\n if (original === originalName) return local;\n }\n return null;\n};\n\nexport type JsxElementMatch = {\n /** The opening JSX element (the one carrying the attributes). */\n openingElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement;\n /** The full element including children, or the self-closing element. */\n element: ts.JsxElement | ts.JsxSelfClosingElement;\n};\n\n/**\n * Finds every JSX element whose tag name resolves to one of `tagNames`\n * (typically the local aliases returned by {@link getImportedAliases}).\n *\n * The result intentionally includes both opening and full elements so callers\n * can manipulate attributes (via `openingElement`) or wrap/replace the whole\n * element (via `element`).\n */\nexport const findJsxElements = (\n sourceFile: ts.SourceFile,\n tagNames: ReadonlyArray<string>\n): ReadonlyArray<JsxElementMatch> => {\n const matches: Array<JsxElementMatch> = [];\n const tagSet = new Set(tagNames);\n\n const visit = (node: ts.Node): void => {\n if (ts.isJsxSelfClosingElement(node)) {\n if (ts.isIdentifier(node.tagName) && tagSet.has(node.tagName.text)) {\n matches.push({ openingElement: node, element: node });\n }\n } else if (ts.isJsxElement(node)) {\n const opening = node.openingElement;\n if (ts.isIdentifier(opening.tagName) && tagSet.has(opening.tagName.text)) {\n matches.push({ openingElement: opening, element: node });\n }\n }\n ts.forEachChild(node, visit);\n };\n\n visit(sourceFile);\n return matches;\n};\n\n/**\n * Looks up a named JSX attribute on the opening element. Returns `null` when\n * the attribute is missing or part of a spread attribute (which we cannot\n * statically inspect).\n */\nexport const findJsxAttribute = (\n openingElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement,\n attributeName: string\n): ts.JsxAttribute | null => {\n for (const attr of openingElement.attributes.properties) {\n if (!ts.isJsxAttribute(attr)) continue;\n if (!ts.isIdentifier(attr.name)) continue;\n if (attr.name.text === attributeName) return attr;\n }\n return null;\n};\n\n/**\n * `true` when the element forwards a spread expression such as `{...rest}`,\n * which means we can't be sure which attributes are actually set.\n */\nexport const hasSpreadAttribute = (openingElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement): boolean => {\n return openingElement.attributes.properties.some(prop => ts.isJsxSpreadAttribute(prop));\n};\n\n/**\n * Parses the file as TSX so JSX is recognised. We do not need a full program\n * here; tsquery's `ast` helper produces a script-kind source file which loses\n * JSX context, so we go through `createSourceFile` directly.\n */\nexport const parseTsx = (content: string, fileName = \"file.tsx\"): ts.SourceFile =>\n ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);\n\n/** Re-exported for codemods that need direct AST queries. */\nexport { tsquery };\n\n/**\n * Helper for codemods to log a one-line summary at the end of a run. Migration\n * runners aggregate logs per migration, so this keeps output focused.\n */\nexport const logSummary = (migrationName: string, touched: number, manualReviewNeeded?: number): void => {\n if (touched === 0) {\n logger.info(` ${migrationName}: no files changed`);\n return;\n }\n const reviewSuffix =\n manualReviewNeeded !== undefined && manualReviewNeeded > 0\n ? ` (${manualReviewNeeded} location(s) need manual review)`\n : \"\";\n logger.info(` ${migrationName}: updated ${touched} file(s)${reviewSuffix}`);\n};\n\n/**\n * Inserts an attribute string immediately after the tag name (so the new\n * attribute appears first). Returns the resulting source content.\n *\n * The added attribute is rendered verbatim, so callers are responsible for\n * including the leading space (e.g. ` title=\"Close\"`).\n */\nexport const insertAttributeIntoOpeningTag = (\n content: string,\n openingElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement,\n attributeText: string\n): string => {\n const tagName = openingElement.tagName;\n const insertPos = tagName.getEnd();\n return content.slice(0, insertPos) + ` ${attributeText}` + content.slice(insertPos);\n};\n"]}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actionbutton-add-title.js","sourceRoot":"","sources":["../../../../../../libs/react/form-components/migrations/v2-0-0/actionbutton-add-title.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Tree, logger } from \"@nx/devkit\";\nimport * as ts from \"typescript\";\nimport {\n findJsxAttribute,\n findJsxElements,\n getImportedAliases,\n hasSpreadAttribute,\n logSummary,\n parseTsx,\n visitTsxFiles,\n} from \"../utils/jsx-utils\";\n\nconst PACKAGE_NAME = \"@trackunit/react-form-components\";\nconst COMPONENT_NAME = \"ActionButton\";\n\nconst ACTION_TYPE_TO_TITLE: Record<string, string> = {\n COPY: \"Copy value\",\n EDIT: \"Edit value\",\n EMAIL: \"Send email\",\n PHONE_NUMBER: \"Call number\",\n WEB_ADDRESS: \"Open link\",\n};\n\nconst DEFAULT_TITLE = \"Action\";\n\nconst resolveTitleForType = (typeAttr: ts.JsxAttribute | null): string => {\n if (typeAttr === null) return DEFAULT_TITLE;\n const initializer = typeAttr.initializer;\n if (initializer === undefined) return DEFAULT_TITLE;\n if (ts.isStringLiteral(initializer)) {\n return ACTION_TYPE_TO_TITLE[initializer.text] ?? DEFAULT_TITLE;\n }\n return DEFAULT_TITLE;\n};\n\nconst transformActionButtonUsage = (filePath: string, content: string): string | null => {\n const sourceFile = parseTsx(content, filePath);\n\n const aliases = getImportedAliases(sourceFile, PACKAGE_NAME);\n if (aliases === null) return null;\n\n const localAlias = Object.entries(aliases).find(([, name]) => name === COMPONENT_NAME)?.[0];\n if (localAlias === undefined) return null;\n\n const matches = findJsxElements(sourceFile, [localAlias]);\n if (matches.length === 0) return null;\n\n type Edit = { offset: number; text: string };\n const edits: Array<Edit> = [];\n\n for (const { openingElement } of matches) {\n if (hasSpreadAttribute(openingElement)) continue;\n if (findJsxAttribute(openingElement, \"title\") !== null) continue;\n\n const typeAttr = findJsxAttribute(openingElement, \"type\");\n const title = resolveTitleForType(typeAttr);\n edits.push({\n offset: openingElement.tagName.getEnd(),\n text: ` title=\"${title}\"`,\n });\n }\n\n if (edits.length === 0) return null;\n\n edits.sort((a, b) => (a.offset === b.offset ? 0 : b.offset - a.offset));\n\n let updated = content;\n for (const { offset, text } of edits) {\n updated = updated.slice(0, offset) + text + updated.slice(offset);\n }\n return updated;\n};\n\n/**\n * Adds the now-required `title` prop to `<ActionButton>` usages that don't\n * already provide one. The default value is derived from the `type` prop when\n * possible (e.g. `COPY` → \"Copy value\"); consumers should replace the\n * placeholders with their own localized strings.\n */\nexport const actionButtonAddTitle = (tree: Tree): void => {\n const touched = visitTsxFiles(tree, \"ActionButton\", transformActionButtonUsage);\n logSummary(\"actionbutton-add-title\", touched);\n if (touched > 0) {\n logger.info(` Replace placeholder ActionButton titles with your own localized strings.`);\n }\n};\n\nexport default actionButtonAddTitle;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-form-components",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "migrations": "./migrations.json",
@@ -7,6 +7,7 @@ import { ReactNode } from "react";
7
7
  import { type FormatOptionLabelMeta, type SelectComponentsConfig, GroupBase } from "react-select";
8
8
  import { FormComponentSizes } from "../../types";
9
9
  import { LockedForReasons } from "../BaseInput/InputLockReasonTooltip";
10
+ import type { MenuGeometry } from "./utils/computeMenuGeometry";
10
11
  interface CustomComponentsProps<TOption> extends CommonProps {
11
12
  disabled: boolean | LockedForReasons;
12
13
  className?: string;
@@ -20,6 +21,13 @@ interface CustomComponentsProps<TOption> extends CommonProps {
20
21
  isMulti: boolean;
21
22
  autoComplete?: "off" | "on" | "no";
22
23
  formatOptionLabel?: (data: TOption, meta: FormatOptionLabelMeta<TOption>) => ReactNode;
24
+ /**
25
+ * Returns the latest edge-aware menu geometry (alignment + min/max-width), computed from the
26
+ * control's rect via `styles.menuPortal` in useSelect.ts. Read directly here (not through
27
+ * react-select's own `styles.menu`, which this file disables via `getStyles={getNoStyles}`
28
+ * in favor of Tailwind classes) and applied as inline styles on the actual menu element.
29
+ */
30
+ getMenuGeometry: () => MenuGeometry | null;
23
31
  }
24
32
  /**
25
33
  * A hook to retrieve components override object.
@@ -31,5 +39,5 @@ interface CustomComponentsProps<TOption> extends CommonProps {
31
39
  * @param {CustomComponentsProps<TOption>} props - The custom components props
32
40
  * @returns {SelectComponentsConfig<TOption, TIsMulti, TGroup>} components object to override react-select default components
33
41
  */
34
- export declare const useCustomComponents: <TOption, TIsMulti extends boolean = false, TGroup extends GroupBase<TOption> = GroupBase<TOption>>({ disabled, readOnly, "data-testid": dataTestId, prefix, hasError, fieldSize, getOptionLabelDescription, getOptionPrefix, className, isMulti, autoComplete, formatOptionLabel, }: CustomComponentsProps<TOption>) => SelectComponentsConfig<TOption, TIsMulti, TGroup>;
42
+ export declare const useCustomComponents: <TOption, TIsMulti extends boolean = false, TGroup extends GroupBase<TOption> = GroupBase<TOption>>({ disabled, readOnly, "data-testid": dataTestId, prefix, hasError, fieldSize, getOptionLabelDescription, getOptionPrefix, className, isMulti, autoComplete, formatOptionLabel, getMenuGeometry, }: CustomComponentsProps<TOption>) => SelectComponentsConfig<TOption, TIsMulti, TGroup>;
35
43
  export {};
@@ -0,0 +1,26 @@
1
+ import { GroupBase, StylesConfig } from "react-select";
2
+ type MenuPortalStyleFn = NonNullable<StylesConfig<unknown, boolean, GroupBase<unknown>>["menuPortal"]>;
3
+ type PortalStyleArgs = Parameters<MenuPortalStyleFn>[1];
4
+ export interface MenuGeometry {
5
+ alignRight: boolean;
6
+ minWidth: number;
7
+ maxWidth: number;
8
+ }
9
+ /**
10
+ * Derives the dropdown's alignment/sizing from the control's rect and the viewport:
11
+ * - flips to right-aligned ("poking out" left of the control) only when the control no
12
+ * longer fits to the right AND flipping actually buys more room
13
+ * - the width ceiling on the anchored side (bounded by the viewport edge, minus padding)
14
+ *
15
+ * It's a bare function rather than a hook because it's a pure, synchronous transform with no
16
+ * React state of its own, called from inside `useSelect.ts`'s `styles.menuPortal` (react-select's
17
+ * own extension point, not ours) -- there's nowhere in that call chain to invoke a hook.
18
+ *
19
+ * Its result is applied as an inline style on the actual menu box by useCustomComponents.tsx's
20
+ * custom `Menu` component (via a `getMenuGeometry` getter), not on the portal itself: `MenuPortal`
21
+ * is a zero-size, out-of-flow wrapper, and an absolutely-positioned child (the menu) doesn't
22
+ * contribute to its shrink-to-fit sizing, so styling the portal can't drive the visible menu's
23
+ * width or position.
24
+ */
25
+ export declare const computeMenuGeometry: (rect: PortalStyleArgs["rect"]) => MenuGeometry;
26
+ export {};
@@ -0,0 +1,31 @@
1
+ export declare const MENU_PLACEMENT_RECOMPUTE_DEBOUNCE_MS = 120;
2
+ /**
3
+ * react-select's own `MenuPlacer` (internal to the library) decides the escaped dropdown's
4
+ * up/down flip exactly once per open, inside a `useLayoutEffect` gated on `[maxMenuHeight,
5
+ * menuPlacement, menuPosition, menuShouldScrollIntoView, minMenuHeight, controlHeight]` --
6
+ * none of which change on their own when the control's position shifts relative to the viewport
7
+ * (e.g. scrolling the modal/drawer/page the select sits in without closing the menu). A menu that
8
+ * correctly opened "up" (or "down") can end up clipped once the layout around it moves.
9
+ *
10
+ * `useSelect.ts` feeds the returned `tick` into `maxMenuHeight` -- the one prop in that dependency
11
+ * list safe to toggle purely to force a fresh measurement, with no other side effect: the
12
+ * resulting `maxHeight` react-select computes is never consumed on our end (the custom
13
+ * `Menu`/`MenuList` in useCustomComponents.tsx disable react-select's per-slot styling via
14
+ * `getStyles={getNoStyles}` and constrain height with Tailwind's `cvaMenuList` instead) -- only
15
+ * the *change* in value, not the value itself, matters. `minMenuHeight` must stay untouched:
16
+ * passing any finite number there (instead of `undefined`) would make react-select treat "the
17
+ * menu barely fits, if constrained" as true almost everywhere, short-circuiting past the actual
18
+ * flip check.
19
+ *
20
+ * Deliberately a trailing *debounce*, not a per-frame (rAF) throttle: `resize` (and, to a lesser
21
+ * extent, `scroll`) fires continuously while the user is actively dragging, so a control that
22
+ * happens to sit right at the fits-below/doesn't-fit-below threshold would otherwise have its
23
+ * flip decision -- and therefore the whole menu -- recomputed and visibly toggle top/bottom on
24
+ * practically every frame of the drag. Waiting until the gesture actually settles means the menu
25
+ * still only flips (at most) once, to whatever's correct for the final size/position.
26
+ */
27
+ export declare const useMenuPlacementRecompute: () => {
28
+ tick: number;
29
+ onMenuOpen: () => void;
30
+ onMenuClose: () => void;
31
+ };
package/src/index.d.ts CHANGED
@@ -12,8 +12,7 @@ export * from "./components/BaseSelect/BaseSelect";
12
12
  export * from "./components/BaseSelect/BaseSelect.variants";
13
13
  export * from "./components/BaseSelect/CreatableSelect";
14
14
  export * from "./components/BaseSelect/useCreatableSelect";
15
- export * from "./components/BaseSelect/useCustomComponents";
16
- export * from "./components/BaseSelect/useSelect";
15
+ export { type AsyncSelect, type SelectProps, useSelect } from "./components/BaseSelect/useSelect";
17
16
  export * from "./components/Checkbox/Checkbox";
18
17
  export * from "./components/CheckboxField/CheckboxField";
19
18
  export * from "./components/ColorField/ColorField";