Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UnitControl: fix controlled unit behavior #39148

Merged
merged 9 commits into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Normalize `font-family` on `Button`, `ColorPalette`, `ComoboboxControl`, `DateTimePicker`, `FormTokenField`, `InputControl`, `SelectControl`, and `ToggleGroupControl` ([#38969](https://github.com/WordPress/gutenberg/pull/38969)).
- Fix input value selection of `InputControl`-based controls in Firefox and Safari with axial constraint of drag gesture ([#38968](https://github.com/WordPress/gutenberg/pull/38968)).
- Fix `UnitControl`'s behavior around updating the unit when a new `value` is passed (i.e. in controlled mode). ([#39148](https://github.com/WordPress/gutenberg/pull/39148)).

## 19.5.0 (2022-02-23)

Expand Down
6 changes: 5 additions & 1 deletion packages/components/src/unit-control/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { forwardRef, useMemo, useRef } from '@wordpress/element';
import { forwardRef, useMemo, useRef, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { ENTER } from '@wordpress/keycodes';

Expand Down Expand Up @@ -70,6 +70,10 @@ function UnitControl(
}
);

useEffect( () => {
setUnit( initialUnit );
}, [ initialUnit ] );

// Stores parsed value for hand-off in state reducer.
const refParsedValue = useRef< string | null >( null );

Expand Down
70 changes: 69 additions & 1 deletion packages/components/src/unit-control/test/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
/**
* External dependencies
*/
import { render, fireEvent } from '@testing-library/react';
import { render, fireEvent, screen } from '@testing-library/react';

/**
* WordPress dependencies
*/
import { UP, DOWN, ENTER, ESCAPE } from '@wordpress/keycodes';
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import UnitControl from '../';
import { parseUnit } from '../utils';

const getComponent = () =>
document.body.querySelector( '.components-unit-control' );
Expand All @@ -25,6 +27,47 @@ const getUnitLabel = () =>
const fireKeyDown = ( data ) =>
fireEvent.keyDown( document.activeElement || document.body, data );

const ControlledSyncUnits = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: Was this story only useful for testing this particular fix or do you think there is value in keeping it around? It's not immediately clear to me whether the main purpose of the story is to demonstrate a particular behavior/implementation, or to provide a debugging playground moving forward for the synced usage. My personal impression is that unit tests are sufficient and better for this kind of behavioral QA, unless you expect frequent manual testing of this "controlled" case in the near future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You make a good point. This story is actually very targeted to test the buggy behaviour, and probably doesn't bring much value to Storybook users. I'll go ahead and remove it from this PR.

const [ state, setState ] = useState( { valueA: '', valueB: '' } );

// Keep the unit sync'd between the two `UnitControl` instances.
const onUnitControlChange = ( fieldName, newValue ) => {
// eslint-disable-next-line @wordpress/no-unused-vars-before-return
const [ quantity, newUnit ] = parseUnit( newValue );

if ( ! Number.isFinite( quantity ) ) {
return;
}

const nextState = { ...state, [ fieldName ]: newValue };

Object.entries( state ).forEach( ( [ stateProp, stateValue ] ) => {
const [ stateQuantity, stateUnit ] = parseUnit( stateValue );

if ( stateProp !== fieldName && stateUnit !== newUnit ) {
nextState[ stateProp ] = `${ stateQuantity }${ newUnit }`;
}
} );

setState( nextState );
};

return (
<>
<UnitControl
label="Field A"
value={ state.valueA }
onChange={ ( v ) => onUnitControlChange( 'valueA', v ) }
/>
<UnitControl
label="Field B"
value={ state.valueB }
onChange={ ( v ) => onUnitControlChange( 'valueB', v ) }
/>
</>
);
};

describe( 'UnitControl', () => {
describe( 'Basic rendering', () => {
it( 'should render', () => {
Expand Down Expand Up @@ -266,6 +309,31 @@ describe( 'UnitControl', () => {

expect( state ).toBe( '62%' );
} );

it( 'should update unit value when a new raw value is passed', () => {
render( <ControlledSyncUnits /> );

const [ inputA, inputB ] = screen.getAllByRole( 'spinbutton' );
const [ selectA, selectB ] = screen.getAllByRole( 'combobox' );

inputA.focus();
fireEvent.change( inputA, { target: { value: '55' } } );

inputB.focus();
fireEvent.change( inputB, { target: { value: '14' } } );

selectA.focus();
fireEvent.change( selectA, { target: { value: 'rem' } } );

expect( selectA ).toHaveValue( 'rem' );
expect( selectB ).toHaveValue( 'rem' );

selectB.focus();
fireEvent.change( selectB, { target: { value: 'vw' } } );

expect( selectA ).toHaveValue( 'vw' );
expect( selectB ).toHaveValue( 'vw' );
} );
} );

describe( 'Unit Parser', () => {
Expand Down