diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index fdc4f65cf62c7d..2451b17d8708dd 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -60,6 +60,7 @@ - Convert `LinkControl` and `URLInput` to TypeScript ([#84844](https://github.com/WordPress/gutenberg/pull/84844)). - Convert `Warning`, `BlockEdit` and `PrivateBlockContext`, the `BlockControls` test and the store performance test to TypeScript ([#84846](https://github.com/WordPress/gutenberg/pull/84846)). - Convert the layouts, `LayoutStyle`, the `align` hook, `BlockAlignmentControl`, `BlockVerticalAlignmentControl` and `JustifyContentControl` to TypeScript ([#84847](https://github.com/WordPress/gutenberg/pull/84847)). +- Convert the typography controls, the font size utilities and `withFontSizes` to TypeScript ([#84848](https://github.com/WordPress/gutenberg/pull/84848)). ## 18.1.0 (2026-10-07) diff --git a/packages/block-editor/src/components/font-appearance-control/index.jsx b/packages/block-editor/src/components/font-appearance-control/index.tsx similarity index 82% rename from packages/block-editor/src/components/font-appearance-control/index.jsx rename to packages/block-editor/src/components/font-appearance-control/index.tsx index 68061f66285f2d..b0830411db80ed 100644 --- a/packages/block-editor/src/components/font-appearance-control/index.jsx +++ b/packages/block-editor/src/components/font-appearance-control/index.tsx @@ -1,16 +1,26 @@ import { Field, Select } from '@wordpress/ui'; import { __ } from '@wordpress/i18n'; import { getFontStylesAndWeights } from '../../utils/get-font-styles-and-weights'; +import type { FontAppearanceControlProps, FontAppearanceValue } from './types'; + +interface FontAppearanceOption { + key: string | undefined; + name: string | undefined; + style: FontAppearanceValue; +} /** * Adjusts font appearance field label in case either font styles or weights * are disabled. * - * @param {boolean} hasFontStyles Whether font styles are enabled and present. - * @param {boolean} hasFontWeights Whether font weights are enabled and present. - * @return {string} A label representing what font appearance is being edited. + * @param hasFontStyles Whether font styles are enabled and present. + * @param hasFontWeights Whether font weights are enabled and present. + * @return A label representing what font appearance is being edited. */ -const getFontAppearanceLabel = ( hasFontStyles, hasFontWeights ) => { +const getFontAppearanceLabel = ( + hasFontStyles: boolean, + hasFontWeights: boolean +): string => { if ( ! hasFontStyles ) { return __( 'Font weight' ); } @@ -25,11 +35,13 @@ const getFontAppearanceLabel = ( hasFontStyles, hasFontWeights ) => { /** * Control to display font style and weight options of the active font. * - * @param {Object} props Component props. + * @param props Component props. * - * @return {Element} Font appearance control. + * @return Font appearance control. */ -export default function FontAppearanceControl( props ) { +export default function FontAppearanceControl( + props: FontAppearanceControlProps +) { const { /** * Start opting into the larger default height that will become the default size in a future version. @@ -67,7 +79,7 @@ export default function FontAppearanceControl( props ) { // Generates select options for combined font styles and weights. const combineOptions = () => { - const combinedOptions = [ defaultOption ]; + const combinedOptions: FontAppearanceOption[] = [ defaultOption ]; if ( combinedStyleAndWeightOptions ) { combinedOptions.push( ...combinedStyleAndWeightOptions ); } @@ -76,7 +88,7 @@ export default function FontAppearanceControl( props ) { // Generates select options for font styles only. const styleOptions = () => { - const combinedOptions = [ defaultOption ]; + const combinedOptions: FontAppearanceOption[] = [ defaultOption ]; fontStyles.forEach( ( { name, value } ) => { combinedOptions.push( { key: value, @@ -89,7 +101,7 @@ export default function FontAppearanceControl( props ) { // Generates select options for font weights only. const weightOptions = () => { - const combinedOptions = [ defaultOption ]; + const combinedOptions: FontAppearanceOption[] = [ defaultOption ]; fontWeights.forEach( ( { name, value } ) => { combinedOptions.push( { key: value, diff --git a/packages/block-editor/src/components/font-appearance-control/test/index.jsdom.test.js b/packages/block-editor/src/components/font-appearance-control/test/index.jsdom.test.ts similarity index 100% rename from packages/block-editor/src/components/font-appearance-control/test/index.jsdom.test.js rename to packages/block-editor/src/components/font-appearance-control/test/index.jsdom.test.ts diff --git a/packages/block-editor/src/components/font-appearance-control/types.ts b/packages/block-editor/src/components/font-appearance-control/types.ts new file mode 100644 index 00000000000000..84500df63e25e6 --- /dev/null +++ b/packages/block-editor/src/components/font-appearance-control/types.ts @@ -0,0 +1,62 @@ +import type { ComponentProps } from 'react'; +import type { Select } from '@wordpress/ui'; +import type { FontFamilyFace } from '../../utils/types'; + +/** + * A font style and weight. + */ +export interface FontAppearanceValue { + fontStyle?: string; + fontWeight?: string; +} + +/** + * Props of `FontAppearanceControl`. The props forwarded to the select's + * trigger, such as event handlers, are accepted too. + */ +export interface FontAppearanceControlProps extends Omit< + ComponentProps< typeof Select.Trigger >, + 'onChange' | 'value' +> { + /** + * Start opting into the larger default height that will become the + * default size in a future version. + * + * @deprecated Default behavior since WordPress 7.1. Prop can be safely removed. + */ + __next40pxDefaultSize?: boolean; + /** + * Called with the selected font style and weight. + */ + onChange: ( value: FontAppearanceValue ) => void; + /** + * Whether font styles are enabled and present. + */ + hasFontStyles?: boolean; + /** + * Whether font weights are enabled and present. + */ + hasFontWeights?: boolean; + /** + * The font faces of the active font family. + */ + fontFamilyFaces?: FontFamilyFace[]; + /** + * The current font style and weight. + */ + value: FontAppearanceValue; + hideLabelFromVision?: boolean; + disabled?: boolean; + /* + * Props of the previous implementation, accepted and ignored so they + * don't reach the trigger. + */ + className?: string; + label?: string; + describedBy?: string; + options?: unknown; + showSelectedHint?: boolean; + __experimentalShowSelectedHint?: boolean; + __nextUnconstrainedWidth?: boolean; + __nextHasNoMarginBottom?: boolean; +} diff --git a/packages/block-editor/src/components/font-family/index.jsx b/packages/block-editor/src/components/font-family/index.tsx similarity index 75% rename from packages/block-editor/src/components/font-family/index.jsx rename to packages/block-editor/src/components/font-family/index.tsx index c36795b992b8ae..d8ad43c53efd34 100644 --- a/packages/block-editor/src/components/font-family/index.jsx +++ b/packages/block-editor/src/components/font-family/index.tsx @@ -1,7 +1,9 @@ import clsx from 'clsx'; import { CustomSelectControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import type { FontFamilyPreset } from '@wordpress/global-styles-engine'; import { useSettings } from '../use-settings'; +import type { FontFamilyControlProps } from './types'; export default function FontFamilyControl( { /** @@ -15,10 +17,11 @@ export default function FontFamilyControl( { fontFamilies, className, ...props -} ) { +}: FontFamilyControlProps ) { const [ blockLevelFontFamilies ] = useSettings( 'typography.fontFamilies' ); if ( ! fontFamilies ) { - fontFamilies = blockLevelFontFamilies; + // The setting lists the font family presets. + fontFamilies = blockLevelFontFamilies as FontFamilyPreset[] | undefined; } if ( ! fontFamilies || fontFamilies.length === 0 ) { @@ -42,7 +45,8 @@ export default function FontFamilyControl( { return ( onChange( selectedItem.key ) } options={ options } className={ clsx( 'block-editor-font-family-control', className ) } diff --git a/packages/block-editor/src/components/font-family/stories/index.story.jsx b/packages/block-editor/src/components/font-family/stories/index.story.tsx similarity index 70% rename from packages/block-editor/src/components/font-family/stories/index.story.jsx rename to packages/block-editor/src/components/font-family/stories/index.story.tsx index 5547abded8bfe8..1f70942b5bbfc4 100644 --- a/packages/block-editor/src/components/font-family/stories/index.story.jsx +++ b/packages/block-editor/src/components/font-family/stories/index.story.tsx @@ -1,15 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; import { useState } from '@wordpress/element'; import FontFamilyControl from '..'; +import type { FontFamilyControlProps } from '../types'; export default { component: FontFamilyControl, id: 'blockeditor-fontfamilycontrol', title: 'Editor/Block Editor/FontFamilyControl', -}; +} satisfies Meta< typeof FontFamilyControl >; + +type Story = StoryObj< typeof FontFamilyControl >; -export const Default = { - render: function Template( props ) { - const [ value, setValue ] = useState(); +export const Default: Story = { + render: function Template( props: Partial< FontFamilyControlProps > ) { + const [ value, setValue ] = useState< string >(); return ( ; + +/** + * A font family the control offers, labelled by its name or its CSS value. + */ +export type FontFamilyOption = Pick< FontFamilyPreset, 'fontFamily' > & + Partial< FontFamilyPreset >; + +/** + * Props of `FontFamilyControl`. + */ +export interface FontFamilyControlProps extends Partial< + Omit< CustomSelectControlProps, 'value' | 'onChange' | 'options' > +> { + /** + * Start opting into the larger default height that will become the + * default size in a future version. + * + * @deprecated Default behavior since WordPress 7.1. Prop can be safely removed. + */ + __next40pxDefaultSize?: boolean; + /** + * The selected font family. + */ + value?: string; + /** + * Called with the selected font family, or `''` for the default. + */ + onChange: ( value: string ) => void; + /** + * The font families to choose from, in place of the block's settings. + */ + fontFamilies?: FontFamilyOption[]; + className?: string; +} diff --git a/packages/block-editor/src/components/font-sizes/fluid-utils.js b/packages/block-editor/src/components/font-sizes/fluid-utils.ts similarity index 86% rename from packages/block-editor/src/components/font-sizes/fluid-utils.js rename to packages/block-editor/src/components/font-sizes/fluid-utils.ts index 595011b68bafb6..486cf5363e131e 100644 --- a/packages/block-editor/src/components/font-sizes/fluid-utils.js +++ b/packages/block-editor/src/components/font-sizes/fluid-utils.ts @@ -12,6 +12,12 @@ const DEFAULT_MINIMUM_FONT_SIZE_FACTOR_MIN = 0.25; const DEFAULT_MINIMUM_FONT_SIZE_FACTOR_MAX = 0.75; const DEFAULT_MINIMUM_FONT_SIZE_LIMIT = '14px'; +import type { + FluidTypographyArgs, + TypographyValueAndUnit, + TypographyValueAndUnitOptions, +} from './types'; + /** * Computes a fluid font-size value that uses clamp(). A minimum and maximum * font size OR a single font size can be specified. @@ -50,7 +56,7 @@ export function getComputedFluidTypographyValue( { maximumViewportWidth = DEFAULT_MAXIMUM_VIEWPORT_WIDTH, scaleFactor = DEFAULT_SCALE_FACTOR, minimumFontSizeLimit, -} ) { +}: FluidTypographyArgs ): string | null { // Validate incoming settings and set defaults. minimumFontSizeLimit = !! getTypographyValueAndUnit( minimumFontSizeLimit ) ? minimumFontSizeLimit @@ -131,7 +137,9 @@ export function getComputedFluidTypographyValue( { // Only use calculated min font size if it's > $minimum_font_size_limit value. if ( !! minimumFontSizeLimitParsed?.value && - calculatedMinimumFontSize < minimumFontSizeLimitParsed?.value + // A finite size rounds to a number. + ( calculatedMinimumFontSize as number ) < + minimumFontSizeLimitParsed?.value ) { minimumFontSize = `${ minimumFontSizeLimitParsed.value }${ minimumFontSizeLimitParsed.unit }`; } else { @@ -196,13 +204,16 @@ export function getComputedFluidTypographyValue( { ); const viewportWidthOffset = - roundToPrecision( minViewportWidthOffsetValue, 3 ) + fontSizeUnit; + // A finite width rounds to a number. + roundToPrecision( minViewportWidthOffsetValue as number, 3 ) + + fontSizeUnit; const linearFactor = 100 * ( ( maximumFontSizeParsed.value - minimumFontSizeParsed.value ) / linearDenominator ); const linearFactorScaled = roundToPrecision( - ( linearFactor || 1 ) * scaleFactor, + // A `null` factor multiplies as `0`, as before. + ( linearFactor || 1 ) * ( scaleFactor as number ), 3 ); const fluidTargetFontSize = `${ minimumFontSizeRem.value }${ minimumFontSizeRem.unit } + ((1vw - ${ viewportWidthOffset }) * ${ linearFactorScaled })`; @@ -214,18 +225,22 @@ export function getComputedFluidTypographyValue( { * Internal method that checks a string for a unit and value and returns an array consisting of `'value'` and `'unit'`, e.g., [ '42', 'rem' ]. * A raw font size of `value + unit` is expected. If the value is an integer, it will convert to `value + 'px'`. * - * @param {string|number} rawValue Raw size value from theme.json. - * @param {Object|undefined} options Calculation options. + * @param rawValue Raw size value from theme.json. + * @param options Calculation options. * - * @return {{ unit: string, value: number }|null} An object consisting of `'value'` and `'unit'` properties. + * @return An object consisting of `'value'` and `'unit'` properties. */ -export function getTypographyValueAndUnit( rawValue, options = {} ) { +export function getTypographyValueAndUnit( + rawValue: unknown, + options: TypographyValueAndUnitOptions = {} +): TypographyValueAndUnit | null { if ( typeof rawValue !== 'string' && typeof rawValue !== 'number' ) { return null; } // Converts numeric values to pixel values by default. - if ( isFinite( rawValue ) ) { + // `isFinite` coerces a numeric string. + if ( isFinite( rawValue as number ) ) { rawValue = `${ rawValue }px`; } @@ -242,7 +257,8 @@ export function getTypographyValueAndUnit( rawValue, options = {} ) { `^(\\d*\\.?\\d+)(${ acceptableUnitsGroup }){1,1}$` ); - const matches = rawValue.match( regexUnits ); + // A number was turned into a string above. + const matches = ( rawValue as string ).match( regexUnits ); // We need a number value and a unit. if ( ! matches || matches.length < 3 ) { @@ -276,7 +292,8 @@ export function getTypographyValueAndUnit( rawValue, options = {} ) { } return { - value: roundToPrecision( returnValue, 3 ), + // A parsed number is finite, so it rounds to a number. + value: roundToPrecision( returnValue, 3 ) as number, unit, }; } @@ -285,14 +302,20 @@ export function getTypographyValueAndUnit( rawValue, options = {} ) { * Returns a value rounded to defined precision. * Returns `undefined` if the value is not a valid finite number. * - * @param {number} value Raw value. - * @param {number} digits The number of digits to appear after the decimal point + * @param value Raw value. + * @param digits The number of digits to appear after the decimal point * - * @return {number|undefined} Value rounded to standard precision. + * @return Value rounded to standard precision. */ -export function roundToPrecision( value, digits = 3 ) { +export function roundToPrecision( + value: number, + digits = 3 +): number | undefined { const base = Math.pow( 10, digits ); return Number.isFinite( value ) - ? parseFloat( Math.round( value * base ) / base ) + ? // `parseFloat` reads the rounded number as its string form. + parseFloat( + ( Math.round( value * base ) / base ) as unknown as string + ) : undefined; } diff --git a/packages/block-editor/src/components/font-sizes/font-size-picker.jsx b/packages/block-editor/src/components/font-sizes/font-size-picker.tsx similarity index 69% rename from packages/block-editor/src/components/font-sizes/font-size-picker.jsx rename to packages/block-editor/src/components/font-sizes/font-size-picker.tsx index c6bc4bf81e11ac..3f3566733b9195 100644 --- a/packages/block-editor/src/components/font-sizes/font-size-picker.jsx +++ b/packages/block-editor/src/components/font-sizes/font-size-picker.tsx @@ -1,7 +1,8 @@ import { FontSizePicker as BaseFontSizePicker } from '@wordpress/components'; import { useSettings } from '../use-settings'; +import type { FontSizePickerProps } from './types'; -function FontSizePicker( props ) { +function FontSizePicker( props: FontSizePickerProps ) { const [ fontSizes, customFontSize ] = useSettings( 'typography.fontSizes', 'typography.customFontSize' @@ -10,7 +11,8 @@ function FontSizePicker( props ) { return ( ); diff --git a/packages/block-editor/src/components/font-sizes/index.js b/packages/block-editor/src/components/font-sizes/index.ts similarity index 100% rename from packages/block-editor/src/components/font-sizes/index.js rename to packages/block-editor/src/components/font-sizes/index.ts diff --git a/packages/block-editor/src/components/font-sizes/test/fluid-utils.js b/packages/block-editor/src/components/font-sizes/test/fluid-utils.ts similarity index 98% rename from packages/block-editor/src/components/font-sizes/test/fluid-utils.js rename to packages/block-editor/src/components/font-sizes/test/fluid-utils.ts index 9301a0efcedcc2..34b86f2237b3f1 100644 --- a/packages/block-editor/src/components/font-sizes/test/fluid-utils.js +++ b/packages/block-editor/src/components/font-sizes/test/fluid-utils.ts @@ -64,6 +64,7 @@ describe( 'getComputedFluidTypographyValue()', () => { it( 'should return a fluid font size when given a scale factor', () => { const fluidTypographyValues = getComputedFluidTypographyValue( { fontSize: '30px', + // @ts-expect-error A numeric string multiplies like the number. scaleFactor: '2', } ); expect( fluidTypographyValues ).toBe( diff --git a/packages/block-editor/src/components/font-sizes/test/utils.js b/packages/block-editor/src/components/font-sizes/test/utils.ts similarity index 100% rename from packages/block-editor/src/components/font-sizes/test/utils.js rename to packages/block-editor/src/components/font-sizes/test/utils.ts diff --git a/packages/block-editor/src/components/font-sizes/test/with-font-sizes.tsx b/packages/block-editor/src/components/font-sizes/test/with-font-sizes.tsx new file mode 100644 index 00000000000000..cfaca86f15799c --- /dev/null +++ b/packages/block-editor/src/components/font-sizes/test/with-font-sizes.tsx @@ -0,0 +1,94 @@ +import type { ComponentType } from 'react'; +import { Component } from '@wordpress/element'; +import { describe, expect, it } from 'vitest'; +import withFontSizes from '../with-font-sizes'; +import type { FontSizeObject } from '../types'; + +/* + * These checks run at type-check time: each `@ts-expect-error` fails the + * type check if the line below it stops being a type error. + */ +describe( 'withFontSizes types', () => { + it( 'infers the props of the wrapped component', () => { + const Labeled: ComponentType< { + fontSize: FontSizeObject; + label: string; + } > = () => null; + const WithFontSize = withFontSizes( 'fontSize' )( Labeled ); + + const withLabel = ( + {} } + /> + ); + const withoutLabel = ( + // @ts-expect-error The own props stay required. + {} } /> + ); + + expect( [ withLabel, withoutLabel ] ).toHaveLength( 2 ); + } ); + + it( 'infers the props of a wrapped class component', () => { + class Labeled extends Component< { + fontSize: FontSizeObject; + label: string; + } > { + render() { + return null; + } + } + const WithFontSize = withFontSizes( 'fontSize' )( Labeled ); + + const withLabel = ( + {} } + /> + ); + const withoutLabel = ( + // @ts-expect-error The own props stay required. + {} } /> + ); + + expect( [ withLabel, withoutLabel ] ).toHaveLength( 2 ); + } ); + + it( 'accepts injected values that fit the declared props', () => { + const Labeled: ComponentType< { + setFontSize: ( size: number ) => void; + label: string; + } > = () => null; + const WithFontSize = withFontSizes( 'fontSize' )( Labeled ); + + expect( + {} } + /> + ).toBeDefined(); + } ); + + it( 'rejects injected values that do not fit the declared props', () => { + const Narrow: ComponentType< { fontSize: { size: number } } > = () => + null; + + // @ts-expect-error The injected size can be unset or a string. + const WithFontSize = withFontSizes( 'fontSize' )( Narrow ); + + expect( WithFontSize ).toBeDefined(); + } ); + + it( 'rejects injected props of another type', () => { + const Mistyped: ComponentType< { setFontSize: number } > = () => null; + + // @ts-expect-error The wrapper passes a function. + const WithFontSizeMistyped = withFontSizes( 'fontSize' )( Mistyped ); + + expect( WithFontSizeMistyped ).toBeDefined(); + } ); +} ); diff --git a/packages/block-editor/src/components/font-sizes/types.ts b/packages/block-editor/src/components/font-sizes/types.ts new file mode 100644 index 00000000000000..65994b6be21fbb --- /dev/null +++ b/packages/block-editor/src/components/font-sizes/types.ts @@ -0,0 +1,129 @@ +import type { ComponentProps } from 'react'; +import type { FontSizePicker as BaseFontSizePicker } from '@wordpress/components'; +import type { FontSizePreset } from '@wordpress/global-styles-engine'; + +/** + * A font size, either a preset or just a custom size. + */ +export type FontSizeObject = + | FontSizePreset + | { size: FontSizePreset[ 'size' ] | undefined; slug?: undefined }; + +/** + * Arguments of `getComputedFluidTypographyValue`. + */ +export interface FluidTypographyArgs { + /** + * Minimum viewport size from which type will have fluidity. Optional if + * fontSize is specified. + */ + minimumViewportWidth?: string | null; + /** + * Maximum size up to which type will have fluidity. Optional if fontSize + * is specified. + */ + maximumViewportWidth?: string | null; + /** + * Size to derive maximumFontSize and minimumFontSize from, if necessary. + * Optional if minimumFontSize and maximumFontSize are specified. + */ + fontSize?: string | number; + /** + * Maximum font size for any clamp() calculation. Optional. + */ + maximumFontSize?: string | null; + /** + * Minimum font size for any clamp() calculation. Optional. + */ + minimumFontSize?: string | null; + /** + * A scale factor to determine how fast a font scales within boundaries. + * Optional. + */ + scaleFactor?: number | null; + /** + * The smallest a calculated font size may be. Optional. + */ + minimumFontSizeLimit?: string | null; +} + +/** + * Options of `getTypographyValueAndUnit`. + */ +export interface TypographyValueAndUnitOptions { + coerceTo?: string; + rootSizeValue?: number; + acceptableUnits?: string[]; +} + +/** + * A typography value with its unit. + */ +export interface TypographyValueAndUnit { + value: number; + unit: string; +} + +/** + * Props of the block editor's `FontSizePicker`, which reads the font sizes + * from the block settings. + */ +export type FontSizePickerProps = ComponentProps< typeof BaseFontSizePicker >; + +/** + * Props of a component wrapped by `withFontSizes`. + */ +export interface WithFontSizesProps { + attributes: Record< string, unknown >; + setAttributes: ( attributes: Record< string, unknown > ) => void; + fontSizes?: FontSizePreset[]; + [ prop: string ]: unknown; +} + +/** + * The props `withFontSizes` injects for the given font size attribute names: + * the font size object of each name and its `set` setter. + */ +export type InjectedFontSizesProps< Name extends string > = { + [ N in Name ]: FontSizeObject & { class?: string }; +} & { + [ N in Name as `set${ Capitalize< N > }` ]: ( + fontSizeValue?: number | string + ) => void; +}; + +/** + * Rejects a wrapped component that declares an injected prop with a type the + * injected value does not fit, naming the props in the error. + */ +export type IncompatibleInjectedProps< Props, Injected > = [ + { + [ K in keyof Props & keyof Injected ]: Injected[ K ] extends Props[ K ] + ? never + : K; + }[ keyof Props & keyof Injected ], +] extends [ never ] + ? unknown + : { + incompatibleInjectedProps: { + [ + K in keyof Props & keyof Injected + ]: Injected[ K ] extends Props[ K ] ? never : K; + }[ keyof Props & keyof Injected ]; + }; + +/** + * The font size objects `withFontSizes` passes, keyed by attribute name. + */ +export type WithFontSizesState = Record< + string, + FontSizeObject & { class?: string } +>; + +/** + * The font size setters `withFontSizes` passes, such as `setFontSize`. + */ +export type FontSizeSetters = Record< + string, + ( fontSizeValue?: number | string ) => void +>; diff --git a/packages/block-editor/src/components/font-sizes/utils.js b/packages/block-editor/src/components/font-sizes/utils.ts similarity index 77% rename from packages/block-editor/src/components/font-sizes/utils.js rename to packages/block-editor/src/components/font-sizes/utils.ts index f9fd51ac5ec0bc..f9286c09118959 100644 --- a/packages/block-editor/src/components/font-sizes/utils.js +++ b/packages/block-editor/src/components/font-sizes/utils.ts @@ -1,4 +1,6 @@ import { kebabCase } from '@wordpress/kebab-case'; +import type { FontSizePreset } from '@wordpress/global-styles-engine'; +import type { FontSizeObject } from './types'; /** * Returns the font size object based on an array of named font sizes and the namedFontSize and customFontSize values. @@ -12,10 +14,10 @@ import { kebabCase } from '@wordpress/kebab-case'; * Otherwise, an object with just the size value based on customFontSize is returned. */ export const getFontSize = ( - fontSizes, - fontSizeAttribute, - customFontSizeAttribute -) => { + fontSizes: FontSizePreset[] | undefined, + fontSizeAttribute: string | null | undefined, + customFontSizeAttribute: number | string | null | undefined +): FontSizeObject => { if ( fontSizeAttribute ) { const fontSizeObject = fontSizes?.find( ( { slug } ) => slug === fontSizeAttribute @@ -37,7 +39,10 @@ export const getFontSize = ( * * @return {Object} Font size object. */ -export function getFontSizeObjectByValue( fontSizes, value ) { +export function getFontSizeObjectByValue( + fontSizes: FontSizePreset[] | undefined, + value: FontSizePreset[ 'size' ] | undefined +): FontSizeObject { const fontSizeObject = fontSizes?.find( ( { size } ) => size === value ); if ( fontSizeObject ) { return fontSizeObject; @@ -56,7 +61,9 @@ export function getFontSizeObjectByValue( fontSizes, value ) { * @return {string | undefined} String with the class corresponding to the fontSize passed. * The class is generated by appending 'has-' followed by fontSizeSlug in kebabCase and ending with '-font-size'. */ -export function getFontSizeClass( fontSizeSlug ) { +export function getFontSizeClass( + fontSizeSlug: string | number | null | undefined +): string | undefined { if ( ! fontSizeSlug ) { return; } diff --git a/packages/block-editor/src/components/font-sizes/with-font-sizes.jsx b/packages/block-editor/src/components/font-sizes/with-font-sizes.tsx similarity index 64% rename from packages/block-editor/src/components/font-sizes/with-font-sizes.jsx rename to packages/block-editor/src/components/font-sizes/with-font-sizes.tsx index 13e7f6385fef5c..dc5690b0151208 100644 --- a/packages/block-editor/src/components/font-sizes/with-font-sizes.jsx +++ b/packages/block-editor/src/components/font-sizes/with-font-sizes.tsx @@ -1,18 +1,27 @@ import { createHigherOrderComponent, compose } from '@wordpress/compose'; import { Component } from '@wordpress/element'; +import type { ComponentClass, ComponentType, FunctionComponent } from 'react'; +import type { FontSizePreset } from '@wordpress/global-styles-engine'; import { getFontSize, getFontSizeClass } from './utils'; import { useSettings } from '../use-settings'; +import type { + FontSizeSetters, + IncompatibleInjectedProps, + InjectedFontSizesProps, + WithFontSizesProps, + WithFontSizesState, +} from './types'; -const DEFAULT_FONT_SIZES = []; +const DEFAULT_FONT_SIZES: FontSizePreset[] = []; /** * Capitalizes the first letter in a string. * * @param {string} str The string whose first letter the function will capitalize. * - * @return {string} Capitalized string. + * @return Capitalized string. */ -const upperFirst = ( [ firstLetter, ...rest ] ) => +const upperFirst = ( [ firstLetter, ...rest ]: string ): string => firstLetter.toUpperCase() + rest.join( '' ); /** @@ -25,7 +34,7 @@ const upperFirst = ( [ firstLetter, ...rest ] ) => * * @return {Function} Higher-order component. */ -export default ( ...fontSizeNames ) => { +export default < Name extends string >( ...fontSizeNames: Name[] ) => { /* * Computes an object whose key is the font size attribute name as passed in the array, * and the value is the custom font size attribute name. @@ -37,29 +46,42 @@ export default ( ...fontSizeNames ) => { `custom${ upperFirst( fontSizeAttributeName ) }`; return fontSizeAttributeNamesAccumulator; }, - {} + // The reducer fills the empty object. + {} as Record< string, string > ); + // The outer cast types the props each call injects. return createHigherOrderComponent( + // `compose` loses the types of the components it composes. compose( [ createHigherOrderComponent( - ( WrappedComponent ) => - function WithFontSizesInner( props ) { + ( WrappedComponent: ComponentType< WithFontSizesProps > ) => + function WithFontSizesInner( props: WithFontSizesProps ) { const [ fontSizes ] = useSettings( 'typography.fontSizes' ); return ( ); }, 'withFontSizes' ), - ( WrappedComponent ) => { - return class WithFontSizes extends Component { - constructor( props ) { + ( WrappedComponent: ComponentType< WithFontSizesProps > ) => { + return class WithFontSizes extends Component< + WithFontSizesProps, + WithFontSizesState + > { + declare setters: FontSizeSetters; + + constructor( props: WithFontSizesProps ) { super( props ); this.setters = this.createSetters(); @@ -67,7 +89,7 @@ export default ( ...fontSizeNames ) => { this.state = {}; } - createSetters() { + createSetters(): FontSizeSetters { return Object.entries( fontSizeAttributeNames ).reduce( ( settersAccumulator, @@ -86,15 +108,16 @@ export default ( ...fontSizeNames ) => { ); return settersAccumulator; }, - {} + // The reducer fills the empty object. + {} as FontSizeSetters ); } createSetFontSize( - fontSizeAttributeName, - customFontSizeAttributeName + fontSizeAttributeName: string, + customFontSizeAttributeName: string ) { - return ( fontSizeValue ) => { + return ( fontSizeValue?: number | string ) => { const fontSizeObject = this.props.fontSizes?.find( ( { size } ) => size === Number( fontSizeValue ) ); @@ -112,12 +135,13 @@ export default ( ...fontSizeNames ) => { } static getDerivedStateFromProps( - { attributes, fontSizes }, - previousState + { attributes, fontSizes }: WithFontSizesProps, + previousState: WithFontSizesState ) { const didAttributesChange = ( - customFontSizeAttributeName, - fontSizeAttributeName + customFontSizeAttributeName: string, + // `some` passes the index here, as it always has. + fontSizeAttributeName: string | number ) => { if ( previousState[ fontSizeAttributeName ] ) { // If new font size is name compare with the previous slug. @@ -161,14 +185,17 @@ export default ( ...fontSizeNames ) => { customFontSizeAttributeName, ] ) => { - const fontSizeAttributeValue = - attributes[ fontSizeAttributeName ]; + // The font size attribute holds a slug. + const fontSizeAttributeValue = attributes[ + fontSizeAttributeName + ] as string | undefined; const fontSizeObject = getFontSize( fontSizes, fontSizeAttributeValue, + // The custom font size attribute holds a size. attributes[ customFontSizeAttributeName - ] + ] as number | string | undefined ); newStateAccumulator[ fontSizeAttributeName @@ -180,7 +207,8 @@ export default ( ...fontSizeNames ) => { }; return newStateAccumulator; }, - {} + // The reducer fills the empty object. + {} as WithFontSizesState ); return { @@ -203,7 +231,19 @@ export default ( ...fontSizeNames ) => { } }; }, - ] ), + ] ) as ( + Inner: ComponentType< WithFontSizesProps > + ) => ComponentType< WithFontSizesProps >, 'withFontSizes' - ); + ) as < Props >( + WrappedComponent: ( + | FunctionComponent< Props & InjectedFontSizesProps< Name > > + // A class component's `props` type must match, so it takes its own props. + | ComponentClass< Props > + ) & + IncompatibleInjectedProps< Props, InjectedFontSizesProps< Name > > + ) => ComponentType< + Omit< Props, keyof InjectedFontSizesProps< Name > > & + Pick< WithFontSizesProps, 'attributes' | 'setAttributes' > + >; }; diff --git a/packages/block-editor/src/components/letter-spacing-control/index.jsx b/packages/block-editor/src/components/letter-spacing-control/index.tsx similarity index 50% rename from packages/block-editor/src/components/letter-spacing-control/index.jsx rename to packages/block-editor/src/components/letter-spacing-control/index.tsx index 790f45fb80b4c2..0bac22af0e1b60 100644 --- a/packages/block-editor/src/components/letter-spacing-control/index.jsx +++ b/packages/block-editor/src/components/letter-spacing-control/index.tsx @@ -4,27 +4,33 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useSettings } from '../../components/use-settings'; +import type { LetterSpacingControlProps } from './types'; /** * Control for letter-spacing. * - * @param {Object} props Component props. - * @param {boolean} [props.__next40pxDefaultSize] Start opting into the larger default height that will become the default size in a future version. - * @param {string} props.value Currently selected letter-spacing. - * @param {Function} props.onChange Handles change in letter-spacing selection. - * @param {string|number|undefined} props.__unstableInputWidth Input width to pass through to inner UnitControl. Should be a valid CSS value. + * @param props Component props. + * @param props.__next40pxDefaultSize Start opting into the larger default height that will become the default size in a future version. + * @param props.value Currently selected letter-spacing. + * @param props.onChange Handles change in letter-spacing selection. + * @param props.__unstableInputWidth Input width to pass through to inner UnitControl. Should be a valid CSS value. * - * @return {Element} Letter-spacing control. + * @return Letter-spacing control. */ export default function LetterSpacingControl( { value, onChange, __unstableInputWidth = '60px', ...otherProps -} ) { +}: LetterSpacingControlProps ) { const [ availableUnits ] = useSettings( 'spacing.units' ); const units = useCustomUnits( { - availableUnits: availableUnits || [ 'px', 'em', 'rem' ], + // The setting lists unit strings. + availableUnits: ( availableUnits as string[] | undefined ) || [ + 'px', + 'em', + 'rem', + ], defaultValues: { px: 2, em: 0.2, rem: 0.2 }, } ); diff --git a/packages/block-editor/src/components/letter-spacing-control/stories/index.story.jsx b/packages/block-editor/src/components/letter-spacing-control/stories/index.story.tsx similarity index 73% rename from packages/block-editor/src/components/letter-spacing-control/stories/index.story.jsx rename to packages/block-editor/src/components/letter-spacing-control/stories/index.story.tsx index 2b9c5e4e86225c..dd22aef2ef2896 100644 --- a/packages/block-editor/src/components/letter-spacing-control/stories/index.story.jsx +++ b/packages/block-editor/src/components/letter-spacing-control/stories/index.story.tsx @@ -1,7 +1,8 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; import { useState } from '@wordpress/element'; import LetterSpacingControl from '../'; -const meta = { +const meta: Meta< typeof LetterSpacingControl > = { id: 'blockeditor-letterspacingcontrol', title: 'Editor/Block Editor/LetterSpacingControl', component: LetterSpacingControl, @@ -16,6 +17,7 @@ const meta = { }, argTypes: { value: { + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'The current value of the letter spacing setting.', table: { @@ -24,6 +26,7 @@ const meta = { }, onChange: { action: 'onChange', + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'A callback function invoked when the value is changed.', @@ -45,15 +48,18 @@ const meta = { export default meta; -export const Default = { +type Story = StoryObj< typeof meta >; + +export const Default: Story = { render: function Template( { onChange, ...args } ) { - const [ value, setValue ] = useState( '' ); + const [ value, setValue ] = useState< string | undefined >( '' ); return ( { - onChange( newValue ); + // Storybook passes the `onChange` action. + onChange!( newValue ); setValue( newValue ); } } /> diff --git a/packages/block-editor/src/components/letter-spacing-control/types.ts b/packages/block-editor/src/components/letter-spacing-control/types.ts new file mode 100644 index 00000000000000..9d376e508ca049 --- /dev/null +++ b/packages/block-editor/src/components/letter-spacing-control/types.ts @@ -0,0 +1,31 @@ +import type { ComponentProps } from 'react'; +import type { __experimentalUnitControl as UnitControl } from '@wordpress/components'; + +type UnitControlProps = ComponentProps< typeof UnitControl >; + +/** + * Props of `LetterSpacingControl`. + */ +export interface LetterSpacingControlProps extends Omit< + UnitControlProps, + 'value' | 'onChange' | 'label' +> { + /** + * Start opting into the larger default height that will become the + * default size in a future version. + */ + __next40pxDefaultSize?: boolean; + /** + * Currently selected letter-spacing. + */ + value?: string; + /** + * Handles change in letter-spacing selection. + */ + onChange: ( value?: string ) => void; + /** + * Input width to pass through to inner UnitControl. Should be a valid CSS + * value. + */ + __unstableInputWidth?: UnitControlProps[ '__unstableInputWidth' ]; +} diff --git a/packages/block-editor/src/components/line-height-control/index.jsx b/packages/block-editor/src/components/line-height-control/index.tsx similarity index 79% rename from packages/block-editor/src/components/line-height-control/index.jsx rename to packages/block-editor/src/components/line-height-control/index.tsx index e796d45298833f..c1da1689aa4f14 100644 --- a/packages/block-editor/src/components/line-height-control/index.jsx +++ b/packages/block-editor/src/components/line-height-control/index.tsx @@ -7,6 +7,11 @@ import { SPIN_FACTOR, isLineHeightDefined, } from './utils'; +import type { + LineHeightControlOnChange, + LineHeightControlProps, + LineHeightStateReducer, +} from './types'; const LineHeightControl = ( { /** @@ -19,10 +24,13 @@ const LineHeightControl = ( { onChange, __unstableInputWidth = '60px', ...otherProps -} ) => { +}: LineHeightControlProps ) => { const isDefined = isLineHeightDefined( lineHeight ); - const adjustNextValue = ( nextValue, wasTypedOrPasted ) => { + const adjustNextValue = ( + nextValue: string | number | undefined, + wasTypedOrPasted: boolean + ) => { // Set the next value without modification if lineHeight has been defined. if ( isDefined ) { return nextValue; @@ -58,21 +66,26 @@ const LineHeightControl = ( { } }; - const stateReducer = ( state, action ) => { + const stateReducer: LineHeightStateReducer = ( state, action ) => { // Be careful when changing this — cross-browser behavior of the // `inputType` field in `input` events are inconsistent. // For example, Firefox emits an input event with inputType="insertReplacementText" // on spin button clicks, while other browsers do not even emit an input event. const wasTypedOrPasted = [ 'insertText', 'insertFromPaste' ].includes( - action.payload.event.nativeEvent?.inputType + // Change actions carry the input event. + ( action.payload.event!.nativeEvent as InputEvent )?.inputType ); const value = adjustNextValue( state.value, wasTypedOrPasted ); - return { ...state, value }; + // The input takes the numeric spin values as they are. + return { ...state, value: value as string | undefined }; }; const value = isDefined ? lineHeight : RESET_VALUE; - const handleOnChange = ( nextValue, { event } ) => { + const handleOnChange: LineHeightControlOnChange = ( + nextValue, + { event } + ) => { if ( nextValue === '' ) { onChange(); return; @@ -89,7 +102,8 @@ const LineHeightControl = ( { return (
; -const Template = ( props ) => { - const [ value, setValue ] = useState(); +const Template: Story = ( props ) => { + const [ value, setValue ] = useState< string | number >(); return ( ); }; -export const Default = Template.bind( {} ); +type Story = StoryFn< Partial< LineHeightControlProps > >; + +export const Default: Story = Template.bind( {} ); Default.args = { __unstableInputWidth: '100px', }; -export const UnconstrainedWidth = Template.bind( {} ); +export const UnconstrainedWidth: Story = Template.bind( {} ); UnconstrainedWidth.args = { ...Default.args, __unstableInputWidth: '100%', diff --git a/packages/block-editor/src/components/line-height-control/test/index.jsdom.test.jsx b/packages/block-editor/src/components/line-height-control/test/index.jsdom.test.tsx similarity index 97% rename from packages/block-editor/src/components/line-height-control/test/index.jsdom.test.jsx rename to packages/block-editor/src/components/line-height-control/test/index.jsdom.test.tsx index 612803c039bb89..8f12a1b8f7da0f 100644 --- a/packages/block-editor/src/components/line-height-control/test/index.jsdom.test.jsx +++ b/packages/block-editor/src/components/line-height-control/test/index.jsdom.test.tsx @@ -10,7 +10,7 @@ globalThis.wpVitest.mockMatchMedia(); const SPIN = STEP * SPIN_FACTOR; const ControlledLineHeightControl = () => { - const [ value, setValue ] = useState(); + const [ value, setValue ] = useState< string | number >(); return ; }; diff --git a/packages/block-editor/src/components/line-height-control/types.ts b/packages/block-editor/src/components/line-height-control/types.ts new file mode 100644 index 00000000000000..865cd75e6b5a19 --- /dev/null +++ b/packages/block-editor/src/components/line-height-control/types.ts @@ -0,0 +1,46 @@ +import type { ComponentProps } from 'react'; +import type { __experimentalNumberControl as NumberControl } from '@wordpress/components'; + +type NumberControlProps = ComponentProps< typeof NumberControl >; + +/** + * The state reducer `NumberControl` runs on each change. + */ +export type LineHeightStateReducer = NonNullable< + NumberControlProps[ '__unstableStateReducer' ] +>; + +/** + * The change handler `NumberControl` calls. + */ +export type LineHeightControlOnChange = NonNullable< + NumberControlProps[ 'onChange' ] +>; + +/** + * Props of `LineHeightControl`. + */ +export interface LineHeightControlProps extends Omit< + NumberControlProps, + 'value' | 'onChange' +> { + /** + * Start opting into the larger default height that will become the + * default size in a future version. + * + * @deprecated Default behavior since WordPress 7.1. Prop can be safely removed. + */ + __next40pxDefaultSize?: boolean; + /** + * The line height, unset when `undefined`. + */ + value?: string | number; + /** + * Called with the next line height, or nothing when it is cleared. + */ + onChange: ( value?: string | number ) => void; + /** + * The width of the input. + */ + __unstableInputWidth?: NumberControlProps[ '__unstableInputWidth' ]; +} diff --git a/packages/block-editor/src/components/line-height-control/utils.js b/packages/block-editor/src/components/line-height-control/utils.ts similarity index 84% rename from packages/block-editor/src/components/line-height-control/utils.js rename to packages/block-editor/src/components/line-height-control/utils.ts index e9bf66259991e3..2d43d8a0ff78fc 100644 --- a/packages/block-editor/src/components/line-height-control/utils.js +++ b/packages/block-editor/src/components/line-height-control/utils.ts @@ -20,10 +20,10 @@ export const RESET_VALUE = ''; /** * Determines if the lineHeight attribute has been properly defined. * - * @param {any} lineHeight The value to check. + * @param lineHeight The value to check. * - * @return {boolean} Whether the lineHeight attribute is valid. + * @return Whether the lineHeight attribute is valid. */ -export function isLineHeightDefined( lineHeight ) { +export function isLineHeightDefined( lineHeight: unknown ): boolean { return lineHeight !== undefined && lineHeight !== RESET_VALUE; } diff --git a/packages/block-editor/src/components/text-alignment-control/index.jsx b/packages/block-editor/src/components/text-alignment-control/index.tsx similarity index 74% rename from packages/block-editor/src/components/text-alignment-control/index.jsx rename to packages/block-editor/src/components/text-alignment-control/index.tsx index cf0cf43296cc38..91559a26f93093 100644 --- a/packages/block-editor/src/components/text-alignment-control/index.jsx +++ b/packages/block-editor/src/components/text-alignment-control/index.tsx @@ -11,6 +11,7 @@ import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, } from '@wordpress/components'; +import type { TextAlignmentControlProps } from './types'; const TEXT_ALIGNMENT_OPTIONS = [ { @@ -40,20 +41,20 @@ const DEFAULT_OPTIONS = [ 'left', 'center', 'right' ]; /** * Control to facilitate text alignment selections. * - * @param {Object} props Component props. - * @param {string} props.className Class name to add to the control. - * @param {string} props.value Currently selected text alignment. - * @param {Function} props.onChange Handles change in text alignment selection. - * @param {string[]} props.options Array of text alignment options to display. + * @param props Component props. + * @param props.className Class name to add to the control. + * @param props.value Currently selected text alignment. + * @param props.onChange Handles change in text alignment selection. + * @param props.options Array of text alignment options to display. * - * @return {Element} Text alignment control. + * @return Text alignment control. */ export default function TextAlignmentControl( { className, value, onChange, options = DEFAULT_OPTIONS, -} ) { +}: TextAlignmentControlProps ) { const validOptions = useMemo( () => TEXT_ALIGNMENT_OPTIONS.filter( ( option ) => @@ -76,7 +77,12 @@ export default function TextAlignmentControl( { ) } value={ value } onChange={ ( newValue ) => { - onChange( newValue === value ? undefined : newValue ); + // The options' values are strings. + onChange( + newValue === value + ? undefined + : ( newValue as string | undefined ) + ); } } > { validOptions.map( ( option ) => { diff --git a/packages/block-editor/src/components/text-alignment-control/stories/index.story.jsx b/packages/block-editor/src/components/text-alignment-control/stories/index.story.tsx similarity index 78% rename from packages/block-editor/src/components/text-alignment-control/stories/index.story.jsx rename to packages/block-editor/src/components/text-alignment-control/stories/index.story.tsx index 02b8b3526821fb..4a86ce2435d11d 100644 --- a/packages/block-editor/src/components/text-alignment-control/stories/index.story.jsx +++ b/packages/block-editor/src/components/text-alignment-control/stories/index.story.tsx @@ -1,7 +1,8 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; import { useState } from '@wordpress/element'; import TextAlignmentControl from '../'; -const meta = { +const meta: Meta< typeof TextAlignmentControl > = { id: 'blockeditor-textalignmentcontrol', title: 'Editor/Block Editor/TextAlignmentControl', component: TextAlignmentControl, @@ -16,6 +17,7 @@ const meta = { }, argTypes: { value: { + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'Currently selected text alignment value.', table: { @@ -26,6 +28,7 @@ const meta = { }, onChange: { action: 'onChange', + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'Handles change in text alignment selection.', table: { @@ -54,9 +57,11 @@ const meta = { export default meta; -export const Default = { +type Story = StoryObj< typeof meta >; + +export const Default: Story = { render: function Template( { onChange, ...args } ) { - const [ value, setValue ] = useState(); + const [ value, setValue ] = useState< string >(); return ( void; + /** + * Array of text alignment options to display. + */ + options?: string[]; +} diff --git a/packages/block-editor/src/components/text-decoration-control/index.jsx b/packages/block-editor/src/components/text-decoration-control/index.tsx similarity index 70% rename from packages/block-editor/src/components/text-decoration-control/index.jsx rename to packages/block-editor/src/components/text-decoration-control/index.tsx index 4dfa3c7dd18bd0..477243196ea6e4 100644 --- a/packages/block-editor/src/components/text-decoration-control/index.jsx +++ b/packages/block-editor/src/components/text-decoration-control/index.tsx @@ -5,6 +5,7 @@ import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, } from '@wordpress/components'; +import type { TextDecorationControlProps } from './types'; const TEXT_DECORATIONS = [ { @@ -27,18 +28,18 @@ const TEXT_DECORATIONS = [ /** * Control to facilitate text decoration selections. * - * @param {Object} props Component props. - * @param {string} props.value Currently selected text decoration. - * @param {Function} props.onChange Handles change in text decoration selection. - * @param {string} props.className Additional class name to apply. + * @param props Component props. + * @param props.value Currently selected text decoration. + * @param props.onChange Handles change in text decoration selection. + * @param props.className Additional class name to apply. * - * @return {Element} Text decoration control. + * @return Text decoration control. */ export default function TextDecorationControl( { value, onChange, className, -} ) { +}: TextDecorationControlProps ) { return ( { - onChange( newValue === value ? undefined : newValue ); + // The options' values are strings. + onChange( + newValue === value + ? undefined + : ( newValue as string | undefined ) + ); } } > { TEXT_DECORATIONS.map( ( option ) => { diff --git a/packages/block-editor/src/components/text-decoration-control/stories/index.story.jsx b/packages/block-editor/src/components/text-decoration-control/stories/index.story.tsx similarity index 75% rename from packages/block-editor/src/components/text-decoration-control/stories/index.story.jsx rename to packages/block-editor/src/components/text-decoration-control/stories/index.story.tsx index fc4f87d85ce414..28e2a9f23ad215 100644 --- a/packages/block-editor/src/components/text-decoration-control/stories/index.story.jsx +++ b/packages/block-editor/src/components/text-decoration-control/stories/index.story.tsx @@ -1,7 +1,8 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; import { useState } from '@wordpress/element'; import TextDecorationControl from '../'; -const meta = { +const meta: Meta< typeof TextDecorationControl > = { id: 'blockeditor-textdecorationcontrol', title: 'Editor/Block Editor/TextDecorationControl', component: TextDecorationControl, @@ -15,6 +16,7 @@ const meta = { }, argTypes: { value: { + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'Currently selected text decoration.', table: { @@ -25,6 +27,7 @@ const meta = { }, onChange: { action: 'onChange', + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'Handles change in text decoration selection.', table: { @@ -45,9 +48,11 @@ const meta = { export default meta; -export const Default = { +type Story = StoryObj< typeof meta >; + +export const Default: Story = { render: function Template( { onChange, ...args } ) { - const [ value, setValue ] = useState(); + const [ value, setValue ] = useState< string >(); return ( void; +} diff --git a/packages/block-editor/src/components/text-indent-control/index.jsx b/packages/block-editor/src/components/text-indent-control/index.tsx similarity index 72% rename from packages/block-editor/src/components/text-indent-control/index.jsx rename to packages/block-editor/src/components/text-indent-control/index.tsx index 0c67f14fcfa3c4..70134670749385 100644 --- a/packages/block-editor/src/components/text-indent-control/index.jsx +++ b/packages/block-editor/src/components/text-indent-control/index.tsx @@ -11,21 +11,22 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useSettings } from '../../components/use-settings'; +import type { TextIndentControlProps } from './types'; /** * Control for line text indent. * - * @param {Object} props Component props. - * @param {string} props.value Currently selected text indent. - * @param {Function} props.onChange Handles change in text indent selection. - * @param {string|number|undefined} props.__unstableInputWidth Input width to pass through to inner UnitControl. Should be a valid CSS value. - * @param {boolean} props.withSlider Whether to show the slider control. - * @param {boolean} props.hasBottomMargin Whether to add bottom margin below the control. - * @param {string} props.help Help text to display below the control. - * @param {string} props.className Class name to add to the inner UnitControl. - * @param {string} props.placeholder Placeholder for the inner UnitControl. + * @param props Component props. + * @param props.value Currently selected text indent. + * @param props.onChange Handles change in text indent selection. + * @param props.__unstableInputWidth Input width to pass through to inner UnitControl. Should be a valid CSS value. + * @param props.withSlider Whether to show the slider control. + * @param props.hasBottomMargin Whether to add bottom margin below the control. + * @param props.help Help text to display below the control. + * @param props.className Class name to add to the inner UnitControl. + * @param props.placeholder Placeholder for the inner UnitControl. * - * @return {Element} Text indent control. + * @return Text indent control. */ export default function TextIndentControl( { value, @@ -37,10 +38,11 @@ export default function TextIndentControl( { className, placeholder, ...otherProps -} ) { +}: TextIndentControlProps ) { const [ availableUnits ] = useSettings( 'spacing.units' ); const units = useCustomUnits( { - availableUnits: availableUnits || [ + // The setting lists unit strings. + availableUnits: ( availableUnits as string[] | undefined ) || [ 'px', 'em', 'rem', diff --git a/packages/block-editor/src/components/text-indent-control/types.ts b/packages/block-editor/src/components/text-indent-control/types.ts new file mode 100644 index 00000000000000..3d780cec803d48 --- /dev/null +++ b/packages/block-editor/src/components/text-indent-control/types.ts @@ -0,0 +1,46 @@ +import type { ComponentProps, ReactNode } from 'react'; +import type { __experimentalUnitControl as UnitControl } from '@wordpress/components'; + +type UnitControlProps = ComponentProps< typeof UnitControl >; + +/** + * Props of `TextIndentControl`. + */ +export interface TextIndentControlProps extends Omit< + UnitControlProps, + 'value' | 'onChange' | 'label' +> { + /** + * Currently selected text indent. + */ + value?: string; + /** + * Handles change in text indent selection. + */ + onChange?: ( value?: string ) => void; + /** + * Input width to pass through to inner UnitControl. Should be a valid CSS + * value. + */ + __unstableInputWidth?: UnitControlProps[ '__unstableInputWidth' ]; + /** + * Whether to show the slider control. + */ + withSlider?: boolean; + /** + * Whether to add bottom margin below the control. + */ + hasBottomMargin?: boolean; + /** + * Help text to display below the control. + */ + help?: ReactNode; + /** + * Class name to add to the inner UnitControl. + */ + className?: string; + /** + * Placeholder for the inner UnitControl. + */ + placeholder?: string; +} diff --git a/packages/block-editor/src/components/text-transform-control/index.jsx b/packages/block-editor/src/components/text-transform-control/index.tsx similarity index 67% rename from packages/block-editor/src/components/text-transform-control/index.jsx rename to packages/block-editor/src/components/text-transform-control/index.tsx index dd10152bf7ac24..cceb1397da671c 100644 --- a/packages/block-editor/src/components/text-transform-control/index.jsx +++ b/packages/block-editor/src/components/text-transform-control/index.tsx @@ -10,6 +10,7 @@ import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, } from '@wordpress/components'; +import type { TextTransformControlProps } from './types'; const TEXT_TRANSFORMS = [ { @@ -37,14 +38,18 @@ const TEXT_TRANSFORMS = [ /** * Control to facilitate text transform selections. * - * @param {Object} props Component props. - * @param {string} props.className Class name to add to the control. - * @param {string} props.value Currently selected text transform. - * @param {Function} props.onChange Handles change in text transform selection. + * @param props Component props. + * @param props.className Class name to add to the control. + * @param props.value Currently selected text transform. + * @param props.onChange Handles change in text transform selection. * - * @return {Element} Text transform control. + * @return Text transform control. */ -export default function TextTransformControl( { className, value, onChange } ) { +export default function TextTransformControl( { + className, + value, + onChange, +}: TextTransformControlProps ) { return ( { - onChange( newValue === value ? undefined : newValue ); + // The options' values are strings. + onChange( + newValue === value + ? undefined + : ( newValue as string | undefined ) + ); } } > { TEXT_TRANSFORMS.map( ( option ) => { diff --git a/packages/block-editor/src/components/text-transform-control/stories/index.story.jsx b/packages/block-editor/src/components/text-transform-control/stories/index.story.tsx similarity index 75% rename from packages/block-editor/src/components/text-transform-control/stories/index.story.jsx rename to packages/block-editor/src/components/text-transform-control/stories/index.story.tsx index 1d575ac2cafbe4..d4d72834ad5588 100644 --- a/packages/block-editor/src/components/text-transform-control/stories/index.story.jsx +++ b/packages/block-editor/src/components/text-transform-control/stories/index.story.tsx @@ -1,7 +1,8 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; import { useState } from '@wordpress/element'; import TextTransformControl from '../'; -const meta = { +const meta: Meta< typeof TextTransformControl > = { id: 'blockeditor-texttransformcontrol', title: 'Editor/Block Editor/TextTransformControl', component: TextTransformControl, @@ -18,6 +19,7 @@ const meta = { onChange: { action: 'onChange', control: { + // @ts-expect-error Storybook types omit `null`, which hides the control. type: null, }, description: 'Handles change in text transform selection.', @@ -35,6 +37,7 @@ const meta = { }, }, value: { + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'Currently selected text transform.', table: { @@ -46,9 +49,11 @@ const meta = { export default meta; -export const Default = { +type Story = StoryObj< typeof meta >; + +export const Default: Story = { render: function Template( { onChange, ...args } ) { - const [ value, setValue ] = useState(); + const [ value, setValue ] = useState< string >(); return ( void; +} diff --git a/packages/block-editor/src/components/writing-mode-control/index.jsx b/packages/block-editor/src/components/writing-mode-control/index.tsx similarity index 64% rename from packages/block-editor/src/components/writing-mode-control/index.jsx rename to packages/block-editor/src/components/writing-mode-control/index.tsx index f85af1d5b73d15..2b9c1416f36f69 100644 --- a/packages/block-editor/src/components/writing-mode-control/index.jsx +++ b/packages/block-editor/src/components/writing-mode-control/index.tsx @@ -5,6 +5,7 @@ import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, } from '@wordpress/components'; +import type { WritingModeControlProps } from './types'; const WRITING_MODES = [ { @@ -22,14 +23,18 @@ const WRITING_MODES = [ /** * Control to facilitate writing mode selections. * - * @param {Object} props Component props. - * @param {string} props.className Class name to add to the control. - * @param {string} props.value Currently selected writing mode. - * @param {Function} props.onChange Handles change in the writing mode selection. + * @param props Component props. + * @param props.className Class name to add to the control. + * @param props.value Currently selected writing mode. + * @param props.onChange Handles change in the writing mode selection. * - * @return {Element} Writing Mode control. + * @return Writing Mode control. */ -export default function WritingModeControl( { className, value, onChange } ) { +export default function WritingModeControl( { + className, + value, + onChange, +}: WritingModeControlProps ) { return ( { - onChange( newValue === value ? undefined : newValue ); + // The options' values are strings. + onChange( + newValue === value + ? undefined + : ( newValue as string | undefined ) + ); } } > { WRITING_MODES.map( ( option ) => { diff --git a/packages/block-editor/src/components/writing-mode-control/stories/index.story.jsx b/packages/block-editor/src/components/writing-mode-control/stories/index.story.tsx similarity index 71% rename from packages/block-editor/src/components/writing-mode-control/stories/index.story.jsx rename to packages/block-editor/src/components/writing-mode-control/stories/index.story.tsx index 15b476586ffdaf..d03c4b704137bf 100644 --- a/packages/block-editor/src/components/writing-mode-control/stories/index.story.jsx +++ b/packages/block-editor/src/components/writing-mode-control/stories/index.story.tsx @@ -1,7 +1,8 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; import { useState } from '@wordpress/element'; import WritingModeControl from '../'; -const meta = { +const meta: Meta< typeof WritingModeControl > = { id: 'blockeditor-writingmodecontrol', title: 'Editor/Block Editor/WritingModeControl', component: WritingModeControl, @@ -15,6 +16,7 @@ const meta = { }, argTypes: { value: { + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'Currently selected writing mode.', }, @@ -24,6 +26,7 @@ const meta = { }, onChange: { action: 'onChange', + // @ts-expect-error Storybook types omit `null`, which hides the control. control: { type: null }, description: 'Handles change in the writing mode selection.', }, @@ -32,9 +35,11 @@ const meta = { export default meta; -export const Default = { +type Story = StoryObj< typeof meta >; + +export const Default: Story = { render: function Template( { onChange, ...args } ) { - const [ value, setValue ] = useState(); + const [ value, setValue ] = useState< string >(); return ( void; +} diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json index 04dc1ab72c742d..ae79766b2a1ceb 100644 --- a/tools/eslint/suppressions.json +++ b/tools/eslint/suppressions.json @@ -1677,76 +1677,6 @@ "count": 1 } }, - "packages/block-editor/src/components/font-appearance-control/index.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-appearance-control/test/index.jsdom.test.js": { - "gutenberg/use-typescript": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-family/index.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-family/stories/index.story.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-sizes/fluid-utils.js": { - "gutenberg/use-typescript": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-sizes/font-size-picker.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-sizes/index.js": { - "gutenberg/use-typescript": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-sizes/test/fluid-utils.js": { - "gutenberg/use-typescript": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-sizes/test/utils.js": { - "gutenberg/use-typescript": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-sizes/utils.js": { - "gutenberg/use-typescript": { - "count": 1 - } - }, - "packages/block-editor/src/components/font-sizes/with-font-sizes.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, "packages/block-editor/src/components/global-styles/advanced-panel.jsx": { "@wordpress/use-recommended-components": { "count": 2 @@ -2647,51 +2577,6 @@ "count": 1 } }, - "packages/block-editor/src/components/letter-spacing-control/index.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/letter-spacing-control/stories/index.story.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/line-height-control/index.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/line-height-control/stories/index.story.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/line-height-control/test/index.jsdom.test.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/line-height-control/utils.js": { - "gutenberg/use-typescript": { - "count": 1 - } - }, "packages/block-editor/src/components/link-control/index.tsx": { "@wordpress/use-recommended-components": { "count": 3 @@ -3436,63 +3321,9 @@ "count": 1 } }, - "packages/block-editor/src/components/text-alignment-control/index.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/text-alignment-control/stories/index.story.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/text-decoration-control/index.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/text-decoration-control/stories/index.story.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/text-indent-control/index.jsx": { + "packages/block-editor/src/components/text-indent-control/index.tsx": { "@wordpress/use-recommended-components": { "count": 5 - }, - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/text-transform-control/index.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/text-transform-control/stories/index.story.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 } }, "packages/block-editor/src/components/tool-selector/index.js": { @@ -3730,22 +3561,6 @@ "count": 1 } }, - "packages/block-editor/src/components/writing-mode-control/index.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, - "packages/block-editor/src/components/writing-mode-control/stories/index.story.jsx": { - "gutenberg/use-typescript": { - "count": 1 - }, - "react/jsx-filename-extension": { - "count": 1 - } - }, "packages/block-editor/src/elements/index.js": { "gutenberg/use-typescript": { "count": 1