@codecademy/gamut 72.2.3-alpha.e393cd.0 → 72.2.3-alpha.f0a032.0
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/dist/Form/SelectDropdown/SelectDropdown.js +35 -104
- package/dist/Form/SelectDropdown/core/accessibility.d.ts +3 -0
- package/dist/Form/SelectDropdown/core/accessibility.js +12 -0
- package/dist/Form/SelectDropdown/core/constants.d.ts +13 -0
- package/dist/Form/SelectDropdown/core/constants.js +14 -0
- package/dist/Form/SelectDropdown/{styles.js → core/styles.js} +2 -2
- package/dist/Form/SelectDropdown/{utils.d.ts → core/utils.d.ts} +6 -2
- package/dist/Form/SelectDropdown/{utils.js → core/utils.js} +7 -0
- package/dist/Form/SelectDropdown/elements/controls.d.ts +1 -10
- package/dist/Form/SelectDropdown/elements/controls.js +0 -21
- package/dist/Form/SelectDropdown/elements/index.d.ts +2 -1
- package/dist/Form/SelectDropdown/elements/index.js +2 -1
- package/dist/Form/SelectDropdown/hooks/useSelectHandlers.d.ts +22 -0
- package/dist/Form/SelectDropdown/hooks/useSelectHandlers.js +62 -0
- package/dist/Form/SelectDropdown/hooks/useSelectOptions.d.ts +14 -0
- package/dist/Form/SelectDropdown/hooks/useSelectOptions.js +39 -0
- package/dist/Form/SelectDropdown/types/internal.d.ts +3 -3
- package/dist/Form/SelectDropdown/types/styles.d.ts +1 -1
- package/dist/Form/styles/index.d.ts +1 -1
- package/dist/Form/styles/index.js +1 -1
- package/package.json +6 -6
- /package/dist/Form/SelectDropdown/{styles.d.ts → core/styles.d.ts} +0 -0
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
import { useTheme } from '@emotion/react';
|
|
2
|
-
import {
|
|
2
|
+
import { useId, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { getMemoizedStyles } from './styles';
|
|
7
|
-
import {
|
|
4
|
+
import { onFocus } from './core/accessibility';
|
|
5
|
+
import { defaultComponents } from './core/constants';
|
|
6
|
+
import { getMemoizedStyles } from './core/styles';
|
|
7
|
+
import { resolveNoOptionsMessage } from './core/utils';
|
|
8
|
+
import { formatGroupLabel, formatOptionLabel, SelectDropdownContext, TypedReactSelect } from './elements';
|
|
9
|
+
import { useSelectHandlers } from './hooks/useSelectHandlers';
|
|
10
|
+
import { useSelectOptions } from './hooks/useSelectOptions';
|
|
8
11
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
-
const defaultProps = {
|
|
10
|
-
name: undefined,
|
|
11
|
-
components: {
|
|
12
|
-
DropdownIndicator: DropdownButton,
|
|
13
|
-
IndicatorSeparator: () => null,
|
|
14
|
-
ClearIndicator: RemoveAllButton,
|
|
15
|
-
SelectContainer: CustomContainer,
|
|
16
|
-
ValueContainer: CustomValueContainer,
|
|
17
|
-
MultiValue: MultiValueWithColorMode,
|
|
18
|
-
MultiValueRemove: MultiValueRemoveButton,
|
|
19
|
-
Option: IconOption,
|
|
20
|
-
SingleValue: AbbreviatedSingleValue,
|
|
21
|
-
Input: CustomInput
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
const onChangeAction = 'select-option';
|
|
25
|
-
|
|
26
12
|
/**
|
|
27
13
|
* A flexible dropdown select component built on top of react-select.
|
|
28
14
|
*
|
|
@@ -99,87 +85,32 @@ export const SelectDropdown = ({
|
|
|
99
85
|
const isSearchable = isCreatable || isSearchableProp;
|
|
100
86
|
const rawInputId = useId();
|
|
101
87
|
const inputId = name ?? `${id}-select-dropdown-${rawInputId}`;
|
|
102
|
-
const [activated, setActivated] = useState(false);
|
|
103
|
-
const [currentFocusedValue, setCurrentFocusedValue] = useState(undefined);
|
|
104
|
-
|
|
105
|
-
// these are used to programatically manage the focus state of our multi-select options + 'Remove all' button
|
|
106
88
|
const removeAllButtonRef = useRef(null);
|
|
107
89
|
const selectInputRef = useRef(null);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}, [selectOptions, value]);
|
|
133
|
-
const [multiValues, setMultiValues] = useState(multiple &&
|
|
134
|
-
// To keep this efficient for non-multiSelect
|
|
135
|
-
filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions)));
|
|
136
|
-
|
|
137
|
-
// Sync multi-select value from props when controlled (`value` is a string[]).
|
|
138
|
-
// Uncontrolled multi (`value` undefined or '') keeps selection in local state.
|
|
139
|
-
useEffect(() => {
|
|
140
|
-
if (!multiple || !Array.isArray(value)) return;
|
|
141
|
-
const newMultiValues = filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions));
|
|
142
|
-
if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
|
|
143
|
-
|
|
144
|
-
// We only update this when our passed in options or value changes, not multiValues.
|
|
145
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
|
-
}, [options, value, multiple]);
|
|
147
|
-
const changeHandler = useCallback((optionEvent, actionMeta) => {
|
|
148
|
-
setActivated(true);
|
|
149
|
-
if (actionMeta.action === 'create-option') {
|
|
150
|
-
const createdValue = getCreatedOptionValue(optionEvent, actionMeta, multiple);
|
|
151
|
-
if (createdValue) {
|
|
152
|
-
onCreateOption?.(createdValue);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const onChangeProps = {
|
|
156
|
-
onChange,
|
|
157
|
-
multiple
|
|
158
|
-
};
|
|
159
|
-
const forwardedMeta = actionMeta.action === 'create-option' ? actionMeta : {
|
|
160
|
-
action: onChangeAction,
|
|
161
|
-
option: isMultipleSelectProps(onChangeProps) ? undefined : optionEvent
|
|
162
|
-
};
|
|
163
|
-
if (isSingleSelectProps(onChangeProps)) {
|
|
164
|
-
const singleOptionEvent = optionEvent;
|
|
165
|
-
onChangeProps.onChange?.(singleOptionEvent, forwardedMeta);
|
|
166
|
-
}
|
|
167
|
-
if (isMultipleSelectProps(onChangeProps)) {
|
|
168
|
-
setMultiValues(optionEvent);
|
|
169
|
-
onChangeProps.onChange?.(optionEvent, forwardedMeta);
|
|
170
|
-
}
|
|
171
|
-
}, [onChange, multiple, onCreateOption]);
|
|
172
|
-
const keyPressHandler = e => {
|
|
173
|
-
if (multiple && e.key === 'Enter' && currentFocusedValue && multiValues) {
|
|
174
|
-
const newMultiValues = removeValueFromSelectedOptions(multiValues, currentFocusedValue);
|
|
175
|
-
if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
|
|
176
|
-
}
|
|
177
|
-
if (removeAllButtonRef.current !== null && e.key === 'ArrowRight' && multiValues && currentFocusedValue === multiValues[multiValues.length - 1].value) {
|
|
178
|
-
removeAllButtonRef.current.focus();
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
const noOptionsMessage = validationMessage === undefined ? undefined // fall back to react-select default ("No options")
|
|
182
|
-
: typeof validationMessage === 'function' ? validationMessage : () => validationMessage;
|
|
90
|
+
const [currentFocusedValue, setCurrentFocusedValue] = useState(undefined);
|
|
91
|
+
const {
|
|
92
|
+
selectOptions,
|
|
93
|
+
parsedValue
|
|
94
|
+
} = useSelectOptions({
|
|
95
|
+
options,
|
|
96
|
+
id,
|
|
97
|
+
size,
|
|
98
|
+
value: value
|
|
99
|
+
});
|
|
100
|
+
const {
|
|
101
|
+
activated,
|
|
102
|
+
multiValues,
|
|
103
|
+
changeHandler,
|
|
104
|
+
keyPressHandler
|
|
105
|
+
} = useSelectHandlers({
|
|
106
|
+
onChange,
|
|
107
|
+
multiple,
|
|
108
|
+
onCreateOption,
|
|
109
|
+
selectOptions,
|
|
110
|
+
value,
|
|
111
|
+
currentFocusedValue,
|
|
112
|
+
removeAllButtonRef
|
|
113
|
+
});
|
|
183
114
|
const theme = useTheme();
|
|
184
115
|
const memoizedStyles = useMemo(() => {
|
|
185
116
|
return getMemoizedStyles(theme, zIndex);
|
|
@@ -192,12 +123,12 @@ export const SelectDropdown = ({
|
|
|
192
123
|
selectInputRef
|
|
193
124
|
},
|
|
194
125
|
children: /*#__PURE__*/_jsx(TypedReactSelect, {
|
|
195
|
-
...defaultProps,
|
|
196
126
|
activated: activated,
|
|
197
127
|
"aria-live": "assertive",
|
|
198
128
|
ariaLiveMessages: {
|
|
199
129
|
onFocus
|
|
200
130
|
},
|
|
131
|
+
components: defaultComponents,
|
|
201
132
|
dropdownWidth: dropdownWidth,
|
|
202
133
|
error: Boolean(error),
|
|
203
134
|
formatCreateLabel: formatCreateLabel,
|
|
@@ -217,7 +148,7 @@ export const SelectDropdown = ({
|
|
|
217
148
|
isValidNewOption: isValidNewOption,
|
|
218
149
|
menuAlignment: menuAlignment,
|
|
219
150
|
name: name,
|
|
220
|
-
noOptionsMessage:
|
|
151
|
+
noOptionsMessage: resolveNoOptionsMessage(validationMessage),
|
|
221
152
|
options: selectOptions,
|
|
222
153
|
placeholder: placeholder,
|
|
223
154
|
selectRef: selectInputRef,
|
|
@@ -227,7 +158,7 @@ export const SelectDropdown = ({
|
|
|
227
158
|
value: multiple ? multiValues : parsedValue,
|
|
228
159
|
onChange: changeHandler,
|
|
229
160
|
onInputChange: onInputChange,
|
|
230
|
-
onKeyDown: multiple ?
|
|
161
|
+
onKeyDown: multiple ? keyPressHandler : undefined,
|
|
231
162
|
...rest
|
|
232
163
|
})
|
|
233
164
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const onFocus = ({
|
|
2
|
+
focused: {
|
|
3
|
+
label,
|
|
4
|
+
subtitle,
|
|
5
|
+
rightLabel,
|
|
6
|
+
disabled
|
|
7
|
+
}
|
|
8
|
+
}) => {
|
|
9
|
+
const formattedSubtitle = `, ${subtitle}`;
|
|
10
|
+
const formattedRightLabel = `, ${rightLabel}`;
|
|
11
|
+
return `You are currently focused on option ${label}${subtitle ? formattedSubtitle : ''} ${rightLabel ? formattedRightLabel : ''}${disabled ? ', disabled' : ''}`;
|
|
12
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const defaultComponents: {
|
|
2
|
+
DropdownIndicator: (props: import("../types").SizedIndicatorProps) => import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
IndicatorSeparator: () => null;
|
|
4
|
+
ClearIndicator: (props: import("../types").SizedIndicatorProps) => import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
SelectContainer: ({ children, ...rest }: import("../types").CustomSelectComponentProps<(<Option_18, IsMulti_18 extends boolean, Group_18 extends import("react-select").GroupBase<Option_18>>(props: import("react-select").ContainerProps<Option_18, IsMulti_18, Group_18>) => import("@emotion/react").jsx.JSX.Element)>) => import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
ValueContainer: ({ ...rest }: import("../types").CustomSelectComponentProps<(<Option_20, IsMulti_20 extends boolean, Group_20 extends import("react-select").GroupBase<Option_20>>(props: import("react-select").ValueContainerProps<Option_20, IsMulti_20, Group_20>) => import("@emotion/react").jsx.JSX.Element)>) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
MultiValue: (props: import("react-select").MultiValueProps<import("..").ExtendedOption, true, import("react-select").GroupBase<import("..").ExtendedOption>>) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
MultiValueRemove: (props: import("react-select").MultiValueRemoveProps<import("..").ExtendedOption, true, import("react-select").GroupBase<import("..").ExtendedOption>>) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
Option: ({ children, ...rest }: import("../types").CustomSelectComponentProps<(<Option_16, IsMulti_16 extends boolean, Group_16 extends import("react-select").GroupBase<Option_16>>(props: import("react-select").OptionProps<Option_16, IsMulti_16, Group_16>) => import("@emotion/react").jsx.JSX.Element)>) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
SingleValue: (props: import("react-select").SingleValueProps<import("..").ExtendedOption, false>) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
Input: ({ ...rest }: import("../types").CustomSelectComponentProps<(<Option_7, IsMulti_7 extends boolean, Group_7 extends import("react-select").GroupBase<Option_7>>(props: import("react-select").InputProps<Option_7, IsMulti_7, Group_7>) => import("@emotion/react").jsx.JSX.Element)>) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
};
|
|
13
|
+
export declare const ON_CHANGE_ACTION: "select-option";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AbbreviatedSingleValue, CustomContainer, CustomInput, CustomValueContainer, DropdownButton, IconOption, MultiValueRemoveButton, MultiValueWithColorMode, RemoveAllButton } from '../elements';
|
|
2
|
+
export const defaultComponents = {
|
|
3
|
+
DropdownIndicator: DropdownButton,
|
|
4
|
+
IndicatorSeparator: () => null,
|
|
5
|
+
ClearIndicator: RemoveAllButton,
|
|
6
|
+
SelectContainer: CustomContainer,
|
|
7
|
+
ValueContainer: CustomValueContainer,
|
|
8
|
+
MultiValue: MultiValueWithColorMode,
|
|
9
|
+
MultiValueRemove: MultiValueRemoveButton,
|
|
10
|
+
Option: IconOption,
|
|
11
|
+
SingleValue: AbbreviatedSingleValue,
|
|
12
|
+
Input: CustomInput
|
|
13
|
+
};
|
|
14
|
+
export const ON_CHANGE_ACTION = 'select-option';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { css, states, variant } from '@codecademy/gamut-styles';
|
|
2
|
-
import { dismissSharedStyles, tagBaseStyles, tagLabelFontSize, tagLabelPadding } from '
|
|
3
|
-
import { formBaseComponentStyles, formBaseFieldStylesObject, formFieldDisabledStyles, formFieldPaddingStyles, InputSelectors } from '
|
|
2
|
+
import { dismissSharedStyles, tagBaseStyles, tagLabelFontSize, tagLabelPadding } from '../../../Tag/styles';
|
|
3
|
+
import { formBaseComponentStyles, formBaseFieldStylesObject, formFieldDisabledStyles, formFieldPaddingStyles, InputSelectors } from '../../styles';
|
|
4
4
|
const selectDropdownStyles = css({
|
|
5
5
|
...formBaseFieldStylesObject,
|
|
6
6
|
display: 'flex',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
1
2
|
import { ActionMeta, Options as OptionsType } from 'react-select';
|
|
2
|
-
import { SelectOptionBase } from '
|
|
3
|
-
import { BaseOnChangeProps, ExtendedOption, MultiSelectDropdownProps, OptionStrict, SelectDropdownGroup, SelectDropdownOptions, SelectDropdownProps, SingleSelectDropdownProps } from '
|
|
3
|
+
import { SelectOptionBase } from '../../utils';
|
|
4
|
+
import { BaseOnChangeProps, ExtendedOption, MultiSelectDropdownProps, OptionStrict, SelectDropdownGroup, SelectDropdownOptions, SelectDropdownProps, SingleSelectDropdownProps } from '../types';
|
|
4
5
|
export declare const isMultipleSelectProps: (props: BaseOnChangeProps) => props is MultiSelectDropdownProps;
|
|
5
6
|
export declare const isSingleSelectProps: (props: BaseOnChangeProps) => props is SingleSelectDropdownProps;
|
|
6
7
|
/**
|
|
@@ -29,4 +30,7 @@ export declare const filterValueFromOptions: (options: SelectOptionBase[] | Sele
|
|
|
29
30
|
* @param value - The value or values to remove
|
|
30
31
|
* @returns New array with the specified values removed
|
|
31
32
|
*/
|
|
33
|
+
export declare const resolveNoOptionsMessage: (validationMessage: SelectDropdownProps["validationMessage"]) => ((obj: {
|
|
34
|
+
inputValue: string;
|
|
35
|
+
}) => React.ReactNode) | undefined;
|
|
32
36
|
export declare const removeValueFromSelectedOptions: (selectedOptions: ExtendedOption[] | SelectOptionBase[], value: SelectDropdownProps["value"]) => SelectOptionBase[];
|
|
@@ -44,6 +44,13 @@ export const filterValueFromOptions = (options, value, optionsAreGrouped) => {
|
|
|
44
44
|
* @param value - The value or values to remove
|
|
45
45
|
* @returns New array with the specified values removed
|
|
46
46
|
*/
|
|
47
|
+
export const resolveNoOptionsMessage = validationMessage => {
|
|
48
|
+
if (validationMessage === undefined) return undefined;
|
|
49
|
+
if (typeof validationMessage === 'function') {
|
|
50
|
+
return validationMessage;
|
|
51
|
+
}
|
|
52
|
+
return () => validationMessage;
|
|
53
|
+
};
|
|
47
54
|
export const removeValueFromSelectedOptions = (selectedOptions, value) => {
|
|
48
55
|
return selectedOptions.filter(option => {
|
|
49
56
|
if (Array.isArray(value)) {
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ExtendedOption, SizedIndicatorProps } from '../types';
|
|
3
|
-
/**
|
|
4
|
-
* Generates accessible focus messages for screen readers.
|
|
5
|
-
* Provides detailed information about the currently focused option.
|
|
6
|
-
*
|
|
7
|
-
* @param params - Object containing the focused option details
|
|
8
|
-
* @returns Formatted accessibility message
|
|
9
|
-
*/
|
|
10
|
-
export declare const onFocus: AriaOnFocus<ExtendedOption>;
|
|
1
|
+
import { SizedIndicatorProps } from '../types';
|
|
11
2
|
/**
|
|
12
3
|
* Custom dropdown indicator that shows either a chevron or search icon.
|
|
13
4
|
* The icon type depends on whether the select is searchable or not.
|
|
@@ -5,27 +5,6 @@ const {
|
|
|
5
5
|
DropdownIndicator
|
|
6
6
|
} = SelectDropdownElements;
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Generates accessible focus messages for screen readers.
|
|
10
|
-
* Provides detailed information about the currently focused option.
|
|
11
|
-
*
|
|
12
|
-
* @param params - Object containing the focused option details
|
|
13
|
-
* @returns Formatted accessibility message
|
|
14
|
-
*/
|
|
15
|
-
export const onFocus = ({
|
|
16
|
-
focused: {
|
|
17
|
-
label,
|
|
18
|
-
subtitle,
|
|
19
|
-
rightLabel,
|
|
20
|
-
disabled
|
|
21
|
-
}
|
|
22
|
-
}) => {
|
|
23
|
-
const formattedSubtitle = `, ${subtitle}`;
|
|
24
|
-
const formattedRightLabel = `, ${rightLabel}`;
|
|
25
|
-
const msg = `You are currently focused on option ${label}${subtitle ? formattedSubtitle : ''} ${rightLabel ? formattedRightLabel : ''}${disabled ? ', disabled' : ''}`;
|
|
26
|
-
return msg;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
8
|
/**
|
|
30
9
|
* Custom dropdown indicator that shows either a chevron or search icon.
|
|
31
10
|
* The icon type depends on whether the select is searchable or not.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { iconSize, selectedIconSize, indicatorIcons } from './constants';
|
|
2
2
|
export { MultiValueWithColorMode, MultiValueRemoveButton, RemoveAllButton, } from './multi-value';
|
|
3
|
-
export { DropdownButton
|
|
3
|
+
export { DropdownButton } from './controls';
|
|
4
|
+
export { onFocus } from '../core/accessibility';
|
|
4
5
|
export { SelectDropdownContext, CustomContainer, CustomInput, CustomValueContainer, TypedReactSelect, } from './containers';
|
|
5
6
|
export { IconOption, AbbreviatedSingleValue, formatOptionLabel, formatGroupLabel, } from './options';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { iconSize, selectedIconSize, indicatorIcons } from './constants';
|
|
2
2
|
export { MultiValueWithColorMode, MultiValueRemoveButton, RemoveAllButton } from './multi-value';
|
|
3
|
-
export { DropdownButton
|
|
3
|
+
export { DropdownButton } from './controls';
|
|
4
|
+
export { onFocus } from '../core/accessibility';
|
|
4
5
|
export { SelectDropdownContext, CustomContainer, CustomInput, CustomValueContainer, TypedReactSelect } from './containers';
|
|
5
6
|
export { IconOption, AbbreviatedSingleValue, formatOptionLabel, formatGroupLabel } from './options';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { KeyboardEvent } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { ActionMeta, Options as OptionsType } from 'react-select';
|
|
4
|
+
import { SelectOptionBase } from '../../utils';
|
|
5
|
+
import { MultiSelectDropdownProps, OptionStrict, SelectDropdownGroup, SelectDropdownProps, SingleSelectDropdownProps } from '../types';
|
|
6
|
+
interface UseSelectHandlersArgs {
|
|
7
|
+
onChange?: SingleSelectDropdownProps['onChange'] | MultiSelectDropdownProps['onChange'];
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
onCreateOption?: (inputValue: string) => void;
|
|
10
|
+
selectOptions: SelectOptionBase[] | SelectDropdownGroup[];
|
|
11
|
+
value?: SelectDropdownProps['value'];
|
|
12
|
+
currentFocusedValue: unknown;
|
|
13
|
+
removeAllButtonRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
14
|
+
}
|
|
15
|
+
interface UseSelectHandlersReturn {
|
|
16
|
+
activated: boolean;
|
|
17
|
+
multiValues: OptionStrict[] | false;
|
|
18
|
+
changeHandler: (optionEvent: OptionStrict | OptionsType<OptionStrict>, actionMeta: ActionMeta<OptionStrict>) => void;
|
|
19
|
+
keyPressHandler: (e: KeyboardEvent<HTMLDivElement>) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare const useSelectHandlers: ({ onChange, multiple, onCreateOption, selectOptions, value, currentFocusedValue, removeAllButtonRef, }: UseSelectHandlersArgs) => UseSelectHandlersReturn;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { ON_CHANGE_ACTION } from '../core/constants';
|
|
3
|
+
import { filterValueFromOptions, getCreatedOptionValue, isMultipleSelectProps, isOptionsGrouped, isSingleSelectProps, removeValueFromSelectedOptions } from '../core/utils';
|
|
4
|
+
export const useSelectHandlers = ({
|
|
5
|
+
onChange,
|
|
6
|
+
multiple,
|
|
7
|
+
onCreateOption,
|
|
8
|
+
selectOptions,
|
|
9
|
+
value,
|
|
10
|
+
currentFocusedValue,
|
|
11
|
+
removeAllButtonRef
|
|
12
|
+
}) => {
|
|
13
|
+
const [activated, setActivated] = useState(false);
|
|
14
|
+
const [multiValues, setMultiValues] = useState(multiple ? filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions)) : false);
|
|
15
|
+
|
|
16
|
+
// Sync multi-select value from props when controlled (`value` is a string[]).
|
|
17
|
+
// Uncontrolled multi (`value` undefined or '') keeps selection in local state.
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!multiple || !Array.isArray(value)) return;
|
|
20
|
+
const newMultiValues = filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions));
|
|
21
|
+
if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
24
|
+
}, [selectOptions, value, multiple]);
|
|
25
|
+
const changeHandler = useCallback((optionEvent, actionMeta) => {
|
|
26
|
+
setActivated(true);
|
|
27
|
+
if (actionMeta.action === 'create-option') {
|
|
28
|
+
const createdValue = getCreatedOptionValue(optionEvent, actionMeta, multiple);
|
|
29
|
+
if (createdValue) onCreateOption?.(createdValue);
|
|
30
|
+
}
|
|
31
|
+
const onChangeProps = {
|
|
32
|
+
onChange,
|
|
33
|
+
multiple
|
|
34
|
+
};
|
|
35
|
+
const forwardedMeta = actionMeta.action === 'create-option' ? actionMeta : {
|
|
36
|
+
action: ON_CHANGE_ACTION,
|
|
37
|
+
option: isMultipleSelectProps(onChangeProps) ? undefined : optionEvent
|
|
38
|
+
};
|
|
39
|
+
if (isSingleSelectProps(onChangeProps)) {
|
|
40
|
+
onChangeProps.onChange?.(optionEvent, forwardedMeta);
|
|
41
|
+
}
|
|
42
|
+
if (isMultipleSelectProps(onChangeProps)) {
|
|
43
|
+
setMultiValues(optionEvent);
|
|
44
|
+
onChangeProps.onChange?.(optionEvent, forwardedMeta);
|
|
45
|
+
}
|
|
46
|
+
}, [onChange, multiple, onCreateOption]);
|
|
47
|
+
const keyPressHandler = e => {
|
|
48
|
+
if (multiple && e.key === 'Enter' && currentFocusedValue && multiValues) {
|
|
49
|
+
const newMultiValues = removeValueFromSelectedOptions(multiValues, currentFocusedValue);
|
|
50
|
+
if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
|
|
51
|
+
}
|
|
52
|
+
if (removeAllButtonRef.current !== null && e.key === 'ArrowRight' && multiValues && currentFocusedValue === multiValues[multiValues.length - 1].value) {
|
|
53
|
+
removeAllButtonRef.current.focus();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
activated,
|
|
58
|
+
multiValues,
|
|
59
|
+
changeHandler,
|
|
60
|
+
keyPressHandler
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SelectOptionBase } from '../../utils';
|
|
2
|
+
import { SelectDropdownGroup, SelectDropdownOptions, SelectDropdownSizes } from '../types';
|
|
3
|
+
interface UseSelectOptionsArgs {
|
|
4
|
+
options?: SelectDropdownOptions | SelectDropdownGroup[];
|
|
5
|
+
id?: string;
|
|
6
|
+
size?: SelectDropdownSizes['size'];
|
|
7
|
+
value?: string | string[];
|
|
8
|
+
}
|
|
9
|
+
interface UseSelectOptionsReturn {
|
|
10
|
+
selectOptions: SelectOptionBase[] | SelectDropdownGroup[];
|
|
11
|
+
parsedValue: SelectOptionBase | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare const useSelectOptions: ({ options, id, size, value, }: UseSelectOptionsArgs) => UseSelectOptionsReturn;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { parseOptions } from '../../utils';
|
|
3
|
+
import { isOptionsGrouped } from '../core/utils';
|
|
4
|
+
export const useSelectOptions = ({
|
|
5
|
+
options,
|
|
6
|
+
id,
|
|
7
|
+
size,
|
|
8
|
+
value
|
|
9
|
+
}) => {
|
|
10
|
+
const selectOptions = useMemo(() => {
|
|
11
|
+
if (!options || Array.isArray(options) && !options.length || typeof options === 'object' && !Array.isArray(options) && Object.keys(options).length === 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
if (isOptionsGrouped(options)) {
|
|
15
|
+
return options;
|
|
16
|
+
}
|
|
17
|
+
return parseOptions({
|
|
18
|
+
options,
|
|
19
|
+
id,
|
|
20
|
+
size
|
|
21
|
+
});
|
|
22
|
+
}, [options, id, size]);
|
|
23
|
+
const parsedValue = useMemo(() => {
|
|
24
|
+
if (isOptionsGrouped(selectOptions)) {
|
|
25
|
+
for (const group of selectOptions) {
|
|
26
|
+
if (group.options) {
|
|
27
|
+
const foundOption = group.options.find(option => option.value === value);
|
|
28
|
+
if (foundOption) return foundOption;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return selectOptions.find(option => option.value === value);
|
|
34
|
+
}, [selectOptions, value]);
|
|
35
|
+
return {
|
|
36
|
+
selectOptions,
|
|
37
|
+
parsedValue
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -14,7 +14,7 @@ export type InternalSelectProps = {
|
|
|
14
14
|
* Ref type for programmatic focus management.
|
|
15
15
|
* Used for managing focus on select input and remove all button.
|
|
16
16
|
*/
|
|
17
|
-
export type
|
|
17
|
+
export type ProgrammaticFocusRef = React.MutableRefObject<HTMLDivElement> | React.MutableRefObject<null>;
|
|
18
18
|
/**
|
|
19
19
|
* Context value for SelectDropdown internal state management.
|
|
20
20
|
* Provides access to focus state and refs for keyboard navigation.
|
|
@@ -25,9 +25,9 @@ export interface SelectDropdownContextValueTypes {
|
|
|
25
25
|
/** Function to update the currently focused value */
|
|
26
26
|
setCurrentFocusedValue?: React.Dispatch<React.SetStateAction<unknown>>;
|
|
27
27
|
/** Ref to the select input for programmatic focus */
|
|
28
|
-
selectInputRef?:
|
|
28
|
+
selectInputRef?: ProgrammaticFocusRef;
|
|
29
29
|
/** Ref to the remove all button for programmatic focus */
|
|
30
|
-
removeAllButtonRef?:
|
|
30
|
+
removeAllButtonRef?: ProgrammaticFocusRef;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Props for sized indicator components (dropdown arrow, search icon, etc.).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StyleProps } from '@codecademy/variance';
|
|
2
|
-
import { conditionalBorderStates } from '../styles';
|
|
2
|
+
import { conditionalBorderStates } from '../core/styles';
|
|
3
3
|
import { InternalInputsProps } from './component-props';
|
|
4
4
|
/**
|
|
5
5
|
* Size variants for the SelectDropdown component.
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codecademy/gamut",
|
|
3
3
|
"description": "Styleguide & Component library for Codecademy",
|
|
4
|
-
"version": "72.2.3-alpha.
|
|
4
|
+
"version": "72.2.3-alpha.f0a032.0",
|
|
5
5
|
"author": "Codecademy Engineering <dev@codecademy.com>",
|
|
6
6
|
"bin": "./bin/gamut.mjs",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@codecademy/gamut-icons": "9.57.10-alpha.
|
|
9
|
-
"@codecademy/gamut-illustrations": "0.58.16-alpha.
|
|
10
|
-
"@codecademy/gamut-patterns": "0.10.35-alpha.
|
|
11
|
-
"@codecademy/gamut-styles": "20.0.3-alpha.
|
|
12
|
-
"@codecademy/variance": "0.26.2-alpha.
|
|
8
|
+
"@codecademy/gamut-icons": "9.57.10-alpha.f0a032.0",
|
|
9
|
+
"@codecademy/gamut-illustrations": "0.58.16-alpha.f0a032.0",
|
|
10
|
+
"@codecademy/gamut-patterns": "0.10.35-alpha.f0a032.0",
|
|
11
|
+
"@codecademy/gamut-styles": "20.0.3-alpha.f0a032.0",
|
|
12
|
+
"@codecademy/variance": "0.26.2-alpha.f0a032.0",
|
|
13
13
|
"@formatjs/intl-locale": "5.3.1",
|
|
14
14
|
"@react-aria/interactions": "3.25.0",
|
|
15
15
|
"@types/marked": "^4.0.8",
|
|
File without changes
|