@codecademy/gamut 72.2.3-alpha.f0a032.0 → 72.2.3-alpha.f2e313.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/agent-tools/skills/gamut-forms/SKILL.md +0 -16
- package/dist/Form/SelectDropdown/SelectDropdown.js +100 -48
- package/dist/Form/SelectDropdown/elements/constants.d.ts +8 -0
- package/dist/Form/SelectDropdown/elements/constants.js +9 -1
- package/dist/Form/SelectDropdown/elements/containers.d.ts +2 -6
- package/dist/Form/SelectDropdown/elements/containers.js +1 -17
- package/dist/Form/SelectDropdown/elements/controls.d.ts +15 -1
- package/dist/Form/SelectDropdown/elements/controls.js +91 -2
- package/dist/Form/SelectDropdown/elements/index.d.ts +1 -2
- package/dist/Form/SelectDropdown/elements/index.js +1 -2
- package/dist/Form/SelectDropdown/elements/options.d.ts +0 -1
- package/dist/Form/SelectDropdown/elements/options.js +2 -5
- package/dist/Form/SelectDropdown/{core/styles.js → styles.js} +12 -30
- package/dist/Form/SelectDropdown/types/component-props.d.ts +6 -54
- package/dist/Form/SelectDropdown/types/internal.d.ts +3 -3
- package/dist/Form/SelectDropdown/types/styles.d.ts +1 -5
- package/dist/Form/SelectDropdown/{core/utils.d.ts → utils.d.ts} +2 -12
- package/dist/Form/SelectDropdown/{core/utils.js → utils.js} +0 -23
- package/dist/Form/styles/index.d.ts +1 -1
- package/dist/Form/styles/index.js +1 -1
- package/package.json +6 -6
- package/agent-tools/skills/gamut-select-dropdown/SKILL.md +0 -236
- package/dist/Form/SelectDropdown/core/accessibility.d.ts +0 -3
- package/dist/Form/SelectDropdown/core/accessibility.js +0 -12
- package/dist/Form/SelectDropdown/core/constants.d.ts +0 -13
- package/dist/Form/SelectDropdown/core/constants.js +0 -14
- package/dist/Form/SelectDropdown/hooks/useSelectHandlers.d.ts +0 -22
- package/dist/Form/SelectDropdown/hooks/useSelectHandlers.js +0 -62
- package/dist/Form/SelectDropdown/hooks/useSelectOptions.d.ts +0 -14
- package/dist/Form/SelectDropdown/hooks/useSelectOptions.js +0 -39
- /package/dist/Form/SelectDropdown/{core/styles.d.ts → styles.d.ts} +0 -0
|
@@ -26,22 +26,6 @@ For typical product forms, prefer `GridForm` (declarative `fields`, `LayoutGrid`
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
-
## SelectDropdown vs Select
|
|
30
|
-
|
|
31
|
-
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:
|
|
32
|
-
|
|
33
|
-
- Styled dropdown menu (react-select appearance)
|
|
34
|
-
- Search / typeahead
|
|
35
|
-
- Multi-select with tags
|
|
36
|
-
- Creatable options
|
|
37
|
-
- Option icons, subtitles, right labels, abbreviations, or grouped options
|
|
38
|
-
|
|
39
|
-
`SelectDropdown` carries a larger JS dependency (react-select); don't reach for it as a default drop-in for `Select`.
|
|
40
|
-
|
|
41
|
-
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.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
29
|
## `FormGroup` (baseline)
|
|
46
30
|
|
|
47
31
|
`packages/gamut/src/Form/elements/FormGroup.tsx`
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { useTheme } from '@emotion/react';
|
|
2
|
-
import { useId, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { getMemoizedStyles } from './
|
|
7
|
-
import {
|
|
8
|
-
import { formatGroupLabel, formatOptionLabel, SelectDropdownContext, TypedReactSelect } from './elements';
|
|
9
|
-
import { useSelectHandlers } from './hooks/useSelectHandlers';
|
|
10
|
-
import { useSelectOptions } from './hooks/useSelectOptions';
|
|
4
|
+
import { parseOptions } from '../utils';
|
|
5
|
+
import { AbbreviatedSingleValue, CustomContainer, CustomInput, CustomValueContainer, DropdownButton, formatGroupLabel, formatOptionLabel, IconOption, MultiValueRemoveButton, MultiValueWithColorMode, onFocus, RemoveAllButton, SelectDropdownContext, TypedReactSelect } from './elements';
|
|
6
|
+
import { getMemoizedStyles } from './styles';
|
|
7
|
+
import { filterValueFromOptions, isMultipleSelectProps, isOptionsGrouped, isSingleSelectProps, removeValueFromSelectedOptions } from './utils';
|
|
11
8
|
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
|
+
|
|
12
26
|
/**
|
|
13
27
|
* A flexible dropdown select component built on top of react-select.
|
|
14
28
|
*
|
|
@@ -59,58 +73,101 @@ export const SelectDropdown = ({
|
|
|
59
73
|
disabled,
|
|
60
74
|
dropdownWidth,
|
|
61
75
|
error,
|
|
62
|
-
formatCreateLabel = inputValue => `Add "${inputValue}"`,
|
|
63
76
|
id,
|
|
64
77
|
inputProps,
|
|
65
78
|
inputWidth,
|
|
66
|
-
|
|
67
|
-
isSearchable: isSearchableProp = false,
|
|
68
|
-
isValidNewOption,
|
|
79
|
+
isSearchable = false,
|
|
69
80
|
menuAlignment = 'left',
|
|
70
81
|
multiple,
|
|
71
82
|
name,
|
|
72
83
|
onChange,
|
|
73
|
-
onCreateOption,
|
|
74
|
-
onInputChange,
|
|
75
84
|
options,
|
|
76
85
|
placeholder = 'Select an option',
|
|
77
86
|
shownOptionsLimit = 6,
|
|
78
87
|
size,
|
|
79
|
-
validationMessage,
|
|
80
88
|
value,
|
|
81
89
|
zIndex,
|
|
82
90
|
...rest
|
|
83
91
|
}) => {
|
|
84
|
-
// isSearchable is forced true when isCreatable is true (CreatableSelect requires a text input)
|
|
85
|
-
const isSearchable = isCreatable || isSearchableProp;
|
|
86
92
|
const rawInputId = useId();
|
|
87
93
|
const inputId = name ?? `${id}-select-dropdown-${rawInputId}`;
|
|
94
|
+
const [activated, setActivated] = useState(false);
|
|
95
|
+
const [currentFocusedValue, setCurrentFocusedValue] = useState(undefined);
|
|
96
|
+
|
|
97
|
+
// these are used to programatically manage the focus state of our multi-select options + 'Remove all' button
|
|
88
98
|
const removeAllButtonRef = useRef(null);
|
|
89
99
|
const selectInputRef = useRef(null);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
100
|
+
const selectOptions = useMemo(() => {
|
|
101
|
+
if (!options || Array.isArray(options) && !options.length || typeof options === 'object' && !Array.isArray(options) && Object.keys(options).length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
if (isOptionsGrouped(options)) {
|
|
105
|
+
return options;
|
|
106
|
+
}
|
|
107
|
+
return parseOptions({
|
|
108
|
+
options,
|
|
109
|
+
id,
|
|
110
|
+
size
|
|
111
|
+
});
|
|
112
|
+
}, [options, id, size]);
|
|
113
|
+
const parsedValue = useMemo(() => {
|
|
114
|
+
if (isOptionsGrouped(selectOptions)) {
|
|
115
|
+
for (const group of selectOptions) {
|
|
116
|
+
if (group.options) {
|
|
117
|
+
const foundOption = group.options.find(option => option.value === value);
|
|
118
|
+
if (foundOption) return foundOption;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
return selectOptions.find(option => option.value === value);
|
|
124
|
+
}, [selectOptions, value]);
|
|
125
|
+
const [multiValues, setMultiValues] = useState(multiple &&
|
|
126
|
+
// To keep this efficient for non-multiSelect
|
|
127
|
+
filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions)));
|
|
128
|
+
|
|
129
|
+
// If the caller changes the initial value, let's update our value to match.
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const newMultiValues = filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions));
|
|
132
|
+
if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
|
|
133
|
+
|
|
134
|
+
//
|
|
135
|
+
// We only update this when our passed in options or value changes, not multiValues.
|
|
136
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
+
}, [options, value]);
|
|
138
|
+
const changeHandler = useCallback(optionEvent => {
|
|
139
|
+
setActivated(true);
|
|
140
|
+
|
|
141
|
+
// 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!
|
|
142
|
+
// Should probably come with: https://codecademy.atlassian.net/browse/GM-354
|
|
143
|
+
const onChangeProps = {
|
|
144
|
+
onChange,
|
|
145
|
+
multiple
|
|
146
|
+
};
|
|
147
|
+
if (isSingleSelectProps(onChangeProps)) {
|
|
148
|
+
const singleOptionEvent = optionEvent;
|
|
149
|
+
onChangeProps.onChange?.(singleOptionEvent, {
|
|
150
|
+
action: onChangeAction,
|
|
151
|
+
option: singleOptionEvent
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (isMultipleSelectProps(onChangeProps)) {
|
|
155
|
+
setMultiValues(optionEvent);
|
|
156
|
+
onChangeProps.onChange?.(optionEvent, {
|
|
157
|
+
action: onChangeAction,
|
|
158
|
+
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)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}, [onChange, multiple]);
|
|
162
|
+
const keyPressHandler = e => {
|
|
163
|
+
if (multiple && e.key === 'Enter' && currentFocusedValue && multiValues) {
|
|
164
|
+
const newMultiValues = removeValueFromSelectedOptions(multiValues, currentFocusedValue);
|
|
165
|
+
if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
|
|
166
|
+
}
|
|
167
|
+
if (removeAllButtonRef.current !== null && e.key === 'ArrowRight' && multiValues && currentFocusedValue === multiValues[multiValues.length - 1].value) {
|
|
168
|
+
removeAllButtonRef.current.focus();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
114
171
|
const theme = useTheme();
|
|
115
172
|
const memoizedStyles = useMemo(() => {
|
|
116
173
|
return getMemoizedStyles(theme, zIndex);
|
|
@@ -123,15 +180,14 @@ export const SelectDropdown = ({
|
|
|
123
180
|
selectInputRef
|
|
124
181
|
},
|
|
125
182
|
children: /*#__PURE__*/_jsx(TypedReactSelect, {
|
|
183
|
+
...defaultProps,
|
|
126
184
|
activated: activated,
|
|
127
185
|
"aria-live": "assertive",
|
|
128
186
|
ariaLiveMessages: {
|
|
129
187
|
onFocus
|
|
130
188
|
},
|
|
131
|
-
components: defaultComponents,
|
|
132
189
|
dropdownWidth: dropdownWidth,
|
|
133
190
|
error: Boolean(error),
|
|
134
|
-
formatCreateLabel: formatCreateLabel,
|
|
135
191
|
formatGroupLabel: formatGroupLabel,
|
|
136
192
|
formatOptionLabel: formatOptionLabel,
|
|
137
193
|
id: id || rest.htmlFor || rawInputId,
|
|
@@ -140,15 +196,12 @@ export const SelectDropdown = ({
|
|
|
140
196
|
...inputProps
|
|
141
197
|
},
|
|
142
198
|
inputWidth: inputWidth,
|
|
143
|
-
isCreatable: isCreatable,
|
|
144
199
|
isDisabled: disabled,
|
|
145
200
|
isMulti: multiple,
|
|
146
201
|
isOptionDisabled: option => option.disabled,
|
|
147
202
|
isSearchable: isSearchable,
|
|
148
|
-
isValidNewOption: isValidNewOption,
|
|
149
203
|
menuAlignment: menuAlignment,
|
|
150
204
|
name: name,
|
|
151
|
-
noOptionsMessage: resolveNoOptionsMessage(validationMessage),
|
|
152
205
|
options: selectOptions,
|
|
153
206
|
placeholder: placeholder,
|
|
154
207
|
selectRef: selectInputRef,
|
|
@@ -157,8 +210,7 @@ export const SelectDropdown = ({
|
|
|
157
210
|
styles: memoizedStyles,
|
|
158
211
|
value: multiple ? multiValues : parsedValue,
|
|
159
212
|
onChange: changeHandler,
|
|
160
|
-
|
|
161
|
-
onKeyDown: multiple ? keyPressHandler : undefined,
|
|
213
|
+
onKeyDown: multiple ? e => keyPressHandler(e) : undefined,
|
|
162
214
|
...rest
|
|
163
215
|
})
|
|
164
216
|
});
|
|
@@ -15,6 +15,14 @@ export declare const indicatorIcons: {
|
|
|
15
15
|
size: number;
|
|
16
16
|
icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
17
17
|
};
|
|
18
|
+
smallSearchable: {
|
|
19
|
+
size: number;
|
|
20
|
+
icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
21
|
+
};
|
|
22
|
+
mediumSearchable: {
|
|
23
|
+
size: number;
|
|
24
|
+
icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
25
|
+
};
|
|
18
26
|
smallRemove: {
|
|
19
27
|
size: number;
|
|
20
28
|
icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArrowChevronDownIcon, CloseIcon, MiniChevronDownIcon, MiniDeleteIcon } from '@codecademy/gamut-icons';
|
|
1
|
+
import { ArrowChevronDownIcon, CloseIcon, MiniChevronDownIcon, MiniDeleteIcon, SearchIcon } from '@codecademy/gamut-icons';
|
|
2
2
|
export const iconSize = {
|
|
3
3
|
small: 12,
|
|
4
4
|
medium: 16
|
|
@@ -16,6 +16,14 @@ export const indicatorIcons = {
|
|
|
16
16
|
size: iconSize.medium,
|
|
17
17
|
icon: ArrowChevronDownIcon
|
|
18
18
|
},
|
|
19
|
+
smallSearchable: {
|
|
20
|
+
size: iconSize.small,
|
|
21
|
+
icon: SearchIcon
|
|
22
|
+
},
|
|
23
|
+
mediumSearchable: {
|
|
24
|
+
size: iconSize.medium,
|
|
25
|
+
icon: SearchIcon
|
|
26
|
+
},
|
|
19
27
|
smallRemove: {
|
|
20
28
|
size: iconSize.small,
|
|
21
29
|
icon: MiniDeleteIcon
|
|
@@ -24,10 +24,6 @@ export declare const CustomValueContainer: ({ ...rest }: CustomSelectComponentPr
|
|
|
24
24
|
export declare const CustomInput: ({ ...rest }: CustomSelectComponentProps<typeof SelectDropdownElements.Input>) => import("react/jsx-runtime").JSX.Element;
|
|
25
25
|
/**
|
|
26
26
|
* Typed wrapper around react-select component.
|
|
27
|
-
*
|
|
28
|
-
* Creatable-only props (formatCreateLabel, isValidNewOption) are stripped from
|
|
29
|
-
* the non-creatable path so they don't reach ReactSelect. `onCreateOption` is
|
|
30
|
-
* handled in SelectDropdown's changeHandler — do not pass it to CreatableSelect
|
|
31
|
-
* or react-select will skip onChange on create.
|
|
27
|
+
* Provides type safety for the underlying react-select implementation.
|
|
32
28
|
*/
|
|
33
|
-
export declare function TypedReactSelect<OptionType, IsMulti extends boolean = false, GroupType extends GroupBase<OptionType> = GroupBase<OptionType>>({ selectRef,
|
|
29
|
+
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;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createContext, useLayoutEffect } from 'react';
|
|
2
2
|
import ReactSelect, { components as SelectDropdownElements } from 'react-select';
|
|
3
|
-
import CreatableSelect from 'react-select/creatable';
|
|
4
3
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
5
4
|
/**
|
|
6
5
|
* React context for sharing state between SelectDropdown components.
|
|
@@ -117,27 +116,12 @@ export const CustomInput = ({
|
|
|
117
116
|
|
|
118
117
|
/**
|
|
119
118
|
* Typed wrapper around react-select component.
|
|
120
|
-
*
|
|
121
|
-
* Creatable-only props (formatCreateLabel, isValidNewOption) are stripped from
|
|
122
|
-
* the non-creatable path so they don't reach ReactSelect. `onCreateOption` is
|
|
123
|
-
* handled in SelectDropdown's changeHandler — do not pass it to CreatableSelect
|
|
124
|
-
* or react-select will skip onChange on create.
|
|
119
|
+
* Provides type safety for the underlying react-select implementation.
|
|
125
120
|
*/
|
|
126
121
|
export function TypedReactSelect({
|
|
127
122
|
selectRef,
|
|
128
|
-
isCreatable,
|
|
129
|
-
formatCreateLabel,
|
|
130
|
-
isValidNewOption,
|
|
131
123
|
...props
|
|
132
124
|
}) {
|
|
133
|
-
if (isCreatable) {
|
|
134
|
-
return /*#__PURE__*/_jsx(CreatableSelect, {
|
|
135
|
-
...props,
|
|
136
|
-
formatCreateLabel: formatCreateLabel,
|
|
137
|
-
isValidNewOption: isValidNewOption,
|
|
138
|
-
ref: selectRef
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
125
|
return /*#__PURE__*/_jsx(ReactSelect, {
|
|
142
126
|
...props,
|
|
143
127
|
ref: selectRef
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AriaOnFocus } from 'react-select';
|
|
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>;
|
|
2
11
|
/**
|
|
3
12
|
* Custom dropdown indicator that shows either a chevron or search icon.
|
|
4
13
|
* The icon type depends on whether the select is searchable or not.
|
|
5
14
|
*/
|
|
6
15
|
export declare const DropdownButton: (props: SizedIndicatorProps) => import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
/**
|
|
17
|
+
* Custom remove all button for multi-select mode.
|
|
18
|
+
* Provides keyboard navigation and accessible removal of all selected values.
|
|
19
|
+
*/
|
|
20
|
+
export declare const RemoveAllButton: (props: SizedIndicatorProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,23 +1,50 @@
|
|
|
1
|
+
import _styled from "@emotion/styled/base";
|
|
2
|
+
import { css, theme } from '@codecademy/gamut-styles';
|
|
3
|
+
import { useContext } from 'react';
|
|
1
4
|
import { components as SelectDropdownElements } from 'react-select';
|
|
2
5
|
import { indicatorIcons } from './constants';
|
|
6
|
+
import { SelectDropdownContext } from './containers';
|
|
3
7
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
4
8
|
const {
|
|
5
9
|
DropdownIndicator
|
|
6
10
|
} = SelectDropdownElements;
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Generates accessible focus messages for screen readers.
|
|
14
|
+
* Provides detailed information about the currently focused option.
|
|
15
|
+
*
|
|
16
|
+
* @param params - Object containing the focused option details
|
|
17
|
+
* @returns Formatted accessibility message
|
|
18
|
+
*/
|
|
19
|
+
export const onFocus = ({
|
|
20
|
+
focused: {
|
|
21
|
+
label,
|
|
22
|
+
subtitle,
|
|
23
|
+
rightLabel,
|
|
24
|
+
disabled
|
|
25
|
+
}
|
|
26
|
+
}) => {
|
|
27
|
+
const formattedSubtitle = `, ${subtitle}`;
|
|
28
|
+
const formattedRightLabel = `, ${rightLabel}`;
|
|
29
|
+
const msg = `You are currently focused on option ${label}${subtitle ? formattedSubtitle : ''} ${rightLabel ? formattedRightLabel : ''}${disabled ? ', disabled' : ''}`;
|
|
30
|
+
return msg;
|
|
31
|
+
};
|
|
32
|
+
|
|
8
33
|
/**
|
|
9
34
|
* Custom dropdown indicator that shows either a chevron or search icon.
|
|
10
35
|
* The icon type depends on whether the select is searchable or not.
|
|
11
36
|
*/
|
|
12
37
|
export const DropdownButton = props => {
|
|
13
38
|
const {
|
|
14
|
-
size
|
|
39
|
+
size,
|
|
40
|
+
isSearchable
|
|
15
41
|
} = props.selectProps;
|
|
16
42
|
const color = props.isDisabled ? 'text-disabled' : 'text';
|
|
17
43
|
const iconSize = size ?? 'medium';
|
|
44
|
+
const iconType = isSearchable ? 'Searchable' : 'Chevron';
|
|
18
45
|
const {
|
|
19
46
|
...iconProps
|
|
20
|
-
} = indicatorIcons[`${iconSize}
|
|
47
|
+
} = indicatorIcons[`${iconSize}${iconType}`];
|
|
21
48
|
const {
|
|
22
49
|
icon: IndicatorIcon
|
|
23
50
|
} = iconProps;
|
|
@@ -28,4 +55,66 @@ export const DropdownButton = props => {
|
|
|
28
55
|
color: color
|
|
29
56
|
})
|
|
30
57
|
});
|
|
58
|
+
};
|
|
59
|
+
const CustomStyledRemoveAllDiv = /*#__PURE__*/_styled('div', {
|
|
60
|
+
target: "e1xkmr70",
|
|
61
|
+
label: "CustomStyledRemoveAllDiv"
|
|
62
|
+
})(css({
|
|
63
|
+
'&:focus': {
|
|
64
|
+
outline: `2px solid ${theme.colors.primary}`
|
|
65
|
+
},
|
|
66
|
+
'&:focus-visible': {
|
|
67
|
+
outline: `2px solid ${theme.colors.primary}`
|
|
68
|
+
}
|
|
69
|
+
}), 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== */");
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Custom remove all button for multi-select mode.
|
|
73
|
+
* Provides keyboard navigation and accessible removal of all selected values.
|
|
74
|
+
*/
|
|
75
|
+
export const RemoveAllButton = props => {
|
|
76
|
+
const {
|
|
77
|
+
getStyles,
|
|
78
|
+
innerProps: {
|
|
79
|
+
...restInnerProps
|
|
80
|
+
},
|
|
81
|
+
selectProps: {
|
|
82
|
+
size
|
|
83
|
+
}
|
|
84
|
+
} = props;
|
|
85
|
+
const {
|
|
86
|
+
removeAllButtonRef,
|
|
87
|
+
selectInputRef
|
|
88
|
+
} = useContext(SelectDropdownContext);
|
|
89
|
+
const iconSize = size ?? 'medium';
|
|
90
|
+
const {
|
|
91
|
+
...iconProps
|
|
92
|
+
} = indicatorIcons[`${iconSize}Remove`];
|
|
93
|
+
const {
|
|
94
|
+
icon: IndicatorIcon
|
|
95
|
+
} = iconProps;
|
|
96
|
+
const onKeyPress = e => {
|
|
97
|
+
if (e.key === 'Enter' && restInnerProps.onMouseDown) {
|
|
98
|
+
restInnerProps.onMouseDown(e);
|
|
99
|
+
}
|
|
100
|
+
if (selectInputRef?.current && (e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'ArrowDown')) {
|
|
101
|
+
selectInputRef?.current.focus();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const style = getStyles('clearIndicator', props);
|
|
105
|
+
return /*#__PURE__*/_jsx(CustomStyledRemoveAllDiv, {
|
|
106
|
+
"aria-label": "Remove all selected",
|
|
107
|
+
role: "button",
|
|
108
|
+
tabIndex: 0,
|
|
109
|
+
...restInnerProps,
|
|
110
|
+
ref: removeAllButtonRef
|
|
111
|
+
// eslint-disable-next-line gamut/no-inline-style
|
|
112
|
+
,
|
|
113
|
+
style: style,
|
|
114
|
+
onKeyDown: onKeyPress,
|
|
115
|
+
children: /*#__PURE__*/_jsx(IndicatorIcon, {
|
|
116
|
+
...iconProps,
|
|
117
|
+
color: "text"
|
|
118
|
+
})
|
|
119
|
+
});
|
|
31
120
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { iconSize, selectedIconSize, indicatorIcons } from './constants';
|
|
2
2
|
export { MultiValueWithColorMode, MultiValueRemoveButton, RemoveAllButton, } from './multi-value';
|
|
3
|
-
export { DropdownButton } from './controls';
|
|
4
|
-
export { onFocus } from '../core/accessibility';
|
|
3
|
+
export { DropdownButton, onFocus } from './controls';
|
|
5
4
|
export { SelectDropdownContext, CustomContainer, CustomInput, CustomValueContainer, TypedReactSelect, } from './containers';
|
|
6
5
|
export { IconOption, AbbreviatedSingleValue, formatOptionLabel, formatGroupLabel, } from './options';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { iconSize, selectedIconSize, indicatorIcons } from './constants';
|
|
2
2
|
export { MultiValueWithColorMode, MultiValueRemoveButton, RemoveAllButton } from './multi-value';
|
|
3
|
-
export { DropdownButton } from './controls';
|
|
4
|
-
export { onFocus } from '../core/accessibility';
|
|
3
|
+
export { DropdownButton, onFocus } from './controls';
|
|
5
4
|
export { SelectDropdownContext, CustomContainer, CustomInput, CustomValueContainer, TypedReactSelect } from './containers';
|
|
6
5
|
export { IconOption, AbbreviatedSingleValue, formatOptionLabel, formatGroupLabel } from './options';
|
|
@@ -3,7 +3,6 @@ import { CustomSelectComponentProps, ExtendedOption, SelectDropdownGroup } from
|
|
|
3
3
|
/**
|
|
4
4
|
* Custom option component that displays a check icon for selected items.
|
|
5
5
|
* Also manages ARIA attributes for accessibility.
|
|
6
|
-
* Skips the check icon for react-select/creatable's "Add" row (__isNew__).
|
|
7
6
|
*/
|
|
8
7
|
export declare const IconOption: ({ children, ...rest }: CustomSelectComponentProps<typeof SelectDropdownElements.Option>) => import("react/jsx-runtime").JSX.Element;
|
|
9
8
|
/**
|
|
@@ -44,7 +44,6 @@ const IconOptionLabel = ({
|
|
|
44
44
|
/**
|
|
45
45
|
* Custom option component that displays a check icon for selected items.
|
|
46
46
|
* Also manages ARIA attributes for accessibility.
|
|
47
|
-
* Skips the check icon for react-select/creatable's "Add" row (__isNew__).
|
|
48
47
|
*/
|
|
49
48
|
export const IconOption = ({
|
|
50
49
|
children,
|
|
@@ -55,17 +54,15 @@ export const IconOption = ({
|
|
|
55
54
|
} = rest.selectProps;
|
|
56
55
|
const {
|
|
57
56
|
isFocused,
|
|
58
|
-
innerProps
|
|
59
|
-
data
|
|
57
|
+
innerProps
|
|
60
58
|
} = rest;
|
|
61
|
-
const isNew = data?.__isNew__;
|
|
62
59
|
return /*#__PURE__*/_jsxs(SelectDropdownElements.Option, {
|
|
63
60
|
...rest,
|
|
64
61
|
innerProps: {
|
|
65
62
|
...innerProps,
|
|
66
63
|
'aria-selected': isFocused
|
|
67
64
|
},
|
|
68
|
-
children: [children,
|
|
65
|
+
children: [children, rest?.isSelected && /*#__PURE__*/_jsx(CheckIcon, {
|
|
69
66
|
size: selectedIconSize[size ?? 'medium']
|
|
70
67
|
})]
|
|
71
68
|
});
|
|
@@ -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',
|
|
@@ -62,7 +62,7 @@ const textColor = css({
|
|
|
62
62
|
color: 'text'
|
|
63
63
|
});
|
|
64
64
|
const placeholderColor = css({
|
|
65
|
-
color: 'text-
|
|
65
|
+
color: 'text-disabled'
|
|
66
66
|
});
|
|
67
67
|
export const getMemoizedStyles = (theme, zIndex) => {
|
|
68
68
|
return {
|
|
@@ -137,8 +137,6 @@ export const getMemoizedStyles = (theme, zIndex) => {
|
|
|
137
137
|
error: state.selectProps.error,
|
|
138
138
|
theme
|
|
139
139
|
}),
|
|
140
|
-
// Drop react-select's default menu drop shadow; the border above defines the edge.
|
|
141
|
-
boxShadow: 'none',
|
|
142
140
|
...(dropdownWidth ? {
|
|
143
141
|
minWidth: dropdownWidth,
|
|
144
142
|
width: dropdownWidth
|
|
@@ -196,32 +194,16 @@ export const getMemoizedStyles = (theme, zIndex) => {
|
|
|
196
194
|
backgroundColor: theme.colors['secondary-hover']
|
|
197
195
|
}
|
|
198
196
|
}),
|
|
199
|
-
|
|
200
|
-
...
|
|
201
|
-
|
|
197
|
+
option: (provided, state) => ({
|
|
198
|
+
...getOptionBackground(state.isSelected, state.isFocused)({
|
|
199
|
+
theme
|
|
200
|
+
}),
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
color: state.isDisabled ? 'text-disabled' : 'default',
|
|
203
|
+
cursor: state.isDisabled ? 'not-allowed' : 'pointer',
|
|
204
|
+
display: 'flex',
|
|
205
|
+
padding: state.selectProps.size === 'small' ? '3px 14px' : '11px 14px'
|
|
202
206
|
}),
|
|
203
|
-
option: (provided, state) => {
|
|
204
|
-
const isNew = state.data?.__isNew__;
|
|
205
|
-
const isSmall = state.selectProps.size === 'small';
|
|
206
|
-
return {
|
|
207
|
-
...getOptionBackground(state.isSelected, state.isFocused)({
|
|
208
|
-
theme
|
|
209
|
-
}),
|
|
210
|
-
alignItems: 'center',
|
|
211
|
-
color: state.isDisabled ? theme.colors['text-disabled'] : isNew ? theme.colors.primary : theme.colors.text,
|
|
212
|
-
cursor: state.isDisabled ? 'not-allowed' : 'pointer',
|
|
213
|
-
display: 'flex',
|
|
214
|
-
padding: isSmall ? '3px 14px' : '11px 14px',
|
|
215
|
-
...(isNew && {
|
|
216
|
-
// Gradient creates the 1px divider line at the top edge of the option background
|
|
217
|
-
backgroundImage: `linear-gradient(${theme.colors['text-disabled']} 1px, transparent 1px)`,
|
|
218
|
-
backgroundPosition: '0 0',
|
|
219
|
-
backgroundRepeat: 'no-repeat',
|
|
220
|
-
backgroundSize: '100% 1px',
|
|
221
|
-
paddingTop: isSmall ? '11px' : '19px'
|
|
222
|
-
})
|
|
223
|
-
};
|
|
224
|
-
},
|
|
225
207
|
placeholder: provided => ({
|
|
226
208
|
...provided,
|
|
227
209
|
...placeholderColor({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Ref, SelectHTMLAttributes } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Props as NamedProps } from 'react-select';
|
|
3
3
|
import { SelectComponentProps } from '../../inputs/Select';
|
|
4
4
|
import { OptionStrict, SelectDropdownGroup, SelectDropdownOptions } from './options';
|
|
5
5
|
import { ReactSelectAdditionalProps, SelectDropdownSizes, SharedProps } from './styles';
|
|
@@ -28,7 +28,7 @@ export type SelectDropdownBaseProps = Omit<SelectComponentProps, 'onChange' | 'd
|
|
|
28
28
|
* Core props interface that defines the essential properties for SelectDropdown.
|
|
29
29
|
* This interface combines base props with react-select props and HTML select attributes.
|
|
30
30
|
*/
|
|
31
|
-
export interface SelectDropdownCoreProps extends SelectDropdownBaseProps, Omit<NamedProps<OptionStrict, boolean>, 'formatOptionLabel' | 'isDisabled' | 'value' | 'options' | 'components' | 'styles' | 'theme' | 'onChange' | 'multiple'
|
|
31
|
+
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 {
|
|
32
32
|
/** Required name attribute for the select input */
|
|
33
33
|
name: string;
|
|
34
34
|
/** Placeholder text shown when no option is selected.
|
|
@@ -38,39 +38,6 @@ export interface SelectDropdownCoreProps extends SelectDropdownBaseProps, Omit<N
|
|
|
38
38
|
placeholder?: string;
|
|
39
39
|
/** Array of options or option groups to display in the dropdown */
|
|
40
40
|
options?: SelectDropdownOptions | SelectDropdownGroup[];
|
|
41
|
-
/**
|
|
42
|
-
* Allows users to create new options by typing a value not in the options list.
|
|
43
|
-
* When true, isSearchable is automatically set to true.
|
|
44
|
-
* Pair with onCreateOption to persist new options.
|
|
45
|
-
*/
|
|
46
|
-
isCreatable?: boolean;
|
|
47
|
-
/**
|
|
48
|
-
* Called when the user confirms a new option via the "Add" row.
|
|
49
|
-
* Convenience callback for persisting the new value to your `options` list.
|
|
50
|
-
* Selection updates are delivered through `onChange` with `action: 'create-option'`.
|
|
51
|
-
*/
|
|
52
|
-
onCreateOption?: (inputValue: string) => void;
|
|
53
|
-
/**
|
|
54
|
-
* Customises the label shown in the "Add" row.
|
|
55
|
-
* Defaults to: (inputValue) => `Add "${inputValue}"`.
|
|
56
|
-
*/
|
|
57
|
-
formatCreateLabel?: (inputValue: string) => React.ReactNode;
|
|
58
|
-
/**
|
|
59
|
-
* Controls when the "Add" row is visible.
|
|
60
|
-
* Receives the current input, selected values, and all options.
|
|
61
|
-
* Defaults to react-select's built-in logic (hidden when input matches an existing option label).
|
|
62
|
-
* Use cases: minimum-length gating, pattern validation, case-insensitive dedup, max-items cap.
|
|
63
|
-
*/
|
|
64
|
-
isValidNewOption?: (inputValue: string, value: OptionsType<OptionStrict>, options: OptionsType<OptionStrict>) => boolean;
|
|
65
|
-
/**
|
|
66
|
-
* Customizes the message shown inside the dropdown menu when no option matches
|
|
67
|
-
* the current input (react-select's "No options" state). Useful for surfacing
|
|
68
|
-
* validation/error text directly in the dropdown. Accepts a node, or a function
|
|
69
|
-
* receiving the current input value.
|
|
70
|
-
*/
|
|
71
|
-
validationMessage?: React.ReactNode | ((obj: {
|
|
72
|
-
inputValue: string;
|
|
73
|
-
}) => React.ReactNode);
|
|
74
41
|
}
|
|
75
42
|
/**
|
|
76
43
|
* Props for single-select mode.
|
|
@@ -92,23 +59,11 @@ export interface MultiSelectDropdownProps extends SelectDropdownCoreProps {
|
|
|
92
59
|
/** Callback fired when the selected values change */
|
|
93
60
|
onChange?: NamedProps<OptionStrict, true>['onChange'];
|
|
94
61
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Enforces that isSearchable cannot be false when isCreatable is true.
|
|
97
|
-
* Creatable mode requires the search input so users can type new option values.
|
|
98
|
-
*/
|
|
99
|
-
type CreatableConstraint = {
|
|
100
|
-
isCreatable?: false | undefined;
|
|
101
|
-
isSearchable?: boolean;
|
|
102
|
-
} | {
|
|
103
|
-
isCreatable: true;
|
|
104
|
-
isSearchable?: true;
|
|
105
|
-
};
|
|
106
62
|
/**
|
|
107
63
|
* Union type for all SelectDropdown prop variants.
|
|
108
|
-
* Supports both single and multi-select modes through discriminated union
|
|
109
|
-
* intersected with CreatableConstraint to enforce isSearchable compatibility.
|
|
64
|
+
* Supports both single and multi-select modes through discriminated union.
|
|
110
65
|
*/
|
|
111
|
-
export type SelectDropdownProps =
|
|
66
|
+
export type SelectDropdownProps = SingleSelectDropdownProps | MultiSelectDropdownProps;
|
|
112
67
|
/**
|
|
113
68
|
* Base interface for onChange-related props.
|
|
114
69
|
* Used internally for type checking and prop validation.
|
|
@@ -121,12 +76,9 @@ export interface BaseOnChangeProps {
|
|
|
121
76
|
}
|
|
122
77
|
/**
|
|
123
78
|
* Props for the typed React Select component wrapper.
|
|
124
|
-
* Extends ReactSelectAdditionalProps with an optional ref
|
|
79
|
+
* Extends ReactSelectAdditionalProps with an optional ref.
|
|
125
80
|
*/
|
|
126
|
-
export interface TypedReactSelectProps extends ReactSelectAdditionalProps
|
|
81
|
+
export interface TypedReactSelectProps extends ReactSelectAdditionalProps {
|
|
127
82
|
/** Optional ref to the underlying react-select component */
|
|
128
83
|
selectRef?: Ref<any>;
|
|
129
|
-
/** When true, renders CreatableSelect instead of ReactSelect */
|
|
130
|
-
isCreatable?: boolean;
|
|
131
84
|
}
|
|
132
|
-
export {};
|
|
@@ -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 ProgramaticFocusRef = 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?: ProgramaticFocusRef;
|
|
29
29
|
/** Ref to the remove all button for programmatic focus */
|
|
30
|
-
removeAllButtonRef?:
|
|
30
|
+
removeAllButtonRef?: ProgramaticFocusRef;
|
|
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 '../
|
|
2
|
+
import { conditionalBorderStates } from '../styles';
|
|
3
3
|
import { InternalInputsProps } from './component-props';
|
|
4
4
|
/**
|
|
5
5
|
* Size variants for the SelectDropdown component.
|
|
@@ -69,9 +69,5 @@ export type ControlState = BaseSelectComponentProps & InteractionStates & {
|
|
|
69
69
|
export type OptionState = BaseSelectComponentProps & InteractionStates & {
|
|
70
70
|
/** Whether the option is selected */
|
|
71
71
|
isSelected: boolean;
|
|
72
|
-
/** Option data — includes __isNew__ for react-select/creatable's "Add" row */
|
|
73
|
-
data?: {
|
|
74
|
-
__isNew__?: boolean;
|
|
75
|
-
};
|
|
76
72
|
};
|
|
77
73
|
export {};
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { SelectOptionBase } from '../../utils';
|
|
4
|
-
import { BaseOnChangeProps, ExtendedOption, MultiSelectDropdownProps, OptionStrict, SelectDropdownGroup, SelectDropdownOptions, SelectDropdownProps, SingleSelectDropdownProps } from '../types';
|
|
1
|
+
import { SelectOptionBase } from '../utils';
|
|
2
|
+
import { BaseOnChangeProps, ExtendedOption, MultiSelectDropdownProps, SelectDropdownGroup, SelectDropdownOptions, SelectDropdownProps, SingleSelectDropdownProps } from './types';
|
|
5
3
|
export declare const isMultipleSelectProps: (props: BaseOnChangeProps) => props is MultiSelectDropdownProps;
|
|
6
4
|
export declare const isSingleSelectProps: (props: BaseOnChangeProps) => props is SingleSelectDropdownProps;
|
|
7
|
-
/**
|
|
8
|
-
* Resolves the value for a newly created option from react-select action metadata
|
|
9
|
-
* or the onChange option payload. Returns undefined when no reliable value exists.
|
|
10
|
-
*/
|
|
11
|
-
export declare const getCreatedOptionValue: (optionEvent: OptionStrict | OptionsType<OptionStrict>, actionMeta: ActionMeta<OptionStrict>, multiple?: boolean) => string | undefined;
|
|
12
5
|
export declare const isOptionGroup: (obj: unknown) => obj is SelectDropdownGroup;
|
|
13
6
|
export declare const isOptionsGrouped: (options: SelectDropdownOptions) => options is SelectDropdownGroup[];
|
|
14
7
|
/**
|
|
@@ -30,7 +23,4 @@ export declare const filterValueFromOptions: (options: SelectOptionBase[] | Sele
|
|
|
30
23
|
* @param value - The value or values to remove
|
|
31
24
|
* @returns New array with the specified values removed
|
|
32
25
|
*/
|
|
33
|
-
export declare const resolveNoOptionsMessage: (validationMessage: SelectDropdownProps["validationMessage"]) => ((obj: {
|
|
34
|
-
inputValue: string;
|
|
35
|
-
}) => React.ReactNode) | undefined;
|
|
36
26
|
export declare const removeValueFromSelectedOptions: (selectedOptions: ExtendedOption[] | SelectOptionBase[], value: SelectDropdownProps["value"]) => SelectOptionBase[];
|
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
export const isMultipleSelectProps = props => !!props.multiple;
|
|
2
2
|
export const isSingleSelectProps = props => !props.multiple;
|
|
3
|
-
/**
|
|
4
|
-
* Resolves the value for a newly created option from react-select action metadata
|
|
5
|
-
* or the onChange option payload. Returns undefined when no reliable value exists.
|
|
6
|
-
*/
|
|
7
|
-
export const getCreatedOptionValue = (optionEvent, actionMeta, multiple) => {
|
|
8
|
-
const metaValue = actionMeta.option?.value;
|
|
9
|
-
if (metaValue) return metaValue;
|
|
10
|
-
if (!multiple) {
|
|
11
|
-
const {
|
|
12
|
-
value
|
|
13
|
-
} = optionEvent;
|
|
14
|
-
return value || undefined;
|
|
15
|
-
}
|
|
16
|
-
const newOption = optionEvent.find(option => option.__isNew__);
|
|
17
|
-
return newOption?.value || undefined;
|
|
18
|
-
};
|
|
19
3
|
export const isOptionGroup = obj => obj != null && typeof obj === 'object' && 'options' in obj && obj.options !== undefined;
|
|
20
4
|
export const isOptionsGrouped = options => Array.isArray(options) && options.some(option => isOptionGroup(option));
|
|
21
5
|
|
|
@@ -44,13 +28,6 @@ export const filterValueFromOptions = (options, value, optionsAreGrouped) => {
|
|
|
44
28
|
* @param value - The value or values to remove
|
|
45
29
|
* @returns New array with the specified values removed
|
|
46
30
|
*/
|
|
47
|
-
export const resolveNoOptionsMessage = validationMessage => {
|
|
48
|
-
if (validationMessage === undefined) return undefined;
|
|
49
|
-
if (typeof validationMessage === 'function') {
|
|
50
|
-
return validationMessage;
|
|
51
|
-
}
|
|
52
|
-
return () => validationMessage;
|
|
53
|
-
};
|
|
54
31
|
export const removeValueFromSelectedOptions = (selectedOptions, value) => {
|
|
55
32
|
return selectedOptions.filter(option => {
|
|
56
33
|
if (Array.isArray(value)) {
|
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.f2e313.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.f2e313.0",
|
|
9
|
+
"@codecademy/gamut-illustrations": "0.58.16-alpha.f2e313.0",
|
|
10
|
+
"@codecademy/gamut-patterns": "0.10.35-alpha.f2e313.0",
|
|
11
|
+
"@codecademy/gamut-styles": "20.0.3-alpha.f2e313.0",
|
|
12
|
+
"@codecademy/variance": "0.26.2-alpha.f2e313.0",
|
|
13
13
|
"@formatjs/intl-locale": "5.3.1",
|
|
14
14
|
"@react-aria/interactions": "3.25.0",
|
|
15
15
|
"@types/marked": "^4.0.8",
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: gamut-select-dropdown
|
|
3
|
-
description: Use when implementing or auditing SelectDropdown — single/multi modes, controlled vs uncontrolled value, creatable options, FormGroup wiring, and onChange contract. Pair with gamut-forms for error live regions, ConnectedForm, and field-level validation.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Gamut SelectDropdown
|
|
7
|
-
|
|
8
|
-
Styled dropdown built on react-select.
|
|
9
|
-
|
|
10
|
-
Source: `@codecademy/gamut` — [SelectDropdown.tsx](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Form/SelectDropdown/SelectDropdown.tsx)
|
|
11
|
-
|
|
12
|
-
See also: [`gamut-forms`](../gamut-forms/SKILL.md) — FormGroup wiring, error regions, and validation UX.
|
|
13
|
-
|
|
14
|
-
Storybook: [Atoms / FormInputs / SelectDropdown](https://gamut.codecademy.com/?path=/docs-atoms-forminputs-selectdropdown--docs)
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## When to use SelectDropdown vs Select
|
|
19
|
-
|
|
20
|
-
Use `Select` for standard single-select forms with minimal bundle cost. Use `SelectDropdown` when designs specify the styled dropdown menu, search, multi-select tags, creatable options, icons, groups, or abbreviations. SelectDropdown has a larger JavaScript dependency (react-select).
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## Options
|
|
25
|
-
|
|
26
|
-
`options` accepts plain strings or option objects. `value` is always a string and references an option's `value`.
|
|
27
|
-
|
|
28
|
-
| Field | Required | Notes |
|
|
29
|
-
| -------------- | -------- | -------------------------------------------------------------------- |
|
|
30
|
-
| `label` | yes | Display text |
|
|
31
|
-
| `value` | yes | Unique string; what `value` / `string[]` reference |
|
|
32
|
-
| `disabled` | no | Option cannot be selected |
|
|
33
|
-
| `subtitle` | no | Secondary text below the label |
|
|
34
|
-
| `rightLabel` | no | Text on the right side of the option |
|
|
35
|
-
| `icon` | no | A `@codecademy/gamut-icons` component |
|
|
36
|
-
| `abbreviation` | no | Short text shown in the input while the full label shows in the menu |
|
|
37
|
-
|
|
38
|
-
Grouped options: `{ label, options: [...], divider? }` (extends react-select `GroupBase`; `divider` draws a rule above the group).
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Controlled vs uncontrolled
|
|
43
|
-
|
|
44
|
-
SelectDropdown does **not** accept `defaultValue`.
|
|
45
|
-
|
|
46
|
-
| Mode | Uncontrolled | Controlled |
|
|
47
|
-
| ---------------- | -------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
48
|
-
| Single | Not supported | `value` (string) + update in `onChange` |
|
|
49
|
-
| Multi | Omit `value` or pass non-array (`undefined`, `''`) | `value: string[]` + update in `onChange` |
|
|
50
|
-
| Creatable single | Not supported | Same as single; `onCreateOption` appends to `options` |
|
|
51
|
-
| Creatable multi | Omit `value`; `onCreateOption` for options | `value: string[]`; update in `onChange` on every change including `create-option` |
|
|
52
|
-
|
|
53
|
-
Single-select selection is derived from the `value` prop only — internal state is not kept. Multi-select without `value: string[]` keeps selection in internal `multiValues`.
|
|
54
|
-
|
|
55
|
-
**Controlled creatable multi pitfall:** Updating `options` alone without syncing `value` in `onChange` clears selection when options re-render.
|
|
56
|
-
|
|
57
|
-
### When to use uncontrolled (multi only)
|
|
58
|
-
|
|
59
|
-
Uncontrolled multi is appropriate when:
|
|
60
|
-
|
|
61
|
-
- No other part of the UI needs to react to the current selection (no live summary, no dependent field, no enabled/disabled button).
|
|
62
|
-
- You only need the value at form submission — via `FormData`, a submit handler reading the DOM, or react-hook-form's `getValues`.
|
|
63
|
-
- Simplicity is the priority; omitting `value` means one less piece of state to manage.
|
|
64
|
-
|
|
65
|
-
```tsx
|
|
66
|
-
// Good fit: a "tags" field where only the submitted array matters
|
|
67
|
-
<SelectDropdown
|
|
68
|
-
multiple
|
|
69
|
-
name="tags"
|
|
70
|
-
options={tagOptions}
|
|
71
|
-
onCreateOption={(v) => setTagOptions((prev) => [...prev, v])}
|
|
72
|
-
/>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### When to use controlled
|
|
76
|
-
|
|
77
|
-
Use controlled when:
|
|
78
|
-
|
|
79
|
-
- Another part of the UI must reflect the current selection in real time (summary text, a filtered list, an enable/disable condition).
|
|
80
|
-
- You need to pre-populate from an API response, reset on cancel, or sync with a form library like react-hook-form.
|
|
81
|
-
- You are using single-select (the only supported mode for single).
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
// Good fit: pre-populate from API, clear on cancel, show live summary
|
|
85
|
-
const [selected, setSelected] = useState<string[]>(initialValues);
|
|
86
|
-
|
|
87
|
-
<SelectDropdown
|
|
88
|
-
multiple
|
|
89
|
-
name="languages"
|
|
90
|
-
options={languageOptions}
|
|
91
|
-
value={selected}
|
|
92
|
-
onChange={(opts) => setSelected(opts.map((o) => o.value))}
|
|
93
|
-
/>
|
|
94
|
-
<p>Selected: {selected.join(', ') || 'none'}</p>
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## onChange contract
|
|
100
|
-
|
|
101
|
-
`onChange` receives option object(s), not `event.target.value`:
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
// Single
|
|
105
|
-
onChange={(option) => setValue(option.value)}
|
|
106
|
-
|
|
107
|
-
// Multi
|
|
108
|
-
onChange={(selected) => setValue(selected.map((o) => o.value))}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Second argument is react-select `ActionMeta`. For creatable creates: `meta.action === 'create-option'`. Do **not** pass `onCreateOption` to react-select directly — Gamut invokes it from `changeHandler` while still forwarding `create-option` to consumer `onChange`.
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## Creatable
|
|
116
|
-
|
|
117
|
-
- `isCreatable` forces `isSearchable: true` (TypeScript enforces this).
|
|
118
|
-
- `onCreateOption(inputValue)` — convenience hook to append to `options`.
|
|
119
|
-
- `onChange(selected, meta)` — use `meta.action === 'create-option'` to sync controlled `value` and `options` together.
|
|
120
|
-
- `isValidNewOption` — return `false` to hide the Add row.
|
|
121
|
-
- `validationMessage` — replaces menu "No options" text; mirror in `FormGroup` `error` for field-level feedback.
|
|
122
|
-
|
|
123
|
-
**Validation after blur:** react-select clears input on blur before `onBlur` fires, so the value is gone by the time you'd validate it. Store the last typed value in a ref and re-validate from it on `input-blur`:
|
|
124
|
-
|
|
125
|
-
```tsx
|
|
126
|
-
const lastInput = useRef('');
|
|
127
|
-
|
|
128
|
-
<SelectDropdown
|
|
129
|
-
isCreatable
|
|
130
|
-
onInputChange={(value, { action }) => {
|
|
131
|
-
if (action === 'input-change') lastInput.current = value;
|
|
132
|
-
if (action === 'input-blur') validate(lastInput.current);
|
|
133
|
-
}}
|
|
134
|
-
/>;
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## FormGroup wiring
|
|
140
|
-
|
|
141
|
-
- `FormGroup` `htmlFor` must match control `id` (not `name`). Alternatively, pass `htmlFor` directly on SelectDropdown and it becomes `id` downstream.
|
|
142
|
-
- Pass `name` on SelectDropdown (required for forms).
|
|
143
|
-
- Pass `aria-label` (required for forms); it must match the FormGroupLabel `htmlFor`.
|
|
144
|
-
- Pass `error` boolean when FormGroup has an error.
|
|
145
|
-
- Generic FormGroup live-region behavior: see [`gamut-forms`](../gamut-forms/SKILL.md).
|
|
146
|
-
|
|
147
|
-
```tsx
|
|
148
|
-
<FormGroup htmlFor="country" isSoloField label="Country" error={errors.country}>
|
|
149
|
-
<SelectDropdown
|
|
150
|
-
id="country"
|
|
151
|
-
name="country"
|
|
152
|
-
aria-label="country"
|
|
153
|
-
options={options}
|
|
154
|
-
value={value}
|
|
155
|
-
error={Boolean(errors.country)}
|
|
156
|
-
onChange={(option) => setValue(option.value)}
|
|
157
|
-
/>
|
|
158
|
-
</FormGroup>
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
## Styling & layout props
|
|
164
|
-
|
|
165
|
-
| Prop | Type | Default | Notes |
|
|
166
|
-
| ------------------- | ------------------------ | -------- | --------------------------------------------------------- |
|
|
167
|
-
| `size` | `'small' \| 'medium'` | `medium` | Control height/density |
|
|
168
|
-
| `shownOptionsLimit` | `1`–`6` | `6` | Visible options before the menu scrolls |
|
|
169
|
-
| `inputWidth` | `string \| number` | — | Width of the input independent of the menu |
|
|
170
|
-
| `dropdownWidth` | `string \| number` | — | Width of the menu independent of the input |
|
|
171
|
-
| `menuAlignment` | `'left' \| 'right'` | `left` | Menu edge alignment |
|
|
172
|
-
| `zIndex` | `number` | auto | Menu z-index |
|
|
173
|
-
| `inputProps` | `{ hidden?, combobox? }` | — | `data-*` / `aria-*` only, forwarded to the input elements |
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
## Examples
|
|
178
|
-
|
|
179
|
-
### Single (controlled)
|
|
180
|
-
|
|
181
|
-
```tsx
|
|
182
|
-
const [value, setValue] = useState('us');
|
|
183
|
-
|
|
184
|
-
<SelectDropdown
|
|
185
|
-
name="country"
|
|
186
|
-
options={options}
|
|
187
|
-
value={value}
|
|
188
|
-
onChange={(option) => setValue(option.value)}
|
|
189
|
-
/>;
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Multi (uncontrolled)
|
|
193
|
-
|
|
194
|
-
```tsx
|
|
195
|
-
<SelectDropdown
|
|
196
|
-
multiple
|
|
197
|
-
name="tags"
|
|
198
|
-
options={options}
|
|
199
|
-
onChange={(selected) => console.log(selected)}
|
|
200
|
-
/>
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### Creatable multi (uncontrolled)
|
|
204
|
-
|
|
205
|
-
```tsx
|
|
206
|
-
const [options, setOptions] = useState(['Apple', 'Banana']);
|
|
207
|
-
|
|
208
|
-
<SelectDropdown
|
|
209
|
-
isCreatable
|
|
210
|
-
multiple
|
|
211
|
-
name="fruits"
|
|
212
|
-
options={options}
|
|
213
|
-
onCreateOption={(v) => setOptions((prev) => [...prev, v])}
|
|
214
|
-
/>;
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
### Creatable multi (controlled)
|
|
218
|
-
|
|
219
|
-
```tsx
|
|
220
|
-
const [options, setOptions] = useState(['Apple', 'Banana']);
|
|
221
|
-
const [value, setValue] = useState<string[]>([]);
|
|
222
|
-
|
|
223
|
-
<SelectDropdown
|
|
224
|
-
isCreatable
|
|
225
|
-
multiple
|
|
226
|
-
name="fruits"
|
|
227
|
-
options={options}
|
|
228
|
-
value={value}
|
|
229
|
-
onChange={(selected, meta) => {
|
|
230
|
-
setValue(selected.map((o) => o.value));
|
|
231
|
-
if (meta.action === 'create-option' && meta.option) {
|
|
232
|
-
setOptions((prev) => [...prev, meta.option.value]);
|
|
233
|
-
}
|
|
234
|
-
}}
|
|
235
|
-
/>;
|
|
236
|
-
```
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
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";
|
|
@@ -1,14 +0,0 @@
|
|
|
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,22 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,62 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,39 +0,0 @@
|
|
|
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
|
-
};
|
|
File without changes
|