@codecademy/gamut
72.2.272.2.3-alpha.f0a032.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