@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 = ({ 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
- onMenuOpen, onMenuClose, options, value, ...restProps }) => {
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: restProps.readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
1728
+ readOnly: readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
1706
1729
  "data-testid": dataTestId ?? "select",
1707
- prefix: restProps.prefix,
1708
- hasError: restProps.hasError,
1709
- fieldSize: restProps.fieldSize,
1710
- getOptionLabelDescription: restProps.getOptionLabelDescription,
1711
- getOptionPrefix: restProps.getOptionPrefix,
1712
- isMulti: restProps.isMulti ?? false,
1713
- className: restProps.className,
1714
- autoComplete: restProps.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(restProps.readOnly), [disabled, restProps.readOnly]);
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(() => (restProps.menuPortalTarget !== undefined ? restProps.menuPortalTarget : document.body), [restProps.menuPortalTarget]);
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 wrapper callbacks stable
1723
- // This prevents unnecessary re-renders when parent components don't memoize these callbacks
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 commeont next to menuShouldBlockScroll below for more
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
- ...restProps,
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": restProps.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 ? (restProps.isSearchable ?? true) : false,
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 ? (restProps.openMenuOnClick ?? true) : false,
1767
- openMenuOnFocus: Boolean(restProps.openMenuOnFocus),
1768
- closeMenuOnSelect: !Boolean(restProps.isMulti),
1812
+ openMenuOnClick: interactable ? (openMenuOnClick ?? true) : false,
1813
+ openMenuOnFocus: Boolean(openMenuOnFocus),
1814
+ isMulti,
1815
+ closeMenuOnSelect: !Boolean(isMulti),
1769
1816
  isDisabled: Boolean(disabled),
1770
- isClearable: Boolean(restProps.isClearable),
1771
- menuPlacement: restProps.menuPlacement ?? "auto",
1772
- placeholder: interactable ? restProps.placeholder : undefined,
1773
- hideSelectedOptions: Boolean(restProps.hideSelectedOptions),
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
- // Wire up our custom menu open/close handlers
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
- disabled,
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
- const { onCreateOption, allowCreateWhileLoading, isMulti } = props;
1820
- const baseSelectProps = useSelect(props);
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: onCreateOptionRef.current,
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 = ({ disabled = false, //renaming to isDisabled, so not directly passed to react-select
1699
- "data-testid": dataTestId,
1700
- // Extract critical props that must trigger recalculation when they change
1701
- onMenuOpen, onMenuClose, options, value, ...restProps }) => {
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: restProps.readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
1727
+ readOnly: readOnly ?? false, // intentionally not evaluated as boolean, since it can be object too!
1705
1728
  "data-testid": dataTestId ?? "select",
1706
- prefix: restProps.prefix,
1707
- hasError: restProps.hasError,
1708
- fieldSize: restProps.fieldSize,
1709
- getOptionLabelDescription: restProps.getOptionLabelDescription,
1710
- getOptionPrefix: restProps.getOptionPrefix,
1711
- isMulti: restProps.isMulti ?? false,
1712
- className: restProps.className,
1713
- autoComplete: restProps.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(restProps.readOnly), [disabled, restProps.readOnly]);
1738
+ const interactable = useMemo(() => !Boolean(disabled) && !Boolean(readOnly), [disabled, readOnly]);
1716
1739
  // Determine the portal target for the menu
1717
- const portalTarget = useMemo(() => (restProps.menuPortalTarget !== undefined ? restProps.menuPortalTarget : document.body), [restProps.menuPortalTarget]);
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 wrapper callbacks stable
1722
- // This prevents unnecessary re-renders when parent components don't memoize these callbacks
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 commeont next to menuShouldBlockScroll below for more
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
- ...restProps,
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": restProps.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 ? (restProps.isSearchable ?? true) : false,
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 ? (restProps.openMenuOnClick ?? true) : false,
1766
- openMenuOnFocus: Boolean(restProps.openMenuOnFocus),
1767
- closeMenuOnSelect: !Boolean(restProps.isMulti),
1811
+ openMenuOnClick: interactable ? (openMenuOnClick ?? true) : false,
1812
+ openMenuOnFocus: Boolean(openMenuOnFocus),
1813
+ isMulti,
1814
+ closeMenuOnSelect: !Boolean(isMulti),
1768
1815
  isDisabled: Boolean(disabled),
1769
- isClearable: Boolean(restProps.isClearable),
1770
- menuPlacement: restProps.menuPlacement ?? "auto",
1771
- placeholder: interactable ? restProps.placeholder : undefined,
1772
- hideSelectedOptions: Boolean(restProps.hideSelectedOptions),
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
- // Wire up our custom menu open/close handlers
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
- disabled,
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
- const { onCreateOption, allowCreateWhileLoading, isMulti } = props;
1819
- const baseSelectProps = useSelect(props);
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: onCreateOptionRef.current,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-form-components",
3
- "version": "1.8.131",
3
+ "version": "1.8.132",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -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>>({ disabled, "data-testid": dataTestId, onMenuOpen, onMenuClose, options, value, ...restProps }: SelectProps<TOption, TIsAsync, TIsMulti, TGroup>) => ReactSelectProps<TOption, TIsMulti, TGroup>;
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>;