diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx
index cb17b7a16e363..2a87bb497d344 100644
--- a/packages/components/src/font-size-picker/index.tsx
+++ b/packages/components/src/font-size-picker/index.tsx
@@ -13,14 +13,6 @@ import { useState, useMemo, forwardRef } from '@wordpress/element';
/**
* Internal dependencies
*/
-import { Button } from '../button';
-import RangeControl from '../range-control';
-import { Flex, FlexItem } from '../flex';
-import {
- default as UnitControl,
- parseQuantityAndUnitFromRawValue,
- useCustomUnits,
-} from '../unit-control';
import { VisuallyHidden } from '../visually-hidden';
import { getCommonSizeUnit } from './utils';
import type { FontSizePickerProps } from './types';
@@ -34,10 +26,9 @@ import {
import { Spacer } from '../spacer';
import FontSizePickerSelect from './font-size-picker-select';
import FontSizePickerToggleGroup from './font-size-picker-toggle-group';
+import SizeControl, { DEFAULT_UNITS } from '../size-control';
import { T_SHIRT_NAMES } from './constants';
-const DEFAULT_UNITS = [ 'px', 'em', 'rem', 'vw', 'vh' ];
-
const UnforwardedFontSizePicker = (
props: FontSizePickerProps,
ref: ForwardedRef< any >
@@ -49,16 +40,12 @@ const UnforwardedFontSizePicker = (
disableCustomFontSizes = false,
onChange,
size = 'default',
- units: unitsProp = DEFAULT_UNITS,
+ units = DEFAULT_UNITS,
value,
withSlider = false,
withReset = true,
} = props;
- const units = useCustomUnits( {
- availableUnits: unitsProp,
- } );
-
const shouldUseSelectControl = fontSizes.length > 5;
const selectedFontSize = fontSizes.find(
( fontSize ) => fontSize.size === value
@@ -107,14 +94,6 @@ const UnforwardedFontSizePicker = (
const hasUnits =
typeof value === 'string' || typeof fontSizes[ 0 ]?.size === 'string';
- const [ valueQuantity, valueUnit ] = parseQuantityAndUnitFromRawValue(
- value,
- units
- );
- const isValueUnitRelative =
- !! valueUnit && [ 'em', 'rem', 'vw', 'vh' ].includes( valueUnit );
- const isDisabled = value === undefined;
-
return (
{ __( 'Font size' ) }
@@ -201,85 +180,16 @@ const UnforwardedFontSizePicker = (
/>
) }
{ ! disableCustomFontSizes && showCustomValueControl && (
-
-
- {
- if ( newValue === undefined ) {
- onChange?.( undefined );
- } else {
- onChange?.(
- hasUnits
- ? newValue
- : parseInt( newValue, 10 )
- );
- }
- } }
- size={ size }
- units={ hasUnits ? units : [] }
- min={ 0 }
- />
-
- { withSlider && (
-
-
- {
- if ( newValue === undefined ) {
- onChange?.( undefined );
- } else if ( hasUnits ) {
- onChange?.(
- newValue +
- ( valueUnit ?? 'px' )
- );
- } else {
- onChange?.( newValue );
- }
- } }
- min={ 0 }
- max={ isValueUnitRelative ? 10 : 100 }
- step={ isValueUnitRelative ? 0.1 : 1 }
- />
-
-
- ) }
- { withReset && (
-
-
-
- ) }
-
+
) }
diff --git a/packages/components/src/font-size-picker/test/index.tsx b/packages/components/src/font-size-picker/test/index.tsx
index 9bb3b2d8677b6..a14c159e92d52 100644
--- a/packages/components/src/font-size-picker/test/index.tsx
+++ b/packages/components/src/font-size-picker/test/index.tsx
@@ -8,7 +8,7 @@ import userEvent from '@testing-library/user-event';
* Internal dependencies
*/
import FontSizePicker from '../';
-import type { FontSize } from '../types';
+import type { Size } from '../../size-control/types';
describe( 'FontSizePicker', () => {
test.each( [
@@ -474,7 +474,7 @@ describe( 'FontSizePicker', () => {
commonTests( fontSizes );
} );
- function commonToggleGroupTests( fontSizes: FontSize[] ) {
+ function commonToggleGroupTests( fontSizes: Size[] ) {
it( 'defaults to M when value is 16px', () => {
render(
{
);
}
- function commonSelectTests( fontSizes: FontSize[] ) {
+ function commonSelectTests( fontSizes: Size[] ) {
it( 'shows custom input when Custom is selected', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
@@ -519,7 +519,7 @@ describe( 'FontSizePicker', () => {
} );
}
- function commonTests( fontSizes: FontSize[] ) {
+ function commonTests( fontSizes: Size[] ) {
it( 'shows custom input when value is unknown', () => {
render( );
expect( screen.getByLabelText( 'Custom' ) ).toBeInTheDocument();
diff --git a/packages/components/src/font-size-picker/types.ts b/packages/components/src/font-size-picker/types.ts
index 6b4ed4b7ee75a..109b8af47f2a2 100644
--- a/packages/components/src/font-size-picker/types.ts
+++ b/packages/components/src/font-size-picker/types.ts
@@ -1,4 +1,12 @@
-export type FontSizePickerProps = {
+/**
+ * Internal dependencies
+ */
+import type { SizeControlBaseProps, Size } from '../size-control/types';
+
+export type FontSizePickerProps = Omit<
+ SizeControlBaseProps,
+ 'fallbackValue' | 'hasUnit'
+> & {
/**
* If `true`, it will not be possible to choose a custom fontSize. The user
* will be forced to pick one of the pre-defined sizes passed in fontSizes.
@@ -15,82 +23,7 @@ export type FontSizePickerProps = {
* An array of font size objects. The object should contain properties size,
* name, and slug.
*/
- fontSizes?: FontSize[];
- /**
- * A function that receives the new font size value.
- * If onChange is called without any parameter, it should reset the value,
- * attending to what reset means in that context, e.g., set the font size to
- * undefined or set the font size a starting value.
- */
- onChange?: (
- value: number | string | undefined,
- selectedItem?: FontSize
- ) => void;
- /**
- * Available units for custom font size selection.
- *
- * @default `[ 'px', 'em', 'rem' ]`
- */
- units?: string[];
- /**
- * The current font size value.
- */
- value?: number | string;
- /**
- * If `true`, the UI will contain a slider, instead of a numeric text input
- * field. If `false`, no slider will be present.
- *
- * @default false
- */
- withSlider?: boolean;
- /**
- * If `true`, a reset button will be displayed alongside the input field
- * when a custom font size is active. Has no effect when
- * `disableCustomFontSizes` or `withSlider` is `true`.
- *
- * @default true
- */
- withReset?: boolean;
- /**
- * Start opting into the new margin-free styles that will become the default
- * in a future version, currently scheduled to be WordPress 6.4. (The prop
- * can be safely removed once this happens.)
- *
- * @default false
- * @deprecated Default behavior since WP 6.5. Prop can be safely removed.
- * @ignore
- */
- __nextHasNoMarginBottom?: boolean;
- /**
- * Start opting into the larger default height that will become the default size in a future version.
- *
- * @default false
- */
- __next40pxDefaultSize?: boolean;
- /**
- * Size of the control.
- *
- * @default 'default'
- */
- size?: 'default' | '__unstable-large';
-};
-
-export type FontSize = {
- /**
- * The property `size` contains a number with the font size value, in `px` or
- * a string specifying the font size CSS property that should be used eg:
- * "13px", "1em", or "clamp(12px, 5vw, 100px)".
- */
- size: number | string;
- /**
- * The `name` property includes a label for that font size e.g.: `Small`.
- */
- name?: string;
- /**
- * The `slug` property is a string with a unique identifier for the font
- * size. Used for the class generation process.
- */
- slug: string;
+ fontSizes?: Size[];
};
export type FontSizePickerSelectProps = Pick<
@@ -109,7 +42,7 @@ export type FontSizePickerSelectProps = Pick<
export type FontSizePickerSelectOption = {
key: string;
name: string;
- value?: FontSize[ 'size' ];
+ value?: Size[ 'size' ];
__experimentalHint?: string;
};
diff --git a/packages/components/src/font-size-picker/utils.ts b/packages/components/src/font-size-picker/utils.ts
index cf81c7ed27b18..249dc3620de25 100644
--- a/packages/components/src/font-size-picker/utils.ts
+++ b/packages/components/src/font-size-picker/utils.ts
@@ -6,7 +6,8 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
-import type { FontSizePickerProps, FontSize } from './types';
+import type { Size } from '../size-control/types';
+import type { FontSizePickerProps } from './types';
import { parseQuantityAndUnitFromRawValue } from '../unit-control';
/**
@@ -31,7 +32,7 @@ export function isSimpleCssValue(
* @param fontSizes List of font sizes.
* @return The common unit, or null.
*/
-export function getCommonSizeUnit( fontSizes: FontSize[] ) {
+export function getCommonSizeUnit( fontSizes: Size[] ) {
const [ firstFontSize, ...otherFontSizes ] = fontSizes;
if ( ! firstFontSize ) {
return null;
diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts
index f55373664efff..2be3a8793ec64 100644
--- a/packages/components/src/private-apis.ts
+++ b/packages/components/src/private-apis.ts
@@ -26,6 +26,7 @@ import {
DropdownMenuItemLabel as DropdownMenuItemLabelV2,
DropdownMenuItemHelpText as DropdownMenuItemHelpTextV2,
} from './dropdown-menu-v2';
+import SizeControl from './size-control';
import { ComponentsContext } from './context/context-system-provider';
import Theme from './theme';
import Tabs from './tabs';
@@ -53,5 +54,6 @@ lock( privateApis, {
DropdownMenuSeparatorV2,
DropdownMenuItemLabelV2,
DropdownMenuItemHelpTextV2,
+ SizeControl,
kebabCase,
} );
diff --git a/packages/components/src/size-control/index.tsx b/packages/components/src/size-control/index.tsx
new file mode 100644
index 0000000000000..156bba1f670df
--- /dev/null
+++ b/packages/components/src/size-control/index.tsx
@@ -0,0 +1,160 @@
+/**
+ * External dependencies
+ */
+import type { ForwardedRef } from 'react';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { forwardRef } from '@wordpress/element';
+import { useInstanceId } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import { BaseControl, useBaseControlProps } from '../base-control';
+import type { WordPressComponentProps } from '../context/wordpress-component';
+import { Button } from '../button';
+import RangeControl from '../range-control';
+import { Flex, FlexItem } from '../flex';
+import type { SizeControlProps } from './types';
+import {
+ default as UnitControl,
+ parseQuantityAndUnitFromRawValue,
+ useCustomUnits,
+} from '../unit-control';
+
+import { Spacer } from '../spacer';
+
+export const DEFAULT_UNITS = [ 'px', 'em', 'rem', 'vw', 'vh' ];
+
+function UnforwardedSizeControl(
+ props: WordPressComponentProps< SizeControlProps, 'input', true >,
+ ref: ForwardedRef< HTMLInputElement >
+) {
+ const { baseControlProps } = useBaseControlProps( props );
+
+ const instanceId = useInstanceId( UnforwardedSizeControl );
+ const id = `size-control-${ instanceId }`;
+
+ const {
+ __next40pxDefaultSize = true,
+ __nextHasNoMarginBottom = true,
+ hasUnit = true,
+ value,
+ disabled,
+ size = 'default',
+ units: unitsProp = DEFAULT_UNITS,
+ withSlider = true,
+ withReset = true,
+ onChange,
+ fallbackValue,
+ } = props;
+
+ const units = useCustomUnits( {
+ availableUnits: unitsProp,
+ } );
+
+ const [ valueQuantity, valueUnit ] = parseQuantityAndUnitFromRawValue(
+ value,
+ units
+ );
+ const isValueUnitRelative =
+ !! valueUnit && [ 'em', 'rem', 'vw', 'vh' ].includes( valueUnit );
+
+ const handleValueChange = ( newValue: string | number | undefined ) => {
+ // On Reset
+ if ( newValue === undefined ) {
+ onChange?.( undefined );
+ return;
+ }
+
+ // If the component is initalized as a unitless value (for retrocompatibility)
+ if ( ! hasUnit ) {
+ onChange?.( parseInt( String( newValue ), 10 ) );
+ return;
+ }
+
+ // Parse the new value and unit.
+ const [ newQuantity, newUnit ] = parseQuantityAndUnitFromRawValue(
+ newValue,
+ units
+ );
+ onChange?.(
+ // If the new value is empty or couldn't be parsed, pass the raw value received.
+ newQuantity ? newQuantity + ( newUnit ?? 'px' ) : newValue
+ );
+ };
+
+ return (
+
+
+
+
+
+ { withSlider && (
+
+
+
+
+
+ ) }
+ { withReset && (
+
+
+
+ ) }
+
+
+ );
+}
+
+export const SizeControl = forwardRef( UnforwardedSizeControl );
+
+export default SizeControl;
diff --git a/packages/components/src/size-control/stories/index.story.tsx b/packages/components/src/size-control/stories/index.story.tsx
new file mode 100644
index 0000000000000..1c11825f95901
--- /dev/null
+++ b/packages/components/src/size-control/stories/index.story.tsx
@@ -0,0 +1,92 @@
+/**
+ * External dependencies
+ */
+import type { Meta, StoryFn } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import SizeControl from '..';
+import type { SizeControlProps } from '../types';
+
+const meta: Meta< typeof SizeControl > = {
+ title: 'Components (Experimental)/SizeControl',
+ tags: [ 'status-private' ],
+ component: SizeControl,
+ argTypes: {
+ value: { control: { type: null } },
+ },
+ parameters: {
+ actions: { argTypesRegex: '^on.*' },
+ controls: { expanded: true },
+ docs: { canvas: { sourceState: 'shown' } },
+ },
+};
+
+export default meta;
+
+const Template: StoryFn< typeof SizeControl > = ( args: SizeControlProps ) => {
+ const [ value, setValue ] = useState( args.value );
+
+ return (
+ {
+ // If it's resetting, use the fallbackValue
+ const newValue = val ?? '16px';
+ setValue( newValue );
+ action( 'onChange' )( newValue );
+ } }
+ />
+ );
+};
+
+export const Default = Template.bind( {} );
+Default.args = {
+ value: '16px',
+ size: 'default',
+ units: [ 'px', 'em', 'rem', 'vw', 'vh' ],
+ label: 'Size Control Label',
+};
+
+export const Disabled = Template.bind( {} );
+Disabled.args = {
+ value: '16px',
+ size: 'default',
+ units: [ 'px', 'em', 'rem', 'vw', 'vh' ],
+ label: 'Size Control Label',
+ disabled: true,
+};
+
+export const WithReset = Template.bind( {} );
+WithReset.args = {
+ value: '16px',
+ withReset: true,
+ size: 'default',
+ units: [ 'px', 'em', 'rem', 'vw', 'vh' ],
+ label: 'Size Control Label',
+};
+
+export const WithoutSlider = Template.bind( {} );
+WithoutSlider.args = {
+ value: '16px',
+ withSlider: false,
+ size: 'default',
+ units: [ 'px', 'em', 'rem', 'vw', 'vh' ],
+ label: 'Size Control Label',
+};
+
+export const CustomUnits = Template.bind( {} );
+CustomUnits.args = {
+ value: '16%',
+ size: 'default',
+ units: [ '%', 'em' ],
+ label: 'Size Control Label',
+};
diff --git a/packages/components/src/size-control/test/index.tsx b/packages/components/src/size-control/test/index.tsx
new file mode 100644
index 0000000000000..7ba67faf7a3ad
--- /dev/null
+++ b/packages/components/src/size-control/test/index.tsx
@@ -0,0 +1,75 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import SizeControl from '../';
+
+describe( 'SizeControl', () => {
+ test.each( [
+ // Use units when initial value uses units.
+ { value: '12px', expectedValue: '80px' },
+ // Use a different unit than the default px.
+ { value: '12em', expectedValue: '80em' },
+ // Don't use units when initial value does not use units.
+ { value: 12, expectedValue: 80, hasUnit: false },
+ ] )(
+ 'should call onChange( $expectedValue ) after user types 80 when value is $value',
+ async ( { value, expectedValue, hasUnit } ) => {
+ const user = userEvent.setup();
+ const onChange = jest.fn();
+ render(
+
+ );
+ const input = screen.getByLabelText( 'Size Control' );
+ await user.clear( input );
+ await user.type( input, '80' );
+ expect( onChange ).toHaveBeenCalledTimes( 3 ); // Once for the clear, then once per keystroke.
+ expect( onChange ).toHaveBeenCalledWith( expectedValue );
+ }
+ );
+
+ it( 'does not display a slider when withSlider is false', async () => {
+ render( );
+ expect(
+ screen.queryByLabelText( 'Custom Size' )
+ ).not.toBeInTheDocument();
+ } );
+
+ it( 'allows a slider by default', async () => {
+ const onChange = jest.fn();
+ render( );
+
+ const sliderInput = screen.getByLabelText( 'Custom Size' );
+ fireEvent.change( sliderInput, {
+ target: { value: 80 },
+ } );
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
+ expect( onChange ).toHaveBeenCalledWith( '80px' );
+ } );
+
+ it( 'allows reset by default', async () => {
+ const user = userEvent.setup();
+ const onChange = jest.fn();
+ render( );
+ await user.click( screen.getByRole( 'button', { name: 'Reset' } ) );
+ expect( onChange ).toHaveBeenCalledTimes( 1 );
+ expect( onChange ).toHaveBeenCalledWith( undefined );
+ } );
+
+ it( 'does not allow reset when withReset is false', async () => {
+ render( );
+ expect(
+ screen.queryByRole( 'button', { name: 'Reset' } )
+ ).not.toBeInTheDocument();
+ } );
+} );
diff --git a/packages/components/src/size-control/types.ts b/packages/components/src/size-control/types.ts
new file mode 100644
index 0000000000000..945a8e7c4bb64
--- /dev/null
+++ b/packages/components/src/size-control/types.ts
@@ -0,0 +1,85 @@
+/**
+ * Internal dependencies
+ */
+import type { BaseControlProps } from '../base-control/types';
+
+export type Size = {
+ /**
+ * The property `size` contains a number with the font size value, in `px` or
+ * a string specifying the font size CSS property that should be used eg:
+ * "13px", "1em", or "clamp(12px, 5vw, 100px)".
+ */
+ size: number | string;
+ /**
+ * The `name` property includes a label for that size e.g.: `Small`.
+ */
+ name?: string;
+ /**
+ * The `slug` property is a string with a unique identifier for the font
+ * size. Used for the class generation process.
+ */
+ slug: string;
+};
+
+export type SizeControlBaseProps = {
+ /**
+ * If no value exists, this prop defines the starting position for the font
+ * size picker slider. Only relevant if `withSlider` is `true`.
+ */
+ fallbackValue?: number;
+ /**
+ * A function that receives the new font size value.
+ * If onChange is called without any parameter, it should reset the value,
+ * attending to what reset means in that context, e.g., set the font size to
+ * undefined or set the font size a starting value.
+ */
+ onChange?: (
+ value: number | string | undefined,
+ selectedItem?: Size
+ ) => void;
+ /**
+ * Available units for custom font size selection.
+ *
+ * @default `[ 'px', 'em', 'rem' ]`
+ */
+ units?: string[];
+ /**
+ * The current font size value.
+ */
+ value?: number | string;
+ /**
+ * If `true`, the UI will contain a slider, instead of a numeric text input
+ * field. If `false`, no slider will be present.
+ *
+ * @default true
+ */
+ withSlider?: boolean;
+ /**
+ * If `true`, a reset button will be displayed alongside the input field
+ * when a custom font size is active. Has no effect when
+ *
+ * @default true
+ */
+ withReset?: boolean;
+ /**
+ * Start opting into the larger default height that will become the default size in a future version.
+ *
+ * @default false
+ */
+ __next40pxDefaultSize?: boolean;
+ /**
+ * Size of the control.
+ *
+ * @default 'default'
+ */
+ size?: 'default' | '__unstable-large';
+ /**
+ * If the control should handle values with units.
+ *
+ * @default true
+ */
+ hasUnit?: boolean;
+};
+
+export type SizeControlProps = SizeControlBaseProps &
+ Omit< BaseControlProps, 'children' >;
diff --git a/packages/edit-site/src/components/global-styles/font-sizes/confirm-delete-font-size-dialog.js b/packages/edit-site/src/components/global-styles/font-sizes/confirm-delete-font-size-dialog.js
new file mode 100644
index 0000000000000..a73727385b0c5
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-sizes/confirm-delete-font-size-dialog.js
@@ -0,0 +1,49 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ __experimentalConfirmDialog as ConfirmDialog,
+ __experimentalUseNavigator as useNavigator,
+} from '@wordpress/components';
+import { __, sprintf } from '@wordpress/i18n';
+
+function ConfirmDeleteFontSizeDialog( {
+ fontSize,
+ isOpen,
+ toggleOpen,
+ handleRemoveFontSize,
+} ) {
+ const navigator = useNavigator();
+
+ const handleConfirm = async () => {
+ toggleOpen();
+ handleRemoveFontSize( fontSize );
+ navigator.goTo( '/typography/' );
+ };
+
+ const handleCancel = () => {
+ toggleOpen();
+ };
+
+ return (
+
+ { fontSize &&
+ sprintf(
+ /* translators: %s: Name of the font size preset. */
+ __(
+ 'Are you sure you want to delete "%s" font size preset?'
+ ),
+ fontSize.name
+ ) }
+
+ );
+}
+
+export default ConfirmDeleteFontSizeDialog;
diff --git a/packages/edit-site/src/components/global-styles/font-sizes/font-size-preview.js b/packages/edit-site/src/components/global-styles/font-sizes/font-size-preview.js
new file mode 100644
index 0000000000000..b11e96393ab01
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-sizes/font-size-preview.js
@@ -0,0 +1,28 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../../lock-unlock';
+const { useGlobalStyle } = unlock( blockEditorPrivateApis );
+
+function FontSizePreview( { fontSize } ) {
+ const [ font ] = useGlobalStyle( 'typography' );
+ return (
+
+ { __( 'Aa' ) }
+
+ );
+}
+
+export default FontSizePreview;
diff --git a/packages/edit-site/src/components/global-styles/font-sizes/font-size.js b/packages/edit-site/src/components/global-styles/font-sizes/font-size.js
new file mode 100644
index 0000000000000..6e093df408398
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-sizes/font-size.js
@@ -0,0 +1,255 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+import { __, sprintf } from '@wordpress/i18n';
+import {
+ __experimentalSpacer as Spacer,
+ __experimentalUseNavigator as useNavigator,
+ __experimentalView as View,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+ privateApis as componentsPrivateApis,
+ Button,
+ FlexItem,
+ ToggleControl,
+} from '@wordpress/components';
+import { moreVertical } from '@wordpress/icons';
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../../lock-unlock';
+const {
+ SizeControl,
+ DropdownMenuV2: DropdownMenu,
+ DropdownMenuItemV2: DropdownMenuItem,
+ DropdownMenuItemLabelV2: DropdownMenuItemLabel,
+} = unlock( componentsPrivateApis );
+const { useGlobalSetting } = unlock( blockEditorPrivateApis );
+import ScreenHeader from '../header';
+import FontSizePreview from './font-size-preview';
+import ConfirmDeleteFontSizeDialog from './confirm-delete-font-size-dialog';
+import RenameFontSizeDialog from './rename-font-size-dialog';
+
+function FontSize() {
+ const [ isDeleteConfirmOpen, setIsDeleteConfirmOpen ] = useState( false );
+ const [ isRenameDialogOpen, setIsRenameDialogOpen ] = useState( false );
+
+ const {
+ params: { slug },
+ goBack,
+ } = useNavigator();
+ const [ fontSizes, setFontSizes ] = useGlobalSetting(
+ 'typography.fontSizes'
+ );
+ // Get the font sizes from the theme or use the default ones.
+ const sizes = fontSizes.theme ?? fontSizes.default ?? [];
+
+ // Get the font size by slug.
+ const fontSize = sizes.find( ( size ) => size.slug === slug );
+
+ const hasUnits = typeof fontSize.size === 'string';
+
+ // Whether fluid is true or an object, set it to true, otherwise false.
+ const isFluid = !! fontSize.fluid ?? false;
+
+ // Whether custom fluid values are used.
+ const isCustomFluid = typeof fontSize.fluid === 'object';
+
+ const handleNameChange = ( value ) => {
+ updateFontSize( 'name', value );
+ };
+
+ const handleFontSizeChange = ( value ) => {
+ updateFontSize( 'size', value );
+ };
+
+ const handleFluidChange = ( value ) => {
+ updateFontSize( 'fluid', value );
+ };
+
+ const handleCustomFluidValues = ( value ) => {
+ if ( value ) {
+ // If custom values are used, init the values with the current ones.
+ updateFontSize( 'fluid', {
+ min: fontSize.size,
+ max: fontSize.size,
+ } );
+ } else {
+ // If custom fluid values are disabled, set fluid to true.
+ updateFontSize( 'fluid', true );
+ }
+ };
+
+ const handleMinChange = ( value ) => {
+ updateFontSize( 'fluid', { ...fontSize.fluid, min: value } );
+ };
+
+ const handleMaxChange = ( value ) => {
+ updateFontSize( 'fluid', { ...fontSize.fluid, max: value } );
+ };
+
+ const updateFontSize = ( key, value ) => {
+ const newFontSizes = sizes.map( ( size ) => {
+ if ( size.slug === slug ) {
+ return { ...size, [ key ]: value }; // Create a new object with updated key
+ }
+ return size;
+ } );
+
+ setFontSizes( {
+ ...fontSizes,
+ theme: newFontSizes,
+ } );
+ };
+
+ const handleRemoveFontSize = () => {
+ // Navigate to the font sizes list.
+ goBack();
+
+ const newFontSizes = sizes.filter( ( size ) => size.slug !== slug );
+ setFontSizes( {
+ ...fontSizes,
+ theme: newFontSizes,
+ } );
+ };
+
+ const toggleDeleteConfirm = () => {
+ setIsDeleteConfirmOpen( ! isDeleteConfirmOpen );
+ };
+
+ const toggleRenameDialog = () => {
+ setIsRenameDialogOpen( ! isRenameDialogOpen );
+ };
+
+ return (
+ <>
+
+
+ { isRenameDialogOpen && (
+
+ ) }
+
+
+
+
+
+
+
+ }
+ >
+
+
+ { __( 'Rename' ) }
+
+
+
+
+ { __( 'Delete' ) }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ { isFluid && (
+
+ ) }
+
+ { isCustomFluid && (
+ <>
+
+
+ >
+ ) }
+
+
+
+
+ >
+ );
+}
+
+export default FontSize;
diff --git a/packages/edit-site/src/components/global-styles/font-sizes/font-sizes.js b/packages/edit-site/src/components/global-styles/font-sizes/font-sizes.js
new file mode 100644
index 0000000000000..8c55d2871bad4
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-sizes/font-sizes.js
@@ -0,0 +1,113 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
+import { __, sprintf } from '@wordpress/i18n';
+import {
+ __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue,
+ __experimentalItemGroup as ItemGroup,
+ __experimentalVStack as VStack,
+ __experimentalHStack as HStack,
+ FlexItem,
+ Button,
+} from '@wordpress/components';
+import { plus } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../../lock-unlock';
+const { useGlobalSetting } = unlock( blockEditorPrivateApis );
+import Subtitle from '../subtitle';
+import { NavigationButtonAsItem } from '../navigation-button';
+import { getNewIndexFromPresets } from '../utils';
+
+/**
+ * Coefficients to normalize font sizes to pixels.
+ *
+ * em/rm on units are bases the default font size of 16px.
+ * Viewport units are based on a 1920x1080 screen.
+ */
+const NORMALIZED_FONT_SIZE_COEFFICIENT = {
+ px: 1,
+ em: 16,
+ rem: 16,
+ vw: 19.2,
+ vh: 10.8,
+};
+
+/*
+ * Normalize a font size value to a specific unit.
+ *
+ * @param {string} size The font size value to normalize.
+ * @return {string} The normalized font size value.
+ */
+function normalizeFontSize( size ) {
+ const [ quantity, unit ] = parseQuantityAndUnitFromRawValue( size );
+ const normalizedSize =
+ quantity * ( NORMALIZED_FONT_SIZE_COEFFICIENT[ unit ] ?? 1 );
+ if ( isNaN( normalizedSize ) || isNaN( quantity ) ) {
+ return 1;
+ }
+ return normalizedSize;
+}
+
+function FontSizes() {
+ const [ fontSizes, setFontSizes ] = useGlobalSetting(
+ 'typography.fontSizes'
+ );
+
+ // Get the font sizes from the theme or use the default ones.
+ const sizes = fontSizes.theme ?? fontSizes.default ?? [];
+ const normalizedSizes = sizes
+ .map( ( fontSize ) => ( {
+ ...fontSize,
+ normalizedSize: normalizeFontSize( fontSize.size ),
+ } ) )
+ .sort( ( a, b ) => a.normalizedSize - b.normalizedSize );
+
+ const handleAddFontSize = () => {
+ const index = getNewIndexFromPresets( sizes, 'custom-' );
+ const newFontSize = {
+ /* translators: %d: font size index */
+ name: sprintf( __( 'New Font Size %d' ), index ),
+ size: '16px',
+ slug: `custom-${ index }`,
+ };
+
+ setFontSizes( { ...fontSizes, theme: [ ...sizes, newFontSize ] } );
+ };
+
+ return (
+
+
+ { __( 'Font Sizes' ) }
+
+
+
+ { normalizedSizes.map( ( size ) => (
+
+
+
+ { size.name }
+
+
+ { size.size }
+
+
+
+ ) ) }
+
+
+ );
+}
+
+export default FontSizes;
diff --git a/packages/edit-site/src/components/global-styles/font-sizes/rename-font-size-dialog.js b/packages/edit-site/src/components/global-styles/font-sizes/rename-font-size-dialog.js
new file mode 100644
index 0000000000000..5d197a12e683c
--- /dev/null
+++ b/packages/edit-site/src/components/global-styles/font-sizes/rename-font-size-dialog.js
@@ -0,0 +1,64 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ __experimentalInputControl as InputControl,
+ __experimentalSpacer as Spacer,
+ Button,
+ Flex,
+ FlexItem,
+ Modal,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useState } from '@wordpress/element';
+
+function RenameFontSizeDialog( { fontSize, toggleOpen, handleRename } ) {
+ const [ newName, setNewName ] = useState( fontSize.name );
+
+ const handleConfirm = () => {
+ // If the new name is not empty, call the handleRename function
+ if ( newName.trim() ) {
+ handleRename( newName );
+ }
+ toggleOpen();
+ };
+
+ return (
+
+
+
+ );
+}
+
+export default RenameFontSizeDialog;
diff --git a/packages/edit-site/src/components/global-styles/screen-typography.js b/packages/edit-site/src/components/global-styles/screen-typography.js
index 08472325ebc1f..fac9aa2ccc873 100644
--- a/packages/edit-site/src/components/global-styles/screen-typography.js
+++ b/packages/edit-site/src/components/global-styles/screen-typography.js
@@ -13,6 +13,7 @@ import TypographyElements from './typography-elements';
import TypographyVariations from './variations/variations-typography';
import FontFamilies from './font-families';
import ScreenHeader from './header';
+import FontSizes from './font-sizes/font-sizes';
function ScreenTypography() {
const fontLibraryEnabled = useSelect(
@@ -35,6 +36,7 @@ function ScreenTypography() {
fontLibraryEnabled && }
+
>
diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss
index 51e83f33c03ee..a3018e210484d 100644
--- a/packages/edit-site/src/components/global-styles/style.scss
+++ b/packages/edit-site/src/components/global-styles/style.scss
@@ -23,6 +23,17 @@
overflow: hidden;
}
+.edit-site-font-size__item {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ line-break: anywhere;
+}
+
+.edit-site-font-size__item-value {
+ max-width: 50% !important;
+}
+
.edit-site-global-styles-screen {
margin: $grid-unit-15 $grid-unit-20 $grid-unit-20;
}
diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js
index 08cd20e9aac6a..77e9b2b3fd524 100644
--- a/packages/edit-site/src/components/global-styles/ui.js
+++ b/packages/edit-site/src/components/global-styles/ui.js
@@ -33,6 +33,7 @@ import {
import ScreenBlock from './screen-block';
import ScreenTypography from './screen-typography';
import ScreenTypographyElement from './screen-typography-element';
+import FontSize from './font-sizes/font-size';
import ScreenColors from './screen-colors';
import ScreenColorPalette from './screen-color-palette';
import { ScreenShadows, ScreenShadowsEdit } from './screen-shadows';
@@ -231,7 +232,12 @@ function GlobalStylesBlockLink() {
if ( newPath !== currentPath ) {
navigator.goTo( newPath, { skipFocus: true } );
}
- }, [ selectedBlockClientId, selectedBlockName, blockHasGlobalStyles ] );
+ }, [
+ selectedBlockClientId,
+ selectedBlockName,
+ blockHasGlobalStyles,
+ navigator,
+ ] );
}
function GlobalStylesEditorCanvasContainerLink() {
@@ -282,7 +288,7 @@ function GlobalStylesEditorCanvasContainerLink() {
goTo( '/' );
break;
}
- }, [ editorCanvasContainerView, isRevisionsOpen, goTo ] );
+ }, [ editorCanvasContainerView, isRevisionsOpen, goTo, path ] );
}
function GlobalStylesUI() {
@@ -313,6 +319,10 @@ function GlobalStylesUI() {
+
+
+
+