@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 +173 -14
- package/index.esm.js +174 -14
- package/migrations/entry.js.map +1 -0
- package/migrations/utils/jsx-utils.js.map +1 -0
- package/migrations/v2-0-0/actionbutton-add-title.js.map +1 -0
- package/package.json +1 -1
- package/src/components/BaseSelect/useCustomComponents.d.ts +9 -1
- package/src/components/BaseSelect/utils/computeMenuGeometry.d.ts +26 -0
- package/src/components/BaseSelect/utils/useMenuPlacementRecompute.d.ts +31 -0
- package/src/index.d.ts +1 -2
package/index.cjs.js
CHANGED
|
@@ -1638,7 +1638,11 @@ const cvaSelectSingleValue = cssClassVarianceUtilities.cvaMerge([
|
|
|
1638
1638
|
"text-ellipsis",
|
|
1639
1639
|
"whitespace-nowrap",
|
|
1640
1640
|
]);
|
|
1641
|
-
|
|
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:
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
2435
|
-
|
|
2436
|
-
omit
|
|
2437
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
@@ -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
|
|
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";
|