Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Switch to HSL(A) based input and output to fix HSL UX
  • Loading branch information
sarayourfriend committed Aug 9, 2021
commit f405c0aab73ea2e9e2258c2badf2090bd10e9c34
27 changes: 19 additions & 8 deletions packages/components/src/ui/color-picker/color-display.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import colorize from 'tinycolor2';
import colorize, { ColorFormats } from 'tinycolor2';

/**
* WordPress dependencies
Expand All @@ -20,13 +20,13 @@ import type { ColorType } from './types';
import { space } from '../utils/space';

interface ColorDisplayProps {
color: string;
color: ColorFormats.HSLA;
colorType: ColorType;
enableAlpha: boolean;
}

interface DisplayProps {
color: string;
color: ColorFormats.HSLA;
enableAlpha: boolean;
}

Expand Down Expand Up @@ -80,8 +80,14 @@ const RgbDisplay = ( { color, enableAlpha }: DisplayProps ) => {
return <ValueDisplay values={ values } />;
};

const HexDisplay = ( { color }: DisplayProps ) => {
const colorWithoutHash = color.slice( 1 ).toUpperCase();
const HexDisplay = ( { color, enableAlpha }: DisplayProps ) => {
const colorized = colorize( color );
const colorWithoutHash = ( enableAlpha
? colorized.toHex8String()
: colorized.toHexString()
)
.slice( 1 )
.toUpperCase();
return (
<FlexItem>
<Text color="blue">#</Text>
Expand Down Expand Up @@ -121,17 +127,22 @@ export const ColorDisplay = ( {
}
default:
case 'hex': {
return color.toUpperCase();
const colorized = colorize( color );
return enableAlpha
? colorized.toHex8String()
: colorized.toHexString();
}
}
},
() => setCopiedColor( color )
() => setCopiedColor( colorize( color ).toHex8String() )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed a minor UX improvement that we could make:

  1. User clicks on the color, value is copied in the clipboard. The tooltip changes from "Copy" to "Copied!"
  2. User interacts with other parts of the UI, maybe copies another string of text
  3. User remembers that they need the color value, and so they hover over the color string again. But the tooltip keeps saying "Copied!", instead of "Copy"

This to say, should we use a timer to unset the copiedColor after some time has passed?

);
return (
<Tooltip
content={
<Text color="white">
{ copiedColor === color ? __( 'Copied!' ) : __( 'Copy' ) }
{ copiedColor === colorize( color ).toHex8String()
? __( 'Copied!' )
: __( 'Copy' ) }
</Text>
}
>
Expand Down
9 changes: 7 additions & 2 deletions packages/components/src/ui/color-picker/color-input.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import type { ColorFormats } from 'tinycolor2';

/**
* Internal dependencies
*/
Expand All @@ -7,8 +12,8 @@ import { HexInput } from './hex-input';

interface ColorInputProps {
colorType: 'hsl' | 'hex' | 'rgb';
color: string;
onChange: ( value: string ) => void;
color: ColorFormats.HSLA;
onChange: ( value: ColorFormats.HSLA ) => void;
enableAlpha: boolean;
}

Expand Down
40 changes: 20 additions & 20 deletions packages/components/src/ui/color-picker/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
// eslint-disable-next-line no-restricted-imports
import type { Ref } from 'react';
import colorize from 'tinycolor2';
import type { ColorFormats } from 'tinycolor2';

/**
* WordPress dependencies
Expand All @@ -24,19 +24,18 @@ import { HStack } from '../../h-stack';
import Button from '../../button';
import { Spacer } from '../../spacer';
import { ColorfulWrapper, SelectControl } from './styles';
import { HexColorPicker } from './hex-color-picker';
import { RgbaColorPicker } from './rgba-color-picker';
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?: string;
onChange?: ( hexColor: string ) => void;
defaultValue?: string;
color?: ColorFormats.HSL | ColorFormats.HSLA;
onChange?: ( hexColor: ColorFormats.HSL | ColorFormats.HSLA ) => void;
defaultValue?: ColorFormats.HSL | ColorFormats.HSLA;
copyFormat?: ColorType;
}

Expand All @@ -46,9 +45,10 @@ const options = [
{ label: 'Hex', value: 'hex' as const },
];

const getSafeColor = ( color: string | undefined, enableAlpha: boolean ) => {
const def = enableAlpha ? '#ffffffff' : '#ffffff';
return ! color ? def : color;
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 = (
Expand All @@ -72,27 +72,27 @@ const ColorPicker = (
// Debounce to prevent rapid changes from conflicting with one another.
const debouncedSetColor = useDebounce( setColor );

const handleChange = ( nextValue: string ) => {
debouncedSetColor(
enableAlpha
? colorize( nextValue ).toHex8String()
: colorize( nextValue ).toHexString()
);
const handleChange = (
nextValue: ColorFormats.HSLA | ColorFormats.HSL
) => {
debouncedSetColor( nextValue );
};

// Use a safe default value for the color and remove the possibility of `undefined`. This is what react-colorful defaults to if it's not passed anything so it's the safest default
const safeColor = getSafeColor( color, enableAlpha );
// 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'
);

const Picker = enableAlpha ? RgbaColorPicker : HexColorPicker;

return (
<ColorfulWrapper ref={ forwardedRef }>
<Picker onChange={ handleChange } color={ safeColor } />
<Picker
onChange={ handleChange }
color={ safeColor }
enableAlpha={ enableAlpha }
/>
<HStack justify="space-between">
{ showInputs ? (
<SelectControl
Expand Down
19 changes: 0 additions & 19 deletions packages/components/src/ui/color-picker/hex-color-picker.tsx

This file was deleted.

15 changes: 10 additions & 5 deletions packages/components/src/ui/color-picker/hex-input.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import colorize from 'tinycolor2';
import colorize, { ColorFormats } from 'tinycolor2';

/**
* Internal dependencies
Expand All @@ -12,8 +12,8 @@ import InputControl from '../../input-control';
import { space } from '../utils/space';

interface HexInputProps {
color: string;
onChange: ( value: string ) => void;
color: ColorFormats.HSLA;
onChange: ( value: ColorFormats.HSLA ) => void;
enableAlpha: boolean;
}

Expand All @@ -24,6 +24,11 @@ export const HexInput = ( { color, onChange, enableAlpha }: HexInputProps ) => {
}
};

const colorized = colorize( color );
const value = enableAlpha
? colorized.toHex8String()
: colorized.toHexString();

return (
<InputControl
__unstableInputWidth="8em"
Expand All @@ -32,9 +37,9 @@ export const HexInput = ( { color, onChange, enableAlpha }: HexInputProps ) => {
#
</Spacer>
}
value={ color.slice( 1 ).toUpperCase() }
value={ value.slice( 1 ).toUpperCase() }
onChange={ ( nextValue ) =>
onChange( colorize( nextValue ).toHex8String() )
onChange( colorize( nextValue ).toHsl() )
}
onValidate={ handleValidate }
maxLength={ enableAlpha ? 8 : 6 }
Expand Down
36 changes: 15 additions & 21 deletions packages/components/src/ui/color-picker/hsl-input.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
/**
* External dependencies
*/
import colorize from 'tinycolor2';
import type { ColorFormats } from 'tinycolor2';

/**
* Internal dependencies
*/
import { InputWithSlider } from './input-with-slider';

interface HslInputProps {
color: string;
onChange: ( color: string ) => void;
color: ColorFormats.HSLA;
onChange: ( color: ColorFormats.HSLA ) => void;
enableAlpha: boolean;
}

export const HslInput = ( { color, onChange, enableAlpha }: HslInputProps ) => {
const { h, s, l, a } = colorize( color ).toHsl();
const { h, s, l, a } = color;

return (
<>
Expand All @@ -26,31 +26,27 @@ export const HslInput = ( { color, onChange, enableAlpha }: HslInputProps ) => {
abbreviation="H"
value={ Math.trunc( h ) }
onChange={ ( nextH: number ) =>
onChange( colorize( { h: nextH, s, l, a } ).toHex8String() )
onChange( { h: nextH, s, l, a } )
}
/>
<InputWithSlider
min={ 0 }
max={ 100 }
label="Saturation"
abbreviation="S"
value={ Math.trunc( 100 * s ) }
value={ s <= 1 ? Math.trunc( 100 * s ) : s }
onChange={ ( nextS: number ) =>
onChange(
colorize( { h, s: nextS / 100, l, a } ).toHex8String()
)
onChange( { h, s: nextS / 100, l, a } )
}
/>
<InputWithSlider
min={ 0 }
max={ 100 }
label="Lightness"
abbreviation="L"
value={ Math.trunc( 100 * l ) }
value={ l <= 1 ? Math.trunc( 100 * l ) : l }
onChange={ ( nextL: number ) =>
onChange(
colorize( { h, s, l: nextL / 100, a } ).toHex8String()
)
onChange( { h, s, l: nextL / 100, a } )
}
/>
{ enableAlpha && (
Expand All @@ -61,14 +57,12 @@ export const HslInput = ( { color, onChange, enableAlpha }: HslInputProps ) => {
abbreviation="A"
value={ Math.trunc( 100 * a ) }
onChange={ ( nextA: number ) =>
onChange(
colorize( {
h,
s,
l,
a: nextA / 100,
} ).toHex8String()
)
onChange( {
h,
s,
l,
a: nextA / 100,
} )
}
/>
) }
Expand Down
26 changes: 26 additions & 0 deletions packages/components/src/ui/color-picker/picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* External dependencies
*/
import type { ColorFormats } from 'tinycolor2';
import { HslColorPicker, HslaColorPicker } from 'react-colorful';

interface PickerProps {
color: ColorFormats.HSL | ColorFormats.HSLA;
enableAlpha: boolean;
onChange: ( nextColor: ColorFormats.HSL | ColorFormats.HSLA ) => void;
}

export const Picker = ( { color, enableAlpha, onChange }: PickerProps ) => {
// RC accepts s and l as a range from 0 - 100, whereas tinycolor expects a decimal value representing the percentage
// so we need to make sure that we're giving RC the correct color format that it expects
const reactColorfulColor = {
h: color.h,
s: color.s <= 1 ? Math.floor( color.s * 100 ) : color.s,
l: color.l <= 1 ? Math.floor( color.l * 100 ) : color.l,
a: ( color as ColorFormats.HSLA ).a,
};

const Component = enableAlpha ? HslaColorPicker : HslColorPicker;

return <Component color={ reactColorfulColor } onChange={ onChange } />;
};
Loading