diff --git a/packages/eui/src/components/search_bar/filters/field_value_selection_filter.spec.tsx b/packages/eui/src/components/search_bar/filters/field_value_selection_filter.spec.tsx index ee42c9ef8fc..7f1f3464bea 100644 --- a/packages/eui/src/components/search_bar/filters/field_value_selection_filter.spec.tsx +++ b/packages/eui/src/components/search_bar/filters/field_value_selection_filter.spec.tsx @@ -15,6 +15,7 @@ import { requiredProps } from '../../../test'; import { FieldValueSelectionFilter, FieldValueSelectionFilterProps, + FieldValueSelectionFilterConfigType, } from './field_value_selection_filter'; import { Query } from '../query'; @@ -34,6 +35,29 @@ const staticOptions = [ ]; describe('FieldValueSelectionFilter', () => { + const FieldValueSelectionFilterWithState = ( + config: Partial + ) => { + const [query, setQuery] = useState(Query.parse('')); + const onChange = (newQuery: Query) => setQuery(newQuery); + + const props: FieldValueSelectionFilterProps = { + ...requiredProps, + index: 0, + onChange, + query, + config: { + type: 'field_value_selection', + field: 'tag', + name: 'Tag', + options: staticOptions, + ...config, + }, + }; + + return ; + }; + it('allows options as a function', () => { const props: FieldValueSelectionFilterProps = { ...requiredProps, @@ -140,31 +164,6 @@ describe('FieldValueSelectionFilter', () => { }); describe('multi-select testing', () => { - const FieldValueSelectionFilterWithState = ({ - multiSelect, - }: { - multiSelect: 'or' | boolean; - }) => { - const [query, setQuery] = useState(Query.parse('')); - const onChange = (newQuery: Query) => setQuery(newQuery); - - const props: FieldValueSelectionFilterProps = { - ...requiredProps, - index: 0, - onChange, - query, - config: { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - multiSelect, - options: staticOptions, - }, - }; - - return ; - }; - it('uses multi-select OR', () => { cy.mount(); cy.get('button').click(); @@ -226,33 +225,6 @@ describe('FieldValueSelectionFilter', () => { }); describe('auto-close testing', () => { - const FieldValueSelectionFilterWithState = ({ - autoClose, - multiSelect, - }: { - autoClose: undefined | boolean; - multiSelect: 'or' | boolean; - }) => { - const [query, setQuery] = useState(Query.parse('')); - const onChange = (newQuery: Query) => setQuery(newQuery); - - const props: FieldValueSelectionFilterProps = { - ...requiredProps, - index: 0, - onChange, - query, - config: { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - multiSelect, - autoClose, - options: staticOptions, - }, - }; - - return ; - }; const selectFilter = () => { // Open popover cy.get('button').click(); @@ -339,51 +311,33 @@ describe('FieldValueSelectionFilter', () => { }); describe('autoSortOptions', () => { - it('sorts selected options to the top by default', () => { - const props: FieldValueSelectionFilterProps = { - index: 0, - onChange: () => {}, - query: Query.parse('tag:bug'), - config: { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - options: staticOptions, - }, - }; - cy.mount(); - cy.get('.euiNotificationBadge').should('exist'); + const getOptions = () => cy.get('.euiSelectableListItem'); + it('sorts selected options to the top by default', () => { + cy.mount(); cy.get('button').click(); - const getOptions = () => cy.get('.euiSelectableListItem'); - getOptions().should('have.length', 3); - getOptions().eq(0).should('have.attr', 'title', 'Bug'); - getOptions().eq(1).should('have.attr', 'title', 'feature'); + + getOptions().last().should('have.attr', 'title', 'Bug').click(); + // Should have moved to the top of the list and retained active focus + getOptions() + .first() + .should('have.attr', 'title', 'Bug') + .should('have.attr', 'aria-checked', 'true') + .should('have.attr', 'aria-selected', 'true'); }); it('does not sort selected options to the top when set to false', () => { - const props: FieldValueSelectionFilterProps = { - index: 0, - onChange: () => {}, - query: Query.parse('tag:bug'), - config: { - type: 'field_value_selection', - field: 'tag', - name: 'Tag', - options: staticOptions, - autoSortOptions: false, - }, - }; - cy.mount(); - cy.get('.euiNotificationBadge').should('exist'); - + cy.mount(); cy.get('button').click(); - const getOptions = () => cy.get('.euiSelectableListItem'); - getOptions().should('have.length', 3); - getOptions().eq(2).should('have.attr', 'title', 'Bug'); - getOptions().eq(0).should('have.attr', 'title', 'feature'); + + getOptions().last().should('have.attr', 'title', 'Bug').click(); + getOptions() + .last() + .should('have.attr', 'title', 'Bug') + .should('have.attr', 'aria-checked', 'true') + .should('have.attr', 'aria-selected', 'true'); }); }); diff --git a/packages/eui/src/components/search_bar/filters/field_value_selection_filter.tsx b/packages/eui/src/components/search_bar/filters/field_value_selection_filter.tsx index da21b2041ce..82351e67d99 100644 --- a/packages/eui/src/components/search_bar/filters/field_value_selection_filter.tsx +++ b/packages/eui/src/components/search_bar/filters/field_value_selection_filter.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { Component, ReactNode } from 'react'; +import React, { Component, ReactNode, createRef } from 'react'; import { RenderWithEuiTheme } from '../../../services'; import { isArray, isNil } from '../../../services/predicate'; @@ -81,12 +81,14 @@ interface State { } | null; cachedOptions?: FieldValueOptionType[] | null; activeItemsCount: number; + lastCheckedValue?: Value; } export class FieldValueSelectionFilter extends Component< FieldValueSelectionFilterProps, State > { + selectableClassRef = createRef(); cacheTimeout: ReturnType | undefined; constructor(props: FieldValueSelectionFilterProps) { @@ -181,14 +183,35 @@ export class FieldValueSelectionFilter extends Component< }); } - this.setState({ - error: null, - activeItemsCount: items.on.length, - options: { - unsorted: loadedOptions, - sorted: [...items.on, ...items.off, ...items.rest], + this.setState( + { + error: null, + activeItemsCount: items.on.length, + options: { + unsorted: loadedOptions, + sorted: [...items.on, ...items.off, ...items.rest], + }, }, - }); + this.scrollToAutoSortedOption + ); + }; + + scrollToAutoSortedOption = () => { + if (!this.autoSortOptions) return; + + const { lastCheckedValue, options } = this.state; + if (lastCheckedValue) { + const sortedIndex = options!.sorted.findIndex( + (option) => option.value === lastCheckedValue + ); + if (sortedIndex >= 0) { + // EuiSelectable should automatically handle scrolling its list to the new + this.selectableClassRef.current?.setState({ + activeOptionIndex: sortedIndex, + }); + } + this.setState({ lastCheckedValue: undefined }); + } }; resolveOptionName(option: FieldValueOptionType) { @@ -204,6 +227,10 @@ export class FieldValueSelectionFilter extends Component< config: { autoClose, operator = Operator.EQ }, } = this.props; + if (checked && this.autoSortOptions) { + this.setState({ lastCheckedValue: value }); + } + // If the consumer explicitly sets `autoClose`, always defer to that. // Otherwise, default to auto-closing for single selections and leaving the // popover open for multi-select (so users can continue selecting options) @@ -351,6 +378,7 @@ export class FieldValueSelectionFilter extends Component< }} > > + ref={this.selectableClassRef} singleSelection={!this.multiSelect} aria-label={config.name} options={items}