Skip to content

Commit

Permalink
UnitControl component: Refactor JSX components to TypeScript (#35281)
Browse files Browse the repository at this point in the history
* UnitControl component: Refactor UnitSelectControl to TypeScript

* Update size to selectSize to avoid conflict with the size HTML attribute of the Select element

Co-authored-by: Marco Ciampini <[email protected]>

* Fix typo in Readme

* Convert main UnitControl component to TS, add additional types

* Update UnitControlOnChangeCallback to inherit from InputChangeCallback

Co-authored-by: Marco Ciampini <[email protected]>

* Remove optional chaining from parsedValue

* Tidy up UnitControlProps type and inherit from UnitSelectControlProps

* Add type to handleOnBlur

Co-authored-by: Marco Ciampini <[email protected]>

* Use WordPressComponentProps type

* Add JSDoc comment on exported UnitControl component

Co-authored-by: Marco Ciampini <[email protected]>

* Add comments to types

* Update isUnitSelectTabbable prop to be optional

Co-authored-by: Marco Ciampini <[email protected]>

* Add defaults to JSDoc comments, update headings in Readme to use TypeScript style type description

* Refactor to  `WPUnitControlUnitList` type

* Update type for Value and reword the description

Co-authored-by: Marco Ciampini <[email protected]>
  • Loading branch information
andrewserong and ciampo authored Oct 14, 2021
1 parent 1961b03 commit 0160b7f
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 142 deletions.
10 changes: 6 additions & 4 deletions packages/components/src/input-control/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ interface BaseProps {
size?: 'default' | 'small';
}

export type InputChangeCallback<
E = ChangeEvent< HTMLInputElement >,
P = {}
> = ( nextValue: string | undefined, extra: { event: E } & P ) => void;

export interface InputFieldProps extends BaseProps {
dragDirection?: DragDirection;
dragThreshold?: number;
isDragEnabled?: boolean;
isPressEnterToChange?: boolean;
onChange?: (
nextValue: string | undefined,
extra: { event: ChangeEvent< HTMLInputElement > }
) => void;
onChange?: InputChangeCallback;
onValidate?: (
nextValue: string,
event?: SyntheticEvent< HTMLInputElement >
Expand Down
45 changes: 21 additions & 24 deletions packages/components/src/unit-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,70 +21,69 @@ const Example = () => {

## Props

### disabledUnits
### `disableUnits`: `boolean`

If true, the unit `<select>` is hidden.

- Type: `Boolean`
- Required: No
- Default: `false`

### isPressEnterToChange
### `isPressEnterToChange`: `boolean`

If true, the `ENTER` key press is required in order to trigger an `onChange`. If enabled, a change is also triggered when tabbing away (`onBlur`).
If `true`, the `ENTER` key press is required in order to trigger an `onChange`. If enabled, a change is also triggered when tabbing away (`onBlur`).

- Type: `Boolean`
- Required: No
- Default: `false`

### isUnitSelectTabbable
### `isResetValueOnUnitChange`: `boolean`

If `true`, and the selected unit provides a `default` value, this value is set when changing units.

- Required: No
- Default: `false`

### `isUnitSelectTabbable`: `boolean`

Determines if the unit `<select>` is tabbable.

- Type: `Boolean`
- Required: No
- Default: `true`

### label
### `label`: `string`

If this property is added, a label will be generated using label property as the content.

- Type: `String`
- Required: No

### labelPosition
### `labelPosition`: `string`

The position of the label (`top`, `side`, `bottom`, or `edge`).

- Type: `String`
- Required: No

### onChange
### `onChange`: `UnitControlOnChangeCallback`

Callback when the `value` changes.

- Type: `Function`
- Required: No
- Default: `noop`

### onUnitChange
### `onUnitChange`: `UnitControlOnChangeCallback`

Callback when the `unit` changes.

- Type: `Function`
- Required: No
- Default: `noop`

### size
### `size`: `string`

Adjusts the size of the input.
Sizes include: `default`, `small`

- Type: `String`
- Required: No
- Default: `default`

### unit
### `unit`: `string`

Deprecated: Current unit value.
Instead, provide a unit with a value through the `value` prop.
Expand All @@ -95,14 +94,12 @@ Example:
<UnitControl value="50%" />
```

- Type: `String`
- Required: No

### units
### `units`: `WPUnitControlUnitList`

Collection of available units.

- Type: `Array<WPUnitControlUnit>`
- Required: No

Example:
Expand All @@ -126,15 +123,15 @@ const Example = () => {

A `default` value (in the example above, `10` for `%`), if defined, is set as the new `value` when a unit changes. This is helpful in scenarios where changing a unit may cause drastic results, such as changing from `px` to `vh`.

### value
### `value`: `number | string`

Current value. To set a unit, provide a unit with a value through the `value` prop.
Current value. If passed as a string, the current unit will be inferred from this value.
For example, a `value` of `50%` will set the current unit to `%`.

Example:

```jsx
<UnitControl value="50%" />
```

- Type: `Number`|`String`
- Required: No
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import type {
FocusEventHandler,
KeyboardEvent,
Ref,
SyntheticEvent,
} from 'react';
import { noop, omit } from 'lodash';
import classnames from 'classnames';

Expand All @@ -14,6 +21,7 @@ import { ENTER } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import type { WordPressComponentProps } from '../ui/context';
import * as inputControlActionTypes from '../input-control/reducer/actions';
import { composeStateReducers } from '../input-control/reducer/reducer';
import { Root, ValueInput } from './styles/unit-control-styles';
Expand All @@ -25,6 +33,8 @@ import {
getValidParsedUnit,
} from './utils';
import { useControlledState } from '../utils/hooks';
import type { UnitControlProps, UnitControlOnChangeCallback } from './types';
import type { StateReducer } from '../input-control/reducer/state';

function UnitControl(
{
Expand All @@ -45,24 +55,31 @@ function UnitControl(
units: unitsProp = CSS_UNITS,
value: valueProp,
...props
},
ref
}: WordPressComponentProps< UnitControlProps, 'input', false >,
forwardedRef: Ref< any >
) {
const units = useMemo(
() => getUnitsWithCurrentUnit( valueProp, unitProp, unitsProp ),
[ valueProp, unitProp, unitsProp ]
);
const [ value, initialUnit ] = getParsedValue( valueProp, unitProp, units );
const [ unit, setUnit ] = useControlledState( unitProp, {
initial: initialUnit,
} );
const [ unit, setUnit ] = useControlledState< string | undefined >(
unitProp,
{
initial: initialUnit,
fallback: '',
}
);

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

const classes = classnames( 'components-unit-control', className );

const handleOnChange = ( next, changeProps ) => {
const handleOnChange: UnitControlOnChangeCallback = (
next,
changeProps
) => {
if ( next === '' ) {
onChange( '', changeProps );
return;
Expand All @@ -77,7 +94,10 @@ function UnitControl(
onChange( next, changeProps );
};

const handleOnUnitChange = ( next, changeProps ) => {
const handleOnUnitChange: UnitControlOnChangeCallback = (
next,
changeProps
) => {
const { data } = changeProps;

let nextValue = `${ value }${ next }`;
Expand All @@ -92,24 +112,24 @@ function UnitControl(
setUnit( next );
};

const mayUpdateUnit = ( event ) => {
if ( ! isNaN( event.target.value ) ) {
const mayUpdateUnit = ( event: SyntheticEvent< HTMLInputElement > ) => {
if ( ! isNaN( Number( event.currentTarget.value ) ) ) {
refParsedValue.current = null;
return;
}
const [ parsedValue, parsedUnit ] = getValidParsedUnit(
event.target.value,
event.currentTarget.value,
units,
value,
unit
);

refParsedValue.current = parsedValue;
refParsedValue.current = parsedValue.toString();

if ( isPressEnterToChange && parsedUnit !== unit ) {
const data = units.find(
( option ) => option.value === parsedUnit
);
const data = Array.isArray( units )
? units.find( ( option ) => option.value === parsedUnit )
: undefined;
const changeProps = { event, data };

onChange( `${ parsedValue }${ parsedUnit }`, changeProps );
Expand All @@ -119,9 +139,9 @@ function UnitControl(
}
};

const handleOnBlur = mayUpdateUnit;
const handleOnBlur: FocusEventHandler< HTMLInputElement > = mayUpdateUnit;

const handleOnKeyDown = ( event ) => {
const handleOnKeyDown = ( event: KeyboardEvent< HTMLInputElement > ) => {
const { keyCode } = event;
if ( keyCode === ENTER ) {
mayUpdateUnit( event );
Expand All @@ -133,11 +153,11 @@ function UnitControl(
* This allows us to tap into actions to transform the (next) state for
* InputControl.
*
* @param {Object} state State from InputControl
* @param {Object} action Action triggering state change
* @return {Object} The updated state to apply to InputControl
* @param state State from InputControl
* @param action Action triggering state change
* @return The updated state to apply to InputControl
*/
const unitControlStateReducer = ( state, action ) => {
const unitControlStateReducer: StateReducer = ( state, action ) => {
/*
* On commits (when pressing ENTER and on blur if
* isPressEnterToChange is true), if a parse has been performed
Expand All @@ -157,11 +177,11 @@ function UnitControl(
<UnitSelectControl
aria-label={ __( 'Select unit' ) }
disabled={ disabled }
isTabbable={ isUnitSelectTabbable }
options={ units }
isUnitSelectTabbable={ isUnitSelectTabbable }
onChange={ handleOnUnitChange }
size={ size }
value={ unit }
unit={ unit }
units={ units }
/>
) : null;

Expand Down Expand Up @@ -191,7 +211,7 @@ function UnitControl(
onBlur={ handleOnBlur }
onKeyDown={ handleOnKeyDown }
onChange={ handleOnChange }
ref={ ref }
ref={ forwardedRef }
size={ size }
suffix={ inputSuffix }
value={ value }
Expand All @@ -205,6 +225,22 @@ function UnitControl(
);
}

/**
* `UnitControl` allows the user to set a value as well as a unit (e.g. `px`).
*
*
* @example
* ```jsx
* import { __experimentalUnitControl as UnitControl } from '@wordpress/components';
* import { useState } from '@wordpress/element';
*
* const Example = () => {
* const [ value, setValue ] = useState( '10px' );
*
* return <UnitControl onChange={ setValue } value={ value } />;
* };
* ```
*/
const ForwardedUnitControl = forwardRef( UnitControl );

export { parseUnit, useCustomUnits } from './utils';
Expand Down
Loading

0 comments on commit 0160b7f

Please sign in to comment.