@codecademy/gamut

72.2.272.2.3-alpha.83b0a8.0
dist/Form/SelectDropdown/SelectDropdown.js
~dist/Form/SelectDropdown/SelectDropdown.jsModified
+48−100
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