From 0cc7e36d433276a6039ac7e82866cd126bce9ac9 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Wed, 6 Sep 2023 18:43:33 +0100 Subject: [PATCH 01/17] Adding tests for `AlignmentMatrixControl` Making sure we have sufficient coverage to be confident that any changes don't break this component. --- .../alignment-matrix-control/test/index.tsx | 93 ++++++++++++++++--- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/test/index.tsx b/packages/components/src/alignment-matrix-control/test/index.tsx index a99f6d70135c5f..1e1c5fef55da64 100644 --- a/packages/components/src/alignment-matrix-control/test/index.tsx +++ b/packages/components/src/alignment-matrix-control/test/index.tsx @@ -24,27 +24,90 @@ describe( 'AlignmentMatrixControl', () => { expect( getControl() ).toBeInTheDocument(); } ); + + it( 'should be centered by default', async () => { + const user = userEvent.setup(); + + render( ); + + await user.tab(); + expect( getCell( 'center center' ) ).toHaveFocus(); + } ); } ); - describe( 'Change value', () => { - const alignments = [ 'center left', 'center center', 'bottom center' ]; - const user = userEvent.setup(); + describe( 'Should change value', () => { + describe( 'with Mouse', () => { + describe( 'on cell click', () => { + it.each( [ + 'top left', + 'top center', + 'top right', + 'center left', + 'center center', + 'center right', + 'bottom left', + 'bottom center', + 'bottom right', + ] )( '%s', async ( alignment ) => { + const user = userEvent.setup(); + const spy = jest.fn(); + + render( + + ); - it.each( alignments )( - 'should change value on %s cell click', - async ( alignment ) => { - const spy = jest.fn(); + await user.click( getCell( alignment ) ); - render( - - ); + expect( getCell( alignment ) ).toHaveFocus(); - await user.click( getCell( alignment ) ); + expect( spy ).toHaveBeenCalledWith( alignment ); + } ); + } ); + } ); - expect( getCell( alignment ) ).toHaveFocus(); + describe( 'with Keyboard', () => { + describe( 'on arrow press', () => { + it.each( [ + [ 'ArrowUp', 'top center' ], + [ 'ArrowLeft', 'center left' ], + [ 'ArrowDown', 'bottom center' ], + [ 'ArrowRight', 'center right' ], + ] )( '%s', async ( keyRef, cellRef ) => { + const user = userEvent.setup(); + const spy = jest.fn(); - expect( spy ).toHaveBeenCalledWith( alignment ); - } - ); + render( ); + + await user.tab(); + await user.keyboard( `[${ keyRef }]` ); + expect( getCell( cellRef ) ).toHaveFocus(); + expect( spy ).toHaveBeenCalledWith( cellRef ); + } ); + } ); + + describe( 'but not at at edge', () => { + it.each( [ + [ 'ArrowUp', 'top left' ], + [ 'ArrowLeft', 'top left' ], + [ 'ArrowDown', 'bottom right' ], + [ 'ArrowRight', 'bottom right' ], + ] )( '%s', async ( keyRef, cellRef ) => { + const user = userEvent.setup(); + const spy = jest.fn(); + + render( ); + + const cell = getCell( cellRef ); + await user.click( cell ); + + await user.keyboard( `[${ keyRef }]` ); + expect( cell ).toHaveFocus(); + expect( spy ).toHaveBeenCalledWith( cellRef ); + } ); + } ); + } ); } ); } ); From c1cf571a2fe7731ec2d54806c083c3d9b49c0adf Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Wed, 6 Sep 2023 18:45:21 +0100 Subject: [PATCH 02/17] Adding `ariakit` Composite reference Temporarily writing it to v2 so other components continue to work while this is ongoing. --- packages/components/src/composite/v2.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 packages/components/src/composite/v2.ts diff --git a/packages/components/src/composite/v2.ts b/packages/components/src/composite/v2.ts new file mode 100644 index 00000000000000..30ea466502ca53 --- /dev/null +++ b/packages/components/src/composite/v2.ts @@ -0,0 +1,16 @@ +/** + * Composite is a component that may contain navigable items represented by + * CompositeItem. It's inspired by the WAI-ARIA Composite Role and implements + * all the keyboard navigation mechanisms to ensure that there's only one + * tab stop for the whole Composite element. This means that it can behave as + * a roving tabindex or aria-activedescendant container. + * + * @see https://ariakit.org/components/composite + */ +export { + Composite, + CompositeGroup, + CompositeItem, + CompositeRow, + useCompositeStore, +} from '@ariakit/react'; From 9b5e714dfb40646000f30f6a602c98b1838931df Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Wed, 6 Sep 2023 18:56:30 +0100 Subject: [PATCH 03/17] First pass at `ariakit` implementation --- .../src/alignment-matrix-control/cell.tsx | 2 +- .../src/alignment-matrix-control/index.tsx | 66 ++++++++----------- .../src/alignment-matrix-control/utils.tsx | 33 ++++++++-- 3 files changed, 57 insertions(+), 44 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/cell.tsx b/packages/components/src/alignment-matrix-control/cell.tsx index b6e19c56022915..d3f14c2c06c82a 100644 --- a/packages/components/src/alignment-matrix-control/cell.tsx +++ b/packages/components/src/alignment-matrix-control/cell.tsx @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { CompositeItem } from '../composite'; +import { CompositeItem } from '../composite/v2'; import Tooltip from '../tooltip'; import { VisuallyHidden } from '../visually-hidden'; diff --git a/packages/components/src/alignment-matrix-control/index.tsx b/packages/components/src/alignment-matrix-control/index.tsx index 0e34e4052346da..0994c212f909da 100644 --- a/packages/components/src/alignment-matrix-control/index.tsx +++ b/packages/components/src/alignment-matrix-control/index.tsx @@ -8,16 +8,16 @@ import classnames from 'classnames'; */ import { __, isRTL } from '@wordpress/i18n'; import { useInstanceId } from '@wordpress/compose'; -import { useState, useEffect } from '@wordpress/element'; +import { useState } from '@wordpress/element'; /** * Internal dependencies */ import Cell from './cell'; -import { Composite, CompositeGroup, useCompositeState } from '../composite'; +import { Composite, CompositeRow, useCompositeStore } from '../composite/v2'; import { Root, Row } from './styles/alignment-matrix-control-styles'; import AlignmentMatrixControlIcon from './icon'; -import { GRID, getItemId } from './utils'; +import { GRID, getItemId, getItemValue, normalizeValue } from './utils'; import type { WordPressComponentProps } from '../ui/context'; import type { AlignmentMatrixControlProps, @@ -26,15 +26,6 @@ import type { const noop = () => {}; -function useBaseId( id?: string ) { - const instanceId = useInstanceId( - AlignmentMatrixControl, - 'alignment-matrix-control' - ); - - return id || instanceId; -} - /** * * AlignmentMatrixControl components enable adjustments to horizontal and vertical alignments for UI. @@ -65,27 +56,31 @@ export function AlignmentMatrixControl( { width = 92, ...props }: WordPressComponentProps< AlignmentMatrixControlProps, 'div', false > ) { - const [ immutableDefaultValue ] = useState( value ?? defaultValue ); - const baseId = useBaseId( id ); - const initialCurrentId = getItemId( baseId, immutableDefaultValue ); + const [ immutableDefaultValue ] = useState( + normalizeValue( value ?? defaultValue ) + ); + const currentCell = normalizeValue( value ?? immutableDefaultValue ); - const composite = useCompositeState( { - baseId, - currentId: initialCurrentId, - rtl: isRTL(), - } ); + const baseId = useInstanceId( + AlignmentMatrixControl, + 'alignment-matrix-control', + id + ); + const defaultActiveId = getItemId( baseId, immutableDefaultValue ); const handleOnChange = ( nextValue: AlignmentMatrixControlValue ) => { onChange( nextValue ); }; - const { setCurrentId } = composite; - - useEffect( () => { - if ( typeof value !== 'undefined' ) { - setCurrentId( getItemId( baseId, value ) ); - } - }, [ value, setCurrentId, baseId ] ); + const compositeStore = useCompositeStore( { + defaultActiveId, + setActiveId: ( activeId ) => { + if ( activeId ) { + handleOnChange( getItemValue( baseId, activeId ) ); + } + }, + rtl: isRTL(), + } ); const classes = classnames( 'component-alignment-matrix-control', @@ -95,37 +90,30 @@ export function AlignmentMatrixControl( { return ( { GRID.map( ( cells, index ) => ( - + { cells.map( ( cell ) => { const cellId = getItemId( baseId, cell ); - const isActive = composite.currentId === cellId; + const isActive = cell === currentCell; return ( handleOnChange( cell ) } - tabIndex={ isActive ? 0 : -1 } /> ); } ) } - + ) ) } ); diff --git a/packages/components/src/alignment-matrix-control/utils.tsx b/packages/components/src/alignment-matrix-control/utils.tsx index ba5e113c42fc7f..7f0e2b08e3abfd 100644 --- a/packages/components/src/alignment-matrix-control/utils.tsx +++ b/packages/components/src/alignment-matrix-control/utils.tsx @@ -31,16 +31,28 @@ export const ALIGNMENT_LABEL: Record< AlignmentMatrixControlValue, string > = { export const ALIGNMENTS = GRID.flat(); /** - * Parses and transforms an incoming value to better match the alignment values + * Normalizes an incoming value to better match the alignment values + * + * @param value an alignment value to normalize + * + * @return The normalized value + */ +export function normalizeValue( value: AlignmentMatrixControlValue ) { + return value === 'center' ? 'center center' : value; +} + +/** + * Normalizes and transforms an incoming value to better match the alignment values * * @param value An alignment value to parse. * * @return The parsed value. */ export function transformValue( value: AlignmentMatrixControlValue ) { - const nextValue = value === 'center' ? 'center center' : value; - - return nextValue.replace( '-', ' ' ) as AlignmentMatrixControlValue; + return normalizeValue( value ).replace( + '-', + ' ' + ) as AlignmentMatrixControlValue; } /** @@ -60,6 +72,19 @@ export function getItemId( return `${ prefixId }-${ valueId }`; } +/** + * Extracts an item value from its ID + * + * @param prefixId An ID prefix to remove + * @param id An item ID + * @return The item value + */ +export function getItemValue( prefixId: string, id: string ) { + return transformValue( + id.replace( prefixId + '-', '' ) as AlignmentMatrixControlValue + ); +} + /** * Retrieves the alignment index from a value. * From 264184de7b15c62f18c2e3a1bc3f7bfd9a5b0dc6 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Tue, 12 Sep 2023 16:10:09 +0100 Subject: [PATCH 04/17] Moving change handler back to individual cells --- .../components/src/alignment-matrix-control/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/index.tsx b/packages/components/src/alignment-matrix-control/index.tsx index 0994c212f909da..1d8a1a08c42600 100644 --- a/packages/components/src/alignment-matrix-control/index.tsx +++ b/packages/components/src/alignment-matrix-control/index.tsx @@ -17,7 +17,7 @@ import Cell from './cell'; import { Composite, CompositeRow, useCompositeStore } from '../composite/v2'; import { Root, Row } from './styles/alignment-matrix-control-styles'; import AlignmentMatrixControlIcon from './icon'; -import { GRID, getItemId, getItemValue, normalizeValue } from './utils'; +import { GRID, getItemId, normalizeValue } from './utils'; import type { WordPressComponentProps } from '../ui/context'; import type { AlignmentMatrixControlProps, @@ -74,11 +74,6 @@ export function AlignmentMatrixControl( { const compositeStore = useCompositeStore( { defaultActiveId, - setActiveId: ( activeId ) => { - if ( activeId ) { - handleOnChange( getItemValue( baseId, activeId ) ); - } - }, rtl: isRTL(), } ); @@ -110,6 +105,7 @@ export function AlignmentMatrixControl( { isActive={ isActive } key={ cell } value={ cell } + onFocus={ () => handleOnChange( cell ) } /> ); } ) } From 2d61c6305a7a22974f6e603c19cb495e1ce3eaff Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Tue, 12 Sep 2023 16:10:52 +0100 Subject: [PATCH 05/17] Updating tests --- .../alignment-matrix-control/test/index.tsx | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/test/index.tsx b/packages/components/src/alignment-matrix-control/test/index.tsx index 1e1c5fef55da64..c1cb2aeb8b557e 100644 --- a/packages/components/src/alignment-matrix-control/test/index.tsx +++ b/packages/components/src/alignment-matrix-control/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, screen, within } from '@testing-library/react'; +import { act, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** @@ -17,10 +17,23 @@ const getCell = ( name: string ) => { return within( getControl() ).getByRole( 'gridcell', { name } ); }; +const asyncRender = async ( jsx: any ) => { + const view = render( jsx ); + await act( async () => { + // This allows the component to properly establish + // its initial state, that sometimes isn't otherwise + // ready in time for tests to start. + await new Promise( requestAnimationFrame ); + await new Promise( requestAnimationFrame ); + await new Promise( requestAnimationFrame ); + } ); + return view; +}; + describe( 'AlignmentMatrixControl', () => { describe( 'Basic rendering', () => { - it( 'should render', () => { - render( ); + it( 'should render', async () => { + await asyncRender( ); expect( getControl() ).toBeInTheDocument(); } ); @@ -28,9 +41,10 @@ describe( 'AlignmentMatrixControl', () => { it( 'should be centered by default', async () => { const user = userEvent.setup(); - render( ); + await asyncRender( ); await user.tab(); + expect( getCell( 'center center' ) ).toHaveFocus(); } ); } ); @@ -52,7 +66,7 @@ describe( 'AlignmentMatrixControl', () => { const user = userEvent.setup(); const spy = jest.fn(); - render( + await asyncRender( { await user.click( getCell( alignment ) ); expect( getCell( alignment ) ).toHaveFocus(); - expect( spy ).toHaveBeenCalledWith( alignment ); } ); } ); @@ -79,10 +92,13 @@ describe( 'AlignmentMatrixControl', () => { const user = userEvent.setup(); const spy = jest.fn(); - render( ); + await asyncRender( + + ); await user.tab(); await user.keyboard( `[${ keyRef }]` ); + expect( getCell( cellRef ) ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( cellRef ); } ); @@ -98,12 +114,14 @@ describe( 'AlignmentMatrixControl', () => { const user = userEvent.setup(); const spy = jest.fn(); - render( ); + await asyncRender( + + ); const cell = getCell( cellRef ); await user.click( cell ); - await user.keyboard( `[${ keyRef }]` ); + expect( cell ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( cellRef ); } ); From 54c87ded4d74d64f797fa771ddeab6e1d9e1128b Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Fri, 22 Sep 2023 20:31:45 +0100 Subject: [PATCH 06/17] Fixing test setup --- .../src/alignment-matrix-control/test/index.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/test/index.tsx b/packages/components/src/alignment-matrix-control/test/index.tsx index c1cb2aeb8b557e..a1f8d37b0a7d28 100644 --- a/packages/components/src/alignment-matrix-control/test/index.tsx +++ b/packages/components/src/alignment-matrix-control/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { act, render, screen, within } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** @@ -19,13 +19,8 @@ const getCell = ( name: string ) => { const asyncRender = async ( jsx: any ) => { const view = render( jsx ); - await act( async () => { - // This allows the component to properly establish - // its initial state, that sometimes isn't otherwise - // ready in time for tests to start. - await new Promise( requestAnimationFrame ); - await new Promise( requestAnimationFrame ); - await new Promise( requestAnimationFrame ); + await waitFor( () => { + expect( getCell( 'top left' ) ).toHaveAttribute( 'tabindex', '-1' ); } ); return view; }; From 8a53dd4cca03c982544055c8d6c6d1866a68ab23 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Fri, 22 Sep 2023 20:32:40 +0100 Subject: [PATCH 07/17] Exporting `ariakit` composite in private API --- packages/components/src/private-apis.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index 6e17abde0c627e..43c140ff6c9aa4 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -6,6 +6,13 @@ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/pri /** * Internal dependencies */ +import { + Composite as CompositeV2, + CompositeGroup as CompositeGroupV2, + CompositeItem as CompositeItemV2, + CompositeRow as CompositeRowV2, + useCompositeStore as useCompositeStoreV2, +} from './composite/v2'; import { default as CustomSelectControl } from './custom-select-control'; import { positionToPlacement as __experimentalPopoverLegacyPositionToPlacement } from './popover/utils'; import { default as ProgressBar } from './progress-bar'; @@ -33,6 +40,11 @@ export const { lock, unlock } = export const privateApis = {}; lock( privateApis, { + CompositeV2, + CompositeGroupV2, + CompositeItemV2, + CompositeRowV2, + useCompositeStoreV2, CustomSelectControl, __experimentalPopoverLegacyPositionToPlacement, createPrivateSlotFill, From bf0e861a0a8f962fc0041c712828d301505b907e Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Thu, 28 Sep 2023 16:14:51 +0100 Subject: [PATCH 08/17] Reworking `AlignmentMatrixControl` --- .../src/alignment-matrix-control/index.tsx | 34 ++++++++-------- .../stories/index.story.tsx | 10 ++--- .../alignment-matrix-control/test/index.tsx | 26 ++++++++++-- .../src/alignment-matrix-control/types.ts | 5 +++ .../src/alignment-matrix-control/utils.tsx | 40 ++++++++++++------- 5 files changed, 73 insertions(+), 42 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/index.tsx b/packages/components/src/alignment-matrix-control/index.tsx index 1d8a1a08c42600..14f94de42ee49b 100644 --- a/packages/components/src/alignment-matrix-control/index.tsx +++ b/packages/components/src/alignment-matrix-control/index.tsx @@ -8,7 +8,6 @@ import classnames from 'classnames'; */ import { __, isRTL } from '@wordpress/i18n'; import { useInstanceId } from '@wordpress/compose'; -import { useState } from '@wordpress/element'; /** * Internal dependencies @@ -17,15 +16,14 @@ import Cell from './cell'; import { Composite, CompositeRow, useCompositeStore } from '../composite/v2'; import { Root, Row } from './styles/alignment-matrix-control-styles'; import AlignmentMatrixControlIcon from './icon'; -import { GRID, getItemId, normalizeValue } from './utils'; +import { GRID, getItemId, getItemValue } from './utils'; import type { WordPressComponentProps } from '../ui/context'; import type { AlignmentMatrixControlProps, AlignmentMatrixControlValue, + VoidableAlignmentMatrixControlValue, } from './types'; -const noop = () => {}; - /** * * AlignmentMatrixControl components enable adjustments to horizontal and vertical alignments for UI. @@ -52,31 +50,32 @@ export function AlignmentMatrixControl( { label = __( 'Alignment Matrix Control' ), defaultValue = 'center center', value, - onChange = noop, + onChange, width = 92, ...props }: WordPressComponentProps< AlignmentMatrixControlProps, 'div', false > ) { - const [ immutableDefaultValue ] = useState( - normalizeValue( value ?? defaultValue ) - ); - const currentCell = normalizeValue( value ?? immutableDefaultValue ); - const baseId = useInstanceId( AlignmentMatrixControl, 'alignment-matrix-control', id ); - const defaultActiveId = getItemId( baseId, immutableDefaultValue ); - - const handleOnChange = ( nextValue: AlignmentMatrixControlValue ) => { - onChange( nextValue ); - }; const compositeStore = useCompositeStore( { - defaultActiveId, + defaultActiveId: getItemId( baseId, defaultValue ), + activeId: getItemId( baseId, value ), + setActiveId: ( nextActiveId ) => { + onChange?.( + getItemValue( + baseId, + nextActiveId as VoidableAlignmentMatrixControlValue + ) as AlignmentMatrixControlValue + ); + }, rtl: isRTL(), } ); + const activeId = compositeStore.useState( 'activeId' ); + const classes = classnames( 'component-alignment-matrix-control', className @@ -97,7 +96,7 @@ export function AlignmentMatrixControl( { { cells.map( ( cell ) => { const cellId = getItemId( baseId, cell ); - const isActive = cell === currentCell; + const isActive = cellId === activeId; return ( handleOnChange( cell ) } /> ); } ) } diff --git a/packages/components/src/alignment-matrix-control/stories/index.story.tsx b/packages/components/src/alignment-matrix-control/stories/index.story.tsx index 24b496d1f2432b..03bec9d92a8b78 100644 --- a/packages/components/src/alignment-matrix-control/stories/index.story.tsx +++ b/packages/components/src/alignment-matrix-control/stories/index.story.tsx @@ -6,7 +6,7 @@ import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies */ -import { useEffect, useState } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { Icon } from '@wordpress/icons'; /** @@ -24,10 +24,11 @@ const meta: Meta< typeof AlignmentMatrixControl > = { 'AlignmentMatrixControl.Icon': AlignmentMatrixControl.Icon, }, argTypes: { - onChange: { action: 'onChange', control: { type: null } }, + onChange: { control: { type: null } }, value: { control: { type: null } }, }, parameters: { + actions: { argTypesRegex: '^on.*' }, controls: { expanded: true }, docs: { canvas: { sourceState: 'shown' } }, }, @@ -42,11 +43,6 @@ const Template: StoryFn< typeof AlignmentMatrixControl > = ( { const [ value, setValue ] = useState< AlignmentMatrixControlProps[ 'value' ] >(); - // Convenience handler for Canvas view so changes are reflected - useEffect( () => { - setValue( defaultValue ); - }, [ defaultValue ] ); - return ( { 'top center', 'top right', 'center left', - 'center center', 'center right', 'bottom left', 'bottom center', @@ -68,11 +67,32 @@ describe( 'AlignmentMatrixControl', () => { /> ); - await user.click( getCell( alignment ) ); + const cell = getCell( alignment ); - expect( getCell( alignment ) ).toHaveFocus(); + await user.click( cell ); + + expect( cell ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( alignment ); } ); + + it( 'unless already focused', async () => { + const user = userEvent.setup(); + const spy = jest.fn(); + + await asyncRender( + + ); + + const cell = getCell( 'center center' ); + + await user.click( cell ); + + expect( cell ).toHaveFocus(); + expect( spy ).not.toHaveBeenCalled(); + } ); } ); } ); diff --git a/packages/components/src/alignment-matrix-control/types.ts b/packages/components/src/alignment-matrix-control/types.ts index 892781234e694c..a9bb96b3e8c190 100644 --- a/packages/components/src/alignment-matrix-control/types.ts +++ b/packages/components/src/alignment-matrix-control/types.ts @@ -10,6 +10,11 @@ export type AlignmentMatrixControlValue = | 'bottom center' | 'bottom right'; +export type VoidableAlignmentMatrixControlValue = + | AlignmentMatrixControlValue + | null + | undefined; + export type AlignmentMatrixControlProps = { /** * Accessible label. If provided, sets the `aria-label` attribute of the diff --git a/packages/components/src/alignment-matrix-control/utils.tsx b/packages/components/src/alignment-matrix-control/utils.tsx index 7f0e2b08e3abfd..fe973788e7c236 100644 --- a/packages/components/src/alignment-matrix-control/utils.tsx +++ b/packages/components/src/alignment-matrix-control/utils.tsx @@ -2,10 +2,14 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ -import type { AlignmentMatrixControlValue } from './types'; +import type { + AlignmentMatrixControlValue, + VoidableAlignmentMatrixControlValue, +} from './types'; export const GRID: AlignmentMatrixControlValue[][] = [ [ 'top left', 'top center', 'top right' ], @@ -37,7 +41,7 @@ export const ALIGNMENTS = GRID.flat(); * * @return The normalized value */ -export function normalizeValue( value: AlignmentMatrixControlValue ) { +export function normalizeValue( value: VoidableAlignmentMatrixControlValue ) { return value === 'center' ? 'center center' : value; } @@ -48,11 +52,11 @@ export function normalizeValue( value: AlignmentMatrixControlValue ) { * * @return The parsed value. */ -export function transformValue( value: AlignmentMatrixControlValue ) { - return normalizeValue( value ).replace( +export function transformValue( value: VoidableAlignmentMatrixControlValue ) { + return normalizeValue( value )?.replace( '-', ' ' - ) as AlignmentMatrixControlValue; + ) as VoidableAlignmentMatrixControlValue; } /** @@ -65,11 +69,10 @@ export function transformValue( value: AlignmentMatrixControlValue ) { */ export function getItemId( prefixId: string, - value: AlignmentMatrixControlValue + value: VoidableAlignmentMatrixControlValue ) { - const valueId = transformValue( value ).replace( ' ', '-' ); - - return `${ prefixId }-${ valueId }`; + const valueId = transformValue( value )?.replace( ' ', '-' ); + return valueId && `${ prefixId }-${ valueId }`; } /** @@ -79,9 +82,12 @@ export function getItemId( * @param id An item ID * @return The item value */ -export function getItemValue( prefixId: string, id: string ) { +export function getItemValue( + prefixId: string, + id: VoidableAlignmentMatrixControlValue +) { return transformValue( - id.replace( prefixId + '-', '' ) as AlignmentMatrixControlValue + id?.replace( prefixId + '-', '' ) as VoidableAlignmentMatrixControlValue ); } @@ -93,10 +99,16 @@ export function getItemValue( prefixId: string, id: string ) { * @return The index of a matching alignment. */ export function getAlignmentIndex( - alignment: AlignmentMatrixControlValue = 'center' + alignment: VoidableAlignmentMatrixControlValue = 'center' ) { - const item = transformValue( alignment ); - const index = ALIGNMENTS.indexOf( item ); + const transformedValue = transformValue( + alignment + ) as AlignmentMatrixControlValue; + + // `transformedValue` could come back as undefined or null, + // but `indexOf` will still return -1, so we can proceed + // without any problems. + const index = ALIGNMENTS.indexOf( transformedValue ); return index > -1 ? index : undefined; } From fc212d6bc7817374d95e1ddb62da007a40b1f682 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Thu, 28 Sep 2023 21:40:36 +0100 Subject: [PATCH 09/17] Small test set-up refactor --- .../components/src/alignment-matrix-control/test/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/test/index.tsx b/packages/components/src/alignment-matrix-control/test/index.tsx index a57b2d7f9acbbe..64e8ecd36a5866 100644 --- a/packages/components/src/alignment-matrix-control/test/index.tsx +++ b/packages/components/src/alignment-matrix-control/test/index.tsx @@ -17,10 +17,10 @@ const getCell = ( name: string ) => { return within( getControl() ).getByRole( 'gridcell', { name } ); }; -const asyncRender = async ( jsx: any ) => { +const asyncRender = async ( jsx: any, focusedCell = 'center center' ) => { const view = render( jsx ); await waitFor( () => { - expect( getCell( 'top left' ) ).toHaveAttribute( 'tabindex', '-1' ); + expect( getCell( focusedCell ) ).toHaveAttribute( 'tabindex', '0' ); } ); return view; }; From 96a3e60b900646ea59499d381a14482b15c6da11 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Thu, 28 Sep 2023 21:44:07 +0100 Subject: [PATCH 10/17] Post-merge deprecation refactor --- .../src/alignment-matrix-control/cell.tsx | 6 +++++- .../src/alignment-matrix-control/index.tsx | 21 +++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/cell.tsx b/packages/components/src/alignment-matrix-control/cell.tsx index 1a4d60a45226c0..162ca879f1a7e5 100644 --- a/packages/components/src/alignment-matrix-control/cell.tsx +++ b/packages/components/src/alignment-matrix-control/cell.tsx @@ -17,6 +17,7 @@ import type { AlignmentMatrixControlCellProps } from './types'; import type { WordPressComponentProps } from '../context'; export default function Cell( { + id, isActive = false, value, ...props @@ -25,7 +26,10 @@ export default function Cell( { return ( - + } + > { /* VoiceOver needs a text content to be rendered within grid cell, otherwise it'll announce the content as "blank". So we use a visually hidden element instead of aria-label. */ } diff --git a/packages/components/src/alignment-matrix-control/index.tsx b/packages/components/src/alignment-matrix-control/index.tsx index 72601aec22155b..248322b084c5da 100644 --- a/packages/components/src/alignment-matrix-control/index.tsx +++ b/packages/components/src/alignment-matrix-control/index.tsx @@ -62,7 +62,7 @@ export function AlignmentMatrixControl( { const compositeStore = useCompositeStore( { defaultActiveId: getItemId( baseId, defaultValue ), - activeId: getItemId( baseId, value ), + activeId: value && getItemId( baseId, value ), setActiveId: ( nextActiveId ) => { onChange?.( getItemValue( @@ -83,17 +83,20 @@ export function AlignmentMatrixControl( { return ( + } > { GRID.map( ( cells, index ) => ( - + } key={ index }> { cells.map( ( cell ) => { const cellId = getItemId( baseId, cell ); const isActive = cellId === activeId; From 1f3478195de9670a916bdf901b8330cfc2873ab3 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Fri, 29 Sep 2023 00:35:58 +0100 Subject: [PATCH 11/17] Refactoring `CircularOptionPicker` --- .../circular-option-picker-option.tsx | 49 ++++++++----------- .../circular-option-picker.tsx | 31 +++--------- .../src/circular-option-picker/types.ts | 13 +++-- packages/components/src/composite/v2.ts | 5 ++ 4 files changed, 42 insertions(+), 56 deletions(-) diff --git a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx index 2834228715115a..f16ec341f12327 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx @@ -8,7 +8,7 @@ import type { ForwardedRef } from 'react'; * WordPress dependencies */ import { useInstanceId } from '@wordpress/compose'; -import { forwardRef, useContext, useEffect } from '@wordpress/element'; +import { forwardRef, useContext } from '@wordpress/element'; import { Icon, check } from '@wordpress/icons'; /** @@ -16,16 +16,14 @@ import { Icon, check } from '@wordpress/icons'; */ import { CircularOptionPickerContext } from './circular-option-picker-context'; import Button from '../button'; -import { CompositeItem } from '../composite'; +import { CompositeItem } from '../composite/v2'; import Tooltip from '../tooltip'; import type { OptionProps, - CircularOptionPickerCompositeState, + CircularOptionPickerCompositeStore, CircularOptionPickerContextProps, } from './types'; -const hasSelectedOption = new Map(); - function UnforwardedOptionAsButton( props: { id?: string; @@ -40,7 +38,7 @@ function UnforwardedOptionAsButton( { ...additionalProps } aria-pressed={ isPressed } ref={ forwardedRef } - > + /> ); } @@ -56,33 +54,28 @@ function UnforwardedOptionAsOption( forwardedRef: ForwardedRef< any > ) { const { id, isSelected, context, ...additionalProps } = props; - const { isComposite, ..._compositeState } = context; - const compositeState = - _compositeState as CircularOptionPickerCompositeState; - const { baseId, currentId, setCurrentId } = compositeState; + const { isComposite, baseId, ..._compositeStore } = context; + const compositeStore = + _compositeStore as CircularOptionPickerCompositeStore; + const { setActiveId } = compositeStore; + const activeId = compositeStore.useState( 'activeId' ); - useEffect( () => { - // If we call `setCurrentId` here, it doesn't update for other - // Option renders in the same pass. So we have to store our own - // map to make sure that we only set the first selected option. - // We still need to check `currentId` because the control will - // update this as the user moves around, and that state should - // be maintained as the group gains and loses focus. - if ( isSelected && ! currentId && ! hasSelectedOption.get( baseId ) ) { - hasSelectedOption.set( baseId, true ); - setCurrentId( id ); - } - }, [ baseId, currentId, id, isSelected, setCurrentId ] ); + if ( isSelected && ! activeId ) { + setActiveId( id ); + } return ( + } + store={ compositeStore } id={ id } - role="option" - aria-selected={ !! isSelected } - ref={ forwardedRef } /> ); } diff --git a/packages/components/src/circular-option-picker/circular-option-picker.tsx b/packages/components/src/circular-option-picker/circular-option-picker.tsx index 08c5ad94a34cb7..8afc034cddb7a5 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker.tsx @@ -7,14 +7,13 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useInstanceId } from '@wordpress/compose'; -import { useEffect } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; /** * Internal dependencies */ import { CircularOptionPickerContext } from './circular-option-picker-context'; -import { Composite, useCompositeState } from '../composite'; +import { Composite, useCompositeStore } from '../composite/v2'; import type { CircularOptionPickerProps, ListboxCircularOptionPickerProps, @@ -85,30 +84,16 @@ function ListboxCircularOptionPicker( children, ...additionalProps } = props; - const rtl = isRTL(); - const compositeState = useCompositeState( { baseId, loop, rtl } ); - const { setBaseId, setLoop, setRTL } = compositeState; - - // These are necessary as `useCompositeState` is sealed after - // the first render, so although unlikely to happen, if a state - // property should change, we need to process it accordingly. - - useEffect( () => { - setBaseId( baseId ); - }, [ setBaseId, baseId ] ); - - useEffect( () => { - setLoop( loop ); - }, [ setLoop, loop ] ); - - useEffect( () => { - setRTL( rtl ); - }, [ setRTL, rtl ] ); + const compositeStore = useCompositeStore( { + focusLoop: loop, + rtl: isRTL(), + } ); const compositeContext = { isComposite: true, - ...compositeState, + baseId, + ...compositeStore, }; return ( @@ -116,7 +101,7 @@ function ListboxCircularOptionPicker( { options } diff --git a/packages/components/src/circular-option-picker/types.ts b/packages/components/src/circular-option-picker/types.ts index cd9d9dca6be8df..e14fdc306bbe6f 100644 --- a/packages/components/src/circular-option-picker/types.ts +++ b/packages/components/src/circular-option-picker/types.ts @@ -14,7 +14,7 @@ import type { Icon } from '@wordpress/icons'; import type { ButtonAsButtonProps } from '../button/types'; import type { DropdownProps } from '../dropdown/types'; import type { WordPressComponentProps } from '../context'; -import type { CompositeState } from '../composite'; +import type { CompositeStore } from '../composite/v2'; type CommonCircularOptionPickerProps = { /** @@ -123,7 +123,10 @@ export type OptionProps = Omit< >; }; -export type CircularOptionPickerCompositeState = CompositeState; -export type CircularOptionPickerContextProps = - | { isComposite?: false; baseId?: string } - | ( { isComposite: true } & CircularOptionPickerCompositeState ); +export type CircularOptionPickerCompositeStore = CompositeStore; +export type CircularOptionPickerContextProps = { + baseId?: string; +} & ( + | { isComposite?: false } + | ( { isComposite: true } & CircularOptionPickerCompositeStore ) +); diff --git a/packages/components/src/composite/v2.ts b/packages/components/src/composite/v2.ts index 30ea466502ca53..edfe80fe2ed339 100644 --- a/packages/components/src/composite/v2.ts +++ b/packages/components/src/composite/v2.ts @@ -7,6 +7,8 @@ * * @see https://ariakit.org/components/composite */ + +/* eslint-disable-next-line no-restricted-imports */ export { Composite, CompositeGroup, @@ -14,3 +16,6 @@ export { CompositeRow, useCompositeStore, } from '@ariakit/react'; + +/* eslint-disable-next-line no-restricted-imports */ +export type { CompositeStore } from '@ariakit/react'; From 4a1e281abe08bfad6b94ed61070d609d42e5e1e7 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Fri, 29 Sep 2023 00:58:42 +0100 Subject: [PATCH 12/17] Fixing broken tests --- .../color-palette/test/__snapshots__/control.js.snap | 3 ++- .../src/circular-option-picker/circular-option-picker.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap index 516fa70a321e9b..242218705b3cff 100644 --- a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap +++ b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap @@ -219,10 +219,11 @@ exports[`ColorPaletteControl matches the snapshot 1`] = ` aria-label="Color: red" aria-selected="true" class="components-button components-circular-option-picker__option" + data-active-item="" + data-command="" id="components-circular-option-picker-0-0" role="option" style="background-color: rgb(255, 0, 0); color: rgb(255, 0, 0);" - tabindex="0" type="button" /> @@ -119,7 +120,7 @@ function ButtonsCircularOptionPicker( const { actions, options, children, baseId, ...additionalProps } = props; return ( -
+
From bacefdc581e6e62ff49740eb8011e007bc240262 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Tue, 3 Oct 2023 11:12:06 +0100 Subject: [PATCH 13/17] Updating CHANGELOG.md --- packages/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 986c7fa19b9ea2..155c78832487a4 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -8,6 +8,7 @@ - `Button`: deprecating `isPressed` prop in favour of `aria-pressed` ([#54740](https://github.com/WordPress/gutenberg/pull/54740)). - `DuotonePicker/ColorListPicker`: Adds appropriate label and description to 'Duotone Filter' picker ([#54473](https://github.com/WordPress/gutenberg/pull/54473)). - `Modal`: Accessibly hide/show outer modal when nested ([#54743](https://github.com/WordPress/gutenberg/pull/54743)). +- `Composite`/`AlignmentMatrixControl`/`CircularOptionPicker`: Starts the `Composite` migration from `reakit` to `ariakit` ([#54225](https://github.com/WordPress/gutenberg/pull/54225)). ### Bug Fix From cf8ab0a33a3ccc1c2ea8bd89d07b9d5577718698 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Wed, 4 Oct 2023 11:22:36 +0100 Subject: [PATCH 14/17] Simplifying composite context usage --- .../circular-option-picker-option.tsx | 25 +++++++------------ .../circular-option-picker.tsx | 7 ++---- .../src/circular-option-picker/types.ts | 6 ++--- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx index f16ec341f12327..7dcfd635557550 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx @@ -18,11 +18,7 @@ import { CircularOptionPickerContext } from './circular-option-picker-context'; import Button from '../button'; import { CompositeItem } from '../composite/v2'; import Tooltip from '../tooltip'; -import type { - OptionProps, - CircularOptionPickerCompositeStore, - CircularOptionPickerContextProps, -} from './types'; +import type { OptionProps, CircularOptionPickerCompositeStore } from './types'; function UnforwardedOptionAsButton( props: { @@ -49,19 +45,15 @@ function UnforwardedOptionAsOption( id: string; className?: string; isSelected?: boolean; - context: CircularOptionPickerContextProps; + compositeStore: CircularOptionPickerCompositeStore; }, forwardedRef: ForwardedRef< any > ) { - const { id, isSelected, context, ...additionalProps } = props; - const { isComposite, baseId, ..._compositeStore } = context; - const compositeStore = - _compositeStore as CircularOptionPickerCompositeStore; - const { setActiveId } = compositeStore; + const { id, isSelected, compositeStore, ...additionalProps } = props; const activeId = compositeStore.useState( 'activeId' ); if ( isSelected && ! activeId ) { - setActiveId( id ); + compositeStore.setActiveId( id ); } return ( @@ -89,8 +81,9 @@ export function Option( { tooltipText, ...additionalProps }: OptionProps ) { - const compositeContext = useContext( CircularOptionPickerContext ); - const { isComposite, baseId } = compositeContext; + const { baseId, compositeStore } = useContext( + CircularOptionPickerContext + ); const id = useInstanceId( Option, baseId || 'components-circular-option-picker__option' @@ -102,10 +95,10 @@ export function Option( { ...additionalProps, }; - const optionControl = isComposite ? ( + const optionControl = compositeStore ? ( ) : ( diff --git a/packages/components/src/circular-option-picker/circular-option-picker.tsx b/packages/components/src/circular-option-picker/circular-option-picker.tsx index cb2f5b9ff318c9..047b0c569c7d3e 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker.tsx @@ -91,9 +91,8 @@ function ListboxCircularOptionPicker( } ); const compositeContext = { - isComposite: true, baseId, - ...compositeStore, + compositeStore, }; return ( @@ -121,9 +120,7 @@ function ButtonsCircularOptionPicker( return (
- + { options } { children } { actions } diff --git a/packages/components/src/circular-option-picker/types.ts b/packages/components/src/circular-option-picker/types.ts index e14fdc306bbe6f..519d81d5905107 100644 --- a/packages/components/src/circular-option-picker/types.ts +++ b/packages/components/src/circular-option-picker/types.ts @@ -126,7 +126,5 @@ export type OptionProps = Omit< export type CircularOptionPickerCompositeStore = CompositeStore; export type CircularOptionPickerContextProps = { baseId?: string; -} & ( - | { isComposite?: false } - | ( { isComposite: true } & CircularOptionPickerCompositeStore ) -); + compositeStore?: CircularOptionPickerCompositeStore; +}; From 50c06dd2c13cfb1c6dbb36227948b5dccead4a38 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Wed, 4 Oct 2023 17:22:50 +0100 Subject: [PATCH 15/17] Refactoring and simplifying --- .../src/alignment-matrix-control/index.tsx | 16 ++--- .../src/alignment-matrix-control/types.ts | 5 -- .../src/alignment-matrix-control/utils.tsx | 61 ++++++++----------- 3 files changed, 28 insertions(+), 54 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/index.tsx b/packages/components/src/alignment-matrix-control/index.tsx index 248322b084c5da..3de0e401187d53 100644 --- a/packages/components/src/alignment-matrix-control/index.tsx +++ b/packages/components/src/alignment-matrix-control/index.tsx @@ -18,11 +18,7 @@ import { Root, Row } from './styles/alignment-matrix-control-styles'; import AlignmentMatrixControlIcon from './icon'; import { GRID, getItemId, getItemValue } from './utils'; import type { WordPressComponentProps } from '../context'; -import type { - AlignmentMatrixControlProps, - AlignmentMatrixControlValue, - VoidableAlignmentMatrixControlValue, -} from './types'; +import type { AlignmentMatrixControlProps } from './types'; /** * @@ -62,14 +58,10 @@ export function AlignmentMatrixControl( { const compositeStore = useCompositeStore( { defaultActiveId: getItemId( baseId, defaultValue ), - activeId: value && getItemId( baseId, value ), + activeId: getItemId( baseId, value ), setActiveId: ( nextActiveId ) => { - onChange?.( - getItemValue( - baseId, - nextActiveId as VoidableAlignmentMatrixControlValue - ) as AlignmentMatrixControlValue - ); + const nextValue = getItemValue( baseId, nextActiveId ); + if ( nextValue ) onChange?.( nextValue ); }, rtl: isRTL(), } ); diff --git a/packages/components/src/alignment-matrix-control/types.ts b/packages/components/src/alignment-matrix-control/types.ts index a9bb96b3e8c190..892781234e694c 100644 --- a/packages/components/src/alignment-matrix-control/types.ts +++ b/packages/components/src/alignment-matrix-control/types.ts @@ -10,11 +10,6 @@ export type AlignmentMatrixControlValue = | 'bottom center' | 'bottom right'; -export type VoidableAlignmentMatrixControlValue = - | AlignmentMatrixControlValue - | null - | undefined; - export type AlignmentMatrixControlProps = { /** * Accessible label. If provided, sets the `aria-label` attribute of the diff --git a/packages/components/src/alignment-matrix-control/utils.tsx b/packages/components/src/alignment-matrix-control/utils.tsx index fe973788e7c236..54455b61229b01 100644 --- a/packages/components/src/alignment-matrix-control/utils.tsx +++ b/packages/components/src/alignment-matrix-control/utils.tsx @@ -6,10 +6,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import type { - AlignmentMatrixControlValue, - VoidableAlignmentMatrixControlValue, -} from './types'; +import type { AlignmentMatrixControlValue } from './types'; export const GRID: AlignmentMatrixControlValue[][] = [ [ 'top left', 'top center', 'top right' ], @@ -34,17 +31,6 @@ export const ALIGNMENT_LABEL: Record< AlignmentMatrixControlValue, string > = { // Transforms GRID into a flat Array of values. export const ALIGNMENTS = GRID.flat(); -/** - * Normalizes an incoming value to better match the alignment values - * - * @param value an alignment value to normalize - * - * @return The normalized value - */ -export function normalizeValue( value: VoidableAlignmentMatrixControlValue ) { - return value === 'center' ? 'center center' : value; -} - /** * Normalizes and transforms an incoming value to better match the alignment values * @@ -52,11 +38,18 @@ export function normalizeValue( value: VoidableAlignmentMatrixControlValue ) { * * @return The parsed value. */ -export function transformValue( value: VoidableAlignmentMatrixControlValue ) { - return normalizeValue( value )?.replace( +function normalize( value?: string | null ) { + const normalized = value === 'center' ? 'center center' : value; + + // Strictly speaking, this could be `string | null | undefined`, + // but will be validated shortly, so we're typecasting to an + // `AlignmentMatrixControlValue` to keep TypeScript happy. + const transformed = normalized?.replace( '-', ' ' - ) as VoidableAlignmentMatrixControlValue; + ) as AlignmentMatrixControlValue; + + return ALIGNMENTS.includes( transformed ) ? transformed : undefined; } /** @@ -69,10 +62,13 @@ export function transformValue( value: VoidableAlignmentMatrixControlValue ) { */ export function getItemId( prefixId: string, - value: VoidableAlignmentMatrixControlValue + value?: AlignmentMatrixControlValue ) { - const valueId = transformValue( value )?.replace( ' ', '-' ); - return valueId && `${ prefixId }-${ valueId }`; + const normalized = normalize( value ); + if ( ! normalized ) return; + + const id = normalized.replace( ' ', '-' ); + return `${ prefixId }-${ id }`; } /** @@ -82,13 +78,9 @@ export function getItemId( * @param id An item ID * @return The item value */ -export function getItemValue( - prefixId: string, - id: VoidableAlignmentMatrixControlValue -) { - return transformValue( - id?.replace( prefixId + '-', '' ) as VoidableAlignmentMatrixControlValue - ); +export function getItemValue( prefixId: string, id?: string | null ) { + const value = id?.replace( prefixId + '-', '' ); + return normalize( value ); } /** @@ -99,16 +91,11 @@ export function getItemValue( * @return The index of a matching alignment. */ export function getAlignmentIndex( - alignment: VoidableAlignmentMatrixControlValue = 'center' + alignment: AlignmentMatrixControlValue = 'center' ) { - const transformedValue = transformValue( - alignment - ) as AlignmentMatrixControlValue; - - // `transformedValue` could come back as undefined or null, - // but `indexOf` will still return -1, so we can proceed - // without any problems. - const index = ALIGNMENTS.indexOf( transformedValue ); + const normalized = normalize( alignment ); + if ( ! normalized ) return undefined; + const index = ALIGNMENTS.indexOf( normalized ); return index > -1 ? index : undefined; } From 582ddf716a4d956c3e13e5d2e43276b2cc8ccc08 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Thu, 5 Oct 2023 12:22:53 +0100 Subject: [PATCH 16/17] Exporting `CompositeGroupLabel` --- packages/components/src/composite/v2.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/composite/v2.ts b/packages/components/src/composite/v2.ts index edfe80fe2ed339..d329fd3fd11dfb 100644 --- a/packages/components/src/composite/v2.ts +++ b/packages/components/src/composite/v2.ts @@ -12,6 +12,7 @@ export { Composite, CompositeGroup, + CompositeGroupLabel, CompositeItem, CompositeRow, useCompositeStore, From 814d669b4f578074f1365c89eda3ced717ee60dc Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Thu, 5 Oct 2023 12:27:10 +0100 Subject: [PATCH 17/17] Renaming `asyncRender` to `renderAndInitCompositeStore` --- .../src/alignment-matrix-control/test/index.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/components/src/alignment-matrix-control/test/index.tsx b/packages/components/src/alignment-matrix-control/test/index.tsx index 64e8ecd36a5866..6836bc7e45f95c 100644 --- a/packages/components/src/alignment-matrix-control/test/index.tsx +++ b/packages/components/src/alignment-matrix-control/test/index.tsx @@ -17,7 +17,10 @@ const getCell = ( name: string ) => { return within( getControl() ).getByRole( 'gridcell', { name } ); }; -const asyncRender = async ( jsx: any, focusedCell = 'center center' ) => { +const renderAndInitCompositeStore = async ( + jsx: JSX.Element, + focusedCell = 'center center' +) => { const view = render( jsx ); await waitFor( () => { expect( getCell( focusedCell ) ).toHaveAttribute( 'tabindex', '0' ); @@ -28,7 +31,7 @@ const asyncRender = async ( jsx: any, focusedCell = 'center center' ) => { describe( 'AlignmentMatrixControl', () => { describe( 'Basic rendering', () => { it( 'should render', async () => { - await asyncRender( ); + await renderAndInitCompositeStore( ); expect( getControl() ).toBeInTheDocument(); } ); @@ -36,7 +39,7 @@ describe( 'AlignmentMatrixControl', () => { it( 'should be centered by default', async () => { const user = userEvent.setup(); - await asyncRender( ); + await renderAndInitCompositeStore( ); await user.tab(); @@ -60,7 +63,7 @@ describe( 'AlignmentMatrixControl', () => { const user = userEvent.setup(); const spy = jest.fn(); - await asyncRender( + await renderAndInitCompositeStore( { const user = userEvent.setup(); const spy = jest.fn(); - await asyncRender( + await renderAndInitCompositeStore( { const user = userEvent.setup(); const spy = jest.fn(); - await asyncRender( + await renderAndInitCompositeStore( ); @@ -129,7 +132,7 @@ describe( 'AlignmentMatrixControl', () => { const user = userEvent.setup(); const spy = jest.fn(); - await asyncRender( + await renderAndInitCompositeStore( );