@codecademy/gamut

71.0.071.0.1-alpha.69ab4c.0
dist/Form/SelectDropdown/SelectDropdown.js
~dist/Form/SelectDropdown/SelectDropdown.jsModified
+35−18
Index: package/dist/Form/SelectDropdown/SelectDropdown.js
===================================================================
--- package/dist/Form/SelectDropdown/SelectDropdown.js
+++ package/dist/Form/SelectDropdown/SelectDropdown.js
@@ -3,9 +3,9 @@
 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 { filterValueFromOptions, getCreatedOptionValue, isMultipleSelectProps, isOptionsGrouped, isSingleSelectProps, removeValueFromSelectedOptions } from './utils';
 import { jsx as _jsx } from "react/jsx-runtime";
 const defaultProps = {
   name: undefined,
   components: {
@@ -72,24 +72,32 @@
 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);
@@ -125,41 +133,43 @@
   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.
+  // Sync multi-select value from props when controlled (`value` is a string[]).
+  // Uncontrolled multi (`value` undefined or '') keeps selection in local state.
   useEffect(() => {
+    if (!multiple || !Array.isArray(value)) return;
     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 => {
+  }, [options, value, multiple]);
+  const changeHandler = useCallback((optionEvent, actionMeta) => {
     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
+    if (actionMeta.action === 'create-option') {
+      const createdValue = getCreatedOptionValue(optionEvent, actionMeta, multiple);
+      if (createdValue) {
+        onCreateOption?.(createdValue);
+      }
+    }
     const onChangeProps = {
       onChange,
       multiple
     };
+    const forwardedMeta = actionMeta.action === 'create-option' ? actionMeta : {
+      action: onChangeAction,
+      option: isMultipleSelectProps(onChangeProps) ? undefined : optionEvent
+    };
     if (isSingleSelectProps(onChangeProps)) {
       const singleOptionEvent = optionEvent;
-      onChangeProps.onChange?.(singleOptionEvent, {
-        action: onChangeAction,
-        option: singleOptionEvent
-      });
+      onChangeProps.onChange?.(singleOptionEvent, forwardedMeta);
     }
     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)
-      });
+      onChangeProps.onChange?.(optionEvent, forwardedMeta);
     }
-  }, [onChange, multiple]);
+  }, [onChange, multiple, onCreateOption]);
   const keyPressHandler = e => {
     if (multiple && e.key === 'Enter' && currentFocusedValue && multiValues) {
       const newMultiValues = removeValueFromSelectedOptions(multiValues, currentFocusedValue);
       if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
@@ -167,8 +177,10 @@
     if (removeAllButtonRef.current !== null && e.key === 'ArrowRight' && multiValues && currentFocusedValue === multiValues[multiValues.length - 1].value) {
       removeAllButtonRef.current.focus();
     }
   };
+  const noOptionsMessage = validationMessage === undefined ? undefined // fall back to react-select default ("No options")
+  : typeof validationMessage === 'function' ? validationMessage : () => validationMessage;
   const theme = useTheme();
   const memoizedStyles = useMemo(() => {
     return getMemoizedStyles(theme, zIndex);
   }, [theme, zIndex]);
@@ -187,30 +199,35 @@
         onFocus
       },
       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: noOptionsMessage,
       options: selectOptions,
       placeholder: placeholder,
       selectRef: selectInputRef,
       shownOptionsLimit: shownOptionsLimit,
       size: size,
       styles: memoizedStyles,
       value: multiple ? multiValues : parsedValue,
       onChange: changeHandler,
+      onInputChange: onInputChange,
       onKeyDown: multiple ? e => keyPressHandler(e) : undefined,
       ...rest
     })
   });