diff --git a/package-lock.json b/package-lock.json
index 594354ed0d4a0..c9b3884216339 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4857,6 +4857,7 @@
"classnames": "^2.2.5",
"clipboard": "^2.0.1",
"dom-scroll-into-view": "^1.2.1",
+ "gradient-parser": "^0.1.5",
"lodash": "^4.17.15",
"memize": "^1.0.5",
"moment": "^2.22.1",
@@ -13831,6 +13832,11 @@
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true
},
+ "gradient-parser": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/gradient-parser/-/gradient-parser-0.1.5.tgz",
+ "integrity": "sha1-DH4heVWeXOfY1x9EI6+TcQCyJIw="
+ },
"grapheme-breaker": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/grapheme-breaker/-/grapheme-breaker-0.3.2.tgz",
diff --git a/packages/block-editor/src/components/gradient-picker/control.js b/packages/block-editor/src/components/gradient-picker/control.js
index 73609ffdf17e2..4b850fa883860 100644
--- a/packages/block-editor/src/components/gradient-picker/control.js
+++ b/packages/block-editor/src/components/gradient-picker/control.js
@@ -7,7 +7,7 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { BaseControl } from '@wordpress/components';
+import { BaseControl, CustomGradientPicker } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
@@ -15,7 +15,7 @@ import { __ } from '@wordpress/i18n';
*/
import GradientPicker from './';
-export default function( { className, ...props } ) {
+export default function( { className, value, onChange, ...props } ) {
return (
+
);
}
diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js
index 06eabc9f6f11f..c7151f726872d 100644
--- a/packages/block-editor/src/store/defaults.js
+++ b/packages/block-editor/src/store/defaults.js
@@ -175,56 +175,56 @@ export const SETTINGS_DEFAULTS = {
},
{
name: __( 'Very light gray to cyan bluish gray' ),
- gradient: 'linear-gradient(135deg, rgb(238, 238, 238) 0%, rgb(169, 184, 195)',
+ gradient: 'linear-gradient(135deg, rgb(238, 238, 238) 0%, rgb(169, 184, 195) 100%)',
},
// The following use new, customized colors.
{
name: __( 'Cool to warm spectrum' ),
- gradient: 'linear-gradient(135deg, rgb(74, 234, 220), rgb(151, 120, 209), rgb(207, 42, 186), rgb(238, 44, 130), rgb(251, 105, 98),rgb(254, 248, 76)',
+ gradient: 'linear-gradient(135deg, rgb(74, 234, 220) 0%, rgb(151, 120, 209) 20%, rgb(207, 42, 186) 40%, rgb(238, 44, 130) 60%, rgb(251, 105, 98) 80%, rgb(254, 248, 76) 100% )',
},
{
name: __( 'Blush light purple' ),
- gradient: 'linear-gradient(135deg, rgb(255, 206, 236), rgb(152, 150, 240)',
+ gradient: 'linear-gradient(135deg, rgb(255, 206, 236) 0%, rgb(152, 150, 240) 100% )',
},
{
name: __( 'Blush bordeaux' ),
- gradient: 'linear-gradient(135deg, rgb(254, 205, 165), rgb(254, 45, 45), rgb(107, 0, 62)',
+ gradient: 'linear-gradient(135deg, rgb(254, 205, 165) 0%, rgb(254, 45, 45) 50%, rgb(107, 0, 62) 100% )',
},
{
name: __( 'Purple crush' ),
- gradient: 'linear-gradient(135deg, rgb(52, 226, 228), rgb(71, 33, 251), rgb(171, 29, 254)',
+ gradient: 'linear-gradient(135deg, rgb(52, 226, 228) 0%, rgb(71, 33, 251) 50%, rgb(171, 29, 254) 100% )',
},
{
name: __( 'Luminous dusk' ),
- gradient: 'linear-gradient(135deg, rgb(255, 203, 112), rgb(199, 81, 192), rgb(65, 88, 208)',
+ gradient: 'linear-gradient(135deg, rgb(255, 203, 112) 0%, rgb(199, 81, 192) 50%, rgb(65, 88, 208) 100% )',
},
{
name: __( 'Hazy dawn' ),
- gradient: 'linear-gradient(135deg, rgb(250, 172, 168), rgb(218, 208, 236)',
+ gradient: 'linear-gradient(135deg, rgb(250, 172, 168) 0%, rgb(218, 208, 236) 100% )',
},
{
name: __( 'Pale ocean' ),
- gradient: 'linear-gradient(135deg, rgb(255, 245, 203), rgb(182, 227, 212), rgb(51, 167, 181)',
+ gradient: 'linear-gradient(135deg, rgb(255, 245, 203) 0%, rgb(182, 227, 212) 50%, rgb(51, 167, 181) 100% )',
},
{
name: __( 'Electric grass' ),
- gradient: 'linear-gradient(135deg, rgb(202, 248, 128), rgb(113, 206, 126)',
+ gradient: 'linear-gradient(135deg, rgb(202, 248, 128) 0%, rgb(113, 206, 126) 100% )',
},
{
name: __( 'Subdued olive' ),
- gradient: 'linear-gradient(135deg, rgb(250, 250, 225), rgb(103, 166, 113)',
+ gradient: 'linear-gradient(135deg, rgb(250, 250, 225) 0%, rgb(103, 166, 113) 100% )',
},
{
name: __( 'Atomic cream' ),
- gradient: 'linear-gradient(135deg, rgb(253, 215, 154), rgb(0, 74, 89)',
+ gradient: 'linear-gradient(135deg, rgb(253, 215, 154) 0%, rgb(0, 74, 89) 100% )',
},
{
name: __( 'Nightshade' ),
- gradient: 'linear-gradient(135deg, rgb(51, 9, 104), rgb(49, 205, 207)',
+ gradient: 'linear-gradient(135deg, rgb(51, 9, 104) 0%, rgb(49, 205, 207) 100% )',
},
{
name: __( 'Midnight' ),
- gradient: 'linear-gradient(135deg, rgb(2, 3, 129), rgb(40, 116, 252)',
+ gradient: 'linear-gradient(135deg, rgb(2, 3, 129) 0%, rgb(40, 116, 252) 100% )',
},
],
};
diff --git a/packages/components/package.json b/packages/components/package.json
index d19667eca46cf..189df5b1d01d9 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -35,6 +35,7 @@
"classnames": "^2.2.5",
"clipboard": "^2.0.1",
"dom-scroll-into-view": "^1.2.1",
+ "gradient-parser": "^0.1.5",
"lodash": "^4.17.15",
"memize": "^1.0.5",
"moment": "^2.22.1",
diff --git a/packages/components/src/custom-gradient-picker/index.js b/packages/components/src/custom-gradient-picker/index.js
new file mode 100644
index 0000000000000..7725cd4de6ccb
--- /dev/null
+++ b/packages/components/src/custom-gradient-picker/index.js
@@ -0,0 +1,405 @@
+
+/**
+ * External dependencies
+ */
+import gradientParser from 'gradient-parser';
+import { compact, map, get, some } from 'lodash';
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { useCallback, useEffect, useState, useRef, useMemo } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import IconButton from '../icon-button';
+import Button from '../button';
+import ColorPicker from '../color-picker';
+import Dropdown from '../dropdown';
+
+const INSERT_POINT_WIDTH = 23;
+const GRADIENT_MARKERS_WIDTH = 18;
+const MINIMUM_DISTANCE_BETWEEN_MARKERS = ( INSERT_POINT_WIDTH + GRADIENT_MARKERS_WIDTH ) / 2;
+const MINIMUM_ABSOLUTE_LEFT_POSITION = 5;
+const MINIMUM_SIGNIFICANT_MOVE = 5;
+const DEFAULT_GRADIENT = 'linear-gradient(135deg, rgba(6, 147, 227, 1) 0%, rgb(155, 81, 224) 100%)';
+
+const serializeGradientColor = ( { type, value } ) => {
+ return `${ type }( ${ value.join( ',' ) })`;
+};
+
+const serializeGradientPosition = ( { type, value } ) => {
+ return `${ value }${ type }`;
+};
+
+const serializeGradientColorStop = ( { type, value, length } ) => {
+ return `${ serializeGradientColor( { type, value } ) } ${ serializeGradientPosition( length ) }`;
+};
+
+const serializeGradientOrientation = ( orientation ) => {
+ if ( ! orientation || orientation.type !== 'angular' ) {
+ return;
+ }
+ return `${ orientation.value }deg`;
+};
+
+const tinyColorRgbToGradientColorStop = ( { r, g, b, a } ) => {
+ if ( a === 1 ) {
+ return {
+ type: 'rgb',
+ value: [ r, g, b ],
+ };
+ }
+ return {
+ type: 'rgba',
+ value: [ r, g, b, a ],
+ };
+};
+
+const serializeGradient = ( { type, orientation, colorStops } ) => {
+ const serializedOrientation = serializeGradientOrientation( orientation );
+ const serializedColorStops = colorStops.sort( ( colorStop1, colorStop2 ) => {
+ return get( colorStop1, [ 'length', 'value' ], 0 ) - get( colorStop2, [ 'length', 'value' ], 0 );
+ } ).map( serializeGradientColorStop );
+ return `${ type }( ${ compact( [ serializedOrientation, ...serializedColorStops ] ).join( ', ' ) } )`;
+};
+
+const getGradientAbsolutePosition = ( relativeValue, maximumAbsoluteValue, minimumAbsoluteValue ) => {
+ return Math.round(
+ ( relativeValue * ( maximumAbsoluteValue - minimumAbsoluteValue ) / 100 ) + minimumAbsoluteValue
+ );
+};
+
+const getGradientRelativePosition = ( absoluteValue, maximumAbsoluteValue, minimumAbsoluteValue ) => {
+ const relativePosition = Math.round(
+ ( ( absoluteValue - minimumAbsoluteValue ) * 100 ) / ( maximumAbsoluteValue - minimumAbsoluteValue )
+ );
+ return Math.min( Math.max( relativePosition, 0 ), 100 );
+};
+
+export default function CustomGradientPicker( { value = DEFAULT_GRADIENT, onChange } ) {
+ //return null;
+ const parsedGradient = useMemo(
+ () => {
+ try {
+ return gradientParser.parse( value )[ 0 ];
+ } catch ( error ) {
+ return gradientParser.parse( DEFAULT_GRADIENT )[ 0 ];
+ }
+ },
+ [ value ]
+ );
+ const [ maximumInsertPointPosition, setMaximumInsertPointPosition ] = useState();
+ const [ gradientPickerXPosition, setGradientPickerXPosition ] = useState();
+ const [ insertPointPosition, setInsertPointPosition ] = useState( null );
+ const [ insertPointMoveEnabled, setInsertPointMoveEnabled ] = useState( true );
+ const [ isEditingColorAtPosition, setIsEditingColorAtPosition ] = useState( null );
+ const controlPointMoveState = useRef();
+ const gradientPickerRef = useRef();
+ const markerPoints = useMemo(
+ () => {
+ if ( ! parsedGradient ) {
+ return [];
+ }
+ return compact(
+ map( parsedGradient.colorStops, ( colorStop ) => {
+ if ( ! colorStop || ! colorStop.length || colorStop.length.type !== '%' ) {
+ return null;
+ }
+ return {
+ color: serializeGradientColor( colorStop ),
+ absolutePosition: getGradientAbsolutePosition( colorStop.length.value, maximumInsertPointPosition, MINIMUM_ABSOLUTE_LEFT_POSITION ),
+ position: colorStop.length.value,
+ };
+ } )
+ );
+ },
+ [ parsedGradient, maximumInsertPointPosition ]
+ );
+ const updateInsertPointPosition = useCallback(
+ ( event ) => {
+ if ( ! gradientPickerXPosition || ! insertPointMoveEnabled ) {
+ return;
+ }
+ let insertPosition = event.clientX - gradientPickerXPosition - ( INSERT_POINT_WIDTH / 2 );
+ if ( insertPosition < 0 ) {
+ insertPosition = 0;
+ }
+ if ( insertPosition > maximumInsertPointPosition ) {
+ insertPosition = maximumInsertPointPosition;
+ }
+ if ( some(
+ markerPoints,
+ ( { absolutePosition } ) => {
+ return Math.abs( insertPosition - absolutePosition ) < MINIMUM_DISTANCE_BETWEEN_MARKERS;
+ }
+ ) ) {
+ setInsertPointPosition( null );
+ return;
+ }
+
+ setInsertPointPosition( insertPosition );
+ }
+ );
+
+ const onMouseLeave = useCallback(
+ () => {
+ if ( ! insertPointMoveEnabled ) {
+ return;
+ }
+ setInsertPointPosition( null );
+ }
+ );
+
+ useEffect( () => {
+ if ( ! gradientPickerRef || ! gradientPickerRef.current ) {
+ return;
+ }
+ const rect = gradientPickerRef.current.getBoundingClientRect();
+ setGradientPickerXPosition( rect.left );
+ setMaximumInsertPointPosition( rect.width - INSERT_POINT_WIDTH );
+ }, [] );
+
+ const controlPointMove = useCallback(
+ ( event ) => {
+ const relativePosition = getGradientRelativePosition(
+ event.clientX - gradientPickerXPosition - ( GRADIENT_MARKERS_WIDTH / 2 ),
+ maximumInsertPointPosition,
+ MINIMUM_ABSOLUTE_LEFT_POSITION
+ );
+ const { parsedGradient: referenceParsedGradient, position, significantMoveHappened } = controlPointMoveState.current;
+ if ( ! significantMoveHappened ) {
+ const initialPosition = referenceParsedGradient.colorStops[ position ].length.value;
+ if ( Math.abs( initialPosition - relativePosition ) >= MINIMUM_SIGNIFICANT_MOVE ) {
+ controlPointMoveState.current.significantMoveHappened = true;
+ }
+ }
+ onChange(
+ serializeGradient(
+ {
+ ...referenceParsedGradient,
+ colorStops: referenceParsedGradient.colorStops.map(
+ ( colorStop, colorStopIndex ) => {
+ if ( colorStopIndex !== position ) {
+ return colorStop;
+ }
+ return {
+ ...colorStop,
+ length: {
+ ...colorStop.length,
+ value: relativePosition.toString(),
+ },
+ };
+ }
+ ),
+ }
+ )
+ );
+ },
+ [ controlPointMoveState, onChange, maximumInsertPointPosition ]
+ );
+
+ const unbindEventListeners = useCallback(
+ () => {
+ if ( window && window.removeEventListener ) {
+ window.removeEventListener( 'mousemove', controlPointMove );
+ window.removeEventListener( 'mouseup', unbindEventListeners );
+ }
+ },
+ [ controlPointMove ]
+ );
+
+ const controlPointMouseDown = useMemo(
+ () => {
+ return parsedGradient.colorStops.map(
+ ( colorStop, index ) => ( event ) => {
+ if ( window && window.addEventListener ) {
+ controlPointMoveState.current = {
+ parsedGradient,
+ position: index,
+ significantMoveHappened: false,
+ };
+ controlPointMove( event );
+ window.addEventListener( 'mousemove', controlPointMove );
+ window.addEventListener( 'mouseup', unbindEventListeners );
+ }
+ }
+ );
+ },
+ [ parsedGradient, controlPointMove ]
+ );
+
+ return (
+
+
+ { insertPointPosition !== null && (
+ {
+ setInsertPointMoveEnabled( true );
+ setIsEditingColorAtPosition( null );
+ setInsertPointPosition( null );
+ } }
+ renderToggle={ ( { isOpen, onToggle } ) => (
+ {
+ setInsertPointMoveEnabled( false );
+ onToggle();
+ } }
+ className="components-custom-gradient-picker__insert-point"
+ icon="insert"
+ style={ {
+ left: insertPointPosition !== null ? insertPointPosition : undefined,
+ } }
+ />
+ ) }
+ renderContent={ () => (
+ {
+ const relativePosition = getGradientRelativePosition( insertPointPosition, maximumInsertPointPosition, MINIMUM_ABSOLUTE_LEFT_POSITION );
+ const colorStop = tinyColorRgbToGradientColorStop( rgb );
+ colorStop.length = {
+ type: '%',
+ value: relativePosition,
+ };
+ let newGradient;
+ if ( isEditingColorAtPosition === null ) {
+ newGradient = {
+ ...parsedGradient,
+ colorStops: [
+ ...parsedGradient.colorStops,
+ colorStop,
+ ],
+ };
+ setIsEditingColorAtPosition( relativePosition.toString() );
+ } else {
+ newGradient = {
+ ...parsedGradient,
+ colorStops: parsedGradient.colorStops.map(
+ ( currentColorStop ) => {
+ if ( currentColorStop.length.value !== isEditingColorAtPosition ) {
+ return currentColorStop;
+ }
+ return colorStop;
+ }
+ ),
+ };
+ }
+ onChange( serializeGradient( newGradient ) );
+ } }
+ />
+ ) }
+ popoverProps={ {
+ className: 'components-custom-gradient-picker__color-picker-popover',
+ position: 'top',
+ } }
+ />
+
+ ) }
+ { markerPoints.length > 0 && (
+ markerPoints.map(
+ ( point, index ) => (
+ isEditingColorAtPosition !== point.position && (
+ {
+ setInsertPointMoveEnabled( true );
+ setIsEditingColorAtPosition( null );
+ setInsertPointPosition( null );
+ } }
+ renderToggle={ ( { isOpen, onToggle } ) => (
+
+
+ );
+}
diff --git a/packages/components/src/custom-gradient-picker/style.scss b/packages/components/src/custom-gradient-picker/style.scss
new file mode 100644
index 0000000000000..cdd74aef5799d
--- /dev/null
+++ b/packages/components/src/custom-gradient-picker/style.scss
@@ -0,0 +1,45 @@
+.components-custom-gradient-picker {
+ height: 23px;
+ width: 100%;
+ border-radius: 20px;
+ margin-top: 10px;
+
+ .components-custom-gradient-picker__markers-container {
+ position: relative;
+ }
+
+ .components-custom-gradient-picker__insert-point {
+ border-radius: 50%;
+ background: $white;
+ padding: 2px;
+ width: 23px;
+ height: 23px;
+ position: relative;
+ }
+
+ .components-custom-gradient-picker__marker-point {
+ border: 2px solid $white;
+ border-radius: 50%;
+ height: 18px;
+ position: absolute;
+ width: 18px;
+ top: 2px;
+
+ &.is-active {
+ background: #fafafa;
+ color: #23282d;
+ border-color: #999;
+ box-shadow:
+ inset 0 -1px 0 #999,
+ 0 0 0 1px $white,
+ 0 0 0 3px $blue-medium-focus;
+ }
+ }
+}
+
+.components-custom-gradient-picker__color-picker-popover .components-custom-gradient-picker__remove-control-point {
+ margin-left: auto;
+ margin-right: auto;
+ display: block;
+ margin-bottom: 8px;
+}
diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js
index 6c5cffa6e5e4b..81d1f4dbac078 100644
--- a/packages/components/src/dropdown/index.js
+++ b/packages/components/src/dropdown/index.js
@@ -62,6 +62,9 @@ class Dropdown extends Component {
}
close() {
+ if ( this.props.onClose ) {
+ this.props.onClose();
+ }
this.setState( { isOpen: false } );
}
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index d13be6e9ac2e3..d0d19cf09932b 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -10,6 +10,7 @@ export { default as ClipboardButton } from './clipboard-button';
export { default as ColorIndicator } from './color-indicator';
export { default as ColorPalette } from './color-palette';
export { default as ColorPicker } from './color-picker';
+export { default as CustomGradientPicker } from './custom-gradient-picker';
export { default as Dashicon } from './dashicon';
export { DateTimePicker, DatePicker, TimePicker } from './date-time';
export { default as Disabled } from './disabled';
diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss
index 4830ac6a84b71..cb77a2b49c0e8 100644
--- a/packages/components/src/style.scss
+++ b/packages/components/src/style.scss
@@ -8,6 +8,7 @@
@import "./color-indicator/style.scss";
@import "./color-palette/style.scss";
@import "./color-picker/style.scss";
+@import "./custom-gradient-picker/style.scss";
@import "./dashicon/style.scss";
@import "./date-time/style.scss";
@import "./disabled/style.scss";