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

BoxControl: Convert to TypeScript #47622

Merged
merged 29 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
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 @@ -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)).

Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/box-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ If this property is true, when the box control is unlinked, vertical and horizon

### inputProps

Props for the internal [InputControl](../input-control) components.
Props for the internal [UnitControl](../unit-control) components.

- Type: `Object`
- Required: No
- Default: `{ min: 0 }`
Copy link
Member Author

Choose a reason for hiding this comment

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

These props are actually passed through to UnitControl, not InputControl. Why does it matter? InputControlProps[ 'max' ] is string but UnitControlProps[ 'max' ] is number ☠️

Copy link
Contributor

Choose a reason for hiding this comment

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

😵‍💫 Hopefully this will be progressively addressed as we work our way up from NumberControl


### label

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Copy link
Member Author

Choose a reason for hiding this comment

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

The placeholder prop on inputs do not accept null.


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 ) );
Copy link
Member Author

Choose a reason for hiding this comment

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

Just making the undefined case explicit. Should not change runtime behavior.

const nextValue = isNumeric ? next : undefined;
const nextValues = applyValueToSides( values, nextValue, sides );

Expand All @@ -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 );
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand All @@ -44,7 +48,7 @@ export default function AxialInputControls( {
}
};

const createHandleOnHoverOff = ( side ) => () => {
const createHandleOnHoverOff = ( side: GroupedSide ) => () => {
if ( ! onHoverOff ) {
return;
}
Expand All @@ -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' ) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Internal dependencies
*/
import type { WordPressComponentProps } from '../ui/context';
import {
Root,
Viewbox,
Expand All @@ -9,6 +10,7 @@ import {
BottomStroke,
LeftStroke,
} from './styles/box-control-icon-styles';
import type { BoxControlIconProps, BoxControlProps } from './types';

const BASE_ICON_SIZE = 24;

Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,50 @@ import {
isValuesDefined,
} from './utils';
import { useControlledState } from '../utils/hooks';
import type {
BoxControlIconProps,
BoxControlProps,
BoxControlValue,
} from './types';

const defaultInputProps = {
min: 0,
};

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 (
* <BoxControl
* values={ values }
* onChange={ ( nextValues ) => setValues( nextValues ) }
* />
* );
* };
* ```
*/
function BoxControl( {
id: idProp,
inputProps = defaultInputProps,
onChange = noop,
Expand All @@ -54,7 +85,7 @@ export default function BoxControl( {
resetValues = DEFAULT_VALUES,
onMouseOver,
onMouseOut,
} ) {
}: BoxControlProps ) {
const [ values, setValues ] = useControlledState( valuesProp, {
fallback: DEFAULT_VALUES,
} );
Expand All @@ -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 ],
Expand All @@ -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 );
Expand Down Expand Up @@ -132,7 +166,7 @@ export default function BoxControl( {
<FlexItem>
<Button
className="component-box-control__reset-button"
isSecondary
variant="secondary"
Copy link
Member Author

Choose a reason for hiding this comment

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

Updated a deprecated prop.

isSmall
onClick={ handleOnReset }
disabled={ ! isDirty }
Expand Down Expand Up @@ -176,3 +210,4 @@ export default function BoxControl( {
}

export { applyValueToSides } from './utils';
export default BoxControl;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import UnitControl from './unit-control';
import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils';
import { ALL_SIDES, LABELS } from './utils';
import { LayoutContainer, Layout } from './styles/box-control-styles';
import type { BoxControlInputControlProps, BoxControlValue } from './types';
import type { UnitControlProps } from '../unit-control/types';

const noop = () => {};

Expand All @@ -18,29 +20,33 @@ export default function BoxInputControls( {
setSelectedUnits,
sides,
...props
} ) {
const createHandleOnFocus = ( side ) => ( event ) => {
onFocus( event, { side } );
};
}: BoxControlInputControlProps ) {
const createHandleOnFocus =
( side: keyof BoxControlValue ) =>
( event: React.FocusEvent< HTMLInputElement > ) => {
onFocus( event, { side } );
};

const createHandleOnHoverOn = ( side ) => () => {
const createHandleOnHoverOn = ( side: keyof BoxControlValue ) => () => {
onHoverOn( { [ side ]: true } );
};

const createHandleOnHoverOff = ( side ) => () => {
const createHandleOnHoverOff = ( side: keyof BoxControlValue ) => () => {
onHoverOff( { [ side ]: false } );
};

const handleOnChange = ( nextValues ) => {
const handleOnChange = ( nextValues: BoxControlValue ) => {
onChange( nextValues );
};

const createHandleOnChange =
const createHandleOnChange: (
side: keyof BoxControlValue
) => UnitControlProps[ 'onChange' ] =
( side ) =>
( next, { event } ) => {
const { altKey } = event;
const nextValues = { ...values };
const isNumeric = ! isNaN( parseFloat( next ) );
const isNumeric =
next !== undefined && ! isNaN( parseFloat( next ) );
const nextValue = isNumeric ? next : undefined;

nextValues[ side ] = nextValue;
Expand All @@ -49,7 +55,9 @@ export default function BoxInputControls( {
* Supports changing pair sides. For example, holding the ALT key
* when changing the TOP will also update BOTTOM.
*/
if ( altKey ) {
// @ts-expect-error - event.altKey is only present when the change event was
// triggered by a keyboard event.
if ( event.altKey ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure it's a better solution, but we could avoid the @ts-expect-error by adding a typecast (and a runtime check for good measure?)

Suggested change
// @ts-expect-error - event.altKey is only present when the change event was
// triggered by a keyboard event.
if ( event.altKey ) {
if (
event.nativeEvent instanceof KeyboardEvent &&
( event as React.KeyboardEvent ).altKey
) {

haven't tested it at runtime, though

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll add a TODO comment here because I think it should be addressed in a different layer, for example in how the upstream change handler passes events. c64c2a6

switch ( side ) {
case 'top':
nextValues.bottom = nextValue;
Expand All @@ -69,11 +77,12 @@ export default function BoxInputControls( {
handleOnChange( nextValues );
};

const createHandleOnUnitChange = ( side ) => ( next ) => {
const newUnits = { ...selectedUnits };
newUnits[ side ] = next;
setSelectedUnits( newUnits );
};
const createHandleOnUnitChange =
( side: keyof BoxControlValue ) => ( next?: string ) => {
const newUnits = { ...selectedUnits };
newUnits[ side ] = next;
setSelectedUnits( newUnits );
};

// Filter sides if custom configuration provided, maintaining default order.
const filteredSides = sides?.length
Expand Down
Loading