@codecademy/gamut

72.2.272.2.3-alpha.f0a032.0
~

Modified (18 files)

Index: package/dist/Form/SelectDropdown/elements/constants.js
===================================================================
--- package/dist/Form/SelectDropdown/elements/constants.js
+++ package/dist/Form/SelectDropdown/elements/constants.js
@@ -1,5 +1,5 @@
-import { ArrowChevronDownIcon, CloseIcon, MiniChevronDownIcon, MiniDeleteIcon, SearchIcon } from '@codecademy/gamut-icons';
+import { ArrowChevronDownIcon, CloseIcon, MiniChevronDownIcon, MiniDeleteIcon } from '@codecademy/gamut-icons';
 export const iconSize = {
   small: 12,
   medium: 16
 };
@@ -15,16 +15,8 @@
   mediumChevron: {
     size: iconSize.medium,
     icon: ArrowChevronDownIcon
   },
-  smallSearchable: {
-    size: iconSize.small,
-    icon: SearchIcon
-  },
-  mediumSearchable: {
-    size: iconSize.medium,
-    icon: SearchIcon
-  },
   smallRemove: {
     size: iconSize.small,
     icon: MiniDeleteIcon
   },
Index: package/dist/Form/SelectDropdown/elements/containers.js
===================================================================
--- package/dist/Form/SelectDropdown/elements/containers.js
+++ package/dist/Form/SelectDropdown/elements/containers.js
@@ -1,6 +1,7 @@
 import { createContext, useLayoutEffect } from 'react';
 import ReactSelect, { components as SelectDropdownElements } from 'react-select';
+import CreatableSelect from 'react-select/creatable';
 import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
 /**
  * React context for sharing state between SelectDropdown components.
  * Provides access to focus state and refs for keyboard navigation.
@@ -115,14 +116,29 @@
 };
 
 /**
  * Typed wrapper around react-select component.
- * Provides type safety for the underlying react-select implementation.
+ * Renders CreatableSelect when isCreatable is true, ReactSelect otherwise.
+ * Creatable-only props (formatCreateLabel, isValidNewOption) are stripped from
+ * the non-creatable path so they don't reach ReactSelect. `onCreateOption` is
+ * handled in SelectDropdown's changeHandler — do not pass it to CreatableSelect
+ * or react-select will skip onChange on create.
  */
 export function TypedReactSelect({
   selectRef,
+  isCreatable,
+  formatCreateLabel,
+  isValidNewOption,
   ...props
 }) {
+  if (isCreatable) {
+    return /*#__PURE__*/_jsx(CreatableSelect, {
+      ...props,
+      formatCreateLabel: formatCreateLabel,
+      isValidNewOption: isValidNewOption,
+      ref: selectRef
+    });
+  }
   return /*#__PURE__*/_jsx(ReactSelect, {
     ...props,
     ref: selectRef
   });
Index: package/dist/Form/SelectDropdown/elements/controls.js
===================================================================
--- package/dist/Form/SelectDropdown/elements/controls.js
+++ package/dist/Form/SelectDropdown/elements/controls.js
@@ -1,51 +1,24 @@
-import _styled from "@emotion/styled/base";
-import { css, theme } from '@codecademy/gamut-styles';
-import { useContext } from 'react';
 import { components as SelectDropdownElements } from 'react-select';
 import { indicatorIcons } from './constants';
-import { SelectDropdownContext } from './containers';
 import { jsx as _jsx } from "react/jsx-runtime";
 const {
   DropdownIndicator
 } = SelectDropdownElements;
 
 /**
- * Generates accessible focus messages for screen readers.
- * Provides detailed information about the currently focused option.
- *
- * @param params - Object containing the focused option details
- * @returns Formatted accessibility message
- */
-export const onFocus = ({
-  focused: {
-    label,
-    subtitle,
-    rightLabel,
-    disabled
-  }
-}) => {
-  const formattedSubtitle = `, ${subtitle}`;
-  const formattedRightLabel = `, ${rightLabel}`;
-  const msg = `You are currently focused on option ${label}${subtitle ? formattedSubtitle : ''} ${rightLabel ? formattedRightLabel : ''}${disabled ? ', disabled' : ''}`;
-  return msg;
-};
-
-/**
  * Custom dropdown indicator that shows either a chevron or search icon.
  * The icon type depends on whether the select is searchable or not.
  */
 export const DropdownButton = props => {
   const {
-    size,
-    isSearchable
+    size
   } = props.selectProps;
   const color = props.isDisabled ? 'text-disabled' : 'text';
   const iconSize = size ?? 'medium';
-  const iconType = isSearchable ? 'Searchable' : 'Chevron';
   const {
     ...iconProps
-  } = indicatorIcons[`${iconSize}${iconType}`];
+  } = indicatorIcons[`${iconSize}Chevron`];
   const {
     icon: IndicatorIcon
   } = iconProps;
   return /*#__PURE__*/_jsx(DropdownIndicator, {
@@ -54,67 +27,5 @@
       ...iconProps,
       color: color
     })
   });
-};
-const CustomStyledRemoveAllDiv = /*#__PURE__*/_styled('div', {
-  target: "e1xkmr70",
-  label: "CustomStyledRemoveAllDiv"
-})(css({
-  '&:focus': {
-    outline: `2px solid ${theme.colors.primary}`
-  },
-  '&:focus-visible': {
-    outline: `2px solid ${theme.colors.primary}`
-  }
-}), process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9Gb3JtL1NlbGVjdERyb3Bkb3duL2VsZW1lbnRzL2NvbnRyb2xzLnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFxRGlDIiwiZmlsZSI6Ii4uLy4uLy4uLy4uL3NyYy9Gb3JtL1NlbGVjdERyb3Bkb3duL2VsZW1lbnRzL2NvbnRyb2xzLnRzeCIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNzcywgdGhlbWUgfSBmcm9tICdAY29kZWNhZGVteS9nYW11dC1zdHlsZXMnO1xuaW1wb3J0IHN0eWxlZCBmcm9tICdAZW1vdGlvbi9zdHlsZWQnO1xuaW1wb3J0IHsgS2V5Ym9hcmRFdmVudCwgdXNlQ29udGV4dCB9IGZyb20gJ3JlYWN0JztcbmltcG9ydCB7XG4gIEFyaWFPbkZvY3VzLFxuICBjb21wb25lbnRzIGFzIFNlbGVjdERyb3Bkb3duRWxlbWVudHMsXG59IGZyb20gJ3JlYWN0LXNlbGVjdCc7XG5cbmltcG9ydCB7IEV4dGVuZGVkT3B0aW9uLCBTaXplZEluZGljYXRvclByb3BzIH0gZnJvbSAnLi4vdHlwZXMnO1xuaW1wb3J0IHsgaW5kaWNhdG9ySWNvbnMgfSBmcm9tICcuL2NvbnN0YW50cyc7XG5pbXBvcnQgeyBTZWxlY3REcm9wZG93bkNvbnRleHQgfSBmcm9tICcuL2NvbnRhaW5lcnMnO1xuXG5jb25zdCB7IERyb3Bkb3duSW5kaWNhdG9yIH0gPSBTZWxlY3REcm9wZG93bkVsZW1lbnRzO1xuXG4vKipcbiAqIEdlbmVyYXRlcyBhY2Nlc3NpYmxlIGZvY3VzIG1lc3NhZ2VzIGZvciBzY3JlZW4gcmVhZGVycy5cbiAqIFByb3ZpZGVzIGRldGFpbGVkIGluZm9ybWF0aW9uIGFib3V0IHRoZSBjdXJyZW50bHkgZm9jdXNlZCBvcHRpb24uXG4gKlxuICogQHBhcmFtIHBhcmFtcyAtIE9iamVjdCBjb250YWluaW5nIHRoZSBmb2N1c2VkIG9wdGlvbiBkZXRhaWxzXG4gKiBAcmV0dXJucyBGb3JtYXR0ZWQgYWNjZXNzaWJpbGl0eSBtZXNzYWdlXG4gKi9cbmV4cG9ydCBjb25zdCBvbkZvY3VzOiBBcmlhT25Gb2N1czxFeHRlbmRlZE9wdGlvbj4gPSAoe1xuICBmb2N1c2VkOiB7IGxhYmVsLCBzdWJ0aXRsZSwgcmlnaHRMYWJlbCwgZGlzYWJsZWQgfSxcbn0pID0+IHtcbiAgY29uc3QgZm9ybWF0dGVkU3VidGl0bGUgPSBgLCAke3N1YnRpdGxlfWA7XG4gIGNvbnN0IGZvcm1hdHRlZFJpZ2h0TGFiZWwgPSBgLCAke3JpZ2h0TGFiZWx9YDtcblxuICBjb25zdCBtc2cgPSBgWW91IGFyZSBjdXJyZW50bHkgZm9jdXNlZCBvbiBvcHRpb24gJHtsYWJlbH0ke1xuICAgIHN1YnRpdGxlID8gZm9ybWF0dGVkU3VidGl0bGUgOiAnJ1xuICB9ICR7cmlnaHRMYWJlbCA/IGZvcm1hdHRlZFJpZ2h0TGFiZWwgOiAnJ30ke2Rpc2FibGVkID8gJywgZGlzYWJsZWQnIDogJyd9YDtcblxuICByZXR1cm4gbXNnO1xufTtcblxuLyoqXG4gKiBDdXN0b20gZHJvcGRvd24gaW5kaWNhdG9yIHRoYXQgc2hvd3MgZWl0aGVyIGEgY2hldnJvbiBvciBzZWFyY2ggaWNvbi5cbiAqIFRoZSBpY29uIHR5cGUgZGVwZW5kcyBvbiB3aGV0aGVyIHRoZSBzZWxlY3QgaXMgc2VhcmNoYWJsZSBvciBub3QuXG4gKi9cbmV4cG9ydCBjb25zdCBEcm9wZG93bkJ1dHRvbiA9IChwcm9wczogU2l6ZWRJbmRpY2F0b3JQcm9wcykgPT4ge1xuICBjb25zdCB7IHNpemUsIGlzU2VhcmNoYWJsZSB9ID0gcHJvcHMuc2VsZWN0UHJvcHM7XG4gIGNvbnN0IGNvbG9yID0gcHJvcHMuaXNEaXNhYmxlZCA/ICd0ZXh0LWRpc2FibGVkJyA6ICd0ZXh0JztcbiAgY29uc3QgaWNvblNpemUgPSBzaXplID8/ICdtZWRpdW0nO1xuICBjb25zdCBpY29uVHlwZSA9IGlzU2VhcmNoYWJsZSA/ICdTZWFyY2hhYmxlJyA6ICdDaGV2cm9uJztcbiAgY29uc3QgeyAuLi5pY29uUHJvcHMgfSA9IGluZGljYXRvckljb25zW2Ake2ljb25TaXplfSR7aWNvblR5cGV9YF07XG4gIGNvbnN0IHsgaWNvbjogSW5kaWNhdG9ySWNvbiB9ID0gaWNvblByb3BzO1xuXG4gIHJldHVybiAoXG4gICAgPERyb3Bkb3duSW5kaWNhdG9yIHsuLi5wcm9wc30+XG4gICAgICA8SW5kaWNhdG9ySWNvbiB7Li4uaWNvblByb3BzfSBjb2xvcj17Y29sb3J9IC8+XG4gICAgPC9Ecm9wZG93bkluZGljYXRvcj5cbiAgKTtcbn07XG5cbmNvbnN0IEN1c3RvbVN0eWxlZFJlbW92ZUFsbERpdiA9IHN0eWxlZCgnZGl2JykoXG4gIGNzcyh7XG4gICAgJyY6Zm9jdXMnOiB7XG4gICAgICBvdXRsaW5lOiBgMnB4IHNvbGlkICR7dGhlbWUuY29sb3JzLnByaW1hcnl9YCxcbiAgICB9LFxuICAgICcmOmZvY3VzLXZpc2libGUnOiB7XG4gICAgICBvdXRsaW5lOiBgMnB4IHNvbGlkICR7dGhlbWUuY29sb3JzLnByaW1hcnl9YCxcbiAgICB9LFxuICB9KVxuKTtcblxuLyoqXG4gKiBDdXN0b20gcmVtb3ZlIGFsbCBidXR0b24gZm9yIG11bHRpLXNlbGVjdCBtb2RlLlxuICogUHJvdmlkZXMga2V5Ym9hcmQgbmF2aWdhdGlvbiBhbmQgYWNjZXNzaWJsZSByZW1vdmFsIG9mIGFsbCBzZWxlY3RlZCB2YWx1ZXMuXG4gKi9cbmV4cG9ydCBjb25zdCBSZW1vdmVBbGxCdXR0b24gPSAocHJvcHM6IFNpemVkSW5kaWNhdG9yUHJvcHMpID0+IHtcbiAgY29uc3Qge1xuICAgIGdldFN0eWxlcyxcbiAgICBpbm5lclByb3BzOiB7IC4uLnJlc3RJbm5lclByb3BzIH0sXG4gICAgc2VsZWN0UHJvcHM6IHsgc2l6ZSB9LFxuICB9ID0gcHJvcHM7XG5cbiAgY29uc3QgeyByZW1vdmVBbGxCdXR0b25SZWYsIHNlbGVjdElucHV0UmVmIH0gPSB1c2VDb250ZXh0KFxuICAgIFNlbGVjdERyb3Bkb3duQ29udGV4dFxuICApO1xuXG4gIGNvbnN0IGljb25TaXplID0gc2l6ZSA/PyAnbWVkaXVtJztcbiAgY29uc3QgeyAuLi5pY29uUHJvcHMgfSA9IGluZGljYXRvckljb25zW2Ake2ljb25TaXplfVJlbW92ZWBdO1xuICBjb25zdCB7IGljb246IEluZGljYXRvckljb24gfSA9IGljb25Qcm9wcztcblxuICBjb25zdCBvbktleVByZXNzID0gKGU6IEtleWJvYXJkRXZlbnQ8SFRNTERpdkVsZW1lbnQ+KSA9PiB7XG4gICAgaWYgKGUua2V5ID09PSAnRW50ZXInICYmIHJlc3RJbm5lclByb3BzLm9uTW91c2VEb3duKSB7XG4gICAgICByZXN0SW5uZXJQcm9wcy5vbk1vdXNlRG93bihlIGFzIGFueSk7XG4gICAgfVxuXG4gICAgaWYgKFxuICAgICAgc2VsZWN0SW5wdXRSZWY/LmN1cnJlbnQgJiZcbiAgICAgIChlLmtleSA9PT0gJ0Fycm93UmlnaHQnIHx8IGUua2V5ID09PSAnQXJyb3dMZWZ0JyB8fCBlLmtleSA9PT0gJ0Fycm93RG93bicpXG4gICAgKSB7XG4gICAgICBzZWxlY3RJbnB1dFJlZj8uY3VycmVudC5mb2N1cygpO1xuICAgIH1cbiAgfTtcblxuICBjb25zdCBzdHlsZSA9IGdldFN0eWxlcygnY2xlYXJJbmRpY2F0b3InLCBwcm9wcykgYXMgUmVhY3QuQ1NTUHJvcGVydGllcztcblxuICByZXR1cm4gKFxuICAgIDxDdXN0b21TdHlsZWRSZW1vdmVBbGxEaXZcbiAgICAgIGFyaWEtbGFiZWw9XCJSZW1vdmUgYWxsIHNlbGVjdGVkXCJcbiAgICAgIHJvbGU9XCJidXR0b25cIlxuICAgICAgdGFiSW5kZXg9ezB9XG4gICAgICB7Li4ucmVzdElubmVyUHJvcHN9XG4gICAgICByZWY9e3JlbW92ZUFsbEJ1dHRvblJlZn1cbiAgICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBnYW11dC9uby1pbmxpbmUtc3R5bGVcbiAgICAgIHN0eWxlPXtzdHlsZX1cbiAgICAgIG9uS2V5RG93bj17b25LZXlQcmVzc31cbiAgICA+XG4gICAgICA8SW5kaWNhdG9ySWNvbiB7Li4uaWNvblByb3BzfSBjb2xvcj1cInRleHRcIiAvPlxuICAgIDwvQ3VzdG9tU3R5bGVkUmVtb3ZlQWxsRGl2PlxuICApO1xufTtcbiJdfQ== */");
-
-/**
- * Custom remove all button for multi-select mode.
- * Provides keyboard navigation and accessible removal of all selected values.
- */
-export const RemoveAllButton = props => {
-  const {
-    getStyles,
-    innerProps: {
-      ...restInnerProps
-    },
-    selectProps: {
-      size
-    }
-  } = props;
-  const {
-    removeAllButtonRef,
-    selectInputRef
-  } = useContext(SelectDropdownContext);
-  const iconSize = size ?? 'medium';
-  const {
-    ...iconProps
-  } = indicatorIcons[`${iconSize}Remove`];
-  const {
-    icon: IndicatorIcon
-  } = iconProps;
-  const onKeyPress = e => {
-    if (e.key === 'Enter' && restInnerProps.onMouseDown) {
-      restInnerProps.onMouseDown(e);
-    }
-    if (selectInputRef?.current && (e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'ArrowDown')) {
-      selectInputRef?.current.focus();
-    }
-  };
-  const style = getStyles('clearIndicator', props);
-  return /*#__PURE__*/_jsx(CustomStyledRemoveAllDiv, {
-    "aria-label": "Remove all selected",
-    role: "button",
-    tabIndex: 0,
-    ...restInnerProps,
-    ref: removeAllButtonRef
-    // eslint-disable-next-line gamut/no-inline-style
-    ,
-    style: style,
-    onKeyDown: onKeyPress,
-    children: /*#__PURE__*/_jsx(IndicatorIcon, {
-      ...iconProps,
-      color: "text"
-    })
-  });
 };
\ No newline at end of file
Index: package/dist/Form/SelectDropdown/elements/index.js
===================================================================
--- package/dist/Form/SelectDropdown/elements/index.js
+++ package/dist/Form/SelectDropdown/elements/index.js
@@ -1,5 +1,6 @@
 export { iconSize, selectedIconSize, indicatorIcons } from './constants';
 export { MultiValueWithColorMode, MultiValueRemoveButton, RemoveAllButton } from './multi-value';
-export { DropdownButton, onFocus } from './controls';
+export { DropdownButton } from './controls';
+export { onFocus } from '../core/accessibility';
 export { SelectDropdownContext, CustomContainer, CustomInput, CustomValueContainer, TypedReactSelect } from './containers';
 export { IconOption, AbbreviatedSingleValue, formatOptionLabel, formatGroupLabel } from './options';
\ No newline at end of file
Index: package/dist/Form/styles/index.js
===================================================================
--- package/dist/Form/styles/index.js
+++ package/dist/Form/styles/index.js
@@ -1,4 +1,4 @@
 export * from './shared-system-props';
 export * from './Checkbox-styles';
 export * from './Radio-styles';
-export * from '../SelectDropdown/styles';
\ No newline at end of file
+export * from '../SelectDropdown/core/styles';
\ No newline at end of file
Index: package/dist/Form/SelectDropdown/elements/options.js
===================================================================
--- package/dist/Form/SelectDropdown/elements/options.js
+++ package/dist/Form/SelectDropdown/elements/options.js
@@ -43,8 +43,9 @@
 
 /**
  * Custom option component that displays a check icon for selected items.
  * Also manages ARIA attributes for accessibility.
+ * Skips the check icon for react-select/creatable's "Add" row (__isNew__).
  */
 export const IconOption = ({
   children,
   ...rest
@@ -53,17 +54,19 @@
     size
   } = rest.selectProps;
   const {
     isFocused,
-    innerProps
+    innerProps,
+    data
   } = rest;
+  const isNew = data?.__isNew__;
   return /*#__PURE__*/_jsxs(SelectDropdownElements.Option, {
     ...rest,
     innerProps: {
       ...innerProps,
       'aria-selected': isFocused
     },
-    children: [children, rest?.isSelected && /*#__PURE__*/_jsx(CheckIcon, {
+    children: [children, !isNew && rest?.isSelected && /*#__PURE__*/_jsx(CheckIcon, {
       size: selectedIconSize[size ?? 'medium']
     })]
   });
 };
Index: package/dist/Form/SelectDropdown/SelectDropdown.js
===================================================================
--- package/dist/Form/SelectDropdown/SelectDropdown.js
+++ package/dist/Form/SelectDropdown/SelectDropdown.js
@@ -1,29 +1,15 @@
 import { useTheme } from '@emotion/react';
-import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
+import { useId, useMemo, useRef, useState } from 'react';
 import * as React from 'react';
-import { parseOptions } from '../utils';
-import { AbbreviatedSingleValue, CustomContainer, CustomInput, CustomValueContainer, DropdownButton, formatGroupLabel, formatOptionLabel, IconOption, MultiValueRemoveButton, MultiValueWithColorMode, onFocus, RemoveAllButton, SelectDropdownContext, TypedReactSelect } from './elements';
-import { getMemoizedStyles } from './styles';
-import { filterValueFromOptions, isMultipleSelectProps, isOptionsGrouped, isSingleSelectProps, removeValueFromSelectedOptions } from './utils';
+import { onFocus } from './core/accessibility';
+import { defaultComponents } from './core/constants';
+import { getMemoizedStyles } from './core/styles';
+import { resolveNoOptionsMessage } from './core/utils';
+import { formatGroupLabel, formatOptionLabel, SelectDropdownContext, TypedReactSelect } from './elements';
+import { useSelectHandlers } from './hooks/useSelectHandlers';
+import { useSelectOptions } from './hooks/useSelectOptions';
 import { jsx as _jsx } from "react/jsx-runtime";
-const defaultProps = {
-  name: undefined,
-  components: {
-    DropdownIndicator: DropdownButton,
-    IndicatorSeparator: () => null,
-    ClearIndicator: RemoveAllButton,
-    SelectContainer: CustomContainer,
-    ValueContainer: CustomValueContainer,
-    MultiValue: MultiValueWithColorMode,
-    MultiValueRemove: MultiValueRemoveButton,
-    Option: IconOption,
-    SingleValue: AbbreviatedSingleValue,
-    Input: CustomInput
-  }
-};
-const onChangeAction = 'select-option';
-
 /**
  * A flexible dropdown select component built on top of react-select.
  *
  * Supports both single and multi-select modes with customizable options including
@@ -72,103 +58,60 @@
 export const SelectDropdown = ({
   disabled,
   dropdownWidth,
   error,
+  formatCreateLabel = inputValue => `Add "${inputValue}"`,
   id,
   inputProps,
   inputWidth,
-  isSearchable = false,
+  isCreatable = false,
+  isSearchable: isSearchableProp = false,
+  isValidNewOption,
   menuAlignment = 'left',
   multiple,
   name,
   onChange,
+  onCreateOption,
+  onInputChange,
   options,
   placeholder = 'Select an option',
   shownOptionsLimit = 6,
   size,
+  validationMessage,
   value,
   zIndex,
   ...rest
 }) => {
+  // isSearchable is forced true when isCreatable is true (CreatableSelect requires a text input)
+  const isSearchable = isCreatable || isSearchableProp;
   const rawInputId = useId();
   const inputId = name ?? `${id}-select-dropdown-${rawInputId}`;
-  const [activated, setActivated] = useState(false);
-  const [currentFocusedValue, setCurrentFocusedValue] = useState(undefined);
-
-  // these are used to programatically manage the focus state of our multi-select options + 'Remove all' button
   const removeAllButtonRef = useRef(null);
   const selectInputRef = useRef(null);
-  const selectOptions = useMemo(() => {
-    if (!options || Array.isArray(options) && !options.length || typeof options === 'object' && !Array.isArray(options) && Object.keys(options).length === 0) {
-      return [];
-    }
-    if (isOptionsGrouped(options)) {
-      return options;
-    }
-    return parseOptions({
-      options,
-      id,
-      size
-    });
-  }, [options, id, size]);
-  const parsedValue = useMemo(() => {
-    if (isOptionsGrouped(selectOptions)) {
-      for (const group of selectOptions) {
-        if (group.options) {
-          const foundOption = group.options.find(option => option.value === value);
-          if (foundOption) return foundOption;
-        }
-      }
-      return undefined;
-    }
-    return selectOptions.find(option => option.value === value);
-  }, [selectOptions, value]);
-  const [multiValues, setMultiValues] = useState(multiple &&
-  // To keep this efficient for non-multiSelect
-  filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions)));
-
-  // If the caller changes the initial value, let's update our value to match.
-  useEffect(() => {
-    const newMultiValues = filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions));
-    if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
-
-    //
-    // We only update this when our passed in options or value changes, not multiValues.
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [options, value]);
-  const changeHandler = useCallback(optionEvent => {
-    setActivated(true);
-
-    // We have to do this because the version of typescript we have doesn't have the transitivity of these type guards yet. But, we will soon!
-    // Should probably come with: https://codecademy.atlassian.net/browse/GM-354
-    const onChangeProps = {
-      onChange,
-      multiple
-    };
-    if (isSingleSelectProps(onChangeProps)) {
-      const singleOptionEvent = optionEvent;
-      onChangeProps.onChange?.(singleOptionEvent, {
-        action: onChangeAction,
-        option: singleOptionEvent
-      });
-    }
-    if (isMultipleSelectProps(onChangeProps)) {
-      setMultiValues(optionEvent);
-      onChangeProps.onChange?.(optionEvent, {
-        action: onChangeAction,
-        option: undefined // At the moment this isn't used, but when multi select is built for real, boom (https://codecademy.atlassian.net/browse/GM-354)
-      });
-    }
-  }, [onChange, multiple]);
-  const keyPressHandler = e => {
-    if (multiple && e.key === 'Enter' && currentFocusedValue && multiValues) {
-      const newMultiValues = removeValueFromSelectedOptions(multiValues, currentFocusedValue);
-      if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
-    }
-    if (removeAllButtonRef.current !== null && e.key === 'ArrowRight' && multiValues && currentFocusedValue === multiValues[multiValues.length - 1].value) {
-      removeAllButtonRef.current.focus();
-    }
-  };
+  const [currentFocusedValue, setCurrentFocusedValue] = useState(undefined);
+  const {
+    selectOptions,
+    parsedValue
+  } = useSelectOptions({
+    options,
+    id,
+    size,
+    value: value
+  });
+  const {
+    activated,
+    multiValues,
+    changeHandler,
+    keyPressHandler
+  } = useSelectHandlers({
+    onChange,
+    multiple,
+    onCreateOption,
+    selectOptions,
+    value,
+    currentFocusedValue,
+    removeAllButtonRef
+  });
   const theme = useTheme();
   const memoizedStyles = useMemo(() => {
     return getMemoizedStyles(theme, zIndex);
   }, [theme, zIndex]);
@@ -179,39 +122,44 @@
       removeAllButtonRef,
       selectInputRef
     },
     children: /*#__PURE__*/_jsx(TypedReactSelect, {
-      ...defaultProps,
       activated: activated,
       "aria-live": "assertive",
       ariaLiveMessages: {
         onFocus
       },
+      components: defaultComponents,
       dropdownWidth: dropdownWidth,
       error: Boolean(error),
+      formatCreateLabel: formatCreateLabel,
       formatGroupLabel: formatGroupLabel,
       formatOptionLabel: formatOptionLabel,
       id: id || rest.htmlFor || rawInputId,
       inputId: inputId,
       inputProps: {
         ...inputProps
       },
       inputWidth: inputWidth,
+      isCreatable: isCreatable,
       isDisabled: disabled,
       isMulti: multiple,
       isOptionDisabled: option => option.disabled,
       isSearchable: isSearchable,
+      isValidNewOption: isValidNewOption,
       menuAlignment: menuAlignment,
       name: name,
+      noOptionsMessage: resolveNoOptionsMessage(validationMessage),
       options: selectOptions,
       placeholder: placeholder,
       selectRef: selectInputRef,
       shownOptionsLimit: shownOptionsLimit,
       size: size,
       styles: memoizedStyles,
       value: multiple ? multiValues : parsedValue,
       onChange: changeHandler,
-      onKeyDown: multiple ? e => keyPressHandler(e) : undefined,
+      onInputChange: onInputChange,
+      onKeyDown: multiple ? keyPressHandler : undefined,
       ...rest
     })
   });
 };
\ No newline at end of file
Index: package/package.json
===================================================================
--- package/package.json
+++ package/package.json
@@ -1,16 +1,16 @@
 {
   "name": "@codecademy/gamut",
   "description": "Styleguide & Component library for Codecademy",
-  "version": "72.2.2",
+  "version": "72.2.3-alpha.f0a032.0",
   "author": "Codecademy Engineering <[email protected]>",
   "bin": "./bin/gamut.mjs",
   "dependencies": {
-    "@codecademy/gamut-icons": "9.57.9",
-    "@codecademy/gamut-illustrations": "0.58.15",
-    "@codecademy/gamut-patterns": "0.10.34",
-    "@codecademy/gamut-styles": "20.0.2",
-    "@codecademy/variance": "0.26.1",
+    "@codecademy/gamut-icons": "9.57.10-alpha.f0a032.0",
+    "@codecademy/gamut-illustrations": "0.58.16-alpha.f0a032.0",
+    "@codecademy/gamut-patterns": "0.10.35-alpha.f0a032.0",
+    "@codecademy/gamut-styles": "20.0.3-alpha.f0a032.0",
+    "@codecademy/variance": "0.26.2-alpha.f0a032.0",
     "@formatjs/intl-locale": "5.3.1",
     "@react-aria/interactions": "3.25.0",
     "@types/marked": "^4.0.8",
     "@vidstack/react": "^1.12.12",
Index: package/agent-tools/skills/gamut-forms/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-forms/SKILL.md
+++ package/agent-tools/skills/gamut-forms/SKILL.md
@@ -25,8 +25,24 @@
 - Checkbox, Radio, Select: same pairing; checkbox/radio use the visually hidden input pattern from `@codecademy/gamut-styles` where applicable.
 
 ---
 
+## SelectDropdown vs Select
+
+Use `Select` for standard single-select fields where bundle size matters and no special styling is needed. Use `SelectDropdown` when the design calls for any of:
+
+- Styled dropdown menu (react-select appearance)
+- Search / typeahead
+- Multi-select with tags
+- Creatable options
+- Option icons, subtitles, right labels, abbreviations, or grouped options
+
+`SelectDropdown` carries a larger JS dependency (react-select); don't reach for it as a default drop-in for `Select`.
+
+For full SelectDropdown API detail — controlled vs uncontrolled patterns, creatable options, react-select action metadata — use [`gamut-select-dropdown`](../gamut-select-dropdown/SKILL.md). Generic `FormGroup` wiring (labels, errors, live regions) still applies as documented below.
+
+---
+
 ## `FormGroup` (baseline)
 
 `packages/gamut/src/Form/elements/FormGroup.tsx`
Index: package/dist/Form/SelectDropdown/types/component-props.d.ts
===================================================================
--- package/dist/Form/SelectDropdown/types/component-props.d.ts
+++ package/dist/Form/SelectDropdown/types/component-props.d.ts
@@ -1,6 +1,6 @@
 import { Ref, SelectHTMLAttributes } from 'react';
-import { Props as NamedProps } from 'react-select';
+import { Options as OptionsType, Props as NamedProps } from 'react-select';
 import { SelectComponentProps } from '../../inputs/Select';
 import { OptionStrict, SelectDropdownGroup, SelectDropdownOptions } from './options';
 import { ReactSelectAdditionalProps, SelectDropdownSizes, SharedProps } from './styles';
 /**
@@ -27,9 +27,9 @@
 /**
  * Core props interface that defines the essential properties for SelectDropdown.
  * This interface combines base props with react-select props and HTML select attributes.
  */
-export interface SelectDropdownCoreProps extends SelectDropdownBaseProps, Omit<NamedProps<OptionStrict, boolean>, 'formatOptionLabel' | 'isDisabled' | 'value' | 'options' | 'components' | 'styles' | 'theme' | 'onChange' | 'multiple'>, Pick<SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'disabled' | 'onClick'>, SharedProps {
+export interface SelectDropdownCoreProps extends SelectDropdownBaseProps, Omit<NamedProps<OptionStrict, boolean>, 'formatOptionLabel' | 'isDisabled' | 'value' | 'options' | 'components' | 'styles' | 'theme' | 'onChange' | 'multiple' | 'isSearchable'>, Pick<SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'disabled' | 'onClick'>, SharedProps {
     /** Required name attribute for the select input */
     name: string;
     /** Placeholder text shown when no option is selected.
      * Placeholder text is not recommended for accessibility. If you need to use placeholder text,
@@ -37,8 +37,41 @@
      * I.e - if the placeholder text describes an action you'd like the user to take, please use a label instead. */
     placeholder?: string;
     /** Array of options or option groups to display in the dropdown */
     options?: SelectDropdownOptions | SelectDropdownGroup[];
+    /**
+     * Allows users to create new options by typing a value not in the options list.
+     * When true, isSearchable is automatically set to true.
+     * Pair with onCreateOption to persist new options.
+     */
+    isCreatable?: boolean;
+    /**
+     * Called when the user confirms a new option via the "Add" row.
+     * Convenience callback for persisting the new value to your `options` list.
+     * Selection updates are delivered through `onChange` with `action: 'create-option'`.
+     */
+    onCreateOption?: (inputValue: string) => void;
+    /**
+     * Customises the label shown in the "Add" row.
+     * Defaults to: (inputValue) => `Add "${inputValue}"`.
+     */
+    formatCreateLabel?: (inputValue: string) => React.ReactNode;
+    /**
+     * Controls when the "Add" row is visible.
+     * Receives the current input, selected values, and all options.
+     * Defaults to react-select's built-in logic (hidden when input matches an existing option label).
+     * Use cases: minimum-length gating, pattern validation, case-insensitive dedup, max-items cap.
+     */
+    isValidNewOption?: (inputValue: string, value: OptionsType<OptionStrict>, options: OptionsType<OptionStrict>) => boolean;
+    /**
+     * Customizes the message shown inside the dropdown menu when no option matches
+     * the current input (react-select's "No options" state). Useful for surfacing
+     * validation/error text directly in the dropdown. Accepts a node, or a function
+     * receiving the current input value.
+     */
+    validationMessage?: React.ReactNode | ((obj: {
+        inputValue: string;
+    }) => React.ReactNode);
 }
 /**
  * Props for single-select mode.
  * When multiple is false or undefined, only one option can be selected.
@@ -59,12 +92,24 @@
     /** Callback fired when the selected values change */
     onChange?: NamedProps<OptionStrict, true>['onChange'];
 }
 /**
+ * Enforces that isSearchable cannot be false when isCreatable is true.
+ * Creatable mode requires the search input so users can type new option values.
+ */
+type CreatableConstraint = {
+    isCreatable?: false | undefined;
+    isSearchable?: boolean;
+} | {
+    isCreatable: true;
+    isSearchable?: true;
+};
+/**
  * Union type for all SelectDropdown prop variants.
- * Supports both single and multi-select modes through discriminated union.
+ * Supports both single and multi-select modes through discriminated union,
+ * intersected with CreatableConstraint to enforce isSearchable compatibility.
  */
-export type SelectDropdownProps = SingleSelectDropdownProps | MultiSelectDropdownProps;
+export type SelectDropdownProps = (SingleSelectDropdownProps | MultiSelectDropdownProps) & CreatableConstraint;
 /**
  * Base interface for onChange-related props.
  * Used internally for type checking and prop validation.
  */
@@ -75,10 +120,13 @@
     onChange?: SingleSelectDropdownProps['onChange'] | MultiSelectDropdownProps['onChange'];
 }
 /**
  * Props for the typed React Select component wrapper.
- * Extends ReactSelectAdditionalProps with an optional ref.
+ * Extends ReactSelectAdditionalProps with an optional ref and creatable flag.
  */
-export interface TypedReactSelectProps extends ReactSelectAdditionalProps {
+export interface TypedReactSelectProps extends ReactSelectAdditionalProps, Pick<SelectDropdownCoreProps, 'formatCreateLabel' | 'isValidNewOption'> {
     /** Optional ref to the underlying react-select component */
     selectRef?: Ref<any>;
+    /** When true, renders CreatableSelect instead of ReactSelect */
+    isCreatable?: boolean;
 }
+export {};
Index: package/dist/Form/SelectDropdown/elements/constants.d.ts
===================================================================
--- package/dist/Form/SelectDropdown/elements/constants.d.ts
+++ package/dist/Form/SelectDropdown/elements/constants.d.ts
@@ -14,16 +14,8 @@
     mediumChevron: {
         size: number;
         icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
     };
-    smallSearchable: {
-        size: number;
-        icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
-    };
-    mediumSearchable: {
-        size: number;
-        icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
-    };
     smallRemove: {
         size: number;
         icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
     };
Index: package/dist/Form/SelectDropdown/elements/containers.d.ts
===================================================================
--- package/dist/Form/SelectDropdown/elements/containers.d.ts
+++ package/dist/Form/SelectDropdown/elements/containers.d.ts
@@ -23,7 +23,11 @@
  */
 export declare const CustomInput: ({ ...rest }: CustomSelectComponentProps<typeof SelectDropdownElements.Input>) => import("react/jsx-runtime").JSX.Element;
 /**
  * Typed wrapper around react-select component.
- * Provides type safety for the underlying react-select implementation.
+ * Renders CreatableSelect when isCreatable is true, ReactSelect otherwise.
+ * Creatable-only props (formatCreateLabel, isValidNewOption) are stripped from
+ * the non-creatable path so they don't reach ReactSelect. `onCreateOption` is
+ * handled in SelectDropdown's changeHandler — do not pass it to CreatableSelect
+ * or react-select will skip onChange on create.
  */
-export declare function TypedReactSelect<OptionType, IsMulti extends boolean = false, GroupType extends GroupBase<OptionType> = GroupBase<OptionType>>({ selectRef, ...props }: Props<OptionType, IsMulti, GroupType> & TypedReactSelectProps): import("react/jsx-runtime").JSX.Element;
+export declare function TypedReactSelect<OptionType, IsMulti extends boolean = false, GroupType extends GroupBase<OptionType> = GroupBase<OptionType>>({ selectRef, isCreatable, formatCreateLabel, isValidNewOption, ...props }: Props<OptionType, IsMulti, GroupType> & TypedReactSelectProps): import("react/jsx-runtime").JSX.Element;
Index: package/dist/Form/SelectDropdown/elements/controls.d.ts
===================================================================
--- package/dist/Form/SelectDropdown/elements/controls.d.ts
+++ package/dist/Form/SelectDropdown/elements/controls.d.ts
@@ -1,20 +1,6 @@
-import { AriaOnFocus } from 'react-select';
-import { ExtendedOption, SizedIndicatorProps } from '../types';
+import { SizedIndicatorProps } from '../types';
 /**
- * Generates accessible focus messages for screen readers.
- * Provides detailed information about the currently focused option.
- *
- * @param params - Object containing the focused option details
- * @returns Formatted accessibility message
- */
-export declare const onFocus: AriaOnFocus<ExtendedOption>;
-/**
  * Custom dropdown indicator that shows either a chevron or search icon.
  * The icon type depends on whether the select is searchable or not.
  */
 export declare const DropdownButton: (props: SizedIndicatorProps) => import("react/jsx-runtime").JSX.Element;
-/**
- * Custom remove all button for multi-select mode.
- * Provides keyboard navigation and accessible removal of all selected values.
- */
-export declare const RemoveAllButton: (props: SizedIndicatorProps) => import("react/jsx-runtime").JSX.Element;
Index: package/dist/Form/SelectDropdown/elements/index.d.ts
===================================================================
--- package/dist/Form/SelectDropdown/elements/index.d.ts
+++ package/dist/Form/SelectDropdown/elements/index.d.ts
@@ -1,5 +1,6 @@
 export { iconSize, selectedIconSize, indicatorIcons } from './constants';
 export { MultiValueWithColorMode, MultiValueRemoveButton, RemoveAllButton, } from './multi-value';
-export { DropdownButton, onFocus } from './controls';
+export { DropdownButton } from './controls';
+export { onFocus } from '../core/accessibility';
 export { SelectDropdownContext, CustomContainer, CustomInput, CustomValueContainer, TypedReactSelect, } from './containers';
 export { IconOption, AbbreviatedSingleValue, formatOptionLabel, formatGroupLabel, } from './options';
Index: package/dist/Form/styles/index.d.ts
===================================================================
--- package/dist/Form/styles/index.d.ts
+++ package/dist/Form/styles/index.d.ts
@@ -1,4 +1,4 @@
 export * from './shared-system-props';
 export * from './Checkbox-styles';
 export * from './Radio-styles';
-export * from '../SelectDropdown/styles';
+export * from '../SelectDropdown/core/styles';
Index: package/dist/Form/SelectDropdown/types/internal.d.ts
===================================================================
--- package/dist/Form/SelectDropdown/types/internal.d.ts
+++ package/dist/Form/SelectDropdown/types/internal.d.ts
@@ -13,9 +13,9 @@
 /**
  * Ref type for programmatic focus management.
  * Used for managing focus on select input and remove all button.
  */
-export type ProgramaticFocusRef = React.MutableRefObject<HTMLDivElement> | React.MutableRefObject<null>;
+export type ProgrammaticFocusRef = React.MutableRefObject<HTMLDivElement> | React.MutableRefObject<null>;
 /**
  * Context value for SelectDropdown internal state management.
  * Provides access to focus state and refs for keyboard navigation.
  */
@@ -24,11 +24,11 @@
     currentFocusedValue?: SelectOptionBase['value'];
     /** Function to update the currently focused value */
     setCurrentFocusedValue?: React.Dispatch<React.SetStateAction<unknown>>;
     /** Ref to the select input for programmatic focus */
-    selectInputRef?: ProgramaticFocusRef;
+    selectInputRef?: ProgrammaticFocusRef;
     /** Ref to the remove all button for programmatic focus */
-    removeAllButtonRef?: ProgramaticFocusRef;
+    removeAllButtonRef?: ProgrammaticFocusRef;
 }
 /**
  * Props for sized indicator components (dropdown arrow, search icon, etc.).
  * Combines react-select's DropdownIndicatorProps with internal select props.
Index: package/dist/Form/SelectDropdown/elements/options.d.ts
===================================================================
--- package/dist/Form/SelectDropdown/elements/options.d.ts
+++ package/dist/Form/SelectDropdown/elements/options.d.ts
@@ -2,8 +2,9 @@
 import { CustomSelectComponentProps, ExtendedOption, SelectDropdownGroup } from '../types';
 /**
  * Custom option component that displays a check icon for selected items.
  * Also manages ARIA attributes for accessibility.
+ * Skips the check icon for react-select/creatable's "Add" row (__isNew__).
  */
 export declare const IconOption: ({ children, ...rest }: CustomSelectComponentProps<typeof SelectDropdownElements.Option>) => import("react/jsx-runtime").JSX.Element;
 /**
  * Custom single value component that displays abbreviated text when available.
Index: package/dist/Form/SelectDropdown/types/styles.d.ts
===================================================================
--- package/dist/Form/SelectDropdown/types/styles.d.ts
+++ package/dist/Form/SelectDropdown/types/styles.d.ts
@@ -1,6 +1,6 @@
 import { StyleProps } from '@codecademy/variance';
-import { conditionalBorderStates } from '../styles';
+import { conditionalBorderStates } from '../core/styles';
 import { InternalInputsProps } from './component-props';
 /**
  * Size variants for the SelectDropdown component.
  */
@@ -68,6 +68,10 @@
  */
 export type OptionState = BaseSelectComponentProps & InteractionStates & {
     /** Whether the option is selected */
     isSelected: boolean;
+    /** Option data — includes __isNew__ for react-select/creatable's "Add" row */
+    data?: {
+        __isNew__?: boolean;
+    };
 };
 export {};