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' ) } + + + + + + + + + ); +} + +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() { + + + +