@@ -11,9 +11,12 @@ import {
1111import * as React from 'react' ;
1212import { Options as OptionsType , StylesConfig } from 'react-select' ;
1313
14- import { parseOptions } from '../utils' ;
14+ import { parseOptions , SelectOptionBase } from '../utils' ;
1515import {
16+ AbbreviatedSingleValue ,
1617 CustomContainer ,
18+ CustomInput ,
19+ CustomValueContainer ,
1720 DropdownButton ,
1821 formatGroupLabel ,
1922 formatOptionLabel ,
@@ -26,11 +29,15 @@ import {
2629 TypedReactSelect ,
2730} from './elements' ;
2831import { getMemoizedStyles } from './styles' ;
29- import { ExtendedOption , OptionStrict , SelectDropdownProps } from './types' ;
32+ import {
33+ OptionStrict ,
34+ SelectDropdownGroup ,
35+ SelectDropdownProps ,
36+ } from './types' ;
3037import {
3138 filterValueFromOptions ,
3239 isMultipleSelectProps ,
33- isOptionGroup ,
40+ isOptionsGrouped ,
3441 isSingleSelectProps ,
3542 removeValueFromSelectedOptions ,
3643} from './utils' ;
@@ -42,27 +49,79 @@ const defaultProps = {
4249 IndicatorSeparator : ( ) => null ,
4350 ClearIndicator : RemoveAllButton ,
4451 SelectContainer : CustomContainer ,
52+ ValueContainer : CustomValueContainer ,
4553 MultiValue : MultiValueWithColorMode ,
4654 MultiValueRemove : MultiValueRemoveButton ,
4755 Option : IconOption ,
56+ SingleValue : AbbreviatedSingleValue ,
57+ Input : CustomInput ,
4858 } ,
4959} ;
5060const onChangeAction = 'select-option' ;
5161
62+ /**
63+ * A flexible dropdown select component built on top of react-select.
64+ *
65+ * Supports both single and multi-select modes with customizable options including
66+ * icons, subtitles, right labels, and abbreviations. The component provides
67+ * accessibility features, keyboard navigation, and responsive styling.
68+ *
69+ * @example
70+ * ```tsx
71+ * // Basic single select
72+ * <SelectDropdown
73+ * name="country"
74+ * options={[
75+ * { label: 'United States', value: 'us' },
76+ * { label: 'Canada', value: 'ca' }
77+ * ]}
78+ * onChange={(option) => console.log(option)}
79+ * />
80+ *
81+ * // Multi-select with icons
82+ * <SelectDropdown
83+ * name="skills"
84+ * multiple
85+ * options={[
86+ * { label: 'React', value: 'react', icon: ReactIcon },
87+ * { label: 'TypeScript', value: 'ts', icon: TypeScriptIcon }
88+ * ]}
89+ * onChange={(options) => console.log(options)}
90+ * />
91+ *
92+ * // Grouped options with extended features
93+ * <SelectDropdown
94+ * name="category"
95+ * placeholder="Default placeholder text"
96+ * options={[
97+ * {
98+ * label: 'Frontend',
99+ * options: [
100+ * { label: 'React', value: 'react', subtitle: 'UI Library' },
101+ * { label: 'Vue', value: 'vue', subtitle: 'Progressive Framework' }
102+ * ]
103+ * }
104+ * ]}
105+ * />
106+ * ```
107+ */
52108export const SelectDropdown : React . FC < SelectDropdownProps > = ( {
53- options,
54- id,
55- size,
56- error,
57109 disabled,
58- onChange,
59- value,
60- name,
61- placeholder = 'Select an option' ,
110+ dropdownWidth,
111+ error,
112+ id,
62113 inputProps,
63- multiple ,
114+ inputWidth ,
64115 isSearchable = false ,
116+ menuAlignment = 'left' ,
117+ multiple,
118+ name,
119+ onChange,
120+ options,
121+ placeholder = 'Select an option' ,
65122 shownOptionsLimit = 6 ,
123+ size,
124+ value,
66125 ...rest
67126} ) => {
68127 const rawInputId = useId ( ) ;
@@ -75,40 +134,64 @@ export const SelectDropdown: React.FC<SelectDropdownProps> = ({
75134 const removeAllButtonRef = useRef < HTMLDivElement > ( null ) ;
76135 const selectInputRef = useRef < HTMLDivElement > ( null ) ;
77136
78- const optionsAreGrouped = useMemo ( ( ) => {
79- if ( options ?. length ) {
80- return ( options as any ) ?. some ( ( option : any ) => isOptionGroup ( option ) ) ;
137+ const selectOptions = useMemo ( ( ) :
138+ | SelectOptionBase [ ]
139+ | SelectDropdownGroup [ ] => {
140+ if (
141+ ! options ||
142+ ( Array . isArray ( options ) && ! options . length ) ||
143+ ( typeof options === 'object' &&
144+ ! Array . isArray ( options ) &&
145+ Object . keys ( options ) . length === 0 )
146+ ) {
147+ return [ ] ;
81148 }
82- return false ;
83- } , [ options ] ) ;
84149
85- const selectOptions = useMemo ( ( ) => {
86- return parseOptions ( { options : options as ExtendedOption [ ] , id, size } ) ;
150+ if ( isOptionsGrouped ( options ) ) {
151+ return options ;
152+ }
153+
154+ return parseOptions ( { options, id, size } ) ;
87155 } , [ options , id , size ] ) ;
88156
89- const parsedValue = useMemo (
90- ( ) => selectOptions . find ( ( option ) => option . value === value ) ,
91- [ selectOptions , value ]
92- ) ;
157+ const parsedValue = useMemo ( ( ) => {
158+ if ( isOptionsGrouped ( selectOptions ) ) {
159+ for ( const group of selectOptions ) {
160+ if ( group . options ) {
161+ const foundOption = group . options . find (
162+ ( option ) => option . value === value
163+ ) ;
164+ if ( foundOption ) return foundOption ;
165+ }
166+ }
167+ return undefined ;
168+ }
169+
170+ return selectOptions . find ( ( option ) => option . value === value ) ;
171+ } , [ selectOptions , value ] ) ;
93172
94173 const [ multiValues , setMultiValues ] = useState (
95174 multiple && // To keep this efficient for non-multiSelect
96- filterValueFromOptions ( selectOptions , value , optionsAreGrouped )
175+ filterValueFromOptions (
176+ selectOptions ,
177+ value ,
178+ isOptionsGrouped ( selectOptions )
179+ )
97180 ) ;
98181
99182 // If the caller changes the initial value, let's update our value to match.
100183 useEffect ( ( ) => {
101184 const newMultiValues = filterValueFromOptions (
102185 selectOptions ,
103186 value ,
104- optionsAreGrouped
187+ isOptionsGrouped ( selectOptions )
105188 ) ;
106189 if ( newMultiValues !== multiValues ) setMultiValues ( newMultiValues ) ;
107190
108- // For now, only handle the "change the value" case.
109- // Changing the options can be looked into when this component is fleshed out (GM-354)
191+ //
192+ // We only update this when our passed in options or value changes, not multiValues.
110193 // eslint-disable-next-line react-hooks/exhaustive-deps
111- } , [ value ] ) ;
194+ } , [ options , value ] ) ;
112195
113196 const changeHandler = useCallback (
114197 ( optionEvent : OptionStrict | OptionsType < OptionStrict > ) => {
@@ -179,16 +262,19 @@ export const SelectDropdown: React.FC<SelectDropdownProps> = ({
179262 ariaLiveMessages = { {
180263 onFocus,
181264 } }
265+ dropdownWidth = { dropdownWidth }
182266 error = { Boolean ( error ) }
183267 formatGroupLabel = { formatGroupLabel }
184268 formatOptionLabel = { formatOptionLabel }
185269 id = { id || rest . htmlFor || rawInputId }
186270 inputId = { inputId }
187- inputProps = { { ...inputProps , name } }
271+ inputProps = { { ...inputProps } }
272+ inputWidth = { inputWidth }
188273 isDisabled = { disabled }
189274 isMulti = { multiple }
190275 isOptionDisabled = { ( option ) => option . disabled }
191276 isSearchable = { isSearchable }
277+ menuAlignment = { menuAlignment }
192278 name = { name }
193279 options = { selectOptions }
194280 placeholder = { placeholder }
0 commit comments