@codecademy/gamut 72.2.3-alpha.c298d6.0 → 72.2.3-alpha.e393cd.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.
@@ -26,6 +26,22 @@ 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
+
29
45
  ## `FormGroup` (baseline)
30
46
 
31
47
  `packages/gamut/src/Form/elements/FormGroup.tsx`
@@ -0,0 +1,236 @@
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
+ ```
@@ -16,7 +16,6 @@ export const IconButton = /*#__PURE__*/forwardRef(({
16
16
  const iconSize = iconSizeMapping[buttonSize];
17
17
  return /*#__PURE__*/_jsx(ToolTip, {
18
18
  info: tip,
19
- closeOnClick: true,
20
19
  ...tipProps,
21
20
  children: /*#__PURE__*/_jsx(IconButtonBase, {
22
21
  ...props,
@@ -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
- isSearchable = false,
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
- // If the caller changes the initial value, let's update our value to match.
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
- // 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
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, SearchIcon } from '@codecademy/gamut-icons';
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
- * Provides type safety for the underlying react-select implementation.
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
- * Provides type safety for the underlying react-select implementation.
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
@@ -13,8 +13,3 @@ export declare const onFocus: AriaOnFocus<ExtendedOption>;
13
13
  * The icon type depends on whether the select is searchable or not.
14
14
  */
15
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,9 +1,5 @@
1
- import _styled from "@emotion/styled/base";
2
- import { css, theme } from '@codecademy/gamut-styles';
3
- import { useContext } from 'react';
4
1
  import { components as SelectDropdownElements } from 'react-select';
5
2
  import { indicatorIcons } from './constants';
6
- import { SelectDropdownContext } from './containers';
7
3
  import { jsx as _jsx } from "react/jsx-runtime";
8
4
  const {
9
5
  DropdownIndicator
@@ -36,15 +32,13 @@ export const onFocus = ({
36
32
  */
37
33
  export const DropdownButton = props => {
38
34
  const {
39
- size,
40
- isSearchable
35
+ size
41
36
  } = props.selectProps;
42
37
  const color = props.isDisabled ? 'text-disabled' : 'text';
43
38
  const iconSize = size ?? 'medium';
44
- const iconType = isSearchable ? 'Searchable' : 'Chevron';
45
39
  const {
46
40
  ...iconProps
47
- } = indicatorIcons[`${iconSize}${iconType}`];
41
+ } = indicatorIcons[`${iconSize}Chevron`];
48
42
  const {
49
43
  icon: IndicatorIcon
50
44
  } = iconProps;
@@ -55,66 +49,4 @@ export const DropdownButton = props => {
55
49
  color: color
56
50
  })
57
51
  });
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
- });
120
52
  };
@@ -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
  });
@@ -62,7 +62,7 @@ const textColor = css({
62
62
  color: 'text'
63
63
  });
64
64
  const placeholderColor = css({
65
- color: 'text-disabled'
65
+ color: 'text-secondary'
66
66
  });
67
67
  export const getMemoizedStyles = (theme, zIndex) => {
68
68
  return {
@@ -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
- 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'
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: 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
+ },
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,12 @@ 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
- export interface TypedReactSelectProps extends ReactSelectAdditionalProps {
126
+ export interface TypedReactSelectProps extends ReactSelectAdditionalProps, Pick<SelectDropdownCoreProps, 'formatCreateLabel' | 'isValidNewOption'> {
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;
84
131
  }
132
+ 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
 
@@ -1,12 +1,7 @@
1
1
  import { WithChildrenProp } from '../../utils';
2
- import { TipCenterAlignment, TipNewBaseProps } from '../shared/types';
3
- export type ToolTipProps = TipNewBaseProps & WithChildrenProp & {
2
+ import { TipBaseProps, TipCenterAlignment } from '../shared/types';
3
+ export type ToolTipProps = TipBaseProps & WithChildrenProp & {
4
4
  alignment?: TipCenterAlignment;
5
- /**
6
- * If true, the tooltip closes immediately when the trigger is clicked or activated via keyboard.
7
- * Pass `false` via `tipProps` on IconButton to opt out (e.g. copy → copied patterns).
8
- */
9
- closeOnClick?: boolean;
10
5
  /**
11
6
  * Can be used for accessibility - the same id needs to be passed to the `aria-describedby` attribute of the element that the tooltip is describing.
12
7
  */
@@ -11,7 +11,6 @@ export const FloatingTip = ({
11
11
  alignment,
12
12
  avatar,
13
13
  children,
14
- closeOnClick,
15
14
  escapeKeyPressHandler,
16
15
  inheritDims,
17
16
  info,
@@ -102,19 +101,6 @@ export const FloatingTip = ({
102
101
  const isHoverType = type === 'tool' || type === 'preview';
103
102
  const isPreviewType = type === 'preview';
104
103
  const toolOnlyEventFunc = isHoverType ? e => handleShowHideAction(e) : undefined;
105
- const handleClick = useCallback(() => {
106
- if (hoverDelayRef.current) {
107
- clearTimeout(hoverDelayRef.current);
108
- hoverDelayRef.current = undefined;
109
- }
110
- if (focusDelayRef.current) {
111
- clearTimeout(focusDelayRef.current);
112
- focusDelayRef.current = undefined;
113
- }
114
- setIsOpen(false);
115
- setIsFocused(false);
116
- }, []);
117
- const clickHandler = closeOnClick && isHoverType ? handleClick : undefined;
118
104
  const contents = isPreviewType ? /*#__PURE__*/_jsx(PreviewTipContents, {
119
105
  avatar: avatar,
120
106
  info: info,
@@ -136,7 +122,6 @@ export const FloatingTip = ({
136
122
  ref: ref,
137
123
  width: inheritDims ? 'inherit' : undefined,
138
124
  onBlur: toolOnlyEventFunc,
139
- onClick: clickHandler,
140
125
  onFocus: toolOnlyEventFunc,
141
126
  onKeyDown: escapeKeyPressHandler,
142
127
  onMouseDown: e => e.preventDefault(),
@@ -1,4 +1,3 @@
1
- import { useCallback, useState } from 'react';
2
1
  import { InfoTipContainer } from '../InfoTip/styles';
3
2
  import { PreviewTipContents, PreviewTipShadow } from '../PreviewTip/elements';
4
3
  import { ToolTipContainer } from '../ToolTip/elements';
@@ -10,7 +9,6 @@ export const InlineTip = ({
10
9
  alignment,
11
10
  avatar,
12
11
  children,
13
- closeOnClick,
14
12
  escapeKeyPressHandler,
15
13
  id,
16
14
  inheritDims,
@@ -27,12 +25,6 @@ export const InlineTip = ({
27
25
  zIndex
28
26
  }) => {
29
27
  const isHoverType = type === 'tool' || type === 'preview';
30
- const [isSuppressed, setIsSuppressed] = useState(false);
31
- const handleClick = useCallback(() => {
32
- if (closeOnClick) setIsSuppressed(true);
33
- }, [closeOnClick]);
34
- const handleBlur = useCallback(() => setIsSuppressed(false), []);
35
- const handleMouseLeave = useCallback(() => setIsSuppressed(false), []);
36
28
  const InlineTipWrapper = isHoverType ? ToolTipWrapper : InfoTipWrapper;
37
29
  const InlineTipBodyWrapper = isHoverType ? ToolTipContainer : InfoTipContainer;
38
30
  const inlineWrapperProps = isHoverType ? {} : {
@@ -47,17 +39,10 @@ export const InlineTip = ({
47
39
  type
48
40
  });
49
41
  const isHorizontalCenter = tipBodyAlignment === 'horizontalCenter';
50
- const suppressedBodyStyle = isHoverType && isSuppressed ? {
51
- opacity: 0,
52
- visibility: 'hidden',
53
- transition: 'none'
54
- } : undefined;
55
42
  const target = /*#__PURE__*/_jsx(TargetContainer, {
56
43
  height: inheritDims ? 'inherit' : undefined,
57
44
  ref: wrapperRef,
58
45
  width: inheritDims ? 'inherit' : undefined,
59
- onBlur: isHoverType ? handleBlur : undefined,
60
- onClick: isHoverType ? handleClick : undefined,
61
46
  onKeyDown: escapeKeyPressHandler,
62
47
  children: children
63
48
  });
@@ -65,7 +50,6 @@ export const InlineTip = ({
65
50
  alignment: alignment,
66
51
  zIndex: zIndex ?? 1,
67
52
  ...inlineWrapperProps,
68
- style: suppressedBodyStyle,
69
53
  children: /*#__PURE__*/_jsx(TipBody, {
70
54
  alignment: tipBodyAlignment,
71
55
  "aria-hidden": isHoverType,
@@ -94,7 +78,6 @@ export const InlineTip = ({
94
78
  });
95
79
  return /*#__PURE__*/_jsx(InlineTipWrapper, {
96
80
  ...tipWrapperProps,
97
- onMouseLeave: isHoverType ? handleMouseLeave : undefined,
98
81
  children: alignment.includes('top') ? /*#__PURE__*/_jsxs(_Fragment, {
99
82
  children: [tipBody, target]
100
83
  }) : /*#__PURE__*/_jsxs(_Fragment, {
@@ -47,7 +47,6 @@ export type TipPlacementComponentProps = Omit<TipNewBaseProps, 'placement' | 'em
47
47
  id?: string;
48
48
  isTipHidden?: boolean;
49
49
  contentRef?: React.RefObject<HTMLDivElement> | ((node: HTMLDivElement | null) => void);
50
- closeOnClick?: boolean;
51
50
  type: 'info' | 'tool' | 'preview';
52
51
  wrapperRef?: React.RefObject<HTMLDivElement>;
53
52
  zIndex?: number;
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.c298d6.0",
4
+ "version": "72.2.3-alpha.e393cd.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.c298d6.0",
9
- "@codecademy/gamut-illustrations": "0.58.16-alpha.c298d6.0",
10
- "@codecademy/gamut-patterns": "0.10.35-alpha.c298d6.0",
11
- "@codecademy/gamut-styles": "20.0.3-alpha.c298d6.0",
12
- "@codecademy/variance": "0.26.2-alpha.c298d6.0",
8
+ "@codecademy/gamut-icons": "9.57.10-alpha.e393cd.0",
9
+ "@codecademy/gamut-illustrations": "0.58.16-alpha.e393cd.0",
10
+ "@codecademy/gamut-patterns": "0.10.35-alpha.e393cd.0",
11
+ "@codecademy/gamut-styles": "20.0.3-alpha.e393cd.0",
12
+ "@codecademy/variance": "0.26.2-alpha.e393cd.0",
13
13
  "@formatjs/intl-locale": "5.3.1",
14
14
  "@react-aria/interactions": "3.25.0",
15
15
  "@types/marked": "^4.0.8",