@codecademy/gamut 72.0.2 → 72.0.3-alpha.8100dc.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 +6 -0
- package/agent-tools/skills/gamut-select-dropdown/SKILL.md +183 -0
- package/dist/Form/SelectDropdown/SelectDropdown.js +35 -18
- package/dist/Form/SelectDropdown/elements/constants.d.ts +0 -8
- package/dist/Form/SelectDropdown/elements/constants.js +1 -9
- package/dist/Form/SelectDropdown/elements/containers.d.ts +6 -2
- package/dist/Form/SelectDropdown/elements/containers.js +17 -1
- package/dist/Form/SelectDropdown/elements/controls.js +3 -5
- package/dist/Form/SelectDropdown/elements/options.d.ts +1 -0
- package/dist/Form/SelectDropdown/elements/options.js +5 -2
- package/dist/Form/SelectDropdown/styles.js +27 -9
- package/dist/Form/SelectDropdown/types/component-props.d.ts +57 -5
- package/dist/Form/SelectDropdown/types/styles.d.ts +4 -0
- package/dist/Form/SelectDropdown/utils.d.ts +7 -1
- package/dist/Form/SelectDropdown/utils.js +16 -0
- package/package.json +6 -6
|
@@ -26,6 +26,12 @@ For typical product forms, prefer `GridForm` (declarative `fields`, `LayoutGrid`
|
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
29
|
+
## SelectDropdown
|
|
30
|
+
|
|
31
|
+
For `SelectDropdown` — single vs multi value, controlled vs uncontrolled patterns, creatable options, and 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; SelectDropdown-specific state contracts live in that skill.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
29
35
|
## `FormGroup` (baseline)
|
|
30
36
|
|
|
31
37
|
[`FormGroup.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Form/elements/FormGroup.tsx)
|
|
@@ -0,0 +1,183 @@
|
|
|
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 react-select action meta. Pair with gamut-forms for FormGroup/validation patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Gamut SelectDropdown
|
|
7
|
+
|
|
8
|
+
Styled dropdown built on react-select. Supports single and multi-select, searchable menus, creatable options, icons, groups, and abbreviations.
|
|
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
|
+
---
|
|
58
|
+
|
|
59
|
+
## onChange contract
|
|
60
|
+
|
|
61
|
+
`onChange` receives option object(s), not `event.target.value`:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// Single
|
|
65
|
+
onChange={(option) => setValue(option.value)}
|
|
66
|
+
|
|
67
|
+
// Multi
|
|
68
|
+
onChange={(selected) => setValue(selected.map((o) => o.value))}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
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`.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Creatable
|
|
76
|
+
|
|
77
|
+
- `isCreatable` forces `isSearchable: true` (TypeScript enforces this).
|
|
78
|
+
- `onCreateOption(inputValue)` — convenience hook to append to `options`.
|
|
79
|
+
- `onChange(selected, meta)` — use `meta.action === 'create-option'` to sync controlled `value` and `options` together.
|
|
80
|
+
- `isValidNewOption` — return `false` to hide the Add row.
|
|
81
|
+
- `validationMessage` — replaces menu "No options" text; mirror in `FormGroup` `error` for field-level feedback.
|
|
82
|
+
|
|
83
|
+
**Validation after blur:** react-select clears input on blur. Handle `onInputChange`: validate on `input-change`, re-validate from last typed value on `input-blur` so FormGroup error persists.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## FormGroup wiring
|
|
88
|
+
|
|
89
|
+
- `FormGroup` `htmlFor` must match control `id` / `name`.
|
|
90
|
+
- Pass `name` on SelectDropdown (required for forms).
|
|
91
|
+
- Pass `aria-label` (required for forms); it must match the FormGroupLabel `htmlFor` / `name`.
|
|
92
|
+
- Pass `error` boolean when FormGroup has an error.
|
|
93
|
+
- Generic FormGroup live-region behavior: see [`gamut-forms`](../gamut-forms/SKILL.md).
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<FormGroup htmlFor="country" isSoloField label="Country" error={errors.country}>
|
|
97
|
+
<SelectDropdown
|
|
98
|
+
name="country"
|
|
99
|
+
aria-label="country"
|
|
100
|
+
options={options}
|
|
101
|
+
value={value}
|
|
102
|
+
error={Boolean(errors.country)}
|
|
103
|
+
onChange={(option) => setValue(option.value)}
|
|
104
|
+
/>
|
|
105
|
+
</FormGroup>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Styling & layout props
|
|
111
|
+
|
|
112
|
+
| Prop | Type | Default | Notes |
|
|
113
|
+
| ------------------- | ------------------------ | -------- | --------------------------------------------------------- |
|
|
114
|
+
| `size` | `'small' \| 'medium'` | `medium` | Control height/density |
|
|
115
|
+
| `shownOptionsLimit` | `1`–`6` | `6` | Visible options before the menu scrolls |
|
|
116
|
+
| `inputWidth` | `string \| number` | — | Width of the input independent of the menu |
|
|
117
|
+
| `dropdownWidth` | `string \| number` | — | Width of the menu independent of the input |
|
|
118
|
+
| `menuAlignment` | `'left' \| 'right'` | `left` | Menu edge alignment |
|
|
119
|
+
| `zIndex` | `number` | auto | Menu z-index |
|
|
120
|
+
| `inputProps` | `{ hidden?, combobox? }` | — | `data-*` / `aria-*` only, forwarded to the input elements |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Examples
|
|
125
|
+
|
|
126
|
+
### Single (controlled)
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
const [value, setValue] = useState('us');
|
|
130
|
+
|
|
131
|
+
<SelectDropdown
|
|
132
|
+
name="country"
|
|
133
|
+
options={options}
|
|
134
|
+
value={value}
|
|
135
|
+
onChange={(option) => setValue(option.value)}
|
|
136
|
+
/>;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Multi (uncontrolled)
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<SelectDropdown
|
|
143
|
+
multiple
|
|
144
|
+
name="tags"
|
|
145
|
+
options={options}
|
|
146
|
+
onChange={(selected) => console.log(selected)}
|
|
147
|
+
/>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Creatable multi (uncontrolled)
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
const [options, setOptions] = useState(['Apple', 'Banana']);
|
|
154
|
+
|
|
155
|
+
<SelectDropdown
|
|
156
|
+
isCreatable
|
|
157
|
+
multiple
|
|
158
|
+
name="fruits"
|
|
159
|
+
options={options}
|
|
160
|
+
onCreateOption={(v) => setOptions((prev) => [...prev, v])}
|
|
161
|
+
/>;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Creatable multi (controlled)
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
const [options, setOptions] = useState(['Apple', 'Banana']);
|
|
168
|
+
const [value, setValue] = useState<string[]>([]);
|
|
169
|
+
|
|
170
|
+
<SelectDropdown
|
|
171
|
+
isCreatable
|
|
172
|
+
multiple
|
|
173
|
+
name="fruits"
|
|
174
|
+
options={options}
|
|
175
|
+
value={value}
|
|
176
|
+
onChange={(selected, meta) => {
|
|
177
|
+
setValue(selected.map((o) => o.value));
|
|
178
|
+
if (meta.action === 'create-option' && meta.option) {
|
|
179
|
+
setOptions((prev) => [...prev, meta.option.value]);
|
|
180
|
+
}
|
|
181
|
+
}}
|
|
182
|
+
/>;
|
|
183
|
+
```
|
|
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|
|
4
4
|
import { parseOptions } from '../utils';
|
|
5
5
|
import { AbbreviatedSingleValue, CustomContainer, CustomInput, CustomValueContainer, DropdownButton, formatGroupLabel, formatOptionLabel, IconOption, MultiValueRemoveButton, MultiValueWithColorMode, onFocus, RemoveAllButton, SelectDropdownContext, TypedReactSelect } from './elements';
|
|
6
6
|
import { getMemoizedStyles } from './styles';
|
|
7
|
-
import { filterValueFromOptions, isMultipleSelectProps, isOptionsGrouped, isSingleSelectProps, removeValueFromSelectedOptions } from './utils';
|
|
7
|
+
import { filterValueFromOptions, getCreatedOptionValue, isMultipleSelectProps, isOptionsGrouped, isSingleSelectProps, removeValueFromSelectedOptions } from './utils';
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
const defaultProps = {
|
|
10
10
|
name: undefined,
|
|
@@ -73,22 +73,30 @@ export const SelectDropdown = ({
|
|
|
73
73
|
disabled,
|
|
74
74
|
dropdownWidth,
|
|
75
75
|
error,
|
|
76
|
+
formatCreateLabel = inputValue => `Add "${inputValue}"`,
|
|
76
77
|
id,
|
|
77
78
|
inputProps,
|
|
78
79
|
inputWidth,
|
|
79
|
-
|
|
80
|
+
isCreatable = false,
|
|
81
|
+
isSearchable: isSearchableProp = false,
|
|
82
|
+
isValidNewOption,
|
|
80
83
|
menuAlignment = 'left',
|
|
81
84
|
multiple,
|
|
82
85
|
name,
|
|
83
86
|
onChange,
|
|
87
|
+
onCreateOption,
|
|
88
|
+
onInputChange,
|
|
84
89
|
options,
|
|
85
90
|
placeholder = 'Select an option',
|
|
86
91
|
shownOptionsLimit = 6,
|
|
87
92
|
size,
|
|
93
|
+
validationMessage,
|
|
88
94
|
value,
|
|
89
95
|
zIndex,
|
|
90
96
|
...rest
|
|
91
97
|
}) => {
|
|
98
|
+
// isSearchable is forced true when isCreatable is true (CreatableSelect requires a text input)
|
|
99
|
+
const isSearchable = isCreatable || isSearchableProp;
|
|
92
100
|
const rawInputId = useId();
|
|
93
101
|
const inputId = name ?? `${id}-select-dropdown-${rawInputId}`;
|
|
94
102
|
const [activated, setActivated] = useState(false);
|
|
@@ -126,39 +134,41 @@ export const SelectDropdown = ({
|
|
|
126
134
|
// To keep this efficient for non-multiSelect
|
|
127
135
|
filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions)));
|
|
128
136
|
|
|
129
|
-
//
|
|
137
|
+
// Sync multi-select value from props when controlled (`value` is a string[]).
|
|
138
|
+
// Uncontrolled multi (`value` undefined or '') keeps selection in local state.
|
|
130
139
|
useEffect(() => {
|
|
140
|
+
if (!multiple || !Array.isArray(value)) return;
|
|
131
141
|
const newMultiValues = filterValueFromOptions(selectOptions, value, isOptionsGrouped(selectOptions));
|
|
132
142
|
if (newMultiValues !== multiValues) setMultiValues(newMultiValues);
|
|
133
143
|
|
|
134
|
-
//
|
|
135
144
|
// We only update this when our passed in options or value changes, not multiValues.
|
|
136
145
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
-
}, [options, value]);
|
|
138
|
-
const changeHandler = useCallback(optionEvent => {
|
|
146
|
+
}, [options, value, multiple]);
|
|
147
|
+
const changeHandler = useCallback((optionEvent, actionMeta) => {
|
|
139
148
|
setActivated(true);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
if (actionMeta.action === 'create-option') {
|
|
150
|
+
const createdValue = getCreatedOptionValue(optionEvent, actionMeta, multiple);
|
|
151
|
+
if (createdValue) {
|
|
152
|
+
onCreateOption?.(createdValue);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
143
155
|
const onChangeProps = {
|
|
144
156
|
onChange,
|
|
145
157
|
multiple
|
|
146
158
|
};
|
|
159
|
+
const forwardedMeta = actionMeta.action === 'create-option' ? actionMeta : {
|
|
160
|
+
action: onChangeAction,
|
|
161
|
+
option: isMultipleSelectProps(onChangeProps) ? undefined : optionEvent
|
|
162
|
+
};
|
|
147
163
|
if (isSingleSelectProps(onChangeProps)) {
|
|
148
164
|
const singleOptionEvent = optionEvent;
|
|
149
|
-
onChangeProps.onChange?.(singleOptionEvent,
|
|
150
|
-
action: onChangeAction,
|
|
151
|
-
option: singleOptionEvent
|
|
152
|
-
});
|
|
165
|
+
onChangeProps.onChange?.(singleOptionEvent, forwardedMeta);
|
|
153
166
|
}
|
|
154
167
|
if (isMultipleSelectProps(onChangeProps)) {
|
|
155
168
|
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
|
-
});
|
|
169
|
+
onChangeProps.onChange?.(optionEvent, forwardedMeta);
|
|
160
170
|
}
|
|
161
|
-
}, [onChange, multiple]);
|
|
171
|
+
}, [onChange, multiple, onCreateOption]);
|
|
162
172
|
const keyPressHandler = e => {
|
|
163
173
|
if (multiple && e.key === 'Enter' && currentFocusedValue && multiValues) {
|
|
164
174
|
const newMultiValues = removeValueFromSelectedOptions(multiValues, currentFocusedValue);
|
|
@@ -168,6 +178,8 @@ export const SelectDropdown = ({
|
|
|
168
178
|
removeAllButtonRef.current.focus();
|
|
169
179
|
}
|
|
170
180
|
};
|
|
181
|
+
const noOptionsMessage = validationMessage === undefined ? undefined // fall back to react-select default ("No options")
|
|
182
|
+
: typeof validationMessage === 'function' ? validationMessage : () => validationMessage;
|
|
171
183
|
const theme = useTheme();
|
|
172
184
|
const memoizedStyles = useMemo(() => {
|
|
173
185
|
return getMemoizedStyles(theme, zIndex);
|
|
@@ -188,6 +200,7 @@ export const SelectDropdown = ({
|
|
|
188
200
|
},
|
|
189
201
|
dropdownWidth: dropdownWidth,
|
|
190
202
|
error: Boolean(error),
|
|
203
|
+
formatCreateLabel: formatCreateLabel,
|
|
191
204
|
formatGroupLabel: formatGroupLabel,
|
|
192
205
|
formatOptionLabel: formatOptionLabel,
|
|
193
206
|
id: id || rest.htmlFor || rawInputId,
|
|
@@ -196,12 +209,15 @@ export const SelectDropdown = ({
|
|
|
196
209
|
...inputProps
|
|
197
210
|
},
|
|
198
211
|
inputWidth: inputWidth,
|
|
212
|
+
isCreatable: isCreatable,
|
|
199
213
|
isDisabled: disabled,
|
|
200
214
|
isMulti: multiple,
|
|
201
215
|
isOptionDisabled: option => option.disabled,
|
|
202
216
|
isSearchable: isSearchable,
|
|
217
|
+
isValidNewOption: isValidNewOption,
|
|
203
218
|
menuAlignment: menuAlignment,
|
|
204
219
|
name: name,
|
|
220
|
+
noOptionsMessage: noOptionsMessage,
|
|
205
221
|
options: selectOptions,
|
|
206
222
|
placeholder: placeholder,
|
|
207
223
|
selectRef: selectInputRef,
|
|
@@ -210,6 +226,7 @@ export const SelectDropdown = ({
|
|
|
210
226
|
styles: memoizedStyles,
|
|
211
227
|
value: multiple ? multiValues : parsedValue,
|
|
212
228
|
onChange: changeHandler,
|
|
229
|
+
onInputChange: onInputChange,
|
|
213
230
|
onKeyDown: multiple ? e => keyPressHandler(e) : undefined,
|
|
214
231
|
...rest
|
|
215
232
|
})
|
|
@@ -15,14 +15,6 @@ 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
|
-
};
|
|
26
18
|
smallRemove: {
|
|
27
19
|
size: number;
|
|
28
20
|
icon: import("react").ForwardRefExoticComponent<import("@codecademy/gamut-icons").GamutIconProps & import("react").RefAttributes<SVGSVGElement>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArrowChevronDownIcon, CloseIcon, MiniChevronDownIcon, MiniDeleteIcon
|
|
1
|
+
import { ArrowChevronDownIcon, CloseIcon, MiniChevronDownIcon, MiniDeleteIcon } from '@codecademy/gamut-icons';
|
|
2
2
|
export const iconSize = {
|
|
3
3
|
small: 12,
|
|
4
4
|
medium: 16
|
|
@@ -16,14 +16,6 @@ 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
|
-
},
|
|
27
19
|
smallRemove: {
|
|
28
20
|
size: iconSize.small,
|
|
29
21
|
icon: MiniDeleteIcon
|
|
@@ -24,6 +24,10 @@ 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
|
-
*
|
|
27
|
+
* Renders CreatableSelect when isCreatable is true, ReactSelect otherwise.
|
|
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.
|
|
28
32
|
*/
|
|
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;
|
|
33
|
+
export declare function TypedReactSelect<OptionType, IsMulti extends boolean = false, GroupType extends GroupBase<OptionType> = GroupBase<OptionType>>({ selectRef, isCreatable, formatCreateLabel, isValidNewOption, ...props }: Props<OptionType, IsMulti, GroupType> & TypedReactSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext, useLayoutEffect } from 'react';
|
|
2
2
|
import ReactSelect, { components as SelectDropdownElements } from 'react-select';
|
|
3
|
+
import CreatableSelect from 'react-select/creatable';
|
|
3
4
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
5
|
/**
|
|
5
6
|
* React context for sharing state between SelectDropdown components.
|
|
@@ -116,12 +117,27 @@ export const CustomInput = ({
|
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
119
|
* Typed wrapper around react-select component.
|
|
119
|
-
*
|
|
120
|
+
* Renders CreatableSelect when isCreatable is true, ReactSelect otherwise.
|
|
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.
|
|
120
125
|
*/
|
|
121
126
|
export function TypedReactSelect({
|
|
122
127
|
selectRef,
|
|
128
|
+
isCreatable,
|
|
129
|
+
formatCreateLabel,
|
|
130
|
+
isValidNewOption,
|
|
123
131
|
...props
|
|
124
132
|
}) {
|
|
133
|
+
if (isCreatable) {
|
|
134
|
+
return /*#__PURE__*/_jsx(CreatableSelect, {
|
|
135
|
+
...props,
|
|
136
|
+
formatCreateLabel: formatCreateLabel,
|
|
137
|
+
isValidNewOption: isValidNewOption,
|
|
138
|
+
ref: selectRef
|
|
139
|
+
});
|
|
140
|
+
}
|
|
125
141
|
return /*#__PURE__*/_jsx(ReactSelect, {
|
|
126
142
|
...props,
|
|
127
143
|
ref: selectRef
|
|
@@ -36,15 +36,13 @@ export const onFocus = ({
|
|
|
36
36
|
*/
|
|
37
37
|
export const DropdownButton = props => {
|
|
38
38
|
const {
|
|
39
|
-
size
|
|
40
|
-
isSearchable
|
|
39
|
+
size
|
|
41
40
|
} = props.selectProps;
|
|
42
41
|
const color = props.isDisabled ? 'text-disabled' : 'text';
|
|
43
42
|
const iconSize = size ?? 'medium';
|
|
44
|
-
const iconType = isSearchable ? 'Searchable' : 'Chevron';
|
|
45
43
|
const {
|
|
46
44
|
...iconProps
|
|
47
|
-
} = indicatorIcons[`${iconSize}
|
|
45
|
+
} = indicatorIcons[`${iconSize}Chevron`];
|
|
48
46
|
const {
|
|
49
47
|
icon: IndicatorIcon
|
|
50
48
|
} = iconProps;
|
|
@@ -66,7 +64,7 @@ const CustomStyledRemoveAllDiv = /*#__PURE__*/_styled('div', {
|
|
|
66
64
|
'&:focus-visible': {
|
|
67
65
|
outline: `2px solid ${theme.colors.primary}`
|
|
68
66
|
}
|
|
69
|
-
}), process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
67
|
+
}), process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9Gb3JtL1NlbGVjdERyb3Bkb3duL2VsZW1lbnRzL2NvbnRyb2xzLnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFvRGlDIiwiZmlsZSI6Ii4uLy4uLy4uLy4uL3NyYy9Gb3JtL1NlbGVjdERyb3Bkb3duL2VsZW1lbnRzL2NvbnRyb2xzLnRzeCIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNzcywgdGhlbWUgfSBmcm9tICdAY29kZWNhZGVteS9nYW11dC1zdHlsZXMnO1xuaW1wb3J0IHN0eWxlZCBmcm9tICdAZW1vdGlvbi9zdHlsZWQnO1xuaW1wb3J0IHsgS2V5Ym9hcmRFdmVudCwgdXNlQ29udGV4dCB9IGZyb20gJ3JlYWN0JztcbmltcG9ydCB7XG4gIEFyaWFPbkZvY3VzLFxuICBjb21wb25lbnRzIGFzIFNlbGVjdERyb3Bkb3duRWxlbWVudHMsXG59IGZyb20gJ3JlYWN0LXNlbGVjdCc7XG5cbmltcG9ydCB7IEV4dGVuZGVkT3B0aW9uLCBTaXplZEluZGljYXRvclByb3BzIH0gZnJvbSAnLi4vdHlwZXMnO1xuaW1wb3J0IHsgaW5kaWNhdG9ySWNvbnMgfSBmcm9tICcuL2NvbnN0YW50cyc7XG5pbXBvcnQgeyBTZWxlY3REcm9wZG93bkNvbnRleHQgfSBmcm9tICcuL2NvbnRhaW5lcnMnO1xuXG5jb25zdCB7IERyb3Bkb3duSW5kaWNhdG9yIH0gPSBTZWxlY3REcm9wZG93bkVsZW1lbnRzO1xuXG4vKipcbiAqIEdlbmVyYXRlcyBhY2Nlc3NpYmxlIGZvY3VzIG1lc3NhZ2VzIGZvciBzY3JlZW4gcmVhZGVycy5cbiAqIFByb3ZpZGVzIGRldGFpbGVkIGluZm9ybWF0aW9uIGFib3V0IHRoZSBjdXJyZW50bHkgZm9jdXNlZCBvcHRpb24uXG4gKlxuICogQHBhcmFtIHBhcmFtcyAtIE9iamVjdCBjb250YWluaW5nIHRoZSBmb2N1c2VkIG9wdGlvbiBkZXRhaWxzXG4gKiBAcmV0dXJucyBGb3JtYXR0ZWQgYWNjZXNzaWJpbGl0eSBtZXNzYWdlXG4gKi9cbmV4cG9ydCBjb25zdCBvbkZvY3VzOiBBcmlhT25Gb2N1czxFeHRlbmRlZE9wdGlvbj4gPSAoe1xuICBmb2N1c2VkOiB7IGxhYmVsLCBzdWJ0aXRsZSwgcmlnaHRMYWJlbCwgZGlzYWJsZWQgfSxcbn0pID0+IHtcbiAgY29uc3QgZm9ybWF0dGVkU3VidGl0bGUgPSBgLCAke3N1YnRpdGxlfWA7XG4gIGNvbnN0IGZvcm1hdHRlZFJpZ2h0TGFiZWwgPSBgLCAke3JpZ2h0TGFiZWx9YDtcblxuICBjb25zdCBtc2cgPSBgWW91IGFyZSBjdXJyZW50bHkgZm9jdXNlZCBvbiBvcHRpb24gJHtsYWJlbH0ke1xuICAgIHN1YnRpdGxlID8gZm9ybWF0dGVkU3VidGl0bGUgOiAnJ1xuICB9ICR7cmlnaHRMYWJlbCA/IGZvcm1hdHRlZFJpZ2h0TGFiZWwgOiAnJ30ke2Rpc2FibGVkID8gJywgZGlzYWJsZWQnIDogJyd9YDtcblxuICByZXR1cm4gbXNnO1xufTtcblxuLyoqXG4gKiBDdXN0b20gZHJvcGRvd24gaW5kaWNhdG9yIHRoYXQgc2hvd3MgZWl0aGVyIGEgY2hldnJvbiBvciBzZWFyY2ggaWNvbi5cbiAqIFRoZSBpY29uIHR5cGUgZGVwZW5kcyBvbiB3aGV0aGVyIHRoZSBzZWxlY3QgaXMgc2VhcmNoYWJsZSBvciBub3QuXG4gKi9cbmV4cG9ydCBjb25zdCBEcm9wZG93bkJ1dHRvbiA9IChwcm9wczogU2l6ZWRJbmRpY2F0b3JQcm9wcykgPT4ge1xuICBjb25zdCB7IHNpemUgfSA9IHByb3BzLnNlbGVjdFByb3BzO1xuICBjb25zdCBjb2xvciA9IHByb3BzLmlzRGlzYWJsZWQgPyAndGV4dC1kaXNhYmxlZCcgOiAndGV4dCc7XG4gIGNvbnN0IGljb25TaXplID0gc2l6ZSA/PyAnbWVkaXVtJztcbiAgY29uc3QgeyAuLi5pY29uUHJvcHMgfSA9IGluZGljYXRvckljb25zW2Ake2ljb25TaXplfUNoZXZyb25gXTtcbiAgY29uc3QgeyBpY29uOiBJbmRpY2F0b3JJY29uIH0gPSBpY29uUHJvcHM7XG5cbiAgcmV0dXJuIChcbiAgICA8RHJvcGRvd25JbmRpY2F0b3Igey4uLnByb3BzfT5cbiAgICAgIDxJbmRpY2F0b3JJY29uIHsuLi5pY29uUHJvcHN9IGNvbG9yPXtjb2xvcn0gLz5cbiAgICA8L0Ryb3Bkb3duSW5kaWNhdG9yPlxuICApO1xufTtcblxuY29uc3QgQ3VzdG9tU3R5bGVkUmVtb3ZlQWxsRGl2ID0gc3R5bGVkKCdkaXYnKShcbiAgY3NzKHtcbiAgICAnJjpmb2N1cyc6IHtcbiAgICAgIG91dGxpbmU6IGAycHggc29saWQgJHt0aGVtZS5jb2xvcnMucHJpbWFyeX1gLFxuICAgIH0sXG4gICAgJyY6Zm9jdXMtdmlzaWJsZSc6IHtcbiAgICAgIG91dGxpbmU6IGAycHggc29saWQgJHt0aGVtZS5jb2xvcnMucHJpbWFyeX1gLFxuICAgIH0sXG4gIH0pXG4pO1xuXG4vKipcbiAqIEN1c3RvbSByZW1vdmUgYWxsIGJ1dHRvbiBmb3IgbXVsdGktc2VsZWN0IG1vZGUuXG4gKiBQcm92aWRlcyBrZXlib2FyZCBuYXZpZ2F0aW9uIGFuZCBhY2Nlc3NpYmxlIHJlbW92YWwgb2YgYWxsIHNlbGVjdGVkIHZhbHVlcy5cbiAqL1xuZXhwb3J0IGNvbnN0IFJlbW92ZUFsbEJ1dHRvbiA9IChwcm9wczogU2l6ZWRJbmRpY2F0b3JQcm9wcykgPT4ge1xuICBjb25zdCB7XG4gICAgZ2V0U3R5bGVzLFxuICAgIGlubmVyUHJvcHM6IHsgLi4ucmVzdElubmVyUHJvcHMgfSxcbiAgICBzZWxlY3RQcm9wczogeyBzaXplIH0sXG4gIH0gPSBwcm9wcztcblxuICBjb25zdCB7IHJlbW92ZUFsbEJ1dHRvblJlZiwgc2VsZWN0SW5wdXRSZWYgfSA9IHVzZUNvbnRleHQoXG4gICAgU2VsZWN0RHJvcGRvd25Db250ZXh0XG4gICk7XG5cbiAgY29uc3QgaWNvblNpemUgPSBzaXplID8/ICdtZWRpdW0nO1xuICBjb25zdCB7IC4uLmljb25Qcm9wcyB9ID0gaW5kaWNhdG9ySWNvbnNbYCR7aWNvblNpemV9UmVtb3ZlYF07XG4gIGNvbnN0IHsgaWNvbjogSW5kaWNhdG9ySWNvbiB9ID0gaWNvblByb3BzO1xuXG4gIGNvbnN0IG9uS2V5UHJlc3MgPSAoZTogS2V5Ym9hcmRFdmVudDxIVE1MRGl2RWxlbWVudD4pID0+IHtcbiAgICBpZiAoZS5rZXkgPT09ICdFbnRlcicgJiYgcmVzdElubmVyUHJvcHMub25Nb3VzZURvd24pIHtcbiAgICAgIHJlc3RJbm5lclByb3BzLm9uTW91c2VEb3duKGUgYXMgYW55KTtcbiAgICB9XG5cbiAgICBpZiAoXG4gICAgICBzZWxlY3RJbnB1dFJlZj8uY3VycmVudCAmJlxuICAgICAgKGUua2V5ID09PSAnQXJyb3dSaWdodCcgfHwgZS5rZXkgPT09ICdBcnJvd0xlZnQnIHx8IGUua2V5ID09PSAnQXJyb3dEb3duJylcbiAgICApIHtcbiAgICAgIHNlbGVjdElucHV0UmVmPy5jdXJyZW50LmZvY3VzKCk7XG4gICAgfVxuICB9O1xuXG4gIGNvbnN0IHN0eWxlID0gZ2V0U3R5bGVzKCdjbGVhckluZGljYXRvcicsIHByb3BzKSBhcyBSZWFjdC5DU1NQcm9wZXJ0aWVzO1xuXG4gIHJldHVybiAoXG4gICAgPEN1c3RvbVN0eWxlZFJlbW92ZUFsbERpdlxuICAgICAgYXJpYS1sYWJlbD1cIlJlbW92ZSBhbGwgc2VsZWN0ZWRcIlxuICAgICAgcm9sZT1cImJ1dHRvblwiXG4gICAgICB0YWJJbmRleD17MH1cbiAgICAgIHsuLi5yZXN0SW5uZXJQcm9wc31cbiAgICAgIHJlZj17cmVtb3ZlQWxsQnV0dG9uUmVmfVxuICAgICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGdhbXV0L25vLWlubGluZS1zdHlsZVxuICAgICAgc3R5bGU9e3N0eWxlfVxuICAgICAgb25LZXlEb3duPXtvbktleVByZXNzfVxuICAgID5cbiAgICAgIDxJbmRpY2F0b3JJY29uIHsuLi5pY29uUHJvcHN9IGNvbG9yPVwidGV4dFwiIC8+XG4gICAgPC9DdXN0b21TdHlsZWRSZW1vdmVBbGxEaXY+XG4gICk7XG59O1xuIl19 */");
|
|
70
68
|
|
|
71
69
|
/**
|
|
72
70
|
* Custom remove all button for multi-select mode.
|
|
@@ -3,6 +3,7 @@ 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__).
|
|
6
7
|
*/
|
|
7
8
|
export declare const IconOption: ({ children, ...rest }: CustomSelectComponentProps<typeof SelectDropdownElements.Option>) => import("react/jsx-runtime").JSX.Element;
|
|
8
9
|
/**
|
|
@@ -44,6 +44,7 @@ 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__).
|
|
47
48
|
*/
|
|
48
49
|
export const IconOption = ({
|
|
49
50
|
children,
|
|
@@ -54,15 +55,17 @@ export const IconOption = ({
|
|
|
54
55
|
} = rest.selectProps;
|
|
55
56
|
const {
|
|
56
57
|
isFocused,
|
|
57
|
-
innerProps
|
|
58
|
+
innerProps,
|
|
59
|
+
data
|
|
58
60
|
} = rest;
|
|
61
|
+
const isNew = data?.__isNew__;
|
|
59
62
|
return /*#__PURE__*/_jsxs(SelectDropdownElements.Option, {
|
|
60
63
|
...rest,
|
|
61
64
|
innerProps: {
|
|
62
65
|
...innerProps,
|
|
63
66
|
'aria-selected': isFocused
|
|
64
67
|
},
|
|
65
|
-
children: [children, rest?.isSelected && /*#__PURE__*/_jsx(CheckIcon, {
|
|
68
|
+
children: [children, !isNew && rest?.isSelected && /*#__PURE__*/_jsx(CheckIcon, {
|
|
66
69
|
size: selectedIconSize[size ?? 'medium']
|
|
67
70
|
})]
|
|
68
71
|
});
|
|
@@ -137,6 +137,8 @@ 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',
|
|
140
142
|
...(dropdownWidth ? {
|
|
141
143
|
minWidth: dropdownWidth,
|
|
142
144
|
width: dropdownWidth
|
|
@@ -194,16 +196,32 @@ export const getMemoizedStyles = (theme, zIndex) => {
|
|
|
194
196
|
backgroundColor: theme.colors['secondary-hover']
|
|
195
197
|
}
|
|
196
198
|
}),
|
|
197
|
-
|
|
198
|
-
...
|
|
199
|
-
|
|
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'
|
|
199
|
+
noOptionsMessage: provided => ({
|
|
200
|
+
...provided,
|
|
201
|
+
color: theme.colors['text-secondary']
|
|
206
202
|
}),
|
|
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: isNew ? state.isDisabled ? theme.colors['text-disabled'] : theme.colors.primary : state.isDisabled ? theme.colors['text-disabled'] : 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 centred in the 16px spacer above the option text
|
|
217
|
+
backgroundImage: `linear-gradient(${theme.colors['text-disabled']} 1px, transparent 1px)`,
|
|
218
|
+
backgroundPosition: '0 8px',
|
|
219
|
+
backgroundRepeat: 'no-repeat',
|
|
220
|
+
backgroundSize: '100% 1px',
|
|
221
|
+
paddingTop: isSmall ? '19px' : '27px'
|
|
222
|
+
})
|
|
223
|
+
};
|
|
224
|
+
},
|
|
207
225
|
placeholder: provided => ({
|
|
208
226
|
...provided,
|
|
209
227
|
...placeholderColor({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Ref, SelectHTMLAttributes } from 'react';
|
|
2
|
-
import { Props as NamedProps } from 'react-select';
|
|
2
|
+
import { Options as OptionsType, 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'>, Pick<SelectHTMLAttributes<HTMLSelectElement>, 'value' | 'disabled' | 'onClick'>, SharedProps {
|
|
31
|
+
export interface SelectDropdownCoreProps extends SelectDropdownBaseProps, Omit<NamedProps<OptionStrict, boolean>, 'formatOptionLabel' | 'isDisabled' | 'value' | 'options' | 'components' | 'styles' | 'theme' | 'onChange' | 'multiple' | 'isSearchable'>, 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,6 +38,39 @@ 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);
|
|
41
74
|
}
|
|
42
75
|
/**
|
|
43
76
|
* Props for single-select mode.
|
|
@@ -59,11 +92,23 @@ export interface MultiSelectDropdownProps extends SelectDropdownCoreProps {
|
|
|
59
92
|
/** Callback fired when the selected values change */
|
|
60
93
|
onChange?: NamedProps<OptionStrict, true>['onChange'];
|
|
61
94
|
}
|
|
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
|
+
};
|
|
62
106
|
/**
|
|
63
107
|
* Union type for all SelectDropdown prop variants.
|
|
64
|
-
* Supports both single and multi-select modes through discriminated union
|
|
108
|
+
* Supports both single and multi-select modes through discriminated union,
|
|
109
|
+
* intersected with CreatableConstraint to enforce isSearchable compatibility.
|
|
65
110
|
*/
|
|
66
|
-
export type SelectDropdownProps = SingleSelectDropdownProps | MultiSelectDropdownProps;
|
|
111
|
+
export type SelectDropdownProps = (SingleSelectDropdownProps | MultiSelectDropdownProps) & CreatableConstraint;
|
|
67
112
|
/**
|
|
68
113
|
* Base interface for onChange-related props.
|
|
69
114
|
* Used internally for type checking and prop validation.
|
|
@@ -76,9 +121,16 @@ export interface BaseOnChangeProps {
|
|
|
76
121
|
}
|
|
77
122
|
/**
|
|
78
123
|
* Props for the typed React Select component wrapper.
|
|
79
|
-
* Extends ReactSelectAdditionalProps with an optional ref.
|
|
124
|
+
* Extends ReactSelectAdditionalProps with an optional ref and creatable flag.
|
|
80
125
|
*/
|
|
81
126
|
export interface TypedReactSelectProps extends ReactSelectAdditionalProps {
|
|
82
127
|
/** Optional ref to the underlying react-select component */
|
|
83
128
|
selectRef?: Ref<any>;
|
|
129
|
+
/** When true, renders CreatableSelect instead of ReactSelect */
|
|
130
|
+
isCreatable?: boolean;
|
|
131
|
+
/** Customises the "Add" row label */
|
|
132
|
+
formatCreateLabel?: (inputValue: string) => React.ReactNode;
|
|
133
|
+
/** Controls visibility of the "Add" row */
|
|
134
|
+
isValidNewOption?: (inputValue: string, value: OptionsType<OptionStrict>, options: OptionsType<OptionStrict>) => boolean;
|
|
84
135
|
}
|
|
136
|
+
export {};
|
|
@@ -69,5 +69,9 @@ 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
|
+
};
|
|
72
76
|
};
|
|
73
77
|
export {};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { ActionMeta, Options as OptionsType } from 'react-select';
|
|
1
2
|
import { SelectOptionBase } from '../utils';
|
|
2
|
-
import { BaseOnChangeProps, ExtendedOption, MultiSelectDropdownProps, SelectDropdownGroup, SelectDropdownOptions, SelectDropdownProps, SingleSelectDropdownProps } from './types';
|
|
3
|
+
import { BaseOnChangeProps, ExtendedOption, MultiSelectDropdownProps, OptionStrict, SelectDropdownGroup, SelectDropdownOptions, SelectDropdownProps, SingleSelectDropdownProps } from './types';
|
|
3
4
|
export declare const isMultipleSelectProps: (props: BaseOnChangeProps) => props is MultiSelectDropdownProps;
|
|
4
5
|
export declare const isSingleSelectProps: (props: BaseOnChangeProps) => props is SingleSelectDropdownProps;
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the value for a newly created option from react-select action metadata
|
|
8
|
+
* or the onChange option payload. Returns undefined when no reliable value exists.
|
|
9
|
+
*/
|
|
10
|
+
export declare const getCreatedOptionValue: (optionEvent: OptionStrict | OptionsType<OptionStrict>, actionMeta: ActionMeta<OptionStrict>, multiple?: boolean) => string | undefined;
|
|
5
11
|
export declare const isOptionGroup: (obj: unknown) => obj is SelectDropdownGroup;
|
|
6
12
|
export declare const isOptionsGrouped: (options: SelectDropdownOptions) => options is SelectDropdownGroup[];
|
|
7
13
|
/**
|
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
};
|
|
3
19
|
export const isOptionGroup = obj => obj != null && typeof obj === 'object' && 'options' in obj && obj.options !== undefined;
|
|
4
20
|
export const isOptionsGrouped = options => Array.isArray(options) && options.some(option => isOptionGroup(option));
|
|
5
21
|
|
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.0.
|
|
4
|
+
"version": "72.0.3-alpha.8100dc.0",
|
|
5
5
|
"author": "Codecademy Engineering <dev@codecademy.com>",
|
|
6
6
|
"bin": "./bin/gamut.mjs",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@codecademy/gamut-icons": "9.57.
|
|
9
|
-
"@codecademy/gamut-illustrations": "0.58.
|
|
10
|
-
"@codecademy/gamut-patterns": "0.10.
|
|
11
|
-
"@codecademy/gamut-styles": "20.0.
|
|
12
|
-
"@codecademy/variance": "0.26.
|
|
8
|
+
"@codecademy/gamut-icons": "9.57.10-alpha.8100dc.0",
|
|
9
|
+
"@codecademy/gamut-illustrations": "0.58.16-alpha.8100dc.0",
|
|
10
|
+
"@codecademy/gamut-patterns": "0.10.35-alpha.8100dc.0",
|
|
11
|
+
"@codecademy/gamut-styles": "20.0.3-alpha.8100dc.0",
|
|
12
|
+
"@codecademy/variance": "0.26.2-alpha.8100dc.0",
|
|
13
13
|
"@formatjs/intl-locale": "5.3.1",
|
|
14
14
|
"@react-aria/interactions": "3.25.0",
|
|
15
15
|
"@types/marked": "^4.0.8",
|