From 14d80aa66819682118a73f3cb40485806b9b8c44 Mon Sep 17 00:00:00 2001
From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
Date: Fri, 6 Aug 2021 06:04:11 -0700
Subject: [PATCH] components: InputControl to TypeScript (#33696)
* components: InputControl to TypeScript
* Add back event to onChange
* Fix on validate and remove todo
* Remove cast and allow undefined values
* Add explanatory comment for omitting prefix
* Use currentTarget
* components: Convert Select to TypeScript (#33784)
* Convert Select to TypeScript
* Stop spreading props into InputBase and add all HTML attributes for select
* Remove need for `omit`
* Remove unused import
* Consistently use currentTarget
* Require isFocused props
---
packages/components/src/base-control/index.js | 2 +-
.../{backdrop.js => backdrop.tsx} | 0
.../src/input-control/{index.js => index.tsx} | 9 +-
.../{input-base.js => input-base.tsx} | 30 ++--
.../{input-field.js => input-field.tsx} | 53 ++++--
.../src/input-control/{label.js => label.tsx} | 4 +-
.../src/input-control/reducer/actions.ts | 66 ++++++++
.../{state.js => reducer/reducer.ts} | 156 +++++++++---------
.../src/input-control/reducer/state.ts | 36 ++++
...rol-styles.js => input-control-styles.tsx} | 119 ++++++++-----
.../components/src/input-control/types.ts | 86 ++++++++++
.../src/input-control/{utils.js => utils.ts} | 12 +-
.../components/src/number-control/index.js | 6 +-
.../select-control/{index.js => index.tsx} | 54 ++++--
...rol-styles.js => select-control-styles.ts} | 22 ++-
.../components/src/select-control/types.ts | 1 +
packages/components/src/unit-control/index.js | 6 +-
packages/components/tsconfig.json | 1 +
18 files changed, 469 insertions(+), 194 deletions(-)
rename packages/components/src/input-control/{backdrop.js => backdrop.tsx} (100%)
rename packages/components/src/input-control/{index.js => index.tsx} (89%)
rename packages/components/src/input-control/{input-base.js => input-base.tsx} (75%)
rename packages/components/src/input-control/{input-field.js => input-field.tsx} (76%)
rename packages/components/src/input-control/{label.js => label.tsx} (72%)
create mode 100644 packages/components/src/input-control/reducer/actions.ts
rename packages/components/src/input-control/{state.js => reducer/reducer.ts} (51%)
create mode 100644 packages/components/src/input-control/reducer/state.ts
rename packages/components/src/input-control/styles/{input-control-styles.js => input-control-styles.tsx} (64%)
create mode 100644 packages/components/src/input-control/types.ts
rename packages/components/src/input-control/{utils.js => utils.ts} (76%)
rename packages/components/src/select-control/{index.js => index.tsx} (66%)
rename packages/components/src/select-control/styles/{select-control-styles.js => select-control-styles.ts} (72%)
create mode 100644 packages/components/src/select-control/types.ts
diff --git a/packages/components/src/base-control/index.js b/packages/components/src/base-control/index.js
index bbeb0a7b77bb4d..4ec051c01159bd 100644
--- a/packages/components/src/base-control/index.js
+++ b/packages/components/src/base-control/index.js
@@ -20,7 +20,7 @@ import {
* That element should be passed as a child.
* @property {import('react').ReactNode} help If this property is added, a help text will be
* generated using help property as the content.
- * @property {import('react').ReactNode} label If this property is added, a label will be generated
+ * @property {import('react').ReactNode} [label] If this property is added, a label will be generated
* using label property as the content.
* @property {boolean} [hideLabelFromVision] If true, the label will only be visible to screen readers.
* @property {string} [className] The class that will be added with "components-base-control" to the
diff --git a/packages/components/src/input-control/backdrop.js b/packages/components/src/input-control/backdrop.tsx
similarity index 100%
rename from packages/components/src/input-control/backdrop.js
rename to packages/components/src/input-control/backdrop.tsx
diff --git a/packages/components/src/input-control/index.js b/packages/components/src/input-control/index.tsx
similarity index 89%
rename from packages/components/src/input-control/index.js
rename to packages/components/src/input-control/index.tsx
index 56eb23d950f704..cb20755bc54c9b 100644
--- a/packages/components/src/input-control/index.js
+++ b/packages/components/src/input-control/index.tsx
@@ -3,6 +3,8 @@
*/
import { noop } from 'lodash';
import classNames from 'classnames';
+// eslint-disable-next-line no-restricted-imports
+import type { Ref } from 'react';
/**
* WordPress dependencies
@@ -15,8 +17,9 @@ import { useState, forwardRef } from '@wordpress/element';
*/
import InputBase from './input-base';
import InputField from './input-field';
+import type { InputControlProps } from './types';
-function useUniqueId( idProp ) {
+function useUniqueId( idProp?: string ) {
const instanceId = useInstanceId( InputControl );
const id = `inspector-input-control-${ instanceId }`;
@@ -42,8 +45,8 @@ export function InputControl(
suffix,
value,
...props
- },
- ref
+ }: InputControlProps,
+ ref: Ref< HTMLInputElement >
) {
const [ isFocused, setIsFocused ] = useState( false );
diff --git a/packages/components/src/input-control/input-base.js b/packages/components/src/input-control/input-base.tsx
similarity index 75%
rename from packages/components/src/input-control/input-base.js
rename to packages/components/src/input-control/input-base.tsx
index 232e5c16a1f238..59fc18e5d93b52 100644
--- a/packages/components/src/input-control/input-base.js
+++ b/packages/components/src/input-control/input-base.tsx
@@ -1,3 +1,9 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import type { Ref } from 'react';
+
/**
* WordPress dependencies
*/
@@ -16,8 +22,9 @@ import {
Suffix,
LabelWrapper,
} from './styles/input-control-styles';
+import type { InputBaseProps, LabelPosition } from './types';
-function useUniqueId( idProp ) {
+function useUniqueId( idProp?: string ) {
const instanceId = useInstanceId( InputBase );
const id = `input-base-control-${ instanceId }`;
@@ -25,8 +32,8 @@ function useUniqueId( idProp ) {
}
// Adapter to map props for the new ui/flex compopnent.
-function getUIFlexProps( { labelPosition } ) {
- const props = {};
+function getUIFlexProps( labelPosition?: LabelPosition ) {
+ const props: { direction?: string; gap?: number; justify?: string } = {};
switch ( labelPosition ) {
case 'top':
props.direction = 'column';
@@ -59,21 +66,21 @@ export function InputBase(
size = 'default',
suffix,
...props
- },
- ref
+ }: InputBaseProps,
+ ref: Ref< HTMLDivElement >
) {
const id = useUniqueId( idProp );
const hideLabel = hideLabelFromVision || ! label;
return (
+ // @ts-expect-error The `direction` prop from Flex (FlexDirection) conflicts with legacy SVGAttributes `direction` (string) that come from React intrinsic prop definitions
);
diff --git a/packages/components/src/input-control/input-field.js b/packages/components/src/input-control/input-field.tsx
similarity index 76%
rename from packages/components/src/input-control/input-field.js
rename to packages/components/src/input-control/input-field.tsx
index 5931c789f651b9..099c72890fcfe7 100644
--- a/packages/components/src/input-control/input-field.js
+++ b/packages/components/src/input-control/input-field.tsx
@@ -3,6 +3,16 @@
*/
import { noop } from 'lodash';
import { useDrag } from 'react-use-gesture';
+// eslint-disable-next-line no-restricted-imports
+import type {
+ SyntheticEvent,
+ ChangeEvent,
+ KeyboardEvent,
+ PointerEvent,
+ FocusEvent,
+ Ref,
+ MouseEvent,
+} from 'react';
/**
* WordPress dependencies
@@ -12,11 +22,13 @@ import { UP, DOWN, ENTER } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
+import type { PolymorphicComponentProps } from '../ui/context';
import { useDragCursor } from './utils';
import { Input } from './styles/input-control-styles';
-import { useInputControlStateReducer } from './state';
+import { useInputControlStateReducer } from './reducer/reducer';
import { isValueEmpty } from '../utils/values';
import { useUpdateEffect } from '../utils';
+import type { InputFieldProps } from './types';
function InputField(
{
@@ -37,12 +49,12 @@ function InputField(
onValidate = noop,
size = 'default',
setIsFocused,
- stateReducer = ( state ) => state,
+ stateReducer = ( state: any ) => state,
value: valueProp,
type,
...props
- },
- ref
+ }: PolymorphicComponentProps< InputFieldProps, 'input', false >,
+ ref: Ref< HTMLInputElement >
) {
const {
// State
@@ -82,14 +94,16 @@ function InputField(
return;
}
if ( ! isFocused && ! wasDirtyOnBlur.current ) {
- update( valueProp );
+ update( valueProp, _event as SyntheticEvent );
} else if ( ! isDirty ) {
- onChange( value, { event: _event } );
+ onChange( value, {
+ event: _event as ChangeEvent< HTMLInputElement >,
+ } );
wasDirtyOnBlur.current = false;
}
}, [ value, isDirty, isFocused, valueProp ] );
- const handleOnBlur = ( event ) => {
+ const handleOnBlur = ( event: FocusEvent< HTMLInputElement > ) => {
onBlur( event );
setIsFocused( false );
@@ -102,23 +116,23 @@ function InputField(
if ( ! isValueEmpty( value ) ) {
handleOnCommit( event );
} else {
- reset( valueProp );
+ reset( valueProp, event );
}
}
};
- const handleOnFocus = ( event ) => {
+ const handleOnFocus = ( event: FocusEvent< HTMLInputElement > ) => {
onFocus( event );
setIsFocused( true );
};
- const handleOnChange = ( event ) => {
+ const handleOnChange = ( event: ChangeEvent< HTMLInputElement > ) => {
const nextValue = event.target.value;
change( nextValue, event );
};
- const handleOnCommit = ( event ) => {
- const nextValue = event.target.value;
+ const handleOnCommit = ( event: SyntheticEvent< HTMLInputElement > ) => {
+ const nextValue = event.currentTarget.value;
try {
onValidate( nextValue, event );
@@ -128,7 +142,7 @@ function InputField(
}
};
- const handleOnKeyDown = ( event ) => {
+ const handleOnKeyDown = ( event: KeyboardEvent< HTMLInputElement > ) => {
const { keyCode } = event;
onKeyDown( event );
@@ -152,7 +166,7 @@ function InputField(
}
};
- const dragGestureProps = useDrag(
+ const dragGestureProps = useDrag< PointerEvent< HTMLInputElement > >(
( dragProps ) => {
const { distance, dragging, event } = dragProps;
// The event is persisted to prevent errors in components using this
@@ -193,10 +207,13 @@ function InputField(
*/
let handleOnMouseDown;
if ( type === 'number' ) {
- handleOnMouseDown = ( event ) => {
+ handleOnMouseDown = ( event: MouseEvent< HTMLInputElement > ) => {
props.onMouseDown?.( event );
- if ( event.target !== event.target.ownerDocument.activeElement ) {
- event.target.focus();
+ if (
+ event.currentTarget !==
+ event.currentTarget.ownerDocument.activeElement
+ ) {
+ event.currentTarget.focus();
}
};
}
@@ -216,7 +233,7 @@ function InputField(
onKeyDown={ handleOnKeyDown }
onMouseDown={ handleOnMouseDown }
ref={ ref }
- size={ size }
+ inputSize={ size }
value={ value }
type={ type }
/>
diff --git a/packages/components/src/input-control/label.js b/packages/components/src/input-control/label.tsx
similarity index 72%
rename from packages/components/src/input-control/label.js
rename to packages/components/src/input-control/label.tsx
index 1c5888fa965128..537c335373b5d8 100644
--- a/packages/components/src/input-control/label.js
+++ b/packages/components/src/input-control/label.tsx
@@ -3,13 +3,15 @@
*/
import { VisuallyHidden } from '../visually-hidden';
import { Label as BaseLabel } from './styles/input-control-styles';
+import type { PolymorphicComponentProps } from '../ui/context';
+import type { InputControlLabelProps } from './types';
export default function Label( {
children,
hideLabelFromVision,
htmlFor,
...props
-} ) {
+}: PolymorphicComponentProps< InputControlLabelProps, 'label', false > ) {
if ( ! children ) return null;
if ( hideLabelFromVision ) {
diff --git a/packages/components/src/input-control/reducer/actions.ts b/packages/components/src/input-control/reducer/actions.ts
new file mode 100644
index 00000000000000..652e7211e251e4
--- /dev/null
+++ b/packages/components/src/input-control/reducer/actions.ts
@@ -0,0 +1,66 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import type { SyntheticEvent } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import type { DragProps } from '../types';
+
+export const CHANGE = 'CHANGE';
+export const COMMIT = 'COMMIT';
+export const DRAG_END = 'DRAG_END';
+export const DRAG_START = 'DRAG_START';
+export const DRAG = 'DRAG';
+export const INVALIDATE = 'INVALIDATE';
+export const PRESS_DOWN = 'PRESS_DOWN';
+export const PRESS_ENTER = 'PRESS_ENTER';
+export const PRESS_UP = 'PRESS_UP';
+export const RESET = 'RESET';
+export const UPDATE = 'UPDATE';
+
+interface EventPayload {
+ event?: SyntheticEvent;
+}
+
+interface Action< Type, ExtraPayload = {} > {
+ type: Type;
+ payload: EventPayload & ExtraPayload;
+}
+
+interface ValuePayload {
+ value: string;
+}
+
+export type ChangeAction = Action< typeof CHANGE, ValuePayload >;
+export type CommitAction = Action< typeof COMMIT, ValuePayload >;
+export type PressUpAction = Action< typeof PRESS_UP >;
+export type PressDownAction = Action< typeof PRESS_DOWN >;
+export type PressEnterAction = Action< typeof PRESS_ENTER >;
+export type DragStartAction = Action< typeof DRAG_START, DragProps >;
+export type DragEndAction = Action< typeof DRAG_END, DragProps >;
+export type DragAction = Action< typeof DRAG, DragProps >;
+export type ResetAction = Action< typeof RESET, Partial< ValuePayload > >;
+export type UpdateAction = Action< typeof UPDATE, ValuePayload >;
+export type InvalidateAction = Action<
+ typeof INVALIDATE,
+ { error: Error | null }
+>;
+
+export type ChangeEventAction =
+ | ChangeAction
+ | ResetAction
+ | CommitAction
+ | UpdateAction;
+
+export type DragEventAction = DragStartAction | DragEndAction | DragAction;
+
+export type KeyEventAction = PressDownAction | PressUpAction | PressEnterAction;
+
+export type InputAction =
+ | ChangeEventAction
+ | KeyEventAction
+ | DragEventAction
+ | InvalidateAction;
diff --git a/packages/components/src/input-control/state.js b/packages/components/src/input-control/reducer/reducer.ts
similarity index 51%
rename from packages/components/src/input-control/state.js
rename to packages/components/src/input-control/reducer/reducer.ts
index d0e0a413ed578b..2499096a843384 100644
--- a/packages/components/src/input-control/state.js
+++ b/packages/components/src/input-control/reducer/reducer.ts
@@ -2,47 +2,34 @@
* External dependencies
*/
import { isEmpty } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
+import type { SyntheticEvent } from 'react';
+
/**
* WordPress dependencies
*/
import { useReducer } from '@wordpress/element';
-const initialStateReducer = ( state ) => state;
-
-const initialInputControlState = {
- _event: {},
- error: null,
- initialValue: '',
- isDirty: false,
- isDragEnabled: false,
- isDragging: false,
- isPressEnterToChange: false,
- value: '',
-};
-
-const actionTypes = {
- CHANGE: 'CHANGE',
- COMMIT: 'COMMIT',
- DRAG_END: 'DRAG_END',
- DRAG_START: 'DRAG_START',
- DRAG: 'DRAG',
- INVALIDATE: 'INVALIDATE',
- PRESS_DOWN: 'PRESS_DOWN',
- PRESS_ENTER: 'PRESS_ENTER',
- PRESS_UP: 'PRESS_UP',
- RESET: 'RESET',
- UPDATE: 'UPDATE',
-};
-
-export const inputControlActionTypes = actionTypes;
+/**
+ * Internal dependencies
+ */
+import {
+ InputState,
+ StateReducer,
+ initialInputControlState,
+ initialStateReducer,
+} from './state';
+import * as actions from './actions';
/**
* Prepares initialState for the reducer.
*
- * @param {Object} initialState The initial state.
- * @return {Object} Prepared initialState for the reducer
+ * @param initialState The initial state.
+ * @return Prepared initialState for the reducer
*/
-function mergeInitialState( initialState = initialInputControlState ) {
+function mergeInitialState(
+ initialState: Partial< InputState > = initialInputControlState
+): InputState {
const { value } = initialState;
return {
@@ -56,15 +43,17 @@ function mergeInitialState( initialState = initialInputControlState ) {
* Composes multiple stateReducers into a single stateReducer, building
* the pipeline to control the flow for state and actions.
*
- * @param {...Function} fns State reducers.
- * @return {Function} The single composed stateReducer.
+ * @param fns State reducers.
+ * @return The single composed stateReducer.
*/
-export const composeStateReducers = ( ...fns ) => {
+export const composeStateReducers = (
+ ...fns: StateReducer[]
+): StateReducer => {
return ( ...args ) => {
return fns.reduceRight( ( state, fn ) => {
const fnState = fn( ...args );
return isEmpty( fnState ) ? state : { ...state, ...fnState };
- }, {} );
+ }, {} as InputState );
};
};
@@ -75,43 +64,44 @@ export const composeStateReducers = ( ...fns ) => {
* This technique uses the "stateReducer" design pattern:
* https://kentcdodds.com/blog/the-state-reducer-pattern/
*
- * @param {Function} composedStateReducers A custom reducer that can subscribe and modify state.
- * @return {Function} The reducer.
+ * @param composedStateReducers A custom reducer that can subscribe and modify state.
+ * @return The reducer.
*/
-function inputControlStateReducer( composedStateReducers ) {
+function inputControlStateReducer(
+ composedStateReducers: StateReducer
+): StateReducer {
return ( state, action ) => {
const nextState = { ...state };
- const { type, payload } = action;
- switch ( type ) {
+ switch ( action.type ) {
/**
* Keyboard events
*/
- case actionTypes.PRESS_UP:
+ case actions.PRESS_UP:
nextState.isDirty = false;
break;
- case actionTypes.PRESS_DOWN:
+ case actions.PRESS_DOWN:
nextState.isDirty = false;
break;
/**
* Drag events
*/
- case actionTypes.DRAG_START:
+ case actions.DRAG_START:
nextState.isDragging = true;
break;
- case actionTypes.DRAG_END:
+ case actions.DRAG_END:
nextState.isDragging = false;
break;
/**
* Input events
*/
- case actionTypes.CHANGE:
+ case actions.CHANGE:
nextState.error = null;
- nextState.value = payload.value;
+ nextState.value = action.payload.value;
if ( state.isPressEnterToChange ) {
nextState.isDirty = true;
@@ -119,32 +109,32 @@ function inputControlStateReducer( composedStateReducers ) {
break;
- case actionTypes.COMMIT:
- nextState.value = payload.value;
+ case actions.COMMIT:
+ nextState.value = action.payload.value;
nextState.isDirty = false;
break;
- case actionTypes.RESET:
+ case actions.RESET:
nextState.error = null;
nextState.isDirty = false;
- nextState.value = payload.value || state.initialValue;
+ nextState.value = action.payload.value || state.initialValue;
break;
- case actionTypes.UPDATE:
- nextState.value = payload.value;
+ case actions.UPDATE:
+ nextState.value = action.payload.value;
nextState.isDirty = false;
break;
/**
* Validation
*/
- case actionTypes.INVALIDATE:
- nextState.error = payload.error;
+ case actions.INVALIDATE:
+ nextState.error = action.payload.error;
break;
}
- if ( payload.event ) {
- nextState._event = payload.event;
+ if ( action.payload.event ) {
+ nextState._event = action.payload.event;
}
/**
@@ -166,20 +156,23 @@ function inputControlStateReducer( composedStateReducers ) {
* This technique uses the "stateReducer" design pattern:
* https://kentcdodds.com/blog/the-state-reducer-pattern/
*
- * @param {Function} stateReducer An external state reducer.
- * @param {Object} initialState The initial state for the reducer.
- * @return {Object} State, dispatch, and a collection of actions.
+ * @param stateReducer An external state reducer.
+ * @param initialState The initial state for the reducer.
+ * @return State, dispatch, and a collection of actions.
*/
export function useInputControlStateReducer(
- stateReducer = initialStateReducer,
- initialState = initialInputControlState
+ stateReducer: StateReducer = initialStateReducer,
+ initialState: Partial< InputState > = initialInputControlState
) {
- const [ state, dispatch ] = useReducer(
+ const [ state, dispatch ] = useReducer< StateReducer >(
inputControlStateReducer( stateReducer ),
mergeInitialState( initialState )
);
- const createChangeEvent = ( type ) => ( nextValue, event ) => {
+ const createChangeEvent = ( type: actions.ChangeEventAction[ 'type' ] ) => (
+ nextValue: actions.ChangeEventAction[ 'payload' ][ 'value' ],
+ event: actions.ChangeEventAction[ 'payload' ][ 'event' ]
+ ) => {
/**
* Persist allows for the (Synthetic) event to be used outside of
* this function call.
@@ -192,10 +185,12 @@ export function useInputControlStateReducer(
dispatch( {
type,
payload: { value: nextValue, event },
- } );
+ } as actions.InputAction );
};
- const createKeyEvent = ( type ) => ( event ) => {
+ const createKeyEvent = ( type: actions.KeyEventAction[ 'type' ] ) => (
+ event: actions.KeyEventAction[ 'payload' ][ 'event' ]
+ ) => {
/**
* Persist allows for the (Synthetic) event to be used outside of
* this function call.
@@ -208,26 +203,29 @@ export function useInputControlStateReducer(
dispatch( { type, payload: { event } } );
};
- const createDragEvent = ( type ) => ( dragProps ) => {
- dispatch( { type, payload: dragProps } );
+ const createDragEvent = ( type: actions.DragEventAction[ 'type' ] ) => (
+ payload: actions.DragEventAction[ 'payload' ]
+ ) => {
+ dispatch( { type, payload } );
};
/**
* Actions for the reducer
*/
- const change = createChangeEvent( actionTypes.CHANGE );
- const invalidate = createChangeEvent( actionTypes.INVALIDATE );
- const reset = createChangeEvent( actionTypes.RESET );
- const commit = createChangeEvent( actionTypes.COMMIT );
- const update = createChangeEvent( actionTypes.UPDATE );
+ const change = createChangeEvent( actions.CHANGE );
+ const invalidate = ( error: Error, event: SyntheticEvent ) =>
+ dispatch( { type: actions.INVALIDATE, payload: { error, event } } );
+ const reset = createChangeEvent( actions.RESET );
+ const commit = createChangeEvent( actions.COMMIT );
+ const update = createChangeEvent( actions.UPDATE );
- const dragStart = createDragEvent( actionTypes.DRAG_START );
- const drag = createDragEvent( actionTypes.DRAG );
- const dragEnd = createDragEvent( actionTypes.DRAG_END );
+ const dragStart = createDragEvent( actions.DRAG_START );
+ const drag = createDragEvent( actions.DRAG );
+ const dragEnd = createDragEvent( actions.DRAG_END );
- const pressUp = createKeyEvent( actionTypes.PRESS_UP );
- const pressDown = createKeyEvent( actionTypes.PRESS_DOWN );
- const pressEnter = createKeyEvent( actionTypes.PRESS_ENTER );
+ const pressUp = createKeyEvent( actions.PRESS_UP );
+ const pressDown = createKeyEvent( actions.PRESS_DOWN );
+ const pressEnter = createKeyEvent( actions.PRESS_ENTER );
return {
change,
@@ -243,5 +241,5 @@ export function useInputControlStateReducer(
reset,
state,
update,
- };
+ } as const;
}
diff --git a/packages/components/src/input-control/reducer/state.ts b/packages/components/src/input-control/reducer/state.ts
new file mode 100644
index 00000000000000..9fcbfad2d1ae28
--- /dev/null
+++ b/packages/components/src/input-control/reducer/state.ts
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import type { Reducer } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import type { InputAction } from './actions';
+
+export interface InputState {
+ _event: Event | {};
+ error: Error | null;
+ initialValue?: string;
+ isDirty: boolean;
+ isDragEnabled: boolean;
+ isDragging: boolean;
+ isPressEnterToChange: boolean;
+ value?: string;
+}
+
+export type StateReducer = Reducer< InputState, InputAction >;
+
+export const initialStateReducer: StateReducer = ( state: InputState ) => state;
+
+export const initialInputControlState: InputState = {
+ _event: {},
+ error: null,
+ initialValue: '',
+ isDirty: false,
+ isDragEnabled: false,
+ isDragging: false,
+ isPressEnterToChange: false,
+ value: '',
+};
diff --git a/packages/components/src/input-control/styles/input-control-styles.js b/packages/components/src/input-control/styles/input-control-styles.tsx
similarity index 64%
rename from packages/components/src/input-control/styles/input-control-styles.js
rename to packages/components/src/input-control/styles/input-control-styles.tsx
index f588268f0cf3e4..bbd518ccb4822d 100644
--- a/packages/components/src/input-control/styles/input-control-styles.js
+++ b/packages/components/src/input-control/styles/input-control-styles.tsx
@@ -1,27 +1,39 @@
/**
* External dependencies
*/
-import { css } from '@emotion/react';
+import { css, SerializedStyles } from '@emotion/react';
import styled from '@emotion/styled';
+// eslint-disable-next-line no-restricted-imports
+import type { CSSProperties, ReactNode } from 'react';
/**
* Internal dependencies
*/
+import type { PolymorphicComponentProps } from '../../ui/context';
import { Flex, FlexItem } from '../../flex';
import { Text } from '../../text';
import { COLORS, rtl } from '../../utils';
+import type { LabelPosition } from '../types';
-const rootFloatLabelStyles = () => {
- return css( { paddingTop: 0 } );
+type ContainerProps = {
+ disabled?: boolean;
+ hideLabel?: boolean;
+ __unstableInputWidth?: CSSProperties[ 'width' ];
+ labelPosition?: LabelPosition;
};
-const rootFocusedStyles = ( { isFocused } ) => {
+type RootProps = {
+ isFocused?: boolean;
+ labelPosition?: LabelPosition;
+};
+
+const rootFocusedStyles = ( { isFocused }: RootProps ) => {
if ( ! isFocused ) return '';
return css( { zIndex: 1 } );
};
-const rootLabelPositionStyles = ( { labelPosition } ) => {
+const rootLabelPositionStyles = ( { labelPosition }: RootProps ) => {
switch ( labelPosition ) {
case 'top':
return css`
@@ -42,16 +54,15 @@ const rootLabelPositionStyles = ( { labelPosition } ) => {
}
};
-export const Root = styled( Flex )`
+export const Root = styled( Flex )< RootProps >`
position: relative;
border-radius: 2px;
-
- ${ rootFloatLabelStyles }
+ padding-top: 0;
${ rootFocusedStyles }
${ rootLabelPositionStyles }
`;
-const containerDisabledStyles = ( { disabled } ) => {
+const containerDisabledStyles = ( { disabled }: ContainerProps ) => {
const backgroundColor = disabled
? COLORS.ui.backgroundDisabled
: COLORS.ui.background;
@@ -60,11 +71,14 @@ const containerDisabledStyles = ( { disabled } ) => {
};
// Normalizes the margins from the (components/ui/flex/) container.
-const containerMarginStyles = ( { hideLabel } ) => {
+const containerMarginStyles = ( { hideLabel }: ContainerProps ) => {
return hideLabel ? css( { margin: '0 !important' } ) : null;
};
-const containerWidthStyles = ( { __unstableInputWidth, labelPosition } ) => {
+const containerWidthStyles = ( {
+ __unstableInputWidth,
+ labelPosition,
+}: ContainerProps ) => {
if ( ! __unstableInputWidth ) return css( { width: '100%' } );
if ( labelPosition === 'side' ) return '';
@@ -78,7 +92,7 @@ const containerWidthStyles = ( { __unstableInputWidth, labelPosition } ) => {
return css( { width: __unstableInputWidth } );
};
-export const Container = styled.div`
+export const Container = styled.div< ContainerProps >`
align-items: center;
box-sizing: border-box;
border-radius: inherit;
@@ -91,7 +105,16 @@ export const Container = styled.div`
${ containerWidthStyles }
`;
-const disabledStyles = ( { disabled } ) => {
+type Size = 'default' | 'small';
+
+type InputProps = {
+ disabled?: boolean;
+ inputSize?: Size;
+ isDragging?: boolean;
+ dragCursor?: CSSProperties[ 'cursor' ];
+};
+
+const disabledStyles = ( { disabled }: InputProps ) => {
if ( ! disabled ) return '';
return css( {
@@ -99,13 +122,13 @@ const disabledStyles = ( { disabled } ) => {
} );
};
-const fontSizeStyles = ( { size } ) => {
+const fontSizeStyles = ( { inputSize: size }: InputProps ) => {
const sizes = {
default: '13px',
small: '11px',
};
- const fontSize = sizes[ size ];
+ const fontSize = sizes[ size as Size ] || sizes.default;
const fontSizeMobile = '16px';
if ( ! fontSize ) return '';
@@ -119,7 +142,7 @@ const fontSizeStyles = ( { size } ) => {
`;
};
-const sizeStyles = ( { size } ) => {
+const sizeStyles = ( { inputSize: size }: InputProps ) => {
const sizes = {
default: {
height: 30,
@@ -133,22 +156,14 @@ const sizeStyles = ( { size } ) => {
},
};
- const style = sizes[ size ] || sizes.default;
+ const style = sizes[ size as Size ] || sizes.default;
return css( style );
};
-const placeholderStyles = () => {
- return css`
- &::-webkit-input-placeholder {
- line-height: normal;
- }
- `;
-};
-
-const dragStyles = ( { isDragging, dragCursor } ) => {
- let defaultArrowStyles = '';
- let activeDragCursorStyles = '';
+const dragStyles = ( { isDragging, dragCursor }: InputProps ) => {
+ let defaultArrowStyles: SerializedStyles | undefined;
+ let activeDragCursorStyles: SerializedStyles | undefined;
if ( isDragging ) {
defaultArrowStyles = css`
@@ -180,7 +195,7 @@ const dragStyles = ( { isDragging, dragCursor } ) => {
// TODO: Resolve need to use &&& to increase specificity
// https://github.com/WordPress/gutenberg/issues/18483
-export const Input = styled.input`
+export const Input = styled.input< InputProps >`
&&& {
background-color: transparent;
box-sizing: border-box;
@@ -199,19 +214,17 @@ export const Input = styled.input`
${ fontSizeStyles }
${ sizeStyles }
- ${ placeholderStyles }
+ &::-webkit-input-placeholder {
+ line-height: normal;
+ }
}
`;
-const labelTruncation = () => {
- return css`
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- `;
-};
-
-const labelPadding = ( { labelPosition } ) => {
+const labelPadding = ( {
+ labelPosition,
+}: {
+ labelPosition?: LabelPosition;
+} ) => {
let paddingBottom = 4;
if ( labelPosition === 'edge' || labelPosition === 'side' ) {
@@ -221,7 +234,7 @@ const labelPadding = ( { labelPosition } ) => {
return css( { paddingTop: 0, paddingBottom } );
};
-const BaseLabel = styled( Text )`
+const BaseLabel = styled( Text )< { labelPosition?: LabelPosition } >`
&&& {
box-sizing: border-box;
color: currentColor;
@@ -231,20 +244,36 @@ const BaseLabel = styled( Text )`
z-index: 1;
${ labelPadding }
- ${ labelTruncation }
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
`;
-export const Label = ( props ) => ;
+export const Label = (
+ props: PolymorphicComponentProps<
+ { labelPosition?: LabelPosition; children: ReactNode },
+ 'label',
+ false
+ >
+) => ;
export const LabelWrapper = styled( FlexItem )`
max-width: calc( 100% - 10px );
`;
-const backdropFocusedStyles = ( { disabled, isFocused } ) => {
+type BackdropProps = {
+ disabled?: boolean;
+ isFocused?: boolean;
+};
+
+const backdropFocusedStyles = ( {
+ disabled,
+ isFocused,
+}: BackdropProps ): SerializedStyles => {
let borderColor = isFocused ? COLORS.ui.borderFocus : COLORS.ui.border;
- let boxShadow = null;
+ let boxShadow;
if ( isFocused ) {
boxShadow = `0 0 0 1px ${ COLORS.ui.borderFocus } inset`;
@@ -262,7 +291,7 @@ const backdropFocusedStyles = ( { disabled, isFocused } ) => {
} );
};
-export const BackdropUI = styled.div`
+export const BackdropUI = styled.div< BackdropProps >`
&&& {
box-sizing: border-box;
border-radius: inherit;
diff --git a/packages/components/src/input-control/types.ts b/packages/components/src/input-control/types.ts
new file mode 100644
index 00000000000000..bb22b704005655
--- /dev/null
+++ b/packages/components/src/input-control/types.ts
@@ -0,0 +1,86 @@
+/**
+ * External dependencies
+ */
+// eslint-disable-next-line no-restricted-imports
+import type {
+ CSSProperties,
+ ReactNode,
+ ChangeEvent,
+ SyntheticEvent,
+} from 'react';
+import type { useDrag } from 'react-use-gesture';
+
+/**
+ * Internal dependencies
+ */
+import type { StateReducer } from './reducer/state';
+import type { FlexProps } from '../flex/types';
+import type { PolymorphicComponentProps } from '../ui/context';
+
+export type LabelPosition = 'top' | 'bottom' | 'side' | 'edge';
+
+export type DragDirection = 'n' | 's' | 'e' | 'w';
+
+export type DragProps = Parameters< Parameters< typeof useDrag >[ 0 ] >[ 0 ];
+
+interface BaseProps {
+ __unstableInputWidth?: CSSProperties[ 'width' ];
+ hideLabelFromVision?: boolean;
+ isFocused: boolean;
+ labelPosition?: LabelPosition;
+ size?: 'default' | 'small';
+}
+
+export interface InputFieldProps extends BaseProps {
+ dragDirection?: DragDirection;
+ dragThreshold?: number;
+ isDragEnabled?: boolean;
+ isPressEnterToChange?: boolean;
+ onChange?: (
+ nextValue: string | undefined,
+ extra: { event: ChangeEvent< HTMLInputElement > }
+ ) => void;
+ onValidate?: (
+ nextValue: string,
+ event?: SyntheticEvent< HTMLInputElement >
+ ) => void;
+ setIsFocused: ( isFocused: boolean ) => void;
+ stateReducer?: StateReducer;
+ value?: string;
+ onDragEnd?: ( dragProps: DragProps ) => void;
+ onDragStart?: ( dragProps: DragProps ) => void;
+ onDrag?: ( dragProps: DragProps ) => void;
+}
+
+export interface InputBaseProps extends BaseProps, FlexProps {
+ children: ReactNode;
+ prefix?: ReactNode;
+ suffix?: ReactNode;
+ disabled?: boolean;
+ className?: string;
+ id?: string;
+ label?: ReactNode;
+}
+
+export interface InputControlProps
+ extends Omit< InputBaseProps, 'children' >,
+ /**
+ * The `prefix` prop in `PolymorphicComponentProps< InputFieldProps, 'input', false >` comes from the
+ * `HTMLInputAttributes` and clashes with the one from `InputBaseProps`. So we have to omit it from
+ * `PolymorphicComponentProps< InputFieldProps, 'input', false >` in order that `InputBaseProps[ 'prefix' ]`
+ * be the only prefix prop. Otherwise it tries to do a union of the two prefix properties and you end up
+ * with an unresolvable type.
+ */
+ Omit<
+ PolymorphicComponentProps< InputFieldProps, 'input', false >,
+ 'stateReducer' | 'prefix'
+ > {
+ __unstableStateReducer?: InputFieldProps[ 'stateReducer' ];
+}
+
+export interface InputControlLabelProps {
+ children: ReactNode;
+ hideLabelFromVision?: BaseProps[ 'hideLabelFromVision' ];
+ labelPosition?: BaseProps[ 'labelPosition' ];
+ size?: BaseProps[ 'size' ];
+}
diff --git a/packages/components/src/input-control/utils.js b/packages/components/src/input-control/utils.ts
similarity index 76%
rename from packages/components/src/input-control/utils.js
rename to packages/components/src/input-control/utils.ts
index ae896c4932e55d..3c755de9b7c174 100644
--- a/packages/components/src/input-control/utils.js
+++ b/packages/components/src/input-control/utils.ts
@@ -6,10 +6,10 @@ import { useEffect } from '@wordpress/element';
/**
* Gets a CSS cursor value based on a drag direction.
*
- * @param {string} dragDirection The drag direction.
- * @return {string} The CSS cursor value.
+ * @param dragDirection The drag direction.
+ * @return The CSS cursor value.
*/
-export function getDragCursor( dragDirection ) {
+export function getDragCursor( dragDirection: string ): string {
let dragCursor = 'ns-resize';
switch ( dragDirection ) {
@@ -35,13 +35,17 @@ export function getDragCursor( dragDirection ) {
*
* @return {string} The CSS cursor value.
*/
-export function useDragCursor( isDragging, dragDirection ) {
+export function useDragCursor(
+ isDragging: boolean,
+ dragDirection: string
+): string {
const dragCursor = getDragCursor( dragDirection );
useEffect( () => {
if ( isDragging ) {
document.documentElement.style.cursor = dragCursor;
} else {
+ // @ts-expect-error
document.documentElement.style.cursor = null;
}
}, [ isDragging ] );
diff --git a/packages/components/src/number-control/index.js b/packages/components/src/number-control/index.js
index 5ba0fb3e7aaee9..2df94561f8ecb5 100644
--- a/packages/components/src/number-control/index.js
+++ b/packages/components/src/number-control/index.js
@@ -13,10 +13,8 @@ import { isRTL } from '@wordpress/i18n';
* Internal dependencies
*/
import { Input } from './styles/number-control-styles';
-import {
- inputControlActionTypes,
- composeStateReducers,
-} from '../input-control/state';
+import * as inputControlActionTypes from '../input-control/reducer/actions';
+import { composeStateReducers } from '../input-control/reducer/reducer';
import { add, subtract, roundClamp } from '../utils/math';
import { useJumpStep } from '../utils/hooks';
import { isValueEmpty } from '../utils/values';
diff --git a/packages/components/src/select-control/index.js b/packages/components/src/select-control/index.tsx
similarity index 66%
rename from packages/components/src/select-control/index.js
rename to packages/components/src/select-control/index.tsx
index d93be19e11b353..7a036f5ce9234e 100644
--- a/packages/components/src/select-control/index.js
+++ b/packages/components/src/select-control/index.tsx
@@ -3,6 +3,8 @@
*/
import { isEmpty, noop } from 'lodash';
import classNames from 'classnames';
+// eslint-disable-next-line no-restricted-imports
+import type { ChangeEvent, FocusEvent, Ref } from 'react';
/**
* WordPress dependencies
@@ -16,15 +18,39 @@ import { Icon, chevronDown } from '@wordpress/icons';
*/
import BaseControl from '../base-control';
import InputBase from '../input-control/input-base';
+import type { InputBaseProps, LabelPosition } from '../input-control/types';
import { Select, DownArrowWrapper } from './styles/select-control-styles';
+import type { Size } from './types';
+import type { PolymorphicComponentProps } from '../ui/context';
-function useUniqueId( idProp ) {
+function useUniqueId( idProp?: string ) {
const instanceId = useInstanceId( SelectControl );
const id = `inspector-select-control-${ instanceId }`;
return idProp || id;
}
+export interface SelectControlProps extends Omit< InputBaseProps, 'children' > {
+ help?: string;
+ hideLabelFromVision?: boolean;
+ multiple?: boolean;
+ onBlur?: ( event: FocusEvent< HTMLSelectElement > ) => void;
+ onFocus?: ( event: FocusEvent< HTMLSelectElement > ) => void;
+ onChange?: (
+ value: string | string[],
+ extra?: { event?: ChangeEvent< HTMLSelectElement > }
+ ) => void;
+ options?: {
+ label: string;
+ value: string;
+ id?: string;
+ disabled?: boolean;
+ }[];
+ size?: Size;
+ value?: string | string[];
+ labelPosition?: LabelPosition;
+}
+
function SelectControl(
{
className,
@@ -41,9 +67,11 @@ function SelectControl(
size = 'default',
value: valueProp,
labelPosition = 'top',
+ prefix,
+ suffix,
...props
- },
- ref
+ }: PolymorphicComponentProps< SelectControlProps, 'select', false >,
+ ref: Ref< HTMLSelectElement >
) {
const [ isFocused, setIsFocused ] = useState( false );
const id = useUniqueId( idProp );
@@ -52,17 +80,17 @@ function SelectControl(
// Disable reason: A select with an onchange throws a warning
if ( isEmpty( options ) ) return null;
- const handleOnBlur = ( event ) => {
+ const handleOnBlur = ( event: FocusEvent< HTMLSelectElement > ) => {
onBlur( event );
setIsFocused( false );
};
- const handleOnFocus = ( event ) => {
+ const handleOnFocus = ( event: FocusEvent< HTMLSelectElement > ) => {
onFocus( event );
setIsFocused( true );
};
- const handleOnChange = ( event ) => {
+ const handleOnChange = ( event: ChangeEvent< HTMLSelectElement > ) => {
if ( multiple ) {
const selectedOptions = [ ...event.target.options ].filter(
( { selected } ) => selected
@@ -79,7 +107,7 @@ function SelectControl(
/* eslint-disable jsx-a11y/no-onchange */
return (
-
+
-
-
+ suffix || (
+
+
+
+ )
}
+ prefix={ prefix }
labelPosition={ labelPosition }
- { ...props }
>