diff --git a/CHANGELOG.md b/CHANGELOG.md index 05ebb6b7f1c..b78217fcb85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,18 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - - Added `paddingSize` prop to `EuiCard` ([#3638](https://github.com/elastic/eui/pull/3638)) - Added `isClearable` and `placeholder` options to `EuiColorPicker` ([#3689](https://github.com/elastic/eui/pull/3689)) - Added SASS helper files for EUI theme globals ([#3691](https://github.com/elastic/eui/pull/3691)) +**Breaking changes** + +- Significant accessibility refactor of `EuiSelectable` ([#3169](https://github.com/elastic/eui/pull/3169)) + - `react-virtualized` replaced with `react-window` + - `virtualizedProps` on `EuiSelectableOptionsList` renamed to `windowProps` + - Removed `rootId` and added `makeOptionId`, `listId`, and `setActiveOptionIndex` to `EuiSelectableList` + - Added `listId` to `EuiSelectableSearch` + - `options` passed into `EuiSelectable` cannot have an `id` + - Requires an `onChange` to be passed into `EuiSelectableSearch` + ## [`26.3.0`](https://github.com/elastic/eui/tree/v26.3.0) - Expanded `EuiBasicTable`'s default action's name configuration to accept any React node ([#3688](https://github.com/elastic/eui/pull/3688)) diff --git a/package.json b/package.json index 95cd227f9f8..55b22cebc6a 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "@types/numeral": "^0.0.25", "@types/react-beautiful-dnd": "^12.1.2", "@types/react-input-autosize": "^2.0.2", - "@types/react-virtualized": "^9.18.7", + "@types/react-virtualized-auto-sizer": "^1.0.0", + "@types/react-window": "^1.8.1", "chroma-js": "^2.0.4", "classnames": "^2.2.5", "highlight.js": "^9.12.0", @@ -66,7 +67,8 @@ "react-focus-lock": "^1.17.7", "react-input-autosize": "^2.2.2", "react-is": "~16.3.0", - "react-virtualized": "^9.21.2", + "react-virtualized-auto-sizer": "^1.0.2", + "react-window": "^1.8.5", "resize-observer-polyfill": "^1.5.0", "tabbable": "^3.0.0", "text-diff": "^1.0.1", @@ -101,8 +103,8 @@ "@typescript-eslint/eslint-plugin": "^3.2.0", "@typescript-eslint/parser": "^3.2.0", "autoprefixer": "^7.1.5", - "axe-core": "^3.3.2", - "axe-puppeteer": "^1.0.0", + "axe-core": "^3.5.4", + "axe-puppeteer": "^1.1.0", "babel-core": "7.0.0-bridge.0", "babel-eslint": "^10.0.1", "babel-jest": "^24.1.0", diff --git a/scripts/a11y-testing.js b/scripts/a11y-testing.js index 6358d1d16be..39c9f5378b1 100644 --- a/scripts/a11y-testing.js +++ b/scripts/a11y-testing.js @@ -38,7 +38,6 @@ const docsPages = async (root, page) => { `${root}#/forms/color-selection`, `${root}#/forms/code-editor`, `${root}#/forms/date-picker`, - `${root}#/forms/selectable`, `${root}#/forms/suggest`, `${root}#/forms/super-date-picker`, `${root}#/elastic-charts/creating-charts`, @@ -102,7 +101,7 @@ const printResult = result => { id: 'color-contrast', enabled: false }, { id: 'scrollable-region-focusable', - matches: '[role="grid"]', + selector: '[data-skip-axe="scrollable-region-focusable"]', }, ], }) diff --git a/src-docs/src/views/combo_box/combo_box_example.js b/src-docs/src/views/combo_box/combo_box_example.js index 5e25caa9ee9..4133c4cf74b 100644 --- a/src-docs/src/views/combo_box/combo_box_example.js +++ b/src-docs/src/views/combo_box/combo_box_example.js @@ -252,8 +252,8 @@ export const ComboBoxExample = { text: (

EuiComboBoxList uses{' '} - - react-virtualized + + react-window {' '} to only render visible options to be super fast no matter how many options there are. diff --git a/src-docs/src/views/selectable/data.ts b/src-docs/src/views/selectable/data.ts index 85b0bda4ad9..67f425d3394 100644 --- a/src-docs/src/views/selectable/data.ts +++ b/src-docs/src/views/selectable/data.ts @@ -15,7 +15,6 @@ export const Options: EuiSelectableOption[] = [ }, { label: 'Dione', - id: 'id_dione', }, { label: 'Iapetus', diff --git a/src-docs/src/views/selectable/selectable.tsx b/src-docs/src/views/selectable/selectable.tsx index 29993c2bb17..75977051b5b 100644 --- a/src-docs/src/views/selectable/selectable.tsx +++ b/src-docs/src/views/selectable/selectable.tsx @@ -8,6 +8,7 @@ export default () => { return ( setOptions(newOptions)}> diff --git a/src-docs/src/views/selectable/selectable_custom_render.js b/src-docs/src/views/selectable/selectable_custom_render.js index 046b55d5566..0d9debff55d 100644 --- a/src-docs/src/views/selectable/selectable_custom_render.js +++ b/src-docs/src/views/selectable/selectable_custom_render.js @@ -1,4 +1,4 @@ -import React, { useState, Fragment } from 'react'; +import React, { useState } from 'react'; import { EuiBadge, @@ -13,7 +13,6 @@ import { createDataStore } from '../tables/data_store'; export default () => { const countries = createDataStore().countries.map(country => { return { - id: country.code, label: `${country.name}`, prepend: country.flag, append: {country.code}, @@ -26,7 +25,7 @@ export default () => { }); const [options, setOptions] = useState(countries); - const [useCustomContent, setUseCustomContent] = useState(countries); + const [useCustomContent, setUseCustomContent] = useState(false); const onChange = options => { setOptions(options); @@ -38,13 +37,13 @@ export default () => { const renderCountryOption = (option, searchValue) => { return ( - + <> {option.label}
I am secondary content, I am! -
+ ); }; @@ -61,9 +60,9 @@ export default () => { } return ( - + <> @@ -71,17 +70,18 @@ export default () => { {(list, search) => ( - + <> {search} {list} - + )} - + ); }; diff --git a/src-docs/src/views/selectable/selectable_example.js b/src-docs/src/views/selectable/selectable_example.js index 22389c8567d..7d9c9fb2e53 100644 --- a/src-docs/src/views/selectable/selectable_example.js +++ b/src-docs/src/views/selectable/selectable_example.js @@ -104,6 +104,7 @@ export const SelectableExample = { }, demo: , snippet: ` setOptions(newOptions)} listProps={{ bordered: true }}> @@ -141,6 +142,7 @@ export const SelectableExample = { props: { EuiSelectable }, demo: , snippet: `, snippet: ` setOptions(newOptions)} - singleSelection={true} - listProps={{ bordered: true }}> - {list => list} - - `, + aria-label="Single selection example" + options={options} + onChange={newOptions => setOptions(newOptions)} + singleSelection={true} + listProps={{ bordered: true }}> + {list => list} +`, }, { title: 'Sizing and containers', @@ -255,6 +257,7 @@ export const SelectableExample = { props: { EuiSelectable }, demo: , snippet: ` setOptions(newOptions)}> diff --git a/src-docs/src/views/selectable/selectable_exclusion.tsx b/src-docs/src/views/selectable/selectable_exclusion.tsx index 1a4c87d27b1..48614971c55 100644 --- a/src-docs/src/views/selectable/selectable_exclusion.tsx +++ b/src-docs/src/views/selectable/selectable_exclusion.tsx @@ -8,6 +8,7 @@ export default () => { return ( setOptions(newOptions)}> diff --git a/src-docs/src/views/selectable/selectable_messages.tsx b/src-docs/src/views/selectable/selectable_messages.tsx index 95ec006c00a..26da0f56b65 100644 --- a/src-docs/src/views/selectable/selectable_messages.tsx +++ b/src-docs/src/views/selectable/selectable_messages.tsx @@ -30,7 +30,11 @@ export default () => { checked={isLoading} /> - + {list => (useCustomMessage && !isLoading ? customMessage : list)} diff --git a/src-docs/src/views/selectable/selectable_popover.js b/src-docs/src/views/selectable/selectable_popover.js index c5fa3c3d9dd..dc9afc30529 100644 --- a/src-docs/src/views/selectable/selectable_popover.js +++ b/src-docs/src/views/selectable/selectable_popover.js @@ -99,6 +99,7 @@ export default () => { {isFlyoutVisible && ( { {}} style={{ width: 300 }} diff --git a/src-docs/src/views/selectable/selectable_search.tsx b/src-docs/src/views/selectable/selectable_search.tsx index 4566e56ab71..6649a7ac9cd 100644 --- a/src-docs/src/views/selectable/selectable_search.tsx +++ b/src-docs/src/views/selectable/selectable_search.tsx @@ -9,6 +9,7 @@ export default () => { return ( { return ( setOptions(newOptions)} singleSelection={true} diff --git a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap index ecab3704429..9eff4a11e59 100644 --- a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap +++ b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap @@ -317,14 +317,224 @@ exports[`props options list is rendered 1`] = ` class="euiComboBoxOptionsList__rowWrap" >

+ style="position: relative; height: 189px; width: 0px; overflow: auto; direction: ltr;" + > +
+ + + + + + + + + +
+
diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx index b7f119a334d..0c5bd260bc1 100644 --- a/src/components/combo_box/combo_box.tsx +++ b/src/components/combo_box/combo_box.tsx @@ -575,6 +575,7 @@ export class EuiComboBox extends Component< relatedTarget && this.comboBoxRefInstance && this.comboBoxRefInstance.contains(relatedTarget); + if (!focusedInOptionsList && !focusedInInput) { this.closeList(); @@ -693,6 +694,10 @@ export class EuiComboBox extends Component< if (singleSelection) { requestAnimationFrame(this.closeList); + } else { + this.setState({ + activeOptionIndex: this.state.matchingOptions.indexOf(addedOption), + }); } }; diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx index e1824c6e7e8..6ca32b98af1 100644 --- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx +++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx @@ -24,7 +24,11 @@ import React, { RefCallback, } from 'react'; import classNames from 'classnames'; -import { List, ListProps } from 'react-virtualized'; // eslint-disable-line import/named +import { + FixedSizeList, + ListProps, + ListChildComponentProps, +} from 'react-window'; import { EuiCode } from '../../../components/code'; import { EuiFlexGroup, EuiFlexItem } from '../../flex'; @@ -103,6 +107,8 @@ export class EuiComboBoxOptionsList extends Component< EuiComboBoxOptionsListProps > { listRefInstance: RefInstance = null; + listRef: FixedSizeList | null = null; + listBoxRef: HTMLUListElement | null = null; static defaultProps = { 'data-test-subj': '', @@ -146,6 +152,10 @@ export class EuiComboBoxOptionsList extends Component< ) { this.updatePosition(); } + + if (this.listRef && typeof this.props.activeOptionIndex !== 'undefined') { + this.listRef.scrollToItem(this.props.activeOptionIndex, 'auto'); + } } componentWillUnmount() { @@ -173,6 +183,80 @@ export class EuiComboBoxOptionsList extends Component< this.listRefInstance = ref; }; + setListRef = (ref: FixedSizeList | null) => { + this.listRef = ref; + }; + + setListBoxRef = (ref: HTMLUListElement | null) => { + this.listBoxRef = ref; + + if (ref) { + ref.setAttribute('id', this.props.rootId('listbox')); + ref.setAttribute('role', 'listBox'); + ref.setAttribute('tabIndex', '0'); + } + }; + + ListRow = ({ data, index, style }: ListChildComponentProps) => { + const option = data[index]; + const { isGroupLabelOption, label, value, ...rest } = option; + const { + singleSelection, + selectedOptions, + onOptionClick, + optionRef, + activeOptionIndex, + renderOption, + searchValue, + rootId, + } = this.props; + + if (isGroupLabelOption) { + return ( +
+ {label} +
+ ); + } + + let checked: FilterChecked | undefined = undefined; + if ( + singleSelection && + selectedOptions.length && + selectedOptions[0].label === label + ) { + checked = 'on'; + } + + return ( + { + if (onOptionClick) { + onOptionClick(option); + } + }} + ref={optionRef.bind(this, index)} + isFocused={activeOptionIndex === index} + checked={checked} + showIcons={singleSelection ? true : false} + id={rootId(`_option-${index}`)} + title={label} + {...rest}> + {renderOption ? ( + renderOption(option, searchValue, OPTION_CONTENT_CLASSNAME) + ) : ( + + {label} + + )} + + ); + }; + render() { const { 'data-test-subj': dataTestSubj, @@ -311,65 +395,17 @@ export class EuiComboBoxOptionsList extends Component< const height = numVisibleOptions * rowHeight; const optionsList = ( - { - const option = matchingOptions[index]; - const { isGroupLabelOption, label, value, ...rest } = option; - - if (isGroupLabelOption) { - return ( -
- {label} -
- ); - } - - let checked: FilterChecked | undefined = undefined; - if ( - singleSelection && - selectedOptions.length && - selectedOptions[0].label === label - ) { - checked = 'on'; - } - - return ( - { - if (onOptionClick) { - onOptionClick(option); - } - }} - ref={optionRef.bind(this, index)} - isFocused={activeOptionIndex === index} - checked={checked} - showIcons={singleSelection ? true : false} - id={rootId(`_option-${index}`)} - title={label} - {...rest}> - {renderOption ? ( - renderOption(option, searchValue, OPTION_CONTENT_CLASSNAME) - ) : ( - - {label} - - )} - - ); - }} - role="listbox" - rowCount={matchingOptions.length} - rowHeight={rowHeight} - scrollToIndex={scrollToIndex} - width={width} - /> + itemCount={matchingOptions.length} + itemSize={rowHeight} + itemData={matchingOptions} + ref={this.setListRef} + innerRef={this.setListBoxRef} + width={width}> + {this.ListRow} + ); const classes = classNames( diff --git a/src/components/selectable/__snapshots__/selectable.test.tsx.snap b/src/components/selectable/__snapshots__/selectable.test.tsx.snap index ec2b5e77f42..7bbd9e21b00 100644 --- a/src/components/selectable/__snapshots__/selectable.test.tsx.snap +++ b/src/components/selectable/__snapshots__/selectable.test.tsx.snap @@ -2,7 +2,6 @@ exports[`EuiSelectable is rendered 1`] = `
diff --git a/src/components/selectable/selectable.test.tsx b/src/components/selectable/selectable.test.tsx index 2638ed03eeb..daaf0ff1745 100644 --- a/src/components/selectable/selectable.test.tsx +++ b/src/components/selectable/selectable.test.tsx @@ -38,6 +38,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( diff --git a/src/components/selectable/selectable.tsx b/src/components/selectable/selectable.tsx index 61bb338ab84..e0e5c55d335 100644 --- a/src/components/selectable/selectable.tsx +++ b/src/components/selectable/selectable.tsx @@ -33,13 +33,11 @@ import { EuiSelectableMessage } from './selectable_message'; import { EuiSelectableList } from './selectable_list'; import { EuiLoadingChart } from '../loading'; import { getMatchingOptions } from './matching_options'; -import { keys } from '../../services'; +import { keys, htmlIdGenerator } from '../../services'; import { EuiI18n } from '../i18n'; import { EuiSelectableOption } from './selectable_option'; -import { - EuiSelectableOptionsListProps, - EuiSelectableSingleOptionProps, -} from './selectable_list/selectable_list'; +import { EuiSelectableOptionsListProps } from './selectable_list/selectable_list'; +import { EuiSelectableSearchProps } from './selectable_search/selectable_search'; type RequiredEuiSelectableOptionsListProps = Omit< EuiSelectableOptionsListProps, @@ -65,7 +63,7 @@ type EuiSelectableSearchableProps = ExclusiveUnion< /** * Passes props down to the `EuiFieldSearch` */ - searchProps?: {}; + searchProps?: Partial; } >; @@ -88,7 +86,7 @@ export type EuiSelectableProps = Omit< /** * Array of EuiSelectableOption objects. See #EuiSelectableOptionProps */ - options: EuiSelectableOption[]; + options: Array>; /** * Passes back the altered `options` array with selected options as */ @@ -99,7 +97,7 @@ export type EuiSelectableProps = Omit< * `true`: only allows one selection * `always`: can and must have only one selection */ - singleSelection?: EuiSelectableSingleOptionProps; + singleSelection?: EuiSelectableOptionsListProps['singleSelection']; /** * Allows marking options as `checked='off'` as well as `'on'` */ @@ -129,6 +127,7 @@ export interface EuiSelectableState { activeOptionIndex?: number; searchValue: string; visibleOptions: EuiSelectableOption[]; + isFocused: boolean; } export class EuiSelectable extends Component< @@ -142,7 +141,7 @@ export class EuiSelectable extends Component< }; private optionsListRef = createRef(); - + rootId = htmlIdGenerator(); constructor(props: EuiSelectableProps) { super(props); @@ -165,6 +164,7 @@ export class EuiSelectable extends Component< activeOptionIndex, searchValue: initialSearchValue, visibleOptions, + isFocused: false, }; } @@ -193,6 +193,27 @@ export class EuiSelectable extends Component< return this.state.activeOptionIndex != null; }; + onFocus = () => { + if (!this.state.visibleOptions.length || this.state.activeOptionIndex) { + return; + } + + const firstSelected = this.state.visibleOptions.findIndex( + option => option.checked && !option.disabled && !option.isGroupLabel + ); + + if (firstSelected > -1) { + this.setState({ activeOptionIndex: firstSelected, isFocused: true }); + } else { + this.setState({ + activeOptionIndex: this.state.visibleOptions.findIndex( + option => !option.disabled && !option.isGroupLabel + ), + isFocused: true, + }); + } + }; + onKeyDown = (event: KeyboardEvent) => { const optionsList = this.optionsListRef.current; @@ -210,6 +231,8 @@ export class EuiSelectable extends Component< break; case keys.ENTER: + case keys.SPACE: + event.preventDefault(); event.stopPropagation(); if (this.state.activeOptionIndex != null && optionsList) { optionsList.onAddOrRemoveOption( @@ -218,20 +241,23 @@ export class EuiSelectable extends Component< } break; - case keys.TAB: - // Disallow tabbing when the user is navigating the options. - // TODO: Can we force the tab to the next sibling element? - if (this.hasActiveOption()) { - event.preventDefault(); - event.stopPropagation(); - } + case keys.HOME: + event.preventDefault(); + event.stopPropagation(); + this.setState({ activeOptionIndex: 0 }); + break; + + case keys.END: + event.preventDefault(); + event.stopPropagation(); + this.setState({ + activeOptionIndex: this.state.visibleOptions.length - 1, + }); break; default: - if (this.props.onKeyDown) { - this.props.onKeyDown(event); - } - this.clearActiveOption(); + this.setState({ activeOptionIndex: undefined }, this.onFocus); + break; } }; @@ -258,10 +284,12 @@ export class EuiSelectable extends Component< } } - // Group titles are included in option list but are not selectable - // Skip group title options + // Group titles and disabled options are included in option list but are not selectable const direction = amount > 0 ? 1 : -1; - while (visibleOptions[nextActiveOptionIndex].isGroupLabel) { + while ( + visibleOptions[nextActiveOptionIndex].isGroupLabel || + visibleOptions[nextActiveOptionIndex].disabled + ) { nextActiveOptionIndex = nextActiveOptionIndex + direction; if (nextActiveOptionIndex < 0) { @@ -275,29 +303,35 @@ export class EuiSelectable extends Component< }); }; - clearActiveOption = () => { - this.setState({ - activeOptionIndex: undefined, - }); - }; - onSearchChange = ( visibleOptions: EuiSelectableOption[], searchValue: string ) => { - this.setState({ - visibleOptions, - searchValue, - }); + this.setState( + { + visibleOptions, + searchValue, + activeOptionIndex: undefined, + }, + () => { + if (this.state.isFocused) { + this.onFocus(); + } + } + ); }; onContainerBlur = () => { - this.clearActiveOption(); + this.setState({ + activeOptionIndex: undefined, + isFocused: false, + }); }; onOptionClick = (options: EuiSelectableOption[]) => { this.setState(state => ({ visibleOptions: getMatchingOptions(options, state.searchValue), + activeOptionIndex: this.state.activeOptionIndex, })); if (this.props.onChange) { this.props.onChange(options); @@ -319,12 +353,32 @@ export class EuiSelectable extends Component< renderOption, height, allowExclusions, + 'aria-label': ariaLabel, + 'aria-describedby': ariaDescribedby, ...rest } = this.props; const { searchValue, visibleOptions, activeOptionIndex } = this.state; - let messageContent; + // Some messy destructuring here to remove aria-label/describedby from searchProps and listProps + // Made messier by some TS requirements + // The aria attributes are then used in getAccessibleName() to place them where they need to go + 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: JSX.Element | undefined; if (isLoading) { messageContent = ( @@ -368,36 +422,126 @@ export class EuiSelectable extends Component< className ); + const messageContentId = messageContent && this.rootId('messageContent'); + 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 (
diff --git a/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap b/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap index dbb151532e0..bf34412568e 100644 --- a/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap +++ b/src/components/selectable/selectable_list/__snapshots__/selectable_list.test.tsx.snap @@ -2,282 +2,154 @@ exports[`EuiSelectableListItem is rendered 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props activeOptionIndex 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props allowExclusions 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props bordered 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props height is forced 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props height is full 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props renderOption 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props rowHeight 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props searchValue 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props searchValue 2`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props showIcons can be turned off 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props singleSelection can be forced so that at least one must be selected 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props singleSelection can be turned on 1`] = `
-
-
+ />
`; exports[`EuiSelectableListItem props visibleOptions 1`] = `
-
-
+ />
`; diff --git a/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap b/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap index 16b977c8c09..a6151277629 100644 --- a/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap +++ b/src/components/selectable/selectable_list/__snapshots__/selectable_list_item.test.tsx.snap @@ -1,12 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiSelectableListItem is rendered 1`] = ` - + `; exports[`EuiSelectableListItem props append 1`] = ` - + `; exports[`EuiSelectableListItem props checked is off 1`] = ` - + `; exports[`EuiSelectableListItem props checked is on 1`] = ` - + `; exports[`EuiSelectableListItem props disabled 1`] = ` - + `; exports[`EuiSelectableListItem props isFocused 1`] = ` - + `; exports[`EuiSelectableListItem props prepend 1`] = ` - + `; exports[`EuiSelectableListItem props showIcons can be turned off 1`] = ` - + `; diff --git a/src/components/selectable/selectable_list/_selectable_list.scss b/src/components/selectable/selectable_list/_selectable_list.scss index fff87543bd9..6e314a96eec 100644 --- a/src/components/selectable/selectable_list/_selectable_list.scss +++ b/src/components/selectable/selectable_list/_selectable_list.scss @@ -1,4 +1,10 @@ // Expand height of list via flex box +.euiSelectableList { + &:focus-within { + @include euiFocusRing; + } +} + .euiSelectableList-fullHeight { flex-grow: 1; } diff --git a/src/components/selectable/selectable_list/_selectable_list_item.scss b/src/components/selectable/selectable_list/_selectable_list_item.scss index c7f42bd29ca..cd2bbd28c2d 100644 --- a/src/components/selectable/selectable_list/_selectable_list_item.scss +++ b/src/components/selectable/selectable_list/_selectable_list_item.scss @@ -5,32 +5,25 @@ width: 100%; text-align: left; color: $euiTextColor; + cursor: pointer; &:not(:last-of-type) { border-bottom: $euiSelectableListItemBorder; } - &:hover, - &:focus { + &-isFocused:not([aria-disabled='true']), + &:hover:not([aria-disabled='true']) { + color: $euiColorPrimary; + background-color: $euiFocusBackgroundColor; + .euiSelectableListItem__text { text-decoration: underline; } } - &:focus, - &-isFocused { - cursor: pointer; - color: $euiColorPrimary; - background-color: $euiFocusBackgroundColor; - } - - &[disabled] { + &[aria-disabled='true'] { color: $euiColorMediumShade; cursor: not-allowed; - - &:hover { - text-decoration: none; - } } } diff --git a/src/components/selectable/selectable_list/selectable_list.test.tsx b/src/components/selectable/selectable_list/selectable_list.test.tsx index 94250cac96a..e1ccaa32ba4 100644 --- a/src/components/selectable/selectable_list/selectable_list.test.tsx +++ b/src/components/selectable/selectable_list/selectable_list.test.tsx @@ -52,14 +52,18 @@ const options: EuiSelectableOption[] = [ }, ]; +const selectableListRequiredProps = { + makeOptionId: (index: number | undefined) => `option_${index}`, + listId: 'list', + onOptionClick: () => {}, + setActiveOptionIndex: () => {}, + ...requiredProps, +}; + describe('EuiSelectableListItem', () => { test('is rendered', () => { const component = render( - {}} - {...requiredProps} - /> + ); expect(component).toMatchSnapshot(); @@ -70,7 +74,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -82,7 +86,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -94,7 +98,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -112,7 +116,7 @@ describe('EuiSelectableListItem', () => { ); }} - onOptionClick={() => {}} + {...selectableListRequiredProps} /> ); @@ -124,7 +128,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -136,7 +140,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -148,7 +152,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -160,7 +164,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -172,7 +176,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -184,7 +188,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -196,7 +200,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -208,7 +212,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); @@ -220,7 +224,7 @@ describe('EuiSelectableListItem', () => { {}} + {...selectableListRequiredProps} /> ); diff --git a/src/components/selectable/selectable_list/selectable_list.tsx b/src/components/selectable/selectable_list/selectable_list.tsx index 1b582760efc..bfb5c1a02bd 100644 --- a/src/components/selectable/selectable_list/selectable_list.tsx +++ b/src/components/selectable/selectable_list/selectable_list.tsx @@ -17,20 +17,26 @@ * under the License. */ -import React, { Component, HTMLAttributes, ReactNode } from 'react'; +import React, { Component, HTMLAttributes, ReactNode, memo } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../../common'; -// eslint-disable-next-line import/named -import { List, AutoSizer, ListProps } from 'react-virtualized'; -import { htmlIdGenerator } from '../../../services'; import { EuiSelectableListItem, EuiSelectableListItemProps, } from './selectable_list_item'; import { EuiHighlight } from '../../highlight'; import { EuiSelectableOption } from '../selectable_option'; - -export type EuiSelectableSingleOptionProps = 'always' | boolean; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { + FixedSizeList, + ListProps, + ListChildComponentProps as ReactWindowListChildComponentProps, + areEqual, +} from 'react-window'; + +interface ListChildComponentProps extends ReactWindowListChildComponentProps { + data: EuiSelectableOption[]; +} // Consumer Configurable Props via `EuiSelectable.listProps` export type EuiSelectableOptionsListProps = CommonProps & @@ -49,11 +55,11 @@ export type EuiSelectableOptionsListProps = CommonProps & * Show the check/cross selection indicator icons */ showIcons?: boolean; - singleSelection?: EuiSelectableSingleOptionProps; + singleSelection?: 'always' | boolean; /** - * Any props to send specifically to the react-virtualized `List` + * Any props to send specifically to the react-window `FixedSizeList` */ - virtualizedProps?: ListProps; + windowProps?: ListProps; /** * Adds a border around the list to indicate the bounds; * Useful when the list scrolls, otherwise use your own container @@ -97,7 +103,10 @@ export type EuiSelectableListProps = EuiSelectableOptionsListProps & { * and not just on and undefined */ allowExclusions?: boolean; - rootId?: (appendix?: string) => string; + searchable?: boolean; + makeOptionId: (index: number | undefined) => string; + listId: string; + setActiveOptionIndex: (index: number, cb?: () => void) => void; }; export class EuiSelectableList extends Component { @@ -106,12 +115,139 @@ export class EuiSelectableList extends Component { searchValue: '', }; - rootId = this.props.rootId || htmlIdGenerator(); + listRef: FixedSizeList | null = null; + listBoxRef: HTMLUListElement | null = null; + + setListRef = (ref: FixedSizeList | null) => { + this.listRef = ref; + + if (ref && this.props.activeOptionIndex) { + ref.scrollToItem(this.props.activeOptionIndex, 'auto'); + } + }; + + removeScrollableTabStop = (ref: HTMLDivElement | null) => { + // Firefox adds a tab stop for scrollable containers + // We handle this inside so need to stop firefox from doing its thing + if (ref) { + ref.setAttribute('tabindex', '-1'); + } + }; + + setListBoxRef = (ref: HTMLUListElement | null) => { + this.listBoxRef = ref; + const { + listId, + searchable, + singleSelection, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-describedby': ariaDescribedby, + } = this.props; + + if (ref) { + ref.setAttribute('id', listId); + ref.setAttribute('role', 'listbox'); + + if (searchable !== true) { + ref.setAttribute('tabindex', '0'); + + if (singleSelection !== 'always' && singleSelection !== true) { + ref.setAttribute('aria-multiselectable', 'true'); + } + } + + if (typeof ariaLabel === 'string') { + ref.setAttribute('aria-label', ariaLabel); + } else if (typeof ariaLabelledby === 'string') { + ref.setAttribute('aria-labelledby', ariaLabelledby); + } + + if (typeof ariaDescribedby === 'string') { + ref.setAttribute('aria-labelledby', ariaDescribedby); + } + } + }; + + componentDidUpdate() { + const { activeOptionIndex } = this.props; + + if (this.listBoxRef && this.props.searchable !== true) { + this.listBoxRef.setAttribute( + 'aria-activedescendant', + `${this.props.makeOptionId(activeOptionIndex)}` + ); + } + + if (this.listRef && typeof this.props.activeOptionIndex !== 'undefined') { + this.listRef.scrollToItem(this.props.activeOptionIndex, 'auto'); + } + } constructor(props: EuiSelectableListProps) { super(props); } + ListRow = memo(({ data, index, style }: ListChildComponentProps) => { + const option = data[index]; + const { + label, + isGroupLabel, + checked, + disabled, + prepend, + append, + ref, + key, + ...optionRest + } = option; + + if (isGroupLabel) { + return ( +
  • }> + {prepend} + {label} + {append} +
  • + ); + } + + const labelCount = data.filter(option => option.isGroupLabel).length; + + return ( + { + this.props.setActiveOptionIndex(index); + }} + onClick={() => this.onAddOrRemoveOption(option)} + ref={ref ? ref.bind(null, index) : undefined} + isFocused={this.props.activeOptionIndex === index} + title={label} + showIcons={this.props.showIcons} + checked={checked} + disabled={disabled} + prepend={prepend} + append={append} + aria-posinset={index + 1 - labelCount} + aria-setsize={data.length - labelCount} + allowExclusions={this.props.allowExclusions} + {...optionRest as EuiSelectableListItemProps}> + {this.props.renderOption ? ( + this.props.renderOption(option, this.props.searchValue) + ) : ( + {label} + )} + + ); + }, areEqual); + render() { const { className, @@ -120,15 +256,21 @@ export class EuiSelectableList extends Component { onOptionClick, renderOption, height: forcedHeight, - virtualizedProps, + windowProps, rowHeight, activeOptionIndex, - rootId, + makeOptionId, showIcons, singleSelection, visibleOptions, allowExclusions, bordered, + searchable, + listId, + setActiveOptionIndex, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + 'aria-describedby': ariaDescribedby, ...rest } = this.props; @@ -168,66 +310,21 @@ export class EuiSelectableList extends Component {
    {({ width, height }) => ( - { - const option = optionArray[index]; - const { - label, - isGroupLabel, - checked, - disabled, - prepend, - append, - ref, - key, - ...optionRest - } = option; - if (isGroupLabel) { - return ( -
    }> - {prepend} - {label} - {append} -
    - ); - } - return ( - this.onAddOrRemoveOption(option)} - ref={ref ? ref.bind(null, index) : undefined} - isFocused={activeOptionIndex === index} - title={label} - showIcons={showIcons} - checked={checked} - disabled={disabled} - prepend={prepend} - append={append} - {...optionRest as EuiSelectableListItemProps}> - {renderOption ? ( - renderOption(option, searchValue) - ) : ( - {label} - )} - - ); - }} - /> + itemCount={optionArray.length} + itemData={optionArray} + itemSize={rowHeight} + innerElementType="ul" + innerRef={this.setListBoxRef} + {...windowProps}> + {this.ListRow} + )}
    @@ -239,15 +336,20 @@ export class EuiSelectableList extends Component { return; } - const { allowExclusions } = this.props; - - if (option.checked === 'on' && allowExclusions) { - this.onExcludeOption(option); - } else if (option.checked === 'on' || option.checked === 'off') { - this.onRemoveOption(option); - } else { - this.onAddOption(option); - } + const { allowExclusions, options, visibleOptions = options } = this.props; + + this.props.setActiveOptionIndex( + visibleOptions.findIndex(({ label }) => label === option.label), + () => { + if (option.checked === 'on' && allowExclusions) { + this.onExcludeOption(option); + } else if (option.checked === 'on' || option.checked === 'off') { + this.onRemoveOption(option); + } else { + this.onAddOption(option); + } + } + ); }; private onAddOption = (addedOption: EuiSelectableOption) => { diff --git a/src/components/selectable/selectable_list/selectable_list_item.tsx b/src/components/selectable/selectable_list/selectable_list_item.tsx index ad2e9c173ff..c0bc01d0086 100644 --- a/src/components/selectable/selectable_list/selectable_list_item.tsx +++ b/src/components/selectable/selectable_list/selectable_list_item.tsx @@ -17,11 +17,13 @@ * under the License. */ -import React, { Component, ButtonHTMLAttributes } from 'react'; import classNames from 'classnames'; +import React, { Component, LiHTMLAttributes } from 'react'; import { CommonProps } from '../../common'; -import { EuiIcon, IconType, IconColor } from '../../icon'; +import { EuiI18n } from '../../i18n'; +import { EuiIcon, IconColor, IconType } from '../../icon'; import { EuiSelectableOptionCheckedType } from '../selectable_option'; +import { EuiScreenReaderOnly } from '../../accessibility'; function resolveIconAndColor( checked: EuiSelectableOptionCheckedType @@ -34,9 +36,7 @@ function resolveIconAndColor( : { icon: 'cross', color: 'text' }; } -export type EuiSelectableListItemProps = ButtonHTMLAttributes< - HTMLButtonElement -> & +export type EuiSelectableListItemProps = LiHTMLAttributes & CommonProps & { children?: React.ReactNode; /** @@ -54,6 +54,7 @@ export type EuiSelectableListItemProps = ButtonHTMLAttributes< disabled?: boolean; prepend?: React.ReactNode; append?: React.ReactNode; + allowExclusions?: boolean; }; // eslint-disable-next-line react/prefer-stateless-function @@ -72,12 +73,13 @@ export class EuiSelectableListItem extends Component< const { children, className, - disabled, + disabled = false, checked, isFocused, showIcons, prepend, append, + allowExclusions, ...rest } = this.props; @@ -89,10 +91,10 @@ export class EuiSelectableListItem extends Component< className ); - let buttonIcon: React.ReactNode; + let optionIcon: React.ReactNode; if (showIcons) { const { icon, color } = resolveIconAndColor(checked); - buttonIcon = ( + optionIcon = ( + + + + + ); + instruction = ( + + + + + + ); + } else if (allowExclusions && checked === 'off') { + state = ( + + + + + + ); + instruction = ( + + + + + + ); + } + return ( - + ); } } diff --git a/src/components/selectable/selectable_option.tsx b/src/components/selectable/selectable_option.tsx index f64ee0f5c89..6b3480b5be8 100644 --- a/src/components/selectable/selectable_option.tsx +++ b/src/components/selectable/selectable_option.tsx @@ -62,11 +62,11 @@ export interface EuiSelectableGroupLabelOption isGroupLabel: true; } -export interface EuiSelectableButtonOption +export interface EuiSelectableLIOption extends EuiSelectableOptionBase, - ButtonHTMLAttributes {} + ButtonHTMLAttributes {} export type EuiSelectableOption = ExclusiveUnion< EuiSelectableGroupLabelOption, - EuiSelectableButtonOption + EuiSelectableLIOption >; diff --git a/src/components/selectable/selectable_search/__snapshots__/selectable_search.test.tsx.snap b/src/components/selectable/selectable_search/__snapshots__/selectable_search.test.tsx.snap index 71cd413bde2..917776643ee 100644 --- a/src/components/selectable/selectable_search/__snapshots__/selectable_search.test.tsx.snap +++ b/src/components/selectable/selectable_search/__snapshots__/selectable_search.test.tsx.snap @@ -8,10 +8,16 @@ exports[`EuiSelectableSearch is rendered 1`] = ` class="euiFormControlLayout__childrenWrapper" > @@ -40,8 +46,14 @@ exports[`EuiSelectableSearch props defaultValue 1`] = ` class="euiFormControlLayout__childrenWrapper" > diff --git a/src/components/selectable/selectable_search/selectable_search.test.tsx b/src/components/selectable/selectable_search/selectable_search.test.tsx index 7517ceb5ac1..36aa85985aa 100644 --- a/src/components/selectable/selectable_search/selectable_search.test.tsx +++ b/src/components/selectable/selectable_search/selectable_search.test.tsx @@ -26,7 +26,12 @@ import { EuiSelectableSearch } from './selectable_search'; describe('EuiSelectableSearch', () => { test('is rendered', () => { const component = render( - + {}} + {...requiredProps} + /> ); expect(component).toMatchSnapshot(); @@ -35,7 +40,12 @@ describe('EuiSelectableSearch', () => { describe('props', () => { test('defaultValue', () => { const component = render( - + {}} + defaultValue="Mi" + /> ); expect(component).toMatchSnapshot(); diff --git a/src/components/selectable/selectable_search/selectable_search.tsx b/src/components/selectable/selectable_search/selectable_search.tsx index 5d32865b230..34e4ad29212 100644 --- a/src/components/selectable/selectable_search/selectable_search.tsx +++ b/src/components/selectable/selectable_search/selectable_search.tsx @@ -17,27 +17,25 @@ * under the License. */ -import React, { Component, InputHTMLAttributes } from 'react'; +import React, { Component } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../../common'; import { EuiFieldSearch, EuiFieldSearchProps } from '../../form'; import { getMatchingOptions } from '../matching_options'; import { EuiSelectableOption } from '../selectable_option'; -export type EuiSelectableSearchProps = Omit< - InputHTMLAttributes & EuiFieldSearchProps, - 'onChange' -> & +export type EuiSelectableSearchProps = Omit & CommonProps & { /** * Passes back (matchingOptions, searchValue) */ - onChange?: ( + onChange: ( matchingOptions: EuiSelectableOption[], searchValue: string ) => void; options: EuiSelectableOption[]; defaultValue: string; + listId: string; }; export interface EuiSelectableSearchState { @@ -61,38 +59,48 @@ export class EuiSelectableSearch extends Component< } componentDidMount() { - const { options } = this.props; const { searchValue } = this.state; - const matchingOptions = getMatchingOptions(options, searchValue); - this.passUpMatches(matchingOptions, searchValue); + const matchingOptions = getMatchingOptions(this.props.options, searchValue); + this.props.onChange(matchingOptions, searchValue); } onSearchChange = (value: string) => { - this.setState({ searchValue: value }); - const { options } = this.props; - const matchingOptions = getMatchingOptions(options, value); - this.passUpMatches(matchingOptions, value); - }; - - passUpMatches = (matches: EuiSelectableOption[], searchValue: string) => { - if (this.props.onChange) { - this.props.onChange(matches, searchValue); + if (value !== this.state.searchValue) { + this.setState({ searchValue: value }, () => { + const matchingOptions = getMatchingOptions(this.props.options, value); + this.props.onChange(matchingOptions, value); + }); } }; render() { - const { className, onChange, options, defaultValue, ...rest } = this.props; + const { + className, + onChange, + options, + defaultValue, + listId, + placeholder, + ...rest + } = this.props; const classes = classNames('euiSelectableSearch', className); return ( ); diff --git a/yarn.lock b/yarn.lock index 9ad07a23dfe..d4405c4f6f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1311,12 +1311,18 @@ dependencies: "@types/react" "*" -"@types/react-virtualized@^9.18.7": - version "9.21.5" - resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.5.tgz#8b849c7c3c92c7b28b29bc75602c067a912310c6" - integrity sha512-oCoGJzkW90YQkvXwvtkCBDN0TTYvaQs217TJDOh+VipzJ9iiHD/NpD0ILvB844+ewf3/4xYOI5Oj5kj5m6J/4w== +"@types/react-virtualized-auto-sizer@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.0.tgz#fc32f30a8dab527b5816f3a757e1e1d040c8f272" + integrity sha512-NMErdIdSnm2j/7IqMteRiRvRulpjoELnXWUwdbucYCz84xG9PHcoOrr7QfXwB/ku7wd6egiKFrzt/+QK4Imeeg== + dependencies: + "@types/react" "*" + +"@types/react-window@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.1.tgz#6e1ceab2e6f2f78dbf1f774ee0e00f1bb0364bb3" + integrity sha512-V3k1O5cbfZIRa0VVbQ81Ekq/7w42CK1SuiB9U1oPMTxv270D9qUn7rHb3sZoqMkIJFfB1NZxaH7NRDlk+ToDsg== dependencies: - "@types/prop-types" "*" "@types/react" "*" "@types/react@*", "@types/react@^16.9.34": @@ -2228,17 +2234,17 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== -axe-core@^3.1.2, axe-core@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.4.0.tgz#a57ee620c182d5389aff229586aaae06bc541abe" - integrity sha512-5C0OdgxPv/DrQguO6Taj5F1dY5OlkWg4SVmZIVABFYKWlnAc5WTLPzG+xJSgIwf2fmY+NiNGiZXhXx2qT0u/9Q== +axe-core@^3.5.3, axe-core@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.4.tgz#5a7b49ba8c989cd59cde219810ccfb0b7cf72e97" + integrity sha512-JRuxixN5bPHre+815qnyqBQzNpRTqGxLWflvjr4REpGZ5o0WXm+ik2IS4PZ01EnacWmVRB4jCPWFiYENMiiasA== -axe-puppeteer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/axe-puppeteer/-/axe-puppeteer-1.0.0.tgz#cebbeec2c65a2e0cb7d5fd1e7aef26c5f71895a4" - integrity sha512-hTF3u4mtatgTN7fsLVyVgbRdNc15ngjDcTEuqhn9A7ugqLhLCryJWp9fzqZkNlrW8awPcxugyTwLPR7mRdPZmA== +axe-puppeteer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/axe-puppeteer/-/axe-puppeteer-1.1.0.tgz#efcf0666a7d8fefec6930b4b6ec3476c403d1b91" + integrity sha512-VS17Y1rDQe6A0PdeTPxwOSBfmOdj6efgxyre9cN1du1snnVilczSDtQsgifBKBlzoL/3DKfGpgIi+N+zrzODOg== dependencies: - axe-core "^3.1.2" + axe-core "^3.5.3" axios@^0.18.1: version "0.18.1" @@ -3527,11 +3533,6 @@ cloneable-readable@^1.0.0: process-nextick-args "^1.0.6" through2 "^2.0.1" -clsx@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.4.tgz#0c0171f6d5cb2fe83848463c15fcc26b4df8c2ec" - integrity sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg== - co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -4449,11 +4450,6 @@ csstype@^2.2.0: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== -csstype@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" - integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== - currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -4940,14 +4936,6 @@ dom-converter@~0.1: dependencies: utila "~0.3" -dom-helpers@^5.0.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" - integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw== - dependencies: - "@babel/runtime" "^7.6.3" - csstype "^2.6.7" - dom-serializer@0, dom-serializer@^0.1.0, dom-serializer@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -9718,7 +9706,7 @@ loose-envify@^1.0.0, loose-envify@^1.3.1, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0: +loose-envify@^1.1.0, loose-envify@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" integrity sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg= @@ -9968,7 +9956,7 @@ mem@^4.0.0: mimic-fn "^1.0.0" p-is-promise "^1.1.0" -memoize-one@^5.1.1: +"memoize-one@>=3.1.1 <6", memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== @@ -13068,7 +13056,7 @@ react-is@~16.3.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" integrity sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q== -react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: +react-lifecycles-compat@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== @@ -13162,17 +13150,18 @@ react-test-renderer@^16.2.0: object-assign "^4.1.1" prop-types "^15.6.0" -react-virtualized@^9.21.2: - version "9.21.2" - resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" - integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA== +react-virtualized-auto-sizer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd" + integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg== + +react-window@^1.8.5: + version "1.8.5" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1" + integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q== dependencies: - babel-runtime "^6.26.0" - clsx "^1.0.1" - dom-helpers "^5.0.0" - loose-envify "^1.3.0" - prop-types "^15.6.0" - react-lifecycles-compat "^3.0.4" + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" react@^16.12.0: version "16.12.0"