@trackunit/react-form-components 1.8.131 โ 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
|
@@ -1696,31 +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
|
-
|
|
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]);
|
|
1703
1726
|
const customComponents = useCustomComponents({
|
|
1704
1727
|
disabled, // intentionally not evaluated as boolean, since it can be object too!
|
|
1705
|
-
readOnly:
|
|
1728
|
+
readOnly: readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
|
|
1706
1729
|
"data-testid": dataTestId ?? "select",
|
|
1707
|
-
prefix:
|
|
1708
|
-
hasError:
|
|
1709
|
-
fieldSize:
|
|
1710
|
-
getOptionLabelDescription:
|
|
1711
|
-
getOptionPrefix:
|
|
1712
|
-
isMulti:
|
|
1713
|
-
className:
|
|
1714
|
-
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,
|
|
1715
1738
|
});
|
|
1716
|
-
const interactable = react.useMemo(() => !Boolean(disabled) && !Boolean(
|
|
1739
|
+
const interactable = react.useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
|
|
1717
1740
|
// Determine the portal target for the menu
|
|
1718
|
-
const portalTarget = react.useMemo(() => (
|
|
1741
|
+
const portalTarget = react.useMemo(() => (stableRestProps.menuPortalTarget !== undefined ? stableRestProps.menuPortalTarget : document.body), [stableRestProps.menuPortalTarget]);
|
|
1719
1742
|
// Use custom scroll blocking hook to prevent layout shifts
|
|
1720
1743
|
// Pass the portal target so we only block scroll when menu is portaled to document.body
|
|
1721
1744
|
const { blockScroll, restoreScroll } = reactComponents.useScrollBlock(portalTarget);
|
|
1722
|
-
// Store callbacks in refs to keep
|
|
1723
|
-
//
|
|
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
|
|
1724
1748
|
const onMenuOpenRef = react.useRef(onMenuOpen);
|
|
1725
1749
|
const onMenuCloseRef = react.useRef(onMenuClose);
|
|
1726
1750
|
react.useEffect(() => {
|
|
@@ -1732,7 +1756,7 @@ onMenuOpen, onMenuClose, options, value, ...restProps }) => {
|
|
|
1732
1756
|
// Wrap user's onMenuOpen callback to block scrolling
|
|
1733
1757
|
// We apply scroll blocking directly instead of using react-select's menuShouldBlockScroll
|
|
1734
1758
|
// because it doesn't properly account for existing body padding, causing layout shifts
|
|
1735
|
-
// See
|
|
1759
|
+
// See comment next to menuShouldBlockScroll below for more
|
|
1736
1760
|
const handleMenuOpen = react.useCallback(() => {
|
|
1737
1761
|
blockScroll();
|
|
1738
1762
|
onMenuOpenRef.current?.();
|
|
@@ -1742,14 +1766,36 @@ onMenuOpen, onMenuClose, options, value, ...restProps }) => {
|
|
|
1742
1766
|
restoreScroll();
|
|
1743
1767
|
onMenuCloseRef.current?.();
|
|
1744
1768
|
}, [restoreScroll]);
|
|
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
|
|
1745
1775
|
return react.useMemo(() => {
|
|
1746
1776
|
return {
|
|
1747
|
-
...
|
|
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
|
|
1748
1793
|
options,
|
|
1749
1794
|
value,
|
|
1795
|
+
defaultValue,
|
|
1750
1796
|
components: customComponents,
|
|
1751
1797
|
unstyled: true,
|
|
1752
|
-
"aria-label":
|
|
1798
|
+
"aria-label": label,
|
|
1753
1799
|
"data-testid": dataTestId ?? "select",
|
|
1754
1800
|
tabSelectsValue: false,
|
|
1755
1801
|
blurInputOnSelect: false,
|
|
@@ -1757,22 +1803,38 @@ onMenuOpen, onMenuClose, options, value, ...restProps }) => {
|
|
|
1757
1803
|
// Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
|
|
1758
1804
|
// the parent element instead of 'document.body'.
|
|
1759
1805
|
menuPortalTarget: portalTarget,
|
|
1760
|
-
isSearchable: interactable ? (
|
|
1806
|
+
isSearchable: interactable ? (isSearchable ?? true) : false,
|
|
1761
1807
|
// Disable react-select's built-in scroll blocking as we handle it ourselves in onMenuOpen/onMenuClose
|
|
1762
1808
|
// to prevent layout shifts caused by not accounting for existing body padding in the react-select implementation.
|
|
1763
1809
|
// See: https://github.com/JedWatson/react-select/issues/5342 AND https://github.com/JedWatson/react-select/issues/5020
|
|
1764
1810
|
menuShouldBlockScroll: false,
|
|
1765
1811
|
menuShouldScrollIntoView: true,
|
|
1766
|
-
openMenuOnClick: interactable ? (
|
|
1767
|
-
openMenuOnFocus: Boolean(
|
|
1768
|
-
|
|
1812
|
+
openMenuOnClick: interactable ? (openMenuOnClick ?? true) : false,
|
|
1813
|
+
openMenuOnFocus: Boolean(openMenuOnFocus),
|
|
1814
|
+
isMulti,
|
|
1815
|
+
closeMenuOnSelect: !Boolean(isMulti),
|
|
1769
1816
|
isDisabled: Boolean(disabled),
|
|
1770
|
-
isClearable: Boolean(
|
|
1771
|
-
menuPlacement:
|
|
1772
|
-
placeholder: interactable ?
|
|
1773
|
-
hideSelectedOptions: Boolean(
|
|
1817
|
+
isClearable: Boolean(isClearable),
|
|
1818
|
+
menuPlacement: menuPlacement ?? "auto",
|
|
1819
|
+
placeholder: interactable ? placeholder : undefined,
|
|
1820
|
+
hideSelectedOptions: Boolean(hideSelectedOptions),
|
|
1774
1821
|
menuIsOpen: interactable ? undefined : false, // close it if not interactable, otherwise leave state to react-select
|
|
1775
|
-
//
|
|
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)
|
|
1776
1838
|
onMenuOpen: handleMenuOpen,
|
|
1777
1839
|
onMenuClose: handleMenuClose,
|
|
1778
1840
|
// ๐ putting these here to avoid them _accidentally_ being overwritten in the future๐
|
|
@@ -1783,16 +1845,55 @@ onMenuOpen, onMenuClose, options, value, ...restProps }) => {
|
|
|
1783
1845
|
styles: undefined,
|
|
1784
1846
|
};
|
|
1785
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,
|
|
1786
1867
|
customComponents,
|
|
1787
|
-
|
|
1868
|
+
label,
|
|
1869
|
+
dataTestId,
|
|
1870
|
+
portalTarget,
|
|
1788
1871
|
interactable,
|
|
1872
|
+
isSearchable,
|
|
1873
|
+
openMenuOnClick,
|
|
1874
|
+
openMenuOnFocus,
|
|
1875
|
+
isMulti,
|
|
1876
|
+
disabled,
|
|
1877
|
+
isClearable,
|
|
1878
|
+
menuPlacement,
|
|
1879
|
+
placeholder,
|
|
1880
|
+
hideSelectedOptions,
|
|
1881
|
+
onChange,
|
|
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,
|
|
1789
1895
|
handleMenuOpen,
|
|
1790
1896
|
handleMenuClose,
|
|
1791
|
-
dataTestId,
|
|
1792
|
-
portalTarget,
|
|
1793
|
-
options,
|
|
1794
|
-
value,
|
|
1795
|
-
restProps,
|
|
1796
1897
|
]);
|
|
1797
1898
|
};
|
|
1798
1899
|
|
|
@@ -1812,26 +1913,41 @@ const BaseSelect = (props) => {
|
|
|
1812
1913
|
/**
|
|
1813
1914
|
* A hook used by creatable selects that extends useSelect with creatable-specific functionality
|
|
1814
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
|
|
1815
1920
|
* @param props - The props for the CreatableSelect component
|
|
1816
|
-
* @returns {ReactCreatableProps} Props for react-select creatable component
|
|
1921
|
+
* @returns {ReactCreatableProps<TOption, TIsMulti, TGroup>} Props for react-select creatable component
|
|
1817
1922
|
*/
|
|
1818
1923
|
const useCreatableSelect = (props) => {
|
|
1819
|
-
|
|
1820
|
-
|
|
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;
|
|
1821
1931
|
// Store onCreateOption in a ref to keep wrapper stable
|
|
1822
1932
|
// This prevents unnecessary re-renders when parent components don't memoize this callback
|
|
1823
1933
|
const onCreateOptionRef = react.useRef(onCreateOption);
|
|
1824
1934
|
react.useEffect(() => {
|
|
1825
1935
|
onCreateOptionRef.current = onCreateOption;
|
|
1826
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);
|
|
1827
1943
|
return react.useMemo(() => ({
|
|
1828
1944
|
...baseSelectProps,
|
|
1829
1945
|
allowCreateWhileLoading,
|
|
1830
|
-
onCreateOption:
|
|
1946
|
+
onCreateOption: hasOnCreateOption ? handleCreateOption : undefined,
|
|
1831
1947
|
// Override some defaults specific to creatable selects
|
|
1832
1948
|
closeMenuOnSelect: false, // Keep menu open for multi-creation
|
|
1833
1949
|
blurInputOnSelect: !Boolean(isMulti), // Only blur if not multi
|
|
1834
|
-
}), [baseSelectProps, allowCreateWhileLoading, isMulti]);
|
|
1950
|
+
}), [baseSelectProps, allowCreateWhileLoading, hasOnCreateOption, handleCreateOption, isMulti]);
|
|
1835
1951
|
};
|
|
1836
1952
|
|
|
1837
1953
|
/**
|
package/index.esm.js
CHANGED
|
@@ -1695,31 +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
|
-
|
|
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]);
|
|
1702
1725
|
const customComponents = useCustomComponents({
|
|
1703
1726
|
disabled, // intentionally not evaluated as boolean, since it can be object too!
|
|
1704
|
-
readOnly:
|
|
1727
|
+
readOnly: readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
|
|
1705
1728
|
"data-testid": dataTestId ?? "select",
|
|
1706
|
-
prefix:
|
|
1707
|
-
hasError:
|
|
1708
|
-
fieldSize:
|
|
1709
|
-
getOptionLabelDescription:
|
|
1710
|
-
getOptionPrefix:
|
|
1711
|
-
isMulti:
|
|
1712
|
-
className:
|
|
1713
|
-
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,
|
|
1714
1737
|
});
|
|
1715
|
-
const interactable = useMemo(() => !Boolean(disabled) && !Boolean(
|
|
1738
|
+
const interactable = useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
|
|
1716
1739
|
// Determine the portal target for the menu
|
|
1717
|
-
const portalTarget = useMemo(() => (
|
|
1740
|
+
const portalTarget = useMemo(() => (stableRestProps.menuPortalTarget !== undefined ? stableRestProps.menuPortalTarget : document.body), [stableRestProps.menuPortalTarget]);
|
|
1718
1741
|
// Use custom scroll blocking hook to prevent layout shifts
|
|
1719
1742
|
// Pass the portal target so we only block scroll when menu is portaled to document.body
|
|
1720
1743
|
const { blockScroll, restoreScroll } = useScrollBlock(portalTarget);
|
|
1721
|
-
// Store callbacks in refs to keep
|
|
1722
|
-
//
|
|
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
|
|
1723
1747
|
const onMenuOpenRef = useRef(onMenuOpen);
|
|
1724
1748
|
const onMenuCloseRef = useRef(onMenuClose);
|
|
1725
1749
|
useEffect(() => {
|
|
@@ -1731,7 +1755,7 @@ onMenuOpen, onMenuClose, options, value, ...restProps }) => {
|
|
|
1731
1755
|
// Wrap user's onMenuOpen callback to block scrolling
|
|
1732
1756
|
// We apply scroll blocking directly instead of using react-select's menuShouldBlockScroll
|
|
1733
1757
|
// because it doesn't properly account for existing body padding, causing layout shifts
|
|
1734
|
-
// See
|
|
1758
|
+
// See comment next to menuShouldBlockScroll below for more
|
|
1735
1759
|
const handleMenuOpen = useCallback(() => {
|
|
1736
1760
|
blockScroll();
|
|
1737
1761
|
onMenuOpenRef.current?.();
|
|
@@ -1741,14 +1765,36 @@ onMenuOpen, onMenuClose, options, value, ...restProps }) => {
|
|
|
1741
1765
|
restoreScroll();
|
|
1742
1766
|
onMenuCloseRef.current?.();
|
|
1743
1767
|
}, [restoreScroll]);
|
|
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
|
|
1744
1774
|
return useMemo(() => {
|
|
1745
1775
|
return {
|
|
1746
|
-
...
|
|
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
|
|
1747
1792
|
options,
|
|
1748
1793
|
value,
|
|
1794
|
+
defaultValue,
|
|
1749
1795
|
components: customComponents,
|
|
1750
1796
|
unstyled: true,
|
|
1751
|
-
"aria-label":
|
|
1797
|
+
"aria-label": label,
|
|
1752
1798
|
"data-testid": dataTestId ?? "select",
|
|
1753
1799
|
tabSelectsValue: false,
|
|
1754
1800
|
blurInputOnSelect: false,
|
|
@@ -1756,22 +1802,38 @@ onMenuOpen, onMenuClose, options, value, ...restProps }) => {
|
|
|
1756
1802
|
// Setting menuPortalTarget to 'null' specifies that the dropdown should be rendered within
|
|
1757
1803
|
// the parent element instead of 'document.body'.
|
|
1758
1804
|
menuPortalTarget: portalTarget,
|
|
1759
|
-
isSearchable: interactable ? (
|
|
1805
|
+
isSearchable: interactable ? (isSearchable ?? true) : false,
|
|
1760
1806
|
// Disable react-select's built-in scroll blocking as we handle it ourselves in onMenuOpen/onMenuClose
|
|
1761
1807
|
// to prevent layout shifts caused by not accounting for existing body padding in the react-select implementation.
|
|
1762
1808
|
// See: https://github.com/JedWatson/react-select/issues/5342 AND https://github.com/JedWatson/react-select/issues/5020
|
|
1763
1809
|
menuShouldBlockScroll: false,
|
|
1764
1810
|
menuShouldScrollIntoView: true,
|
|
1765
|
-
openMenuOnClick: interactable ? (
|
|
1766
|
-
openMenuOnFocus: Boolean(
|
|
1767
|
-
|
|
1811
|
+
openMenuOnClick: interactable ? (openMenuOnClick ?? true) : false,
|
|
1812
|
+
openMenuOnFocus: Boolean(openMenuOnFocus),
|
|
1813
|
+
isMulti,
|
|
1814
|
+
closeMenuOnSelect: !Boolean(isMulti),
|
|
1768
1815
|
isDisabled: Boolean(disabled),
|
|
1769
|
-
isClearable: Boolean(
|
|
1770
|
-
menuPlacement:
|
|
1771
|
-
placeholder: interactable ?
|
|
1772
|
-
hideSelectedOptions: Boolean(
|
|
1816
|
+
isClearable: Boolean(isClearable),
|
|
1817
|
+
menuPlacement: menuPlacement ?? "auto",
|
|
1818
|
+
placeholder: interactable ? placeholder : undefined,
|
|
1819
|
+
hideSelectedOptions: Boolean(hideSelectedOptions),
|
|
1773
1820
|
menuIsOpen: interactable ? undefined : false, // close it if not interactable, otherwise leave state to react-select
|
|
1774
|
-
//
|
|
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)
|
|
1775
1837
|
onMenuOpen: handleMenuOpen,
|
|
1776
1838
|
onMenuClose: handleMenuClose,
|
|
1777
1839
|
// ๐ putting these here to avoid them _accidentally_ being overwritten in the future๐
|
|
@@ -1782,16 +1844,55 @@ onMenuOpen, onMenuClose, options, value, ...restProps }) => {
|
|
|
1782
1844
|
styles: undefined,
|
|
1783
1845
|
};
|
|
1784
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,
|
|
1785
1866
|
customComponents,
|
|
1786
|
-
|
|
1867
|
+
label,
|
|
1868
|
+
dataTestId,
|
|
1869
|
+
portalTarget,
|
|
1787
1870
|
interactable,
|
|
1871
|
+
isSearchable,
|
|
1872
|
+
openMenuOnClick,
|
|
1873
|
+
openMenuOnFocus,
|
|
1874
|
+
isMulti,
|
|
1875
|
+
disabled,
|
|
1876
|
+
isClearable,
|
|
1877
|
+
menuPlacement,
|
|
1878
|
+
placeholder,
|
|
1879
|
+
hideSelectedOptions,
|
|
1880
|
+
onChange,
|
|
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,
|
|
1788
1894
|
handleMenuOpen,
|
|
1789
1895
|
handleMenuClose,
|
|
1790
|
-
dataTestId,
|
|
1791
|
-
portalTarget,
|
|
1792
|
-
options,
|
|
1793
|
-
value,
|
|
1794
|
-
restProps,
|
|
1795
1896
|
]);
|
|
1796
1897
|
};
|
|
1797
1898
|
|
|
@@ -1811,26 +1912,41 @@ const BaseSelect = (props) => {
|
|
|
1811
1912
|
/**
|
|
1812
1913
|
* A hook used by creatable selects that extends useSelect with creatable-specific functionality
|
|
1813
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
|
|
1814
1919
|
* @param props - The props for the CreatableSelect component
|
|
1815
|
-
* @returns {ReactCreatableProps} Props for react-select creatable component
|
|
1920
|
+
* @returns {ReactCreatableProps<TOption, TIsMulti, TGroup>} Props for react-select creatable component
|
|
1816
1921
|
*/
|
|
1817
1922
|
const useCreatableSelect = (props) => {
|
|
1818
|
-
|
|
1819
|
-
|
|
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;
|
|
1820
1930
|
// Store onCreateOption in a ref to keep wrapper stable
|
|
1821
1931
|
// This prevents unnecessary re-renders when parent components don't memoize this callback
|
|
1822
1932
|
const onCreateOptionRef = useRef(onCreateOption);
|
|
1823
1933
|
useEffect(() => {
|
|
1824
1934
|
onCreateOptionRef.current = onCreateOption;
|
|
1825
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);
|
|
1826
1942
|
return useMemo(() => ({
|
|
1827
1943
|
...baseSelectProps,
|
|
1828
1944
|
allowCreateWhileLoading,
|
|
1829
|
-
onCreateOption:
|
|
1945
|
+
onCreateOption: hasOnCreateOption ? handleCreateOption : undefined,
|
|
1830
1946
|
// Override some defaults specific to creatable selects
|
|
1831
1947
|
closeMenuOnSelect: false, // Keep menu open for multi-creation
|
|
1832
1948
|
blurInputOnSelect: !Boolean(isMulti), // Only blur if not multi
|
|
1833
|
-
}), [baseSelectProps, allowCreateWhileLoading, isMulti]);
|
|
1949
|
+
}), [baseSelectProps, allowCreateWhileLoading, hasOnCreateOption, handleCreateOption, isMulti]);
|
|
1834
1950
|
};
|
|
1835
1951
|
|
|
1836
1952
|
/**
|
package/package.json
CHANGED
|
@@ -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>;
|