Skip to content

Commit

Permalink
[EuiSelectable] Add a mixed state to EuiSelectableListItem that ren…
Browse files Browse the repository at this point in the history
…ders a new icon and SR-only text (#6774)

* Swapped icon assignment flow to use switch/case.

* Added logic for mixed option state. Added SR text.

* Updated a11y language for exclusions. Added snapshot and a11y tests.

* Added changelog and two updated snapshots.

* Updating docs for checked: 'mixed' value.

* Fixed linting errors in src-docs.

* Updated unit test snapshot.

* Update src/components/selectable/selectable_option.tsx

Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com>

* Update src/components/selectable/selectable_option.tsx

Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com>

* Refactoring allowExceptions logic to switch case.

* Updated tests, documentation to be more granular.

* Added note about aria-checked mixed with MDN links.

* Update upcoming_changelogs/6774.md

Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com>

* Refactor `aria-checked` to work for all eligible roles

+ `aria-disabled` and `aria-checked` is valid - do not return undefined if disabled

+ `aria-checked` mixed is invalid for certain roles - return false if so

* Update unit tests

- add describe block tests for `role` and `aria-checked`
- add describe blocks for `checked`
- switch to RTL render

* Massive screen reader text cleanup/refactor

- DRY out repeated i18n tokens/strings

- Make branching easier to follow (hopefully...)

- Fix some incorrect branching logic leading to missing SR text (namely non-exclusion mixed)

- Improve SR reading by using a period instead of a hyphen (at least for FF+VO)

- Add more unit tests to snapshot branching output

+ rename various vars to be slightly clearer

* [PR feedback] Docs

- Remove `allowExclusions` from example - while they work together, it's unlikely consumers will use both, and we should focus on just mixed options for this demo to isolate concerns

- Document when consumers would want to use this state

- Add button that allows resetting mixed state display

- Fix snippet

* [pr feedback] minor type cleanup

* fix snippet

* Escaped quotes for lint error.

* code editor valid quotes

---------

Co-authored-by: Cee Chen <549407+cee-chen@users.noreply.github.com>
Co-authored-by: Constance Chen <constance.chen.3@gmail.com>
  • Loading branch information
3 people authored May 18, 2023
1 parent cfb6d58 commit f8bc384
Show file tree
Hide file tree
Showing 13 changed files with 1,515 additions and 1,131 deletions.
58 changes: 49 additions & 9 deletions src-docs/src/views/selectable/selectable_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const selectableSingleSource = require('!!raw-loader!./selectable_single');
import SelectableExclusion from './selectable_exclusion';
const selectableExclusionSource = require('!!raw-loader!./selectable_exclusion');

import SelectableMixed from './selectable_mixed';
const selectableMixedSource = require('!!raw-loader!./selectable_mixed');

import SelectableMessages from './selectable_messages';
const selectableMessagesSource = require('!!raw-loader!./selectable_messages');

Expand Down Expand Up @@ -95,9 +98,10 @@ export const SelectableExample = {
>
<p>
<strong>EuiSelectable</strong> offers the ability to{' '}
<strong>exclude</strong> selections. Therefore, the{' '}
<EuiCode>checked</EuiCode> property is one of{' '}
<EuiCode>{"undefined | 'on' | 'off'"}</EuiCode>,{' '}
<strong>exclude</strong> selections or{' '}
<strong>include selections for some</strong> (mixed). Therefore,
the <EuiCode>checked</EuiCode> property is one of{' '}
<EuiCode>{"undefined | 'on' | 'off' | 'mixed'"}</EuiCode>,{' '}
<EuiCode>{"'on'"}</EuiCode> being the default for selected options
when <EuiCode>allowExclusions = false</EuiCode>.
</p>
Expand Down Expand Up @@ -217,12 +221,10 @@ export const SelectableExample = {
},
],
text: (
<>
<p>
Currently, adding <EuiCode>allowExclusions</EuiCode> simply allows
cycling through the checked options (on {'-> off ->'} undefined).
</p>
</>
<p>
Adding <EuiCode>allowExclusions</EuiCode> allows cycling through the
checked options (on {'-> off ->'} undefined).
</p>
),
props,
demo: <SelectableExclusion />,
Expand All @@ -235,6 +237,44 @@ export const SelectableExample = {
{list => list}
</EuiSelectable>`,
},

{
title: 'Options can be mixed (indeterminate)',
source: [
{
type: GuideSectionTypes.TSX,
code: selectableMixedSource,
},
],
text: (
<>
<p>
Setting an option to <EuiCode>checked: &ldquo;mixed&rdquo;</EuiCode>{' '}
allows showing an indeterminate/mixed state. This state can only be
set by the consuming application, and should typically be used to
show that another state being controlled by the{' '}
<strong>EuiSelectable</strong> has some, but not all, items
selected.
</p>
<p>
When clicking a mixed option, the option will cycle to
&quot;on&quot;, and after that cycle between {'on -> off'} (if{' '}
<EuiCode>allowExclusions</EuiCode> is true) {'-> undefined'}). Users
cannot manually cycle back to mixed.
</p>
</>
),
props,
demo: <SelectableMixed />,
snippet: `<EuiSelectable
aria-label="Example supporting mixed (indeterminate) options"
options={[{ label: '', checked: 'mixed' }]}
onChange={newOptions => setOptions(newOptions)}
>
{list => list}
</EuiSelectable>`,
},

{
title: 'Messages and loading',
source: [
Expand Down
4 changes: 3 additions & 1 deletion src-docs/src/views/selectable/selectable_exclusion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ export default () => {
},
{
label: 'Dione',
checked: 'off',
},
{
label: 'Iapetus',
checked: 'on',
checked: 'off',
},
{
label: 'Phoebe',
checked: 'on',
},
{
label: 'Rhea',
Expand Down
68 changes: 68 additions & 0 deletions src-docs/src/views/selectable/selectable_mixed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useState } from 'react';

import {
EuiSelectable,
EuiSelectableOption,
EuiButton,
EuiSpacer,
} from '../../../../src';

const initialOptions: EuiSelectableOption[] = [
{
label: 'Titan',
'data-test-subj': 'titanOption',
checked: 'mixed',
},
{
label: 'Enceladus is disabled',
disabled: true,
},
{
label: 'Mimas',
checked: 'on',
},
{
label: 'Dione',
checked: 'mixed',
},
{
label: 'Iapetus',
},
{
label: 'Phoebe',
checked: 'mixed',
},
{
label: 'Rhea',
},
{
label:
"Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
},
{
label: 'Tethys',
},
{
label: 'Hyperion',
},
];

export default () => {
const [options, setOptions] = useState(initialOptions);

return (
<>
<EuiSelectable
aria-label="Example of Selectable supporting mixed state"
options={options}
onChange={(newOptions) => setOptions(newOptions)}
>
{(list) => list}
</EuiSelectable>
<EuiSpacer size="s" />
<EuiButton onClick={() => setOptions(initialOptions)}>
Reset mixed options
</EuiButton>
</>
);
};
4 changes: 2 additions & 2 deletions src/components/filter_group/filter_group.a11y.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,15 +257,15 @@ describe('EuiFilterGroup multiselect example', () => {
.find('span.euiSelectableListItem__text')
.should(
'have.text',
'Dmitri Shostakovich - Selected option. To exclude this option, press enter.'
'Dmitri Shostakovich - Checked option. To exclude this option, press Enter.'
);
cy.realPress('ArrowDown');
cy.repeatRealPress('Enter');
cy.get('li[aria-selected="true"]')
.find('span.euiSelectableListItem__text')
.should(
'have.text',
'Felix Mendelssohn-Bartholdy - Excluded option. To uncheck this option, press enter.'
'Felix Mendelssohn-Bartholdy - Excluded option. To uncheck this option, press Enter.'
);
cy.checkAxe();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ExclusiveUnion } from '../../common';
import { EuiPopover, EuiPopoverTitle } from '../../popover';
import { EuiFilterButton } from '../../filter_group';
import { EuiSelectable, EuiSelectableProps } from '../../selectable';
import { EuiSelectableOptionCheckedType } from '../../../components/selectable/selectable_option';
import { Query } from '../query';
import { Clause, Operator, OperatorType, Value } from '../query/ast';

Expand Down Expand Up @@ -246,7 +247,7 @@ export class FieldValueSelectionFilter extends Component<
onOptionClick(
field: string,
value: Value,
checked: 'on' | 'off' | undefined
checked?: Omit<EuiSelectableOptionCheckedType, 'mixed'>
) {
const multiSelect = this.resolveMultiSelect();
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ exports[`EuiSelectable custom options with data 1`] = `
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem euiSelectableListItem--paddingSmall"
data-test-selected="false"
id="generated-id_listbox_option-0"
role="option"
style="position:absolute;left:0;top:0;height:32px;width:100%"
Expand Down Expand Up @@ -52,7 +51,6 @@ exports[`EuiSelectable custom options with data 1`] = `
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem euiSelectableListItem--paddingSmall"
data-test-selected="false"
id="generated-id_listbox_option-1"
role="option"
style="position:absolute;left:0;top:32px;height:32px;width:100%"
Expand Down Expand Up @@ -80,7 +78,6 @@ exports[`EuiSelectable custom options with data 1`] = `
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem euiSelectableListItem--paddingSmall"
data-test-selected="false"
id="generated-id_listbox_option-2"
role="option"
style="position:absolute;left:0;top:64px;height:32px;width:100%"
Expand Down Expand Up @@ -149,7 +146,6 @@ exports[`EuiSelectable errorMessage prop does not render the message when not de
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem euiSelectableListItem--paddingSmall"
data-test-selected="false"
data-test-subj="titanOption"
id="generated-id_listbox_option-0"
role="option"
Expand Down Expand Up @@ -178,7 +174,6 @@ exports[`EuiSelectable errorMessage prop does not render the message when not de
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem euiSelectableListItem--paddingSmall"
data-test-selected="false"
id="generated-id_listbox_option-1"
role="option"
style="position:absolute;left:0;top:32px;height:32px;width:100%"
Expand Down Expand Up @@ -206,7 +201,6 @@ exports[`EuiSelectable errorMessage prop does not render the message when not de
aria-selected="false"
aria-setsize="3"
class="euiSelectableListItem euiSelectableListItem--paddingSmall"
data-test-selected="false"
id="generated-id_listbox_option-2"
role="option"
style="position:absolute;left:0;top:64px;height:32px;width:100%"
Expand Down
82 changes: 56 additions & 26 deletions src/components/selectable/selectable.a11y.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,37 @@ const options: EuiSelectableProps['options'] = [
},
];

const EuiSelectableListboxOnly = (args: Partial<EuiSelectableProps>) => {
return (
<EuiSelectable options={options} {...args}>
{(list) => <>{list}</>}
</EuiSelectable>
);
};

const EuiSelectableWithSearchInput = (args: Partial<EuiSelectableProps>) => {
return (
<EuiSelectable searchable options={options} {...args}>
{(list, search) => (
<>
{search}
{list}
</>
)}
</EuiSelectable>
);
};
const excludedOptions: EuiSelectableProps['options'] = [
{
label: 'Titan',
'data-test-subj': 'titanOption',
checked: 'on',
},
{
label: 'Enceladus',
checked: 'off',
},
{
label:
"Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
checked: 'mixed',
},
];

describe('EuiSelectable', () => {
describe('with a `searchable` configuration', () => {
it('has no accessibility errors', () => {
const onChange = cy.stub();
cy.realMount(<EuiSelectableWithSearchInput onChange={onChange} />);
cy.realMount(
<EuiSelectable options={options} onChange={onChange} searchable>
{(list, search) => (
<>
{search}
{list}
</>
)}
</EuiSelectable>
);
cy.checkAxe();
});
});
Expand All @@ -64,10 +69,29 @@ describe('EuiSelectable', () => {
it('has no accessibility errors', () => {
const onChange = cy.stub();
cy.realMount(
<EuiSelectableListboxOnly
<EuiSelectable
aria-label="No search box"
options={options}
onChange={onChange}
/>
>
{(list) => <>{list}</>}
</EuiSelectable>
);
cy.checkAxe();
});
});

describe('with excluded and mixed options configuration', () => {
it('has no accessibility errors', () => {
const onChange = cy.stub();
cy.realMount(
<EuiSelectable
aria-label="Excluded and mixed options"
options={excludedOptions}
onChange={onChange}
>
{(list) => <>{list}</>}
</EuiSelectable>
);
cy.checkAxe();
});
Expand Down Expand Up @@ -101,13 +125,19 @@ describe('EuiSelectable', () => {
isOpen={isPopoverOpen}
closePopover={onClosePopover}
>
<EuiSelectableWithSearchInput
<EuiSelectable
aria-label="With popover"
options={options}
onChange={onChange}
searchable
>
{(list) => <>{list}</>}
</EuiSelectableWithSearchInput>
{(list, search) => (
<>
{search}
{list}
</>
)}
</EuiSelectable>
</EuiPopover>
);
};
Expand Down
Loading

0 comments on commit f8bc384

Please sign in to comment.