From 1f78cd7c42ad0a8f40b8fbfe69ecbd2de8b1b4da Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 10 Feb 2021 16:28:22 -1000 Subject: [PATCH 1/8] Add swatch icon --- packages/icons/src/index.js | 1 + packages/icons/src/library/swatch.js | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 packages/icons/src/library/swatch.js diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 3fa3853b61568f..dfb71e79e6d6c2 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -187,6 +187,7 @@ export { default as shipping } from './library/shipping'; export { default as stretchWide } from './library/stretch-wide'; export { default as subscript } from './library/subscript'; export { default as superscript } from './library/superscript'; +export { default as swatch } from './library/swatch'; export { default as tableColumnAfter } from './library/table-column-after'; export { default as tableColumnBefore } from './library/table-column-before'; export { default as tableColumnDelete } from './library/table-column-delete'; diff --git a/packages/icons/src/library/swatch.js b/packages/icons/src/library/swatch.js new file mode 100644 index 00000000000000..1d9a63e84838ef --- /dev/null +++ b/packages/icons/src/library/swatch.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/primitives'; + +const swatch = ( + + + +); + +export default swatch; From 29e8d718a1948f7d55ed0f4ca875d1e05afa1a5a Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 10 Feb 2021 16:33:59 -1000 Subject: [PATCH 2/8] Add duotone to theme.json --- docs/how-to-guides/themes/theme-json.md | 1 + lib/class-wp-theme-json.php | 1 + lib/experimental-default-theme.json | 42 +++++++++++++++++++ lib/experimental-i18n-theme.json | 5 +++ phpunit/class-wp-theme-json-resolver-test.php | 5 +++ 5 files changed, 54 insertions(+) diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md index d1f0df50ed148d..f2465e4df10827 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/theme-json.md @@ -183,6 +183,7 @@ The settings section has the following structure and default values: "color": { "custom": true, /* false to opt-out, as in add_theme_support('disable-custom-colors') */ "customGradient": true, /* false to opt-out, as in add_theme_support('disable-custom-gradients') */ + "duotone": [ ... ], /* duotone presets, a list of { "colors": [ "#000", "#FFF" ], "slug": "black-and-white", "name": "Black and White" } */ "gradients": [ ... ], /* gradient presets, as in add_theme_support('editor-gradient-presets', ... ) */ "link": false, /* true to opt-in, as in add_theme_support('experimental-link-color') */ "palette": [ ... ], /* color presets, as in add_theme_support('editor-color-palette', ... ) */ diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php index e81472dbd23b17..76e26cd1d4fffe 100644 --- a/lib/class-wp-theme-json.php +++ b/lib/class-wp-theme-json.php @@ -108,6 +108,7 @@ class WP_Theme_JSON { 'gradients' => null, 'link' => null, 'palette' => null, + 'duotone' => null, ), 'spacing' => array( 'customPadding' => null, diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json index 5eebd221e8a2c6..447a768dcd5bdf 100644 --- a/lib/experimental-default-theme.json +++ b/lib/experimental-default-theme.json @@ -126,6 +126,48 @@ "slug": "midnight" } ], + "duotone": [ + { + "name": "Dark grayscale" , + "colors": [ "#000000", "#7f7f7f" ], + "slug": "dark-grayscale" + }, + { + "name": "Grayscale" , + "colors": [ "#000000", "#ffffff" ], + "slug": "grayscale" + }, + { + "name": "Purple and yellow" , + "colors": [ "#8c00b7", "#fcff41" ], + "slug": "purple-yellow" + }, + { + "name": "Blue and red" , + "colors": [ "#000097", "#ff4747" ], + "slug": "blue-red" + }, + { + "name": "Midnight" , + "colors": [ "#000000", "#00a5ff" ], + "slug": "midnight" + }, + { + "name": "Magenta and yellow" , + "colors": [ "#c7005a", "#fff278" ], + "slug": "magenta-yellow" + }, + { + "name": "Purple and green" , + "colors": [ "#a60072", "#67ff66" ], + "slug": "purple-green" + }, + { + "name": "Blue and orange" , + "colors": [ "#1900d8", "#ffa96b" ], + "slug": "blue-orange" + } + ], "custom": true, "link": false, "customGradient": true diff --git a/lib/experimental-i18n-theme.json b/lib/experimental-i18n-theme.json index 94889f733a3310..a9dc4c429ea47a 100644 --- a/lib/experimental-i18n-theme.json +++ b/lib/experimental-i18n-theme.json @@ -43,6 +43,11 @@ { "name": "Gradient name" } + ], + "duotone": [ + { + "name": "Duotone name" + } ] } } diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index 9e9d298ab66662..f840f07f4a1647 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -84,6 +84,11 @@ function test_fields_are_extracted() { 'key' => 'name', 'context' => 'Gradient name', ), + array( + 'path' => array( 'settings', '*', 'color', 'duotone' ), + 'key' => 'name', + 'context' => 'Duotone name', + ), array( 'path' => array( 'customTemplates' ), 'key' => 'title', From e98e138e3809deb8757152154ac55f72bd6d5a92 Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Wed, 10 Feb 2021 16:39:38 -1000 Subject: [PATCH 3/8] Add duotone control --- .../duotone-control/duotone-picker-popover.js | 32 +++++++ .../src/components/duotone-control/index.js | 50 ++++++++++ .../src/components/duotone-control/style.scss | 26 ++++++ packages/block-editor/src/components/index.js | 1 + packages/block-editor/src/style.scss | 1 + .../components/src/color-list-picker/index.js | 56 +++++++++++ .../src/color-list-picker/style.scss | 4 + .../src/custom-gradient-bar/control-points.js | 37 +++++--- .../src/custom-gradient-bar/index.js | 40 ++++---- .../src/duotone-picker/custom-duotone-bar.js | 32 +++++++ .../src/duotone-picker/duotone-picker.js | 92 +++++++++++++++++++ .../src/duotone-picker/duotone-swatch.js | 16 ++++ .../components/src/duotone-picker/index.js | 2 + .../components/src/duotone-picker/utils.js | 84 +++++++++++++++++ packages/components/src/index.js | 1 + packages/components/src/style.scss | 2 + packages/components/src/swatch/index.js | 19 ++++ packages/components/src/swatch/style.scss | 21 +++++ 18 files changed, 484 insertions(+), 32 deletions(-) create mode 100644 packages/block-editor/src/components/duotone-control/duotone-picker-popover.js create mode 100644 packages/block-editor/src/components/duotone-control/index.js create mode 100644 packages/block-editor/src/components/duotone-control/style.scss create mode 100644 packages/components/src/color-list-picker/index.js create mode 100644 packages/components/src/color-list-picker/style.scss create mode 100644 packages/components/src/duotone-picker/custom-duotone-bar.js create mode 100644 packages/components/src/duotone-picker/duotone-picker.js create mode 100644 packages/components/src/duotone-picker/duotone-swatch.js create mode 100644 packages/components/src/duotone-picker/index.js create mode 100644 packages/components/src/duotone-picker/utils.js create mode 100644 packages/components/src/swatch/index.js create mode 100644 packages/components/src/swatch/style.scss diff --git a/packages/block-editor/src/components/duotone-control/duotone-picker-popover.js b/packages/block-editor/src/components/duotone-control/duotone-picker-popover.js new file mode 100644 index 00000000000000..7926cd5f977a6e --- /dev/null +++ b/packages/block-editor/src/components/duotone-control/duotone-picker-popover.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { Popover, MenuGroup, DuotonePicker } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +function DuotonePickerPopover( { + value, + onChange, + onToggle, + duotonePalette, + colorPalette, +} ) { + return ( + + + + + + ); +} + +export default DuotonePickerPopover; diff --git a/packages/block-editor/src/components/duotone-control/index.js b/packages/block-editor/src/components/duotone-control/index.js new file mode 100644 index 00000000000000..c7f597c5612371 --- /dev/null +++ b/packages/block-editor/src/components/duotone-control/index.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { ToolbarButton, DuotoneSwatch } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { DOWN } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import DuotonePickerPopover from './duotone-picker-popover'; + +function DuotoneControl( { colorPalette, duotonePalette, value, onChange } ) { + const [ isOpen, setIsOpen ] = useState( false ); + const onToggle = () => { + setIsOpen( ( prev ) => ! prev ); + }; + const openOnArrowDown = ( event ) => { + if ( ! isOpen && event.keyCode === DOWN ) { + event.preventDefault(); + event.stopPropagation(); + onToggle(); + } + }; + return ( + <> + } + /> + { isOpen && ( + + ) } + + ); +} + +export default DuotoneControl; diff --git a/packages/block-editor/src/components/duotone-control/style.scss b/packages/block-editor/src/components/duotone-control/style.scss new file mode 100644 index 00000000000000..133f0e0b8a7489 --- /dev/null +++ b/packages/block-editor/src/components/duotone-control/style.scss @@ -0,0 +1,26 @@ +.block-editor-duotone-control__popover { + .components-popover__content { + border: $border-width solid $gray-900; + min-width: 214px; + } + + .components-circular-option-picker { + padding: $grid-unit-15; + } + + .components-menu-group__label { + padding: $grid-unit-15 $grid-unit-15 0 $grid-unit-15; + width: 100%; + } +} + +.block-editor-duotone-control__popover > .components-popover__content { + // Matches 8 swatches in width. + width: 334px; +} + +// Better align the popover under the swatch. +// @todo: when the positioning for popovers gets refactored, this can presumably be removed. +.block-editor-duotone-control__popover:not([data-y-axis="middle"][data-x-axis="right"]) > .components-popover__content { + margin-left: -14px; +} diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index ac7471317acbe4..ad27f85c085d8e 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -36,6 +36,7 @@ export { default as ButtonBlockerAppender } from './button-block-appender'; export { default as ColorPalette } from './color-palette'; export { default as ColorPaletteControl } from './color-palette/control'; export { default as ContrastChecker } from './contrast-checker'; +export { default as __experimentalDuotoneControl } from './duotone-control'; export { default as __experimentalGradientPicker } from './gradient-picker'; export { default as __experimentalGradientPickerControl } from './gradient-picker/control'; export { default as __experimentalGradientPickerPanel } from './gradient-picker/panel'; diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index f80eeefd3be42d..f0b7bcaa363428 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -31,6 +31,7 @@ @import "./components/colors-gradients/style.scss"; @import "./components/contrast-checker/style.scss"; @import "./components/default-block-appender/style.scss"; +@import "./components/duotone-control/style.scss"; @import "./components/font-appearance-control/style.scss"; @import "./components/justify-content-control/style.scss"; @import "./components/link-control/style.scss"; diff --git a/packages/components/src/color-list-picker/index.js b/packages/components/src/color-list-picker/index.js new file mode 100644 index 00000000000000..7550cb8b12b1c7 --- /dev/null +++ b/packages/components/src/color-list-picker/index.js @@ -0,0 +1,56 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Button from '../button'; +import ColorPalette from '../color-palette'; +import Swatch from '../swatch'; + +function ColorOption( { label, value, colors, onChange } ) { + const [ isOpen, setIsOpen ] = useState( false ); + return ( + <> + + { isOpen && ( + + ) } + + ); +} + +function ColorListPicker( { colors, labels, value = [], onChange } ) { + return ( +
+ { labels.map( ( label, index ) => ( + { + const newColors = value.slice(); + newColors[ index ] = newColor; + onChange( newColors ); + } } + /> + ) ) } +
+ ); +} + +export default ColorListPicker; diff --git a/packages/components/src/color-list-picker/style.scss b/packages/components/src/color-list-picker/style.scss new file mode 100644 index 00000000000000..09a8e7348066cc --- /dev/null +++ b/packages/components/src/color-list-picker/style.scss @@ -0,0 +1,4 @@ +.components-color-list-picker, +.components-color-list-picker__swatch-button { + width: 100%; +} diff --git a/packages/components/src/custom-gradient-bar/control-points.js b/packages/components/src/custom-gradient-bar/control-points.js index d8d5ec16c2fa71..5bec42a2f2ae6c 100644 --- a/packages/components/src/custom-gradient-bar/control-points.js +++ b/packages/components/src/custom-gradient-bar/control-points.js @@ -109,6 +109,8 @@ function ControlPointButton( { } function ControlPoints( { + disableRemove, + disableAlpha, gradientPickerDomRef, ignoreMarkerPosition, value: controlPoints, @@ -223,6 +225,7 @@ function ControlPoints( { renderContent={ ( { onClose } ) => ( <> { onChange( @@ -234,21 +237,23 @@ function ControlPoints( { ); } } /> - + { ! disableRemove && ( + + ) } ) } popoverProps={ COLOR_POPOVER_PROPS } @@ -264,6 +269,7 @@ function InsertPoint( { onOpenInserter, onCloseInserter, insertPosition, + disableAlpha, } ) { const [ alreadyInsertedPoint, setAlreadyInsertedPoint ] = useState( false ); return ( @@ -297,6 +303,7 @@ function InsertPoint( { ) } renderContent={ () => ( { if ( ! alreadyInsertedPoint ) { onChange( diff --git a/packages/components/src/custom-gradient-bar/index.js b/packages/components/src/custom-gradient-bar/index.js index bd8b1922cdf6e6..823e3d00992a32 100644 --- a/packages/components/src/custom-gradient-bar/index.js +++ b/packages/components/src/custom-gradient-bar/index.js @@ -76,6 +76,8 @@ export default function CustomGradientBar( { hasGradient, value: controlPoints, onChange, + disableInserter = false, + disableAlpha = false, } ) { const gradientPickerDomRef = useRef(); @@ -129,24 +131,28 @@ export default function CustomGradientBar( { onMouseLeave={ onMouseLeave } >
- { ( isMovingInserter || isInsertingControlPoint ) && ( - { - gradientBarStateDispatch( { - type: 'OPEN_INSERTER', - } ); - } } - onCloseInserter={ () => { - gradientBarStateDispatch( { - type: 'CLOSE_INSERTER', - } ); - } } - /> - ) } + { ! disableInserter && + ( isMovingInserter || isInsertingControlPoint ) && ( + { + gradientBarStateDispatch( { + type: 'OPEN_INSERTER', + } ); + } } + onCloseInserter={ () => { + gradientBarStateDispatch( { + type: 'CLOSE_INSERTER', + } ); + } } + /> + ) } { + const newValue = getColorsFromColorStops( newColorStops ); + onChange( newValue ); + } } + /> + ); +} diff --git a/packages/components/src/duotone-picker/duotone-picker.js b/packages/components/src/duotone-picker/duotone-picker.js new file mode 100644 index 00000000000000..fb15f121625592 --- /dev/null +++ b/packages/components/src/duotone-picker/duotone-picker.js @@ -0,0 +1,92 @@ +/** + * External dependencies + */ +import { isEqual } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import ColorListPicker from '../color-list-picker'; +import CircularOptionPicker from '../circular-option-picker'; + +import CustomDuotoneBar from './custom-duotone-bar'; +import { getDefaultColors, getGradientFromCSSColors } from './utils'; + +function DuotonePicker( { colorPalette, duotonePalette, value, onChange } ) { + const [ defaultDark, defaultLight ] = useMemo( + () => getDefaultColors( colorPalette ), + [ colorPalette ] + ); + return ( + { + const style = { + background: getGradientFromCSSColors( colors, '135deg' ), + color: 'transparent', + }; + const tooltipText = + name ?? + sprintf( + // translators: %s: duotone code e.g: "dark-grayscale" or "7f7f7f-ffffff". + __( 'Duotone code: %s' ), + slug + ); + const label = name + ? sprintf( + // translators: %s: The name of the option e.g: "Dark grayscale". + __( 'Duotone: %s' ), + name + ) + : tooltipText; + const isSelected = isEqual( colors, value ); + + return ( + { + onChange( isSelected ? undefined : colors ); + } } + /> + ); + } ) } + actions={ + onChange( undefined ) } + > + { __( 'Clear' ) } + + } + > + + { + if ( ! newColors[ 0 ] ) { + newColors[ 0 ] = defaultDark; + } + if ( ! newColors[ 1 ] ) { + newColors[ 1 ] = defaultLight; + } + const newValue = + newColors.length >= 2 ? newColors : undefined; + onChange( newValue ); + } } + /> + + ); +} + +export default DuotonePicker; diff --git a/packages/components/src/duotone-picker/duotone-swatch.js b/packages/components/src/duotone-picker/duotone-swatch.js new file mode 100644 index 00000000000000..1ab35b53034c7d --- /dev/null +++ b/packages/components/src/duotone-picker/duotone-swatch.js @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import Swatch from '../swatch'; + +import { getGradientFromCSSColors } from './utils'; + +function DuotoneSwatch( { values } ) { + return ( + + ); +} + +export default DuotoneSwatch; diff --git a/packages/components/src/duotone-picker/index.js b/packages/components/src/duotone-picker/index.js new file mode 100644 index 00000000000000..0043b439029001 --- /dev/null +++ b/packages/components/src/duotone-picker/index.js @@ -0,0 +1,2 @@ +export { default as DuotonePicker } from './duotone-picker'; +export { default as DuotoneSwatch } from './duotone-swatch'; diff --git a/packages/components/src/duotone-picker/utils.js b/packages/components/src/duotone-picker/utils.js new file mode 100644 index 00000000000000..72c607072e8c6a --- /dev/null +++ b/packages/components/src/duotone-picker/utils.js @@ -0,0 +1,84 @@ +/** + * External dependencies + */ +import tinycolor from 'tinycolor2'; + +/** + * Object representation for a color. + * + * @typedef {Object} RGBColor + * @property {number} r Red component of the color in the range [0,1]. + * @property {number} g Green component of the color in the range [0,1]. + * @property {number} b Blue component of the color in the range [0,1]. + */ + +/** + * Calculate the brightest and darkest values from a color palette. + * + * @param {Object[]} palette Color palette for the theme. + * + * @return {string[]} Tuple of the darkest color and brightest color. + */ +export function getDefaultColors( palette ) { + // A default dark and light color are required. + if ( ! palette || palette.length < 2 ) return [ '#000', '#fff' ]; + + return palette + .map( ( { color } ) => ( { + color, + brightness: tinycolor( color ).getBrightness() / 255, + } ) ) + .reduce( + ( [ min, max ], current ) => { + return [ + current.brightness <= min.brightness ? current : min, + current.brightness >= max.brightness ? current : max, + ]; + }, + [ { brightness: 1 }, { brightness: 0 } ] + ) + .map( ( { color } ) => color ); +} + +/** + * Generate a duotone gradient from a list of colors. + * + * @param {string[]} colors CSS color strings. + * @param {string} angle CSS gradient angle. + * + * @return {string} CSS gradient string for the duotone swatch. + */ +export function getGradientFromCSSColors( colors = [], angle = '90deg' ) { + const l = 100 / colors.length; + + const stops = colors + .map( ( c, i ) => `${ c } ${ i * l }%, ${ c } ${ ( i + 1 ) * l }%` ) + .join( ', ' ); + + return `linear-gradient( ${ angle }, ${ stops } )`; +} + +/** + * Convert a color array to an array of color stops. + * + * @param {string[]} colors CSS colors array + * + * @return {Object[]} Color stop information. + */ +export function getColorStopsFromColors( colors ) { + return colors.map( ( color, i ) => ( { + position: ( i * 100 ) / ( colors.length - 1 ), + color, + } ) ); +} + +/** + * Convert a color stop array to an array colors. + * + * @param {Object[]} colorStops Color stop information. + * + * @return {string[]} CSS colors array. + */ +export function getColorsFromColorStops( colorStops = [] ) { + return colorStops.map( ( { color } ) => color ); +} diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 0c67e9330f035e..efbfcce1723598 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -59,6 +59,7 @@ export { } from './drop-zone/provider'; export { default as Dropdown } from './dropdown'; export { default as DropdownMenu } from './dropdown-menu'; +export { DuotoneSwatch, DuotonePicker } from './duotone-picker'; export { default as ExternalLink } from './external-link'; export { default as Flex } from './flex'; export { default as FlexBlock } from './flex/block'; diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index a7b92c1192b13d..13c2049b894bf5 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -8,6 +8,7 @@ @import "./color-indicator/style.scss"; @import "./color-picker/style.scss"; @import "./combobox-control/style.scss"; +@import "./color-list-picker/style.scss"; @import "./custom-gradient-picker/style.scss"; @import "./custom-select-control/style.scss"; @import "./date-time/style.scss"; @@ -36,6 +37,7 @@ @import "./scroll-lock/style.scss"; @import "./select-control/style.scss"; @import "./snackbar/style.scss"; +@import "./swatch/style.scss"; @import "./tab-panel/style.scss"; @import "./text-control/style.scss"; @import "./tip/style.scss"; diff --git a/packages/components/src/swatch/index.js b/packages/components/src/swatch/index.js new file mode 100644 index 00000000000000..1d86ace0ec7736 --- /dev/null +++ b/packages/components/src/swatch/index.js @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { swatch } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import Icon from '../icon'; + +function Swatch( { fill } ) { + return fill ? ( + + ) : ( + + ); +} + +export default Swatch; diff --git a/packages/components/src/swatch/style.scss b/packages/components/src/swatch/style.scss new file mode 100644 index 00000000000000..b18b361f39d652 --- /dev/null +++ b/packages/components/src/swatch/style.scss @@ -0,0 +1,21 @@ +.components-swatch { + width: 18px; + height: 18px; + border-radius: 50%; + color: transparent; + background: transparent; + + // Regular border doesn't seem to work in the toolbar button, but pseudo-selector border does. + &::after { + content: ""; + display: block; + width: 100%; + height: 100%; + border: $border-width solid rgba(0, 0, 0, 0.2); + border-radius: 50%; + } +} + +.components-button.has-icon.has-text .components-swatch { + margin-right: $grid-unit; +} From 2299f9bf1157da7a68b0dce115220808cb76f42a Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Thu, 11 Feb 2021 09:36:31 -1000 Subject: [PATCH 4/8] Add block supports duotone --- .../block-api/block-supports.md | 228 +++++++++-- lib/block-supports/duotone.php | 364 ++++++++++++++++++ lib/load.php | 1 + packages/block-editor/src/hooks/duotone.js | 255 ++++++++++++ packages/block-editor/src/hooks/index.js | 1 + 5 files changed, 819 insertions(+), 30 deletions(-) create mode 100644 lib/block-supports/duotone.php create mode 100644 packages/block-editor/src/hooks/duotone.js diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index 565b1f0bbbd8e2..ff3f50cad8d858 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -86,12 +86,11 @@ supports: { - Default value: null - Subproperties: - `background`: type `boolean`, default value `true` + - `duotone`: type `string`, default value undefined - `gradients`: type `boolean`, default value `false` - `text`: type `boolean`, default value `true` -This value signals that a block supports some of the CSS style properties related to color. When it does, the block editor will show UI controls for the user to set their values. - -The controls for background and text will source their colors from the `editor-color-palette` [theme support](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-color-palettes), while the gradient's from `editor-gradient-presets` [theme support](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-gradient-presets). +This value signals that a block supports some of the properties related to color. When it does, the block editor will show UI controls for the user to set their values. Note that the `text` and `background` keys have a default value of `true`, so if the `color` property is present they'll also be considered enabled: @@ -115,58 +114,227 @@ supports: { } ``` -When the block has support for a specific color property, the attributes definition is extended to include some attributes. +### color.background + +This property adds UI controls which allow the user to apply a solid background color to a block. -- `style`: attribute of `object` type with no default assigned. This is added when any of support color properties are declared. It stores the custom values set by the user. The block can apply a default style by specifying its own `style` attribute with a default e.g.: +When color support is declared, this property is enabled by default (along with text), so simply setting color will enable background color. ```js -attributes: { - style: { - type: 'object', - default: { - color: { - background: 'value', - gradient: 'value', - text: 'value' - } - } +supports: { + color: true // Enable both background and text +} +``` + +To disable background support while keeping other color supports enabled, set to `false`. + +```js +supports: { + color: { + // Disable background support. Text color support is still enabled. + background: false } } ``` -- When `background` support is declared: it'll be added a new `backgroundColor` attribute of type `string` with no default assigned. It stores the preset values set by the user. The block can apply a default background color by specifying its own attribute with a default e.g.: +When the block declares support for `color.background`, the attributes definition is extended to include two new attributes: `backgroundColor` and `style`: + +- `backgroundColor`: attribute of `string` type with no default assigned. + + When a user chooses from the list of preset background colors, the preset slug is stored in the `backgroundColor` attribute. + + Background color presets are sourced from the `editor-color-palette` [theme support](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-color-palettes). + + The block can apply a default preset background color by specifying its own attribute with a default e.g.: + + ```js + attributes: { + backgroundColor: { + type: 'string', + default: 'some-preset-background-slug', + } + } + ``` + +- `style`: attribute of `object` type with no default assigned. + + When a custom background color is selected (i.e. using the custom color picker), the custom color value is stored in the `style.color.background` attribute. + + The block can apply a default custom background color by specifying its own attribute with a default e.g.: + + ```js + attributes: { + style: { + type: 'object', + default: { + color: { + background: '#aabbcc', + } + } + } + } + ``` + +### color.__experimentalDuotone + +This property adds UI controls which allow to apply a duotone filter to a block or part of a block. + +The parent selector is automatically added much like nesting in Sass/SCSS (however, the `&` selector is not supported). ```js -attributes: { - backgroundColor: { - type: 'string', - default: 'some-value', +supports: { + color: { + // Apply the filter to the same selector in both edit and save. + __experimentalDuotone: '> .duotone-img, > .duotone-video', + + // Default values must be disabled if you don't want to use them with duotone. + background: false, + text: false } } ``` -- When `gradients` support is declared: it'll be added a new `gradient` attribute of type `string` with no default assigned. It stores the preset values set by the user. The block can apply a default text color by specifying its own attribute with a default e.g.: +Duotone presets are sourced from `color.duotone` in [theme.json](https://developer.wordpress.org/block-editor/developers/themes/theme-json/). + +When the block declares support for `color.duotone`, the attributes definition is extended to include the attribute `style`: + +- `style`: attribute of `object` type with no default assigned. + + The block can apply a default duotone color by specifying its own attribute with a default e.g.: + + ```js + attributes: { + style: { + type: 'object', + default: { + color: { + duotone: [ + '#FFF', + '#000 + ] + } + } + } + } + ``` + +### color.gradients + +This property adds UI controls which allow the user to apply a gradient background to a block. ```js -attributes: { - gradient: { - type: 'string', - default: 'some-value', +supports: { + color: { + gradient: true, + + // Default values must be disabled if you don't want to use them with gradient. + background: false, + text: false } } ``` -- When `text` support is declared: it'll be added a new `textColor` attribute of type `string` with no default assigned. It stores the preset values set by the user. The block can apply a default text color by specifying its own attribute with a default e.g.: +Gradient presets are sourced from `editor-gradient-presets` [theme support](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-gradient-presets). + + +When the block declares support for `color.gradient`, the attributes definition is extended to include two new attributes: `gradient` and `style`: + +- `gradient`: attribute of `string` type with no default assigned. + + When a user chooses from the list of preset gradients, the preset slug is stored in the `gradient` attribute. + + The block can apply a default preset gradient by specifying its own attribute with a default e.g.: + + ```js + attributes: { + gradient: { + type: 'string', + default: 'some-preset-gradient-slug', + } + } + ``` + +- `style`: attribute of `object` type with no default assigned. + + When a custom gradient is selected (i.e. using the custom gradient picker), the custom gradient value is stored in the `style.color.gradient` attribute. + + The block can apply a default custom gradient by specifying its own attribute with a default e.g.: + + ```js + attributes: { + style: { + type: 'object', + default: { + color: { + background: 'linear-gradient(135deg,rgb(170,187,204) 0%,rgb(17,34,51) 100%)', + } + } + } + } + ``` + +### color.text + +This property adds block controls which allow the user to set text color in a block. + +When color support is declared, this property is enabled by default (along with background), so simply setting color will enable text color. ```js -attributes: { - textColor: { - type: 'string', - default: 'some-value', +supports: { + color: true // Enable both text and background +} +``` + +To disable text color support while keeping other color supports enabled, set to `false`. + +```js +supports: { + color: { + // Disable text color support. Background support is still enabled. + text: false } } ``` +Text color presets are sourced from the `editor-color-palette` [theme support](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-color-palettes). + + +When the block declares support for `color.text`, the attributes definition is extended to include two new attributes: `textColor` and `style`: + +- `textColor`: attribute of `string` type with no default assigned. + + When a user chooses from the list of preset text colors, the preset slug is stored in the `textColor` attribute. + + The block can apply a default preset text color by specifying its own attribute with a default e.g.: + + ```js + attributes: { + textColor: { + type: 'string', + default: 'some-preset-text-color-slug', + } + } + ``` + +- `style`: attribute of `object` type with no default assigned. + + When a custom text color is selected (i.e. using the custom color picker), the custom color value is stored in the `style.color.text` attribute. + + The block can apply a default custom text color by specifying its own attribute with a default e.g.: + + ```js + attributes: { + style: { + type: 'object', + default: { + color: { + text: '#aabbcc', + } + } + } + } + ``` + ## customClassName - Type: `boolean` diff --git a/lib/block-supports/duotone.php b/lib/block-supports/duotone.php new file mode 100644 index 00000000000000..f3b4743832f0ae --- /dev/null +++ b/lib/block-supports/duotone.php @@ -0,0 +1,364 @@ + gutenberg_tinycolor_bound01( $rgb_color['r'], 255 ) * 255, + 'g' => gutenberg_tinycolor_bound01( $rgb_color['g'], 255 ) * 255, + 'b' => gutenberg_tinycolor_bound01( $rgb_color['b'], 255 ) * 255, + ); +} + +/** + * Helper function for hsl to rgb conversion. + * + * @see https://github.com/bgrins/TinyColor + * + * @param float $p first component. + * @param float $q second component. + * @param float $t third component. + * @return float R, G, or B component. + */ +function gutenberg_tinycolor_hue_to_rgb( $p, $q, $t ) { + if ( $t < 0 ) { + $t += 1; + } + if ( $t > 1 ) { + $t -= 1; + } + if ( $t < 1 / 6 ) { + return $p + ( $q - $p ) * 6 * $t; + } + if ( $t < 1 / 2 ) { + return $q; + } + if ( $t < 2 / 3 ) { + return $p + ( $q - $p ) * ( 2 / 3 - $t ) * 6; + } + return $p; +} + +/** + * Convert an HSL object to an RGB object with converted and rounded values. + * + * @see https://github.com/bgrins/TinyColor + * + * @param array $hsl_color HSL object. + * @return array Rounded and converted RGB object. + */ +function gutenberg_tinycolor_hsl_to_rgb( $hsl_color ) { + $h = gutenberg_tinycolor_bound01( $hsl_color['h'], 360 ); + $s = gutenberg_tinycolor_bound01( $hsl_color['s'], 100 ); + $l = gutenberg_tinycolor_bound01( $hsl_color['l'], 100 ); + + if ( 0 === $s ) { + // Achromatic. + $r = $l; + $g = $l; + $b = $l; + } else { + $q = $l < 0.5 ? $l * ( 1 + $s ) : $l + $s - $l * $s; + $p = 2 * $l - $q; + $r = gutenberg_tinycolor_hue_to_rgb( $p, $q, $h + 1 / 3 ); + $g = gutenberg_tinycolor_hue_to_rgb( $p, $q, $h ); + $b = gutenberg_tinycolor_hue_to_rgb( $p, $q, $h - 1 / 3 ); + } + + return array( + 'r' => $r * 255, + 'g' => $g * 255, + 'b' => $b * 255, + ); +} + +/** + * Parses hex, hsl, and rgb CSS strings using the same regex as tinycolor v1.4.2 + * used in the JavaScript. Only colors output from react-color are implemented + * and the alpha value is ignored as it is not used in duotone. + * + * @see https://github.com/bgrins/TinyColor + * @see https://github.com/casesandberg/react-color/ + * + * @param string $color_str CSS color string. + * @return array RGB object. + */ +function gutenberg_tinycolor_string_to_rgb( $color_str ) { + $color_str = strtolower( trim( $color_str ) ); + + $css_integer = '[-\\+]?\\d+%?'; + $css_number = '[-\\+]?\\d*\\.\\d+%?'; + + $css_unit = '(?:' . $css_number . ')|(?:' . $css_integer . ')'; + + $permissive_match3 = '[\\s|\\(]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')\\s*\\)?'; + $permissive_match4 = '[\\s|\\(]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')[,|\\s]+(' . $css_unit . ')\\s*\\)?'; + + $rgb_regexp = '/^rgb' . $permissive_match3 . '$/'; + if ( preg_match( $rgb_regexp, $color_str, $match ) ) { + return gutenberg_tinycolor_rgb_to_rgb( + array( + 'r' => $match[1], + 'g' => $match[2], + 'b' => $match[3], + ) + ); + } + + $rgba_regexp = '/^rgba' . $permissive_match4 . '$/'; + if ( preg_match( $rgba_regexp, $color_str, $match ) ) { + return gutenberg_tinycolor_rgb_to_rgb( + array( + 'r' => $match[1], + 'g' => $match[2], + 'b' => $match[3], + ) + ); + } + + $hsl_regexp = '/^hsl' . $permissive_match3 . '$/'; + if ( preg_match( $hsl_regexp, $color_str, $match ) ) { + return gutenberg_tinycolor_hsl_to_rgb( + array( + 'h' => $match[1], + 's' => $match[2], + 'l' => $match[3], + ) + ); + } + + $hsla_regexp = '/^hsla' . $permissive_match4 . '$/'; + if ( preg_match( $hsla_regexp, $color_str, $match ) ) { + return gutenberg_tinycolor_hsl_to_rgb( + array( + 'h' => $match[1], + 's' => $match[2], + 'l' => $match[3], + ) + ); + } + + $hex8_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/'; + if ( preg_match( $hex8_regexp, $color_str, $match ) ) { + return gutenberg_tinycolor_rgb_to_rgb( + array( + 'r' => base_convert( $match[1], 16, 10 ), + 'g' => base_convert( $match[2], 16, 10 ), + 'b' => base_convert( $match[3], 16, 10 ), + ) + ); + } + + $hex6_regexp = '/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/'; + if ( preg_match( $hex6_regexp, $color_str, $match ) ) { + return gutenberg_tinycolor_rgb_to_rgb( + array( + 'r' => base_convert( $match[1], 16, 10 ), + 'g' => base_convert( $match[2], 16, 10 ), + 'b' => base_convert( $match[3], 16, 10 ), + ) + ); + } + + $hex4_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/'; + if ( preg_match( $hex4_regexp, $color_str, $match ) ) { + return gutenberg_tinycolor_rgb_to_rgb( + array( + 'r' => base_convert( $match[1] . $match[1], 16, 10 ), + 'g' => base_convert( $match[2] . $match[2], 16, 10 ), + 'b' => base_convert( $match[3] . $match[3], 16, 10 ), + ) + ); + } + + $hex3_regexp = '/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/'; + if ( preg_match( $hex3_regexp, $color_str, $match ) ) { + return gutenberg_tinycolor_rgb_to_rgb( + array( + 'r' => base_convert( $match[1] . $match[1], 16, 10 ), + 'g' => base_convert( $match[2] . $match[2], 16, 10 ), + 'b' => base_convert( $match[3] . $match[3], 16, 10 ), + ) + ); + } +} + + +/** + * Registers the style and colors block attributes for block types that support it. + * + * @param WP_Block_Type $block_type Block Type. + */ +function gutenberg_register_duotone_support( $block_type ) { + $has_duotone_support = false; + if ( property_exists( $block_type, 'supports' ) ) { + $has_duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); + } + + if ( $has_duotone_support ) { + if ( ! $block_type->attributes ) { + $block_type->attributes = array(); + } + + if ( ! array_key_exists( 'style', $block_type->attributes ) ) { + $block_type->attributes['style'] = array( + 'type' => 'object', + ); + } + } +} + +/** + * Render out the duotone stylesheet and SVG. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_render_duotone_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + + $duotone_support = false; + if ( $block_type && property_exists( $block_type, 'supports' ) ) { + $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); + } + + $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] ); + + if ( + ! $duotone_support || + ! $has_duotone_attribute + ) { + return $block_content; + } + + $duotone_colors = $block['attrs']['style']['color']['duotone']; + + $duotone_values = array( + 'r' => array(), + 'g' => array(), + 'b' => array(), + ); + foreach ( $duotone_colors as $color_str ) { + $color = gutenberg_tinycolor_string_to_rgb( $color_str ); + + $duotone_values['r'][] = $color['r'] / 255; + $duotone_values['g'][] = $color['g'] / 255; + $duotone_values['b'][] = $color['b'] / 255; + } + + $duotone_id = 'wp-duotone-filter-' . uniqid(); + + $selectors = explode( ',', $duotone_support ); + $selectors_scoped = array_map( + function ( $selector ) use ( $duotone_id ) { + return '.' . $duotone_id . ' ' . trim( $selector ); + }, + $selectors + ); + $selectors_group = implode( ', ', $selectors_scoped ); + + ob_start(); + + ?> + + + + + + + + values=".299 .587 .114 0 0 + .299 .587 .114 0 0 + .299 .587 .114 0 0 + 0 0 0 1 0" + + /> + + + + + + + + + + register( + 'duotone', + array( + 'register_attribute' => 'gutenberg_register_duotone_support', + ) +); +add_filter( 'render_block', 'gutenberg_render_duotone_support', 10, 2 ); diff --git a/lib/load.php b/lib/load.php index 2deda54b6646b8..dcf9539a1e526e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -126,3 +126,4 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/border.php'; require __DIR__ . '/block-supports/layout.php'; require __DIR__ . '/block-supports/padding.php'; +require __DIR__ . '/block-supports/duotone.php'; diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js new file mode 100644 index 00000000000000..dd2d27a784d7b2 --- /dev/null +++ b/packages/block-editor/src/hooks/duotone.js @@ -0,0 +1,255 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import tinycolor from 'tinycolor2'; + +/** + * WordPress dependencies + */ +import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; +import { SVG } from '@wordpress/components'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { addFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { + BlockControls, + __experimentalDuotoneControl as DuotoneControl, + __experimentalUseEditorFeature as useEditorFeature, +} from '../components'; + +/** + * Convert a list of colors to an object of R, G, and B values. + * + * @param {string[]} colors Array of RBG color strings. + * + * @return {Object} R, G, and B values. + */ +export function getValuesFromColors( colors = [] ) { + const values = { r: [], g: [], b: [] }; + + colors.forEach( ( color ) => { + // Access values directly to skip extra rounding that tinycolor.toRgb() does. + const tcolor = tinycolor( color ); + values.r.push( tcolor._r / 255 ); + values.g.push( tcolor._g / 255 ); + values.b.push( tcolor._b / 255 ); + } ); + + return values; +} + +/** + * Values for the SVG `feComponentTransfer`. + * + * @typedef Values {Object} + * @property {number[]} r Red values. + * @property {number[]} g Green values. + * @property {number[]} b Blue values. + */ + +/** + * SVG and stylesheet needed for rendering the duotone filter. + * + * @param {Object} props Duotone props. + * @param {string} props.selector Selector to apply the filter to. + * @param {string} props.id Unique id for this duotone filter. + * @param {Values} props.values R, G, and B values to filter with. + * @return {WPElement} Duotone element. + */ +function DuotoneFilter( { selector, id, values } ) { + const stylesheet = ` +${ selector } { + filter: url( #${ id } ); +} +`; + + return ( + <> + + + + + + + + + + + + +