Skip to content

Commit

Permalink
[EuiSearchBar] Allow disabling selection auto sort in `field_value_se…
Browse files Browse the repository at this point in the history
…lection` filters (#7958)

Co-authored-by: Cee Chen <[email protected]>
  • Loading branch information
tgalfin and cee-chen authored Aug 30, 2024
1 parent 23bef2b commit 383df3d
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 206 deletions.
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

0 comments on commit 383df3d

Please sign in to comment.