diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 97fe6332529434..114012fe507d97 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -28,6 +28,7 @@ ### Internal - Update Ariakit to 0.4.41 ([#84539](https://github.com/WordPress/gutenberg/pull/84539)). +- Refactor to TypeScript ([#84611](https://github.com/WordPress/gutenberg/pull/84611)). ## 42.0.0 (2026-10-07) diff --git a/packages/components/src/context/constants.js b/packages/components/src/context/constants.ts similarity index 100% rename from packages/components/src/context/constants.js rename to packages/components/src/context/constants.ts diff --git a/packages/components/src/context/types.ts b/packages/components/src/context/types.ts new file mode 100644 index 00000000000000..c101e4aae08cb7 --- /dev/null +++ b/packages/components/src/context/types.ts @@ -0,0 +1 @@ +export type ConnectedProps< TProps > = TProps & { className: string }; diff --git a/packages/components/src/context/use-context-system.js b/packages/components/src/context/use-context-system.ts similarity index 57% rename from packages/components/src/context/use-context-system.js rename to packages/components/src/context/use-context-system.ts index ae5705c69c9542..a44d640b8d843b 100644 --- a/packages/components/src/context/use-context-system.js +++ b/packages/components/src/context/use-context-system.ts @@ -1,24 +1,23 @@ +import type { ReactNode } from 'react'; import warn from '@wordpress/warning'; import { useComponentsContext } from './context-system-provider'; import { getNamespace, getConnectedNamespace } from './utils'; import { getStyledClassNameFromKey } from './get-styled-class-name-from-key'; import { useCx } from '../utils/hooks/use-cx'; - -/** - * @template TProps - * @typedef {TProps & { className: string }} ConnectedProps - */ +import type { ConnectedProps } from './types'; /** * Custom hook that derives registered props from the Context system. * These derived props are then consolidated with incoming component props. * - * @template {{ className?: string }} P - * @param {P} props Incoming props from the component. - * @param {string} namespace The namespace to register and to derive context props from. - * @return {ConnectedProps

} The connected props. + * @param props Incoming props from the component. + * @param namespace The namespace to register and to derive context props from. + * @return The connected props. */ -export function useContextSystem( props, namespace ) { +export function useContextSystem< P extends { className?: string } >( + props: P, + namespace: string +) { const contextSystemProps = useComponentsContext(); if ( typeof namespace === 'undefined' ) { warn( 'useContextSystem: Please provide a namespace' ); @@ -26,27 +25,21 @@ export function useContextSystem( props, namespace ) { const contextProps = contextSystemProps?.[ namespace ] || {}; - /** @type {ConnectedProps

} */ - // @ts-expect-error The initial object cannot satisfy the generic `P`, whose props are copied in below. const finalComponentProps = { ...getConnectedNamespace(), ...getNamespace( namespace ), - }; + } as ConnectedProps< P >; const { _overrides, ...otherContextProps } = contextProps; - /** @type {Record} */ - const overrideProps = /** @type {Record} */ ( _overrides ); + const overrideProps = _overrides as Record< string, unknown >; const initialMergedProps = Object.entries( otherContextProps ).length ? Object.assign( {}, otherContextProps, props ) : props; - /** - * @type {P & { - * children?: React.ReactNode, - * renderChildren?: (props: P) => React.ReactNode - * }} - */ - const propsWithChildren = initialMergedProps; + const propsWithChildren: P & { + children?: ReactNode; + renderChildren?: ( props: P ) => ReactNode; + } = initialMergedProps; const cx = useCx(); @@ -62,20 +55,20 @@ export function useContextSystem( props, namespace ) { : propsWithChildren.children; for ( const key in initialMergedProps ) { - // @ts-expect-error A string key cannot index the merged props type. - finalComponentProps[ key ] = initialMergedProps[ key ]; + ( finalComponentProps as Record< string, unknown > )[ key ] = + initialMergedProps[ key ]; } for ( const key in overrideProps ) { - // @ts-expect-error A string key cannot index the merged props type. - finalComponentProps[ key ] = overrideProps[ key ]; + ( finalComponentProps as Record< string, unknown > )[ key ] = + overrideProps[ key ]; } // Setting an `undefined` explicitly can cause unintended overwrites // when a `cloneElement()` is involved. if ( rendered !== undefined ) { - // @ts-expect-error `children` does not exist on `ConnectedProps

`. - finalComponentProps.children = rendered; + ( finalComponentProps as Record< string, unknown > ).children = + rendered; } finalComponentProps.className = classes; diff --git a/packages/components/src/context/utils.js b/packages/components/src/context/utils.ts similarity index 61% rename from packages/components/src/context/utils.js rename to packages/components/src/context/utils.ts index dec56166e1c78e..7bd09c372835f4 100644 --- a/packages/components/src/context/utils.js +++ b/packages/components/src/context/utils.ts @@ -9,10 +9,12 @@ import { CONNECTED_NAMESPACE, COMPONENT_NAMESPACE } from './constants'; *

* ``` * - * @param {string} componentName The name for the component. - * @return {Record} A props object with the namespaced HTML attribute. + * @param componentName The name for the component. + * @return A props object with the namespaced HTML attribute. */ -export function getNamespace( componentName ) { +export function getNamespace( + componentName: string +): Record< string, unknown > { return { [ COMPONENT_NAMESPACE ]: componentName }; } @@ -25,8 +27,8 @@ export function getNamespace( componentName ) { *
* ``` * - * @return {Record} A props object with the namespaced HTML attribute. + * @return A props object with the namespaced HTML attribute. */ -export function getConnectedNamespace() { +export function getConnectedNamespace(): Record< string, unknown > { return { [ CONNECTED_NAMESPACE ]: true }; } diff --git a/packages/components/src/lock-unlock.js b/packages/components/src/lock-unlock.ts similarity index 100% rename from packages/components/src/lock-unlock.js rename to packages/components/src/lock-unlock.ts diff --git a/packages/components/src/toolbar/index.js b/packages/components/src/toolbar/index.ts similarity index 100% rename from packages/components/src/toolbar/index.js rename to packages/components/src/toolbar/index.ts diff --git a/packages/components/src/utils/breakpoint-values.js b/packages/components/src/utils/breakpoint-values.ts similarity index 100% rename from packages/components/src/utils/breakpoint-values.js rename to packages/components/src/utils/breakpoint-values.ts diff --git a/packages/components/src/utils/breakpoint.js b/packages/components/src/utils/breakpoint.js deleted file mode 100644 index 8cb496d2fe4ddb..00000000000000 --- a/packages/components/src/utils/breakpoint.js +++ /dev/null @@ -1,8 +0,0 @@ -import breakpoints from './breakpoint-values'; - -/** - * @param {keyof typeof breakpoints} point - * @return {string} Media query declaration. - */ -export const breakpoint = ( point ) => - `@media (min-width: ${ breakpoints[ point ] })`; diff --git a/packages/components/src/utils/breakpoint.ts b/packages/components/src/utils/breakpoint.ts new file mode 100644 index 00000000000000..ff337fce1e207c --- /dev/null +++ b/packages/components/src/utils/breakpoint.ts @@ -0,0 +1,8 @@ +import breakpoints from './breakpoint-values'; + +/** + * @param point + * @return Media query declaration. + */ +export const breakpoint = ( point: keyof typeof breakpoints ) => + `@media (min-width: ${ breakpoints[ point ] })`; diff --git a/packages/components/src/utils/colors-values.js b/packages/components/src/utils/colors-values.ts similarity index 100% rename from packages/components/src/utils/colors-values.js rename to packages/components/src/utils/colors-values.ts diff --git a/packages/components/src/utils/colors.js b/packages/components/src/utils/colors.ts similarity index 68% rename from packages/components/src/utils/colors.js rename to packages/components/src/utils/colors.ts index 2f7b2dc0c74103..514fe1fafca730 100644 --- a/packages/components/src/utils/colors.js +++ b/packages/components/src/utils/colors.ts @@ -2,17 +2,16 @@ import memoize from 'memize'; import { colord, extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; -/** @type {HTMLDivElement} */ -let colorComputationNode; +let colorComputationNode: HTMLDivElement; extend( [ namesPlugin ] ); /** * Generating a CSS compliant rgba() color value. * - * @param {string} hexValue The hex value to convert to rgba(). - * @param {number} alpha The alpha value for opacity. - * @return {string} The converted rgba() color value. + * @param hexValue The hex value to convert to rgba(). + * @param alpha The alpha value for opacity. + * @return The converted rgba() color value. * * @example * rgba( '#000000', 0.5 ) @@ -23,7 +22,7 @@ export function rgba( hexValue = '', alpha = 1 ) { } /** - * @return {HTMLDivElement | undefined} The HTML element for color computation. + * @return The HTML element for color computation. */ function getColorComputationNode() { if ( typeof document === 'undefined' ) { @@ -43,11 +42,11 @@ function getColorComputationNode() { } /** - * @param {string | unknown} value + * @param value * - * @return {boolean} Whether the value is a valid color. + * @return Whether the value is a valid color. */ -function isColor( value ) { +function isColor( value: string | unknown ) { if ( typeof value !== 'string' ) { return false; } @@ -60,11 +59,11 @@ function isColor( value ) { * Retrieves the computed background color. This is useful for getting the * value of a CSS variable color. * - * @param {string | unknown} backgroundColor The background color to compute. + * @param backgroundColor The background color to compute. * - * @return {string} The computed background color. + * @return The computed background color. */ -function _getComputedBackgroundColor( backgroundColor ) { +function _getComputedBackgroundColor( backgroundColor: string | unknown ) { if ( typeof backgroundColor !== 'string' ) { return ''; } @@ -100,11 +99,13 @@ const getComputedBackgroundColor = memoize( _getComputedBackgroundColor ); /** * Get the text shade optimized for readability, based on a background color. * - * @param {string | unknown} backgroundColor The background color. + * @param backgroundColor The background color. * - * @return {string} The optimized text color (black or white). + * @return The optimized text color (black or white). */ -export function getOptimalTextColor( backgroundColor ) { +export function getOptimalTextColor( + backgroundColor: string | unknown +): string { const background = getComputedBackgroundColor( backgroundColor ); return colord( background ).isLight() ? '#000000' : '#ffffff'; @@ -113,11 +114,13 @@ export function getOptimalTextColor( backgroundColor ) { /** * Get the text shade optimized for readability, based on a background color. * - * @param {string | unknown} backgroundColor The background color. + * @param backgroundColor The background color. * - * @return {string} The optimized text shade (dark or light). + * @return The optimized text shade (dark or light). */ -export function getOptimalTextShade( backgroundColor ) { +export function getOptimalTextShade( + backgroundColor: string | unknown +): string { const result = getOptimalTextColor( backgroundColor ); return result === '#000000' ? 'dark' : 'light'; diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.ts similarity index 100% rename from packages/components/src/utils/config-values.js rename to packages/components/src/utils/config-values.ts diff --git a/packages/components/src/utils/font-values.js b/packages/components/src/utils/font-values.ts similarity index 100% rename from packages/components/src/utils/font-values.js rename to packages/components/src/utils/font-values.ts diff --git a/packages/components/src/utils/font.js b/packages/components/src/utils/font.js deleted file mode 100644 index 1f23ce85525ea2..00000000000000 --- a/packages/components/src/utils/font.js +++ /dev/null @@ -1,10 +0,0 @@ -import FONT from './font-values'; - -/** - * - * @param {keyof typeof FONT} value Path of value from `FONT` - * @return {string} Font rule value - */ -export function font( value ) { - return FONT[ value ] ?? ''; -} diff --git a/packages/components/src/utils/font.ts b/packages/components/src/utils/font.ts new file mode 100644 index 00000000000000..cfd3f438c4b50c --- /dev/null +++ b/packages/components/src/utils/font.ts @@ -0,0 +1,10 @@ +import FONT from './font-values'; + +/** + * + * @param value Path of value from `FONT` + * @return Font rule value + */ +export function font( value: keyof typeof FONT ) { + return FONT[ value ] ?? ''; +} diff --git a/packages/components/src/utils/hooks/index.js b/packages/components/src/utils/hooks/index.ts similarity index 100% rename from packages/components/src/utils/hooks/index.js rename to packages/components/src/utils/hooks/index.ts diff --git a/packages/components/src/utils/hooks/types.ts b/packages/components/src/utils/hooks/types.ts new file mode 100644 index 00000000000000..243a44c01e8e18 --- /dev/null +++ b/packages/components/src/utils/hooks/types.ts @@ -0,0 +1,10 @@ +export type ControlledStateOptions< T > = { + /** + * Initial value + */ + initial?: T; + /** + * Fallback value + */ + fallback: T | ''; +}; diff --git a/packages/components/src/utils/hooks/use-controlled-state.js b/packages/components/src/utils/hooks/use-controlled-state.ts similarity index 73% rename from packages/components/src/utils/hooks/use-controlled-state.js rename to packages/components/src/utils/hooks/use-controlled-state.ts index b7b1b1516c61ef..7fdb34f646d1b7 100644 --- a/packages/components/src/utils/hooks/use-controlled-state.js +++ b/packages/components/src/utils/hooks/use-controlled-state.ts @@ -1,15 +1,8 @@ import { useEffect, useState, useCallback } from '@wordpress/element'; import { isValueDefined, getDefinedValue } from '../values'; +import type { ControlledStateOptions } from './types'; -/** - * @template T - * @typedef Options - * @property {T} [initial] Initial value - * @property {T | ""} fallback Fallback value - */ - -/** @type {Readonly<{ initial: undefined, fallback: '' }>} */ -const defaultOptions = { +const defaultOptions: Readonly< { initial: undefined; fallback: '' } > = { initial: undefined, /** * Defaults to empty string, as that is preferred for usage with @@ -34,14 +27,15 @@ const defaultOptions = { * Unlike the basic useState hook, useControlledState's state can * be updated if a new incoming prop value is changed. * - * @template T - * - * @param {T | undefined} currentState The current value. - * @param {Options} [options=defaultOptions] Additional options for the hook. + * @param currentState The current value. + * @param [options=defaultOptions] Additional options for the hook. * - * @return {[T | "", (nextState: T) => void]} The controlled value and the value setter. + * @return The controlled value and the value setter. */ -function useControlledState( currentState, options = defaultOptions ) { +function useControlledState< T >( + currentState: T | undefined, + options: ControlledStateOptions< T > = defaultOptions +): [ T | '', ( nextState: T ) => void ] { const { initial, fallback } = { ...defaultOptions, ...options }; const [ internalState, setInternalState ] = useState( currentState ); @@ -61,9 +55,8 @@ function useControlledState( currentState, options = defaultOptions ) { fallback ); - /** @type {(nextState: T) => void} */ const setState = useCallback( - ( nextState ) => { + ( nextState: T ) => { if ( ! hasCurrentState ) { setInternalState( nextState ); } diff --git a/packages/components/src/utils/hooks/use-update-effect.js b/packages/components/src/utils/hooks/use-update-effect.ts similarity index 81% rename from packages/components/src/utils/hooks/use-update-effect.js rename to packages/components/src/utils/hooks/use-update-effect.ts index c0f3b73b14a956..26da4fabab4763 100644 --- a/packages/components/src/utils/hooks/use-update-effect.js +++ b/packages/components/src/utils/hooks/use-update-effect.ts @@ -1,3 +1,4 @@ +import type { DependencyList, EffectCallback } from 'react'; import { useRef, useEffect } from '@wordpress/element'; /** @@ -5,10 +6,10 @@ import { useRef, useEffect } from '@wordpress/element'; * Source: * https://github.com/ariakit/ariakit/blob/main/packages/ariakit-react-core/src/utils/hooks.ts * - * @param {React.EffectCallback} effect - * @param {React.DependencyList} deps + * @param effect + * @param deps */ -function useUpdateEffect( effect, deps ) { +function useUpdateEffect( effect: EffectCallback, deps: DependencyList ) { const mountedRef = useRef( false ); useEffect( () => { if ( mountedRef.current ) { diff --git a/packages/components/src/utils/index.js b/packages/components/src/utils/index.ts similarity index 100% rename from packages/components/src/utils/index.js rename to packages/components/src/utils/index.ts diff --git a/packages/components/src/utils/math.js b/packages/components/src/utils/math.js deleted file mode 100644 index 1c7321c85a3d1f..00000000000000 --- a/packages/components/src/utils/math.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Parses and retrieves a number value. - * - * @param {unknown} value The incoming value. - * - * @return {number} The parsed number value. - */ -export function getNumber( value ) { - const number = Number( value ); - - return isNaN( number ) ? 0 : number; -} - -/** - * Safely adds 2 values. - * - * @param {Array} args Values to add together. - * - * @return {number} The sum of values. - */ -export function add( ...args ) { - return args.reduce( - /** @type {(sum:number, arg: number|string) => number} */ - ( sum, arg ) => sum + getNumber( arg ), - 0 - ); -} - -/** - * Safely subtracts 2 values. - * - * @param {Array} args Values to subtract together. - * - * @return {number} The difference of the values. - */ -export function subtract( ...args ) { - return args.reduce( - /** @type {(diff:number, arg: number|string, index:number) => number} */ - ( diff, arg, index ) => { - const value = getNumber( arg ); - return index === 0 ? value : diff - value; - }, - 0 - ); -} - -/** - * Determines the decimal position of a number value. - * - * @param {number} value The number to evaluate. - * - * @return {number} The number of decimal places. - */ -function getPrecision( value ) { - const split = ( value + '' ).split( '.' ); - return split[ 1 ] !== undefined ? split[ 1 ].length : 0; -} - -/** - * Clamps a value based on a min/max range. - * - * @param {number|string} value The value. - * @param {number} min The minimum range. - * @param {number} max The maximum range. - * - * @return {number} The clamped value. - */ -export function clamp( value, min, max ) { - const baseValue = getNumber( value ); - return Math.max( min, Math.min( baseValue, max ) ); -} - -/** - * Rounds a value to the nearest step offset by a minimum. - * - * @param {number|string} value The value. - * @param {number} min The minimum range. - * @param {number} step The increment for the value. - * - * @return {number} The value as a valid step. - */ -export function ensureValidStep( value, min, step ) { - const baseValue = getNumber( value ); - const minValue = getNumber( min ); - const stepValue = getNumber( step ); - const precision = Math.max( getPrecision( step ), getPrecision( min ) ); - // If the step is not a factor of the minimum then the step must be - // offset by the minimum. - const tare = minValue % stepValue ? minValue : 0; - const rounded = Math.round( ( baseValue - tare ) / stepValue ) * stepValue; - const fromMin = rounded + tare; - return precision ? getNumber( fromMin.toFixed( precision ) ) : fromMin; -} diff --git a/packages/components/src/utils/math.ts b/packages/components/src/utils/math.ts new file mode 100644 index 00000000000000..e93fc9d8f4fb68 --- /dev/null +++ b/packages/components/src/utils/math.ts @@ -0,0 +1,89 @@ +/** + * Parses and retrieves a number value. + * + * @param value The incoming value. + * + * @return The parsed number value. + */ +export function getNumber( value: unknown ) { + const number = Number( value ); + + return isNaN( number ) ? 0 : number; +} + +/** + * Safely adds 2 values. + * + * @param args Values to add together. + * + * @return The sum of values. + */ +export function add( ...args: Array< number | string > ) { + return args.reduce< number >( ( sum, arg ) => sum + getNumber( arg ), 0 ); +} + +/** + * Safely subtracts 2 values. + * + * @param args Values to subtract together. + * + * @return The difference of the values. + */ +export function subtract( ...args: Array< number | string > ) { + return args.reduce< number >( ( diff, arg, index ) => { + const value = getNumber( arg ); + return index === 0 ? value : diff - value; + }, 0 ); +} + +/** + * Determines the decimal position of a number value. + * + * @param value The number to evaluate. + * + * @return The number of decimal places. + */ +function getPrecision( value: number ) { + const split = ( value + '' ).split( '.' ); + return split[ 1 ] !== undefined ? split[ 1 ].length : 0; +} + +/** + * Clamps a value based on a min/max range. + * + * @param value The value. + * @param min The minimum range. + * @param max The maximum range. + * + * @return The clamped value. + */ +export function clamp( value: number | string, min: number, max: number ) { + const baseValue = getNumber( value ); + return Math.max( min, Math.min( baseValue, max ) ); +} + +/** + * Rounds a value to the nearest step offset by a minimum. + * + * @param value The value. + * @param min The minimum range. + * @param step The increment for the value. + * + * @return The value as a valid step. + */ +export function ensureValidStep( + value: number | string, + min: number, + step: number +) { + const baseValue = getNumber( value ); + const minValue = getNumber( min ); + const stepValue = getNumber( step ); + const precision = Math.max( getPrecision( step ), getPrecision( min ) ); + // If the step is not a factor of the minimum then the step must be + // offset by the minimum. + const tare = minValue % stepValue ? minValue : 0; + const rounded = Math.round( ( baseValue - tare ) / stepValue ) * stepValue; + const fromMin = rounded + tare; + return precision ? getNumber( fromMin.toFixed( precision ) ) : fromMin; +} diff --git a/packages/components/src/utils/rtl.js b/packages/components/src/utils/rtl.ts similarity index 59% rename from packages/components/src/utils/rtl.js rename to packages/components/src/utils/rtl.ts index 95c48fa4765f5c..11d59c41083c86 100644 --- a/packages/components/src/utils/rtl.js +++ b/packages/components/src/utils/rtl.ts @@ -1,4 +1,5 @@ -import { css } from '@emotion/react'; +import { css, type CSSObject } from '@emotion/react'; +import type { CSSProperties } from 'react'; import { isRTL } from '@wordpress/i18n'; const LOWER_LEFT_REGEXP = new RegExp( /-left/g ); @@ -9,11 +10,11 @@ const UPPER_RIGHT_REGEXP = new RegExp( /Right/g ); /** * Flips a CSS property from left <-> right. * - * @param {string} key The CSS property name. + * @param key The CSS property name. * - * @return {string} The flipped CSS property name, if applicable. + * @return The flipped CSS property name, if applicable. */ -function getConvertedKey( key ) { +function getConvertedKey( key: string ) { if ( key === 'left' ) { return 'right'; } @@ -44,11 +45,13 @@ function getConvertedKey( key ) { /** * An incredibly basic ltr -> rtl converter for style properties * - * @param {React.CSSProperties} ltrStyles + * @param ltrStyles * - * @return {React.CSSProperties} Converted ltr -> rtl styles + * @return Converted ltr -> rtl styles */ -export const convertLTRToRTL = ( ltrStyles = {} ) => { +export const convertLTRToRTL = ( + ltrStyles: CSSProperties = {} +): CSSProperties => { return Object.fromEntries( Object.entries( ltrStyles ).map( ( [ key, value ] ) => [ getConvertedKey( key ), @@ -60,20 +63,25 @@ export const convertLTRToRTL = ( ltrStyles = {} ) => { /** * A higher-order function that create an incredibly basic ltr -> rtl style converter for CSS objects. * - * @param {React.CSSProperties} ltrStyles Ltr styles. Converts and renders from ltr -> rtl styles, if applicable. - * @param {React.CSSProperties} [rtlStyles] Rtl styles. Renders if provided. + * @param ltrStyles Ltr styles. Converts and renders from ltr -> rtl styles, if applicable. + * @param [rtlStyles] Rtl styles. Renders if provided. * - * @return {() => import('@emotion/react').SerializedStyles} A function to output CSS styles for Emotion's renderer + * @return A function to output CSS styles for Emotion's renderer */ -export function rtl( ltrStyles = {}, rtlStyles ) { +export function rtl( + ltrStyles: CSSProperties = {}, + rtlStyles?: CSSProperties +) { return () => { if ( rtlStyles ) { - // @ts-expect-error `React.CSSProperties` lacks the string index signature Emotion's `CSSObject` requires. - return isRTL() ? css( rtlStyles ) : css( ltrStyles ); + return isRTL() + ? css( rtlStyles as CSSObject ) + : css( ltrStyles as CSSObject ); } - // @ts-expect-error `React.CSSProperties` lacks the string index signature Emotion's `CSSObject` requires. - return isRTL() ? css( convertLTRToRTL( ltrStyles ) ) : css( ltrStyles ); + return isRTL() + ? css( convertLTRToRTL( ltrStyles ) as CSSObject ) + : css( ltrStyles as CSSObject ); }; } diff --git a/packages/components/src/utils/style-mixins.js b/packages/components/src/utils/style-mixins.ts similarity index 100% rename from packages/components/src/utils/style-mixins.js rename to packages/components/src/utils/style-mixins.ts diff --git a/packages/components/src/utils/test/colors.js b/packages/components/src/utils/test/colors.ts similarity index 100% rename from packages/components/src/utils/test/colors.js rename to packages/components/src/utils/test/colors.ts diff --git a/packages/components/src/utils/test/math.js b/packages/components/src/utils/test/math.ts similarity index 87% rename from packages/components/src/utils/test/math.js rename to packages/components/src/utils/test/math.ts index 34d52f2471f9aa..baa77b0ebb430a 100644 --- a/packages/components/src/utils/test/math.js +++ b/packages/components/src/utils/test/math.ts @@ -52,15 +52,36 @@ describe( 'clamp', () => { it( 'should clamp number or string values', () => { expect( clamp( '50', 1, 10 ) ).toBe( 10 ); expect( clamp( '50', -10, 10 ) ).toBe( 10 ); - expect( clamp( -50, -10, '10' ) ).toBe( -10 ); + expect( + clamp( + -50, + -10, + // @ts-expect-error `max` is typed as a number but coerced at runtime. + '10' + ) + ).toBe( -10 ); } ); } ); describe( 'ensureValidStep', () => { it( 'should work with number or string values', () => { expect( ensureValidStep( '49', 0, 10 ) ).toBe( 50 ); - expect( ensureValidStep( 49, '0', 10 ) ).toBe( 50 ); - expect( ensureValidStep( 49, 0, '10' ) ).toBe( 50 ); + expect( + ensureValidStep( + 49, + // @ts-expect-error `min` is typed as a number but coerced at runtime. + '0', + 10 + ) + ).toBe( 50 ); + expect( + ensureValidStep( + 49, + 0, + // @ts-expect-error `step` is typed as a number but coerced at runtime. + '10' + ) + ).toBe( 50 ); } ); it( 'should round to step', () => { diff --git a/packages/components/src/utils/test/rtl.js b/packages/components/src/utils/test/rtl.ts similarity index 94% rename from packages/components/src/utils/test/rtl.js rename to packages/components/src/utils/test/rtl.ts index a472242f9d93ff..698a2248875d85 100644 --- a/packages/components/src/utils/test/rtl.js +++ b/packages/components/src/utils/test/rtl.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from 'vitest'; +import type { CSSProperties } from 'react'; import { convertLTRToRTL } from '../rtl'; describe( 'convertLTRToRTL', () => { it( 'converts (*)Left <-> (*)Right', () => { - const style = { + const style: CSSProperties = { // Left values. borderLeft: '10px solid red', borderLeftColor: 'red', @@ -86,7 +87,8 @@ describe( 'convertLTRToRTL', () => { // Edge cases. 'text-combine-upright': 'none', }; - const nextStyle = convertLTRToRTL( style ); + // Kebab-case keys are not in `CSSProperties`, but pass through at runtime. + const nextStyle = convertLTRToRTL( style ) as Record< string, unknown >; expect( Object.keys( style ).length ).toBe( Object.keys( nextStyle ).length diff --git a/packages/components/src/utils/test/space.js b/packages/components/src/utils/test/space.ts similarity index 100% rename from packages/components/src/utils/test/space.js rename to packages/components/src/utils/test/space.ts diff --git a/packages/components/src/utils/test/strings.js b/packages/components/src/utils/test/strings.ts similarity index 100% rename from packages/components/src/utils/test/strings.js rename to packages/components/src/utils/test/strings.ts diff --git a/packages/components/src/utils/test/unit-values.js b/packages/components/src/utils/test/unit-values.ts similarity index 100% rename from packages/components/src/utils/test/unit-values.js rename to packages/components/src/utils/test/unit-values.ts diff --git a/packages/components/src/utils/values.js b/packages/components/src/utils/values.js deleted file mode 100644 index c55a3ce973e5c0..00000000000000 --- a/packages/components/src/utils/values.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Determines if a value is null or undefined. - * - * @template T - * - * @param {T} value The value to check. - * @return {value is Exclude} Whether value is not null or undefined. - */ -export function isValueDefined( value ) { - return value !== undefined && value !== null; -} - -/** - * Determines if a value is empty, null, or undefined. - * - * @param {string | number | null | undefined} value The value to check. - * @return {value is ("" | null | undefined)} Whether value is empty. - */ -export function isValueEmpty( value ) { - const isEmptyString = value === ''; - - return ! isValueDefined( value ) || isEmptyString; -} - -/** - * Get the first defined/non-null value from an array. - * - * @template T - * - * @param {Array} values Values to derive from. - * @param {T} fallbackValue Fallback value if there are no defined values. - * @return {T} A defined value or the fallback value. - */ -export function getDefinedValue( values = [], fallbackValue ) { - return values.find( isValueDefined ) ?? fallbackValue; -} - -/** - * Converts a string to a number. - * - * @param {string} value - * @return {number} String as a number. - */ -export const stringToNumber = ( value ) => { - return parseFloat( value ); -}; - -/** - * Regardless of the input being a string or a number, returns a number. - * - * Returns `undefined` in case the string is `undefined` or not a valid numeric value. - * - * @param {string | number} value - * @return {number} The parsed number. - */ -export const ensureNumber = ( value ) => { - return typeof value === 'string' ? stringToNumber( value ) : value; -}; diff --git a/packages/components/src/utils/values.ts b/packages/components/src/utils/values.ts new file mode 100644 index 00000000000000..5010173a46c07b --- /dev/null +++ b/packages/components/src/utils/values.ts @@ -0,0 +1,61 @@ +/** + * Determines if a value is null or undefined. + * + * @param value The value to check. + * @return Whether value is not null or undefined. + */ +export function isValueDefined< T >( + value: T +): value is Exclude< T, null | undefined > { + return value !== undefined && value !== null; +} + +/** + * Determines if a value is empty, null, or undefined. + * + * @param value The value to check. + * @return Whether value is empty. + */ +export function isValueEmpty( + value: string | number | null | undefined +): value is '' | null | undefined { + const isEmptyString = value === ''; + + return ! isValueDefined( value ) || isEmptyString; +} + +/** + * Get the first defined/non-null value from an array. + * + * @param values Values to derive from. + * @param fallbackValue Fallback value if there are no defined values. + * @return A defined value or the fallback value. + */ +export function getDefinedValue< T >( + values: Array< T | null | undefined > = [], + fallbackValue: T +) { + return values.find( isValueDefined ) ?? fallbackValue; +} + +/** + * Converts a string to a number. + * + * @param value + * @return String as a number. + */ +export const stringToNumber = ( value: string ) => { + return parseFloat( value ); +}; + +/** + * Regardless of the input being a string or a number, returns a number. + * + * Returns `undefined` in case the string is `undefined` or not a valid numeric value. + * + * @param value + * @return The parsed number. + */ +export const ensureNumber = ( value: string | number ) => { + return typeof value === 'string' ? stringToNumber( value ) : value; +}; diff --git a/tools/eslint/config.mjs b/tools/eslint/config.mjs index 9613142b56b311..5eaaca97e12b3c 100644 --- a/tools/eslint/config.mjs +++ b/tools/eslint/config.mjs @@ -851,7 +851,7 @@ export default dedupePlugins( [ // Override: Components src — restrict admin theme and components color vars. { files: [ 'packages/components/src/**' ], - ignores: [ 'packages/components/src/utils/colors-values.js' ], + ignores: [ 'packages/components/src/utils/colors-values.ts' ], rules: { 'no-restricted-syntax': [ 'error', @@ -860,14 +860,14 @@ export default dedupePlugins( [ selector: ':matches(Literal[value=/--wp-admin-theme-/],TemplateElement[value.cooked=/--wp-admin-theme-/])', message: - '--wp-admin-theme-* variables do not support component theming. Use variables from the COLORS object in packages/components/src/utils/colors-values.js instead.', + '--wp-admin-theme-* variables do not support component theming. Use variables from the COLORS object in packages/components/src/utils/colors-values.ts instead.', }, { selector: // Allow overriding definitions, but not access with var() ':matches(Literal[value=/var\\(\\s*--wp-components-color-/],TemplateElement[value.cooked=/var\\(\\s*--wp-components-color-/])', message: - 'To ensure proper fallbacks, --wp-components-color-* variables should not be used directly. Use variables from the COLORS object in packages/components/src/utils/colors-values.js instead.', + 'To ensure proper fallbacks, --wp-components-color-* variables should not be used directly. Use variables from the COLORS object in packages/components/src/utils/colors-values.ts instead.', }, ], }, diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json index d482a47121e065..ca5c424a05508f 100644 --- a/tools/eslint/suppressions.json +++ b/tools/eslint/suppressions.json @@ -4859,7 +4859,7 @@ "count": 4 } }, - "packages/components/src/utils/rtl.js": { + "packages/components/src/utils/rtl.ts": { "no-restricted-imports": { "count": 1 }