From 7609ca8ef4676da0e6aa2442f01c82c1d76fa2cf Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Mon, 2 Mar 2020 10:33:16 -0500 Subject: [PATCH 01/81] WIP gradient picker --- .../colorPicker/currentColorPicker.js | 181 ++++++++++++++++++ .../components/colorPicker/index.js | 164 ++++------------ .../components/colorPicker/stories/index.js | 8 +- .../components/colorPicker/useColor.js | 124 ++++++++++++ .../components/colorPicker/useReduction.js | 58 ++++++ 5 files changed, 404 insertions(+), 131 deletions(-) create mode 100644 assets/src/edit-story/components/colorPicker/currentColorPicker.js create mode 100644 assets/src/edit-story/components/colorPicker/useColor.js create mode 100644 assets/src/edit-story/components/colorPicker/useReduction.js diff --git a/assets/src/edit-story/components/colorPicker/currentColorPicker.js b/assets/src/edit-story/components/colorPicker/currentColorPicker.js new file mode 100644 index 000000000000..c422f25dd0ac --- /dev/null +++ b/assets/src/edit-story/components/colorPicker/currentColorPicker.js @@ -0,0 +1,181 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { CustomPicker } from 'react-color'; +import { Saturation, Hue, Alpha } from 'react-color/lib/components/common'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { Eyedropper } from '../button'; +import Pointer from './pointer'; +import EditableHexPreview from './editableHexPreview'; + +const CONTAINER_PADDING = 15; +const EYEDROPPER_ICON_SIZE = 15; +const HEADER_FOOTER_HEIGHT = 50; +const BODY_HEIGHT = 140; +const CONTROLS_WIDTH = 12; +const CONTROLS_BORDER_RADIUS = 6; + +const Container = styled.div` + font-family: ${({ theme }) => theme.fonts.body1.family}; + font-style: normal; + font-weight: normal; + font-size: 12px; + user-select: none; +`; + +const Body = styled.div` + padding: ${CONTAINER_PADDING}px; + padding-bottom: 0; + display: grid; + grid: 'saturation hue alpha' ${BODY_HEIGHT}px / 1fr ${CONTROLS_WIDTH}px ${CONTROLS_WIDTH}px; + grid-gap: 10px; +`; + +const SaturationWrapper = styled.div` + position: relative; + width: 167px; + height: ${BODY_HEIGHT}px; + grid-area: saturation; +`; + +const HueWrapper = styled.div` + position: relative; + height: ${BODY_HEIGHT}px; + width: ${CONTROLS_WIDTH}px; + grid-area: hue; +`; + +const AlphaWrapper = styled.div` + position: relative; + height: ${BODY_HEIGHT}px; + width: ${CONTROLS_WIDTH}px; + background: #fff; + border-radius: ${CONTROLS_BORDER_RADIUS}px; + grid-area: alpha; +`; + +const Footer = styled.div` + padding: ${CONTAINER_PADDING}px; + height: ${HEADER_FOOTER_HEIGHT}px; + font-size: ${CONTROLS_WIDTH}px; + line-height: 19px; + position: relative; +`; + +const EyedropperWrapper = styled.div` + position: absolute; + left: ${CONTAINER_PADDING}px; + bottom: ${CONTAINER_PADDING}px; +`; + +const EyedropperButton = styled(Eyedropper)` + line-height: ${EYEDROPPER_ICON_SIZE}px; +`; + +const CurrentWrapper = styled.div` + position: absolute; + left: 0; + right: 0; + text-align: center; + bottom: ${CONTAINER_PADDING}px; +`; + +const CurrentAlphaWrapper = styled.div` + position: absolute; + right: ${CONTAINER_PADDING}px; + bottom: ${CONTAINER_PADDING}px; +`; + +function CurrentColorPicker({ rgb, hsl, hsv, hex, onChange }) { + const alphaPercentage = Math.round(rgb.a * 100); + + return ( + + + + } + hsl={hsl} + hsv={hsv} + onChange={onChange} + /> + + + } + hsl={hsl} + onChange={onChange} + /> + + + } + rgb={rgb} + hsl={hsl} + onChange={onChange} + /> + + +
+ {/* TODO: implement (see https://github.com/google/web-stories-wp/issues/262) */} + + + + + + + {alphaPercentage + '%'} +
+
+ ); +} + +CurrentColorPicker.propTypes = { + onChange: PropTypes.func.isRequired, + rgb: PropTypes.object, + hex: PropTypes.string, + hsl: PropTypes.object, + hsv: PropTypes.object, +}; + +export default CustomPicker(CurrentColorPicker); diff --git a/assets/src/edit-story/components/colorPicker/index.js b/assets/src/edit-story/components/colorPicker/index.js index cdb6131b03f2..1d6a8a1b930b 100644 --- a/assets/src/edit-story/components/colorPicker/index.js +++ b/assets/src/edit-story/components/colorPicker/index.js @@ -19,28 +19,23 @@ */ import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { CustomPicker } from 'react-color'; -import { Saturation, Hue, Alpha } from 'react-color/lib/components/common'; import { rgba } from 'polished'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies */ -import { Close, Eyedropper } from '../button'; -import Pointer from './pointer'; -import EditableHexPreview from './editableHexPreview'; +import { Close } from '../button'; +import CurrentColorPicker from './currentColorPicker'; +import useColor from './useColor'; const CONTAINER_PADDING = 15; -const EYEDROPPER_ICON_SIZE = 15; const HEADER_FOOTER_HEIGHT = 50; -const BODY_HEIGHT = 140; -const CONTROLS_WIDTH = 12; -const CONTROLS_BORDER_RADIUS = 6; const Container = styled.div` border-radius: 4px; @@ -70,71 +65,30 @@ const CloseButton = styled(Close)` top: ${CONTAINER_PADDING}px; `; -const Body = styled.div` - padding: ${CONTAINER_PADDING}px; - padding-bottom: 0; - display: grid; - grid: 'saturation hue alpha' ${BODY_HEIGHT}px / 1fr ${CONTROLS_WIDTH}px ${CONTROLS_WIDTH}px; - grid-gap: 10px; -`; - -const SaturationWrapper = styled.div` - position: relative; - width: 167px; - height: ${BODY_HEIGHT}px; - grid-area: saturation; -`; - -const HueWrapper = styled.div` - position: relative; - height: ${BODY_HEIGHT}px; - width: ${CONTROLS_WIDTH}px; - grid-area: hue; -`; - -const AlphaWrapper = styled.div` - position: relative; - height: ${BODY_HEIGHT}px; - width: ${CONTROLS_WIDTH}px; - background: #fff; - border-radius: ${CONTROLS_BORDER_RADIUS}px; - grid-area: alpha; -`; - -const Footer = styled.div` - padding: ${CONTAINER_PADDING}px; - height: ${HEADER_FOOTER_HEIGHT}px; - font-size: ${CONTROLS_WIDTH}px; - line-height: 19px; - position: relative; -`; - -const EyedropperWrapper = styled.div` - position: absolute; - left: ${CONTAINER_PADDING}px; - bottom: ${CONTAINER_PADDING}px; -`; - -const EyedropperButton = styled(Eyedropper)` - line-height: ${EYEDROPPER_ICON_SIZE}px; -`; - -const CurrentWrapper = styled.div` - position: absolute; - left: 0; - right: 0; - text-align: center; - bottom: ${CONTAINER_PADDING}px; -`; - -const CurrentAlphaWrapper = styled.div` - position: absolute; - right: ${CONTAINER_PADDING}px; - bottom: ${CONTAINER_PADDING}px; -`; - -function ColorPicker({ rgb, hsl, hsv, hex, onChange, onClose }) { - const alphaPercentage = Math.round(rgb.a * 100); +const Body = styled.div``; + +function ColorPicker({ color, onChange, onClose }) { + const { + state: { currentColor, generatedColor }, + actions: { load, updateCurrentColor }, + } = useColor(); + + useEffect(() => { + if (generatedColor && generatedColor !== color) { + onChange(generatedColor); + } + }, [color, generatedColor, onChange]); + + // When color updates from outside, reload in picker unless it's the same at the last export color + const generatedColorRef = useRef(generatedColor); + useEffect(() => { + generatedColorRef.current = generatedColor; + }, [generatedColor]); + useEffect(() => { + if (color && color !== generatedColorRef.current) { + load(color); + } + }, [color, load]); return ( @@ -147,54 +101,11 @@ function ColorPicker({ rgb, hsl, hsv, hex, onChange, onClose }) { /> - - } - hsl={hsl} - hsv={hsv} - onChange={onChange} - /> - - - } - hsl={hsl} - onChange={onChange} - /> - - - } - rgb={rgb} - hsl={hsl} - onChange={onChange} - /> - + -
- {/* TODO: implement (see https://github.com/google/web-stories-wp/issues/262) */} - - - - - - - {alphaPercentage + '%'} -
); } @@ -202,10 +113,11 @@ function ColorPicker({ rgb, hsl, hsv, hex, onChange, onClose }) { ColorPicker.propTypes = { onChange: PropTypes.func.isRequired, onClose: PropTypes.func, - rgb: PropTypes.object, - hex: PropTypes.string, - hsl: PropTypes.object, - hsv: PropTypes.object, + color: PropTypes.string, +}; + +ColorPicker.defaultProps = { + color: null, }; -export default CustomPicker(ColorPicker); +export default ColorPicker; diff --git a/assets/src/edit-story/components/colorPicker/stories/index.js b/assets/src/edit-story/components/colorPicker/stories/index.js index 5f9babcbf5db..fcb603aebd0d 100644 --- a/assets/src/edit-story/components/colorPicker/stories/index.js +++ b/assets/src/edit-story/components/colorPicker/stories/index.js @@ -18,7 +18,7 @@ * External dependencies */ import { text } from '@storybook/addon-knobs'; -import { useState, useCallback, useEffect } from 'react'; +import { useState, useEffect } from 'react'; /** * Internal dependencies @@ -31,13 +31,11 @@ export default { }; export const _default = () => { - const initialColor = text('Initial Color', '#44aaffff'); + const initialColor = text('Initial Color', '#44aaff'); const [color, setColor] = useState(initialColor); useEffect(() => setColor(initialColor), [initialColor]); - const onChange = useCallback(({ rgb }) => setColor(rgb), [setColor]); - - return ; + return ; }; diff --git a/assets/src/edit-story/components/colorPicker/useColor.js b/assets/src/edit-story/components/colorPicker/useColor.js new file mode 100644 index 000000000000..b5febf57021f --- /dev/null +++ b/assets/src/edit-story/components/colorPicker/useColor.js @@ -0,0 +1,124 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ +import useReduction from './useReduction'; + +export const TYPE_SOLID = 'solid'; +export const TYPE_LINEAR = 'linear'; +export const TYPE_RADIAL = 'radial'; +export const TYPE_CONIC = 'conic'; + +const initialState = { + type: TYPE_SOLID, + stops: [], + currentColor: { + r: 0, + g: 0, + b: 0, + a: 0, + }, + currentStopIndex: 0, + angle: 0, + center: [], + size: [], +}; + +const reducer = { + load: (state, { payload }) => ({ + ...state, + /* TODO: Parse from any color string including gradients */ + currentColor: payload, + }), + setToSolid: (state) => ({ + ...state, + type: TYPE_SOLID, + }), + setToGradient: (state, { payload }) => ({ + ...state, + type: payload, + }), + addStopAt: (state, { payload }) => ({ + ...state, + foo: payload, + }), + moveCurrentStopTo: (state, { payload }) => ({ + ...state, + foo: payload, + }), + updateCurrentColor: (state, { payload: { rgb } }) => { + const currentColor = `rgba(${Object.values(rgb).join(',')})`; + const newState = { + ...state, + currentColor, + }; + + if (state.type !== TYPE_SOLID) { + // Also update color for current stop + state.stops = [ + state.stops.slice(0, state.currentStopIndex), + { + ...state.stop[state.currentStopIndex], + color: currentColor, + }, + state.stops.slice(state.currentStopIndex + 1), + ]; + } + + return newState; + }, + rotateClockwise: (state) => ({ + ...state, + angle: state.angle + 90, + regenerate: true, + }), + selectStop: (state, { payload }) => ({ + ...state, + foo: payload, + regenerate: true, + }), + deleteStop: (state, { payload: indexToDelete }) => ({ + ...state, + stops: state.stops.filter((stop, index) => index !== indexToDelete), + regenerate: true, + }), + reverseStops: (state) => ({ + ...state, + stops: [...state.stops].reverse(), + regenerate: true, + }), +}; + +function useColor() { + const [state, actions] = useReduction(initialState, reducer); + + const { currentColor } = state; + + // TODO: Generate color for gradients too + const generatedColor = currentColor; + + return { + state: { + ...state, + generatedColor, + }, + actions, + }; +} + +export default useColor; diff --git a/assets/src/edit-story/components/colorPicker/useReduction.js b/assets/src/edit-story/components/colorPicker/useReduction.js new file mode 100644 index 000000000000..0c64380546d1 --- /dev/null +++ b/assets/src/edit-story/components/colorPicker/useReduction.js @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Modified from https://github.com/anater/useReduction + */ + +/** + * WordPress dependencies + */ +import { useReducer, useMemo } from '@wordpress/element'; + +export default function useReduction(initialState, reducerMap) { + const [state, dispatch] = useReducer(makeReducer(reducerMap), initialState); + const actions = useMemo(() => makeActions(reducerMap, dispatch), [ + reducerMap, + ]); + return [state, actions]; +} + +function makeReducer(reducerMap) { + return (state, action) => { + // if the dispatched action is valid and there's a matching reducer, use it + if (action && action.type && reducerMap[action.type]) { + return reducerMap[action.type](state, action); + } + // always return state if the action has no reducer + return state; + }; +} + +function makeActions(reducerMap, dispatch) { + const types = Object.keys(reducerMap); + return types.reduce((actions, type) => { + // if there isn't already an action with this type + if (!actions[type]) { + // dispatches action with type and payload when called + actions[type] = (payload) => { + const action = { type, payload }; + dispatch(action); + }; + } + return actions; + }, {}); +} From 1df584180fa890f47a8206ae6ccb642d77a908dc Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Tue, 3 Mar 2020 11:39:29 -0500 Subject: [PATCH 02/81] Added types for color and converted color picker to use new type format --- .../components/colorPicker/useColor.js | 51 +++++++++++++++---- assets/src/edit-story/types.js | 27 ++++++++++ 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/assets/src/edit-story/components/colorPicker/useColor.js b/assets/src/edit-story/components/colorPicker/useColor.js index b5febf57021f..bdce2c7727c8 100644 --- a/assets/src/edit-story/components/colorPicker/useColor.js +++ b/assets/src/edit-story/components/colorPicker/useColor.js @@ -40,11 +40,43 @@ const initialState = { }; const reducer = { - load: (state, { payload }) => ({ - ...state, - /* TODO: Parse from any color string including gradients */ - currentColor: payload, - }), + load: (state, { payload }) => { + const { type, color, stops, angle, center, size } = payload; + switch (type) { + case TYPE_LINEAR: + return { + ...state, + color: stops[0].color, + stops, + angle, + }; + + case TYPE_RADIAL: + return { + ...state, + color: stops[0].color, + stops, + center, + size, + }; + + case TYPE_CONIC: + return { + ...state, + color: stops[0].color, + stops, + angle, + center, + }; + + case TYPE_SOLID: + default: + return { + ...state, + currentColor: color, + }; + } + }, setToSolid: (state) => ({ ...state, type: TYPE_SOLID, @@ -89,8 +121,7 @@ const reducer = { }), selectStop: (state, { payload }) => ({ ...state, - foo: payload, - regenerate: true, + currentStopIndex: payload, }), deleteStop: (state, { payload: indexToDelete }) => ({ ...state, @@ -107,10 +138,8 @@ const reducer = { function useColor() { const [state, actions] = useReduction(initialState, reducer); - const { currentColor } = state; - - // TODO: Generate color for gradients too - const generatedColor = currentColor; + // TODO: Generate compact output for color + const generatedColor = {}; return { state: { diff --git a/assets/src/edit-story/types.js b/assets/src/edit-story/types.js index 35f0a7b2fb7c..3c3d9781c4ae 100644 --- a/assets/src/edit-story/types.js +++ b/assets/src/edit-story/types.js @@ -116,3 +116,30 @@ StoryPropTypes.elements.background = PropTypes.shape({ }); export default StoryPropTypes; + +export const Hex = PropTypes.shape({ + r: PropTypes.number.isRequired, + g: PropTypes.number.isRequired, + b: PropTypes.number.isRequired, + a: PropTypes.number, +}); + +export const ColorStop = PropTypes.shape({ + stop: Hex.isRequired, + position: PropTypes.number.isRequired, +}); + +export const Pattern = PropTypes.shape({ + type: PropTypes.oneOf(['solid', 'linear', 'gradient', 'conic']), + color: Hex, + stops: PropTypes.arrayOf(ColorStop), + angle: PropTypes.number.isRequired, + center: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), + size: PropTypes.shape({ + w: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), +}); From 71964ab400b669d0425d988632d48d90725321c6 Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Tue, 3 Mar 2020 16:26:22 -0500 Subject: [PATCH 03/81] Added CSS generator and unit tests for same --- assets/src/edit-story/types.js | 2 +- .../edit-story/utils/generatePatternCSS.js | 109 ++++++++ .../utils/test/generatePatternCSS.js | 259 ++++++++++++++++++ 3 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 assets/src/edit-story/utils/generatePatternCSS.js create mode 100644 assets/src/edit-story/utils/test/generatePatternCSS.js diff --git a/assets/src/edit-story/types.js b/assets/src/edit-story/types.js index 3c3d9781c4ae..3ed9c4861a5c 100644 --- a/assets/src/edit-story/types.js +++ b/assets/src/edit-story/types.js @@ -133,7 +133,7 @@ export const Pattern = PropTypes.shape({ type: PropTypes.oneOf(['solid', 'linear', 'gradient', 'conic']), color: Hex, stops: PropTypes.arrayOf(ColorStop), - angle: PropTypes.number.isRequired, + rotation: PropTypes.number.isRequired, center: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, diff --git a/assets/src/edit-story/utils/generatePatternCSS.js b/assets/src/edit-story/utils/generatePatternCSS.js new file mode 100644 index 000000000000..2ffd50e51154 --- /dev/null +++ b/assets/src/edit-story/utils/generatePatternCSS.js @@ -0,0 +1,109 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import { rgba } from 'polished'; + +function round(val, pos) { + return Number(val.toFixed(pos)); +} + +function getGradientDescription({ type, rotation, center, size }) { + const sizeString = size + ? `ellipse ${round(100 * size.w, 2)}% ${round(100 * size.h, 2)}%` + : ''; + const centerString = center + ? ` at ${round(100 * center.x, 2)}% ${round(100 * center.y, 2)}%` + : ''; + const rotationString = rotation ? `${rotation}turn` : ''; + switch (type) { + case 'radial': + if (!centerString && !sizeString) { + return null; + } + return `${sizeString}${centerString}`.trim(); + + case 'conic': + if (!rotationString && !centerString) { + return null; + } + const fromRotationString = rotationString ? `from ${rotationString}` : ''; + return `${fromRotationString}${centerString}`.trim(); + + case 'linear': + return rotationString; + + default: + return null; + } +} + +function getStopList(stops, isAngular = false) { + const getPosition = (val) => + isAngular ? `${round(val, 4)}turn` : `${round(val * 100, 2)}%`; + const getColor = ({ r, g, b, a = 1 }) => rgba(r, g, b, a); + return stops.map( + ({ color, position }) => `${getColor(color)} ${getPosition(position)}` + ); +} + +/** + * Generate CSS from a Pattern. + * + * @param {Object} pattern Patterns as describe by the Pattern type + * @param {string} property Type of CSS to generate. Defaults to 'background', + * but can also be 'color', 'fill' or 'stroke'. + * + * @return {string} CSS declaration, e.g. 'fill: transparent' or + * 'background-image: radial-gradient(red, blue)'. + */ +function generatePatternCSS(pattern, property = 'background') { + const isBackground = property === 'background'; + if (pattern === null) { + return `${property}: transparent`; + } + + const { type = 'solid' } = pattern; + if (!['solid', 'radial', 'linear', 'conic'].includes(type)) { + throw new Error(`Unknown pattern type: '${type}'`); + } + // Gradients are only possible for backgrounds + if (!isBackground && type !== 'solid') { + throw new Error( + `Can only generate solid colors for property '${property}'` + ); + } + + if (type === 'solid') { + const { + color: { r, g, b, a = 1 }, + } = pattern; + const propertyPostfix = isBackground ? '-color' : ''; + return `${property}${propertyPostfix}: ${rgba(r, g, b, a)}`; + } + + const { stops } = pattern; + const func = `${type}-gradient`; + const description = getGradientDescription(pattern); + const stopList = getStopList(stops, type === 'conic'); + const parms = description ? [description, ...stopList] : stopList; + + return `background-image: ${func}(${parms.join(', ')})`; +} + +export default generatePatternCSS; diff --git a/assets/src/edit-story/utils/test/generatePatternCSS.js b/assets/src/edit-story/utils/test/generatePatternCSS.js new file mode 100644 index 000000000000..65352b56d848 --- /dev/null +++ b/assets/src/edit-story/utils/test/generatePatternCSS.js @@ -0,0 +1,259 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ +import generatePatternCSS from '../generatePatternCSS'; + +describe('generatePatternCSS', () => { + describe('given null', () => { + it('should return transparent', () => { + expect(generatePatternCSS(null)).toBe('background: transparent'); + }); + }); + + describe('given an unknown type', () => { + it('should throw error', () => { + expect(() => generatePatternCSS({ type: 'comic' })).toThrow( + /unknown pattern type/i + ); + }); + }); + + describe('given a color', () => { + it('should return shortest form if possible', () => { + expect(generatePatternCSS({ color: { r: 255, g: 0, b: 0 } })).toBe( + 'background-color: #f00' + ); + }); + + it('should return short form', () => { + expect(generatePatternCSS({ color: { r: 254, g: 0, b: 0, a: 1 } })).toBe( + 'background-color: #fe0000' + ); + }); + + it('should return rgba if transparent', () => { + expect( + generatePatternCSS({ color: { r: 255, g: 0, b: 0, a: 0.7 } }) + ).toBe('background-color: rgba(255,0,0,0.7)'); + }); + + it('should be able to render non-background properties', () => { + expect( + generatePatternCSS({ color: { r: 255, g: 0, b: 0 } }, 'fill') + ).toBe('fill: #f00'); + }); + }); + + describe('given any gradient', () => { + it('should not be able to render non-background properties', () => { + expect(() => + generatePatternCSS( + { + type: 'linear', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0.3 }, + { color: { r: 0, g: 0, b: 255 }, position: 0.7 }, + ], + }, + 'fill' + ) + ).toThrow(/only generate solid/i); + }); + }); + + describe('given a linear gradient', () => { + it('should be able to render a two-stop gradient', () => { + expect( + generatePatternCSS({ + type: 'linear', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + }) + ).toBe('background-image: linear-gradient(#f00 0%, #00f 100%)'); + }); + + it('should be able to render a multi-stop gradient with transparencies at an angle', () => { + expect( + generatePatternCSS({ + type: 'linear', + stops: [ + { color: { r: 255, g: 0, b: 0, a: 0 }, position: 0 }, + { color: { r: 255, g: 0, b: 0 }, position: 0.6 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + rotation: 0.25, + }) + ).toBe( + 'background-image: linear-gradient(0.25turn, rgba(255,0,0,0) 0%, #f00 60%, #00f 100%)' + ); + }); + }); + + describe('given a conic gradient', () => { + it('should be able to render a two-stop gradient', () => { + expect( + generatePatternCSS({ + type: 'conic', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + }) + ).toBe('background-image: conic-gradient(#f00 0turn, #00f 1turn)'); + }); + + it('should be able to render a multi-stop gradient', () => { + expect( + generatePatternCSS({ + type: 'conic', + stops: [ + { color: { r: 255, g: 0, b: 0, a: 0 }, position: 0 }, + { color: { r: 255, g: 0, b: 0 }, position: 0.6 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + }) + ).toBe( + 'background-image: conic-gradient(rgba(255,0,0,0) 0turn, #f00 0.6turn, #00f 1turn)' + ); + }); + + it('should be able to render at an angle', () => { + expect( + generatePatternCSS({ + type: 'conic', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + rotation: 0.25, + }) + ).toBe( + 'background-image: conic-gradient(from 0.25turn, #f00 0turn, #00f 1turn)' + ); + }); + + it('should be able to render off-center', () => { + expect( + generatePatternCSS({ + type: 'conic', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + center: { x: 0.4, y: 0.6 }, + }) + ).toBe( + 'background-image: conic-gradient(at 40% 60%, #f00 0turn, #00f 1turn)' + ); + }); + + it('should be able to at an angle *and* off-center', () => { + expect( + generatePatternCSS({ + type: 'conic', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + rotation: 0.25, + center: { x: 0.4, y: 0.6 }, + }) + ).toBe( + 'background-image: conic-gradient(from 0.25turn at 40% 60%, #f00 0turn, #00f 1turn)' + ); + }); + }); + + describe('given a radial gradient', () => { + it('should be able to render a two-stop gradient', () => { + expect( + generatePatternCSS({ + type: 'radial', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + }) + ).toBe('background-image: radial-gradient(#f00 0%, #00f 100%)'); + }); + + it('should be able to render a multi-stop gradient', () => { + expect( + generatePatternCSS({ + type: 'radial', + stops: [ + { color: { r: 255, g: 0, b: 0, a: 0 }, position: 0 }, + { color: { r: 255, g: 0, b: 0 }, position: 0.6 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + }) + ).toBe( + 'background-image: radial-gradient(rgba(255,0,0,0) 0%, #f00 60%, #00f 100%)' + ); + }); + + it('should be able to render different size', () => { + expect( + generatePatternCSS({ + type: 'radial', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + size: { w: 0.2, h: 0.45678 }, + }) + ).toBe( + 'background-image: radial-gradient(ellipse 20% 45.68%, #f00 0%, #00f 100%)' + ); + }); + + it('should be able to render off-center', () => { + expect( + generatePatternCSS({ + type: 'radial', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + center: { x: 0.4, y: 0.6 }, + }) + ).toBe( + 'background-image: radial-gradient(at 40% 60%, #f00 0%, #00f 100%)' + ); + }); + + it('should be able to different size *and* off-center', () => { + expect( + generatePatternCSS({ + type: 'radial', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + size: { w: 0.2, h: 0.45678 }, + center: { x: 0.4, y: 0.6 }, + }) + ).toBe( + 'background-image: radial-gradient(ellipse 20% 45.68% at 40% 60%, #f00 0%, #00f 100%)' + ); + }); + }); +}); From d6eed7076d4f9f2b0e4209a0d8b5013be43f77b6 Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Tue, 3 Mar 2020 19:08:54 -0500 Subject: [PATCH 04/81] Converted color picker to new format and added migrations --- .../components/colorPicker/index.js | 3 +- .../components/colorPicker/stories/index.js | 13 +- assets/src/edit-story/migration/migrate.js | 2 + .../migrations/test/v0004_colorToPattern.js | 113 ++++++++++++++++++ .../migrations/v0004_colorToPattern.js | 63 ++++++++++ assets/src/edit-story/types.js | 12 +- 6 files changed, 191 insertions(+), 15 deletions(-) create mode 100644 assets/src/edit-story/migration/migrations/test/v0004_colorToPattern.js create mode 100644 assets/src/edit-story/migration/migrations/v0004_colorToPattern.js diff --git a/assets/src/edit-story/components/colorPicker/index.js b/assets/src/edit-story/components/colorPicker/index.js index 1d6a8a1b930b..fdf0c2332914 100644 --- a/assets/src/edit-story/components/colorPicker/index.js +++ b/assets/src/edit-story/components/colorPicker/index.js @@ -30,6 +30,7 @@ import { useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies */ +import { PatternPropType } from '../../types'; import { Close } from '../button'; import CurrentColorPicker from './currentColorPicker'; import useColor from './useColor'; @@ -113,7 +114,7 @@ function ColorPicker({ color, onChange, onClose }) { ColorPicker.propTypes = { onChange: PropTypes.func.isRequired, onClose: PropTypes.func, - color: PropTypes.string, + color: PatternPropType.isRequired, }; ColorPicker.defaultProps = { diff --git a/assets/src/edit-story/components/colorPicker/stories/index.js b/assets/src/edit-story/components/colorPicker/stories/index.js index fcb603aebd0d..aab1f2ec2666 100644 --- a/assets/src/edit-story/components/colorPicker/stories/index.js +++ b/assets/src/edit-story/components/colorPicker/stories/index.js @@ -17,8 +17,7 @@ /** * External dependencies */ -import { text } from '@storybook/addon-knobs'; -import { useState, useEffect } from 'react'; +import { object } from '@storybook/addon-knobs'; /** * Internal dependencies @@ -31,11 +30,9 @@ export default { }; export const _default = () => { - const initialColor = text('Initial Color', '#44aaff'); + const initialColor = object('Initial Color', { + color: { r: 255, g: 0, b: 0 }, + }); - const [color, setColor] = useState(initialColor); - - useEffect(() => setColor(initialColor), [initialColor]); - - return ; + return {}} />; }; diff --git a/assets/src/edit-story/migration/migrate.js b/assets/src/edit-story/migration/migrate.js index 64bec0e98a20..6030f98e42a8 100644 --- a/assets/src/edit-story/migration/migrate.js +++ b/assets/src/edit-story/migration/migrate.js @@ -20,11 +20,13 @@ import storyDataArrayToObject from './migrations/v0001_storyDataArrayToObject'; import dataPixelTo1080 from './migrations/v0002_dataPixelTo1080'; import fullbleedToFill from './migrations/v0003_fullbleedToFill'; +import colorToPattern from './migrations/v0004_colorToPattern'; const MIGRATIONS = { 1: [storyDataArrayToObject], 2: [dataPixelTo1080], 3: [fullbleedToFill], + 4: [colorToPattern], }; export const DATA_VERSION = Math.max.apply( diff --git a/assets/src/edit-story/migration/migrations/test/v0004_colorToPattern.js b/assets/src/edit-story/migration/migrations/test/v0004_colorToPattern.js new file mode 100644 index 000000000000..6c504eab92e3 --- /dev/null +++ b/assets/src/edit-story/migration/migrations/test/v0004_colorToPattern.js @@ -0,0 +1,113 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ +import colorToPattern from '../v0004_colorToPattern'; + +describe('colorToPattern', () => { + it('should fullbleed to fill', () => { + expect( + colorToPattern({ + _test: 'story', + pages: [ + { + _test: 'page1', + backgroundColor: '#fff', + elements: [ + { + _test: 'element1', + }, + { + _test: 'element2', + color: 'black', + }, + { + _test: 'element3', + color: 'transparent', + }, + { + _test: 'element4', + color: null, + }, + { + _test: 'element5', + color: '#f00', + }, + { + _test: 'element6', + backgroundColor: '#c0ffee', + }, + { + _test: 'element7', + backgroundColor: 'rgba(255, 0, 0, .5)', + color: 'salmon', + }, + ], + }, + { + _test: 'page2', + elements: [], + }, + ], + }) + ).toStrictEqual({ + _test: 'story', + pages: [ + { + _test: 'page1', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, + elements: [ + { + _test: 'element1', + }, + { + _test: 'element2', + color: { color: { r: 0, g: 0, b: 0 } }, + }, + { + _test: 'element3', + color: null, + }, + { + _test: 'element4', + color: null, + }, + { + _test: 'element5', + color: { color: { r: 255, g: 0, b: 0 } }, + }, + { + _test: 'element6', + backgroundColor: { color: { r: 192, g: 255, b: 238 } }, + }, + { + _test: 'element7', + backgroundColor: { color: { r: 255, g: 0, b: 0, a: 0.5 } }, + color: { color: { r: 250, g: 128, b: 114 } }, + }, + ], + }, + { + _test: 'page2', + backgroundColor: null, + elements: [], + }, + ], + }); + }); +}); diff --git a/assets/src/edit-story/migration/migrations/v0004_colorToPattern.js b/assets/src/edit-story/migration/migrations/v0004_colorToPattern.js new file mode 100644 index 000000000000..bd332d78ddb4 --- /dev/null +++ b/assets/src/edit-story/migration/migrations/v0004_colorToPattern.js @@ -0,0 +1,63 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import { parseToRgb } from 'polished'; + +function colorToPattern({ pages, ...rest }) { + return { + pages: pages.map(updatePage), + ...rest, + }; +} + +function updatePage({ elements, backgroundColor, ...rest }) { + return { + elements: elements.map(updateElement), + backgroundColor: parse(backgroundColor), + ...rest, + }; +} + +function updateElement(props) { + const newProps = { ...props }; + + if (props.hasOwnProperty('color')) { + newProps.color = parse(newProps.color); + } + + if (props.hasOwnProperty('backgroundColor')) { + newProps.backgroundColor = parse(newProps.backgroundColor); + } + + return newProps; +} + +function parse(colorString) { + if (!colorString || colorString === 'transparent') { + return null; + } + + const { red: r, green: g, blue: b, alpha: a = 1 } = parseToRgb(colorString); + if (a === 1) { + return { color: { r, g, b } }; + } + return { color: { r, g, b, a } }; +} + +export default colorToPattern; diff --git a/assets/src/edit-story/types.js b/assets/src/edit-story/types.js index 3ed9c4861a5c..78a211860054 100644 --- a/assets/src/edit-story/types.js +++ b/assets/src/edit-story/types.js @@ -117,22 +117,22 @@ StoryPropTypes.elements.background = PropTypes.shape({ export default StoryPropTypes; -export const Hex = PropTypes.shape({ +export const HexPropType = PropTypes.shape({ r: PropTypes.number.isRequired, g: PropTypes.number.isRequired, b: PropTypes.number.isRequired, a: PropTypes.number, }); -export const ColorStop = PropTypes.shape({ - stop: Hex.isRequired, +export const ColorStopPropType = PropTypes.shape({ + stop: HexPropType.isRequired, position: PropTypes.number.isRequired, }); -export const Pattern = PropTypes.shape({ +export const PatternPropType = PropTypes.shape({ type: PropTypes.oneOf(['solid', 'linear', 'gradient', 'conic']), - color: Hex, - stops: PropTypes.arrayOf(ColorStop), + color: HexPropType, + stops: PropTypes.arrayOf(ColorStopPropType), rotation: PropTypes.number.isRequired, center: PropTypes.shape({ x: PropTypes.number.isRequired, From c8066c28e3ec6c2b97af3f69a417f3f25b5f8456 Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Tue, 3 Mar 2020 21:02:57 -0500 Subject: [PATCH 05/81] WIP update color editing component --- .../src/edit-story/components/form/color.js | 224 +++++++++++++----- .../src/edit-story/components/panels/color.js | 5 +- .../components/panels/pageBackground.js | 15 +- .../edit-story/components/panels/textStyle.js | 2 +- assets/src/edit-story/types.js | 8 +- .../edit-story/utils/generatePatternCSS.js | 33 ++- .../utils/test/generatePatternCSS.js | 46 +++- 7 files changed, 236 insertions(+), 97 deletions(-) diff --git a/assets/src/edit-story/components/form/color.js b/assets/src/edit-story/components/form/color.js index 067f28cd2726..3e4d2246d470 100644 --- a/assets/src/edit-story/components/form/color.js +++ b/assets/src/edit-story/components/form/color.js @@ -18,89 +18,184 @@ * External dependencies */ import styled from 'styled-components'; +import { rgba } from 'polished'; import PropTypes from 'prop-types'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { useState, useCallback } from '@wordpress/element'; +import { __, _x } from '@wordpress/i18n'; /** * Internal dependencies */ -import { Input } from '../form'; +import { PatternPropType } from '../../types'; +import generatePatternCSS from '../../utils/generatePatternCSS'; +import ColorPicker from '../colorPicker'; -const StyledInput = styled(Input)` +const Container = styled.div` + display: flex; + align-items: center; + position: relative; +`; + +const ColorPickerWrapper = styled.div` + position: absolute; + right: -20px; + top: 0; +`; + +const Label = styled.div` + width: 60px; + color: ${({ theme }) => rgba(theme.colors.fg.v1, 0.55)}; +`; + +const Box = styled.div` + height: 32px; + width: 122px; + color: ${({ theme }) => rgba(theme.colors.fg.v1, 0.86)}; + background-color: ${({ theme }) => rgba(theme.colors.bg.v0, 0.3)}; + border-radius: 4px; + overflow: hidden; + align-items: center; +`; + +const Preview = styled(Box)` + display: flex; + width: 122px; + cursor: pointer; +`; + +const VisualPreview = styled.div` width: 32px; height: 32px; - border: 1px solid ${({ theme }) => theme.colors.fg.v2} !important; - padding: 0; - margin-left: ${({ label }) => (label ? 12 : 0)}px; +`; - &::-webkit-color-swatch-wrapper { - padding: 0; - } +const TextualPreview = styled.div` + padding-left: 10px; + text-align: center; +`; - &::-webkit-color-swatch { - border: none; - border-radius: 0; - } +const OpacityPreview = styled(Box)` + margin-left: 6px; + width: 54px; + line-height: 32px; + text-align: center; + cursor: ew-resize; `; -const Container = styled.div` - color: ${({ theme }) => theme.colors.fg.v1}; - font-family: ${({ theme }) => theme.fonts.body2.family}; - font-size: ${({ theme }) => theme.fonts.body2.size}; - line-height: ${({ theme }) => theme.fonts.body2.lineHeight}; - letter-spacing: ${({ theme }) => theme.fonts.body2.letterSpacing}; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - margin-left: 5px; - margin-right: 5px; +const transparentStyle = { + backgroundImage: + 'conic-gradient(#fff 0.25turn, #d3d4d4 0turn 0.5turn, #fff 0turn .75turn, #d3d4d4 0turn 1turn)', + backgroundSize: '66.67% 66.67%', +}; - & > span { - margin-left: 12px; +function printRGB(r, g, b) { + const hex = (v) => v.toString(16).padStart(2, '0'); + return `${hex(r)}${hex(g)}${hex(b)}`.toUpperCase(); +} + +function getPreviewStyle(pattern, defaultColor) { + if (!pattern) { + if (!defaultColor) { + return transparentStyle; + } + return { backgroundColor: `#${defaultColor}` }; + } + const isSolidPattern = pattern.type === 'solid' || !pattern.type; + if (!isSolidPattern) { + return generatePatternCSS(pattern, { asString: false }); + } + const { + color: { r, g, b, a }, + } = pattern; + // If opacity is 0, create as transparent: + if (a === 0) { + return transparentStyle; } -`; -function ColorInput({ - onBlur, - onChange, - isMultiple, - opacity, - label, - value, - ...rest -}) { - const placeholder = isMultiple ? __('multiple', 'web-stories') : ''; + // Otherwisecreate color, but with full opacity + return generatePatternCSS({ color: { r, g, b } }, { asString: false }); +} + +function getPreviewOpacity(pattern, specifiedOpacity = 1) { + if (!pattern) { + return specifiedOpacity * 100; + } + const isSolidPattern = pattern.type === 'solid' || !pattern.type; + if (!isSolidPattern) { + return specifiedOpacity * 100; + } + const { + color: { a = 1 }, + } = pattern; + return a * 100; +} + +function getPreviewText(pattern) { + if (!pattern) { + return null; + } + switch (pattern.type) { + case 'radial': + return __('Radial', 'web-stories'); + case 'conic': + return __('Conic', 'web-stories'); + case 'linear': + return __('Linear', 'web-stories'); + case 'solid': + default: + const { + color: { r, g, b, a }, + } = pattern; + if (a === 0) { + return null; + } + return printRGB(r, g, b); + } +} + +function ColorInput({ onChange, isMultiple, opacity, label, value }) { + const previewStyle = getPreviewStyle(isMultiple ? null : value); + const previewText = getPreviewText(value); + const opacityPreview = getPreviewOpacity(value, opacity); + + const [isEditingColor, setIsEditingColor] = useState(false); + + const handleOpenEditing = useCallback(() => { + setIsEditingColor(true); + }, []); + const handleCloseEditing = useCallback(() => { + setIsEditingColor(false); + }, []); return ( - {label} - onChange(evt.target.value, evt)} - onBlur={(evt) => { - if (evt.target.form) { - evt.target.form.dispatchEvent(new window.Event('submit')); - } - if (onBlur) { - onBlur(); - } - }} - /> - {value && {value}} - {opacity && ( - - {opacity * 100} - {'%'} - + {isEditingColor && ( + + + + )} + {label && } + + + + {isMultiple + ? __('Multiple', 'web-stories') + : previewText || + _x('None', '"None" as in no color selected', 'web-stories')} + + + {previewText && ( + + {opacityPreview} + {_x('%', 'Percentage', 'web-stories')} + )} ); @@ -108,17 +203,16 @@ function ColorInput({ ColorInput.propTypes = { label: PropTypes.string, - value: PropTypes.any.isRequired, + value: PatternPropType, isMultiple: PropTypes.bool, onChange: PropTypes.func.isRequired, - onBlur: PropTypes.func, opacity: PropTypes.number, - disabled: PropTypes.bool, }; ColorInput.defaultProps = { - disabled: false, + defaultColor: null, isMultiple: false, + opacity: null, }; export default ColorInput; diff --git a/assets/src/edit-story/components/panels/color.js b/assets/src/edit-story/components/panels/color.js index 88d18ec4c192..b984829843f8 100644 --- a/assets/src/edit-story/components/panels/color.js +++ b/assets/src/edit-story/components/panels/color.js @@ -28,7 +28,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { InputGroup } from '../form'; +import { Color } from '../form'; import { SimplePanel } from './panel'; import getCommonValue from './utils/getCommonValue'; @@ -48,8 +48,7 @@ function ColorPanel({ selectedElements, onSetProperties }) { title={__('Color', 'web-stories')} onSubmit={handleSubmit} > - setColor(currentBackground), [currentBackground]); const handleChange = useCallback( @@ -48,12 +46,7 @@ function PageBackgroundPanel() { return ( {/* TODO: Replace with custom color picker component once implemented */} - + ); } diff --git a/assets/src/edit-story/components/panels/textStyle.js b/assets/src/edit-story/components/panels/textStyle.js index aff5a95c1af6..d9304591582a 100644 --- a/assets/src/edit-story/components/panels/textStyle.js +++ b/assets/src/edit-story/components/panels/textStyle.js @@ -302,7 +302,7 @@ function TextStylePanel({ selectedElements, onSetProperties }) { setState({ ...state, backgroundColor: value })} opacity={1} /> diff --git a/assets/src/edit-story/types.js b/assets/src/edit-story/types.js index 78a211860054..0637be55dca1 100644 --- a/assets/src/edit-story/types.js +++ b/assets/src/edit-story/types.js @@ -92,8 +92,8 @@ StoryPropTypes.elements.video = PropTypes.shape({ StoryPropTypes.elements.text = PropTypes.shape({ ...StoryElementPropsTypes, content: PropTypes.string, - color: PropTypes.string, - backgroundColor: PropTypes.string, + color: PatternPropType, + backgroundColor: PatternPropType, fontFamily: PropTypes.string, fontFallback: PropTypes.array, fontSize: PropTypes.number, @@ -107,7 +107,7 @@ StoryPropTypes.elements.text = PropTypes.shape({ StoryPropTypes.elements.square = PropTypes.shape({ ...StoryElementPropsTypes, - backgroundColor: PropTypes.string, + backgroundColor: PatternPropType, }); StoryPropTypes.elements.background = PropTypes.shape({ @@ -133,7 +133,7 @@ export const PatternPropType = PropTypes.shape({ type: PropTypes.oneOf(['solid', 'linear', 'gradient', 'conic']), color: HexPropType, stops: PropTypes.arrayOf(ColorStopPropType), - rotation: PropTypes.number.isRequired, + rotation: PropTypes.number, center: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, diff --git a/assets/src/edit-story/utils/generatePatternCSS.js b/assets/src/edit-story/utils/generatePatternCSS.js index 2ffd50e51154..a05629c9c5ca 100644 --- a/assets/src/edit-story/utils/generatePatternCSS.js +++ b/assets/src/edit-story/utils/generatePatternCSS.js @@ -66,16 +66,24 @@ function getStopList(stops, isAngular = false) { * Generate CSS from a Pattern. * * @param {Object} pattern Patterns as describe by the Pattern type - * @param {string} property Type of CSS to generate. Defaults to 'background', + * @param {Object} options Optional settings + * @param {string} options.property Type of CSS to generate. Defaults to 'background', * but can also be 'color', 'fill' or 'stroke'. + * @param {boolean} options.asString If true (default) generates a string, if false a style object, * - * @return {string} CSS declaration, e.g. 'fill: transparent' or - * 'background-image: radial-gradient(red, blue)'. + * @return {string | Object} CSS declaration as string or object, e.g. 'fill: transparent' or + * {backgroundImage: 'radial-gradient(red, blue)'}. */ -function generatePatternCSS(pattern, property = 'background') { +function generatePatternCSS( + pattern, + { property = 'background', asString = true } = {} +) { const isBackground = property === 'background'; if (pattern === null) { - return `${property}: transparent`; + if (asString) { + return `${property}: transparent`; + } + return { [property]: 'transparent' }; } const { type = 'solid' } = pattern; @@ -93,8 +101,12 @@ function generatePatternCSS(pattern, property = 'background') { const { color: { r, g, b, a = 1 }, } = pattern; - const propertyPostfix = isBackground ? '-color' : ''; - return `${property}${propertyPostfix}: ${rgba(r, g, b, a)}`; + if (asString) { + const stringPropertyPostfix = isBackground ? '-color' : ''; + return `${property}${stringPropertyPostfix}: ${rgba(r, g, b, a)}`; + } + const objectPropertyPostfix = isBackground ? 'Color' : ''; + return { [`${property}${objectPropertyPostfix}`]: rgba(r, g, b, a) }; } const { stops } = pattern; @@ -102,8 +114,11 @@ function generatePatternCSS(pattern, property = 'background') { const description = getGradientDescription(pattern); const stopList = getStopList(stops, type === 'conic'); const parms = description ? [description, ...stopList] : stopList; - - return `background-image: ${func}(${parms.join(', ')})`; + const value = `${func}(${parms.join(', ')})`; + if (asString) { + return `background-image: ${value}`; + } + return { backgroundImage: value }; } export default generatePatternCSS; diff --git a/assets/src/edit-story/utils/test/generatePatternCSS.js b/assets/src/edit-story/utils/test/generatePatternCSS.js index 65352b56d848..9cbf2a723720 100644 --- a/assets/src/edit-story/utils/test/generatePatternCSS.js +++ b/assets/src/edit-story/utils/test/generatePatternCSS.js @@ -55,9 +55,30 @@ describe('generatePatternCSS', () => { it('should be able to render non-background properties', () => { expect( - generatePatternCSS({ color: { r: 255, g: 0, b: 0 } }, 'fill') + generatePatternCSS( + { color: { r: 255, g: 0, b: 0 } }, + { property: 'fill' } + ) ).toBe('fill: #f00'); }); + + it('should be able to render a style object', () => { + expect( + generatePatternCSS( + { color: { r: 255, g: 0, b: 0 } }, + { asString: false } + ) + ).toStrictEqual({ backgroundColor: '#f00' }); + }); + + it('should be able to render a non-background style object', () => { + expect( + generatePatternCSS( + { color: { r: 255, g: 0, b: 0 } }, + { property: 'fill', asString: false } + ) + ).toStrictEqual({ fill: '#f00' }); + }); }); describe('given any gradient', () => { @@ -67,14 +88,31 @@ describe('generatePatternCSS', () => { { type: 'linear', stops: [ - { color: { r: 255, g: 0, b: 0 }, position: 0.3 }, - { color: { r: 0, g: 0, b: 255 }, position: 0.7 }, + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, ], }, - 'fill' + { property: 'fill' } ) ).toThrow(/only generate solid/i); }); + + it('should be able to render a style object', () => { + expect( + generatePatternCSS( + { + type: 'linear', + stops: [ + { color: { r: 255, g: 0, b: 0 }, position: 0 }, + { color: { r: 0, g: 0, b: 255 }, position: 1 }, + ], + }, + { asString: false } + ) + ).toStrictEqual({ + backgroundImage: 'linear-gradient(#f00 0%, #00f 100%)', + }); + }); }); describe('given a linear gradient', () => { From 1170c8ebd59579ad549a7b8839e153bc69766391 Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Tue, 3 Mar 2020 21:42:05 -0500 Subject: [PATCH 06/81] Now displaying color picker next to design panel --- assets/src/edit-story/app/layout/index.js | 1 - .../src/edit-story/components/form/color.js | 41 +++----- .../colorPickerProvider.js | 98 +++++++++++++++++++ .../inspector/colorPickerProvider/context.js | 22 +++++ .../inspector/colorPickerProvider/index.js | 17 ++++ .../colorPickerProvider/useColorPicker.js | 31 ++++++ .../components/inspector/inspectorLayout.js | 19 ++-- .../components/panels/pageBackground.js | 2 +- 8 files changed, 195 insertions(+), 36 deletions(-) create mode 100644 assets/src/edit-story/components/inspector/colorPickerProvider/colorPickerProvider.js create mode 100644 assets/src/edit-story/components/inspector/colorPickerProvider/context.js create mode 100644 assets/src/edit-story/components/inspector/colorPickerProvider/index.js create mode 100644 assets/src/edit-story/components/inspector/colorPickerProvider/useColorPicker.js diff --git a/assets/src/edit-story/app/layout/index.js b/assets/src/edit-story/app/layout/index.js index 899898081273..33649831d10a 100644 --- a/assets/src/edit-story/app/layout/index.js +++ b/assets/src/edit-story/app/layout/index.js @@ -52,7 +52,6 @@ const Editor = styled.div` const Area = styled.div` grid-area: ${({ area }) => area}; position: relative; - overflow: hidden; z-index: ${({ area }) => (area === 'canv' ? 1 : 2)}; `; diff --git a/assets/src/edit-story/components/form/color.js b/assets/src/edit-story/components/form/color.js index 3e4d2246d470..e873934d0343 100644 --- a/assets/src/edit-story/components/form/color.js +++ b/assets/src/edit-story/components/form/color.js @@ -24,7 +24,7 @@ import PropTypes from 'prop-types'; /** * WordPress dependencies */ -import { useState, useCallback } from '@wordpress/element'; +import { useRef, useCallback } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; /** @@ -32,18 +32,11 @@ import { __, _x } from '@wordpress/i18n'; */ import { PatternPropType } from '../../types'; import generatePatternCSS from '../../utils/generatePatternCSS'; -import ColorPicker from '../colorPicker'; +import useColorPicker from '../inspector/colorPickerProvider/useColorPicker'; const Container = styled.div` display: flex; align-items: center; - position: relative; -`; - -const ColorPickerWrapper = styled.div` - position: absolute; - right: -20px; - top: 0; `; const Label = styled.div` @@ -161,26 +154,22 @@ function ColorInput({ onChange, isMultiple, opacity, label, value }) { const previewText = getPreviewText(value); const opacityPreview = getPreviewOpacity(value, opacity); - const [isEditingColor, setIsEditingColor] = useState(false); + const { + actions: { showColorPickerAt, hideColorPicker }, + } = useColorPicker(); + + const ref = useRef(); const handleOpenEditing = useCallback(() => { - setIsEditingColor(true); - }, []); - const handleCloseEditing = useCallback(() => { - setIsEditingColor(false); - }, []); + showColorPickerAt(ref.current, { + color: value, + onChange: () => onChange.bind(), + onClose: hideColorPicker, + }); + }, [showColorPickerAt, hideColorPicker, value, onChange]); return ( - - {isEditingColor && ( - - - - )} + {label && } @@ -188,7 +177,7 @@ function ColorInput({ onChange, isMultiple, opacity, label, value }) { {isMultiple ? __('Multiple', 'web-stories') : previewText || - _x('None', '"None" as in no color selected', 'web-stories')} + _x('None', 'No color or gradient selected', 'web-stories')} {previewText && ( diff --git a/assets/src/edit-story/components/inspector/colorPickerProvider/colorPickerProvider.js b/assets/src/edit-story/components/inspector/colorPickerProvider/colorPickerProvider.js new file mode 100644 index 000000000000..3b6c15b6e1e1 --- /dev/null +++ b/assets/src/edit-story/components/inspector/colorPickerProvider/colorPickerProvider.js @@ -0,0 +1,98 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +/** + * WordPress dependencies + */ +import { useState, useCallback, useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import ColorPicker from '../../colorPicker'; +import Context from './context'; + +const Outer = styled.div` + height: 100%; + position: relative; +`; + +const ColorPickerWrapper = styled.div` + position: absolute; + right: 100%; + top: ${({ top }) => `${top}px`}; +`; + +function ColorPickerProvider({ children }) { + const [colorPickerState, setColorPickerState] = useState(null); + + const hasColorPicker = colorPickerState !== null; + const colorPickerOffset = hasColorPicker && colorPickerState.offset; + const colorPickerProps = hasColorPicker && colorPickerState.props; + + const ref = useRef(); + + const showColorPickerAt = useCallback((node, props) => { + const offset = + node.getBoundingClientRect().y - ref.current.getBoundingClientRect().y; + setColorPickerState({ offset, props }); + }, []); + + const hideColorPicker = useCallback(() => { + setColorPickerState(null); + }, []); + + const value = { + state: {}, + actions: { + showColorPickerAt, + hideColorPicker, + }, + }; + + return ( + /* Any node is needed here to get proper offsets */ + + + {hasColorPicker && ( + + {}} + {...colorPickerProps} + /> + + )} + {children} + + + ); +} + +ColorPickerProvider.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, +}; + +export default ColorPickerProvider; diff --git a/assets/src/edit-story/components/inspector/colorPickerProvider/context.js b/assets/src/edit-story/components/inspector/colorPickerProvider/context.js new file mode 100644 index 000000000000..ce978451b794 --- /dev/null +++ b/assets/src/edit-story/components/inspector/colorPickerProvider/context.js @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +export default createContext({ state: {}, actions: {} }); diff --git a/assets/src/edit-story/components/inspector/colorPickerProvider/index.js b/assets/src/edit-story/components/inspector/colorPickerProvider/index.js new file mode 100644 index 000000000000..6f62969932c7 --- /dev/null +++ b/assets/src/edit-story/components/inspector/colorPickerProvider/index.js @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './colorPickerProvider'; diff --git a/assets/src/edit-story/components/inspector/colorPickerProvider/useColorPicker.js b/assets/src/edit-story/components/inspector/colorPickerProvider/useColorPicker.js new file mode 100644 index 000000000000..05106ee71db7 --- /dev/null +++ b/assets/src/edit-story/components/inspector/colorPickerProvider/useColorPicker.js @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * WordPress dependencies + */ +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Context from './context'; + +function useColorPicker() { + return useContext(Context); +} + +export default useColorPicker; diff --git a/assets/src/edit-story/components/inspector/inspectorLayout.js b/assets/src/edit-story/components/inspector/inspectorLayout.js index 403b3790982a..90efec200d9a 100644 --- a/assets/src/edit-story/components/inspector/inspectorLayout.js +++ b/assets/src/edit-story/components/inspector/inspectorLayout.js @@ -31,6 +31,7 @@ import { useEscapeToBlurEffect } from '../keyboard'; import useInspector from './useInspector'; import InspectorTabs from './inspectorTabs'; import InspectorContent from './inspectorContent'; +import ColorPickerProvider from './colorPickerProvider'; const Layout = styled.div` height: 100%; @@ -61,14 +62,16 @@ function InspectorLayout() { } = useInspector(); useEscapeToBlurEffect(ref); return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/assets/src/edit-story/components/panels/pageBackground.js b/assets/src/edit-story/components/panels/pageBackground.js index 335429bc9f55..3d7f468159c1 100644 --- a/assets/src/edit-story/components/panels/pageBackground.js +++ b/assets/src/edit-story/components/panels/pageBackground.js @@ -27,7 +27,7 @@ import { useStory } from '../../app'; import { Color } from '../form'; import { SimplePanel } from './panel'; -const DEFAULT_COLOR = { color: { r: 255, g: 255, b: 255, a: 0 } }; +const DEFAULT_COLOR = { color: { r: 255, g: 255, b: 255 } }; function PageBackgroundPanel() { const { state: { currentPage }, From 14b53d7e86ea39022381567adf68ca2b12d83303 Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Tue, 3 Mar 2020 22:46:02 -0500 Subject: [PATCH 07/81] Fix color input for texts --- .../components/canvas/displayLayer.js | 7 ++-- .../components/canvas/pagepreview/index.js | 7 ++-- .../components/colorPicker/index.js | 13 +++---- .../components/colorPicker/useColor.js | 31 +++++++++++++++-- .../src/edit-story/components/form/color.js | 24 +++++-------- .../components/panels/pageBackground.js | 1 - .../edit-story/components/panels/textStyle.js | 34 ++++++++++++++----- .../src/edit-story/elements/shared/index.js | 5 +-- .../edit-story/utils/generatePatternCSS.js | 2 +- 9 files changed, 80 insertions(+), 44 deletions(-) diff --git a/assets/src/edit-story/components/canvas/displayLayer.js b/assets/src/edit-story/components/canvas/displayLayer.js index 9394c310c612..3c43f52b22d2 100644 --- a/assets/src/edit-story/components/canvas/displayLayer.js +++ b/assets/src/edit-story/components/canvas/displayLayer.js @@ -23,16 +23,19 @@ import styled from 'styled-components'; * Internal dependencies */ import { useStory } from '../../app'; +import generatePatternCSS from '../../utils/generatePatternCSS'; import useCanvas from './useCanvas'; import DisplayElement from './displayElement'; import { Layer, PageArea } from './layout'; +const DEFAULT_COLOR = { color: { r: 255, g: 255, b: 255 } }; + const DisplayPageArea = styled(PageArea).attrs({ className: 'container', overflowAllowed: false, })` - background-color: ${({ theme, backgroundColor }) => - backgroundColor || theme.colors.fg.v1}; + ${({ backgroundColor }) => + generatePatternCSS(backgroundColor || DEFAULT_COLOR)}; `; function DisplayLayer() { diff --git a/assets/src/edit-story/components/canvas/pagepreview/index.js b/assets/src/edit-story/components/canvas/pagepreview/index.js index b047d70cd8ce..674cf662dcca 100644 --- a/assets/src/edit-story/components/canvas/pagepreview/index.js +++ b/assets/src/edit-story/components/canvas/pagepreview/index.js @@ -24,12 +24,15 @@ import PropTypes from 'prop-types'; * Internal dependencies */ import useStory from '../../../app/story/useStory'; +import generatePatternCSS from '../../../utils/generatePatternCSS'; import { TransformProvider } from '../../transform'; import { UnitsProvider } from '../../../units'; import DisplayElement from '../displayElement'; const PAGE_THUMB_OUTLINE = 2; +const DEFAULT_COLOR = { color: { r: 255, g: 255, b: 255 } }; + const Page = styled.button` padding: 0; margin: 0; @@ -39,8 +42,8 @@ const Page = styled.button` isActive ? theme.colors.selection : theme.colors.bg.v1}; height: ${({ height }) => height}px; width: ${({ width }) => width}px; - background-color: ${({ theme, backgroundColor }) => - backgroundColor || theme.colors.fg.v1}; + ${({ backgroundColor }) => + generatePatternCSS(backgroundColor || DEFAULT_COLOR)}; flex: none; transition: width 0.2s ease, height 0.2s ease; diff --git a/assets/src/edit-story/components/colorPicker/index.js b/assets/src/edit-story/components/colorPicker/index.js index fdf0c2332914..9e7eaedacfd6 100644 --- a/assets/src/edit-story/components/colorPicker/index.js +++ b/assets/src/edit-story/components/colorPicker/index.js @@ -25,7 +25,7 @@ import { rgba } from 'polished'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -75,18 +75,13 @@ function ColorPicker({ color, onChange, onClose }) { } = useColor(); useEffect(() => { - if (generatedColor && generatedColor !== color) { + if (generatedColor) { onChange(generatedColor); } }, [color, generatedColor, onChange]); - // When color updates from outside, reload in picker unless it's the same at the last export color - const generatedColorRef = useRef(generatedColor); useEffect(() => { - generatedColorRef.current = generatedColor; - }, [generatedColor]); - useEffect(() => { - if (color && color !== generatedColorRef.current) { + if (color) { load(color); } }, [color, load]); @@ -114,7 +109,7 @@ function ColorPicker({ color, onChange, onClose }) { ColorPicker.propTypes = { onChange: PropTypes.func.isRequired, onClose: PropTypes.func, - color: PatternPropType.isRequired, + color: PatternPropType, }; ColorPicker.defaultProps = { diff --git a/assets/src/edit-story/components/colorPicker/useColor.js b/assets/src/edit-story/components/colorPicker/useColor.js index bdce2c7727c8..2a97f66d83a4 100644 --- a/assets/src/edit-story/components/colorPicker/useColor.js +++ b/assets/src/edit-story/components/colorPicker/useColor.js @@ -26,6 +26,7 @@ export const TYPE_CONIC = 'conic'; const initialState = { type: TYPE_SOLID, + regenerate: false, stops: [], currentColor: { r: 0, @@ -46,6 +47,7 @@ const reducer = { case TYPE_LINEAR: return { ...state, + regenerate: false, color: stops[0].color, stops, angle, @@ -54,6 +56,7 @@ const reducer = { case TYPE_RADIAL: return { ...state, + regenerate: false, color: stops[0].color, stops, center, @@ -63,6 +66,7 @@ const reducer = { case TYPE_CONIC: return { ...state, + regenerate: false, color: stops[0].color, stops, angle, @@ -73,6 +77,7 @@ const reducer = { default: return { ...state, + regenerate: false, currentColor: color, }; } @@ -80,6 +85,7 @@ const reducer = { setToSolid: (state) => ({ ...state, type: TYPE_SOLID, + regenerate: true, }), setToGradient: (state, { payload }) => ({ ...state, @@ -94,9 +100,10 @@ const reducer = { foo: payload, }), updateCurrentColor: (state, { payload: { rgb } }) => { - const currentColor = `rgba(${Object.values(rgb).join(',')})`; + const currentColor = { ...rgb }; const newState = { ...state, + regenerate: true, currentColor, }; @@ -135,11 +142,29 @@ const reducer = { }), }; +function regenerateColor(pattern) { + const { regenerate, type } = pattern; + if (!regenerate) { + return null; + } + + switch (type) { + case TYPE_SOLID: { + const { + currentColor: { r, g, b, a }, + } = pattern; + const minColor = a === 1 ? { r, g, b } : { r, g, b, a }; + return { color: minColor }; + } + default: + return null; + } +} + function useColor() { const [state, actions] = useReduction(initialState, reducer); - // TODO: Generate compact output for color - const generatedColor = {}; + const generatedColor = regenerateColor(state); return { state: { diff --git a/assets/src/edit-story/components/form/color.js b/assets/src/edit-story/components/form/color.js index e873934d0343..6a84f7563e86 100644 --- a/assets/src/edit-story/components/form/color.js +++ b/assets/src/edit-story/components/form/color.js @@ -39,11 +39,6 @@ const Container = styled.div` align-items: center; `; -const Label = styled.div` - width: 60px; - color: ${({ theme }) => rgba(theme.colors.fg.v1, 0.55)}; -`; - const Box = styled.div` height: 32px; width: 122px; @@ -76,6 +71,7 @@ const OpacityPreview = styled(Box)` line-height: 32px; text-align: center; cursor: ew-resize; + visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')}; `; const transparentStyle = { @@ -149,10 +145,10 @@ function getPreviewText(pattern) { } } -function ColorInput({ onChange, isMultiple, opacity, label, value }) { +function ColorInput({ onChange, isMultiple, opacity, value }) { const previewStyle = getPreviewStyle(isMultiple ? null : value); const previewText = getPreviewText(value); - const opacityPreview = getPreviewOpacity(value, opacity); + const previewOpacity = getPreviewOpacity(value, opacity); const { actions: { showColorPickerAt, hideColorPicker }, @@ -163,14 +159,13 @@ function ColorInput({ onChange, isMultiple, opacity, label, value }) { const handleOpenEditing = useCallback(() => { showColorPickerAt(ref.current, { color: value, - onChange: () => onChange.bind(), + onChange, onClose: hideColorPicker, }); }, [showColorPickerAt, hideColorPicker, value, onChange]); return ( - {label && } @@ -180,18 +175,15 @@ function ColorInput({ onChange, isMultiple, opacity, label, value }) { _x('None', 'No color or gradient selected', 'web-stories')} - {previewText && ( - - {opacityPreview} - {_x('%', 'Percentage', 'web-stories')} - - )} + + {previewOpacity} + {_x('%', 'Percentage', 'web-stories')} + ); } ColorInput.propTypes = { - label: PropTypes.string, value: PatternPropType, isMultiple: PropTypes.bool, onChange: PropTypes.func.isRequired, diff --git a/assets/src/edit-story/components/panels/pageBackground.js b/assets/src/edit-story/components/panels/pageBackground.js index 3d7f468159c1..0e0657cdca80 100644 --- a/assets/src/edit-story/components/panels/pageBackground.js +++ b/assets/src/edit-story/components/panels/pageBackground.js @@ -45,7 +45,6 @@ function PageBackgroundPanel() { ); return ( - {/* TODO: Replace with custom color picker component once implemented */} ); diff --git a/assets/src/edit-story/components/panels/textStyle.js b/assets/src/edit-story/components/panels/textStyle.js index d9304591582a..657a1f73656f 100644 --- a/assets/src/edit-story/components/panels/textStyle.js +++ b/assets/src/edit-story/components/panels/textStyle.js @@ -89,8 +89,10 @@ function TextStylePanel({ selectedElements, onSetProperties }) { const fontWeights = getCommonValue(selectedElements, 'fontWeights'); const fontStyle = getCommonValue(selectedElements, 'fontStyle') || 'normal'; const fontFallback = getCommonValue(selectedElements, 'fontFallback'); - const backgroundColor = - getCommonValue(selectedElements, 'backgroundColor') || '#000000'; + + // TODO make this work for multiple elements! + const backgroundColor = selectedElements[0].backgroundColor || null; + const color = selectedElements[0].color || null; const { state: { fonts }, @@ -108,22 +110,30 @@ function TextStylePanel({ selectedElements, onSetProperties }) { lineHeight, padding, backgroundColor, + color, }); const [lockRatio, setLockRatio] = useState(true); useEffect(() => { - setState({ textAlign, letterSpacing, lineHeight, padding }); + setState((oldState) => ({ + ...oldState, + textAlign, + letterSpacing, + lineHeight, + padding, + })); }, [textAlign, letterSpacing, lineHeight, padding]); useEffect(() => { const currentFontWeights = getFontWeight(fontFamily); const currentFontFallback = getFontFallback(fontFamily); - setState({ + setState((oldState) => ({ + ...oldState, fontFamily, fontStyle, fontSize, fontWeight, fontWeights: currentFontWeights, fontFallback: currentFontFallback, - }); + })); // eslint-disable-next-line react-hooks/exhaustive-deps }, [fontFamily, fontStyle, fontSize, fontWeight, getFontWeight]); const handleSubmit = (evt) => { @@ -147,7 +157,9 @@ function TextStylePanel({ selectedElements, onSetProperties }) { }; }); - evt.preventDefault(); + if (evt) { + evt.preventDefault(); + } }; const fontStyles = [ @@ -299,12 +311,18 @@ function TextStylePanel({ selectedElements, onSetProperties }) { } /> - + + setState({ ...state, color: value })} + /> + + + setState({ ...state, backgroundColor: value })} - opacity={1} /> {/* TODO: Update padding logic */} diff --git a/assets/src/edit-story/elements/shared/index.js b/assets/src/edit-story/elements/shared/index.js index 38bb7d3f8498..d8f791c6e254 100644 --- a/assets/src/edit-story/elements/shared/index.js +++ b/assets/src/edit-story/elements/shared/index.js @@ -22,6 +22,7 @@ import styled, { css } from 'styled-components'; /** * Internal dependencies */ +import generatePatternCSS from '../../utils/generatePatternCSS'; export { default as getMediaProps } from './getMediaProps'; export { default as getFocalFromOffset } from './getFocalFromOffset'; export { default as EditPanMovable } from './editPanMovable'; @@ -71,11 +72,11 @@ export const elementWithRotation = css` `; export const elementWithBackgroundColor = css` - background-color: ${({ backgroundColor }) => backgroundColor}; + ${({ backgroundColor }) => generatePatternCSS(backgroundColor)}; `; export const elementWithFontColor = css` - color: ${({ color }) => color}; + ${({ color }) => generatePatternCSS(color, { property: 'color' })}; `; export const elementWithFont = css` diff --git a/assets/src/edit-story/utils/generatePatternCSS.js b/assets/src/edit-story/utils/generatePatternCSS.js index a05629c9c5ca..ce6c6541cc50 100644 --- a/assets/src/edit-story/utils/generatePatternCSS.js +++ b/assets/src/edit-story/utils/generatePatternCSS.js @@ -75,7 +75,7 @@ function getStopList(stops, isAngular = false) { * {backgroundImage: 'radial-gradient(red, blue)'}. */ function generatePatternCSS( - pattern, + pattern = null, { property = 'background', asString = true } = {} ) { const isBackground = property === 'background'; From a45421b7dc3b607320e09eb7814e15dd49dc27c9 Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Tue, 3 Mar 2020 22:59:06 -0500 Subject: [PATCH 08/81] Fix color for square too, plus fix text output --- .../library/panes/shapes/shapesPane.js | 4 +-- .../src/edit-story/components/panels/style.js | 29 +++++++------------ .../src/edit-story/elements/square/output.js | 5 ++-- assets/src/edit-story/elements/text/output.js | 5 ++-- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/assets/src/edit-story/components/library/panes/shapes/shapesPane.js b/assets/src/edit-story/components/library/panes/shapes/shapesPane.js index b5e95e061aaf..9e411d073447 100644 --- a/assets/src/edit-story/components/library/panes/shapes/shapesPane.js +++ b/assets/src/edit-story/components/library/panes/shapes/shapesPane.js @@ -77,7 +77,7 @@ function ShapesPane(props) { key={'square'} onClick={() => { insertElement('square', { - backgroundColor: '#333', + backgroundColor: { color: { r: 51, g: 51, b: 51 } }, width: 200, height: 200, x: 5, @@ -95,7 +95,7 @@ function ShapesPane(props) { key={mask.type} onClick={() => { insertElement('square', { - backgroundColor: '#333', + backgroundColor: { color: { r: 51, g: 51, b: 51 } }, width: 200, height: 200, x: 5, diff --git a/assets/src/edit-story/components/panels/style.js b/assets/src/edit-story/components/panels/style.js index dc7d87fab925..9a6cc995ffb0 100644 --- a/assets/src/edit-story/components/panels/style.js +++ b/assets/src/edit-story/components/panels/style.js @@ -22,7 +22,7 @@ import PropTypes from 'prop-types'; /** * WordPress dependencies */ -import { useEffect, useState } from '@wordpress/element'; +import { useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -34,26 +34,19 @@ import getCommonValue from './utils/getCommonValue'; function StylePanel({ selectedElements, onSetProperties }) { const backgroundColor = getCommonValue(selectedElements, 'backgroundColor'); - const [state, setState] = useState({ backgroundColor }); - useEffect(() => { - setState({ backgroundColor }); - }, [backgroundColor]); - const handleSubmit = (evt) => { - onSetProperties(state); - evt.preventDefault(); - }; + const handleChange = useCallback( + (newColor) => { + onSetProperties({ backgroundColor: newColor }); + }, + [onSetProperties] + ); return ( - - + + setState({ ...state, backgroundColor: value })} - opacity={1} + onChange={handleChange} /> diff --git a/assets/src/edit-story/elements/square/output.js b/assets/src/edit-story/elements/square/output.js index 6da32cddca83..5ffa23f77488 100644 --- a/assets/src/edit-story/elements/square/output.js +++ b/assets/src/edit-story/elements/square/output.js @@ -18,14 +18,13 @@ * Internal dependencies */ import StoryPropTypes from '../../types'; +import generatePatternCSS from '../../utils/generatePatternCSS'; /** * Returns AMP HTML for saving into post content for displaying in the FE. */ function SquareOutput({ element: { backgroundColor } }) { - const style = { - background: backgroundColor, - }; + const style = generatePatternCSS(backgroundColor, { asString: false }); return
; } diff --git a/assets/src/edit-story/elements/text/output.js b/assets/src/edit-story/elements/text/output.js index 55c505e772a5..d777d5a03042 100644 --- a/assets/src/edit-story/elements/text/output.js +++ b/assets/src/edit-story/elements/text/output.js @@ -19,6 +19,7 @@ */ import StoryPropTypes from '../../types'; import { dataToEditorY } from '../../units'; +import generatePatternCSS from '../../utils/generatePatternCSS'; import { generateFontFamily } from './util'; /** @@ -45,13 +46,13 @@ function TextOutput({ fontStyle: fontStyle ? fontStyle : null, fontFamily: generateFontFamily(fontFamily, fontFallback), fontWeight: fontWeight ? fontWeight : null, - background: backgroundColor, - color, lineHeight, letterSpacing: letterSpacing ? letterSpacing + 'em' : null, padding: padding ? padding + '%' : null, textAlign: textAlign ? textAlign : null, whiteSpace: 'pre-wrap', + ...generatePatternCSS(backgroundColor, { asString: false }), + ...generatePatternCSS(color, { property: 'color', asString: false }), }; return ( From ec75c2f833c235aa33fb9da27e5f4b305227602f Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Tue, 3 Mar 2020 23:01:01 -0500 Subject: [PATCH 09/81] Fix generated page background --- assets/src/edit-story/output/page.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/src/edit-story/output/page.js b/assets/src/edit-story/output/page.js index ad588326c15f..6b68787eb776 100644 --- a/assets/src/edit-story/output/page.js +++ b/assets/src/edit-story/output/page.js @@ -18,6 +18,7 @@ * Internal dependencies */ import StoryPropTypes from '../types'; +import generatePatternCSS from '../utils/generatePatternCSS'; import { PAGE_WIDTH, PAGE_HEIGHT } from '../constants'; import OutputElement from './element'; @@ -33,7 +34,9 @@ function OutputPage({ page }) { fontSize: `calc(100 * min(var(--story-page-vh), var(--story-page-vw) * ${PAGE_HEIGHT / PAGE_WIDTH}))`, }; - const backgroundStyles = backgroundColor ? { backgroundColor } : null; + const backgroundStyles = generatePatternCSS(backgroundColor, { + asString: false, + }); const backgroundNonFullbleedElements = page.elements.filter( (element) => element.id === page.backgroundElementId && From cd913922bc25eb4231cf1afdcd424f000ccbda55 Mon Sep 17 00:00:00 2001 From: Morten Barklund Date: Wed, 4 Mar 2020 14:29:30 -0500 Subject: [PATCH 10/81] Fix all unit tests involving color picker and panels --- .../src/edit-story/components/form/color.js | 6 ++- .../edit-story/components/form/test/color.js | 10 ++-- .../components/panels/pageBackground.js | 6 ++- .../src/edit-story/components/panels/style.js | 1 + .../components/panels/test/color.js | 4 +- .../components/panels/test/pageBackground.js | 20 ++----- .../components/panels/test/style.js | 29 +++------- assets/src/edit-story/types.js | 54 +++++++++---------- 8 files changed, 56 insertions(+), 74 deletions(-) diff --git a/assets/src/edit-story/components/form/color.js b/assets/src/edit-story/components/form/color.js index 1240c8ff823b..fb0c88868aaf 100644 --- a/assets/src/edit-story/components/form/color.js +++ b/assets/src/edit-story/components/form/color.js @@ -145,7 +145,7 @@ function getPreviewText(pattern) { } } -function ColorInput({ onChange, isMultiple, opacity, value }) { +function ColorInput({ onChange, isMultiple, opacity, value, label }) { const previewStyle = getPreviewStyle(isMultiple ? null : value); const previewText = getPreviewText(value); const previewOpacity = getPreviewOpacity(value, opacity); @@ -168,7 +168,7 @@ function ColorInput({ onChange, isMultiple, opacity, value }) { - + {isMultiple ? __('Multiple', 'web-stories') : previewText || @@ -188,12 +188,14 @@ ColorInput.propTypes = { isMultiple: PropTypes.bool, onChange: PropTypes.func.isRequired, opacity: PropTypes.number, + label: PropTypes.string, }; ColorInput.defaultProps = { defaultColor: null, isMultiple: false, opacity: null, + labelledBy: null, }; export default ColorInput; diff --git a/assets/src/edit-story/components/form/test/color.js b/assets/src/edit-story/components/form/test/color.js index 9d498b210196..075b707add28 100644 --- a/assets/src/edit-story/components/form/test/color.js +++ b/assets/src/edit-story/components/form/test/color.js @@ -35,19 +35,19 @@ describe('Form/Color', () => { const onChangeMock = jest.fn(); const onBlurMock = jest.fn(); - const { getByTestId } = arrange( + const { getByLabelText } = arrange( ); - const input = getByTestId('color'); + const element = getByLabelText('color'); - expect(input).toBeDefined(); + expect(element.innerHTML).toStrictEqual('00FF00'); }); // TODO: More tests should be defined as soon as we start https://github.com/google/web-stories-wp/issues/378 }); diff --git a/assets/src/edit-story/components/panels/pageBackground.js b/assets/src/edit-story/components/panels/pageBackground.js index 8da5c8eaf196..782b305b02f3 100644 --- a/assets/src/edit-story/components/panels/pageBackground.js +++ b/assets/src/edit-story/components/panels/pageBackground.js @@ -49,7 +49,11 @@ function PageBackgroundPanel() { ); return ( - + ); } diff --git a/assets/src/edit-story/components/panels/style.js b/assets/src/edit-story/components/panels/style.js index 3292b491f330..c22f75ff41e6 100644 --- a/assets/src/edit-story/components/panels/style.js +++ b/assets/src/edit-story/components/panels/style.js @@ -47,6 +47,7 @@ function StylePanel({ selectedElements, onSetProperties }) { value={backgroundColor} isMultiple={backgroundColor === ''} onChange={handleChange} + label={__('Background color', 'web-stories')} /> diff --git a/assets/src/edit-story/components/panels/test/color.js b/assets/src/edit-story/components/panels/test/color.js index 5e5356fdb1e1..dc3984161559 100644 --- a/assets/src/edit-story/components/panels/test/color.js +++ b/assets/src/edit-story/components/panels/test/color.js @@ -34,14 +34,14 @@ describe('Panels/Color', () => { it('should render panel', () => { const { getByLabelText } = arrange( null} /> ); const element = getByLabelText('Color'); - expect(element).toBeDefined(); + expect(element.innerHTML).toStrictEqual('FF00FF'); }); // TODO: More tests should be defined as soon as we start https://github.com/google/web-stories-wp/issues/378 }); diff --git a/assets/src/edit-story/components/panels/test/pageBackground.js b/assets/src/edit-story/components/panels/test/pageBackground.js index 6a5ed1ad7dbd..366e4e166254 100644 --- a/assets/src/edit-story/components/panels/test/pageBackground.js +++ b/assets/src/edit-story/components/panels/test/pageBackground.js @@ -17,7 +17,7 @@ /** * External dependencies */ -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { ThemeProvider } from 'styled-components'; /** @@ -41,7 +41,7 @@ function setupPanel(backgroundColor = null) { ); - const element = getByLabelText('Background color'); + const element = getByLabelText('Current page color'); return { element, updateCurrentPageProperties, @@ -51,21 +51,11 @@ function setupPanel(backgroundColor = null) { describe('PageBackgroundPanel', () => { it('should display a color picker with default color if none set', () => { const { element } = setupPanel(); - expect(element.value).toStrictEqual('#ffffff'); + expect(element.innerHTML).toStrictEqual('FFFFFF'); }); it('should display a color picker with current color', () => { - const { element } = setupPanel('#ff0000'); - expect(element.value).toStrictEqual('#ff0000'); - }); - - it('should invoke callback with new color when changed', () => { - const { element, updateCurrentPageProperties } = setupPanel('#ff0000'); - - fireEvent.change(element, { target: { value: '#0000ff' } }); - - expect(updateCurrentPageProperties).toHaveBeenCalledWith({ - properties: { backgroundColor: '#0000ff' }, - }); + const { element } = setupPanel({ color: { r: 255, g: 0, b: 0 } }); + expect(element.innerHTML).toStrictEqual('FF0000'); }); }); diff --git a/assets/src/edit-story/components/panels/test/style.js b/assets/src/edit-story/components/panels/test/style.js index c79e067960e6..7fab8766a2de 100644 --- a/assets/src/edit-story/components/panels/test/style.js +++ b/assets/src/edit-story/components/panels/test/style.js @@ -17,7 +17,7 @@ /** * External dependencies */ -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { ThemeProvider } from 'styled-components'; /** @@ -32,32 +32,17 @@ function arrange(children = null) { describe('Panels/Style', () => { it('should render