@trackunit/react-form-components 1.8.128 → 1.8.132
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
|
@@ -1577,10 +1577,10 @@ autoComplete, // see https://github.com/JedWatson/react-select/issues/758
|
|
|
1577
1577
|
return (jsxRuntime.jsx(ReactSelect.components.ClearIndicator, { ...selectClearIndicator, className: cvaSelectClearIndicator({ className: selectClearIndicator.className }), getStyles: getNoStyles, children: jsxRuntime.jsx(reactComponents.Icon, { ariaLabel: t("clearIndicator.icon.tooltip.clearAll"), "data-testid": `${dataTestId}-XMarkIcon`, name: "XCircle", size: "medium" }) }));
|
|
1578
1578
|
},
|
|
1579
1579
|
LoadingMessage: selectLoadingMessage => {
|
|
1580
|
-
return (jsxRuntime.jsx(ReactSelect.components.LoadingMessage, { ...selectLoadingMessage, className: cvaSelectLoadingMessage({ className: selectLoadingMessage.className }), getStyles: getNoStyles, children: t("select.loadingMessage") }));
|
|
1580
|
+
return (jsxRuntime.jsx(ReactSelect.components.LoadingMessage, { ...selectLoadingMessage, className: cvaSelectLoadingMessage({ className: selectLoadingMessage.className }), getStyles: getNoStyles, children: selectLoadingMessage.children ?? t("select.loadingMessage") }));
|
|
1581
1581
|
},
|
|
1582
1582
|
NoOptionsMessage: selectNoOptionsMessage => {
|
|
1583
|
-
return (jsxRuntime.jsx(ReactSelect.components.NoOptionsMessage, { ...selectNoOptionsMessage, className: cvaSelectNoOptionsMessage({ className: selectNoOptionsMessage.className }), getStyles: getNoStyles, children: t("select.noOptionsMessage") }));
|
|
1583
|
+
return (jsxRuntime.jsx(ReactSelect.components.NoOptionsMessage, { ...selectNoOptionsMessage, className: cvaSelectNoOptionsMessage({ className: selectNoOptionsMessage.className }), getStyles: getNoStyles, children: selectNoOptionsMessage.children ?? t("select.noOptionsMessage") }));
|
|
1584
1584
|
},
|
|
1585
1585
|
SingleValue: selectSingleValue => {
|
|
1586
1586
|
const optionPrefix = getOptionPrefix ? getOptionPrefix(selectSingleValue.data) : null;
|
|
@@ -1696,34 +1696,55 @@ const getPlaceholderElement = (children) => {
|
|
|
1696
1696
|
* @param {SelectProps} props - The props for the Select component
|
|
1697
1697
|
* @returns {ReactSelectProps} Props for react-select component
|
|
1698
1698
|
*/
|
|
1699
|
-
const useSelect = (
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
//
|
|
1705
|
-
|
|
1699
|
+
const useSelect = (props) => {
|
|
1700
|
+
const { disabled = false, //renaming to isDisabled, so not directly passed to react-select
|
|
1701
|
+
"data-testid": dataTestId,
|
|
1702
|
+
// Extract critical props that must trigger recalculation when they change
|
|
1703
|
+
options, value, defaultValue,
|
|
1704
|
+
// Extract callbacks - most are passed through directly to allow recalculation when changed
|
|
1705
|
+
// Only onMenuOpen and onMenuClose get wrapped with ref pattern (see below)
|
|
1706
|
+
onMenuOpen, onMenuClose, onChange, onInputChange, onFocus, onBlur, getOptionLabel, getOptionValue, filterOption, formatOptionLabel, formatGroupLabel, isOptionDisabled, isOptionSelected, onKeyDown, onMenuScrollToTop, onMenuScrollToBottom,
|
|
1707
|
+
// Extract other props used in final useMemo
|
|
1708
|
+
label, isSearchable, openMenuOnClick, openMenuOnFocus, isMulti, isClearable, menuPlacement, placeholder, hideSelectedOptions, readOnly,
|
|
1709
|
+
// Extract commonly-used props to prevent them from ending up in restProps
|
|
1710
|
+
// This makes dependency tracking more explicit and improves stability
|
|
1711
|
+
id, name, inputId, isLoading, async: asyncProp, required, tabIndex, closeMenuOnScroll, escapeClearsValue, backspaceRemovesValue, noOptionsMessage, loadingMessage, controlShouldRenderValue, ...restProps } = props;
|
|
1712
|
+
// Deep equality memoization for restProps to achieve hook stability
|
|
1713
|
+
// Problem: restProps is a new object on every render, causing unnecessary recalculations
|
|
1714
|
+
// Solution: Use deep equality check with isEqual to only update when actual values change
|
|
1715
|
+
// Trade-off: Small performance cost of deep comparison vs stability benefits
|
|
1716
|
+
const [stableRestProps, setStableRestProps] = react.useState(restProps);
|
|
1717
|
+
react.useEffect(() => {
|
|
1718
|
+
if (!esToolkit.isEqual(stableRestProps, restProps)) {
|
|
1719
|
+
// setState in useEffect is intentional here for deep equality memoization
|
|
1720
|
+
// We only update stableRestProps when the actual values change, not just the object reference
|
|
1721
|
+
queueMicrotask(() => {
|
|
1722
|
+
setStableRestProps(restProps);
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}, [restProps, stableRestProps]);
|
|
1706
1726
|
const customComponents = useCustomComponents({
|
|
1707
1727
|
disabled, // intentionally not evaluated as boolean, since it can be object too!
|
|
1708
|
-
readOnly:
|
|
1728
|
+
readOnly: readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
|
|
1709
1729
|
"data-testid": dataTestId ?? "select",
|
|
1710
|
-
prefix:
|
|
1711
|
-
hasError:
|
|
1712
|
-
fieldSize:
|
|
1713
|
-
getOptionLabelDescription:
|
|
1714
|
-
getOptionPrefix:
|
|
1715
|
-
isMulti:
|
|
1716
|
-
className:
|
|
1717
|
-
autoComplete:
|
|
1730
|
+
prefix: stableRestProps.prefix,
|
|
1731
|
+
hasError: stableRestProps.hasError,
|
|
1732
|
+
fieldSize: stableRestProps.fieldSize,
|
|
1733
|
+
getOptionLabelDescription: stableRestProps.getOptionLabelDescription,
|
|
1734
|
+
getOptionPrefix: stableRestProps.getOptionPrefix,
|
|
1735
|
+
isMulti: isMulti ?? false,
|
|
1736
|
+
className: stableRestProps.className,
|
|
1737
|
+
autoComplete: stableRestProps.autoComplete,
|
|
1718
1738
|
});
|
|
1719
|
-
const interactable = react.useMemo(() => !Boolean(disabled) && !Boolean(
|
|
1739
|
+
const interactable = react.useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
|
|
1720
1740
|
// Determine the portal target for the menu
|
|
1721
|
-
const portalTarget = react.useMemo(() => (
|
|
1741
|
+
const portalTarget = react.useMemo(() => (stableRestProps.menuPortalTarget !== undefined ? stableRestProps.menuPortalTarget : document.body), [stableRestProps.menuPortalTarget]);
|
|
1722
1742
|
// Use custom scroll blocking hook to prevent layout shifts
|
|
1723
1743
|
// Pass the portal target so we only block scroll when menu is portaled to document.body
|
|
1724
1744
|
const { blockScroll, restoreScroll } = reactComponents.useScrollBlock(portalTarget);
|
|
1725
|
-
// Store callbacks in refs to keep
|
|
1726
|
-
//
|
|
1745
|
+
// Store only the wrapped callbacks in refs to keep them stable
|
|
1746
|
+
// We only do this for onMenuOpen and onMenuClose because we wrap them with additional logic
|
|
1747
|
+
// Other callbacks are passed through directly and should trigger recalculation when they change
|
|
1727
1748
|
const onMenuOpenRef = react.useRef(onMenuOpen);
|
|
1728
1749
|
const onMenuCloseRef = react.useRef(onMenuClose);
|
|
1729
1750
|
react.useEffect(() => {
|
|
@@ -1735,7 +1756,7 @@ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
|
|
|
1735
1756
|
// Wrap user's onMenuOpen callback to block scrolling
|
|
1736
1757
|
// We apply scroll blocking directly instead of using react-select's menuShouldBlockScroll
|
|
1737
1758
|
// because it doesn't properly account for existing body padding, causing layout shifts
|
|
1738
|
-
// See
|
|
1759
|
+
// See comment next to menuShouldBlockScroll below for more
|
|
1739
1760
|
const handleMenuOpen = react.useCallback(() => {
|
|
1740
1761
|
blockScroll();
|
|
1741
1762
|
onMenuOpenRef.current?.();
|
|
@@ -1745,28 +1766,36 @@ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
|
|
|
1745
1766
|
restoreScroll();
|
|
1746
1767
|
onMenuCloseRef.current?.();
|
|
1747
1768
|
}, [restoreScroll]);
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1752
|
-
//
|
|
1753
|
-
//
|
|
1754
|
-
const restPropsRef = react.useRef(restProps);
|
|
1755
|
-
react.useEffect(() => {
|
|
1756
|
-
restPropsRef.current = restProps;
|
|
1757
|
-
}, [restProps]);
|
|
1758
|
-
// eslint-disable-next-line react-hooks/refs
|
|
1769
|
+
// Final memoized props object for react-select
|
|
1770
|
+
// Stability strategy:
|
|
1771
|
+
// 1. stableRestProps - deep-equality memoized to prevent reference changes
|
|
1772
|
+
// 2. Extracted props - explicitly listed in dependency array for clear tracking
|
|
1773
|
+
// 3. Callbacks - most passed through directly to allow recalculation when changed
|
|
1774
|
+
// 4. handleMenuOpen/handleMenuClose - wrapped with ref pattern because they contain custom logic
|
|
1759
1775
|
return react.useMemo(() => {
|
|
1760
|
-
const currentRestProps = restPropsRef.current;
|
|
1761
1776
|
return {
|
|
1762
|
-
...
|
|
1777
|
+
...stableRestProps,
|
|
1778
|
+
// Explicitly set extracted props
|
|
1779
|
+
id,
|
|
1780
|
+
name,
|
|
1781
|
+
inputId,
|
|
1782
|
+
isLoading,
|
|
1783
|
+
async: asyncProp,
|
|
1784
|
+
required,
|
|
1785
|
+
tabIndex,
|
|
1786
|
+
closeMenuOnScroll,
|
|
1787
|
+
escapeClearsValue,
|
|
1788
|
+
backspaceRemovesValue,
|
|
1789
|
+
noOptionsMessage,
|
|
1790
|
+
loadingMessage,
|
|
1791
|
+
controlShouldRenderValue,
|
|
1792
|
+
// Core props
|
|
1763
1793
|
options,
|
|
1764
1794
|
value,
|
|
1765
|
-
onChange,
|
|
1766
1795
|
defaultValue,
|
|
1767
1796
|
components: customComponents,
|
|
1768
1797
|
unstyled: true,
|
|
1769
|
-
"aria-label":
|
|
1798
|
+
"aria-label": label,
|
|
1770
1799
|
"data-testid": dataTestId ?? "select",
|
|
1771
1800
|
tabSelectsValue: false,
|
|
1772
1801
|
blurInputOnSelect: false,
|
|
@@ -1774,22 +1803,38 @@ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
|
|
|
1774
1803
|
// Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
|
|
1775
1804
|
// the parent element instead of 'document.body'.
|
|
1776
1805
|
menuPortalTarget: portalTarget,
|
|
1777
|
-
isSearchable: interactable ? (
|
|
1806
|
+
isSearchable: interactable ? (isSearchable ?? true) : false,
|
|
1778
1807
|
// Disable react-select's built-in scroll blocking as we handle it ourselves in onMenuOpen/onMenuClose
|
|
1779
1808
|
// to prevent layout shifts caused by not accounting for existing body padding in the react-select implementation.
|
|
1780
1809
|
// See: https://github.com/JedWatson/react-select/issues/5342 AND https://github.com/JedWatson/react-select/issues/5020
|
|
1781
1810
|
menuShouldBlockScroll: false,
|
|
1782
1811
|
menuShouldScrollIntoView: true,
|
|
1783
|
-
openMenuOnClick: interactable ? (
|
|
1784
|
-
openMenuOnFocus: Boolean(
|
|
1785
|
-
|
|
1812
|
+
openMenuOnClick: interactable ? (openMenuOnClick ?? true) : false,
|
|
1813
|
+
openMenuOnFocus: Boolean(openMenuOnFocus),
|
|
1814
|
+
isMulti,
|
|
1815
|
+
closeMenuOnSelect: !Boolean(isMulti),
|
|
1786
1816
|
isDisabled: Boolean(disabled),
|
|
1787
|
-
isClearable: Boolean(
|
|
1788
|
-
menuPlacement:
|
|
1789
|
-
placeholder: interactable ?
|
|
1790
|
-
hideSelectedOptions: Boolean(
|
|
1817
|
+
isClearable: Boolean(isClearable),
|
|
1818
|
+
menuPlacement: menuPlacement ?? "auto",
|
|
1819
|
+
placeholder: interactable ? placeholder : undefined,
|
|
1820
|
+
hideSelectedOptions: Boolean(hideSelectedOptions),
|
|
1791
1821
|
menuIsOpen: interactable ? undefined : false, // close it if not interactable, otherwise leave state to react-select
|
|
1792
|
-
//
|
|
1822
|
+
// Pass through callbacks directly (they should trigger recalculation when changed)
|
|
1823
|
+
onChange,
|
|
1824
|
+
onInputChange,
|
|
1825
|
+
onFocus,
|
|
1826
|
+
onBlur,
|
|
1827
|
+
getOptionLabel,
|
|
1828
|
+
getOptionValue,
|
|
1829
|
+
filterOption,
|
|
1830
|
+
formatOptionLabel,
|
|
1831
|
+
formatGroupLabel,
|
|
1832
|
+
isOptionDisabled,
|
|
1833
|
+
isOptionSelected,
|
|
1834
|
+
onKeyDown,
|
|
1835
|
+
onMenuScrollToTop,
|
|
1836
|
+
onMenuScrollToBottom,
|
|
1837
|
+
// Wire up our wrapped menu open/close handlers (with scroll blocking)
|
|
1793
1838
|
onMenuOpen: handleMenuOpen,
|
|
1794
1839
|
onMenuClose: handleMenuClose,
|
|
1795
1840
|
// 👇 putting these here to avoid them _accidentally_ being overwritten in the future👇
|
|
@@ -1800,20 +1845,55 @@ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
|
|
|
1800
1845
|
styles: undefined,
|
|
1801
1846
|
};
|
|
1802
1847
|
}, [
|
|
1848
|
+
stableRestProps,
|
|
1849
|
+
// Explicitly extracted props
|
|
1850
|
+
id,
|
|
1851
|
+
name,
|
|
1852
|
+
inputId,
|
|
1853
|
+
isLoading,
|
|
1854
|
+
asyncProp,
|
|
1855
|
+
required,
|
|
1856
|
+
tabIndex,
|
|
1857
|
+
closeMenuOnScroll,
|
|
1858
|
+
escapeClearsValue,
|
|
1859
|
+
backspaceRemovesValue,
|
|
1860
|
+
noOptionsMessage,
|
|
1861
|
+
loadingMessage,
|
|
1862
|
+
controlShouldRenderValue,
|
|
1863
|
+
// Core props
|
|
1864
|
+
options,
|
|
1865
|
+
value,
|
|
1866
|
+
defaultValue,
|
|
1803
1867
|
customComponents,
|
|
1804
|
-
|
|
1805
|
-
interactable,
|
|
1806
|
-
handleMenuOpen,
|
|
1807
|
-
handleMenuClose,
|
|
1868
|
+
label,
|
|
1808
1869
|
dataTestId,
|
|
1809
1870
|
portalTarget,
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1871
|
+
interactable,
|
|
1872
|
+
isSearchable,
|
|
1873
|
+
openMenuOnClick,
|
|
1874
|
+
openMenuOnFocus,
|
|
1875
|
+
isMulti,
|
|
1876
|
+
disabled,
|
|
1877
|
+
isClearable,
|
|
1878
|
+
menuPlacement,
|
|
1879
|
+
placeholder,
|
|
1880
|
+
hideSelectedOptions,
|
|
1813
1881
|
onChange,
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1882
|
+
onInputChange,
|
|
1883
|
+
onFocus,
|
|
1884
|
+
onBlur,
|
|
1885
|
+
getOptionLabel,
|
|
1886
|
+
getOptionValue,
|
|
1887
|
+
filterOption,
|
|
1888
|
+
formatOptionLabel,
|
|
1889
|
+
formatGroupLabel,
|
|
1890
|
+
isOptionDisabled,
|
|
1891
|
+
isOptionSelected,
|
|
1892
|
+
onKeyDown,
|
|
1893
|
+
onMenuScrollToTop,
|
|
1894
|
+
onMenuScrollToBottom,
|
|
1895
|
+
handleMenuOpen,
|
|
1896
|
+
handleMenuClose,
|
|
1817
1897
|
]);
|
|
1818
1898
|
};
|
|
1819
1899
|
|
|
@@ -1833,26 +1913,41 @@ const BaseSelect = (props) => {
|
|
|
1833
1913
|
/**
|
|
1834
1914
|
* A hook used by creatable selects that extends useSelect with creatable-specific functionality
|
|
1835
1915
|
*
|
|
1916
|
+
* @template TOption - The type of option objects in the select
|
|
1917
|
+
* @template TIsAsync - Boolean flag indicating if the select is async
|
|
1918
|
+
* @template TIsMulti - Boolean flag indicating if the select allows multiple selections
|
|
1919
|
+
* @template TGroup - The group type for grouped options
|
|
1836
1920
|
* @param props - The props for the CreatableSelect component
|
|
1837
|
-
* @returns {ReactCreatableProps} Props for react-select creatable component
|
|
1921
|
+
* @returns {ReactCreatableProps<TOption, TIsMulti, TGroup>} Props for react-select creatable component
|
|
1838
1922
|
*/
|
|
1839
1923
|
const useCreatableSelect = (props) => {
|
|
1840
|
-
|
|
1841
|
-
|
|
1924
|
+
// Extract only creatable-specific props before passing to useSelect
|
|
1925
|
+
// This prevents onCreateOption from triggering recalculation in useSelect
|
|
1926
|
+
// Note: isMulti is NOT extracted - it needs to be passed to useSelect
|
|
1927
|
+
const { onCreateOption, allowCreateWhileLoading, ...selectProps } = props;
|
|
1928
|
+
const baseSelectProps = useSelect(selectProps);
|
|
1929
|
+
// Get isMulti from props for use in this hook
|
|
1930
|
+
const { isMulti } = props;
|
|
1842
1931
|
// Store onCreateOption in a ref to keep wrapper stable
|
|
1843
1932
|
// This prevents unnecessary re-renders when parent components don't memoize this callback
|
|
1844
1933
|
const onCreateOptionRef = react.useRef(onCreateOption);
|
|
1845
1934
|
react.useEffect(() => {
|
|
1846
1935
|
onCreateOptionRef.current = onCreateOption;
|
|
1847
1936
|
}, [onCreateOption]);
|
|
1937
|
+
// Create stable wrapper for onCreateOption callback
|
|
1938
|
+
const handleCreateOption = react.useCallback(inputValue => {
|
|
1939
|
+
onCreateOptionRef.current?.(inputValue);
|
|
1940
|
+
}, []);
|
|
1941
|
+
// Track callback existence with stable boolean
|
|
1942
|
+
const hasOnCreateOption = Boolean(onCreateOption);
|
|
1848
1943
|
return react.useMemo(() => ({
|
|
1849
1944
|
...baseSelectProps,
|
|
1850
1945
|
allowCreateWhileLoading,
|
|
1851
|
-
onCreateOption:
|
|
1946
|
+
onCreateOption: hasOnCreateOption ? handleCreateOption : undefined,
|
|
1852
1947
|
// Override some defaults specific to creatable selects
|
|
1853
1948
|
closeMenuOnSelect: false, // Keep menu open for multi-creation
|
|
1854
1949
|
blurInputOnSelect: !Boolean(isMulti), // Only blur if not multi
|
|
1855
|
-
}), [baseSelectProps, allowCreateWhileLoading, isMulti]);
|
|
1950
|
+
}), [baseSelectProps, allowCreateWhileLoading, hasOnCreateOption, handleCreateOption, isMulti]);
|
|
1856
1951
|
};
|
|
1857
1952
|
|
|
1858
1953
|
/**
|
package/index.esm.js
CHANGED
|
@@ -1576,10 +1576,10 @@ autoComplete, // see https://github.com/JedWatson/react-select/issues/758
|
|
|
1576
1576
|
return (jsx(components.ClearIndicator, { ...selectClearIndicator, className: cvaSelectClearIndicator({ className: selectClearIndicator.className }), getStyles: getNoStyles, children: jsx(Icon, { ariaLabel: t("clearIndicator.icon.tooltip.clearAll"), "data-testid": `${dataTestId}-XMarkIcon`, name: "XCircle", size: "medium" }) }));
|
|
1577
1577
|
},
|
|
1578
1578
|
LoadingMessage: selectLoadingMessage => {
|
|
1579
|
-
return (jsx(components.LoadingMessage, { ...selectLoadingMessage, className: cvaSelectLoadingMessage({ className: selectLoadingMessage.className }), getStyles: getNoStyles, children: t("select.loadingMessage") }));
|
|
1579
|
+
return (jsx(components.LoadingMessage, { ...selectLoadingMessage, className: cvaSelectLoadingMessage({ className: selectLoadingMessage.className }), getStyles: getNoStyles, children: selectLoadingMessage.children ?? t("select.loadingMessage") }));
|
|
1580
1580
|
},
|
|
1581
1581
|
NoOptionsMessage: selectNoOptionsMessage => {
|
|
1582
|
-
return (jsx(components.NoOptionsMessage, { ...selectNoOptionsMessage, className: cvaSelectNoOptionsMessage({ className: selectNoOptionsMessage.className }), getStyles: getNoStyles, children: t("select.noOptionsMessage") }));
|
|
1582
|
+
return (jsx(components.NoOptionsMessage, { ...selectNoOptionsMessage, className: cvaSelectNoOptionsMessage({ className: selectNoOptionsMessage.className }), getStyles: getNoStyles, children: selectNoOptionsMessage.children ?? t("select.noOptionsMessage") }));
|
|
1583
1583
|
},
|
|
1584
1584
|
SingleValue: selectSingleValue => {
|
|
1585
1585
|
const optionPrefix = getOptionPrefix ? getOptionPrefix(selectSingleValue.data) : null;
|
|
@@ -1695,34 +1695,55 @@ const getPlaceholderElement = (children) => {
|
|
|
1695
1695
|
* @param {SelectProps} props - The props for the Select component
|
|
1696
1696
|
* @returns {ReactSelectProps} Props for react-select component
|
|
1697
1697
|
*/
|
|
1698
|
-
const useSelect = (
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
//
|
|
1704
|
-
|
|
1698
|
+
const useSelect = (props) => {
|
|
1699
|
+
const { disabled = false, //renaming to isDisabled, so not directly passed to react-select
|
|
1700
|
+
"data-testid": dataTestId,
|
|
1701
|
+
// Extract critical props that must trigger recalculation when they change
|
|
1702
|
+
options, value, defaultValue,
|
|
1703
|
+
// Extract callbacks - most are passed through directly to allow recalculation when changed
|
|
1704
|
+
// Only onMenuOpen and onMenuClose get wrapped with ref pattern (see below)
|
|
1705
|
+
onMenuOpen, onMenuClose, onChange, onInputChange, onFocus, onBlur, getOptionLabel, getOptionValue, filterOption, formatOptionLabel, formatGroupLabel, isOptionDisabled, isOptionSelected, onKeyDown, onMenuScrollToTop, onMenuScrollToBottom,
|
|
1706
|
+
// Extract other props used in final useMemo
|
|
1707
|
+
label, isSearchable, openMenuOnClick, openMenuOnFocus, isMulti, isClearable, menuPlacement, placeholder, hideSelectedOptions, readOnly,
|
|
1708
|
+
// Extract commonly-used props to prevent them from ending up in restProps
|
|
1709
|
+
// This makes dependency tracking more explicit and improves stability
|
|
1710
|
+
id, name, inputId, isLoading, async: asyncProp, required, tabIndex, closeMenuOnScroll, escapeClearsValue, backspaceRemovesValue, noOptionsMessage, loadingMessage, controlShouldRenderValue, ...restProps } = props;
|
|
1711
|
+
// Deep equality memoization for restProps to achieve hook stability
|
|
1712
|
+
// Problem: restProps is a new object on every render, causing unnecessary recalculations
|
|
1713
|
+
// Solution: Use deep equality check with isEqual to only update when actual values change
|
|
1714
|
+
// Trade-off: Small performance cost of deep comparison vs stability benefits
|
|
1715
|
+
const [stableRestProps, setStableRestProps] = useState(restProps);
|
|
1716
|
+
useEffect(() => {
|
|
1717
|
+
if (!isEqual(stableRestProps, restProps)) {
|
|
1718
|
+
// setState in useEffect is intentional here for deep equality memoization
|
|
1719
|
+
// We only update stableRestProps when the actual values change, not just the object reference
|
|
1720
|
+
queueMicrotask(() => {
|
|
1721
|
+
setStableRestProps(restProps);
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
}, [restProps, stableRestProps]);
|
|
1705
1725
|
const customComponents = useCustomComponents({
|
|
1706
1726
|
disabled, // intentionally not evaluated as boolean, since it can be object too!
|
|
1707
|
-
readOnly:
|
|
1727
|
+
readOnly: readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
|
|
1708
1728
|
"data-testid": dataTestId ?? "select",
|
|
1709
|
-
prefix:
|
|
1710
|
-
hasError:
|
|
1711
|
-
fieldSize:
|
|
1712
|
-
getOptionLabelDescription:
|
|
1713
|
-
getOptionPrefix:
|
|
1714
|
-
isMulti:
|
|
1715
|
-
className:
|
|
1716
|
-
autoComplete:
|
|
1729
|
+
prefix: stableRestProps.prefix,
|
|
1730
|
+
hasError: stableRestProps.hasError,
|
|
1731
|
+
fieldSize: stableRestProps.fieldSize,
|
|
1732
|
+
getOptionLabelDescription: stableRestProps.getOptionLabelDescription,
|
|
1733
|
+
getOptionPrefix: stableRestProps.getOptionPrefix,
|
|
1734
|
+
isMulti: isMulti ?? false,
|
|
1735
|
+
className: stableRestProps.className,
|
|
1736
|
+
autoComplete: stableRestProps.autoComplete,
|
|
1717
1737
|
});
|
|
1718
|
-
const interactable = useMemo(() => !Boolean(disabled) && !Boolean(
|
|
1738
|
+
const interactable = useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
|
|
1719
1739
|
// Determine the portal target for the menu
|
|
1720
|
-
const portalTarget = useMemo(() => (
|
|
1740
|
+
const portalTarget = useMemo(() => (stableRestProps.menuPortalTarget !== undefined ? stableRestProps.menuPortalTarget : document.body), [stableRestProps.menuPortalTarget]);
|
|
1721
1741
|
// Use custom scroll blocking hook to prevent layout shifts
|
|
1722
1742
|
// Pass the portal target so we only block scroll when menu is portaled to document.body
|
|
1723
1743
|
const { blockScroll, restoreScroll } = useScrollBlock(portalTarget);
|
|
1724
|
-
// Store callbacks in refs to keep
|
|
1725
|
-
//
|
|
1744
|
+
// Store only the wrapped callbacks in refs to keep them stable
|
|
1745
|
+
// We only do this for onMenuOpen and onMenuClose because we wrap them with additional logic
|
|
1746
|
+
// Other callbacks are passed through directly and should trigger recalculation when they change
|
|
1726
1747
|
const onMenuOpenRef = useRef(onMenuOpen);
|
|
1727
1748
|
const onMenuCloseRef = useRef(onMenuClose);
|
|
1728
1749
|
useEffect(() => {
|
|
@@ -1734,7 +1755,7 @@ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
|
|
|
1734
1755
|
// Wrap user's onMenuOpen callback to block scrolling
|
|
1735
1756
|
// We apply scroll blocking directly instead of using react-select's menuShouldBlockScroll
|
|
1736
1757
|
// because it doesn't properly account for existing body padding, causing layout shifts
|
|
1737
|
-
// See
|
|
1758
|
+
// See comment next to menuShouldBlockScroll below for more
|
|
1738
1759
|
const handleMenuOpen = useCallback(() => {
|
|
1739
1760
|
blockScroll();
|
|
1740
1761
|
onMenuOpenRef.current?.();
|
|
@@ -1744,28 +1765,36 @@ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
|
|
|
1744
1765
|
restoreScroll();
|
|
1745
1766
|
onMenuCloseRef.current?.();
|
|
1746
1767
|
}, [restoreScroll]);
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
//
|
|
1752
|
-
//
|
|
1753
|
-
const restPropsRef = useRef(restProps);
|
|
1754
|
-
useEffect(() => {
|
|
1755
|
-
restPropsRef.current = restProps;
|
|
1756
|
-
}, [restProps]);
|
|
1757
|
-
// eslint-disable-next-line react-hooks/refs
|
|
1768
|
+
// Final memoized props object for react-select
|
|
1769
|
+
// Stability strategy:
|
|
1770
|
+
// 1. stableRestProps - deep-equality memoized to prevent reference changes
|
|
1771
|
+
// 2. Extracted props - explicitly listed in dependency array for clear tracking
|
|
1772
|
+
// 3. Callbacks - most passed through directly to allow recalculation when changed
|
|
1773
|
+
// 4. handleMenuOpen/handleMenuClose - wrapped with ref pattern because they contain custom logic
|
|
1758
1774
|
return useMemo(() => {
|
|
1759
|
-
const currentRestProps = restPropsRef.current;
|
|
1760
1775
|
return {
|
|
1761
|
-
...
|
|
1776
|
+
...stableRestProps,
|
|
1777
|
+
// Explicitly set extracted props
|
|
1778
|
+
id,
|
|
1779
|
+
name,
|
|
1780
|
+
inputId,
|
|
1781
|
+
isLoading,
|
|
1782
|
+
async: asyncProp,
|
|
1783
|
+
required,
|
|
1784
|
+
tabIndex,
|
|
1785
|
+
closeMenuOnScroll,
|
|
1786
|
+
escapeClearsValue,
|
|
1787
|
+
backspaceRemovesValue,
|
|
1788
|
+
noOptionsMessage,
|
|
1789
|
+
loadingMessage,
|
|
1790
|
+
controlShouldRenderValue,
|
|
1791
|
+
// Core props
|
|
1762
1792
|
options,
|
|
1763
1793
|
value,
|
|
1764
|
-
onChange,
|
|
1765
1794
|
defaultValue,
|
|
1766
1795
|
components: customComponents,
|
|
1767
1796
|
unstyled: true,
|
|
1768
|
-
"aria-label":
|
|
1797
|
+
"aria-label": label,
|
|
1769
1798
|
"data-testid": dataTestId ?? "select",
|
|
1770
1799
|
tabSelectsValue: false,
|
|
1771
1800
|
blurInputOnSelect: false,
|
|
@@ -1773,22 +1802,38 @@ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
|
|
|
1773
1802
|
// Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
|
|
1774
1803
|
// the parent element instead of 'document.body'.
|
|
1775
1804
|
menuPortalTarget: portalTarget,
|
|
1776
|
-
isSearchable: interactable ? (
|
|
1805
|
+
isSearchable: interactable ? (isSearchable ?? true) : false,
|
|
1777
1806
|
// Disable react-select's built-in scroll blocking as we handle it ourselves in onMenuOpen/onMenuClose
|
|
1778
1807
|
// to prevent layout shifts caused by not accounting for existing body padding in the react-select implementation.
|
|
1779
1808
|
// See: https://github.com/JedWatson/react-select/issues/5342 AND https://github.com/JedWatson/react-select/issues/5020
|
|
1780
1809
|
menuShouldBlockScroll: false,
|
|
1781
1810
|
menuShouldScrollIntoView: true,
|
|
1782
|
-
openMenuOnClick: interactable ? (
|
|
1783
|
-
openMenuOnFocus: Boolean(
|
|
1784
|
-
|
|
1811
|
+
openMenuOnClick: interactable ? (openMenuOnClick ?? true) : false,
|
|
1812
|
+
openMenuOnFocus: Boolean(openMenuOnFocus),
|
|
1813
|
+
isMulti,
|
|
1814
|
+
closeMenuOnSelect: !Boolean(isMulti),
|
|
1785
1815
|
isDisabled: Boolean(disabled),
|
|
1786
|
-
isClearable: Boolean(
|
|
1787
|
-
menuPlacement:
|
|
1788
|
-
placeholder: interactable ?
|
|
1789
|
-
hideSelectedOptions: Boolean(
|
|
1816
|
+
isClearable: Boolean(isClearable),
|
|
1817
|
+
menuPlacement: menuPlacement ?? "auto",
|
|
1818
|
+
placeholder: interactable ? placeholder : undefined,
|
|
1819
|
+
hideSelectedOptions: Boolean(hideSelectedOptions),
|
|
1790
1820
|
menuIsOpen: interactable ? undefined : false, // close it if not interactable, otherwise leave state to react-select
|
|
1791
|
-
//
|
|
1821
|
+
// Pass through callbacks directly (they should trigger recalculation when changed)
|
|
1822
|
+
onChange,
|
|
1823
|
+
onInputChange,
|
|
1824
|
+
onFocus,
|
|
1825
|
+
onBlur,
|
|
1826
|
+
getOptionLabel,
|
|
1827
|
+
getOptionValue,
|
|
1828
|
+
filterOption,
|
|
1829
|
+
formatOptionLabel,
|
|
1830
|
+
formatGroupLabel,
|
|
1831
|
+
isOptionDisabled,
|
|
1832
|
+
isOptionSelected,
|
|
1833
|
+
onKeyDown,
|
|
1834
|
+
onMenuScrollToTop,
|
|
1835
|
+
onMenuScrollToBottom,
|
|
1836
|
+
// Wire up our wrapped menu open/close handlers (with scroll blocking)
|
|
1792
1837
|
onMenuOpen: handleMenuOpen,
|
|
1793
1838
|
onMenuClose: handleMenuClose,
|
|
1794
1839
|
// 👇 putting these here to avoid them _accidentally_ being overwritten in the future👇
|
|
@@ -1799,20 +1844,55 @@ onMenuOpen, onMenuClose, options, value, onChange, defaultValue,
|
|
|
1799
1844
|
styles: undefined,
|
|
1800
1845
|
};
|
|
1801
1846
|
}, [
|
|
1847
|
+
stableRestProps,
|
|
1848
|
+
// Explicitly extracted props
|
|
1849
|
+
id,
|
|
1850
|
+
name,
|
|
1851
|
+
inputId,
|
|
1852
|
+
isLoading,
|
|
1853
|
+
asyncProp,
|
|
1854
|
+
required,
|
|
1855
|
+
tabIndex,
|
|
1856
|
+
closeMenuOnScroll,
|
|
1857
|
+
escapeClearsValue,
|
|
1858
|
+
backspaceRemovesValue,
|
|
1859
|
+
noOptionsMessage,
|
|
1860
|
+
loadingMessage,
|
|
1861
|
+
controlShouldRenderValue,
|
|
1862
|
+
// Core props
|
|
1863
|
+
options,
|
|
1864
|
+
value,
|
|
1865
|
+
defaultValue,
|
|
1802
1866
|
customComponents,
|
|
1803
|
-
|
|
1804
|
-
interactable,
|
|
1805
|
-
handleMenuOpen,
|
|
1806
|
-
handleMenuClose,
|
|
1867
|
+
label,
|
|
1807
1868
|
dataTestId,
|
|
1808
1869
|
portalTarget,
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1870
|
+
interactable,
|
|
1871
|
+
isSearchable,
|
|
1872
|
+
openMenuOnClick,
|
|
1873
|
+
openMenuOnFocus,
|
|
1874
|
+
isMulti,
|
|
1875
|
+
disabled,
|
|
1876
|
+
isClearable,
|
|
1877
|
+
menuPlacement,
|
|
1878
|
+
placeholder,
|
|
1879
|
+
hideSelectedOptions,
|
|
1812
1880
|
onChange,
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1881
|
+
onInputChange,
|
|
1882
|
+
onFocus,
|
|
1883
|
+
onBlur,
|
|
1884
|
+
getOptionLabel,
|
|
1885
|
+
getOptionValue,
|
|
1886
|
+
filterOption,
|
|
1887
|
+
formatOptionLabel,
|
|
1888
|
+
formatGroupLabel,
|
|
1889
|
+
isOptionDisabled,
|
|
1890
|
+
isOptionSelected,
|
|
1891
|
+
onKeyDown,
|
|
1892
|
+
onMenuScrollToTop,
|
|
1893
|
+
onMenuScrollToBottom,
|
|
1894
|
+
handleMenuOpen,
|
|
1895
|
+
handleMenuClose,
|
|
1816
1896
|
]);
|
|
1817
1897
|
};
|
|
1818
1898
|
|
|
@@ -1832,26 +1912,41 @@ const BaseSelect = (props) => {
|
|
|
1832
1912
|
/**
|
|
1833
1913
|
* A hook used by creatable selects that extends useSelect with creatable-specific functionality
|
|
1834
1914
|
*
|
|
1915
|
+
* @template TOption - The type of option objects in the select
|
|
1916
|
+
* @template TIsAsync - Boolean flag indicating if the select is async
|
|
1917
|
+
* @template TIsMulti - Boolean flag indicating if the select allows multiple selections
|
|
1918
|
+
* @template TGroup - The group type for grouped options
|
|
1835
1919
|
* @param props - The props for the CreatableSelect component
|
|
1836
|
-
* @returns {ReactCreatableProps} Props for react-select creatable component
|
|
1920
|
+
* @returns {ReactCreatableProps<TOption, TIsMulti, TGroup>} Props for react-select creatable component
|
|
1837
1921
|
*/
|
|
1838
1922
|
const useCreatableSelect = (props) => {
|
|
1839
|
-
|
|
1840
|
-
|
|
1923
|
+
// Extract only creatable-specific props before passing to useSelect
|
|
1924
|
+
// This prevents onCreateOption from triggering recalculation in useSelect
|
|
1925
|
+
// Note: isMulti is NOT extracted - it needs to be passed to useSelect
|
|
1926
|
+
const { onCreateOption, allowCreateWhileLoading, ...selectProps } = props;
|
|
1927
|
+
const baseSelectProps = useSelect(selectProps);
|
|
1928
|
+
// Get isMulti from props for use in this hook
|
|
1929
|
+
const { isMulti } = props;
|
|
1841
1930
|
// Store onCreateOption in a ref to keep wrapper stable
|
|
1842
1931
|
// This prevents unnecessary re-renders when parent components don't memoize this callback
|
|
1843
1932
|
const onCreateOptionRef = useRef(onCreateOption);
|
|
1844
1933
|
useEffect(() => {
|
|
1845
1934
|
onCreateOptionRef.current = onCreateOption;
|
|
1846
1935
|
}, [onCreateOption]);
|
|
1936
|
+
// Create stable wrapper for onCreateOption callback
|
|
1937
|
+
const handleCreateOption = useCallback(inputValue => {
|
|
1938
|
+
onCreateOptionRef.current?.(inputValue);
|
|
1939
|
+
}, []);
|
|
1940
|
+
// Track callback existence with stable boolean
|
|
1941
|
+
const hasOnCreateOption = Boolean(onCreateOption);
|
|
1847
1942
|
return useMemo(() => ({
|
|
1848
1943
|
...baseSelectProps,
|
|
1849
1944
|
allowCreateWhileLoading,
|
|
1850
|
-
onCreateOption:
|
|
1945
|
+
onCreateOption: hasOnCreateOption ? handleCreateOption : undefined,
|
|
1851
1946
|
// Override some defaults specific to creatable selects
|
|
1852
1947
|
closeMenuOnSelect: false, // Keep menu open for multi-creation
|
|
1853
1948
|
blurInputOnSelect: !Boolean(isMulti), // Only blur if not multi
|
|
1854
|
-
}), [baseSelectProps, allowCreateWhileLoading, isMulti]);
|
|
1949
|
+
}), [baseSelectProps, allowCreateWhileLoading, hasOnCreateOption, handleCreateOption, isMulti]);
|
|
1855
1950
|
};
|
|
1856
1951
|
|
|
1857
1952
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-form-components",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.132",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"zod": "^3.23.8",
|
|
15
15
|
"react-hook-form": "7.62.0",
|
|
16
16
|
"tailwind-merge": "^2.0.0",
|
|
17
|
-
"@trackunit/css-class-variance-utilities": "1.7.
|
|
18
|
-
"@trackunit/react-components": "1.10.
|
|
19
|
-
"@trackunit/ui-icons": "1.7.
|
|
20
|
-
"@trackunit/shared-utils": "1.9.
|
|
21
|
-
"@trackunit/ui-design-tokens": "1.7.
|
|
22
|
-
"@trackunit/i18n-library-translation": "1.7.
|
|
17
|
+
"@trackunit/css-class-variance-utilities": "1.7.86",
|
|
18
|
+
"@trackunit/react-components": "1.10.61",
|
|
19
|
+
"@trackunit/ui-icons": "1.7.87",
|
|
20
|
+
"@trackunit/shared-utils": "1.9.86",
|
|
21
|
+
"@trackunit/ui-design-tokens": "1.7.86",
|
|
22
|
+
"@trackunit/i18n-library-translation": "1.7.104",
|
|
23
23
|
"string-ts": "^2.0.0",
|
|
24
24
|
"@js-temporal/polyfill": "^0.5.1",
|
|
25
25
|
"es-toolkit": "^1.39.10",
|
|
@@ -4,7 +4,11 @@ import { CreatableSelectProps } from "./CreatableSelect";
|
|
|
4
4
|
/**
|
|
5
5
|
* A hook used by creatable selects that extends useSelect with creatable-specific functionality
|
|
6
6
|
*
|
|
7
|
+
* @template TOption - The type of option objects in the select
|
|
8
|
+
* @template TIsAsync - Boolean flag indicating if the select is async
|
|
9
|
+
* @template TIsMulti - Boolean flag indicating if the select allows multiple selections
|
|
10
|
+
* @template TGroup - The group type for grouped options
|
|
7
11
|
* @param props - The props for the CreatableSelect component
|
|
8
|
-
* @returns {ReactCreatableProps} Props for react-select creatable component
|
|
12
|
+
* @returns {ReactCreatableProps<TOption, TIsMulti, TGroup>} Props for react-select creatable component
|
|
9
13
|
*/
|
|
10
14
|
export declare const useCreatableSelect: <TOption, TIsAsync extends boolean = false, TIsMulti extends boolean = false, TGroup extends GroupBase<TOption> = GroupBase<TOption>>(props: CreatableSelectProps<TOption, TIsAsync, TIsMulti, TGroup>) => ReactCreatableProps<TOption, TIsMulti, TGroup>;
|
|
@@ -134,4 +134,4 @@ export type SelectProps<TOption, TIsAsync extends boolean = false, TIsMulti exte
|
|
|
134
134
|
* @param {SelectProps} props - The props for the Select component
|
|
135
135
|
* @returns {ReactSelectProps} Props for react-select component
|
|
136
136
|
*/
|
|
137
|
-
export declare const useSelect: <TOption, TIsAsync extends boolean = false, TIsMulti extends boolean = false, TGroup extends GroupBase<TOption> = GroupBase<TOption>>(
|
|
137
|
+
export declare const useSelect: <TOption, TIsAsync extends boolean = false, TIsMulti extends boolean = false, TGroup extends GroupBase<TOption> = GroupBase<TOption>>(props: SelectProps<TOption, TIsAsync, TIsMulti, TGroup>) => ReactSelectProps<TOption, TIsMulti, TGroup>;
|