Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src-docs/src/views/selectable/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export const Options: EuiSelectableOption[] = [
},
{
label: 'Dione',
id: 'id_dione',
},
{
label: 'Iapetus',
Expand Down
1 change: 1 addition & 0 deletions src-docs/src/views/selectable/selectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default () => {

return (
<EuiSelectable
aria-label="Basic example"
options={options}
listProps={{ bordered: true }}
onChange={newOptions => setOptions(newOptions)}>
Expand Down
1 change: 1 addition & 0 deletions src-docs/src/views/selectable/selectable_custom_render.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default class extends Component {
<EuiSpacer />

<EuiSelectable
aria-label="Selectable example with custom list items"
searchable
options={options}
onChange={this.onChange}
Expand Down
16 changes: 10 additions & 6 deletions src-docs/src/views/selectable/selectable_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const SelectableExample = {
},
demo: <Selectable />,
snippet: `<EuiSelectable
aria-label="Basic example"
options={[{ label: '' }, { label: '' }]}
onChange={() => this.onChange(options)}
listProps={{ bordered: true }}>
Expand Down Expand Up @@ -141,6 +142,7 @@ export const SelectableExample = {
props: { EuiSelectable },
demo: <SelectableSearch />,
snippet: `<EuiSelectable
aria-label="Searchable example"
searchable
searchProps={{
'data-test-subj': dataTestSubj,
Expand Down Expand Up @@ -183,12 +185,13 @@ export const SelectableExample = {
demo: <SelectableSingle />,
snippet: `
<EuiSelectable
options={options}
onChange={this.onChange}
singleSelection={true}
listProps={{ bordered: true }}>
{list => list}
</EuiSelectable>
aria-label="Single selection example"
options={options}
onChange={this.onChange}
singleSelection={true}
listProps={{ bordered: true }}>
{list => list}
</EuiSelectable>
`,
},
{
Expand Down Expand Up @@ -255,6 +258,7 @@ export const SelectableExample = {
props: { EuiSelectable },
demo: <SelectableExclusion />,
snippet: `<EuiSelectable
aria-label="Example supporting exclusions"
allowExclusions
options={[]}
onChange={() => this.onChange(options)}>
Expand Down
1 change: 1 addition & 0 deletions src-docs/src/views/selectable/selectable_exclusion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default () => {

return (
<EuiSelectable
aria-label="Example of Selectable supporting exclusions"
allowExclusions
options={options}
onChange={newOptions => setOptions(newOptions)}>
Expand Down
6 changes: 5 additions & 1 deletion src-docs/src/views/selectable/selectable_messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export default () => {
checked={isLoading}
/>
<EuiSpacer />
<EuiSelectable options={[]} style={{ width: 200 }} isLoading={isLoading}>
<EuiSelectable
aria-label="Messaging example"
options={[]}
style={{ width: 200 }}
isLoading={isLoading}>
{list => (useCustomMessage && !isLoading ? customMessage : list)}
</EuiSelectable>
</Fragment>
Expand Down
2 changes: 2 additions & 0 deletions src-docs/src/views/selectable/selectable_popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export default class extends Component {
onClose={this.closeFlyout}
aria-labelledby="flyoutTitle">
<EuiSelectable
aria-label="Popover example"
searchable
options={countries}
onChange={this.onFlyoutChange}
Expand Down Expand Up @@ -158,6 +159,7 @@ export default class extends Component {
<EuiSpacer />

<EuiSelectable
aria-label="Bordered selectable example"
options={options}
onChange={() => {}}
style={{ width: 300 }}
Expand Down
1 change: 1 addition & 0 deletions src-docs/src/views/selectable/selectable_search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default () => {
return (
<Fragment>
<EuiSelectable
aria-label="Searchable example"
searchable
searchProps={{
'data-test-subj': 'selectableSearchHere',
Expand Down
1 change: 1 addition & 0 deletions src-docs/src/views/selectable/selectable_single.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default () => {

return (
<EuiSelectable
aria-label="Single selection example"
options={options}
onChange={newOptions => setOptions(newOptions)}
singleSelection={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

exports[`EuiSelectable is rendered 1`] = `
<div
aria-label="aria-label"
class="euiSelectable testClass1 testClass2"
data-test-subj="test subject string"
/>
Expand Down
5 changes: 5 additions & 0 deletions src/components/selectable/selectable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const options: EuiSelectableOption[] = [
},
];

// Mock the htmlIdGenerator to generate predictable ids for snapshot tests
jest.mock('../../services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => 'htmlId',
}));

describe('EuiSelectable', () => {
test('is rendered', () => {
const component = render(
Expand Down
138 changes: 114 additions & 24 deletions src/components/selectable/selectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import { EuiSelectableMessage } from './selectable_message';
import { EuiSelectableList } from './selectable_list';
import { EuiLoadingChart } from '../loading';
import { getMatchingOptions } from './matching_options';
import { comboBoxKeyCodes } from '../../services';
import { comboBoxKeyCodes, htmlIdGenerator } from '../../services';
import { EuiI18n } from '../i18n';
import { EuiSelectableOption } from './selectable_option';
import { EuiSelectableOptionsListProps } from './selectable_list/selectable_list';
import { EuiSelectableSearchProps } from './selectable_search/selectable_search';

type RequiredEuiSelectableOptionsListProps = Omit<
EuiSelectableOptionsListProps,
Expand All @@ -43,7 +44,7 @@ type EuiSelectableSearchableProps = ExclusiveUnion<
/**
* Passes props down to the `EuiFieldSearch`
*/
searchProps?: {};
searchProps?: Partial<EuiSelectableSearchProps>;
}
>;

Expand Down Expand Up @@ -120,7 +121,7 @@ export class EuiSelectable extends Component<
};

private optionsListRef = createRef<EuiSelectableList>();

rootId = htmlIdGenerator();
constructor(props: EuiSelectableProps) {
super(props);

Expand Down Expand Up @@ -302,10 +303,26 @@ export class EuiSelectable extends Component<
renderOption,
height,
allowExclusions,
'aria-label': ariaLabel,
'aria-describedby': ariaDescribedby,
...rest
} = this.props;

const { searchValue, visibleOptions, activeOptionIndex } = this.state;
const unknownAccessibleName = {
'aria-label': undefined,
'aria-describedby': undefined,
};
const {
'aria-label': searchAriaLabel,
'aria-describedby': searchAriaDescribedby,
...cleanedSearchProps
} = searchProps || unknownAccessibleName;
const {
'aria-label': listAriaLabel,
'aria-describedby': listAriaDescribedby,
...cleanedListProps
} = listProps || unknownAccessibleName;

let messageContent;

Expand Down Expand Up @@ -351,37 +368,110 @@ export class EuiSelectable extends Component<
className
);

const listId = this.rootId('listbox');
const makeOptionId = (index: number | undefined) => {
if (typeof index === 'undefined') {
return '';
}

return `${listId}_option-${index}`;
};

/**
* There are lots of ways to add an accessible name
* Usually we want the same name for the input and the listbox (which is added by aria-label/describedby)
* But you can always override it using searchProps or listProps
* This finds the correct name to use
*
* TODO: This doesn't handle being labelled (<label for="idOfInput">)
*
* @param props
*/
const getAccessibleName = (
props:
| Partial<EuiSelectableSearchProps>
| EuiSelectableOptionsListPropsWithDefaults
| undefined
) => {
if (props && props['aria-label']) {
return { 'aria-label': props['aria-label'] };
}

if (props && props['aria-describedby']) {
return {
'aria-describedby': props['aria-describedby'],
};
}

if (ariaLabel) {
return { 'aria-label': ariaLabel };
}

if (ariaDescribedby) {
return { 'aria-describedby': ariaDescribedby };
}

return {};
};

const searchAccessibleName = getAccessibleName(searchProps);
const searchHasAccessibleName = Boolean(
Object.keys(searchAccessibleName).length
);
const search = searchable ? (
<EuiSelectableSearch
key="listSearch"
options={options}
onChange={this.onSearchChange}
{...searchProps}
/>
<EuiI18n token="euiSelectable.placeholderName" default="Filter options">
{(placeholderName: string) => (
<EuiSelectableSearch
key="listSearch"
options={options}
onChange={this.onSearchChange}
listId={listId}
aria-activedescendant={makeOptionId(activeOptionIndex)} // the current faux-focused option
placeholder={placeholderName}
{...(searchHasAccessibleName
? searchAccessibleName
: { 'aria-label': placeholderName })}
{...cleanedSearchProps}
/>
)}
</EuiI18n>
) : (
undefined
);

const listAccessibleName = getAccessibleName(listProps);
const listHasAccessibleName = Boolean(
Object.keys(listAccessibleName).length
);
const list = messageContent ? (
<EuiSelectableMessage key="listMessage">
{messageContent}
</EuiSelectableMessage>
) : (
<EuiSelectableList
key="list"
options={options}
visibleOptions={visibleOptions}
searchValue={searchValue}
activeOptionIndex={activeOptionIndex}
onOptionClick={this.onOptionClick}
singleSelection={singleSelection}
ref={this.optionsListRef}
renderOption={renderOption}
height={height}
allowExclusions={allowExclusions}
searchable={searchable}
{...listProps}
/>
<EuiI18n token="euiSelectable.placeholderName" default="Filter options">
{(placeholderName: string) => (
<EuiSelectableList
key="list"
options={options}
visibleOptions={visibleOptions}
searchValue={searchValue}
activeOptionIndex={activeOptionIndex}
onOptionClick={this.onOptionClick}
singleSelection={singleSelection}
ref={this.optionsListRef}
renderOption={renderOption}
height={height}
allowExclusions={allowExclusions}
searchable={searchable}
makeOptionId={makeOptionId}
listId={listId}
{...(listHasAccessibleName
? listAccessibleName
: searchable && { 'aria-label': placeholderName })}
{...cleanedListProps}
/>
)}
</EuiI18n>
);

return (
Expand Down
Loading