From 5cca2e1a8021679aaf2fc713c1a2c1cce168c374 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Tue, 10 Aug 2021 06:58:59 -0700 Subject: [PATCH] components: Add new ColorPicker (#33714) * components: InputControl to TypeScript * Add back event to onChange * Convert select-control to TS and ts-nocheck the rest of color picker dependencies * components: Add new ColorPicker * Fix various visual issues and rapid color change stuttering * Rename to `enableAlpha` and add a README * Add copy format and better defaults * More closely mimic "half-controlled" values * Fix misaligned value display in Chrome * Update react-colorful styles to better match designs * Add CHANGELOG entry for RangeControl prop update * Fix bad rebase and bad types * Fix html number arrows showing on Chrome * Switch to HSL(A) based input and output to fix HSL UX * Move styled components and get rid of unnecessary ones * Fix onChange parameter name * Add missing labels --- package-lock.json | 8 + packages/components/CHANGELOG.md | 4 + packages/components/package.json | 1 + packages/components/src/button/deprecated.js | 1 + packages/components/src/button/index.js | 1 + .../src/input-control/input-field.tsx | 6 +- .../src/input-control/reducer/reducer.ts | 2 +- .../components/src/input-control/types.ts | 7 +- .../components/src/number-control/index.js | 1 + .../styles/number-control-styles.js | 9 +- packages/components/src/popover/index.js | 1 + packages/components/src/popover/utils.js | 1 + .../components/src/range-control/index.js | 3 + .../src/range-control/input-range.js | 1 + packages/components/src/range-control/mark.js | 1 + packages/components/src/range-control/rail.js | 1 + .../styles/range-control-styles.js | 1 + .../components/src/range-control/tooltip.js | 1 + .../components/src/range-control/utils.js | 1 + .../components/src/select-control/index.tsx | 5 +- .../styles/select-control-styles.ts | 4 +- .../src/slot-fill/bubbles-virtually/fill.js | 1 + .../bubbles-virtually/slot-fill-context.js | 1 + .../bubbles-virtually/slot-fill-provider.js | 1 + .../src/slot-fill/bubbles-virtually/slot.js | 1 + .../slot-fill/bubbles-virtually/use-slot.js | 1 + packages/components/src/slot-fill/context.js | 1 + packages/components/src/slot-fill/fill.js | 1 + packages/components/src/slot-fill/index.js | 1 + packages/components/src/slot-fill/provider.js | 1 + packages/components/src/slot-fill/slot.js | 1 + packages/components/src/slot-fill/use-slot.js | 1 + packages/components/src/tooltip/index.js | 1 + .../components/src/ui/color-picker/README.md | 57 +++++++ .../src/ui/color-picker/color-display.tsx | 154 ++++++++++++++++++ .../src/ui/color-picker/color-input.tsx | 36 ++++ .../src/ui/color-picker/component.tsx | 141 ++++++++++++++++ .../src/ui/color-picker/hex-input.tsx | 55 +++++++ .../src/ui/color-picker/hsl-input.tsx | 71 ++++++++ .../components/src/ui/color-picker/index.ts | 1 + .../src/ui/color-picker/input-with-slider.tsx | 56 +++++++ .../components/src/ui/color-picker/picker.tsx | 26 +++ .../src/ui/color-picker/rgb-input.tsx | 73 +++++++++ .../src/ui/color-picker/stories/index.js | 59 +++++++ .../components/src/ui/color-picker/styles.ts | 61 +++++++ .../components/src/ui/color-picker/types.ts | 1 + .../utils/hooks/test/use-controlled-value.js | 4 +- .../src/utils/hooks/use-controlled-value.ts | 14 +- packages/components/tsconfig.json | 9 +- packages/compose/README.md | 2 +- .../src/hooks/use-copy-to-clipboard/index.js | 3 +- 51 files changed, 876 insertions(+), 19 deletions(-) create mode 100644 packages/components/src/ui/color-picker/README.md create mode 100644 packages/components/src/ui/color-picker/color-display.tsx create mode 100644 packages/components/src/ui/color-picker/color-input.tsx create mode 100644 packages/components/src/ui/color-picker/component.tsx create mode 100644 packages/components/src/ui/color-picker/hex-input.tsx create mode 100644 packages/components/src/ui/color-picker/hsl-input.tsx create mode 100644 packages/components/src/ui/color-picker/index.ts create mode 100644 packages/components/src/ui/color-picker/input-with-slider.tsx create mode 100644 packages/components/src/ui/color-picker/picker.tsx create mode 100644 packages/components/src/ui/color-picker/rgb-input.tsx create mode 100644 packages/components/src/ui/color-picker/stories/index.js create mode 100644 packages/components/src/ui/color-picker/styles.ts create mode 100644 packages/components/src/ui/color-picker/types.ts diff --git a/package-lock.json b/package-lock.json index 252a27c56a075..cd681908d4866 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18186,6 +18186,7 @@ "memize": "^1.1.0", "moment": "^2.22.1", "re-resizable": "^6.4.0", + "react-colorful": "^5.3.0", "react-dates": "^17.1.1", "react-resize-aware": "^3.1.0", "react-use-gesture": "^9.0.0", @@ -18193,6 +18194,13 @@ "rememo": "^3.0.0", "tinycolor2": "^1.4.2", "uuid": "^8.3.0" + }, + "dependencies": { + "react-colorful": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.3.0.tgz", + "integrity": "sha512-zWE5E88zmjPXFhv6mGnRZqKin9s5vip1O3IIGynY9EhZxN8MATUxZkT3e/9OwTEm4DjQBXc6PFWP6AetY+Px+A==" + } } }, "@wordpress/compose": { diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 83bb14bde931f..d305068a9a31c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,10 @@ - Updated the visual styles of the RangeControl component ([#33824](https://github.com/WordPress/gutenberg/pull/33824)) +### New Feature + +- Add `hideLabelFromVision` prop to `RangeControl` ([#33714](https://github.com/WordPress/gutenberg/pull/33714)) + ## 15.0.0 (2021-07-29) ### Breaking Change diff --git a/packages/components/package.json b/packages/components/package.json index c131e5b6ea52a..a19c1c3986aed 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -59,6 +59,7 @@ "memize": "^1.1.0", "moment": "^2.22.1", "re-resizable": "^6.4.0", + "react-colorful": "^5.3.0", "react-dates": "^17.1.1", "react-resize-aware": "^3.1.0", "react-use-gesture": "^9.0.0", diff --git a/packages/components/src/button/deprecated.js b/packages/components/src/button/deprecated.js index 34e99bd71f54a..86c1ac8547d40 100644 --- a/packages/components/src/button/deprecated.js +++ b/packages/components/src/button/deprecated.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index d610436edd39e..3d36f400af3c5 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/input-control/input-field.tsx b/packages/components/src/input-control/input-field.tsx index 099c72890fcfe..02455733de03c 100644 --- a/packages/components/src/input-control/input-field.tsx +++ b/packages/components/src/input-control/input-field.tsx @@ -105,7 +105,7 @@ function InputField( const handleOnBlur = ( event: FocusEvent< HTMLInputElement > ) => { onBlur( event ); - setIsFocused( false ); + setIsFocused?.( false ); /** * If isPressEnterToChange is set, this commits the value to @@ -123,7 +123,7 @@ function InputField( const handleOnFocus = ( event: FocusEvent< HTMLInputElement > ) => { onFocus( event ); - setIsFocused( true ); + setIsFocused?.( true ); }; const handleOnChange = ( event: ChangeEvent< HTMLInputElement > ) => { @@ -135,7 +135,7 @@ function InputField( const nextValue = event.currentTarget.value; try { - onValidate( nextValue, event ); + onValidate( nextValue ); commit( nextValue, event ); } catch ( err ) { invalidate( err, event ); diff --git a/packages/components/src/input-control/reducer/reducer.ts b/packages/components/src/input-control/reducer/reducer.ts index 2499096a84338..1aad3f8d3e672 100644 --- a/packages/components/src/input-control/reducer/reducer.ts +++ b/packages/components/src/input-control/reducer/reducer.ts @@ -36,7 +36,7 @@ function mergeInitialState( ...initialInputControlState, ...initialState, initialValue: value, - }; + } as InputState; } /** diff --git a/packages/components/src/input-control/types.ts b/packages/components/src/input-control/types.ts index bb22b70400565..e166ffd6219ba 100644 --- a/packages/components/src/input-control/types.ts +++ b/packages/components/src/input-control/types.ts @@ -63,17 +63,20 @@ export interface InputBaseProps extends BaseProps, FlexProps { } export interface InputControlProps - extends Omit< InputBaseProps, 'children' >, + extends Omit< InputBaseProps, 'children' | 'isFocused' >, /** * The `prefix` prop in `PolymorphicComponentProps< InputFieldProps, 'input', false >` comes from the * `HTMLInputAttributes` and clashes with the one from `InputBaseProps`. So we have to omit it from * `PolymorphicComponentProps< InputFieldProps, 'input', false >` in order that `InputBaseProps[ 'prefix' ]` * be the only prefix prop. Otherwise it tries to do a union of the two prefix properties and you end up * with an unresolvable type. + * + * `isFocused` and `setIsFocused` are managed internally by the InputControl, but the rest of the props + * for InputField are passed through. */ Omit< PolymorphicComponentProps< InputFieldProps, 'input', false >, - 'stateReducer' | 'prefix' + 'stateReducer' | 'prefix' | 'isFocused' | 'setIsFocused' > { __unstableStateReducer?: InputFieldProps[ 'stateReducer' ]; } diff --git a/packages/components/src/number-control/index.js b/packages/components/src/number-control/index.js index 2df94561f8ecb..4aa29e17cee79 100644 --- a/packages/components/src/number-control/index.js +++ b/packages/components/src/number-control/index.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/number-control/styles/number-control-styles.js b/packages/components/src/number-control/styles/number-control-styles.js index ec57602ac3ce9..a27784497b018 100644 --- a/packages/components/src/number-control/styles/number-control-styles.js +++ b/packages/components/src/number-control/styles/number-control-styles.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ @@ -12,11 +13,15 @@ const htmlArrowStyles = ( { hideHTMLArrows } ) => { if ( ! hideHTMLArrows ) return ``; return css` - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { + input[type='number']::-webkit-outer-spin-button, + input[type='number']::-webkit-inner-spin-button { -webkit-appearance: none !important; margin: 0 !important; } + + input[type='number'] { + -moz-appearance: textfield; + } `; }; diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index 2948c879b8fa9..a30dfe4fb19fa 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/popover/utils.js b/packages/components/src/popover/utils.js index 25af0b8245507..697a3fceeb6dd 100644 --- a/packages/components/src/popover/utils.js +++ b/packages/components/src/popover/utils.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/range-control/index.js b/packages/components/src/range-control/index.js index 0ce9f4e5fa4d7..6c3094c8b7175 100644 --- a/packages/components/src/range-control/index.js +++ b/packages/components/src/range-control/index.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ @@ -47,6 +48,7 @@ function RangeControl( initialPosition, isShiftStepEnabled = true, label, + hideLabelFromVision = false, marks = false, max = 100, min = 0, @@ -194,6 +196,7 @@ function RangeControl( diff --git a/packages/components/src/range-control/input-range.js b/packages/components/src/range-control/input-range.js index 5fa4235a68974..6a0f57bd8e0ff 100644 --- a/packages/components/src/range-control/input-range.js +++ b/packages/components/src/range-control/input-range.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/range-control/mark.js b/packages/components/src/range-control/mark.js index 38794ca96793b..a6362b0ee903d 100644 --- a/packages/components/src/range-control/mark.js +++ b/packages/components/src/range-control/mark.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/range-control/rail.js b/packages/components/src/range-control/rail.js index 8f1794883e2dd..cc8fdc33062e7 100644 --- a/packages/components/src/range-control/rail.js +++ b/packages/components/src/range-control/rail.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/range-control/styles/range-control-styles.js b/packages/components/src/range-control/styles/range-control-styles.js index 658d369fcdb95..8ec40ad9b82e0 100644 --- a/packages/components/src/range-control/styles/range-control-styles.js +++ b/packages/components/src/range-control/styles/range-control-styles.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/range-control/tooltip.js b/packages/components/src/range-control/tooltip.js index ae9a7791c9e4a..8d350c31ceb7b 100644 --- a/packages/components/src/range-control/tooltip.js +++ b/packages/components/src/range-control/tooltip.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/range-control/utils.js b/packages/components/src/range-control/utils.js index 81e03015e44c4..e2a758a6b4f1c 100644 --- a/packages/components/src/range-control/utils.js +++ b/packages/components/src/range-control/utils.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/select-control/index.tsx b/packages/components/src/select-control/index.tsx index 7a036f5ce9234..db0f7917c33d7 100644 --- a/packages/components/src/select-control/index.tsx +++ b/packages/components/src/select-control/index.tsx @@ -30,7 +30,8 @@ function useUniqueId( idProp?: string ) { return idProp || id; } -export interface SelectControlProps extends Omit< InputBaseProps, 'children' > { +export interface SelectControlProps + extends Omit< InputBaseProps, 'children' | 'isFocused' > { help?: string; hideLabelFromVision?: boolean; multiple?: boolean; @@ -92,7 +93,7 @@ function SelectControl( const handleOnChange = ( event: ChangeEvent< HTMLSelectElement > ) => { if ( multiple ) { - const selectedOptions = [ ...event.target.options ].filter( + const selectedOptions = Array.from( event.target.options ).filter( ( { selected } ) => selected ); const newValues = selectedOptions.map( ( { value } ) => value ); diff --git a/packages/components/src/select-control/styles/select-control-styles.ts b/packages/components/src/select-control/styles/select-control-styles.ts index 2bcb0c24d5182..63b242fd58cab 100644 --- a/packages/components/src/select-control/styles/select-control-styles.ts +++ b/packages/components/src/select-control/styles/select-control-styles.ts @@ -29,7 +29,7 @@ const fontSizeStyles = ( { selectSize }: SelectProps ) => { small: '11px', }; - const fontSize = sizes[ selectSize ]; + const fontSize = sizes[ selectSize as Size ]; const fontSizeMobile = '16px'; if ( ! fontSize ) return ''; @@ -57,7 +57,7 @@ const sizeStyles = ( { selectSize }: SelectProps ) => { }, }; - const style = sizes[ selectSize ] || sizes.default; + const style = sizes[ selectSize as Size ] || sizes.default; return css( style ); }; diff --git a/packages/components/src/slot-fill/bubbles-virtually/fill.js b/packages/components/src/slot-fill/bubbles-virtually/fill.js index 51a141ca2e2dc..0989e34edbeed 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/fill.js +++ b/packages/components/src/slot-fill/bubbles-virtually/fill.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.js b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.js index dda3be6b82693..8ea7d2cc55b9a 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.js +++ b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.js b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.js index c90f7b72810ad..155265613e784 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.js +++ b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot.js b/packages/components/src/slot-fill/bubbles-virtually/slot.js index 5075677c3e173..cce8a473345f1 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot.js +++ b/packages/components/src/slot-fill/bubbles-virtually/slot.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/slot-fill/bubbles-virtually/use-slot.js b/packages/components/src/slot-fill/bubbles-virtually/use-slot.js index bf6fb96029467..c954ad2c7573f 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/use-slot.js +++ b/packages/components/src/slot-fill/bubbles-virtually/use-slot.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/slot-fill/context.js b/packages/components/src/slot-fill/context.js index b0856cb55ff21..0f7961a8ffd3b 100644 --- a/packages/components/src/slot-fill/context.js +++ b/packages/components/src/slot-fill/context.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/slot-fill/fill.js b/packages/components/src/slot-fill/fill.js index 5c3669e67bf23..860059921a1ae 100644 --- a/packages/components/src/slot-fill/fill.js +++ b/packages/components/src/slot-fill/fill.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/slot-fill/index.js b/packages/components/src/slot-fill/index.js index f91461a338527..4bb323c28fad6 100644 --- a/packages/components/src/slot-fill/index.js +++ b/packages/components/src/slot-fill/index.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/slot-fill/provider.js b/packages/components/src/slot-fill/provider.js index 850c0dfb8a3a0..4ea7e7a825541 100644 --- a/packages/components/src/slot-fill/provider.js +++ b/packages/components/src/slot-fill/provider.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/slot-fill/slot.js b/packages/components/src/slot-fill/slot.js index 6d31767bee245..60a32b63dba3b 100644 --- a/packages/components/src/slot-fill/slot.js +++ b/packages/components/src/slot-fill/slot.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/slot-fill/use-slot.js b/packages/components/src/slot-fill/use-slot.js index e77ca92d4a216..96016e267a681 100644 --- a/packages/components/src/slot-fill/use-slot.js +++ b/packages/components/src/slot-fill/use-slot.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * WordPress dependencies */ diff --git a/packages/components/src/tooltip/index.js b/packages/components/src/tooltip/index.js index ca19890f8cbc6..6ae8b56c95c20 100644 --- a/packages/components/src/tooltip/index.js +++ b/packages/components/src/tooltip/index.js @@ -1,3 +1,4 @@ +// @ts-nocheck /** * External dependencies */ diff --git a/packages/components/src/ui/color-picker/README.md b/packages/components/src/ui/color-picker/README.md new file mode 100644 index 0000000000000..53c2ba6292a32 --- /dev/null +++ b/packages/components/src/ui/color-picker/README.md @@ -0,0 +1,57 @@ +# ColorPicker + +
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +
+ +`ColorPicker` is a color picking component based on `react-colorful`. It lets you pick a color visually or by manipulating the individual RGB(A), HSL(A) and Hex(8) color values. + +## Usage + +```jsx +import { ColorPicker } from '@wordpress/components/ui'; + +function Example() { + const [color, setColor] = useState(); + return ( + + ); +} +``` + +## Props + +### `color` + +**Type**: `string` + +The current color value to display in the picker. Must be a hex or hex8 string. + +### `onChange` + +**Type**: `(hex8Color: string) => void` + +Fired when the color changes. Always passes a hex8 color string. + +### `enableAlpha` + +**Type**: `boolean` + +Defaults to `false`. When `true` the color picker will display the alpha channel both in the bottom inputs as well as in the color picker itself. + +### `defaultValue` + +**Type**: `string | undefined` + +An optional default value to use for the color picker. + +### `copyFormat` + +**Type**: `'hex' | 'hsl' | 'rgb' | undefined` + +The format to copy when clicking the displayed color format. diff --git a/packages/components/src/ui/color-picker/color-display.tsx b/packages/components/src/ui/color-picker/color-display.tsx new file mode 100644 index 0000000000000..fca50030c0a67 --- /dev/null +++ b/packages/components/src/ui/color-picker/color-display.tsx @@ -0,0 +1,154 @@ +/** + * External dependencies + */ +import colorize, { ColorFormats } from 'tinycolor2'; + +/** + * WordPress dependencies + */ +import { useCopyToClipboard } from '@wordpress/compose'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { Text } from '../../text'; +import { Flex, FlexItem } from '../../flex'; +import { Tooltip } from '../tooltip'; +import type { ColorType } from './types'; +import { space } from '../utils/space'; + +interface ColorDisplayProps { + color: ColorFormats.HSLA; + colorType: ColorType; + enableAlpha: boolean; +} + +interface DisplayProps { + color: ColorFormats.HSLA; + enableAlpha: boolean; +} + +type Values = [ number, string ][]; + +interface ValueDisplayProps { + values: Values; +} + +const ValueDisplay = ( { values }: ValueDisplayProps ) => ( + <> + { values.map( ( [ value, abbreviation ] ) => { + return ( + + { abbreviation } + { value } + + ); + } ) } + +); + +const HslDisplay = ( { color, enableAlpha }: DisplayProps ) => { + const { h, s, l, a } = colorize( color ).toHsl(); + + const values: Values = [ + [ Math.floor( h ), 'H' ], + [ Math.round( s * 100 ), 'S' ], + [ Math.round( l * 100 ), 'L' ], + ]; + if ( enableAlpha ) { + values.push( [ Math.round( a * 100 ), 'A' ] ); + } + + return ; +}; + +const RgbDisplay = ( { color, enableAlpha }: DisplayProps ) => { + const { r, g, b, a } = colorize( color ).toRgb(); + + const values: Values = [ + [ r, 'R' ], + [ g, 'G' ], + [ b, 'B' ], + ]; + + if ( enableAlpha ) { + values.push( [ Math.round( a * 100 ), 'A' ] ); + } + + return ; +}; + +const HexDisplay = ( { color, enableAlpha }: DisplayProps ) => { + const colorized = colorize( color ); + const colorWithoutHash = ( enableAlpha + ? colorized.toHex8String() + : colorized.toHexString() + ) + .slice( 1 ) + .toUpperCase(); + return ( + + # + { colorWithoutHash } + + ); +}; + +const getComponent = ( colorType: ColorType ) => { + switch ( colorType ) { + case 'hsl': + return HslDisplay; + case 'rgb': + return RgbDisplay; + default: + case 'hex': + return HexDisplay; + } +}; + +export const ColorDisplay = ( { + color, + colorType, + enableAlpha, +}: ColorDisplayProps ) => { + const [ copiedColor, setCopiedColor ] = useState< string | null >( null ); + const props = { color, enableAlpha }; + const Component = getComponent( colorType ); + const copyRef = useCopyToClipboard< HTMLDivElement >( + () => { + switch ( colorType ) { + case 'hsl': { + return colorize( color ).toHslString(); + } + case 'rgb': { + return colorize( color ).toRgbString(); + } + default: + case 'hex': { + const colorized = colorize( color ); + return enableAlpha + ? colorized.toHex8String() + : colorized.toHexString(); + } + } + }, + () => setCopiedColor( colorize( color ).toHex8String() ) + ); + return ( + + { copiedColor === colorize( color ).toHex8String() + ? __( 'Copied!' ) + : __( 'Copy' ) } + + } + > + + + + + ); +}; diff --git a/packages/components/src/ui/color-picker/color-input.tsx b/packages/components/src/ui/color-picker/color-input.tsx new file mode 100644 index 0000000000000..8caa5fe3a7a5f --- /dev/null +++ b/packages/components/src/ui/color-picker/color-input.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import type { ColorFormats } from 'tinycolor2'; + +/** + * Internal dependencies + */ +import { RgbInput } from './rgb-input'; +import { HslInput } from './hsl-input'; +import { HexInput } from './hex-input'; + +interface ColorInputProps { + colorType: 'hsl' | 'hex' | 'rgb'; + color: ColorFormats.HSLA; + onChange: ( value: ColorFormats.HSLA ) => void; + enableAlpha: boolean; +} + +export const ColorInput = ( { + colorType, + color, + onChange, + enableAlpha, +}: ColorInputProps ) => { + const props = { color, onChange, enableAlpha }; + switch ( colorType ) { + case 'hsl': + return ; + case 'rgb': + return ; + default: + case 'hex': + return ; + } +}; diff --git a/packages/components/src/ui/color-picker/component.tsx b/packages/components/src/ui/color-picker/component.tsx new file mode 100644 index 0000000000000..15a034ed5c444 --- /dev/null +++ b/packages/components/src/ui/color-picker/component.tsx @@ -0,0 +1,141 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import type { Ref } from 'react'; +import type { ColorFormats } from 'tinycolor2'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { moreVertical } from '@wordpress/icons'; +import { useDebounce } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { + useContextSystem, + contextConnect, + PolymorphicComponentProps, +} from '../context'; +import { HStack } from '../../h-stack'; +import Button from '../../button'; +import { Spacer } from '../../spacer'; +import { ColorfulWrapper, SelectControl } from './styles'; +import { ColorDisplay } from './color-display'; +import { ColorInput } from './color-input'; +import { Picker } from './picker'; +import { useControlledValue } from '../../utils/hooks'; + +import type { ColorType } from './types'; + +interface ColorPickerProps { + enableAlpha?: boolean; + color?: ColorFormats.HSL | ColorFormats.HSLA; + onChange?: ( color: ColorFormats.HSL | ColorFormats.HSLA ) => void; + defaultValue?: ColorFormats.HSL | ColorFormats.HSLA; + copyFormat?: ColorType; +} + +const options = [ + { label: 'RGB', value: 'rgb' as const }, + { label: 'HSL', value: 'hsl' as const }, + { label: 'Hex', value: 'hex' as const }, +]; + +const getSafeColor = ( + color: ColorFormats.HSL | ColorFormats.HSLA | undefined +): ColorFormats.HSLA => { + return color ? { a: 1, ...color } : { h: 0, s: 0, l: 100, a: 1 }; +}; + +const ColorPicker = ( + props: PolymorphicComponentProps< ColorPickerProps, 'div', false >, + forwardedRef: Ref< any > +) => { + const { + enableAlpha = false, + color: colorProp, + onChange, + defaultValue, + copyFormat, + } = useContextSystem( props, 'ColorPicker' ); + + const [ color, setColor ] = useControlledValue( { + onChange, + value: colorProp, + defaultValue, + } ); + + // Debounce to prevent rapid changes from conflicting with one another. + const debouncedSetColor = useDebounce( setColor ); + + const handleChange = ( + nextValue: ColorFormats.HSLA | ColorFormats.HSL + ) => { + debouncedSetColor( nextValue ); + }; + + // Use a safe default value for the color and remove the possibility of `undefined`. + const safeColor = getSafeColor( color ); + + const [ showInputs, setShowInputs ] = useState< boolean >( false ); + const [ colorType, setColorType ] = useState< ColorType >( + copyFormat || 'hex' + ); + + return ( + + + + { showInputs ? ( + + setColorType( nextColorType as ColorType ) + } + label={ __( 'Color format' ) } + hideLabelFromVision + /> + ) : ( + + ) } +