diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 739fc1a160db68..9b252adb68a586 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -22,6 +22,7 @@ - `ColorPalette`, `BorderControl`, `GradientPicker`: refine types and logic around single vs multiple palettes ([#47384](https://github.com/WordPress/gutenberg/pull/47384)). - `Button`: Convert to TypeScript ([#46997](https://github.com/WordPress/gutenberg/pull/46997)). +- `BoxControl`: Convert to TypeScript ([#47622](https://github.com/WordPress/gutenberg/pull/47622)). - `QueryControls`: Convert to TypeScript ([#46721](https://github.com/WordPress/gutenberg/pull/46721)). - `Notice`: refactor to TypeScript ([47118](https://github.com/WordPress/gutenberg/pull/47118)). diff --git a/packages/components/src/box-control/README.md b/packages/components/src/box-control/README.md index edf3993679033f..83ccd50a6f1ffc 100644 --- a/packages/components/src/box-control/README.md +++ b/packages/components/src/box-control/README.md @@ -30,82 +30,73 @@ const Example = () => { ``` ## Props -### allowReset +### `allowReset`: `boolean` If this property is true, a button to reset the box control is rendered. -- Type: `Boolean` - Required: No - Default: `true` -### splitOnAxis +### `splitOnAxis`: `boolean` If this property is true, when the box control is unlinked, vertical and horizontal controls can be used instead of updating individual sides. -- Type: `Boolean` - Required: No - Default: `false` -### inputProps +### `inputProps`: `object` -Props for the internal [InputControl](../input-control) components. +Props for the internal [UnitControl](../unit-control) components. -- Type: `Object` - Required: No +- Default: `{ min: 0 }` -### label +### `label`: `string` -Heading label for BoxControl. +Heading label for the control. -- Type: `String` - Required: No -- Default: `Box Control` +- Default: `__( 'Box Control' )` -### onChange +### `onChange`: `(next: BoxControlValue) => void` A callback function when an input value changes. -- Type: `Function` - Required: Yes -### resetValues +### `resetValues`: `object` The `top`, `right`, `bottom`, and `left` box dimension values to use when the control is reset. -- Type: `Object` - Required: No +- Default: `{ top: undefined, right: undefined, bottom: undefined, left: undefined }` -### sides +### `sides`: `string[]` -Collection of sides to allow control of. If omitted or empty, all sides will be available. +Collection of sides to allow control of. If omitted or empty, all sides will be available. Allowed values are "top", "right", "bottom", "left", "vertical", and "horizontal". -- Type: `Array` - Required: No -### units +### `units`: `WPUnitControlUnit[]` Collection of available units which are compatible with [UnitControl](../unit-control). -- Type: `Array` - Required: No -### values +### `values`: `object` The `top`, `right`, `bottom`, and `left` box dimension values. -- Type: `Object` - Required: No -### onMouseOver +### `onMouseOver`: `function` A handler for onMouseOver events. -- Type: `Function` - Required: No -### onMouseOut +### `onMouseOut`: `function` A handler for onMouseOut events. -- Type: `Function` - Required: No diff --git a/packages/components/src/box-control/all-input-control.js b/packages/components/src/box-control/all-input-control.tsx similarity index 74% rename from packages/components/src/box-control/all-input-control.js rename to packages/components/src/box-control/all-input-control.tsx index fcde202fd7de8f..b66e10fdb4ce3f 100644 --- a/packages/components/src/box-control/all-input-control.js +++ b/packages/components/src/box-control/all-input-control.tsx @@ -1,6 +1,8 @@ /** * Internal dependencies */ +import type { UnitControlProps } from '../unit-control/types'; +import type { BoxControlInputControlProps } from './types'; import UnitControl from './unit-control'; import { LABELS, @@ -22,18 +24,20 @@ export default function AllInputControl( { selectedUnits, setSelectedUnits, ...props -} ) { +}: BoxControlInputControlProps ) { const allValue = getAllValue( values, selectedUnits, sides ); const hasValues = isValuesDefined( values ); const isMixed = hasValues && isValuesMixed( values, selectedUnits, sides ); - const allPlaceholder = isMixed ? LABELS.mixed : null; + const allPlaceholder = isMixed ? LABELS.mixed : undefined; - const handleOnFocus = ( event ) => { + const handleOnFocus: React.FocusEventHandler< HTMLInputElement > = ( + event + ) => { onFocus( event, { side: 'all' } ); }; - const handleOnChange = ( next ) => { - const isNumeric = ! isNaN( parseFloat( next ) ); + const handleOnChange: UnitControlProps[ 'onChange' ] = ( next ) => { + const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) ); const nextValue = isNumeric ? next : undefined; const nextValues = applyValueToSides( values, nextValue, sides ); @@ -42,7 +46,7 @@ export default function AllInputControl( { // Set selected unit so it can be used as fallback by unlinked controls // when individual sides do not have a value containing a unit. - const handleOnUnitChange = ( unit ) => { + const handleOnUnitChange: UnitControlProps[ 'onUnitChange' ] = ( unit ) => { const newUnits = applyValueToSides( selectedUnits, unit, sides ); setSelectedUnits( newUnits ); }; diff --git a/packages/components/src/box-control/axial-input-controls.js b/packages/components/src/box-control/axial-input-controls.tsx similarity index 71% rename from packages/components/src/box-control/axial-input-controls.js rename to packages/components/src/box-control/axial-input-controls.tsx index 99a09d4ba366c7..627cfce408583a 100644 --- a/packages/components/src/box-control/axial-input-controls.js +++ b/packages/components/src/box-control/axial-input-controls.tsx @@ -5,8 +5,10 @@ import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; import UnitControl from './unit-control'; import { LABELS } from './utils'; import { Layout } from './styles/box-control-styles'; +import type { BoxControlInputControlProps } from './types'; -const groupedSides = [ 'vertical', 'horizontal' ]; +const groupedSides = [ 'vertical', 'horizontal' ] as const; +type GroupedSide = typeof groupedSides[ number ]; export default function AxialInputControls( { onChange, @@ -18,15 +20,17 @@ export default function AxialInputControls( { setSelectedUnits, sides, ...props -} ) { - const createHandleOnFocus = ( side ) => ( event ) => { - if ( ! onFocus ) { - return; - } - onFocus( event, { side } ); - }; +}: BoxControlInputControlProps ) { + const createHandleOnFocus = + ( side: GroupedSide ) => + ( event: React.FocusEvent< HTMLInputElement > ) => { + if ( ! onFocus ) { + return; + } + onFocus( event, { side } ); + }; - const createHandleOnHoverOn = ( side ) => () => { + const createHandleOnHoverOn = ( side: GroupedSide ) => () => { if ( ! onHoverOn ) { return; } @@ -44,7 +48,7 @@ export default function AxialInputControls( { } }; - const createHandleOnHoverOff = ( side ) => () => { + const createHandleOnHoverOff = ( side: GroupedSide ) => () => { if ( ! onHoverOff ) { return; } @@ -62,12 +66,12 @@ export default function AxialInputControls( { } }; - const createHandleOnChange = ( side ) => ( next ) => { + const createHandleOnChange = ( side: GroupedSide ) => ( next?: string ) => { if ( ! onChange ) { return; } const nextValues = { ...values }; - const isNumeric = ! isNaN( parseFloat( next ) ); + const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) ); const nextValue = isNumeric ? next : undefined; if ( side === 'vertical' ) { @@ -83,21 +87,22 @@ export default function AxialInputControls( { onChange( nextValues ); }; - const createHandleOnUnitChange = ( side ) => ( next ) => { - const newUnits = { ...selectedUnits }; + const createHandleOnUnitChange = + ( side: GroupedSide ) => ( next?: string ) => { + const newUnits = { ...selectedUnits }; - if ( side === 'vertical' ) { - newUnits.top = next; - newUnits.bottom = next; - } + if ( side === 'vertical' ) { + newUnits.top = next; + newUnits.bottom = next; + } - if ( side === 'horizontal' ) { - newUnits.left = next; - newUnits.right = next; - } + if ( side === 'horizontal' ) { + newUnits.left = next; + newUnits.right = next; + } - setSelectedUnits( newUnits ); - }; + setSelectedUnits( newUnits ); + }; // Filter sides if custom configuration provided, maintaining default order. const filteredSides = sides?.length diff --git a/packages/components/src/box-control/icon.js b/packages/components/src/box-control/icon.tsx similarity index 69% rename from packages/components/src/box-control/icon.js rename to packages/components/src/box-control/icon.tsx index 2a7db9972c5b13..6cb893648d68ab 100644 --- a/packages/components/src/box-control/icon.js +++ b/packages/components/src/box-control/icon.tsx @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import type { WordPressComponentProps } from '../ui/context'; import { Root, Viewbox, @@ -9,6 +10,7 @@ import { BottomStroke, LeftStroke, } from './styles/box-control-icon-styles'; +import type { BoxControlIconProps, BoxControlProps } from './types'; const BASE_ICON_SIZE = 24; @@ -17,11 +19,14 @@ export default function BoxControlIcon( { side = 'all', sides, ...props -} ) { - const isSideDisabled = ( value ) => - sides?.length && ! sides.includes( value ); +}: WordPressComponentProps< BoxControlIconProps, 'span' > ) { + const isSideDisabled = ( + value: NonNullable< BoxControlProps[ 'sides' ] >[ number ] + ) => sides?.length && ! sides.includes( value ); - const hasSide = ( value ) => { + const hasSide = ( + value: NonNullable< BoxControlProps[ 'sides' ] >[ number ] + ) => { if ( isSideDisabled( value ) ) { return false; } diff --git a/packages/components/src/box-control/index.js b/packages/components/src/box-control/index.tsx similarity index 78% rename from packages/components/src/box-control/index.js rename to packages/components/src/box-control/index.tsx index d8a0fdf4c67483..cf267aff44352e 100644 --- a/packages/components/src/box-control/index.js +++ b/packages/components/src/box-control/index.tsx @@ -29,6 +29,11 @@ import { isValuesDefined, } from './utils'; import { useControlledState } from '../utils/hooks'; +import type { + BoxControlIconProps, + BoxControlProps, + BoxControlValue, +} from './types'; const defaultInputProps = { min: 0, @@ -36,12 +41,38 @@ const defaultInputProps = { const noop = () => {}; -function useUniqueId( idProp ) { +function useUniqueId( idProp?: string ) { const instanceId = useInstanceId( BoxControl, 'inspector-box-control' ); return idProp || instanceId; } -export default function BoxControl( { + +/** + * BoxControl components let users set values for Top, Right, Bottom, and Left. + * This can be used as an input control for values like `padding` or `margin`. + * + * ```jsx + * import { __experimentalBoxControl as BoxControl } from '@wordpress/components'; + * import { useState } from '@wordpress/element'; + * + * const Example = () => { + * const [ values, setValues ] = useState( { + * top: '50px', + * left: '10%', + * right: '10%', + * bottom: '50px', + * } ); + * + * return ( + * setValues( nextValues ) } + * /> + * ); + * }; + * ``` + */ +function BoxControl( { id: idProp, inputProps = defaultInputProps, onChange = noop, @@ -54,7 +85,7 @@ export default function BoxControl( { resetValues = DEFAULT_VALUES, onMouseOver, onMouseOut, -} ) { +}: BoxControlProps ) { const [ values, setValues ] = useControlledState( valuesProp, { fallback: DEFAULT_VALUES, } ); @@ -67,14 +98,14 @@ export default function BoxControl( { ! hasInitialValue || ! isValuesMixed( inputValues ) || hasOneSide ); - const [ side, setSide ] = useState( + const [ side, setSide ] = useState< BoxControlIconProps[ 'side' ] >( getInitialSide( isLinked, splitOnAxis ) ); // Tracking selected units via internal state allows filtering of CSS unit // only values from being saved while maintaining preexisting unit selection // behaviour. Filtering CSS only values prevents invalid style values. - const [ selectedUnits, setSelectedUnits ] = useState( { + const [ selectedUnits, setSelectedUnits ] = useState< BoxControlValue >( { top: parseQuantityAndUnitFromRawValue( valuesProp?.top )[ 1 ], right: parseQuantityAndUnitFromRawValue( valuesProp?.right )[ 1 ], bottom: parseQuantityAndUnitFromRawValue( valuesProp?.bottom )[ 1 ], @@ -89,11 +120,14 @@ export default function BoxControl( { setSide( getInitialSide( ! isLinked, splitOnAxis ) ); }; - const handleOnFocus = ( event, { side: nextSide } ) => { + const handleOnFocus = ( + _event: React.FocusEvent< HTMLInputElement >, + { side: nextSide }: { side: typeof side } + ) => { setSide( nextSide ); }; - const handleOnChange = ( nextValues ) => { + const handleOnChange = ( nextValues: BoxControlValue ) => { onChange( nextValues ); setValues( nextValues ); setIsDirty( true ); @@ -132,7 +166,7 @@ export default function BoxControl( {