From c9e622e74ff83995e6fa4d03325c7de7a8707510 Mon Sep 17 00:00:00 2001 From: Lene Gadewoll Date: Fri, 27 Jun 2025 20:43:48 +0200 Subject: [PATCH 1/6] refactor(combobox): support custom option ids - this ensures that consumers can pass custom ids on options without breaking the internal accessibility handling of combobox --- .../src/components/combo_box/combo_box.tsx | 25 ++++++++++++++++--- .../combo_box_options_list.tsx | 4 +++ .../filter_group/filter_select_item.tsx | 9 ++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/eui/src/components/combo_box/combo_box.tsx b/packages/eui/src/components/combo_box/combo_box.tsx index 6e923b9f8e9..0b7815368fa 100644 --- a/packages/eui/src/components/combo_box/combo_box.tsx +++ b/packages/eui/src/components/combo_box/combo_box.tsx @@ -212,6 +212,7 @@ interface EuiComboBoxState { hasFocus: boolean; isListOpen: boolean; matchingOptions: Array>; + listOptions: Array; searchValue: string; } @@ -249,6 +250,7 @@ export class EuiComboBox extends Component< showPrevSelected: Boolean(this.props.singleSelection), sortMatchesBy: this.props.sortMatchesBy, }), + listOptions: [], searchValue: initialSearchValue, }; @@ -271,6 +273,17 @@ export class EuiComboBox extends Component< this.listRefInstance = ref; }; + setListOptions = (node: HTMLButtonElement | null, index: number) => { + this.setState(({ listOptions }) => { + const _listOptions = listOptions; + _listOptions[index] = node; + + return { + listOptions: _listOptions, + }; + }); + }; + openList = () => { this.setState({ isListOpen: true, @@ -604,9 +617,10 @@ export class EuiComboBox extends Component< if (singleSelection) { requestAnimationFrame(() => this.closeList()); } else { - this.setState({ - activeOptionIndex: this.state.matchingOptions.indexOf(addedOption), - }); + this.setState(({ listOptions, matchingOptions }) => ({ + listOptions: listOptions.slice(0, matchingOptions.length - 1), + activeOptionIndex: matchingOptions.indexOf(addedOption), + })); } }; @@ -809,6 +823,7 @@ export class EuiComboBox extends Component< isCaseSensitive={isCaseSensitive} isLoading={isLoading} listRef={this.listRefCallback} + setListOptions={this.setListOptions} matchingOptions={matchingOptions} onCloseList={this.closeList} onCreateOption={onCreateOption} @@ -876,7 +891,9 @@ export class EuiComboBox extends Component< compressed={compressed} focusedOptionId={ this.hasActiveOption() - ? this.rootId(`_option-${this.state.activeOptionIndex}`) + ? this.state.listOptions[this.state.activeOptionIndex] + ?.id ?? + this.rootId(`_option-${this.state.activeOptionIndex}`) : undefined } fullWidth={fullWidth} diff --git a/packages/eui/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/packages/eui/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx index 3b80aae71a6..46b5cdb94e4 100644 --- a/packages/eui/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx +++ b/packages/eui/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx @@ -65,6 +65,7 @@ export type EuiComboBoxOptionsListProps = CommonProps & { isCaseSensitive?: boolean; isLoading?: boolean; listRef: RefCallback; + setListOptions: (ref: HTMLButtonElement | null, index: number) => void; matchingOptions: Array>; onCloseList: (event: Event) => void; onCreateOption?: ( @@ -168,6 +169,7 @@ export class EuiComboBoxOptionsList extends Component< searchValue, rootId, matchingOptions, + setListOptions, } = this.props; const optionIndex = matchingOptions.indexOf(option); @@ -220,6 +222,7 @@ export class EuiComboBoxOptionsList extends Component< title={label} aria-setsize={matchingOptions.length} aria-posinset={optionIndex + 1} + forwardRef={(ref) => setListOptions(ref, index)} {...rest} > @@ -337,6 +340,7 @@ export class EuiComboBoxOptionsList extends Component< delimiter, truncationProps, listboxAriaLabel, + setListOptions, ...rest } = this.props; diff --git a/packages/eui/src/components/filter_group/filter_select_item.tsx b/packages/eui/src/components/filter_group/filter_select_item.tsx index b7901a8c54a..a43df33777a 100644 --- a/packages/eui/src/components/filter_group/filter_select_item.tsx +++ b/packages/eui/src/components/filter_group/filter_select_item.tsx @@ -28,6 +28,7 @@ export interface EuiFilterSelectItemProps isFocused?: boolean; toolTipContent?: EuiComboBoxOptionOption['toolTipContent']; toolTipProps?: EuiComboBoxOptionOption['toolTipProps']; + forwardRef?: (ref: HTMLButtonElement | null) => void; } const resolveIconAndColor = (checked?: FilterChecked) => { @@ -65,6 +66,11 @@ export class EuiFilterSelectItemClass extends Component< hasFocus: false, }; + setButtonRef = (node: HTMLButtonElement | null) => { + this.buttonRef = node; + this.props.forwardRef?.(node); + }; + focus = () => { if (this.buttonRef) { this.buttonRef.focus(); @@ -95,6 +101,7 @@ export class EuiFilterSelectItemClass extends Component< toolTipContent, toolTipProps, style, + forwardRef, ...rest } = this.props; @@ -140,7 +147,7 @@ export class EuiFilterSelectItemClass extends Component< const optionItem = (