From 4474833cf299e8bf476f791a9e105b232a3d3764 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 4 Nov 2020 09:41:29 +1000 Subject: [PATCH] Block Support: Add text transform block support using CSS variables (#26060) * Add text transform block support * Add config for block preset classes * Update text transform controls * Fix PHPCS errors * Allow for text transform controls alongside decoration The direction the design is headed it to display text decoration and text transform controls side-by-side. This commit provides a new component to handle grouping these together and arranging them within a flex container. It also makes minor tweaks like labelling the controls "Letter case" instead of "Text Transform" and using plain text to provide something like the icons will be. * Add new icons for text transforms * Switch to only opting in for navigation block * Fix return value regression after resolving merge conflict --- lib/block-supports/typography.php | 63 ++++++++++-- lib/experimental-default-theme.json | 14 +++ lib/global-styles.php | 26 +++-- .../text-decoration-and-transform/index.js | 35 +++++++ .../text-decoration-and-transform/style.scss | 3 + .../text-transform-control/index.js | 82 ++++++++++++++++ .../text-transform-control/style.scss | 18 ++++ .../block-editor/src/hooks/text-transform.js | 98 +++++++++++++++++++ packages/block-editor/src/hooks/typography.js | 8 ++ packages/block-editor/src/style.scss | 2 + .../block-library/src/navigation/block.json | 1 + packages/blocks/src/api/constants.js | 1 + .../edit-site/src/components/editor/utils.js | 1 + packages/icons/src/index.js | 3 + .../icons/src/library/format-capitalize.js | 12 +++ .../icons/src/library/format-lowercase.js | 12 +++ .../icons/src/library/format-uppercase.js | 12 +++ 17 files changed, 375 insertions(+), 16 deletions(-) create mode 100644 packages/block-editor/src/components/text-decoration-and-transform/index.js create mode 100644 packages/block-editor/src/components/text-decoration-and-transform/style.scss create mode 100644 packages/block-editor/src/components/text-transform-control/index.js create mode 100644 packages/block-editor/src/components/text-transform-control/style.scss create mode 100644 packages/block-editor/src/hooks/text-transform.js create mode 100644 packages/icons/src/library/format-capitalize.js create mode 100644 packages/icons/src/library/format-lowercase.js create mode 100644 packages/icons/src/library/format-uppercase.js diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 3f7ebb7c28d21e..0f4d7dbce19394 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -21,11 +21,16 @@ function gutenberg_register_typography_support( $block_type ) { $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); } + $has_text_transform_support = false; + if ( property_exists( $block_type, 'supports' ) ) { + $has_text_transform_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextTransform' ), false ); + } + if ( ! $block_type->attributes ) { $block_type->attributes = array(); } - if ( ( $has_font_size_support || $has_line_height_support ) && ! array_key_exists( 'style', $block_type->attributes ) ) { + if ( ( $has_font_size_support || $has_line_height_support || $has_text_transform_support ) && ! array_key_exists( 'style', $block_type->attributes ) ) { $block_type->attributes['style'] = array( 'type' => 'object', ); @@ -48,17 +53,16 @@ function gutenberg_register_typography_support( $block_type ) { * @return array Font size CSS classes and inline styles. */ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { - $has_font_size_support = false; - $classes = array(); - $styles = array(); - if ( property_exists( $block_type, 'supports' ) ) { - $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); + if ( ! property_exists( $block_type, 'supports' ) ) { + return array(); } - $has_line_height_support = false; - if ( property_exists( $block_type, 'supports' ) ) { - $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); - } + $classes = array(); + $styles = array(); + + $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); + $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); + $has_text_transform_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextTransform' ), false ); // Font Size. if ( $has_font_size_support ) { @@ -82,6 +86,14 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { } } + // Text Transform. + if ( $has_text_transform_support ) { + $text_transform_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'textTransform', 'text-transform' ); + if ( $text_transform_style ) { + $styles[] = $text_transform_style; + } + } + $attributes = array(); if ( ! empty( $classes ) ) { $attributes['class'] = implode( ' ', $classes ); @@ -101,3 +113,34 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { 'apply' => 'gutenberg_apply_typography_support', ) ); + +/** + * Generates an inline style for a typography feature e.g. text decoration, + * text transform, and font style. + * + * @param array $attributes Block's attributes. + * @param string $feature Key for the feature within the typography styles. + * @param string $css_property Slug for the CSS property the inline style sets. + * + * @return string CSS inline style. + */ +function gutenberg_typography_get_css_variable_inline_style( $attributes, $feature, $css_property ) { + // Retrieve current attribute value or skip if not found. + $style_value = gutenberg_experimental_get( $attributes, array( 'style', 'typography', $feature ), false ); + if ( ! $style_value ) { + return; + } + + // If we don't have a preset CSS variable, we'll assume it's a regular CSS value. + if ( strpos( $style_value, "var:preset|{$css_property}|" ) === false ) { + return sprintf( '%s:%s;', $css_property, $style_value ); + } + + // We have a preset CSS variable as the style. + // Get the style value from the string and return CSS style. + $index_to_splice = strrpos( $style_value, '|' ) + 1; + $slug = substr( $style_value, $index_to_splice ); + + // Return the actual CSS inline style e.g. `text-decoration:var(--wp--preset--text-decoration--underline);`. + return sprintf( '%s:var(--wp--preset--%s--%s);', $css_property, $css_property, $slug ); +} diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json index 28a20292ff3f88..2e49877e50e136 100644 --- a/lib/experimental-default-theme.json +++ b/lib/experimental-default-theme.json @@ -160,6 +160,20 @@ "slug": "huge", "size": 42 } + ], + "textTransforms": [ + { + "name": "AB", + "slug": "uppercase" + }, + { + "name": "ab", + "slug": "lowercase" + }, + { + "name": "Ab", + "slug": "capitalize" + } ] }, "spacing": { diff --git a/lib/global-styles.php b/lib/global-styles.php index 544ef9fce1eb72..24f0d2e1683525 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -395,6 +395,8 @@ function gutenberg_experimental_global_styles_get_css_property( $style_property return 'font-size'; case 'lineHeight': return 'line-height'; + case 'textTransform': + return 'text-transform'; default: return $style_property; } @@ -413,6 +415,7 @@ function gutenberg_experimental_global_styles_get_style_property() { 'color' => array( 'color', 'text' ), 'fontSize' => array( 'typography', 'fontSize' ), 'lineHeight' => array( 'typography', 'lineHeight' ), + 'textTransform' => array( 'typography', 'textTransform' ), ); } @@ -429,6 +432,7 @@ function gutenberg_experimental_global_styles_get_support_keys() { 'color' => array( 'color' ), 'fontSize' => array( 'fontSize' ), 'lineHeight' => array( 'lineHeight' ), + 'textTransform' => array( '__experimentalTextTransform' ), ); } @@ -439,18 +443,22 @@ function gutenberg_experimental_global_styles_get_support_keys() { */ function gutenberg_experimental_global_styles_get_presets_structure() { return array( - 'color' => array( + 'color' => array( 'path' => array( 'color', 'palette' ), 'key' => 'color', ), - 'gradient' => array( + 'gradient' => array( 'path' => array( 'color', 'gradients' ), 'key' => 'gradient', ), - 'fontSize' => array( + 'fontSize' => array( 'path' => array( 'typography', 'fontSizes' ), 'key' => 'size', ), + 'textTransform' => array( + 'path' => array( 'typography', 'textTransforms' ), + 'key' => 'slug', + ), ); } @@ -489,9 +497,10 @@ function gutenberg_experimental_global_styles_get_block_data() { 'global', array( 'supports' => array( - '__experimentalSelector' => ':root', - 'fontSize' => true, - 'color' => array( + '__experimentalSelector' => ':root', + 'fontSize' => true, + '__experimentalTextTransform' => true, + 'color' => array( 'linkColor' => true, 'gradients' => true, ), @@ -626,6 +635,11 @@ function gutenberg_experimental_global_styles_get_preset_classes( $selector, $se 'key' => 'size', 'property' => 'font-size', ), + 'text-transform' => array( + 'path' => array( 'typography', 'textTransforms' ), + 'key' => 'slug', + 'property' => 'text-transform', + ), ); foreach ( $classes_structure as $class_suffix => $preset_structure ) { diff --git a/packages/block-editor/src/components/text-decoration-and-transform/index.js b/packages/block-editor/src/components/text-decoration-and-transform/index.js new file mode 100644 index 00000000000000..38f589949627a7 --- /dev/null +++ b/packages/block-editor/src/components/text-decoration-and-transform/index.js @@ -0,0 +1,35 @@ +/** + * Internal dependencies + */ +import { + TextTransformEdit, + useIsTextTransformDisabled, +} from '../../hooks/text-transform'; + +/** + * Handles grouping related text decoration and text transform edit components + * so they can be laid out in a more flexible manner within the Typography + * InspectorControls panel. + * + * @param {Object} props Block props to be passed on to individual controls. + * @return {WPElement} Component containing text decoration or transform controls. + */ +export default function TextDecorationAndTransformEdit( props ) { + // Once text decorations block support is added additional checks will + // need to be added below and it's edit component included. + const transformAvailable = ! useIsTextTransformDisabled( props ); + + if ( ! transformAvailable ) { + return null; + } + + return ( + <> + { transformAvailable && ( +
+ { transformAvailable && } +
+ ) } + + ); +} diff --git a/packages/block-editor/src/components/text-decoration-and-transform/style.scss b/packages/block-editor/src/components/text-decoration-and-transform/style.scss new file mode 100644 index 00000000000000..3f62e923036a4b --- /dev/null +++ b/packages/block-editor/src/components/text-decoration-and-transform/style.scss @@ -0,0 +1,3 @@ +.block-editor-text-decoration-and-transform { + display: flex; +} diff --git a/packages/block-editor/src/components/text-transform-control/index.js b/packages/block-editor/src/components/text-transform-control/index.js new file mode 100644 index 00000000000000..545fed8407f398 --- /dev/null +++ b/packages/block-editor/src/components/text-transform-control/index.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { + formatCapitalize, + formatLowercase, + formatUppercase, +} from '@wordpress/icons'; + +/** + * Control to facilitate text transform selections. + * + * @param {Object} props Component props. + * @param {string} props.value Currently selected text transform. + * @param {Array} props.textTransforms Text transforms available for selection. + * @param {Function} props.onChange Handles change in text transform selection. + * @return {WPElement} Text transform control. + */ +export default function TextTransformControl( { + value: textTransform, + textTransforms, + onChange, +} ) { + /** + * Determines what the new text transform is as a result of a user + * interaction with the control. Then passes this on to the supplied + * onChange handler. + * + * @param {string} newTransform Slug for selected text transform. + */ + const handleOnChange = ( newTransform ) => { + // Check if we are toggling a transform off. + const transform = + textTransform === newTransform ? undefined : newTransform; + + // Ensure only defined text transforms are allowed. + const presetTransform = textTransforms.find( + ( { slug } ) => slug === transform + ); + + // Create string that will be turned into CSS custom property + const newTextTransform = presetTransform + ? `var:preset|text-transform|${ presetTransform.slug }` + : undefined; + + onChange( newTextTransform ); + }; + + // Text transform icons to use. + // Icons still to be created/designed. + const icons = { + capitalize: formatCapitalize, + lowercase: formatLowercase, + uppercase: formatUppercase, + }; + + return ( +
+ { __( 'Letter case' ) } +
+ { textTransforms.map( ( presetTransform ) => { + return ( + + ); + } ) } +
+
+ ); +} diff --git a/packages/block-editor/src/components/text-transform-control/style.scss b/packages/block-editor/src/components/text-transform-control/style.scss new file mode 100644 index 00000000000000..09280029a971aa --- /dev/null +++ b/packages/block-editor/src/components/text-transform-control/style.scss @@ -0,0 +1,18 @@ +.block-editor-text-transform-control { + flex: 0 0 50%; + + legend { + margin-bottom: 8px; + } + + .block-editor-text-transform-control__buttons { + display: inline-flex; + margin-bottom: 24px; + + .components-button.has-icon { + min-width: 24px; + padding: 0; + margin-right: 4px; + } + } +} diff --git a/packages/block-editor/src/hooks/text-transform.js b/packages/block-editor/src/hooks/text-transform.js new file mode 100644 index 00000000000000..8d7e807584cb09 --- /dev/null +++ b/packages/block-editor/src/hooks/text-transform.js @@ -0,0 +1,98 @@ +/** + * WordPress dependencies + */ +import { hasBlockSupport } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import TextTransformControl from '../components/text-transform-control'; +import useEditorFeature from '../components/use-editor-feature'; +import { cleanEmptyObject } from './utils'; + +/** + * Key within block settings' supports array indicating support for text + * transforms e.g. settings found in `block.json`. + */ +export const TEXT_TRANSFORM_SUPPORT_KEY = '__experimentalTextTransform'; + +/** + * Inspector control panel containing the text transform options. + * + * @param {Object} props Block properties. + * @return {WPElement} Text transform edit element. + */ +export function TextTransformEdit( props ) { + const { + attributes: { style }, + setAttributes, + } = props; + const textTransforms = useEditorFeature( 'typography.textTransforms' ); + const isDisabled = useIsTextTransformDisabled( props ); + + if ( isDisabled ) { + return null; + } + + const textTransform = getTextTransformFromAttributeValue( + textTransforms, + style?.typography?.textTransform + ); + + function onChange( newTransform ) { + setAttributes( { + style: cleanEmptyObject( { + ...style, + typography: { + ...style?.typography, + textTransform: newTransform, + }, + } ), + } ); + } + + return ( + + ); +} + +/** + * Checks if text-transform settings have been disabled. + * + * @param {string} name Name of the block. + * @return {boolean} Whether or not the setting is disabled. + */ +export function useIsTextTransformDisabled( { name: blockName } = {} ) { + const notSupported = ! hasBlockSupport( + blockName, + TEXT_TRANSFORM_SUPPORT_KEY + ); + const textTransforms = useEditorFeature( 'typography.textTransforms' ); + const hasTextTransforms = !! textTransforms?.length; + + return notSupported || ! hasTextTransforms; +} + +/** + * Extracts the current text transform selection, if available, from the CSS + * variable set as the `styles.typography.textTransform` attribute. + * + * @param {Array} textTransforms Available text transforms as defined in theme.json. + * @param {string} value Attribute value in `styles.typography.textTransform` + * @return {string} Actual text transform value + */ +const getTextTransformFromAttributeValue = ( textTransforms, value ) => { + const attributeParsed = /var:preset\|text-transform\|(.+)/.exec( value ); + + if ( attributeParsed && attributeParsed[ 1 ] ) { + return textTransforms.find( + ( { slug } ) => slug === attributeParsed[ 1 ] + )?.slug; + } + + return value; +}; diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index 49a281cb219eaf..3ddf6b011eca87 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -10,6 +10,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import InspectorControls from '../components/inspector-controls'; +import TextDecorationAndTransformEdit from '../components/text-decoration-and-transform'; import { LINE_HEIGHT_SUPPORT_KEY, @@ -21,10 +22,15 @@ import { FontSizeEdit, useIsFontSizeDisabled, } from './font-size'; +import { + TEXT_TRANSFORM_SUPPORT_KEY, + useIsTextTransformDisabled, +} from './text-transform'; export const TYPOGRAPHY_SUPPORT_KEYS = [ LINE_HEIGHT_SUPPORT_KEY, FONT_SIZE_SUPPORT_KEY, + TEXT_TRANSFORM_SUPPORT_KEY, ]; export function TypographyPanel( props ) { @@ -38,6 +44,7 @@ export function TypographyPanel( props ) { + ); @@ -56,6 +63,7 @@ function useIsTypographyDisabled( props = {} ) { const configs = [ useIsFontSizeDisabled( props ), useIsLineHeightDisabled( props ), + useIsTextTransformDisabled( props ), ]; return configs.filter( Boolean ).length === configs.length; diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 096eb3bacb7969..318c5b349b551f 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -43,6 +43,8 @@ @import "./components/rich-text/format-toolbar/style.scss"; @import "./components/rich-text/style.scss"; @import "./components/skip-to-selected-block/style.scss"; +@import "./components/text-decoration-and-transform/style.scss"; +@import "./components/text-transform-control/style.scss"; @import "./components/tool-selector/style.scss"; @import "./components/url-input/style.scss"; @import "./components/url-popover/style.scss"; diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index b22fdfad89a77a..50e16a5a04c3b3 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -50,6 +50,7 @@ "html": false, "inserter": true, "fontSize": true, + "__experimentalTextTransform": true, "color": { "textColor": true, "backgroundColor": true diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 18488d1db55f79..7b6df1970ef639 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -18,6 +18,7 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { backgroundColor: [ 'color', 'background' ], color: [ 'color', 'text' ], fontSize: [ 'typography', 'fontSize' ], + textTransform: [ 'typography', 'textTransform' ], lineHeight: [ 'typography', 'lineHeight' ], paddingBottom: [ 'spacing', 'padding', 'bottom' ], paddingLeft: [ 'spacing', 'padding', 'left' ], diff --git a/packages/edit-site/src/components/editor/utils.js b/packages/edit-site/src/components/editor/utils.js index 6ae765c15df48f..ae02c64567ac4c 100644 --- a/packages/edit-site/src/components/editor/utils.js +++ b/packages/edit-site/src/components/editor/utils.js @@ -13,6 +13,7 @@ export const PRESET_CATEGORIES = { color: { path: [ 'color', 'palette' ], key: 'color' }, gradient: { path: [ 'color', 'gradients' ], key: 'gradient' }, fontSize: { path: [ 'typography', 'fontSizes' ], key: 'size' }, + textTransform: { path: [ 'typography', 'textTransforms' ], key: 'slug' }, }; export const LINK_COLOR = '--wp--style--color--link'; export const LINK_COLOR_DECLARATION = `a { color: var(${ LINK_COLOR }, #00e); }`; diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index bdfbc5bbadabcb..9e6c62ccaba699 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -60,6 +60,7 @@ export { default as file } from './library/file'; export { default as flipHorizontal } from './library/flip-horizontal'; export { default as flipVertical } from './library/flip-vertical'; export { default as formatBold } from './library/format-bold'; +export { default as formatCapitalize } from './library/format-capitalize'; export { default as formatIndent } from './library/format-indent'; export { default as formatIndentRTL } from './library/format-indent-rtl'; export { default as formatItalic } from './library/format-italic'; @@ -68,10 +69,12 @@ export { default as formatListBulletsRTL } from './library/format-list-bullets-r export { default as formatListNumbered } from './library/format-list-numbered'; export { default as formatListNumberedRTL } from './library/format-list-numbered-rtl'; export { default as formatLtr } from './library/format-ltr'; +export { default as formatLowercase } from './library/format-lowercase'; export { default as formatOutdent } from './library/format-outdent'; export { default as formatOutdentRTL } from './library/format-outdent-rtl'; export { default as formatRtl } from './library/format-rtl'; export { default as formatStrikethrough } from './library/format-strikethrough'; +export { default as formatUppercase } from './library/format-uppercase'; export { default as fullscreen } from './library/fullscreen'; export { default as gallery } from './library/gallery'; export { default as globe } from './library/globe'; diff --git a/packages/icons/src/library/format-capitalize.js b/packages/icons/src/library/format-capitalize.js new file mode 100644 index 00000000000000..a41b335830bc4d --- /dev/null +++ b/packages/icons/src/library/format-capitalize.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const formatCapitalize = ( + + + +); + +export default formatCapitalize; diff --git a/packages/icons/src/library/format-lowercase.js b/packages/icons/src/library/format-lowercase.js new file mode 100644 index 00000000000000..04278051c0bf0a --- /dev/null +++ b/packages/icons/src/library/format-lowercase.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const formatLowercase = ( + + + +); + +export default formatLowercase; diff --git a/packages/icons/src/library/format-uppercase.js b/packages/icons/src/library/format-uppercase.js new file mode 100644 index 00000000000000..d7e3c08a55e1ad --- /dev/null +++ b/packages/icons/src/library/format-uppercase.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const formatUppercase = ( + + + +); + +export default formatUppercase;