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