diff --git a/packages/components/src/custom-select-control/test/index.js b/packages/components/src/custom-select-control/test/index.js
index 150afe4aa75f51..52bb841a4f953e 100644
--- a/packages/components/src/custom-select-control/test/index.js
+++ b/packages/components/src/custom-select-control/test/index.js
@@ -4,54 +4,205 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
/**
* Internal dependencies
*/
import CustomSelectControl from '..';
-describe( 'CustomSelectControl', () => {
- it( 'Captures the keypress event and does not let it propagate', async () => {
- const user = userEvent.setup();
- const onKeyDown = jest.fn();
- const options = [
- {
- key: 'one',
- name: 'Option one',
- },
- {
- key: 'two',
- name: 'Option two',
- },
- {
- key: 'three',
- name: 'Option three',
+const customClass = 'amber-skies';
+
+const props = {
+ 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',
},
- ];
+ },
+ ],
+ __nextUnconstrainedWidth: true,
+};
- render(
-
-
-
+const ControlledCustomSelectControl = ( { options } ) => {
+ const [ value, setValue ] = useState( options[ 0 ] );
+ return (
+ setValue( selectedItem ) }
+ value={ options.find( ( option ) => option.key === value.key ) }
+ />
+ );
+};
+
+describe.each( [
+ [ 'uncontrolled', CustomSelectControl ],
+ [ 'controlled', ControlledCustomSelectControl ],
+] )( 'CustomSelectControl %s', ( ...modeAndComponent ) => {
+ const [ , Component ] = modeAndComponent;
+
+ it( 'Should replace the initial selection when a new item is selected', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const currentSelectedItem = screen.getByRole( 'button', {
+ expanded: false,
+ } );
+
+ await user.click( currentSelectedItem );
+
+ await user.click(
+ screen.getByRole( 'option', {
+ name: 'crimson clover',
+ } )
+ );
+
+ expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' );
+
+ await user.click( currentSelectedItem );
+
+ await user.click(
+ screen.getByRole( 'option', {
+ name: 'poppy',
+ } )
+ );
+
+ expect( currentSelectedItem ).toHaveTextContent( 'poppy' );
+ } );
+
+ it( 'Should keep current selection if dropdown is closed without changing selection', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const currentSelectedItem = screen.getByRole( 'button', {
+ expanded: false,
+ } );
+
+ await user.tab();
+ await user.keyboard( '{enter}' );
+ expect(
+ screen.getByRole( 'listbox', {
+ name: 'label!',
+ } )
+ ).toBeVisible();
+
+ await user.keyboard( '{escape}' );
+ expect(
+ screen.queryByRole( 'listbox', {
+ name: 'label!',
+ } )
+ ).not.toBeInTheDocument();
+
+ expect( currentSelectedItem ).toHaveTextContent(
+ props.options[ 0 ].name
+ );
+ } );
+
+ it( 'Should apply class only to options that have a className defined', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ await user.click(
+ screen.getByRole( 'button', {
+ expanded: false,
+ } )
+ );
+
+ // return an array of items _with_ a className added
+ const itemsWithClass = props.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 = props.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 user = userEvent.setup();
+ const customStyles =
+ 'background-color: rgb(127, 255, 212); rotate: 13deg;';
+
+ render( );
+
+ await user.click(
+ screen.getByRole( 'button', {
+ expanded: false,
+ } )
+ );
+
+ // return an array of items _with_ styles added
+ const styledItems = props.options.filter(
+ ( option ) => option.style !== undefined
);
- const toggleButton = screen.getByRole( 'button' );
- await user.click( toggleButton );
- const customSelect = screen.getByRole( 'listbox' );
- await user.type( customSelect, '{enter}' );
+ // assert against filtered array
+ styledItems.map( ( { name } ) =>
+ expect( screen.getByRole( 'option', { name } ) ).toHaveStyle(
+ customStyles
+ )
+ );
+
+ // return an array of items _without_ styles added
+ const unstyledItems = props.options.filter(
+ ( option ) => option.style === undefined
+ );
- expect( onKeyDown ).toHaveBeenCalledTimes( 0 );
+ // assert against filtered array
+ unstyledItems.map( ( { name } ) =>
+ expect( screen.getByRole( 'option', { name } ) ).not.toHaveStyle(
+ customStyles
+ )
+ );
} );
it( 'does not show selected hint by default', () => {
render(
{
__experimentalHint: 'Hint',
},
] }
- __nextUnconstrainedWidth
/>
);
expect(
@@ -71,6 +221,7 @@ describe( 'CustomSelectControl', () => {
it( 'shows selected hint when __experimentalShowSelectedHint is set', () => {
render(
{
},
] }
__experimentalShowSelectedHint
- __nextUnconstrainedWidth
/>
);
expect(
screen.getByRole( 'button', { name: 'Custom select' } )
).toHaveTextContent( 'Hint' );
} );
+
+ describe( 'Keyboard behavior and accessibility', () => {
+ it( 'Captures the keypress event and does not let it propagate', async () => {
+ const user = userEvent.setup();
+ const onKeyDown = jest.fn();
+
+ render(
+
+
+
+ );
+ const currentSelectedItem = screen.getByRole( 'button', {
+ expanded: false,
+ } );
+ await user.click( currentSelectedItem );
+
+ const customSelect = screen.getByRole( 'listbox', {
+ name: 'label!',
+ } );
+ await user.type( customSelect, '{enter}' );
+
+ expect( onKeyDown ).toHaveBeenCalledTimes( 0 );
+ } );
+
+ it( 'Should be able to change selection using keyboard', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const currentSelectedItem = screen.getByRole( 'button', {
+ expanded: false,
+ } );
+
+ await user.tab();
+ expect( currentSelectedItem ).toHaveFocus();
+
+ await user.keyboard( '{enter}' );
+ expect(
+ screen.getByRole( 'listbox', {
+ name: 'label!',
+ } )
+ ).toHaveFocus();
+
+ await user.keyboard( '{arrowdown}' );
+ await user.keyboard( '{enter}' );
+
+ expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' );
+ } );
+
+ it( 'Should be able to type characters to select matching options', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const currentSelectedItem = screen.getByRole( 'button', {
+ expanded: false,
+ } );
+
+ await user.tab();
+ await user.keyboard( '{enter}' );
+ expect(
+ screen.getByRole( 'listbox', {
+ name: 'label!',
+ } )
+ ).toHaveFocus();
+
+ await user.keyboard( '{a}' );
+ await user.keyboard( '{enter}' );
+ expect( currentSelectedItem ).toHaveTextContent( 'amber' );
+ } );
+
+ it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const currentSelectedItem = screen.getByRole( 'button', {
+ expanded: false,
+ } );
+
+ await user.tab();
+ expect( currentSelectedItem ).toHaveFocus();
+
+ await user.keyboard( '{a}' );
+ await user.keyboard( '{q}' );
+
+ expect(
+ screen.queryByRole( 'listbox', {
+ name: 'label!',
+ hidden: true,
+ } )
+ ).not.toBeInTheDocument();
+
+ await user.keyboard( '{enter}' );
+ expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' );
+ } );
+
+ it( 'Should have correct aria-selected value for selections', async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const currentSelectedItem = screen.getByRole( 'button', {
+ expanded: false,
+ } );
+
+ await user.click( currentSelectedItem );
+
+ // get all items except for first option
+ const unselectedItems = props.options.filter(
+ ( { name } ) => name !== props.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: props.options[ 0 ].name,
+ selected: true,
+ } )
+ ).toBeVisible();
+
+ // change the current selection
+ await user.click( screen.getByRole( 'option', { name: 'poppy' } ) );
+
+ // click button to mount listbox with options again
+ await user.click( currentSelectedItem );
+
+ // check that first item is has aria-selected="false" after new selection
+ expect(
+ screen.getByRole( 'option', {
+ name: props.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();
+ } );
+
+ it( 'Should call custom event handlers', async () => {
+ const user = userEvent.setup();
+ const onFocusMock = jest.fn();
+ const onBlurMock = jest.fn();
+
+ render(
+
+ );
+
+ const currentSelectedItem = screen.getByRole( 'button', {
+ expanded: false,
+ } );
+
+ await user.tab();
+
+ expect( currentSelectedItem ).toHaveFocus();
+ expect( onFocusMock ).toHaveBeenCalledTimes( 1 );
+
+ await user.tab();
+ expect( currentSelectedItem ).not.toHaveFocus();
+ expect( onBlurMock ).toHaveBeenCalledTimes( 1 );
+ } );
+ } );
} );