Skip to content

feat: Add support for multiple selection to Select and Picker #8734

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

devongovett
Copy link
Member

@devongovett devongovett commented Aug 18, 2025

Closes #8738
This adds support for selecting multiple items to RAC Select and S2 Picker. By default, the selected items are concatenated into a comma separated list. Using RAC SelectValue's render props, you can customize this to whatever string you want (e.g. "2 selected items"). Behavior is TBD for Spectrum.

The API is changing from using selectedKey to using value. When multi-select is enabled, value accepts an array instead of a single id. This matches the native React DOM <select> API. The old API is supported for backward compatibility, but only applies to single selection.

Behaviorally, it uses the existing ListBox component which already supports multi-select. Typeahead and arrow key on the button while the select is closed is disabled when using multi-select, and the popover stays open after selection to facilitate selecting multiple items.

/** The textValue of the currently selected item. */
selectedText: string | null
/** The object values of the currently selected items. */
selectedItems: (T | null)[],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this to include null? The other option would be to filter out items that didn't set a value on the ListBoxItem. But then we'd need some other way of knowing the total selected item count at least.

@rspbot
Copy link

rspbot commented Aug 18, 2025

@rspbot
Copy link

rspbot commented Aug 18, 2025

@rspbot
Copy link

rspbot commented Aug 19, 2025

@rspbot
Copy link

rspbot commented Aug 19, 2025

## API Changes

react-aria-components

/react-aria-components:Select

-Select <T extends {} = {
+Select <M extends SelectionMode = 'single', T extends {} = {
   
 }> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoComplete?: string
   autoFocus?: boolean
   children?: ChildrenOrFunction<SelectRenderProps>
   className?: ClassNameOrFunction<SelectRenderProps>
   defaultOpen?: boolean
-  defaultSelectedKey?: Key
+  defaultValue?: ValueType<SelectionMode>
   disabledKeys?: Iterable<Key>
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isOpen?: boolean
   isRequired?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
+  onChange?: (T) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onOpenChange?: (boolean) => void
-  onSelectionChange?: (Key | null) => void
   placeholder?: string = 'Select an item' (localized)
-  selectedKey?: Key | null
+  selectionMode?: SelectionMode = 'single'
   slot?: string | null
   style?: StyleOrFunction<SelectRenderProps>
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'native' | 'aria' = 'native'
+  value?: ValueType<SelectionMode>
 }

/react-aria-components:SelectProps

-SelectProps <T extends {} = {
+SelectProps <M extends SelectionMode = 'single', T extends {} = {
   
 }> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoComplete?: string
   autoFocus?: boolean
   children?: ChildrenOrFunction<SelectRenderProps>
   className?: ClassNameOrFunction<SelectRenderProps>
   defaultOpen?: boolean
-  defaultSelectedKey?: Key
+  defaultValue?: ValueType<SelectionMode>
   disabledKeys?: Iterable<Key>
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isOpen?: boolean
   isRequired?: boolean
   name?: string
   onBlur?: (FocusEvent<Target>) => void
+  onChange?: (T) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onOpenChange?: (boolean) => void
-  onSelectionChange?: (Key | null) => void
   placeholder?: string = 'Select an item' (localized)
-  selectedKey?: Key | null
+  selectionMode?: SelectionMode = 'single'
   slot?: string | null
   style?: StyleOrFunction<SelectRenderProps>
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'native' | 'aria' = 'native'
+  value?: ValueType<SelectionMode>
 }

/react-aria-components:SelectValueRenderProps

 SelectValueRenderProps <T> {
   isPlaceholder: boolean
-  selectedItem: T | null
-  selectedText: string | null
+  selectedItems: Array<T | null>
+  selectedText: string
+  state: SelectState<T, 'single' | 'multiple'>
 }

/react-aria-components:SelectState

-SelectState <T> {
+SelectState <M extends SelectionMode = 'single', T> {
   close: () => void
   collection: Collection<Node<T>>
   commitValidation: () => void
-  defaultSelectedKey: Key | null
+  defaultValue: ValueType<SelectionMode>
   disabledKeys: Set<Key>
   displayValidation: ValidationResult
   focusStrategy: FocusStrategy | null
   isFocused: boolean
   isOpen: boolean
   open: (FocusStrategy | null) => void
   realtimeValidation: ValidationResult
   resetValidation: () => void
-  selectedItem: Node<T> | null
-  selectedKey: Key | null
+  selectedItems: Array<Node<T>>
   selectionManager: SelectionManager
   setFocused: (boolean) => void
   setOpen: (boolean) => void
-  setSelectedKey: (Key | null) => void
+  setValue: (Key | Array<Key> | null) => void
   toggle: (FocusStrategy | null) => void
   updateValidation: (ValidationResult) => void
+  value: ValueType<SelectionMode>
 }

@react-aria/select

/@react-aria/select:useSelect

-useSelect <T> {
+useSelect <M extends SelectionMode = 'single', T> {
-  props: AriaSelectOptions<T>
-  state: SelectState<T>
+  props: AriaSelectOptions<T, M>
+  state: SelectState<T, M>
   ref: RefObject<HTMLElement | null>
   returnVal: undefined
 }

/@react-aria/select:useHiddenSelect

-useHiddenSelect <T> {
+useHiddenSelect <M extends SelectionMode = 'single', T> {
   props: AriaHiddenSelectOptions
-  state: SelectState<T>
+  state: SelectState<T, M>
   triggerRef: RefObject<FocusableElement | null>
   returnVal: undefined
 }

/@react-aria/select:HiddenSelect

-HiddenSelect <T> {
+HiddenSelect <M extends SelectionMode = 'single', T> {
   autoComplete?: string
   form?: string
   isDisabled?: boolean
   label?: ReactNode
   name?: string
-  state: SelectState<T>
+  state: SelectState<T, SelectionMode>
   triggerRef: RefObject<FocusableElement | null>
 }

/@react-aria/select:AriaSelectOptions

-AriaSelectOptions <T> {
+AriaSelectOptions <M extends SelectionMode = 'single', T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoComplete?: string
   autoFocus?: boolean
   defaultOpen?: boolean
-  defaultSelectedKey?: Key
+  defaultValue?: ValueType<SelectionMode>
   description?: ReactNode
   disabledKeys?: Iterable<Key>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isOpen?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   keyboardDelegate?: KeyboardDelegate
   label?: ReactNode
   name?: string
   onBlur?: (FocusEvent<Target>) => void
+  onChange?: (T) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onOpenChange?: (boolean) => void
-  onSelectionChange?: (Key | null) => void
   placeholder?: string
-  selectedKey?: Key | null
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  selectionMode?: SelectionMode = 'single'
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'aria' | 'native' = 'aria'
+  value?: ValueType<SelectionMode>
 }

/@react-aria/select:SelectAria

-SelectAria <T> {
+SelectAria <M extends SelectionMode = 'single', T> {
   descriptionProps: DOMAttributes
   errorMessageProps: DOMAttributes
-  hiddenSelectProps: HiddenSelectProps<T>
+  hiddenSelectProps: HiddenSelectProps<T, SelectionMode>
   isInvalid: boolean
   labelProps: DOMAttributes
   menuProps: AriaListBoxOptions<T>
   triggerProps: AriaButtonProps
   validationErrors: Array<string>
   valueProps: DOMAttributes
 }

/@react-aria/select:HiddenSelectProps

-HiddenSelectProps <T> {
+HiddenSelectProps <M extends SelectionMode = 'single', T> {
   autoComplete?: string
   form?: string
   isDisabled?: boolean
   label?: ReactNode
   name?: string
-  state: SelectState<T>
+  state: SelectState<T, SelectionMode>
   triggerRef: RefObject<FocusableElement | null>
 }

/@react-aria/select:AriaSelectProps

-AriaSelectProps <T> {
+AriaSelectProps <M extends SelectionMode = 'single', T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoComplete?: string
   autoFocus?: boolean
   children: CollectionChildren<T>
   defaultOpen?: boolean
-  defaultSelectedKey?: Key
+  defaultValue?: ValueType<SelectionMode>
   description?: ReactNode
   disabledKeys?: Iterable<Key>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isOpen?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   label?: ReactNode
   name?: string
   onBlur?: (FocusEvent<Target>) => void
+  onChange?: (T) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onOpenChange?: (boolean) => void
-  onSelectionChange?: (Key | null) => void
   placeholder?: string
-  selectedKey?: Key | null
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  selectionMode?: SelectionMode = 'single'
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'aria' | 'native' = 'aria'
+  value?: ValueType<SelectionMode>
 }

@react-spectrum/picker

/@react-spectrum/picker:Picker

 Picker <T extends {}> {
   UNSAFE_className?: string
   UNSAFE_style?: CSSProperties
   align?: Alignment = 'start'
   alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoComplete?: string
   autoFocus?: boolean
   bottom?: Responsive<DimensionValue>
   children: CollectionChildren<{}>
   contextualHelp?: ReactNode
   defaultOpen?: boolean
   defaultSelectedKey?: Key
   description?: ReactNode
   direction?: 'bottom' | 'top' = 'bottom'
   disabledKeys?: Iterable<Key>
   end?: Responsive<DimensionValue>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   flex?: Responsive<string | number | boolean>
   flexBasis?: Responsive<number | string>
   flexGrow?: Responsive<number>
   flexShrink?: Responsive<number>
   form?: string
   gridArea?: Responsive<string>
   gridColumn?: Responsive<string>
   gridColumnEnd?: Responsive<string>
   gridColumnStart?: Responsive<string>
   gridRow?: Responsive<string>
   gridRowEnd?: Responsive<string>
   gridRowStart?: Responsive<string>
   height?: Responsive<DimensionValue>
   id?: string
   isDisabled?: boolean
   isHidden?: Responsive<boolean>
   isInvalid?: boolean
   isLoading?: boolean
   isOpen?: boolean
   isQuiet?: boolean
   isRequired?: boolean
   items?: Iterable<{}>
   justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   left?: Responsive<DimensionValue>
   margin?: Responsive<DimensionValue>
   marginBottom?: Responsive<DimensionValue>
   marginEnd?: Responsive<DimensionValue>
   marginStart?: Responsive<DimensionValue>
   marginTop?: Responsive<DimensionValue>
   marginX?: Responsive<DimensionValue>
   marginY?: Responsive<DimensionValue>
   maxHeight?: Responsive<DimensionValue>
   maxWidth?: Responsive<DimensionValue>
   menuWidth?: DimensionValue
   minHeight?: Responsive<DimensionValue>
   minWidth?: Responsive<DimensionValue>
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBlur?: (FocusEvent<Target>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onLoadMore?: () => any
   onOpenChange?: (boolean) => void
   onSelectionChange?: (Key | null) => void
   order?: Responsive<number>
   placeholder?: string
   position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
   right?: Responsive<DimensionValue>
   selectedKey?: Key | null
   shouldFlip?: boolean = true
   start?: Responsive<DimensionValue>
   top?: Responsive<DimensionValue>
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'aria' | 'native' = 'aria'
   width?: Responsive<DimensionValue>
   zIndex?: Responsive<number>
 }

/@react-spectrum/picker:SpectrumPickerProps

 SpectrumPickerProps <T> {
   UNSAFE_className?: string
   UNSAFE_style?: CSSProperties
   align?: Alignment = 'start'
   alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoComplete?: string
   autoFocus?: boolean
   bottom?: Responsive<DimensionValue>
   children: CollectionChildren<T>
   contextualHelp?: ReactNode
   defaultOpen?: boolean
   defaultSelectedKey?: Key
   description?: ReactNode
   direction?: 'bottom' | 'top' = 'bottom'
   disabledKeys?: Iterable<Key>
   end?: Responsive<DimensionValue>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   flex?: Responsive<string | number | boolean>
   flexBasis?: Responsive<number | string>
   flexGrow?: Responsive<number>
   flexShrink?: Responsive<number>
   form?: string
   gridArea?: Responsive<string>
   gridColumn?: Responsive<string>
   gridColumnEnd?: Responsive<string>
   gridColumnStart?: Responsive<string>
   gridRow?: Responsive<string>
   gridRowEnd?: Responsive<string>
   gridRowStart?: Responsive<string>
   height?: Responsive<DimensionValue>
   id?: string
   isDisabled?: boolean
   isHidden?: Responsive<boolean>
   isInvalid?: boolean
   isLoading?: boolean
   isOpen?: boolean
   isQuiet?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   left?: Responsive<DimensionValue>
   margin?: Responsive<DimensionValue>
   marginBottom?: Responsive<DimensionValue>
   marginEnd?: Responsive<DimensionValue>
   marginStart?: Responsive<DimensionValue>
   marginTop?: Responsive<DimensionValue>
   marginX?: Responsive<DimensionValue>
   marginY?: Responsive<DimensionValue>
   maxHeight?: Responsive<DimensionValue>
   maxWidth?: Responsive<DimensionValue>
   menuWidth?: DimensionValue
   minHeight?: Responsive<DimensionValue>
   minWidth?: Responsive<DimensionValue>
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBlur?: (FocusEvent<Target>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onLoadMore?: () => any
   onOpenChange?: (boolean) => void
   onSelectionChange?: (Key | null) => void
   order?: Responsive<number>
   placeholder?: string
   position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
   right?: Responsive<DimensionValue>
   selectedKey?: Key | null
   shouldFlip?: boolean = true
   start?: Responsive<DimensionValue>
   top?: Responsive<DimensionValue>
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'aria' | 'native' = 'aria'
   width?: Responsive<DimensionValue>
   zIndex?: Responsive<number>
 }

@react-spectrum/s2

/@react-spectrum/s2:Picker

-Picker <T extends {}> {
+Picker <M extends SelectionMode = 'single', T extends {}> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   align?: 'start' | 'end' = 'start'
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoComplete?: string
   autoFocus?: boolean
   children: ReactNode | ({}) => ReactNode
   contextualHelp?: ReactNode
   defaultOpen?: boolean
-  defaultSelectedKey?: Key
+  defaultValue?: ValueType<SelectionMode>
   dependencies?: ReadonlyArray<any>
   description?: ReactNode
   direction?: 'bottom' | 'top' = 'bottom'
   disabledKeys?: Iterable<Key>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isOpen?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   loadingState?: LoadingState
   menuWidth?: number
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBlur?: (FocusEvent<Target>) => void
+  onChange?: (T) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onLoadMore?: () => any
   onOpenChange?: (boolean) => void
-  onSelectionChange?: (Key | null) => void
   placeholder?: string = 'Select an item' (localized)
-  selectedKey?: Key | null
+  selectionMode?: SelectionMode = 'single'
   shouldFlip?: boolean = true
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesProp
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'native' | 'aria' = 'native'
+  value?: ValueType<SelectionMode>
 }

/@react-spectrum/s2:PickerProps

-PickerProps <T extends {}> {
+PickerProps <M extends SelectionMode = 'single', T extends {}> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   align?: 'start' | 'end' = 'start'
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoComplete?: string
   autoFocus?: boolean
   children: ReactNode | ({}) => ReactNode
   contextualHelp?: ReactNode
   defaultOpen?: boolean
-  defaultSelectedKey?: Key
+  defaultValue?: ValueType<SelectionMode>
   dependencies?: ReadonlyArray<any>
   description?: ReactNode
   direction?: 'bottom' | 'top' = 'bottom'
   disabledKeys?: Iterable<Key>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   excludeFromTabOrder?: boolean
   form?: string
   id?: string
   isDisabled?: boolean
   isInvalid?: boolean
   isOpen?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   label?: ReactNode
   labelAlign?: Alignment = 'start'
   labelPosition?: LabelPosition = 'top'
   loadingState?: LoadingState
   menuWidth?: number
   name?: string
   necessityIndicator?: NecessityIndicator = 'icon'
   onBlur?: (FocusEvent<Target>) => void
+  onChange?: (T) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onLoadMore?: () => any
   onOpenChange?: (boolean) => void
-  onSelectionChange?: (Key | null) => void
   placeholder?: string = 'Select an item' (localized)
-  selectedKey?: Key | null
+  selectionMode?: SelectionMode = 'single'
   shouldFlip?: boolean = true
   size?: 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesProp
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'native' | 'aria' = 'native'
+  value?: ValueType<SelectionMode>
 }

@react-stately/select

/@react-stately/select:useSelectState

-useSelectState <T extends {}> {
+useSelectState <M extends SelectionMode = 'single', T extends {}> {
-  props: SelectStateOptions<T>
+  props: SelectStateOptions<T, M>
   returnVal: undefined
 }

/@react-stately/select:SelectProps

-SelectProps <T> {
+SelectProps <M extends SelectionMode = 'single', T> {
   autoFocus?: boolean
   children: CollectionChildren<T>
   defaultOpen?: boolean
-  defaultSelectedKey?: Key
+  defaultValue?: ValueType<SelectionMode>
   description?: ReactNode
   disabledKeys?: Iterable<Key>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   isDisabled?: boolean
   isInvalid?: boolean
   isOpen?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   label?: ReactNode
   onBlur?: (FocusEvent<Target>) => void
+  onChange?: (T) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onOpenChange?: (boolean) => void
-  onSelectionChange?: (Key | null) => void
   placeholder?: string
-  selectedKey?: Key | null
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  selectionMode?: SelectionMode = 'single'
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'aria' | 'native' = 'aria'
+  value?: ValueType<SelectionMode>
 }

/@react-stately/select:SelectState

-SelectState <T> {
+SelectState <M extends SelectionMode = 'single', T> {
   close: () => void
   collection: Collection<Node<T>>
   commitValidation: () => void
-  defaultSelectedKey: Key | null
+  defaultValue: ValueType<SelectionMode>
   disabledKeys: Set<Key>
   displayValidation: ValidationResult
   focusStrategy: FocusStrategy | null
   isFocused: boolean
   isOpen: boolean
   open: (FocusStrategy | null) => void
   realtimeValidation: ValidationResult
   resetValidation: () => void
-  selectedItem: Node<T> | null
-  selectedKey: Key | null
+  selectedItems: Array<Node<T>>
   selectionManager: SelectionManager
   setFocused: (boolean) => void
   setOpen: (boolean) => void
-  setSelectedKey: (Key | null) => void
+  setValue: (Key | Array<Key> | null) => void
   toggle: (FocusStrategy | null) => void
   updateValidation: (ValidationResult) => void
+  value: ValueType<SelectionMode>
 }

/@react-stately/select:SelectStateOptions

-SelectStateOptions <T> {
+SelectStateOptions <M extends SelectionMode = 'single', T> {
   autoFocus?: boolean
   collection?: Collection<Node<T>>
   defaultOpen?: boolean
-  defaultSelectedKey?: Key
+  defaultValue?: ValueType<SelectionMode>
   description?: ReactNode
   disabledKeys?: Iterable<Key>
   errorMessage?: ReactNode | (ValidationResult) => ReactNode
   isDisabled?: boolean
   isInvalid?: boolean
   isOpen?: boolean
   isRequired?: boolean
   items?: Iterable<T>
   label?: ReactNode
   onBlur?: (FocusEvent<Target>) => void
+  onChange?: (T) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
   onKeyDown?: (KeyboardEvent) => void
   onKeyUp?: (KeyboardEvent) => void
   onOpenChange?: (boolean) => void
-  onSelectionChange?: (Key | null) => void
   placeholder?: string
-  selectedKey?: Key | null
-  validate?: (Key) => ValidationError | boolean | null | undefined
+  selectionMode?: SelectionMode = 'single'
+  validate?: (ValidationType<SelectionMode>) => ValidationError | boolean | null | undefined
   validationBehavior?: 'aria' | 'native' = 'aria'
+  value?: ValueType<SelectionMode>
 }

if (e.target.multiple) {
setValue(Array.from(
e.target.selectedOptions,
(option) => option.value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think currently we allow for Key which is string | number
but this would change it to string only right?

if (selectionMode === 'single') {
let key = keys.values().next().value ?? null;
setValue(key);
triggerState.close();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

combine work to keep open with #8733 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

react-aria-components: <SelectValue> breaks when not inside a <Button>
3 participants