From 24e155ff017e49304cf7fd8f6dc3d40282edd5a9 Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Fri, 19 Oct 2018 07:43:17 -0700 Subject: [PATCH] ColorPalette: Add an accessible color picker based on ChromePicker (#10564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add color picker component * Add color picker styles to build * Update to buttons for focus handles * Update styles * Update class name * Add color-picker component to exports * Update name to ColorPicker * Add labels to color input groups * Fix describedby prop name * Update view to use `getDerivedStateFromProps` * Add “Input” component to handle intermediate state when entering values Specifically needed for hex colors, which can be invalid while they’re being entered * Add space around text inputs * Translate toggle label * Add focus styles to colourpicker pointers. * Remove react-color dependency * Add class name to better find the component in the devtools * Add tinycolor2 dependency * Add reference to react-color license & copyright * Better label * Update snapshot * Decouple valueKey and label so we can use semantic labels. * Use KeyboardShortcuts component to avoid separate instances of Mousetrap * Use createRef * Normalize HSV / HSL values H: 0 - 260 S, L, V: 0 - 100 * Omit `valueKey` from being passed to input * Use createRef in all components * Update saturation handle label * Prevent key events on slider handles Fixes the issue where VoiceOver grabs arrow-key input as navigation * Use a focusable div for slider handle * Apply focus style to alpha handle * Add description to hue controls * Add speak commands for view change * Update snapshot * Remove test as the component API surface changed We have done some changes to the ChromePicker so this test can no longer be replicated. * Add README, tests and update Changelog * Remove react-color dependency * Add docs manifest file --- docs/manifest.json | 6 + package-lock.json | 33 +- packages/components/CHANGELOG.md | 4 + packages/components/package.json | 4 +- .../components/src/color-palette/index.js | 4 +- .../test/__snapshots__/index.js.snap | 115 +++++-- .../src/color-palette/test/index.js | 7 - .../components/src/color-picker/README.md | 23 ++ packages/components/src/color-picker/alpha.js | 177 +++++++++++ packages/components/src/color-picker/hue.js | 174 +++++++++++ packages/components/src/color-picker/index.js | 120 ++++++++ .../components/src/color-picker/inputs.js | 289 ++++++++++++++++++ .../components/src/color-picker/saturation.js | 186 +++++++++++ .../components/src/color-picker/style.scss | 209 +++++++++++++ .../test/__snapshots__/index.js.snap | 87 ++++++ .../components/src/color-picker/test/index.js | 26 ++ packages/components/src/color-picker/utils.js | 212 +++++++++++++ packages/components/src/index.js | 1 + packages/components/src/style.scss | 1 + 19 files changed, 1606 insertions(+), 72 deletions(-) create mode 100644 packages/components/src/color-picker/README.md create mode 100644 packages/components/src/color-picker/alpha.js create mode 100644 packages/components/src/color-picker/hue.js create mode 100644 packages/components/src/color-picker/index.js create mode 100644 packages/components/src/color-picker/inputs.js create mode 100644 packages/components/src/color-picker/saturation.js create mode 100644 packages/components/src/color-picker/style.scss create mode 100644 packages/components/src/color-picker/test/__snapshots__/index.js.snap create mode 100644 packages/components/src/color-picker/test/index.js create mode 100644 packages/components/src/color-picker/utils.js diff --git a/docs/manifest.json b/docs/manifest.json index 95de517d25076a..7c2ca6b45ee091 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -581,6 +581,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/components/src/color-palette/README.md", "parent": "components" }, + { + "title": "ColorPicker", + "slug": "color-picker", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/components/src/color-picker/README.md", + "parent": "components" + }, { "title": "Dashicon", "slug": "dashicon", diff --git a/package-lock.json b/package-lock.json index 274fe95ad01bf0..ab5506def23748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2142,9 +2142,9 @@ "mousetrap": "^1.6.2", "re-resizable": "^4.7.1", "react-click-outside": "^2.3.1", - "react-color": "^2.13.4", "react-dates": "^17.1.1", "rememo": "^3.0.0", + "tinycolor2": "^1.4.1", "uuid": "^3.1.0" } }, @@ -13726,11 +13726,6 @@ "integrity": "sha512-NcWuJFHDA8V3wkDgR/j4+gZx+YQwstPgfQDV8ndUeWWzta3dnDTBxpVzqS9lkmJAuV5YX35lmyojl6HO5JXAgw==", "dev": true }, - "material-colors": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" - }, "math-expression-evaluator": { "version": "1.2.17", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", @@ -17113,22 +17108,10 @@ "hoist-non-react-statics": "^1.2.0" } }, - "react-color": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.13.4.tgz", - "integrity": "sha512-rNJTTxMPTImI1NpFaKLggDIvHgKOYRXj0krVh8c+Mo1YNsrLko8O94yiFqqdnSQgtIPteiAcGEJgBo9V5+uqaw==", - "requires": { - "lodash": "^4.0.1", - "material-colors": "^1.2.1", - "prop-types": "^15.5.4", - "reactcss": "^1.2.0", - "tinycolor2": "^1.1.2" - } - }, "react-dates": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/react-dates/-/react-dates-17.1.1.tgz", - "integrity": "sha512-kUQEf6AnXa0h067lW9teAYhzcSNjh8L1ZpUOdB1obzPTBTrzlSu94CvPOfTI43Rf+yiBWOXIpL36Ub+rKf4oKA==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/react-dates/-/react-dates-17.2.0.tgz", + "integrity": "sha512-RDlerU8DdRRrlYS0MQ7Z9igPWABGLDwz6+ykBNff67RM3Sset2TDqeuOr+R5o00Ggn5U47GeLsGcSDxlZd9cHw==", "requires": { "airbnb-prop-types": "^2.10.0", "consolidated-events": "^1.1.1 || ^2.0.0", @@ -17337,14 +17320,6 @@ "global-cache": "^1.2.1" } }, - "reactcss": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", - "requires": { - "lodash": "^4.0.1" - } - }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2665f8e62ee0fc..106fd58c417462 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,9 @@ ## 4.2.0 (Unreleased) +### New Feature + +- Added a new `ColorPicker` component ([#10564](https://github.com/WordPress/gutenberg/pull/10564)). + ### Deprecation - `wp.components.PanelColor` has been deprecated in favor of `wp.editor.PanelColorSettings`. diff --git a/packages/components/package.json b/packages/components/package.json index a43560f88b9202..018c38ea6e525a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -42,9 +42,9 @@ "mousetrap": "^1.6.2", "re-resizable": "^4.7.1", "react-click-outside": "^2.3.1", - "react-color": "^2.13.4", "react-dates": "^17.1.1", "rememo": "^3.0.0", + "tinycolor2": "^1.4.1", "uuid": "^3.1.0" }, "devDependencies": { @@ -54,4 +54,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/components/src/color-palette/index.js b/packages/components/src/color-palette/index.js index 664d5f4ed92a58..5ab52f277df71e 100644 --- a/packages/components/src/color-palette/index.js +++ b/packages/components/src/color-palette/index.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { ChromePicker } from 'react-color'; import { map } from 'lodash'; /** @@ -16,6 +15,7 @@ import { __, sprintf } from '@wordpress/i18n'; import Button from '../button'; import Dropdown from '../dropdown'; import Tooltip from '../tooltip'; +import ColorPicker from '../color-picker'; export default function ColorPalette( { colors, disableCustomColors = false, value, onChange, className } ) { function applyOrUnset( color ) { @@ -71,7 +71,7 @@ export default function ColorPalette( { colors, disableCustomColors = false, val ) } renderContent={ () => ( - onChange( color.hex ) } disableAlpha diff --git a/packages/components/src/color-palette/test/__snapshots__/index.js.snap b/packages/components/src/color-palette/test/__snapshots__/index.js.snap index 7562ecac5015c0..cb4980913feb56 100644 --- a/packages/components/src/color-palette/test/__snapshots__/index.js.snap +++ b/packages/components/src/color-palette/test/__snapshots__/index.js.snap @@ -1,38 +1,89 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ColorPalette Dropdown .renderContent should render dropdown content 1`] = ` - +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
`; exports[`ColorPalette Dropdown .renderToggle should render dropdown content 1`] = ` diff --git a/packages/components/src/color-palette/test/index.js b/packages/components/src/color-palette/test/index.js index 1478430d37ccc5..ef1263535d0f4c 100644 --- a/packages/components/src/color-palette/test/index.js +++ b/packages/components/src/color-palette/test/index.js @@ -90,13 +90,6 @@ describe( 'ColorPalette', () => { test( 'should render dropdown content', () => { expect( renderedContent ).toMatchSnapshot(); } ); - - test( 'should call onToggle on click.', () => { - renderedContent.simulate( 'changeComplete', { hex: currentColor } ); - - expect( onChange ).toHaveBeenCalledTimes( 1 ); - expect( onChange ).toHaveBeenCalledWith( currentColor ); - } ); } ); } ); } ); diff --git a/packages/components/src/color-picker/README.md b/packages/components/src/color-picker/README.md new file mode 100644 index 00000000000000..d2b1307a00d7b6 --- /dev/null +++ b/packages/components/src/color-picker/README.md @@ -0,0 +1,23 @@ +# ColorPicker + +Accessible color picker. + +_Parts of the source code were derived and modified from [react-color](https://github.com/casesandberg/react-color/), released under the MIT license._ + +## Usage +```jsx +import { ColorPicker } from '@wordpress/components'; +import { withState } from '@wordpress/compose'; + +const MyColorPicker = withState( { + color: '#f00', +} )( ( { color, setState } ) => { + return ( + setState( value.hex ) } + disableAlpha + /> + ); +} ); +``` diff --git a/packages/components/src/color-picker/alpha.js b/packages/components/src/color-picker/alpha.js new file mode 100644 index 00000000000000..7e08d687f71bd6 --- /dev/null +++ b/packages/components/src/color-picker/alpha.js @@ -0,0 +1,177 @@ +/** + * Parts of this source were derived and modified from react-color, + * released under the MIT license. + * + * https://github.com/casesandberg/react-color/ + * + * Copyright (c) 2015 Case Sandberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Component, createRef } from '@wordpress/element'; +import { TAB } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { calculateAlphaChange } from './utils'; +import KeyboardShortcuts from '../keyboard-shortcuts'; + +export class Alpha extends Component { + constructor() { + super( ...arguments ); + + this.container = createRef(); + this.increase = this.increase.bind( this ); + this.decrease = this.decrease.bind( this ); + this.handleChange = this.handleChange.bind( this ); + this.handleMouseDown = this.handleMouseDown.bind( this ); + this.handleMouseUp = this.handleMouseUp.bind( this ); + } + + componentWillUnmount() { + this.unbindEventListeners(); + } + + increase( amount = 0.01 ) { + const { hsl, onChange = noop } = this.props; + amount = parseInt( amount * 100, 10 ); + const change = { + h: hsl.h, + s: hsl.s, + l: hsl.l, + a: ( parseInt( hsl.a * 100, 10 ) + amount ) / 100, + source: 'rgb', + }; + onChange( change ); + } + + decrease( amount = 0.01 ) { + const { hsl, onChange = noop } = this.props; + const intValue = parseInt( hsl.a * 100, 10 ) - parseInt( amount * 100, 10 ); + const change = { + h: hsl.h, + s: hsl.s, + l: hsl.l, + a: hsl.a <= amount ? 0 : intValue / 100, + source: 'rgb', + }; + onChange( change ); + } + + handleChange( e ) { + const { onChange = noop } = this.props; + const change = calculateAlphaChange( e, this.props, this.container.current ); + if ( change ) { + onChange( change, e ); + } + } + + handleMouseDown( e ) { + this.handleChange( e ); + window.addEventListener( 'mousemove', this.handleChange ); + window.addEventListener( 'mouseup', this.handleMouseUp ); + } + + handleMouseUp() { + this.unbindEventListeners(); + } + + preventKeyEvents( event ) { + if ( event.keyCode === TAB ) { + return; + } + event.preventDefault(); + } + + unbindEventListeners() { + window.removeEventListener( 'mousemove', this.handleChange ); + window.removeEventListener( 'mouseup', this.handleMouseUp ); + } + + render() { + const { rgb } = this.props; + const rgbString = `${ rgb.r },${ rgb.g },${ rgb.b }`; + const gradient = { + background: `linear-gradient(to right, rgba(${ rgbString }, 0) 0%, rgba(${ rgbString }, 1) 100%)`, + }; + const pointerLocation = { left: `${ rgb.a * 100 }%` }; + + const shortcuts = { + up: () => this.increase(), + right: () => this.increase(), + 'shift+up': () => this.increase( 0.1 ), + 'shift+right': () => this.increase( 0.1 ), + pageup: () => this.increase( 0.1 ), + end: () => this.increase( 1 ), + down: () => this.decrease(), + left: () => this.decrease(), + 'shift+down': () => this.decrease( 0.1 ), + 'shift+left': () => this.decrease( 0.1 ), + pagedown: () => this.decrease( 0.1 ), + home: () => this.decrease( 1 ), + }; + + return ( + +
+
+ { /* eslint-disable jsx-a11y/no-static-element-interactions */ } +
+
+
+ { /* eslint-enable jsx-a11y/no-static-element-interactions */ } +
+ + ); + } +} + +export default Alpha; diff --git a/packages/components/src/color-picker/hue.js b/packages/components/src/color-picker/hue.js new file mode 100644 index 00000000000000..8f78c439365793 --- /dev/null +++ b/packages/components/src/color-picker/hue.js @@ -0,0 +1,174 @@ +/** + * Parts of this source were derived and modified from react-color, + * released under the MIT license. + * + * https://github.com/casesandberg/react-color/ + * + * Copyright (c) 2015 Case Sandberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { withInstanceId } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; +import { Component, createRef } from '@wordpress/element'; +import { TAB } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { calculateHueChange } from './utils'; +import KeyboardShortcuts from '../keyboard-shortcuts'; + +export class Hue extends Component { + constructor() { + super( ...arguments ); + + this.container = createRef(); + this.increase = this.increase.bind( this ); + this.decrease = this.decrease.bind( this ); + this.handleChange = this.handleChange.bind( this ); + this.handleMouseDown = this.handleMouseDown.bind( this ); + this.handleMouseUp = this.handleMouseUp.bind( this ); + } + + componentWillUnmount() { + this.unbindEventListeners(); + } + + increase( amount = 1 ) { + const { hsl, onChange = noop } = this.props; + const change = { + h: hsl.h + amount >= 359 ? 359 : hsl.h + amount, + s: hsl.s, + l: hsl.l, + a: hsl.a, + source: 'rgb', + }; + onChange( change ); + } + + decrease( amount = 1 ) { + const { hsl, onChange = noop } = this.props; + const change = { + h: hsl.h <= amount ? 0 : hsl.h - amount, + s: hsl.s, + l: hsl.l, + a: hsl.a, + source: 'rgb', + }; + onChange( change ); + } + + handleChange( e ) { + const { onChange = noop } = this.props; + const change = calculateHueChange( e, this.props, this.container.current ); + if ( change ) { + onChange( change, e ); + } + } + + handleMouseDown( e ) { + this.handleChange( e ); + window.addEventListener( 'mousemove', this.handleChange ); + window.addEventListener( 'mouseup', this.handleMouseUp ); + } + + handleMouseUp() { + this.unbindEventListeners(); + } + + preventKeyEvents( event ) { + if ( event.keyCode === TAB ) { + return; + } + event.preventDefault(); + } + + unbindEventListeners() { + window.removeEventListener( 'mousemove', this.handleChange ); + window.removeEventListener( 'mouseup', this.handleMouseUp ); + } + + render() { + const { hsl = {}, instanceId } = this.props; + + const pointerLocation = { left: `${ ( hsl.h * 100 ) / 360 }%` }; + const shortcuts = { + up: () => this.increase(), + right: () => this.increase(), + 'shift+up': () => this.increase( 10 ), + 'shift+right': () => this.increase( 10 ), + pageup: () => this.increase( 10 ), + end: () => this.increase( 359 ), + down: () => this.decrease(), + left: () => this.decrease(), + 'shift+down': () => this.decrease( 10 ), + 'shift+left': () => this.decrease( 10 ), + pagedown: () => this.decrease( 10 ), + home: () => this.decrease( 359 ), + }; + + return ( + +
+
+ { /* eslint-disable jsx-a11y/no-static-element-interactions */ } +
+
+

+ { __( 'Move the arrow left or right to change hue.' ) } +

+
+ { /* eslint-enable jsx-a11y/no-static-element-interactions */ } +
+ + ); + } +} + +export default withInstanceId( Hue ); diff --git a/packages/components/src/color-picker/index.js b/packages/components/src/color-picker/index.js new file mode 100644 index 00000000000000..ba786b33fcb326 --- /dev/null +++ b/packages/components/src/color-picker/index.js @@ -0,0 +1,120 @@ +/** + * Parts of this source were derived and modified from react-color, + * released under the MIT license. + * + * https://github.com/casesandberg/react-color/ + * + * Copyright (c) 2015 Case Sandberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * External dependencies + */ +import classnames from 'classnames'; +import { debounce, noop, partial } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Alpha from './alpha'; +import Hue from './hue'; +import Inputs from './inputs'; +import Saturation from './saturation'; +import { colorToState, simpleCheckForValidColor } from './utils'; + +export default class ColorPicker extends Component { + constructor( { color = '0071a1' } ) { + super( ...arguments ); + this.state = colorToState( color ); + this.handleChange = this.handleChange.bind( this ); + } + + handleChange( data ) { + const { oldHue, onChangeComplete = noop } = this.props; + const isValidColor = simpleCheckForValidColor( data ); + if ( isValidColor ) { + const colors = colorToState( data, data.h || oldHue ); + this.setState( + colors, + debounce( partial( onChangeComplete, colors ), 100 ) + ); + } + } + + render() { + const { className, disableAlpha } = this.props; + const { color, hex, hsl, hsv, rgb } = this.state; + const classes = classnames( className, { + 'components-color-picker': true, + 'is-alpha-disabled': disableAlpha, + 'is-alpha-enabled': ! disableAlpha, + } ); + + return ( +
+
+ +
+ +
+
+
+
+
+ +
+ + { disableAlpha ? null : ( + + ) } +
+
+ + +
+
+ ); + } +} diff --git a/packages/components/src/color-picker/inputs.js b/packages/components/src/color-picker/inputs.js new file mode 100644 index 00000000000000..c42aa0022f6c6a --- /dev/null +++ b/packages/components/src/color-picker/inputs.js @@ -0,0 +1,289 @@ +/** + * External dependencies + */ +import { omit } from 'lodash'; + +/** + * WordPress dependencies + */ +import { speak } from '@wordpress/a11y'; +import { __ } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; +import { DOWN, ENTER, UP } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import IconButton from '../icon-button'; +import { isValidHex } from './utils'; +import TextControl from '../text-control'; + +/* Wrapper for TextControl, only used to handle intermediate state while typing. */ +class Input extends Component { + constructor( { value } ) { + super( ...arguments ); + this.state = { value: String( value ).toLowerCase() }; + this.handleBlur = this.handleBlur.bind( this ); + this.handleChange = this.handleChange.bind( this ); + this.handleKeyDown = this.handleKeyDown.bind( this ); + } + + componentWillReceiveProps( nextProps ) { + if ( nextProps.value !== this.props.value ) { + this.setState( { + value: String( nextProps.value ).toLowerCase(), + } ); + } + } + + handleBlur() { + const { valueKey, onChange } = this.props; + const { value } = this.state; + onChange( { [ valueKey ]: value } ); + } + + handleChange( value ) { + const { valueKey, onChange } = this.props; + // Protect against expanding a value while we're typing. + if ( value.length > 4 ) { + onChange( { [ valueKey ]: value } ); + } + this.setState( { value } ); + } + + handleKeyDown( { keyCode } ) { + if ( keyCode !== ENTER && keyCode !== UP && keyCode !== DOWN ) { + return; + } + const { value } = this.state; + const { valueKey, onChange } = this.props; + onChange( { [ valueKey ]: value } ); + } + + render() { + const { label, ...props } = this.props; + const { value } = this.state; + return ( + this.handleChange( newValue ) } + onBlur={ this.handleBlur } + onKeyDown={ this.handleKeyDown } + { ...omit( props, [ 'onChange', 'value', 'valueKey' ] ) } + /> + ); + } +} + +export class Inputs extends Component { + constructor( { hsl } ) { + super( ...arguments ); + + const view = hsl.a === 1 ? 'hex' : 'rgb'; + this.state = { view }; + + this.toggleViews = this.toggleViews.bind( this ); + this.handleChange = this.handleChange.bind( this ); + } + + static getDerivedStateFromProps( props, state ) { + if ( props.hsl.a !== 1 && state.view === 'hex' ) { + return { view: 'rgb' }; + } + return null; + } + + toggleViews() { + if ( this.state.view === 'hex' ) { + this.setState( { view: 'rgb' } ); + + speak( __( 'RGB mode active' ) ); + } else if ( this.state.view === 'rgb' ) { + this.setState( { view: 'hsl' } ); + + speak( __( 'Hue/saturation/lightness mode active' ) ); + } else if ( this.state.view === 'hsl' ) { + if ( this.props.hsl.a === 1 ) { + this.setState( { view: 'hex' } ); + + speak( __( 'Hex color mode active' ) ); + } else { + this.setState( { view: 'rgb' } ); + + speak( __( 'RGB mode active' ) ); + } + } + } + + handleChange( data ) { + if ( data.hex ) { + if ( isValidHex( data.hex ) ) { + this.props.onChange( { + hex: data.hex, + source: 'hex', + } ); + } + } else if ( data.r || data.g || data.b ) { + this.props.onChange( { + r: data.r || this.props.rgb.r, + g: data.g || this.props.rgb.g, + b: data.b || this.props.rgb.b, + source: 'rgb', + } ); + } else if ( data.a ) { + if ( data.a < 0 ) { + data.a = 0; + } else if ( data.a > 1 ) { + data.a = 1; + } + + this.props.onChange( { + h: this.props.hsl.h, + s: this.props.hsl.s, + l: this.props.hsl.l, + a: Math.round( data.a * 100 ) / 100, + source: 'rgb', + } ); + } else if ( data.h || data.s || data.l ) { + this.props.onChange( { + h: data.h || this.props.hsl.h, + s: data.s || this.props.hsl.s, + l: data.l || this.props.hsl.l, + source: 'hsl', + } ); + } + } + + renderFields() { + const { disableAlpha = false } = this.props; + if ( this.state.view === 'hex' ) { + return ( +
+ +
+ ); + } else if ( this.state.view === 'rgb' ) { + return ( +
+ + { __( 'Color value in RGB' ) } + +
+ + + + { disableAlpha ? null : ( + + ) } +
+
+ ); + } else if ( this.state.view === 'hsl' ) { + return ( +
+ + { __( 'Color value in HSL' ) } + +
+ + + + { disableAlpha ? null : ( + + ) } +
+
+ ); + } + } + + render() { + return ( +
+ { this.renderFields() } +
+ +
+
+ ); + } +} + +export default Inputs; diff --git a/packages/components/src/color-picker/saturation.js b/packages/components/src/color-picker/saturation.js new file mode 100644 index 00000000000000..6ba852756609a9 --- /dev/null +++ b/packages/components/src/color-picker/saturation.js @@ -0,0 +1,186 @@ +/** + * Parts of this source were derived and modified from react-color, + * released under the MIT license. + * + * https://github.com/casesandberg/react-color/ + * + * Copyright (c) 2015 Case Sandberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * External dependencies + */ +import { clamp, noop, throttle } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Component, createRef } from '@wordpress/element'; +import { TAB } from '@wordpress/keycodes'; +import { withInstanceId } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { calculateSaturationChange } from './utils'; +import KeyboardShortcuts from '../keyboard-shortcuts'; + +export class Saturation extends Component { + constructor( props ) { + super( props ); + + this.throttle = throttle( ( fn, data, e ) => { + fn( data, e ); + }, 50 ); + + this.container = createRef(); + this.saturate = this.saturate.bind( this ); + this.brighten = this.brighten.bind( this ); + this.handleChange = this.handleChange.bind( this ); + this.handleMouseDown = this.handleMouseDown.bind( this ); + this.handleMouseUp = this.handleMouseUp.bind( this ); + } + + componentWillUnmount() { + this.throttle.cancel(); + this.unbindEventListeners(); + } + + saturate( amount = 0.01 ) { + const { hsv, onChange = noop } = this.props; + const intSaturation = clamp( + hsv.s + Math.round( amount * 100 ), + 0, + 100 + ); + const change = { + h: hsv.h, + s: intSaturation, + v: hsv.v, + a: hsv.a, + source: 'rgb', + }; + + onChange( change ); + } + + brighten( amount = 0.01 ) { + const { hsv, onChange = noop } = this.props; + const intValue = clamp( + hsv.v + Math.round( amount * 100 ), + 0, + 100 + ); + const change = { + h: hsv.h, + s: hsv.s, + v: intValue, + a: hsv.a, + source: 'rgb', + }; + + onChange( change ); + } + + handleChange( e ) { + const { onChange = noop } = this.props; + const change = calculateSaturationChange( e, this.props, this.container.current ); + this.throttle( onChange, change, e ); + } + + handleMouseDown( e ) { + this.handleChange( e ); + window.addEventListener( 'mousemove', this.handleChange ); + window.addEventListener( 'mouseup', this.handleMouseUp ); + } + + handleMouseUp() { + this.unbindEventListeners(); + } + + preventKeyEvents( event ) { + if ( event.keyCode === TAB ) { + return; + } + event.preventDefault(); + } + + unbindEventListeners() { + window.removeEventListener( 'mousemove', this.handleChange ); + window.removeEventListener( 'mouseup', this.handleMouseUp ); + } + + render() { + const { hsv, hsl, instanceId } = this.props; + const pointerLocation = { + top: `${ -( hsv.v ) + 100 }%`, + left: `${ hsv.s }%`, + }; + const shortcuts = { + up: () => this.brighten(), + 'shift+up': () => this.brighten( 0.1 ), + pageup: () => this.brighten( 1 ), + down: () => this.brighten( -0.01 ), + 'shift+down': () => this.brighten( -0.1 ), + pagedown: () => this.brighten( -1 ), + right: () => this.saturate(), + 'shift+right': () => this.saturate( 0.1 ), + end: () => this.saturate( 1 ), + left: () => this.saturate( -0.01 ), + 'shift+left': () => this.saturate( -0.1 ), + home: () => this.saturate( -1 ), + }; + + /* eslint-disable jsx-a11y/no-static-element-interactions */ + return ( + +
+
+
+
+ + ); + /* eslint-enable jsx-a11y/no-static-element-interactions */ + } +} + +export default withInstanceId( Saturation ); diff --git a/packages/components/src/color-picker/style.scss b/packages/components/src/color-picker/style.scss new file mode 100644 index 00000000000000..336c684dfeca98 --- /dev/null +++ b/packages/components/src/color-picker/style.scss @@ -0,0 +1,209 @@ +/** + * Parts of this source were derived and modified from react-color, + * released under the MIT license. + * + * https://github.com/casesandberg/react-color/ + * + * Copyright (c) 2015 Case Sandberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +.components-color-picker { + width: 100%; + overflow: hidden; +} +.components-color-picker__saturation { + width: 100%; + padding-bottom: 55%; + position: relative; +} +.components-color-picker__body { + padding: 16px 16px 12px; +} +.components-color-picker__controls { + display: flex; +} +.components-color-picker__saturation-pointer, +.components-color-picker__hue-pointer, +.components-color-picker__alpha-pointer { + padding: 0; + position: absolute; + cursor: pointer; + box-shadow: none; + border: none; +} + +/* CURRENT COLOR COMPONENT */ +.components-color-picker__swatch { + margin-right: 8px; + width: 32px; + height: 32px; + border-radius: 50%; + position: relative; + overflow: hidden; + background-image: + linear-gradient(45deg, #ddd 25%, transparent 25%), + linear-gradient(-45deg, #ddd 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #ddd 75%), + linear-gradient(-45deg, transparent 75%, #ddd 75%); + background-size: 10px 10px; + background-position: 0 0, 0 5px, 5px -5px, -5px 0; + + .is-alpha-disabled & { + width: 12px; + height: 12px; + margin-top: 0; + } +} +.components-color-picker__active { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50%; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); + z-index: 2; +} + +/* SATURATION COMPONENT */ +.components-color-picker__saturation-color, +.components-color-picker__saturation-white, +.components-color-picker__saturation-black { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +.components-color-picker__saturation-color { + overflow: hidden; +} +.components-color-picker__saturation-white { + background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); +} +.components-color-picker__saturation-black { + background: linear-gradient(to top, #000, rgba(0, 0, 0, 0)); +} +.components-color-picker__saturation-pointer { + width: 8px; + height: 8px; + box-shadow: + 0 0 0 1.5px #fff, + inset 0 0 1px 1px rgba(0, 0, 0, 0.3), + 0 0 1px 2px rgba(0, 0, 0, 0.4); + border-radius: 50%; + background-color: transparent; + transform: translate(-4px, -4px); +} + +/* HUE & ALPHA BARS */ +.components-color-picker__toggles { + flex: 1; +} +.components-color-picker__alpha { + background-image: + linear-gradient(45deg, #ddd 25%, transparent 25%), + linear-gradient(-45deg, #ddd 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #ddd 75%), + linear-gradient(-45deg, transparent 75%, #ddd 75%); + background-size: 10px 10px; + background-position: 0 0, 0 5px, 5px -5px, -5px 0; +} +.components-color-picker__hue-gradient, +.components-color-picker__alpha-gradient { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +.components-color-picker__hue, +.components-color-picker__alpha { + height: 12px; + position: relative; +} +.is-alpha-enabled .components-color-picker__hue { + margin-bottom: 8px; +} +.components-color-picker__hue-bar, +.components-color-picker__alpha-bar { + position: relative; + margin: 0 3px; + height: 100%; + padding: 0 2px; +} +.components-color-picker__hue-gradient { + background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); +} +.components-color-picker__hue-pointer, +.components-color-picker__alpha-pointer { + left: 0; + width: 14px; + height: 14px; + border-radius: 50%; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37); + background: #fff; + transform: translate(-7px, -1px); +} + +.components-color-picker__hue-pointer, +.components-color-picker__saturation-pointer { + transition: box-shadow 0.1s linear; +} + +.components-color-picker__saturation-pointer:focus { + box-shadow: + 0 0 0 2px #fff, + 0 0 0 4px $blue-medium-500, + 0 0 5px 0 $blue-medium-500, + inset 0 0 1px 1px rgba(0, 0, 0, 0.3), + 0 0 1px 2px rgba(0, 0, 0, 0.4); +} + +.components-color-picker__hue-pointer:focus, +.components-color-picker__alpha-pointer:focus { + border-color: $blue-medium-500; + box-shadow: + 0 0 0 2px $blue-medium-500, + 0 0 3px 0 $blue-medium-500; + outline: 2px solid transparent; + outline-offset: -2px; +} + + +/* INPUTS COMPONENT */ +.components-color-picker__inputs-wrapper { + margin: 0 -4px; + padding-top: 16px; + display: flex; + align-items: flex-end; + + fieldset { + flex: 1; + } +} +.components-color-picker__inputs-fields { + display: flex; + + .components-base-control__field { + margin: 0 4px; + } +} diff --git a/packages/components/src/color-picker/test/__snapshots__/index.js.snap b/packages/components/src/color-picker/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..c143801f9e29f5 --- /dev/null +++ b/packages/components/src/color-picker/test/__snapshots__/index.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColorPicker should render color picker 1`] = ` +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+`; diff --git a/packages/components/src/color-picker/test/index.js b/packages/components/src/color-picker/test/index.js new file mode 100644 index 00000000000000..5510da6e138e22 --- /dev/null +++ b/packages/components/src/color-picker/test/index.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import ShallowRenderer from 'react-test-renderer/shallow'; + +/** + * Internal dependencies + */ +import ColorPicker from '../'; + +describe( 'ColorPicker', () => { + test( 'should render color picker', () => { + const color = '#fff'; + + const renderer = new ShallowRenderer(); + renderer.render( + {} } + disableAlpha + /> + ); + + expect( renderer.getRenderOutput() ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/components/src/color-picker/utils.js b/packages/components/src/color-picker/utils.js new file mode 100644 index 00000000000000..dada93c31c16ef --- /dev/null +++ b/packages/components/src/color-picker/utils.js @@ -0,0 +1,212 @@ +/** + * Parts of this source were derived and modified from react-color, + * released under the MIT license. + * + * https://github.com/casesandberg/react-color/ + * + * Copyright (c) 2015 Case Sandberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * External dependencies + */ +import { each } from 'lodash'; +import tinycolor from 'tinycolor2'; + +/** + * Given a hex color, get all other color properties (rgb, alpha, etc). + * + * @param {Object|string} data A hex color string or an object with a hex property + * @param {string} oldHue A reference to the hue of the previous color, otherwise dragging the saturation to zero will reset the current hue to zero as well. See https://github.com/casesandberg/react-color/issues/29#issuecomment-132686909. + * @return {Object} An object of different color representations. + */ +export function colorToState( data = {}, oldHue = false ) { + const color = data.hex ? tinycolor( data.hex ) : tinycolor( data ); + const hsl = color.toHsl(); + hsl.h = Math.round( hsl.h ); + hsl.s = Math.round( hsl.s * 100 ); + hsl.l = Math.round( hsl.l * 100 ); + const hsv = color.toHsv(); + hsv.h = Math.round( hsv.h ); + hsv.s = Math.round( hsv.s * 100 ); + hsv.v = Math.round( hsv.v * 100 ); + const rgb = color.toRgb(); + const hex = color.toHex(); + if ( hsl.s === 0 ) { + hsl.h = oldHue || 0; + hsv.h = oldHue || 0; + } + const transparent = hex === '000000' && rgb.a === 0; + + return { + color, + hex: transparent ? 'transparent' : `#${ hex }`, + hsl, + hsv, + oldHue: data.h || oldHue || hsl.h, + rgb, + source: data.source, + }; +} + +/** + * Get the top/left offsets of a point in a container, also returns the container width/height. + * + * @param {Event} e Mouse or touch event with a location coordinate. + * @param {HTMLElement} container The container div, returned point is relative to this container. + * @return {Object} An object of the offset positions & container size. + */ +function getPointOffset( e, container ) { + e.preventDefault(); + const { + left: containerLeft, + top: containerTop, + width, + height, + } = container.getBoundingClientRect(); + const x = typeof e.pageX === 'number' ? e.pageX : e.touches[ 0 ].pageX; + const y = typeof e.pageY === 'number' ? e.pageY : e.touches[ 0 ].pageY; + let left = x - ( containerLeft + window.pageXOffset ); + let top = y - ( containerTop + window.pageYOffset ); + + if ( left < 0 ) { + left = 0; + } else if ( left > width ) { + left = width; + } else if ( top < 0 ) { + top = 0; + } else if ( top > height ) { + top = height; + } + + return { top, left, width, height }; +} + +/** + * Check if a string is a valid hex color code. + * + * @param {string} hex A possible hex color. + * @return {boolean} True if the color is a valid hex color. + */ +export function isValidHex( hex ) { + // disable hex4 and hex8 + const lh = String( hex ).charAt( 0 ) === '#' ? 1 : 0; + return ( + hex.length !== 4 + lh && hex.length < 7 + lh && tinycolor( hex ).isValid() + ); +} + +/** + * Check an object for any valid color properties. + * + * @param {Object} data A possible object representing a color. + * @return {Object|boolean} If a valid representation of color, returns the data object. Otherwise returns false. + */ +export function simpleCheckForValidColor( data ) { + const keysToCheck = [ 'r', 'g', 'b', 'a', 'h', 's', 'l', 'v' ]; + let checked = 0; + let passed = 0; + each( keysToCheck, ( letter ) => { + if ( data[ letter ] ) { + checked += 1; + if ( ! isNaN( data[ letter ] ) ) { + passed += 1; + } + } + } ); + return checked === passed ? data : false; +} + +/** + * Calculate the current alpha based on a mouse or touch event + * + * @param {Event} e A mouse or touch event on the alpha bar. + * @param {Object} props The current component props + * @param {HTMLElement} container The container div for the alpha bar graph. + * @return {Object|null} If the alpha value has changed, returns a new color object. + */ +export function calculateAlphaChange( e, props, container ) { + const { left, width } = getPointOffset( e, container ); + const a = left < 0 ? 0 : Math.round( ( left * 100 ) / width ) / 100; + + if ( props.hsl.a !== a ) { + return { + h: props.hsl.h, + s: props.hsl.s, + l: props.hsl.l, + a, + source: 'rgb', + }; + } + return null; +} + +/** + * Calculate the current hue based on a mouse or touch event + * + * @param {Event} e A mouse or touch event on the hue bar. + * @param {Object} props The current component props + * @param {HTMLElement} container The container div for the hue bar graph. + * @return {Object|null} If the hue value has changed, returns a new color object. + */ +export function calculateHueChange( e, props, container ) { + const { left, width } = getPointOffset( e, container ); + const percent = ( left * 100 ) / width; + const h = left >= width ? 359 : ( 360 * percent ) / 100; + + if ( props.hsl.h !== h ) { + return { + h, + s: props.hsl.s, + l: props.hsl.l, + a: props.hsl.a, + source: 'rgb', + }; + } + return null; +} + +/** + * Calculate the current saturation & brightness based on a mouse or touch event + * + * @param {Event} e A mouse or touch event on the saturation graph. + * @param {Object} props The current component props + * @param {HTMLElement} container The container div for the 2D saturation graph. + * @return {Object} Returns a new color object. + */ +export function calculateSaturationChange( e, props, container ) { + const { top, left, width, height } = getPointOffset( e, container ); + const saturation = left < 0 ? 0 : ( left * 100 ) / width; + let bright = top >= height ? 0 : -( ( top * 100 ) / height ) + 100; + // `v` values less than 1 are considered in the [0,1] range, causing unexpected behavior at the bottom + // of the chart. To fix this, we assume any value less than 1 should be 0 brightness. + if ( bright < 1 ) { + bright = 0; + } + + return { + h: props.hsl.h, + s: saturation, + v: bright, + a: props.hsl.a, + source: 'rgb', + }; +} diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 2668702a355561..fcaf2b981c9219 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -9,6 +9,7 @@ export { default as CheckboxControl } from './checkbox-control'; 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 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 3a2bb018ac41c2..751eae5aaa3c1d 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -5,6 +5,7 @@ @import "./checkbox-control/style.scss"; @import "./color-indicator/style.scss"; @import "./color-palette/style.scss"; +@import "./color-picker/style.scss"; @import "./dashicon/style.scss"; @import "./date-time/style.scss"; @import "./disabled/style.scss";