Skip to content

Commit

Permalink
[EuiSelectable] Handle more keyboard scenarios (#5613)
Browse files Browse the repository at this point in the history
* handle clear button enter

* handle non-character keys

* CL

* add cypress tests

* use real* cypress events
  • Loading branch information
thompsongl authored Feb 10, 2022
1 parent f05a255 commit 0ae5c20
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 2 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## [`main`](https://github.com/elastic/eui/tree/main)

No public interface changes since `48.0.0`.
- Improved `EuiSelectable` keypress scenarios ([#5613](https://github.com/elastic/eui/pull/5613))

## [`48.0.0`](https://github.com/elastic/eui/tree/v48.0.0)

Expand Down
2 changes: 1 addition & 1 deletion src/components/form/field_search/field_search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export class EuiFieldSearch extends Component<
isLoading={isLoading}
clear={
isClearable && value && !rest.readOnly && !rest.disabled
? { onClick: this.onClear }
? { onClick: this.onClear, 'data-test-subj': 'clearSearchButton' }
: undefined
}
compressed={compressed}
Expand Down
127 changes: 127 additions & 0 deletions src/components/selectable/selectable.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';

import { EuiSelectable, EuiSelectableProps } from './selectable';

const options: EuiSelectableProps['options'] = [
{
label: 'Titan',
'data-test-subj': 'titanOption',
},
{
label: 'Enceladus',
},
{
label:
"Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
},
];

describe('EuiSelectable', () => {
describe('with a `searchable` configuration', () => {
it('filters the list with search', () => {
cy.realMount(
<EuiSelectable searchable options={options}>
{(list, search) => (
<>
{search}
{list}
</>
)}
</EuiSelectable>
);

// Focus the second option
cy.get('input')
.realClick()
.realPress('{downarrow}')
.realPress('{downarrow}')
.then(() => {
cy.get('li[role=option]')
.eq(1)
.should('have.attr', 'aria-selected', 'true');
});

// Focus remains on the second option
cy.get('input')
.realClick()
.realPress('Alt')
.realPress('Control')
.realPress('Meta')
.realPress('Shift')
.then(() => {
cy.get('li[role=option]')
.eq(1)
.should('have.attr', 'aria-selected', 'true');
});

// Filter the list
cy.get('input')
.realClick()
.realType('enc')
.then(() => {
cy.get('li[role=option]')
.first()
.should('have.attr', 'title', 'Enceladus');
});
});

it('can clear the input', () => {
cy.realMount(
<EuiSelectable searchable options={options}>
{(list, search) => (
<>
{search}
{list}
</>
)}
</EuiSelectable>
);

cy.get('input')
.realClick()
.realType('enc')
.then(() => {
cy.get('li[role=option]')
.first()
.should('have.attr', 'title', 'Enceladus');
});

// Using ENTER
cy.get('[data-test-subj="clearSearchButton"]')
.focus()
.realPress('{enter}')
.then(() => {
cy.get('li[role=option]')
.first()
.should('have.attr', 'title', 'Titan');
});

cy.get('input')
.realClick()
.realType('enc')
.then(() => {
cy.get('li[role=option]')
.first()
.should('have.attr', 'title', 'Enceladus');
});

// Using SPACE
cy.get('[data-test-subj="clearSearchButton"]')
.focus()
.realPress('Space')
.then(() => {
cy.get('li[role=option]')
.first()
.should('have.attr', 'title', 'Titan');
});
});
});
});
14 changes: 14 additions & 0 deletions src/components/selectable/selectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export class EuiSelectable<T = {}> extends Component<
searchable: false,
isPreFiltered: false,
};
private inputRef: HTMLInputElement | null = null;
private containerRef = createRef<HTMLDivElement>();
private optionsListRef = createRef<EuiSelectableList<T>>();
private preventOnFocus = false;
Expand Down Expand Up @@ -312,6 +313,12 @@ export class EuiSelectable<T = {}> extends Component<
// via the input box, and as such only ENTER will toggle selection.
return;
}
if (event.target !== this.inputRef) {
// The captured event is not derived from the searchbox.
// The user is attempting to interact with an internal button,
// such as the clear button, and the event should not be altered.
return;
}
event.preventDefault();
event.stopPropagation();
if (this.state.activeOptionIndex != null && optionsList) {
Expand All @@ -321,6 +328,12 @@ export class EuiSelectable<T = {}> extends Component<
}
break;

case keys.ALT:
case keys.SHIFT:
case keys.CTRL:
case keys.META:
break;

default:
this.setState({ activeOptionIndex: undefined }, this.onFocus);
break;
Expand Down Expand Up @@ -631,6 +644,7 @@ export class EuiSelectable<T = {}> extends Component<
aria-activedescendant={this.makeOptionId(activeOptionIndex)} // the current faux-focused option
placeholder={placeholderName}
isPreFiltered={isPreFiltered ?? false}
inputRef={(node) => (this.inputRef = node)}
{...(searchHasAccessibleName
? searchAccessibleName
: { 'aria-label': placeholderName })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ exports[`EuiSelectableSearch props defaultValue 1`] = `
<button
aria-label="Clear input"
class="euiFormControlLayoutClearButton"
data-test-subj="clearSearchButton"
type="button"
>
<span
Expand Down
4 changes: 4 additions & 0 deletions src/components/suggest/__snapshots__/suggest.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ exports[`EuiSuggest props isVirtualized 1`] = `
aria-label="aria-label"
defaultValue=""
fullWidth={true}
inputRef={[Function]}
isLoading={false}
isPreFiltered={false}
key="listSearch"
Expand Down Expand Up @@ -340,6 +341,7 @@ exports[`EuiSuggest props isVirtualized 1`] = `
defaultValue=""
fullWidth={true}
incremental={true}
inputRef={[Function]}
isClearable={true}
isLoading={false}
onBlur={[Function]}
Expand Down Expand Up @@ -611,6 +613,7 @@ exports[`EuiSuggest props maxHeight 1`] = `
aria-label="aria-label"
defaultValue=""
fullWidth={true}
inputRef={[Function]}
isLoading={false}
isPreFiltered={false}
key="listSearch"
Expand Down Expand Up @@ -658,6 +661,7 @@ exports[`EuiSuggest props maxHeight 1`] = `
defaultValue=""
fullWidth={true}
incremental={true}
inputRef={[Function]}
isClearable={true}
isLoading={false}
onBlur={[Function]}
Expand Down
9 changes: 9 additions & 0 deletions src/services/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export const TAB = 'Tab';
export const BACKSPACE = 'Backspace';
export const F2 = 'F2';

export const ALT = 'Alt';
export const SHIFT = 'Shift';
export const CTRL = 'Control';
export const META = 'Meta'; // Windows, Command, Option

export const ARROW_DOWN = 'ArrowDown';
export const ARROW_UP = 'ArrowUp';
export const ARROW_LEFT = 'ArrowLeft';
Expand All @@ -30,6 +35,10 @@ export enum keys {
TAB = 'Tab',
BACKSPACE = 'Backspace',
F2 = 'F2',
ALT = 'Alt',
SHIFT = 'Shift',
CTRL = 'Control',
META = 'Meta', // Windows, Command, Option
ARROW_DOWN = 'ArrowDown',
ARROW_UP = 'ArrowUp',
ARROW_LEFT = 'ArrowLeft',
Expand Down

0 comments on commit 0ae5c20

Please sign in to comment.