Skip to content

Commit

Permalink
refactor(select): Convert more of Select to FC and improve code style (
Browse files Browse the repository at this point in the history
…Workday#827)

* refactor(select): Convert SelectBase to FC and use hooks for scrolling

* refactor(select): Convert menu state from booleans to string literals

* fix(select): Handle interruptions in menu states
For example, closing a menu which is in the process of opening (or vice-versa).

* fix(select): Add rAF to scrolling code

* chore(select): Rename rAF variable

* refactor(select): Remove unnecessary animation prop

* chore(select): Rename animation variable

* refactor(select): Remove unnecessary isEmpty prop

* chore(select): Remove unnecessary open prop

* refactor(select): Convert menu placement from boolean to string literals

* docs(select): Update code comments

* chore(select): Rename menu visibility states for clarity and symmetry

* docs(select): Update code examples in README

* fix(select): Fix issue with focus not resetting after closing menu

* chore(select): Remove switch case fall-through

* docs(select): Update comments

* test(select): Improve method of waiting for menu to close

* chore(select): Convert defaultProps to default parameters

* chore(select): Clean up grow stories

* fix(select): Remove over-aggressive suppression of option selection

* docs(select): Tweak examples
  • Loading branch information
jamesfan authored Oct 13, 2020
1 parent 0c97ee5 commit e33f70a
Show file tree
Hide file tree
Showing 10 changed files with 563 additions and 397 deletions.
92 changes: 90 additions & 2 deletions cypress/integration/SelectLabs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe('Select', () => {
});

context('the menu', () => {
it('should set assistive focus to "Phone" option', () => {
it('should set assistive focus to the "Phone" option', () => {
cy.findByLabelText('Label')
.pipe(h.selectLabs.getMenu)
.pipe(getAssistiveFocus)
Expand Down Expand Up @@ -252,7 +252,7 @@ describe('Select', () => {
});

context('the menu', () => {
it('should set assistive focus to "Mail" option', () => {
it('should set assistive focus to the "Mail" option', () => {
cy.findByLabelText('Label')
.pipe(h.selectLabs.getMenu)
.pipe(getAssistiveFocus)
Expand Down Expand Up @@ -337,6 +337,94 @@ describe('Select', () => {
});
});

context(`given the "Default" story is rendered`, () => {
beforeEach(() => {
h.stories.load('Labs|Select/React/Top Label', 'Default');
});

context('when the menu is opened', () => {
beforeEach(() => {
cy.findByLabelText('Label')
.focus()
.type('{downarrow}');
});

context('the menu', () => {
it('should set assistive focus to the first option ("E-mail")', () => {
cy.findByLabelText('Label')
.pipe(h.selectLabs.getMenu)
.pipe(getAssistiveFocus)
.should('have.text', 'E-mail');
});
});

context('when focus is advanced to the second option ("Phone")', () => {
beforeEach(() => {
cy.focused().type('{downarrow}');
});

context('the menu', () => {
it('should set assistive focus to the second option ("Phone")', () => {
cy.findByLabelText('Label')
.pipe(h.selectLabs.getMenu)
.pipe(getAssistiveFocus)
.should('have.text', 'Phone');
});
});

context(
'when the menu is closed WITHOUT selecting the newly focused option ("Phone")',
() => {
beforeEach(() => {
cy.focused().type('{esc}');
});

context('when the menu is re-opened AFTER it has fully closed', () => {
beforeEach(() => {
// Wait for menu to fully close before we open it again (so we
// don't interrupt the menu's closing animation and cause it to
// re-open while it's in the middle of closing)
cy.findByLabelText('Label')
.pipe(h.selectLabs.getMenu)
.should('not.exist');
cy.findByLabelText('Label')
.focus()
.type('{downarrow}');
});

context('the menu', () => {
it('should have reset assistive focus to the first option ("E-mail")', () => {
cy.findByLabelText('Label')
.pipe(h.selectLabs.getMenu)
.pipe(getAssistiveFocus)
.should('have.text', 'E-mail');
});
});
});

context('when the menu is re-opened BEFORE it has fully closed', () => {
beforeEach(() => {
cy.focused().type('{downarrow}');
});

context('the menu', () => {
it('should still have assistive focus set to the second option ("Phone")', () => {
// Focus is shifting between the button and menu as we close
// and open the menu. It's important that we use getMenu rather
// than cy.focused() to ensure we obtain a reference to the menu.
cy.findByLabelText('Label')
.pipe(h.selectLabs.getMenu)
.pipe(getAssistiveFocus)
.should('have.text', 'Phone');
});
});
});
}
);
});
});
});

context(`given the "Disabled" story is rendered`, () => {
beforeEach(() => {
h.stories.load('Labs|Select/React/Top Label', 'Disabled');
Expand Down
122 changes: 78 additions & 44 deletions modules/_labs/select/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,24 @@ be made fully accessible (see below).
import * as React from 'react';
import Select from '@workday/canvas-kit-labs-react-select';

const options = [
{label: 'E-mail', value: 'email'},
{label: 'Phone', value: 'phone'},
{label: 'Fax (disabled)', value: 'fax', disabled: true},
{label: 'Mail', value: 'mail'},
];

<Select name="contact" onChange={this.handleChange} options={options} value={value} />;
function Example() {
const options = [
{label: 'E-mail', value: 'email'},
{label: 'Phone', value: 'phone'},
{label: 'Fax (disabled)', value: 'fax', disabled: true},
{label: 'Mail', value: 'mail'},
];

const [value, setValue] = React.useState('email');

const handleChange = (event) => {
setValue(event.currentTarget.value);
};

return (
<Select onChange={handleChange} options={options} value={value} />
);
}
```

#### Accessible Example
Expand All @@ -53,16 +63,26 @@ import * as React from 'react';
import Select from '@workday/canvas-kit-labs-react-select';
import FormField from '@workday/canvas-kit-react-form-field';

const options = [
{label: 'E-mail', value: 'email'},
{label: 'Phone', value: 'phone'},
{label: 'Fax (disabled)', value: 'fax', disabled: true},
{label: 'Mail', value: 'mail'},
];
function Example() {
const options = [
{label: 'E-mail', value: 'email'},
{label: 'Phone', value: 'phone'},
{label: 'Fax (disabled)', value: 'fax', disabled: true},
{label: 'Mail', value: 'mail'},
];

const [value, setValue] = React.useState('email');

const handleChange = (event) => {
setValue(event.currentTarget.value);
};

<FormField label="My Field" inputId="my-select-field">
<Select name="contact" onChange={this.handleChange} options={options} value={value} />
</FormField>;
return (
<FormField label="My Select Field" inputId="my-select-field">
<Select onChange={handleChange} options={options} value={value} />
</FormField>
);
}
```

#### Example with Array of Strings
Expand All @@ -74,11 +94,21 @@ import * as React from 'react';
import Select from '@workday/canvas-kit-labs-react-select';
import FormField from '@workday/canvas-kit-react-form-field';

const options = ['California', 'Florida', 'New York', 'Pennsylvania', 'Texas'];
function Example() {
const options = ['California', 'Florida', 'New York', 'Pennsylvania', 'Texas'];

const [value, setValue] = React.useState('California');

<FormField label="My Field" inputId="my-select-field">
<Select name="state" onChange={this.handleChange} options={options} value={value} />
</FormField>;
const handleChange = (event) => {
setValue(event.currentTarget.value);
};

return (
<FormField label="My Select Field" inputId="my-select-field">
<Select onChange={handleChange} options={options} value={value} />
</FormField>
);
}
```

#### Example with Custom Options Data
Expand All @@ -100,32 +130,36 @@ import {
} from '@workday/canvas-system-icons-web';
import {SystemIcon} from '@workday/canvas-kit-react-icon';

const options = [
{value: 'Activity Stream', data: {icon: activityStreamIcon}},
{value: 'Avatar', data: {icon: avatarIcon}},
{value: 'Upload Cloud', data: {icon: uploadCloudIcon}},
{value: 'User', data: {icon: userIcon}},
];
function Example() {
const options = [
{value: 'Activity Stream', data: {icon: activityStreamIcon}},
{value: 'Avatar', data: {icon: avatarIcon}},
{value: 'Upload Cloud', data: {icon: uploadCloudIcon}},
{value: 'User', data: {icon: userIcon}},
];

const renderOption = option => {
const iconColor = option.focused ? typeColors.inverse : colors.blackPepper100;
return (
<div style={{alignItems: 'center', display: 'flex', padding: '3px 0'}}>
<SystemIcon icon={option.data.icon} color={iconColor} colorHover={iconColor} />
<div style={{marginLeft: 5}}>{option.value}</div>
</div>
);
};

const [value, setValue] = React.useState('Activity Stream');

const handleChange = (event) => {
setValue(event.currentTarget.value);
};

const renderOption = option => {
const iconColor = option.focused ? typeColors.inverse : colors.blackPepper100;
return (
<div style={{alignItems: 'center', display: 'flex', padding: '3px 0'}}>
<SystemIcon icon={option.data.icon} color={iconColor} colorHover={iconColor} />
<div style={{marginLeft: 5}}>{option.value}</div>
</div>
<FormField label="My Select Field" inputId="my-select-field">
<Select onChange={handleChange} options={options} renderOption={renderOption} value={value} />
</FormField>
);
};

<FormField label="My Field" inputId="my-select-field">
<Select
name="icon"
onChange={this.handleChange}
options={options}
renderOption={renderOption}
value={value}
/>
</FormField>;
}
```

## Static Properties
Expand Down
Loading

0 comments on commit e33f70a

Please sign in to comment.