From 00ce8ad66ec6c5752187d4d74a1bf7f7549614ca Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 28 Feb 2024 04:57:39 +0900 Subject: [PATCH 1/4] CustomSelectControlV2: Remove legacy adapter layer --- .../default-component/index.tsx | 5 +- .../src/custom-select-control-v2/index.tsx | 2 +- .../legacy-adapter.tsx | 25 - .../legacy-component/test/index.tsx | 467 ++++++++++++++++ .../stories/legacy.story.tsx | 11 +- .../custom-select-control-v2/test/index.tsx | 516 +----------------- 6 files changed, 505 insertions(+), 521 deletions(-) delete mode 100644 packages/components/src/custom-select-control-v2/legacy-adapter.tsx create mode 100644 packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx diff --git a/packages/components/src/custom-select-control-v2/default-component/index.tsx b/packages/components/src/custom-select-control-v2/default-component/index.tsx index 746861ed03b5a..e5650202a4160 100644 --- a/packages/components/src/custom-select-control-v2/default-component/index.tsx +++ b/packages/components/src/custom-select-control-v2/default-component/index.tsx @@ -8,8 +8,11 @@ import * as Ariakit from '@ariakit/react'; */ import _CustomSelect from '../custom-select'; import type { CustomSelectProps } from '../types'; +import type { WordPressComponentProps } from '../../context'; -function CustomSelect( props: CustomSelectProps ) { +function CustomSelect( + props: WordPressComponentProps< CustomSelectProps, 'button', false > +) { const { defaultValue, onChange, value, ...restProps } = props; // Forward props + store from v2 implementation const store = Ariakit.useSelectStore( { diff --git a/packages/components/src/custom-select-control-v2/index.tsx b/packages/components/src/custom-select-control-v2/index.tsx index 58ca626be9161..f05191ad8fc0b 100644 --- a/packages/components/src/custom-select-control-v2/index.tsx +++ b/packages/components/src/custom-select-control-v2/index.tsx @@ -1,5 +1,5 @@ /** * Internal dependencies */ -export { default as CustomSelect } from './legacy-adapter'; +export { default as CustomSelect } from './default-component'; export { default as CustomSelectItem } from './custom-select-item'; diff --git a/packages/components/src/custom-select-control-v2/legacy-adapter.tsx b/packages/components/src/custom-select-control-v2/legacy-adapter.tsx deleted file mode 100644 index ab7fc74d97792..0000000000000 --- a/packages/components/src/custom-select-control-v2/legacy-adapter.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Internal dependencies - */ -import _LegacyCustomSelect from './legacy-component'; -import _NewCustomSelect from './default-component'; -import type { CustomSelectProps, LegacyCustomSelectProps } from './types'; -import type { WordPressComponentProps } from '../context'; - -function isLegacy( props: any ): props is LegacyCustomSelectProps { - return typeof props.options !== 'undefined'; -} - -function CustomSelect( - props: - | LegacyCustomSelectProps - | WordPressComponentProps< CustomSelectProps, 'button', false > -) { - if ( isLegacy( props ) ) { - return <_LegacyCustomSelect { ...props } />; - } - - return <_NewCustomSelect { ...props } />; -} - -export default CustomSelect; diff --git a/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx new file mode 100644 index 0000000000000..620908eb1ee24 --- /dev/null +++ b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx @@ -0,0 +1,467 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import { click, press, sleep, type, waitFor } from '@ariakit/test'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import UncontrolledCustomSelect from '..'; + +const customClass = 'amber-skies'; + +const legacyProps = { + label: 'label!', + options: [ + { + key: 'flower1', + name: 'violets', + }, + { + key: 'flower2', + name: 'crimson clover', + className: customClass, + }, + { + key: 'flower3', + name: 'poppy', + }, + { + key: 'color1', + name: 'amber', + className: customClass, + }, + { + key: 'color2', + name: 'aquamarine', + style: { + backgroundColor: 'rgb(127, 255, 212)', + rotate: '13deg', + }, + }, + ], +}; + +const LegacyControlledCustomSelect = ( { + options, + onChange, + ...restProps +}: React.ComponentProps< typeof UncontrolledCustomSelect > ) => { + const [ value, setValue ] = useState( options[ 0 ] ); + return ( + { + onChange?.( args ); + setValue( args.selectedItem ); + } } + value={ options.find( + ( option: any ) => option.key === value.key + ) } + /> + ); +}; + +describe( 'With Legacy Props', () => { + describe.each( [ + [ 'Uncontrolled', UncontrolledCustomSelect ], + [ 'Controlled', LegacyControlledCustomSelect ], + ] )( '%s', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; + + it( 'Should replace the initial selection when a new item is selected', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await click( currentSelectedItem ); + + await click( + screen.getByRole( 'option', { + name: 'crimson clover', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + + await click( currentSelectedItem ); + + await click( + screen.getByRole( 'option', { + name: 'poppy', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + } ); + + it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toBeVisible(); + + await press.Escape(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + } ) + ).not.toBeInTheDocument(); + + expect( currentSelectedItem ).toHaveTextContent( + legacyProps.options[ 0 ].name + ); + } ); + + it( 'Should apply class only to options that have a className defined', async () => { + render( ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + // return an array of items _with_ a className added + const itemsWithClass = legacyProps.options.filter( + ( option ) => option.className !== undefined + ); + + // assert against filtered array + itemsWithClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveClass( + customClass + ) + ); + + // return an array of items _without_ a className added + const itemsWithoutClass = legacyProps.options.filter( + ( option ) => option.className === undefined + ); + + // assert against filtered array + itemsWithoutClass.map( ( { name } ) => + expect( + screen.getByRole( 'option', { name } ) + ).not.toHaveClass( customClass ) + ); + } ); + + it( 'Should apply styles only to options that have styles defined', async () => { + const customStyles = + 'background-color: rgb(127, 255, 212); rotate: 13deg;'; + + render( ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + // return an array of items _with_ styles added + const styledItems = legacyProps.options.filter( + ( option ) => option.style !== undefined + ); + + // assert against filtered array + styledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( + customStyles + ) + ); + + // return an array of items _without_ styles added + const unstyledItems = legacyProps.options.filter( + ( option ) => option.style === undefined + ); + + // assert against filtered array + unstyledItems.map( ( { name } ) => + expect( + screen.getByRole( 'option', { name } ) + ).not.toHaveStyle( customStyles ) + ); + } ); + + it( 'does not show selected hint by default', async () => { + render( + + ); + await waitFor( () => + expect( + screen.getByRole( 'combobox', { name: 'Custom select' } ) + ).not.toHaveTextContent( 'Hint' ) + ); + } ); + + it( 'shows selected hint when __experimentalShowSelectedHint is set', async () => { + render( + + ); + + await waitFor( () => + expect( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ).toHaveTextContent( /hint/i ) + ); + } ); + + it( 'shows selected hint in list of options when added', async () => { + render( + + ); + + await click( + screen.getByRole( 'combobox', { name: 'Custom select' } ) + ); + + expect( + screen.getByRole( 'option', { name: /hint/i } ) + ).toBeVisible(); + } ); + + it( 'Should return object onChange', async () => { + const mockOnChange = jest.fn(); + + render( + + ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + expect( mockOnChange ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( { + inputValue: '', + isOpen: false, + selectedItem: { key: 'violets', name: 'violets' }, + type: '', + } ) + ); + + await click( + screen.getByRole( 'option', { + name: 'aquamarine', + } ) + ); + + expect( mockOnChange ).toHaveBeenNthCalledWith( + 2, + expect.objectContaining( { + inputValue: '', + isOpen: false, + selectedItem: expect.objectContaining( { + name: 'aquamarine', + } ), + type: '', + } ) + ); + } ); + + it( 'Should return selectedItem object when specified onChange', async () => { + const mockOnChange = jest.fn( + ( { selectedItem } ) => selectedItem.key + ); + + render( + + ); + + await sleep(); + await press.Tab(); + expect( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ).toHaveFocus(); + + await type( 'p' ); + await press.Enter(); + + expect( mockOnChange ).toHaveReturnedWith( 'poppy' ); + } ); + + describe( 'Keyboard behavior and accessibility', () => { + it( 'Should be able to change selection using keyboard', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toHaveFocus(); + + await press.ArrowDown(); + await press.Enter(); + + expect( currentSelectedItem ).toHaveTextContent( + 'crimson clover' + ); + } ); + + it( 'Should be able to type characters to select matching options', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toHaveFocus(); + + await type( 'a' ); + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'amber' ); + } ); + + it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await type( 'aq' ); + + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + hidden: true, + } ) + ).not.toBeInTheDocument(); + + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + } ); + + it( 'Should have correct aria-selected value for selections', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await click( currentSelectedItem ); + + // get all items except for first option + const unselectedItems = legacyProps.options.filter( + ( { name } ) => name !== legacyProps.options[ 0 ].name + ); + + // assert that all other items have aria-selected="false" + unselectedItems.map( ( { name } ) => + expect( + screen.getByRole( 'option', { name, selected: false } ) + ).toBeVisible() + ); + + // assert that first item has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: legacyProps.options[ 0 ].name, + selected: true, + } ) + ).toBeVisible(); + + // change the current selection + await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + + // click combobox to mount listbox with options again + await click( currentSelectedItem ); + + // check that first item is has aria-selected="false" after new selection + expect( + screen.getByRole( 'option', { + name: legacyProps.options[ 0 ].name, + selected: false, + } ) + ).toBeVisible(); + + // check that new selected item now has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: 'poppy', + selected: true, + } ) + ).toBeVisible(); + } ); + } ); + } ); +} ); diff --git a/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx b/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx index 9faa285cd72ab..f97b2376e9deb 100644 --- a/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx +++ b/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx @@ -11,12 +11,11 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import _LegacyCustomSelect from '../legacy-component'; -import { CustomSelect } from '..'; +import CustomSelect from '../legacy-component'; -const meta: Meta< typeof _LegacyCustomSelect > = { +const meta: Meta< typeof CustomSelect > = { title: 'Components (Experimental)/CustomSelectControl v2/Legacy', - component: _LegacyCustomSelect, + component: CustomSelect, argTypes: { onChange: { control: { type: null } }, value: { control: { type: null } }, @@ -43,11 +42,11 @@ const meta: Meta< typeof _LegacyCustomSelect > = { }; export default meta; -const Template: StoryFn< typeof _LegacyCustomSelect > = ( props ) => { +const Template: StoryFn< typeof CustomSelect > = ( props ) => { const [ fontSize, setFontSize ] = useState( props.options[ 0 ] ); const onChange: React.ComponentProps< - typeof _LegacyCustomSelect + typeof CustomSelect >[ 'onChange' ] = ( changeObject ) => { setFontSize( changeObject.selectedItem ); props.onChange?.( changeObject ); diff --git a/packages/components/src/custom-select-control-v2/test/index.tsx b/packages/components/src/custom-select-control-v2/test/index.tsx index dc8df0813b169..39476ef45c40b 100644 --- a/packages/components/src/custom-select-control-v2/test/index.tsx +++ b/packages/components/src/custom-select-control-v2/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen } from '@testing-library/react'; -import { click, press, sleep, type, waitFor } from '@ariakit/test'; +import { click, press, sleep, type } from '@ariakit/test'; /** * WordPress dependencies @@ -13,493 +13,35 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import { CustomSelect as UncontrolledCustomSelect, CustomSelectItem } from '..'; -import type { CustomSelectProps, LegacyCustomSelectProps } from '../types'; - -const customClass = 'amber-skies'; - -const legacyProps = { - label: 'label!', - options: [ - { - key: 'flower1', - name: 'violets', - }, - { - key: 'flower2', - name: 'crimson clover', - className: customClass, - }, - { - key: 'flower3', - name: 'poppy', - }, - { - key: 'color1', - name: 'amber', - className: customClass, - }, - { - key: 'color2', - name: 'aquamarine', - style: { - backgroundColor: 'rgb(127, 255, 212)', - rotate: '13deg', - }, - }, - ], -}; - -const LegacyControlledCustomSelect = ( { - options, - onChange, - ...restProps -}: LegacyCustomSelectProps ) => { - const [ value, setValue ] = useState( options[ 0 ] ); - return ( - { - onChange?.( args ); - setValue( args.selectedItem ); - } } - value={ options.find( - ( option: any ) => option.key === value.key - ) } - /> - ); -}; - -describe( 'With Legacy Props', () => { - describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', LegacyControlledCustomSelect ], - ] )( '%s', ( ...modeAndComponent ) => { - const [ , Component ] = modeAndComponent; - - it( 'Should replace the initial selection when a new item is selected', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await click( currentSelectedItem ); - - await click( - screen.getByRole( 'option', { - name: 'crimson clover', - } ) - ); - - expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); - - await click( currentSelectedItem ); - - await click( - screen.getByRole( 'option', { - name: 'poppy', - } ) - ); - - expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); - } ); - - it( 'Should keep current selection if dropdown is closed without changing selection', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toBeVisible(); - - await press.Escape(); - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - } ) - ).not.toBeInTheDocument(); - - expect( currentSelectedItem ).toHaveTextContent( - legacyProps.options[ 0 ].name - ); - } ); - - it( 'Should apply class only to options that have a className defined', async () => { - render( ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - // return an array of items _with_ a className added - const itemsWithClass = legacyProps.options.filter( - ( option ) => option.className !== undefined - ); - - // assert against filtered array - itemsWithClass.map( ( { name } ) => - expect( screen.getByRole( 'option', { name } ) ).toHaveClass( - customClass - ) - ); - - // return an array of items _without_ a className added - const itemsWithoutClass = legacyProps.options.filter( - ( option ) => option.className === undefined - ); - - // assert against filtered array - itemsWithoutClass.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name } ) - ).not.toHaveClass( customClass ) - ); - } ); - - it( 'Should apply styles only to options that have styles defined', async () => { - const customStyles = - 'background-color: rgb(127, 255, 212); rotate: 13deg;'; - - render( ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - // return an array of items _with_ styles added - const styledItems = legacyProps.options.filter( - ( option ) => option.style !== undefined - ); - - // assert against filtered array - styledItems.map( ( { name } ) => - expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( - customStyles - ) - ); - - // return an array of items _without_ styles added - const unstyledItems = legacyProps.options.filter( - ( option ) => option.style === undefined - ); - - // assert against filtered array - unstyledItems.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name } ) - ).not.toHaveStyle( customStyles ) - ); - } ); - - it( 'does not show selected hint by default', async () => { - render( - - ); - await waitFor( () => - expect( - screen.getByRole( 'combobox', { name: 'Custom select' } ) - ).not.toHaveTextContent( 'Hint' ) - ); - } ); - - it( 'shows selected hint when __experimentalShowSelectedHint is set', async () => { - render( - - ); - - await waitFor( () => - expect( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ).toHaveTextContent( /hint/i ) - ); - } ); - - it( 'shows selected hint in list of options when added', async () => { - render( - - ); - - await click( - screen.getByRole( 'combobox', { name: 'Custom select' } ) - ); - - expect( - screen.getByRole( 'option', { name: /hint/i } ) - ).toBeVisible(); - } ); - - it( 'Should return object onChange', async () => { - const mockOnChange = jest.fn(); - - render( - - ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - expect( mockOnChange ).toHaveBeenNthCalledWith( - 1, - expect.objectContaining( { - inputValue: '', - isOpen: false, - selectedItem: { key: 'violets', name: 'violets' }, - type: '', - } ) - ); - - await click( - screen.getByRole( 'option', { - name: 'aquamarine', - } ) - ); - - expect( mockOnChange ).toHaveBeenNthCalledWith( - 2, - expect.objectContaining( { - inputValue: '', - isOpen: false, - selectedItem: expect.objectContaining( { - name: 'aquamarine', - } ), - type: '', - } ) - ); - } ); - - it( 'Should return selectedItem object when specified onChange', async () => { - const mockOnChange = jest.fn( - ( { selectedItem } ) => selectedItem.key - ); - - render( - - ); - - await sleep(); - await press.Tab(); - expect( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ).toHaveFocus(); - - await type( 'p' ); - await press.Enter(); - - expect( mockOnChange ).toHaveReturnedWith( 'poppy' ); - } ); - - describe( 'Keyboard behavior and accessibility', () => { - it( 'Should be able to change selection using keyboard', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await press.ArrowDown(); - await press.Enter(); - - expect( currentSelectedItem ).toHaveTextContent( - 'crimson clover' - ); - } ); - - it( 'Should be able to type characters to select matching options', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await type( 'a' ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'amber' ); - } ); - - it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await type( 'aq' ); - - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - hidden: true, - } ) - ).not.toBeInTheDocument(); - - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); - } ); - - it( 'Should have correct aria-selected value for selections', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await click( currentSelectedItem ); - - // get all items except for first option - const unselectedItems = legacyProps.options.filter( - ( { name } ) => name !== legacyProps.options[ 0 ].name - ); - - // assert that all other items have aria-selected="false" - unselectedItems.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name, selected: false } ) - ).toBeVisible() - ); - - // assert that first item has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: legacyProps.options[ 0 ].name, - selected: true, - } ) - ).toBeVisible(); - - // change the current selection - await click( screen.getByRole( 'option', { name: 'poppy' } ) ); - - // click combobox to mount listbox with options again - await click( currentSelectedItem ); - - // check that first item is has aria-selected="false" after new selection - expect( - screen.getByRole( 'option', { - name: legacyProps.options[ 0 ].name, - selected: false, - } ) - ).toBeVisible(); - - // check that new selected item now has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: 'poppy', - selected: true, - } ) - ).toBeVisible(); - } ); - } ); - } ); -} ); - -describe( 'static typing', () => { - <> - { /* @ts-expect-error - when `options` prop is passed, `onChange` should have legacy signature */ } - undefined } - /> - undefined } - /> - undefined } - > - foobar - - { /* @ts-expect-error - when `children` are passed, `onChange` should have new default signature */ } - undefined } - > - foobar - - ; -} ); +import type { CustomSelectProps } from '../types'; + +const items = [ + { + key: 'flower1', + value: 'violets', + }, + { + key: 'flower2', + value: 'crimson clover', + }, + { + key: 'flower3', + value: 'poppy', + }, + { + key: 'color1', + value: 'amber', + }, + { + key: 'color2', + value: 'aquamarine', + }, +]; const defaultProps = { label: 'label!', - children: legacyProps.options.map( ( { name, key } ) => ( - + children: items.map( ( { value, key } ) => ( + ) ), }; @@ -575,9 +117,7 @@ describe( 'With Default Props', () => { } ) ).not.toBeInTheDocument(); - expect( currentSelectedItem ).toHaveTextContent( - legacyProps.options[ 0 ].name - ); + expect( currentSelectedItem ).toHaveTextContent( items[ 0 ].value ); } ); describe( 'Keyboard behavior and accessibility', () => { From 3ca4776d521560c848e941fe13717f34e846abfd Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 28 Feb 2024 05:50:48 +0900 Subject: [PATCH 2/4] Remove `never` type for `children` prop on legacy --- packages/components/src/custom-select-control-v2/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/custom-select-control-v2/types.ts b/packages/components/src/custom-select-control-v2/types.ts index b32f3ee211308..51b0cb6b38d6a 100644 --- a/packages/components/src/custom-select-control-v2/types.ts +++ b/packages/components/src/custom-select-control-v2/types.ts @@ -92,7 +92,6 @@ type LegacyOnChangeObject = { }; export type LegacyCustomSelectProps = { - children?: never; /** * Optional classname for the component. */ From b2c9aaec837c0ac98da426a60eb72eeccd3f2950 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 28 Feb 2024 06:06:03 +0900 Subject: [PATCH 3/4] Add changelog --- packages/components/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d4dfaac153f01..b432eb8e489e5 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,10 @@ - `Tooltip`: Explicitly set system font to avoid CSS bleed ([#59307](https://github.com/WordPress/gutenberg/pull/59307)). +### Experimental + +- `CustomSelectControlV2`: Remove legacy adapter layer ([#59420](https://github.com/WordPress/gutenberg/pull/59420)). + ## 27.0.0 (2024-02-21) ### Breaking Changes From 8828d1985deeb2dd432393e640d41cce4c2f81ed Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 28 Feb 2024 21:48:07 +0900 Subject: [PATCH 4/4] Remove unnecessary `describe` wrapper in tests --- .../legacy-component/test/index.tsx | 640 +++++++++--------- .../custom-select-control-v2/test/index.tsx | 544 ++++++++------- 2 files changed, 582 insertions(+), 602 deletions(-) diff --git a/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx index 620908eb1ee24..dbb1ac1d78402 100644 --- a/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx +++ b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx @@ -69,399 +69,389 @@ const LegacyControlledCustomSelect = ( { ); }; -describe( 'With Legacy Props', () => { - describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', LegacyControlledCustomSelect ], - ] )( '%s', ( ...modeAndComponent ) => { - const [ , Component ] = modeAndComponent; - - it( 'Should replace the initial selection when a new item is selected', async () => { - render( ); +describe.each( [ + [ 'Uncontrolled', UncontrolledCustomSelect ], + [ 'Controlled', LegacyControlledCustomSelect ], +] )( 'CustomSelectControl (%s)', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + it( 'Should replace the initial selection when a new item is selected', async () => { + render( ); - await click( currentSelectedItem ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); - await click( - screen.getByRole( 'option', { - name: 'crimson clover', - } ) - ); + await click( currentSelectedItem ); - expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + await click( + screen.getByRole( 'option', { + name: 'crimson clover', + } ) + ); - await click( currentSelectedItem ); + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); - await click( - screen.getByRole( 'option', { - name: 'poppy', - } ) - ); + await click( currentSelectedItem ); - expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); - } ); - - it( 'Should keep current selection if dropdown is closed without changing selection', async () => { - render( ); + await click( + screen.getByRole( 'option', { + name: 'poppy', + } ) + ); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toBeVisible(); + expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + } ); - await press.Escape(); - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - } ) - ).not.toBeInTheDocument(); + it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + render( ); - expect( currentSelectedItem ).toHaveTextContent( - legacyProps.options[ 0 ].name - ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should apply class only to options that have a className defined', async () => { - render( ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toBeVisible(); + + await press.Escape(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + } ) + ).not.toBeInTheDocument(); + + expect( currentSelectedItem ).toHaveTextContent( + legacyProps.options[ 0 ].name + ); + } ); - // return an array of items _with_ a className added - const itemsWithClass = legacyProps.options.filter( - ( option ) => option.className !== undefined - ); + it( 'Should apply class only to options that have a className defined', async () => { + render( ); - // assert against filtered array - itemsWithClass.map( ( { name } ) => - expect( screen.getByRole( 'option', { name } ) ).toHaveClass( - customClass - ) - ); + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + // return an array of items _with_ a className added + const itemsWithClass = legacyProps.options.filter( + ( option ) => option.className !== undefined + ); + + // assert against filtered array + itemsWithClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveClass( + customClass + ) + ); + + // return an array of items _without_ a className added + const itemsWithoutClass = legacyProps.options.filter( + ( option ) => option.className === undefined + ); + + // assert against filtered array + itemsWithoutClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).not.toHaveClass( + customClass + ) + ); + } ); - // return an array of items _without_ a className added - const itemsWithoutClass = legacyProps.options.filter( - ( option ) => option.className === undefined - ); + it( 'Should apply styles only to options that have styles defined', async () => { + const customStyles = + 'background-color: rgb(127, 255, 212); rotate: 13deg;'; - // assert against filtered array - itemsWithoutClass.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name } ) - ).not.toHaveClass( customClass ) - ); - } ); + render( ); - it( 'Should apply styles only to options that have styles defined', async () => { - const customStyles = - 'background-color: rgb(127, 255, 212); rotate: 13deg;'; + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + // return an array of items _with_ styles added + const styledItems = legacyProps.options.filter( + ( option ) => option.style !== undefined + ); + + // assert against filtered array + styledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( + customStyles + ) + ); + + // return an array of items _without_ styles added + const unstyledItems = legacyProps.options.filter( + ( option ) => option.style === undefined + ); + + // assert against filtered array + unstyledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).not.toHaveStyle( + customStyles + ) + ); + } ); - render( ); + it( 'does not show selected hint by default', async () => { + render( + + ); + await waitFor( () => + expect( + screen.getByRole( 'combobox', { name: 'Custom select' } ) + ).not.toHaveTextContent( 'Hint' ) + ); + } ); - await click( + it( 'shows selected hint when __experimentalShowSelectedHint is set', async () => { + render( + + ); + + await waitFor( () => + expect( screen.getByRole( 'combobox', { expanded: false, } ) - ); - - // return an array of items _with_ styles added - const styledItems = legacyProps.options.filter( - ( option ) => option.style !== undefined - ); + ).toHaveTextContent( /hint/i ) + ); + } ); - // assert against filtered array - styledItems.map( ( { name } ) => - expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( - customStyles - ) - ); + it( 'shows selected hint in list of options when added', async () => { + render( + + ); + + await click( + screen.getByRole( 'combobox', { name: 'Custom select' } ) + ); + + expect( screen.getByRole( 'option', { name: /hint/i } ) ).toBeVisible(); + } ); - // return an array of items _without_ styles added - const unstyledItems = legacyProps.options.filter( - ( option ) => option.style === undefined - ); + it( 'Should return object onChange', async () => { + const mockOnChange = jest.fn(); - // assert against filtered array - unstyledItems.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name } ) - ).not.toHaveStyle( customStyles ) - ); - } ); + render( ); - it( 'does not show selected hint by default', async () => { - render( - - ); - await waitFor( () => - expect( - screen.getByRole( 'combobox', { name: 'Custom select' } ) - ).not.toHaveTextContent( 'Hint' ) - ); - } ); + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + expect( mockOnChange ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( { + inputValue: '', + isOpen: false, + selectedItem: { key: 'violets', name: 'violets' }, + type: '', + } ) + ); + + await click( + screen.getByRole( 'option', { + name: 'aquamarine', + } ) + ); + + expect( mockOnChange ).toHaveBeenNthCalledWith( + 2, + expect.objectContaining( { + inputValue: '', + isOpen: false, + selectedItem: expect.objectContaining( { + name: 'aquamarine', + } ), + type: '', + } ) + ); + } ); - it( 'shows selected hint when __experimentalShowSelectedHint is set', async () => { - render( - - ); + it( 'Should return selectedItem object when specified onChange', async () => { + const mockOnChange = jest.fn( + ( { selectedItem } ) => selectedItem.key + ); - await waitFor( () => - expect( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ).toHaveTextContent( /hint/i ) - ); - } ); + render( ); - it( 'shows selected hint in list of options when added', async () => { - render( - - ); + await sleep(); + await press.Tab(); + expect( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ).toHaveFocus(); - await click( - screen.getByRole( 'combobox', { name: 'Custom select' } ) - ); + await type( 'p' ); + await press.Enter(); - expect( - screen.getByRole( 'option', { name: /hint/i } ) - ).toBeVisible(); - } ); + expect( mockOnChange ).toHaveReturnedWith( 'poppy' ); + } ); - it( 'Should return object onChange', async () => { - const mockOnChange = jest.fn(); + describe( 'Keyboard behavior and accessibility', () => { + it( 'Should be able to change selection using keyboard', async () => { + render( ); - render( - - ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); - expect( mockOnChange ).toHaveBeenNthCalledWith( - 1, - expect.objectContaining( { - inputValue: '', - isOpen: false, - selectedItem: { key: 'violets', name: 'violets' }, - type: '', + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', } ) - ); + ).toHaveFocus(); - await click( - screen.getByRole( 'option', { - name: 'aquamarine', - } ) - ); + await press.ArrowDown(); + await press.Enter(); - expect( mockOnChange ).toHaveBeenNthCalledWith( - 2, - expect.objectContaining( { - inputValue: '', - isOpen: false, - selectedItem: expect.objectContaining( { - name: 'aquamarine', - } ), - type: '', - } ) - ); + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); } ); - it( 'Should return selectedItem object when specified onChange', async () => { - const mockOnChange = jest.fn( - ( { selectedItem } ) => selectedItem.key - ); + it( 'Should be able to type characters to select matching options', async () => { + render( ); - render( - - ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); await sleep(); await press.Tab(); + await press.Enter(); expect( - screen.getByRole( 'combobox', { - expanded: false, + screen.getByRole( 'listbox', { + name: 'label!', } ) ).toHaveFocus(); - await type( 'p' ); + await type( 'a' ); await press.Enter(); - - expect( mockOnChange ).toHaveReturnedWith( 'poppy' ); + expect( currentSelectedItem ).toHaveTextContent( 'amber' ); } ); - describe( 'Keyboard behavior and accessibility', () => { - it( 'Should be able to change selection using keyboard', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await press.ArrowDown(); - await press.Enter(); + it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { + render( ); - expect( currentSelectedItem ).toHaveTextContent( - 'crimson clover' - ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should be able to type characters to select matching options', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await type( 'a' ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'amber' ); - } ); + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); - it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { - render( ); + await type( 'aq' ); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + hidden: true, + } ) + ).not.toBeInTheDocument(); - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + } ); - await type( 'aq' ); + it( 'Should have correct aria-selected value for selections', async () => { + render( ); - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - hidden: true, - } ) - ).not.toBeInTheDocument(); - - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should have correct aria-selected value for selections', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await click( currentSelectedItem ); - - // get all items except for first option - const unselectedItems = legacyProps.options.filter( - ( { name } ) => name !== legacyProps.options[ 0 ].name - ); + await click( currentSelectedItem ); - // assert that all other items have aria-selected="false" - unselectedItems.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name, selected: false } ) - ).toBeVisible() - ); + // get all items except for first option + const unselectedItems = legacyProps.options.filter( + ( { name } ) => name !== legacyProps.options[ 0 ].name + ); - // assert that first item has aria-selected="true" + // assert that all other items have aria-selected="false" + unselectedItems.map( ( { name } ) => expect( - screen.getByRole( 'option', { - name: legacyProps.options[ 0 ].name, - selected: true, - } ) - ).toBeVisible(); + screen.getByRole( 'option', { name, selected: false } ) + ).toBeVisible() + ); - // change the current selection - await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + // assert that first item has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: legacyProps.options[ 0 ].name, + selected: true, + } ) + ).toBeVisible(); - // click combobox to mount listbox with options again - await click( currentSelectedItem ); + // change the current selection + await click( screen.getByRole( 'option', { name: 'poppy' } ) ); - // check that first item is has aria-selected="false" after new selection - expect( - screen.getByRole( 'option', { - name: legacyProps.options[ 0 ].name, - selected: false, - } ) - ).toBeVisible(); + // click combobox to mount listbox with options again + await click( currentSelectedItem ); - // check that new selected item now has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: 'poppy', - selected: true, - } ) - ).toBeVisible(); - } ); + // check that first item is has aria-selected="false" after new selection + expect( + screen.getByRole( 'option', { + name: legacyProps.options[ 0 ].name, + selected: false, + } ) + ).toBeVisible(); + + // check that new selected item now has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: 'poppy', + selected: true, + } ) + ).toBeVisible(); } ); } ); } ); diff --git a/packages/components/src/custom-select-control-v2/test/index.tsx b/packages/components/src/custom-select-control-v2/test/index.tsx index 39476ef45c40b..fc8552b7a612a 100644 --- a/packages/components/src/custom-select-control-v2/test/index.tsx +++ b/packages/components/src/custom-select-control-v2/test/index.tsx @@ -59,42 +59,92 @@ const ControlledCustomSelect = ( props: CustomSelectProps ) => { ); }; -describe( 'With Default Props', () => { - describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', ControlledCustomSelect ], - ] )( '%s', ( ...modeAndComponent ) => { - const [ , Component ] = modeAndComponent; - - it( 'Should replace the initial selection when a new item is selected', async () => { +describe.each( [ + [ 'Uncontrolled', UncontrolledCustomSelect ], + [ 'Controlled', ControlledCustomSelect ], +] )( 'CustomSelectControlV2 (%s)', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; + + it( 'Should replace the initial selection when a new item is selected', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await click( currentSelectedItem ); + + await click( + screen.getByRole( 'option', { + name: 'crimson clover', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + + await click( currentSelectedItem ); + + await click( + screen.getByRole( 'option', { + name: 'poppy', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + } ); + + it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toBeVisible(); + + await press.Escape(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + } ) + ).not.toBeInTheDocument(); + + expect( currentSelectedItem ).toHaveTextContent( items[ 0 ].value ); + } ); + + describe( 'Keyboard behavior and accessibility', () => { + it( 'Should be able to change selection using keyboard', async () => { render( ); const currentSelectedItem = screen.getByRole( 'combobox', { expanded: false, } ); - await click( currentSelectedItem ); + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); - await click( - screen.getByRole( 'option', { - name: 'crimson clover', + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', } ) - ); + ).toHaveFocus(); - expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); - - await click( currentSelectedItem ); - - await click( - screen.getByRole( 'option', { - name: 'poppy', - } ) - ); + await press.ArrowDown(); + await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); } ); - it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + it( 'Should be able to type characters to select matching options', async () => { render( ); const currentSelectedItem = screen.getByRole( 'combobox', { @@ -108,285 +158,174 @@ describe( 'With Default Props', () => { screen.getByRole( 'listbox', { name: 'label!', } ) - ).toBeVisible(); + ).toHaveFocus(); + + await type( 'a' ); + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'amber' ); + } ); + + it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await type( 'aq' ); - await press.Escape(); expect( screen.queryByRole( 'listbox', { name: 'label!', + hidden: true, } ) ).not.toBeInTheDocument(); - expect( currentSelectedItem ).toHaveTextContent( items[ 0 ].value ); + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); } ); - describe( 'Keyboard behavior and accessibility', () => { - it( 'Should be able to change selection using keyboard', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await press.ArrowDown(); - await press.Enter(); + it( 'Should have correct aria-selected value for selections', async () => { + render( ); - expect( currentSelectedItem ).toHaveTextContent( - 'crimson clover' - ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should be able to type characters to select matching options', async () => { - render( ); + await click( currentSelectedItem ); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + // assert that first item has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: 'violets', + selected: true, + } ) + ).toBeVisible(); - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); + // change the current selection + await click( screen.getByRole( 'option', { name: 'poppy' } ) ); - await type( 'a' ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'amber' ); - } ); + // click combobox to mount listbox with options again + await click( currentSelectedItem ); - it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { - render( ); + // check that first item is has aria-selected="false" after new selection + expect( + screen.getByRole( 'option', { + name: 'violets', + selected: false, + } ) + ).toBeVisible(); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + // check that new selected item now has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: 'poppy', + selected: true, + } ) + ).toBeVisible(); + } ); + } ); - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); + describe( 'Multiple selection', () => { + it( 'Should be able to select multiple items when provided an array', async () => { + const onChangeMock = jest.fn(); - await type( 'aq' ); + // initial selection as defaultValue + const defaultValues = [ + 'incandescent glow', + 'ultraviolet morning light', + ]; - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - hidden: true, - } ) - ).not.toBeInTheDocument(); + render( + + { [ + 'aurora borealis green', + 'flamingo pink sunrise', + 'incandescent glow', + 'rose blush', + 'ultraviolet morning light', + ].map( ( item ) => ( + + { item } + + ) ) } + + ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should have correct aria-selected value for selections', async () => { - render( ); + // ensure more than one item is selected due to defaultValues + expect( currentSelectedItem ).toHaveTextContent( + `${ defaultValues.length } items selected` + ); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + await click( currentSelectedItem ); - await click( currentSelectedItem ); + expect( screen.getByRole( 'listbox' ) ).toHaveAttribute( + 'aria-multiselectable' + ); - // assert that first item has aria-selected="true" + // ensure defaultValues are selected in list of items + defaultValues.forEach( ( value ) => expect( screen.getByRole( 'option', { - name: 'violets', + name: value, selected: true, } ) - ).toBeVisible(); - - // change the current selection - await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + ).toBeVisible() + ); - // click combobox to mount listbox with options again - await click( currentSelectedItem ); + // name of next selection + const nextSelectionName = 'rose blush'; - // check that first item is has aria-selected="false" after new selection - expect( - screen.getByRole( 'option', { - name: 'violets', - selected: false, - } ) - ).toBeVisible(); - - // check that new selected item now has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: 'poppy', - selected: true, - } ) - ).toBeVisible(); + // element for next selection + const nextSelection = screen.getByRole( 'option', { + name: nextSelectionName, } ); - } ); - describe( 'Multiple selection', () => { - it( 'Should be able to select multiple items when provided an array', async () => { - const onChangeMock = jest.fn(); - - // initial selection as defaultValue - const defaultValues = [ - 'incandescent glow', - 'ultraviolet morning light', - ]; - - render( - - { [ - 'aurora borealis green', - 'flamingo pink sunrise', - 'incandescent glow', - 'rose blush', - 'ultraviolet morning light', - ].map( ( item ) => ( - - { item } - - ) ) } - - ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - // ensure more than one item is selected due to defaultValues - expect( currentSelectedItem ).toHaveTextContent( - `${ defaultValues.length } items selected` - ); - - await click( currentSelectedItem ); - - expect( screen.getByRole( 'listbox' ) ).toHaveAttribute( - 'aria-multiselectable' - ); - - // ensure defaultValues are selected in list of items - defaultValues.forEach( ( value ) => - expect( - screen.getByRole( 'option', { - name: value, - selected: true, - } ) - ).toBeVisible() - ); - - // name of next selection - const nextSelectionName = 'rose blush'; - - // element for next selection - const nextSelection = screen.getByRole( 'option', { - name: nextSelectionName, - } ); + // click next selection to add another item to current selection + await click( nextSelection ); - // click next selection to add another item to current selection - await click( nextSelection ); + // updated array containing defaultValues + the item just selected + const updatedSelection = defaultValues.concat( nextSelectionName ); - // updated array containing defaultValues + the item just selected - const updatedSelection = - defaultValues.concat( nextSelectionName ); + expect( onChangeMock ).toHaveBeenCalledWith( updatedSelection ); - expect( onChangeMock ).toHaveBeenCalledWith( updatedSelection ); + expect( nextSelection ).toHaveAttribute( 'aria-selected' ); - expect( nextSelection ).toHaveAttribute( 'aria-selected' ); - - // expect increased array length for current selection - expect( currentSelectedItem ).toHaveTextContent( - `${ updatedSelection.length } items selected` - ); - } ); - - it( 'Should be able to deselect items when provided an array', async () => { - // initial selection as defaultValue - const defaultValues = [ - 'aurora borealis green', - 'incandescent glow', - 'key lime green', - 'rose blush', - 'ultraviolet morning light', - ]; - - render( - - { defaultValues.map( ( item ) => ( - - { item } - - ) ) } - - ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await click( currentSelectedItem ); - - // Array containing items to deselect - const nextSelection = [ - 'aurora borealis green', - 'rose blush', - 'incandescent glow', - ]; - - // Deselect some items by clicking them to ensure that changes - // are reflected correctly - await Promise.all( - nextSelection.map( async ( value ) => { - await click( - screen.getByRole( 'option', { name: value } ) - ); - expect( - screen.getByRole( 'option', { - name: value, - selected: false, - } ) - ).toBeVisible(); - } ) - ); - - // expect different array length from defaultValues due to deselecting items - expect( currentSelectedItem ).toHaveTextContent( - `${ - defaultValues.length - nextSelection.length - } items selected` - ); - } ); + // expect increased array length for current selection + expect( currentSelectedItem ).toHaveTextContent( + `${ updatedSelection.length } items selected` + ); } ); - it( 'Should allow rendering a custom value when using `renderSelectedValue`', async () => { - const renderValue = ( value: string | string[] ) => { - return {; - }; + it( 'Should be able to deselect items when provided an array', async () => { + // initial selection as defaultValue + const defaultValues = [ + 'aurora borealis green', + 'incandescent glow', + 'key lime green', + 'rose blush', + 'ultraviolet morning light', + ]; render( - - - { renderValue( 'april-29' ) } - - - { renderValue( 'july-9' ) } - + + { defaultValues.map( ( item ) => ( + + { item } + + ) ) } ); @@ -394,26 +333,77 @@ describe( 'With Default Props', () => { expanded: false, } ); - expect( currentSelectedItem ).toBeVisible(); + await click( currentSelectedItem ); - // expect that the initial selection renders an image - expect( currentSelectedItem ).toContainElement( - screen.getByRole( 'img', { name: 'april-29' } ) + // Array containing items to deselect + const nextSelection = [ + 'aurora borealis green', + 'rose blush', + 'incandescent glow', + ]; + + // Deselect some items by clicking them to ensure that changes + // are reflected correctly + await Promise.all( + nextSelection.map( async ( value ) => { + await click( + screen.getByRole( 'option', { name: value } ) + ); + expect( + screen.getByRole( 'option', { + name: value, + selected: false, + } ) + ).toBeVisible(); + } ) ); - expect( - screen.queryByRole( 'img', { name: 'july-9' } ) - ).not.toBeInTheDocument(); - - await click( currentSelectedItem ); + // expect different array length from defaultValues due to deselecting items + expect( currentSelectedItem ).toHaveTextContent( + `${ + defaultValues.length - nextSelection.length + } items selected` + ); + } ); + } ); - // expect that the other image is only visible after opening popover with options - expect( - screen.getByRole( 'img', { name: 'july-9' } ) - ).toBeVisible(); - expect( - screen.getByRole( 'option', { name: 'july-9' } ) - ).toBeVisible(); + it( 'Should allow rendering a custom value when using `renderSelectedValue`', async () => { + const renderValue = ( value: string | string[] ) => { + return {; + }; + + render( + + + { renderValue( 'april-29' ) } + + + { renderValue( 'july-9' ) } + + + ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); + + expect( currentSelectedItem ).toBeVisible(); + + // expect that the initial selection renders an image + expect( currentSelectedItem ).toContainElement( + screen.getByRole( 'img', { name: 'april-29' } ) + ); + + expect( + screen.queryByRole( 'img', { name: 'july-9' } ) + ).not.toBeInTheDocument(); + + await click( currentSelectedItem ); + + // expect that the other image is only visible after opening popover with options + expect( screen.getByRole( 'img', { name: 'july-9' } ) ).toBeVisible(); + expect( + screen.getByRole( 'option', { name: 'july-9' } ) + ).toBeVisible(); } ); } );