Skip to content
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

[EuiSearchBar] Allow disabling selection auto sort in field_value_selection filters #7958

Merged
merged 11 commits into from
Aug 30, 2024
1 change: 1 addition & 0 deletions packages/eui/changelogs/upcoming/7958.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Updated `EuiSearchBar`'s `field_value_selection` filter type with a new `autoSortOptions` config, allowing consumers to configure whether or not selected options are automatically sorted to the top of the filter list
45 changes: 26 additions & 19 deletions packages/eui/src-docs/src/views/search_bar/props_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ export const propsInfo = {
required: true,
type: { name: '#FieldValueOption[] | () => #FieldValueOption[]' },
},
available: {
description:
'A callback that defines whether this filter is currently available',
required: false,
type: { name: '() => boolean' },
},
filterWith: {
description:
'Specify how user input in the option dropdown will filter the available options.',
Expand All @@ -249,19 +255,12 @@ export const propsInfo = {
defaultValue: { value: 'true ("and")' },
type: { name: 'boolean | "or" | "and"' },
},
loadingMessage: {
description:
'The message that will be shown while loading the options',
required: false,
defaultValue: { value: 'Loading...' },
type: { name: 'string' },
},
noOptionsMessage: {
operator: {
description:
'The message that will be shown when no options are found',
'What operator should be used when adding selection to the search bar.',
required: false,
defaultValue: { value: 'No options found' },
type: { name: 'string' },
defaultValue: { value: 'eq' },
type: { name: 'eq | exact | gt | gte | lt | lte' },
},
searchThreshold: {
description:
Expand All @@ -271,25 +270,33 @@ export const propsInfo = {
defaultValue: { value: '10' },
type: { name: 'number' },
},
available: {
noOptionsMessage: {
description:
'A callback that defines whether this filter is currently available',
'The message that will be shown when no options are found',
required: false,
type: { name: '() => boolean' },
defaultValue: { value: 'No options found' },
type: { name: 'string' },
},
loadingMessage: {
description:
'The message that will be shown while loading the options',
required: false,
defaultValue: { value: 'Loading...' },
type: { name: 'string' },
},
autoClose: {
description:
'Should the dropdown close after the user selects a value. If not explicitly passed, will auto-close for single selection and remain open for multi-selection.',
'Whether the dropdown should close after the user selects a value. If not explicitly passed, will auto-close for single selection and remain open for multi-selection.',
required: false,
defaultValue: { value: 'true' },
type: { name: 'boolean' },
},
operator: {
autoSortOptions: {
description:
'What operator should be used when adding selection to the search bar.',
'Whether selected options (on and off) should be shown at the top of the filters list',
required: false,
defaultValue: { value: 'eq' },
type: { name: 'eq | exact | gt | gte | lt | lte' },
defaultValue: { value: 'true' },
type: { name: 'boolean' },
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export const SearchBarFilters = () => {
value: tag.name,
view: <EuiHealth color={tag.color}>{tag.name}</EuiHealth>,
})),
autoSortOptions: false,
},
{
type: 'custom_component',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { requiredProps } from '../../../test';
import {
FieldValueSelectionFilter,
FieldValueSelectionFilterProps,
FieldValueSelectionFilterConfigType,
} from './field_value_selection_filter';
import { Query } from '../query';

Expand All @@ -34,6 +35,29 @@ const staticOptions = [
];

describe('FieldValueSelectionFilter', () => {
const FieldValueSelectionFilterWithState = (
config: Partial<FieldValueSelectionFilterConfigType>
) => {
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 <FieldValueSelectionFilter {...props} />;
};

it('allows options as a function', () => {
const props: FieldValueSelectionFilterProps = {
...requiredProps,
Expand Down Expand Up @@ -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 <FieldValueSelectionFilter {...props} />;
};

it('uses multi-select OR', () => {
cy.mount(<FieldValueSelectionFilterWithState multiSelect="or" />);
cy.get('button').click();
Expand Down Expand Up @@ -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 <FieldValueSelectionFilter {...props} />;
};
const selectFilter = () => {
// Open popover
cy.get('button').click();
Expand Down Expand Up @@ -338,6 +310,37 @@ describe('FieldValueSelectionFilter', () => {
});
});

describe('autoSortOptions', () => {
const getOptions = () => cy.get('.euiSelectableListItem');

it('sorts selected options to the top by default', () => {
cy.mount(<FieldValueSelectionFilterWithState />);
cy.get('button').click();
getOptions().should('have.length', 3);

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', () => {
cy.mount(<FieldValueSelectionFilterWithState autoSortOptions={false} />);
cy.get('button').click();
getOptions().should('have.length', 3);

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');
});
});

it('has inactive filters, field is global', () => {
const props: FieldValueSelectionFilterProps = {
...requiredProps,
Expand Down Expand Up @@ -453,4 +456,66 @@ describe('FieldValueSelectionFilter', () => {
.eq(0)
.should('have.attr', 'title', 'Bug');
});

it('caches options if options is a function and config.cache is set', () => {
// Note: cy.clock()/cy.tick() doesn't currently work in Cypress component testing :T
// We should use that instead of cy.wait once https://github.com/cypress-io/cypress/issues/28846 is fixed
const props: FieldValueSelectionFilterProps = {
index: 0,
onChange: () => {},
query: Query.parse(''),
config: {
type: 'field_value_selection',
field: 'tag',
name: 'Tag',
cache: 5000, // Cache the loaded tags for 5 seconds
options: () =>
new Promise((resolve) => {
setTimeout(() => {
resolve(staticOptions);
}, 1000); // Spoof 1 second load time
}),
},
};
cy.spy(props.config, 'options');

const reducedTimeout = { timeout: 10 };
const assertIsLoading = (expected?: Function) => {
cy.get('.euiSelectableListItem', reducedTimeout).should('have.length', 0);
cy.get('[data-test-subj="euiSelectableMessage"]', reducedTimeout)
.should('have.text', 'Loading options')
.then(() => {
expected?.();
});
};
const assertIsLoaded = (expected?: Function) => {
cy.get('.euiSelectableListItem', reducedTimeout).should('have.length', 3);
cy.get('[data-test-subj="euiSelectableMessage"]', reducedTimeout)
.should('not.exist')
.then(() => {
expected?.();
});
};

cy.mount(<FieldValueSelectionFilter {...props} />);
cy.get('button').click();
assertIsLoading();

// Wait out the async options loader
cy.wait(1000);
assertIsLoaded(() => expect(props.config.options).to.be.calledOnce);

// Close and re-open the popover
cy.get('button').click();
cy.get('button').click();

// Cached options should immediately repopulate
assertIsLoaded(() => expect(props.config.options).to.be.calledOnce);

// Wait out the remainder of the cache, loading state should initiate again
cy.get('button').click();
cy.wait(5000);
cy.get('button').click();
assertIsLoading(() => expect(props.config.options).to.be.calledTwice);
});
});
Loading
Loading