Skip to content

Commit

Permalink
fix(select): handle open click when inside native label (uber#4466)
Browse files Browse the repository at this point in the history
* fix(select): handle open click when inside native label

* Update select-searchable-form-control.scenario.js

* Update select-searchable-form-control.scenario.js

* test(vrt): update visual snapshots for d74c64f (uber#4467)

Co-authored-by: UberOpenSourceBot <[email protected]>

* fix(select): handles toggling the open state

* test(vrt): update visual snapshots for e341ff9 [skip ci] (uber#4474)

Co-authored-by: UberOpenSourceBot <[email protected]>

* fix(select): use only one focus/ref

* fix(select): tab focus and a11y lint

Co-authored-by: UberOpenSourceBot <[email protected]>
Co-authored-by: UberOpenSourceBot <[email protected]>
Co-authored-by: Will Ernest <[email protected]>
  • Loading branch information
4 people authored Aug 20, 2021
1 parent 48fe738 commit 29e35f4
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 28 deletions.
195 changes: 180 additions & 15 deletions src/select/__tests__/select-form-control-label-click.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,191 @@ LICENSE file in the root directory of this source tree.

const {mount} = require('../../../e2e/helpers');

describe('select unmount blur', () => {
it('opens non-searchable listbox when label is clicked', async () => {
await mount(page, 'select--searchable-form-control');
const labels = await page.$$('label');
async function clickOutside(page) {
const el = await page.$('#click-outside');
await el.click();
}

const label = await page.evaluate(el => el.textContent, labels[0]);
expect(label).toBe('not searchable');
async function clickSelectAtIndex(page, index) {
const elements = await page.$$('div[data-baseweb="select"]');
await elements[index].click();
}

await labels[0].click();
await page.waitForSelector('[role="listbox"]');
async function clickLabelAtIndex(page, index) {
const labels = await page.$$('label');
await labels[index].click();
}

async function isListboxOpen(page) {
await page.waitForSelector('ul[role="listbox"]');
}

async function isListboxClosed(page) {
await page.waitForSelector('ul[role="listbox"]', {hidden: true});
}

describe('select click open/close', () => {
describe('baseui form-control label', () => {
describe('non-searchable', () => {
it('label click', async () => {
await mount(page, 'select--searchable-form-control');
await clickLabelAtIndex(page, 0);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickLabelAtIndex(page, 0);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});

it('select click', async () => {
await mount(page, 'select--searchable-form-control');
await clickSelectAtIndex(page, 0);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickSelectAtIndex(page, 0);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});
});

describe('searchable', () => {
it('label click', async () => {
await mount(page, 'select--searchable-form-control');
await clickLabelAtIndex(page, 1);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickLabelAtIndex(page, 1);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});

it('select click', async () => {
await mount(page, 'select--searchable-form-control');
await clickSelectAtIndex(page, 1);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickSelectAtIndex(page, 1);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});
});
});

describe('native label', () => {
describe('non-searchable', () => {
it('label click', async () => {
await mount(page, 'select--searchable-form-control');
await clickLabelAtIndex(page, 2);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickLabelAtIndex(page, 2);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});

it('select click', async () => {
await mount(page, 'select--searchable-form-control');
await clickSelectAtIndex(page, 2);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickSelectAtIndex(page, 2);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});
});

describe('searchable', () => {
it('label click', async () => {
await mount(page, 'select--searchable-form-control');
await clickLabelAtIndex(page, 3);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickLabelAtIndex(page, 3);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});

it('select click', async () => {
await mount(page, 'select--searchable-form-control');
await clickSelectAtIndex(page, 3);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickSelectAtIndex(page, 3);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});
});
});

it('opens searchable listbox when label is clicked', async () => {
await mount(page, 'select--searchable-form-control');
const labels = await page.$$('label');
describe('no label', () => {
it('non-searchable', async () => {
await mount(page, 'select--searchable-form-control');
await clickSelectAtIndex(page, 4);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

await clickSelectAtIndex(page, 4);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);
});

it('searchable', async () => {
await mount(page, 'select--searchable-form-control');
await clickSelectAtIndex(page, 5);
await isListboxOpen(page);

await clickOutside(page);
await isListboxClosed(page);

const label = await page.evaluate(el => el.textContent, labels[1]);
expect(label).toBe('searchable');
await clickSelectAtIndex(page, 5);
await isListboxOpen(page);

await labels[1].click();
await page.waitForSelector('[role="listbox"]');
await clickOutside(page);
await isListboxClosed(page);
});
});
});
63 changes: 57 additions & 6 deletions src/select/__tests__/select-searchable-form-control.scenario.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,21 @@ const options = [
];

export default function Scenario() {
const [notSearchable, setNotSearchable] = React.useState([]);
const [searchable, setSearchable] = React.useState([]);
const [value, setValue] = React.useState([]);

return (
<div>
<div id="click-outside">click outside element</div>

<FormControl label="not searchable">
<Select
id="colors-not-searchable"
clearable={false}
searchable={false}
placeholder="Select color"
value={notSearchable}
value={value}
options={options}
onChange={params => setNotSearchable(params.value)}
onChange={params => setValue(params.value)}
/>
</FormControl>

Expand All @@ -43,11 +44,61 @@ export default function Scenario() {
id="colors-searchable"
clearable={false}
placeholder="Select color"
value={searchable}
value={value}
options={options}
onChange={params => setSearchable(params.value)}
onChange={params => setValue(params.value)}
/>
</FormControl>

<label htmlFor="not-searchable-native-label">
Native not searchable
<Select
id="not-searchable-native-label"
clearable={false}
searchable={false}
placeholder="Select color"
value={value}
options={options}
onChange={params => setValue(params.value)}
/>
</label>

{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label>
Native searchable
<Select
clearable={false}
searchable={true}
placeholder="Select color"
value={value}
options={options}
onChange={params => setValue(params.value)}
/>
</label>

<div id="no-label-not-searchable">
<p>no label not searchable</p>
<Select
clearable={false}
searchable={false}
placeholder="Select color"
value={value}
options={options}
onChange={params => setValue(params.value)}
/>
</div>

<div id="no-label-searchable">
<p>no label</p>
<Select
clearable={false}
searchable={true}
placeholder="Select color"
value={value}
options={options}
onChange={params => setValue(params.value)}
/>
</div>
</div>
);
}
26 changes: 19 additions & 7 deletions src/select/select-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,16 +198,17 @@ class Select extends React.Component<PropsT, SelectStateT> {
if (!this.state.isFocused) {
this.openAfterFocus = this.props.openOnClick;
this.focus();
return;
}

if (!this.state.isOpen) {
this.setState({
isOpen: true,
isFocused: true,
isPseudoFocused: false,
});
return;
}

return;
}

// Ensures that interactive elements within the Select component do not trigger the outer click
Expand All @@ -222,7 +223,12 @@ class Select extends React.Component<PropsT, SelectStateT> {
// text input to filter the dropdown options.
if (!this.props.searchable) {
this.focus();
this.setState(prev => ({isOpen: !prev.isOpen}));
if (this.state.isOpen) {
this.setState({isOpen: false, isFocused: false});
} else {
this.setState({isOpen: true, isFocused: true});
}

return;
}

Expand Down Expand Up @@ -701,16 +707,22 @@ class Select extends React.Component<PropsT, SelectStateT> {
aria-owns={this.state.isOpen ? this.listboxId : null}
aria-required={this.props.required || null}
onFocus={this.handleInputFocus}
ref={this.handleInputRef}
tabIndex={0}
{...sharedProps}
{...inputContainerProps}
>
<input
aria-hidden="true"
aria-hidden
id={this.props.id || null}
onFocus={this.handleInputFocus}
style={{display: 'none'}}
ref={this.handleInputRef}
style={{
opacity: 0,
width: 0,
overflow: 'hidden',
border: 'none',
padding: 0,
}}
tabIndex={-1}
/>
</InputContainer>
);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 29e35f4

Please sign in to comment.