diff --git a/readme.txt b/readme.txt index 952baad6..7c5a8eaf 100644 --- a/readme.txt +++ b/readme.txt @@ -20,6 +20,7 @@ This plugin allows you to: - Create a new style variation - Export a theme - Save user changed templates and styles to the active theme +- Expand the typography controls in the block inspector while authoring a theme (opt-in, under Editor preferences) All newly created themes or style variations will include changes made within the WordPress Editor. diff --git a/src/editor-enhancements/expand-typography-controls.js b/src/editor-enhancements/expand-typography-controls.js new file mode 100644 index 00000000..f7001fa1 --- /dev/null +++ b/src/editor-enhancements/expand-typography-controls.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +// eslint-disable-next-line import/no-unresolved -- Provided as an external at build time via @wordpress/dependency-extraction-webpack-plugin. +import { addFilter } from '@wordpress/hooks'; +import { select } from '@wordpress/data'; +import { store as preferencesStore } from '@wordpress/preferences'; + +// Mapping verified against +// gutenberg/packages/block-editor/src/components/global-styles/typography-panel.js +// fontWeight and fontStyle collapse into the single "fontAppearance" control. +export const TYPOGRAPHY_SUPPORT_TO_CONTROL = { + fontSize: 'fontSize', + lineHeight: 'lineHeight', + textAlign: 'textAlign', + textColumns: 'textColumns', + textIndent: 'textIndent', + __experimentalFontFamily: 'fontFamily', + __experimentalLetterSpacing: 'letterSpacing', + __experimentalTextDecoration: 'textDecoration', + __experimentalTextTransform: 'textTransform', + __experimentalWritingMode: 'writingMode', + __experimentalFontWeight: 'fontAppearance', + __experimentalFontStyle: 'fontAppearance', +}; + +export const PREFERENCE_SCOPE = 'create-block-theme'; +export const PREFERENCE_KEY = 'expandAllTypographyControls'; + +export function expandTypographyDefaults( typographySupport ) { + if ( ! typographySupport || typeof typographySupport !== 'object' ) { + return typographySupport; + } + const defaults = {}; + for ( const [ supportKey, controlKey ] of Object.entries( + TYPOGRAPHY_SUPPORT_TO_CONTROL + ) ) { + if ( typographySupport[ supportKey ] ) { + defaults[ controlKey ] = true; + } + } + return { + ...typographySupport, + __experimentalDefaultControls: { + ...( typographySupport.__experimentalDefaultControls ?? {} ), + ...defaults, + }, + }; +} + +addFilter( + 'blocks.registerBlockType', + 'create-block-theme/expand-typography-controls', + ( settings ) => { + const enabled = select( preferencesStore )?.get?.( + PREFERENCE_SCOPE, + PREFERENCE_KEY + ); + if ( ! enabled || ! settings?.supports?.typography ) { + return settings; + } + return { + ...settings, + supports: { + ...settings.supports, + typography: expandTypographyDefaults( + settings.supports.typography + ), + }, + }; + } +); diff --git a/src/editor-sidebar/editor-preferences-panel.js b/src/editor-sidebar/editor-preferences-panel.js new file mode 100644 index 00000000..6273b39c --- /dev/null +++ b/src/editor-sidebar/editor-preferences-panel.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as preferencesStore } from '@wordpress/preferences'; +import { + // eslint-disable-next-line @wordpress/no-unsafe-wp-apis + __experimentalVStack as VStack, + Card, + CardBody, + CheckboxControl, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import ScreenHeader from './screen-header'; +import { + PREFERENCE_SCOPE, + PREFERENCE_KEY, +} from '../editor-enhancements/expand-typography-controls'; + +export const EditorPreferencesPanel = () => { + const expandAllTypographyControls = useSelect( + ( select ) => + !! select( preferencesStore ).get( + PREFERENCE_SCOPE, + PREFERENCE_KEY + ), + [] + ); + + const { set: setPreference } = useDispatch( preferencesStore ); + + const handleToggle = ( value ) => { + setPreference( PREFERENCE_SCOPE, PREFERENCE_KEY, value ); + // eslint-disable-next-line no-alert + window.alert( + __( + 'Preference updated. The editor will now reload.', + 'create-block-theme' + ) + ); + window.location.reload(); + }; + + return ( + + + + + + + + + ); +}; diff --git a/src/plugin-sidebar.js b/src/plugin-sidebar.js index 90d487d0..2d283361 100644 --- a/src/plugin-sidebar.js +++ b/src/plugin-sidebar.js @@ -1,3 +1,10 @@ +/** + * Internal dependencies + */ +// Side-effect import — must load before block registration so the +// registerBlockType filter is in place when blocks register. +import './editor-enhancements/expand-typography-controls'; + /** * WordPress dependencies */ @@ -39,6 +46,7 @@ import { blockMeta, help, trash, + cog, } from '@wordpress/icons'; /** @@ -55,6 +63,7 @@ import { downloadExportedTheme } from './resolvers'; import downloadFile from './utils/download-file'; import AboutPlugin from './editor-sidebar/about'; import ResetTheme from './editor-sidebar/reset-theme'; +import { EditorPreferencesPanel } from './editor-sidebar/editor-preferences-panel'; import './plugin-styles.scss'; function PluginSidebarItem( { icon, path, children, onClick } ) { @@ -229,6 +238,20 @@ const CreateBlockThemePlugin = () => { + + + + { __( + 'Editor preferences', + 'create-block-theme' + ) } + + + + { + + + + diff --git a/src/test/expand-typography-controls.test.js b/src/test/expand-typography-controls.test.js new file mode 100644 index 00000000..f5164413 --- /dev/null +++ b/src/test/expand-typography-controls.test.js @@ -0,0 +1,173 @@ +// The module under test imports WordPress packages that are provided as +// externals at build time and are not installed as npm dependencies. Mock them +// so Jest can resolve the module to test the pure mapping function. +jest.mock( '@wordpress/hooks', () => ( { addFilter: jest.fn() } ), { + virtual: true, +} ); +jest.mock( '@wordpress/data', () => ( { select: jest.fn() } ), { + virtual: true, +} ); +jest.mock( '@wordpress/preferences', () => ( { store: 'core/preferences' } ), { + virtual: true, +} ); + +/** + * Internal dependencies + */ +// eslint-disable-next-line import/first +import { + expandTypographyDefaults, + TYPOGRAPHY_SUPPORT_TO_CONTROL, +} from '../editor-enhancements/expand-typography-controls'; + +describe( 'expandTypographyDefaults', () => { + it( 'returns input unchanged when null', () => { + expect( expandTypographyDefaults( null ) ).toBe( null ); + } ); + + it( 'returns input unchanged when undefined', () => { + expect( expandTypographyDefaults( undefined ) ).toBe( undefined ); + } ); + + it( 'returns input unchanged when not an object', () => { + expect( expandTypographyDefaults( true ) ).toBe( true ); + } ); + + it( 'yields an empty defaults map when no supports are enabled', () => { + expect( expandTypographyDefaults( {} ) ).toEqual( { + __experimentalDefaultControls: {}, + } ); + } ); + + it( 'maps fontSize to fontSize', () => { + expect( expandTypographyDefaults( { fontSize: true } ) ).toEqual( { + fontSize: true, + __experimentalDefaultControls: { fontSize: true }, + } ); + } ); + + it( 'maps __experimentalFontFamily to fontFamily', () => { + expect( + expandTypographyDefaults( { __experimentalFontFamily: true } ) + ).toEqual( { + __experimentalFontFamily: true, + __experimentalDefaultControls: { fontFamily: true }, + } ); + } ); + + it( 'maps __experimentalFontWeight alone to fontAppearance', () => { + expect( + expandTypographyDefaults( { __experimentalFontWeight: true } ) + ).toEqual( { + __experimentalFontWeight: true, + __experimentalDefaultControls: { fontAppearance: true }, + } ); + } ); + + it( 'maps __experimentalFontStyle alone to fontAppearance', () => { + expect( + expandTypographyDefaults( { __experimentalFontStyle: true } ) + ).toEqual( { + __experimentalFontStyle: true, + __experimentalDefaultControls: { fontAppearance: true }, + } ); + } ); + + it( 'collapses both weight and style into a single fontAppearance entry', () => { + const result = expandTypographyDefaults( { + __experimentalFontWeight: true, + __experimentalFontStyle: true, + } ); + expect( result.__experimentalDefaultControls ).toEqual( { + fontAppearance: true, + } ); + expect( + Object.keys( result.__experimentalDefaultControls ).length + ).toBe( 1 ); + } ); + + it( 'ignores support keys that are falsy', () => { + expect( + expandTypographyDefaults( { + fontSize: true, + lineHeight: false, + textAlign: undefined, + } ) + ).toEqual( { + fontSize: true, + lineHeight: false, + textAlign: undefined, + __experimentalDefaultControls: { fontSize: true }, + } ); + } ); + + it( 'maps a full realistic supports.typography shape to every expected control', () => { + const fullShape = { + fontSize: true, + lineHeight: true, + textAlign: true, + textColumns: true, + textIndent: true, + __experimentalFontFamily: true, + __experimentalLetterSpacing: true, + __experimentalTextDecoration: true, + __experimentalTextTransform: true, + __experimentalWritingMode: true, + __experimentalFontWeight: true, + __experimentalFontStyle: true, + }; + expect( + expandTypographyDefaults( fullShape ).__experimentalDefaultControls + ).toEqual( { + fontSize: true, + lineHeight: true, + textAlign: true, + textColumns: true, + textIndent: true, + fontFamily: true, + letterSpacing: true, + textDecoration: true, + textTransform: true, + writingMode: true, + fontAppearance: true, + } ); + } ); + + it( 'preserves unknown keys already set in __experimentalDefaultControls', () => { + const input = { + fontSize: true, + __experimentalDefaultControls: { + fontSize: false, + someFutureControl: true, + }, + }; + const result = expandTypographyDefaults( input ); + expect( result.__experimentalDefaultControls ).toEqual( { + fontSize: true, + someFutureControl: true, + } ); + } ); + + it( 'preserves other keys on the typography supports object', () => { + const input = { + fontSize: true, + customKey: 'untouched', + }; + const result = expandTypographyDefaults( input ); + expect( result.customKey ).toBe( 'untouched' ); + expect( result.__experimentalDefaultControls ).toEqual( { + fontSize: true, + } ); + } ); +} ); + +describe( 'TYPOGRAPHY_SUPPORT_TO_CONTROL', () => { + it( 'merges fontWeight and fontStyle into fontAppearance', () => { + expect( TYPOGRAPHY_SUPPORT_TO_CONTROL.__experimentalFontWeight ).toBe( + 'fontAppearance' + ); + expect( TYPOGRAPHY_SUPPORT_TO_CONTROL.__experimentalFontStyle ).toBe( + 'fontAppearance' + ); + } ); +} );