(
{ /* Placeholder */ }
{ ( multiple
- ? ! ( getValues() as SelectOptionValue[] )?.length
+ ? ! toValuesArray( getValues() ).length
: ! getValues() ) && (
{
+ return toValuesArray( currentValue ).some( ( val ) => {
if ( val !== null && value !== null && typeof val === 'object' ) {
return (
( val as Record )[ by ] ===
@@ -1056,7 +1063,7 @@ export function SelectItem( {
}
return val === value;
} );
- }, [ value, getValues ] );
+ }, [ value, getValues, multiple, by ] );
const isChecked = useMemo( () => {
if ( typeof selected === 'boolean' ) {
@@ -1068,7 +1075,7 @@ export function SelectItem( {
}
return indx === selectedIndex;
- }, [ multipleChecked, selectedIndex, selected ] );
+ }, [ multipleChecked, selectedIndex, selected, multiple, indx ] );
let itemTabIndex: number | undefined;
if ( ! inlineSearch ) {
@@ -1234,9 +1241,10 @@ const SelectComponent = ( {
] );
const handleMultiSelect: OnClick = ( index, newValue ) => {
- const selectedValues = [
- ...( ( getValues() as SelectOptionValue[] ) ?? [] ),
- ];
+ // getValues() can return a single value here — e.g. `multiple` flipped
+ // to true while a single selection was already made — so normalize
+ // before treating it as a list.
+ const selectedValues = [ ...toValuesArray( getValues() ) ];
const valueIndex = selectedValues.findIndex( ( selectedValue ) => {
if (
selectedValue !== null &&
@@ -1251,20 +1259,31 @@ const SelectComponent = ( {
return selectedValue === newValue;
} );
+ // Toggle: deselect if already selected, otherwise select. The
+ // dropdown stays open so multiple options can be picked in one go;
+ // Escape and outside click (useDismiss) still close it.
if ( valueIndex !== -1 ) {
- return;
+ selectedValues.splice( valueIndex, 1 );
+ if ( selectedIndex === index ) {
+ setSelectedIndex( null );
+ }
+ } else {
+ selectedValues.push( newValue );
+ setSelectedIndex( index );
}
- selectedValues.push( newValue );
if ( ! isControlled ) {
setSelected( selectedValues );
}
- setSelectedIndex( index );
- (
- ( refs.domReference.current ??
- refs.reference.current ) as HTMLElement | null
- )?.focus();
- setIsOpen( false );
+ // inlineSearch options carry no tabIndex and render without a
+ // FloatingFocusManager, so a click would drop focus to document.body
+ // and kill type-to-filter, arrow nav and Backspace-removes-badge.
+ if ( inlineSearch ) {
+ (
+ ( refs.domReference.current ??
+ refs.reference.current ) as HTMLElement | null
+ )?.focus();
+ }
setSearchKeyword( '' );
if ( typeof onChange === 'function' ) {
onChange( selectedValues );
diff --git a/src/components/select/utils.ts b/src/components/select/utils.ts
index 2b18d816..639aa844 100644
--- a/src/components/select/utils.ts
+++ b/src/components/select/utils.ts
@@ -1,4 +1,29 @@
import { type ReactNode, isValidElement } from 'react';
+import type { SelectOptionValue } from './select-types';
+
+/**
+ * Normalize a select value into an array of selected values.
+ *
+ * Multi-select code paths assume an array, but the value can be a single
+ * value when `multiple` is toggled at runtime or when a single value is
+ * passed while `multiple` is true. Wrapping instead of assuming keeps those
+ * paths from throwing on non-iterable values.
+ *
+ * @param {SelectOptionValue | SelectOptionValue[] | null | undefined} value - Current select value.
+ * @return {SelectOptionValue[]} Array of selected values.
+ * @since x.x.x
+ */
+export const toValuesArray = (
+ value: SelectOptionValue | SelectOptionValue[] | null | undefined
+): SelectOptionValue[] => {
+ if ( Array.isArray( value ) ) {
+ return value;
+ }
+ if ( value === null || typeof value === 'undefined' || value === '' ) {
+ return [];
+ }
+ return [ value ];
+};
/**
* Get text content of a node
diff --git a/version.json b/version.json
index 849418d2..b4050a71 100644
--- a/version.json
+++ b/version.json
@@ -1,3 +1,3 @@
{
- "force-ui": "1.8.0"
+ "force-ui": "1.8.1"
}