@@ -521,12 +1029,12 @@ exports[`ToggleGroupControl should render correctly with text options 1`] = `
aria-checked="false"
aria-label="R"
class="emotion-12 components-toggle-group-control-option-base"
+ data-command=""
data-value="rigas"
data-wp-c16t="true"
data-wp-component="ToggleGroupControlOptionBase"
id="toggle-group-control-as-radio-group-0-0"
role="radio"
- tabindex="0"
>
{
- const options = (
- <>
-
-
- >
- );
- const optionsWithTooltip = (
+const ControlledToggleGroupControl = ( {
+ value: valueProp,
+ onChange,
+ ...props
+}: ToggleGroupControlProps ) => {
+ const [ value, setValue ] = useState( valueProp );
+
+ return (
<>
-
- {
+ setValue( ...changeArgs );
+ onChange?.( ...changeArgs );
+ } }
+ value={ value }
/>
+
>
);
+};
+const options = (
+ <>
+
+
+ >
+);
+const optionsWithTooltip = (
+ <>
+
+
+ >
+);
+
+describe.each( [
+ [ 'uncontrolled', ToggleGroupControl ],
+ [ 'controlled', ControlledToggleGroupControl ],
+] )( 'ToggleGroupControl %s', ( ...modeAndComponent ) => {
+ const [ mode, Component ] = modeAndComponent;
describe( 'should render correctly', () => {
it( 'with text options', () => {
const { container } = render(
-
+
{ options }
-
+
);
expect( container ).toMatchSnapshot();
@@ -58,10 +87,7 @@ describe( 'ToggleGroupControl', () => {
it( 'with icons', () => {
const { container } = render(
-
+
{
icon={ formatLowercase }
label="Lowercase"
/>
-
+
);
expect( container ).toMatchSnapshot();
@@ -83,13 +109,13 @@ describe( 'ToggleGroupControl', () => {
const mockOnChange = jest.fn();
render(
-
{ options }
-
+
);
await user.click( screen.getByRole( 'radio', { name: 'R' } ) );
@@ -100,9 +126,9 @@ describe( 'ToggleGroupControl', () => {
it( 'should render tooltip where `showTooltip` === `true`', async () => {
const user = userEvent.setup();
render(
-
+
{ optionsWithTooltip }
-
+
);
const firstRadio = screen.getByLabelText(
@@ -127,9 +153,9 @@ describe( 'ToggleGroupControl', () => {
it( 'should not render tooltip', async () => {
const user = userEvent.setup();
render(
-
+
{ optionsWithTooltip }
-
+
);
const secondRadio = screen.getByLabelText(
@@ -145,6 +171,36 @@ describe( 'ToggleGroupControl', () => {
);
} );
+ if ( mode === 'controlled' ) {
+ it( 'should reset values correctly', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ { options }
+
+ );
+
+ const rigasOption = screen.getByRole( 'radio', { name: 'R' } );
+ const jackOption = screen.getByRole( 'radio', { name: 'J' } );
+
+ await user.click( rigasOption );
+
+ expect( jackOption ).not.toBeChecked();
+ expect( rigasOption ).toBeChecked();
+
+ await user.keyboard( '[ArrowRight]' );
+
+ expect( rigasOption ).not.toBeChecked();
+ expect( jackOption ).toBeChecked();
+
+ await user.click( screen.getByRole( 'button', { name: 'Reset' } ) );
+
+ expect( rigasOption ).not.toBeChecked();
+ expect( jackOption ).not.toBeChecked();
+ } );
+ }
+
describe( 'isDeselectable', () => {
describe( 'isDeselectable = false', () => {
it( 'should not be deselectable', async () => {
@@ -152,13 +208,13 @@ describe( 'ToggleGroupControl', () => {
const user = userEvent.setup();
render(
-
{ options }
-
+
);
const rigas = screen.getByRole( 'radio', {
@@ -173,9 +229,9 @@ describe( 'ToggleGroupControl', () => {
const user = userEvent.setup();
render(
-
+
{ options }
-
+
);
const rigas = screen.getByRole( 'radio', {
@@ -186,7 +242,13 @@ describe( 'ToggleGroupControl', () => {
expect( rigas ).toHaveFocus();
await user.tab();
- expect( rigas.ownerDocument.body ).toHaveFocus();
+
+ const expectedFocusTarget =
+ mode === 'uncontrolled'
+ ? rigas.ownerDocument.body
+ : screen.getByRole( 'button', { name: 'Reset' } );
+
+ expect( expectedFocusTarget ).toHaveFocus();
} );
} );
@@ -196,14 +258,14 @@ describe( 'ToggleGroupControl', () => {
const user = userEvent.setup();
render(
-
{ options }
-
+
);
await user.click(
@@ -213,26 +275,25 @@ describe( 'ToggleGroupControl', () => {
} )
);
expect( mockOnChange ).toHaveBeenCalledTimes( 1 );
- expect( mockOnChange ).toHaveBeenCalledWith( undefined );
- expect(
+ expect( mockOnChange ).toHaveBeenLastCalledWith( undefined );
+
+ await user.click(
screen.getByRole( 'button', {
name: 'R',
pressed: false,
} )
- ).toBeVisible();
+ );
+ expect( mockOnChange ).toHaveBeenCalledTimes( 2 );
+ expect( mockOnChange ).toHaveBeenLastCalledWith( 'rigas' );
} );
it( 'should tab to the next option button', async () => {
const user = userEvent.setup();
render(
-
+
{ options }
-
+
);
await user.tab();
diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/component.tsx b/packages/components/src/toggle-group-control/toggle-group-control-option-base/component.tsx
index eb36f06022eed..572a4b70785eb 100644
--- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/component.tsx
+++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/component.tsx
@@ -3,12 +3,15 @@
*/
import type { ForwardedRef } from 'react';
// eslint-disable-next-line no-restricted-imports
-import { Radio } from 'reakit';
+import { Radio } from '@ariakit/react/radio';
+// eslint-disable-next-line no-restricted-imports
+import { motion, useReducedMotion } from 'framer-motion';
/**
* WordPress dependencies
*/
import { useInstanceId } from '@wordpress/compose';
+import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
@@ -26,6 +29,12 @@ import Tooltip from '../../tooltip';
const { ButtonContentView, LabelView } = styles;
+const REDUCED_MOTION_TRANSITION_CONFIG = {
+ duration: 0,
+};
+
+const LAYOUT_ID = 'toggle-group-backdrop-shared-layout-id';
+
const WithToolTip = ( { showTooltip, text, children }: WithToolTipProps ) => {
if ( showTooltip && text ) {
return (
@@ -38,56 +47,78 @@ const WithToolTip = ( { showTooltip, text, children }: WithToolTipProps ) => {
};
function ToggleGroupControlOptionBase(
- props: WordPressComponentProps<
- ToggleGroupControlOptionBaseProps,
- 'button',
- false
+ props: Omit<
+ WordPressComponentProps<
+ ToggleGroupControlOptionBaseProps,
+ 'button',
+ false
+ >,
+ // the element's id is generated internally
+ 'id'
>,
forwardedRef: ForwardedRef< any >
) {
+ const shouldReduceMotion = useReducedMotion();
const toggleGroupControlContext = useToggleGroupControlContext();
+
const id = useInstanceId(
ToggleGroupControlOptionBase,
toggleGroupControlContext.baseId || 'toggle-group-control-option-base'
- ) as string;
+ );
+
const buttonProps = useContextSystem(
{ ...props, id },
'ToggleGroupControlOptionBase'
);
+
const {
isBlock = false,
isDeselectable = false,
size = 'default',
- ...otherContextProps /* context props for Ariakit Radio */
} = toggleGroupControlContext;
+
const {
className,
isIcon = false,
value,
children,
showTooltip = false,
+ onFocus: onFocusProp,
...otherButtonProps
} = buttonProps;
- const isPressed = otherContextProps.state === value;
+ const isPressed = toggleGroupControlContext.value === value;
const cx = useCx();
- const labelViewClasses = cx( isBlock && styles.labelBlock );
- const classes = cx(
- styles.buttonView( { isDeselectable, isIcon, isPressed, size } ),
- className
+ const labelViewClasses = useMemo(
+ () => cx( isBlock && styles.labelBlock ),
+ [ cx, isBlock ]
);
+ const itemClasses = useMemo(
+ () =>
+ cx(
+ styles.buttonView( {
+ isDeselectable,
+ isIcon,
+ isPressed,
+ size,
+ } ),
+ className
+ ),
+ [ cx, isDeselectable, isIcon, isPressed, size, className ]
+ );
+ const backdropClasses = useMemo( () => cx( styles.backdropView ), [ cx ] );
const buttonOnClick = () => {
if ( isDeselectable && isPressed ) {
- otherContextProps.setState( undefined );
+ toggleGroupControlContext.setValue( undefined );
} else {
- otherContextProps.setState( value );
+ toggleGroupControlContext.setValue( value );
}
};
const commonProps = {
...otherButtonProps,
- className: classes,
+ className: itemClasses,
'data-value': value,
ref: forwardedRef,
};
@@ -101,6 +132,7 @@ function ToggleGroupControlOptionBase(
{ isDeselectable ? (