Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

components: Add new ColorPicker #33714

Merged
merged 17 commits into from
Aug 10, 2021
Merged
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
components: Add new ColorPicker
sarayourfriend committed Aug 6, 2021

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
commit 72037c9ab87dd82a1222018f1a8b3d17d27ec75c
80 changes: 80 additions & 0 deletions packages/components/src/ui/color-picker/color-display.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* External dependencies
*/
import colorize from 'tinycolor2';

/**
* Internal dependencies
*/
import { Text } from '../../text';
import { Spacer } from '../../spacer';
import { space } from '../utils/space';

interface ColorDisplayProps {
color: string;
colorType: 'hex' | 'hsl' | 'rgb';
}

interface DisplayProps {
color: string;
}

const HslDisplay = ( { color }: DisplayProps ) => {
const { h, s, l } = colorize( color ).toHsl();

return (
<div>
{ Math.floor( h ) }
<Spacer as={ Text } marginRight={ space( 1 ) } color="blue">
H
</Spacer>
{ Math.round( s * 100 ) }
<Spacer as={ Text } marginRight={ space( 1 ) } color="blue">
S
</Spacer>
{ Math.round( l * 100 ) }
<Text color="blue">L</Text>
</div>
);
};

const RgbDisplay = ( { color }: DisplayProps ) => {
const { r, g, b } = colorize( color ).toRgb();

return (
<div>
{ r }
<Spacer as={ Text } marginRight={ space( 1 ) } color="blue">
R
</Spacer>
{ g }
<Spacer as={ Text } marginRight={ space( 1 ) } color="blue">
G
</Spacer>
{ b }
<Text color="blue">B</Text>
</div>
);
};

const HexDisplay = ( { color }: DisplayProps ) => {
const colorWithoutHash = color.slice( 1 );
return (
<>
<Text>{ colorWithoutHash }</Text>
<Text color="blue">#</Text>
</>
);
};

export const ColorDisplay = ( { color, colorType }: ColorDisplayProps ) => {
switch ( colorType ) {
case 'hsl':
return <HslDisplay color={ color } />;
case 'rgb':
return <RgbDisplay color={ color } />;
default:
case 'hex':
return <HexDisplay color={ color } />;
}
};
28 changes: 28 additions & 0 deletions packages/components/src/ui/color-picker/color-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Internal dependencies
*/
import { RgbInput } from './rgb-input';
import { HslInput } from './hsl-input';
import { HexInput } from './hex-input';

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

export const ColorInput = ( {
colorType,
color,
onChange,
}: ColorInputProps ) => {
switch ( colorType ) {
case 'hsl':
return <HslInput color={ color } onChange={ onChange } />;
case 'rgb':
return <RgbInput color={ color } onChange={ onChange } />;
default:
case 'hex':
return <HexInput color={ color } onChange={ onChange } />;
}
};
94 changes: 94 additions & 0 deletions packages/components/src/ui/color-picker/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import type { Ref } from 'react';

/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';
import { moreVertical } from '@wordpress/icons';

/**
* Internal dependencies
*/
import {
useContextSystem,
contextConnect,
PolymorphicComponentProps,
} from '../context';
import { HStack } from '../../h-stack';
import Button from '../../button';
import { ColorfulWrapper, SelectControl } from './styles';
import { HexColorPicker } from './hex-color-picker';
import { HslaColorPicker } from './hsla-color-picker';
import { ColorDisplay } from './color-display';
import { ColorInput } from './color-input';

interface ColorPickerProps {
disableAlpha?: boolean;
initialColor?: string;
}

type ColorType = 'rgb' | 'hsl' | 'hex';

const options = [
{ label: 'RGB', value: 'rgb' as const },
{ label: 'HSL', value: 'hsl' as const },
{ label: 'Hex', value: 'hex' as const },
];

const ColorPicker = (
props: PolymorphicComponentProps< ColorPickerProps, 'div', false >,
forwardedRef: Ref< any >
) => {
const { disableAlpha = true, initialColor } = useContextSystem(
props,
'ColorPicker'
);

const [ showInputs, setShowInputs ] = useState< boolean >( false );
const [ colorType, setColorType ] = useState< ColorType >( 'rgb' );

const [ color, setColor ] = useState(
initialColor || ( disableAlpha ? '#000000' : '#000000ff' )
);

const Picker = disableAlpha ? HexColorPicker : HslaColorPicker;

return (
<ColorfulWrapper ref={ forwardedRef }>
<Picker onChange={ setColor } color={ color } />
<HStack>
{ showInputs ? (
<SelectControl
options={ options }
value={ colorType }
onChange={ ( nextColorType ) =>
setColorType( nextColorType as ColorType )
}
/>
) : (
<ColorDisplay color={ color } colorType={ colorType } />
) }
<Button
onClick={ () => setShowInputs( ! showInputs ) }
icon={ moreVertical }
isPressed={ showInputs }
></Button>
</HStack>
{ showInputs && (
<ColorInput
colorType={ colorType }
color={ color }
onChange={ setColor }
/>
) }
</ColorfulWrapper>
);
};

const ConnectedColorPicker = contextConnect( ColorPicker, 'ColorPicker' );

export default ConnectedColorPicker;
19 changes: 19 additions & 0 deletions packages/components/src/ui/color-picker/hex-color-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import type { ComponentProps } from 'react';
import { HexColorPicker as Picker } from 'react-colorful';

/**
* Internal dependencies
*/
import { ColorfulWrapper } from './styles';

type Props = ComponentProps< typeof Picker >;

export const HexColorPicker = ( props: Props ) => (
<ColorfulWrapper>
<Picker { ...props } />
</ColorfulWrapper>
);
41 changes: 41 additions & 0 deletions packages/components/src/ui/color-picker/hex-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* External dependencies
*/
import colorize from 'tinycolor2';

/**
* Internal dependencies
*/
import { Text } from '../../text';
import { Spacer } from '../../spacer';
import InputControl from '../../input-control';
import { space } from '../utils/space';

interface HexInputProps {
color: string;
onChange: ( value: string ) => void;
}

export const HexInput = ( { color, onChange }: HexInputProps ) => {
const handleValidate = ( value: string ) => {
if ( ! colorize( value ).isValid() ) {
throw new Error( 'Invalid hex color input' );
}
};

return (
<InputControl
__unstableInputWidth="7em"
suffix={
<Spacer as={ Text } marginX={ space( 2 ) } color="blue">
#
</Spacer>
}
value={ color.slice( 1 ) }
onChange={ ( nextValue ) =>
onChange( colorize( nextValue ).toHex8String() )
}
onValidate={ handleValidate }
/>
);
};
57 changes: 57 additions & 0 deletions packages/components/src/ui/color-picker/hsl-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* External dependencies
*/
import colorize from 'tinycolor2';

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

interface HslInputProps {
color: string;
onChange: ( color: string ) => void;
}

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

return (
<>
<InputWithSlider
min={ 1 }
max={ 360 }
label="Hue"
abbreviation="H"
value={ Math.trunc( h ) }
onChange={ ( nextH: number ) =>
onChange( colorize( { h: nextH, s, l } ).toHex8String() )
}
/>
<InputWithSlider
min={ 0 }
max={ 100 }
label="Saturation"
abbreviation="S"
value={ Math.trunc( 100 * s ) }
onChange={ ( nextS: number ) =>
onChange(
colorize( { h, s: nextS / 100, l } ).toHex8String()
)
}
/>
<InputWithSlider
min={ 0 }
max={ 100 }
label="Lightness"
abbreviation="L"
value={ Math.trunc( 100 * l ) }
onChange={ ( nextL: number ) =>
onChange(
colorize( { h, s, l: nextL / 100 } ).toHex8String()
)
}
/>
</>
);
};
47 changes: 47 additions & 0 deletions packages/components/src/ui/color-picker/hsla-color-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import type { ComponentProps } from 'react';
import { HslaColorPicker as Picker } from 'react-colorful';
import colorize from 'tinycolor2';

/**
* WordPress dependencies
*/
import { useState, useEffect } from '@wordpress/element';

/**
* Internal dependencies
*/
import { ColorfulWrapper } from './styles';

type PickerProps = ComponentProps< typeof Picker >;
interface OwnProps {
onChange: ( hexColor: string ) => void;
color?: string;
}

export const HslaColorPicker = ( {
onChange,
color,
...props
}: Omit< PickerProps, keyof OwnProps > & OwnProps ) => {
const [ hslaColor, setHslaColor ] = useState( () =>
colorize( color ).toHsl()
);

useEffect( () => {
onChange( colorize( hslaColor ).toHex8String() );
}, [ hslaColor, onChange ] );

return (
<ColorfulWrapper>
<Picker
{ ...props }
onChange={ setHslaColor }
color={ hslaColor }
/>
</ColorfulWrapper>
);
};
1 change: 1 addition & 0 deletions packages/components/src/ui/color-picker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ColorPicker } from './component';
74 changes: 74 additions & 0 deletions packages/components/src/ui/color-picker/input-with-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';

/**
* Internal dependencies
*/
import RangeControl from '../../range-control';
import NumberControl from '../../number-control';
import { HStack } from '../../h-stack';
import { Text } from '../../text';
import { Spacer } from '../../spacer';
import { space } from '../utils/space';
import { StyledField } from '../../base-control/styles/base-control-styles';

const StyledRangeControl = styled( RangeControl )`
flex: 1;
${ StyledField } {
margin-bottom: 0;
}
`;

const Wrapper = styled( HStack )`
margin-bottom: ${ space( 2 ) };
`;

interface InputWithSliderProps {
min: number;
max: number;
value: number;
label: string;
abbreviation: string;
onChange: ( value: number ) => void;
}

export const InputWithSlider = ( {
min,
max,
label,
abbreviation,
onChange,
value,
}: InputWithSliderProps ) => {
return (
<Wrapper>
<NumberControl
__unstableInputWidth="5em"
min={ min }
max={ max }
label={ label }
hideLabelFromVision
value={ value }
onChange={ onChange }
suffix={
<Spacer as={ Text } paddingX={ space( 1 ) } color="blue">
{ abbreviation }
</Spacer>
}
hideHTMLArrows
/>
<StyledRangeControl
label={ label }
hideLabelFromVision
min={ min }
max={ max }
value={ value }
onChange={ onChange }
withInputField={ false }
/>
</Wrapper>
);
};
53 changes: 53 additions & 0 deletions packages/components/src/ui/color-picker/rgb-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* External dependencies
*/
import colorize from 'tinycolor2';

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

interface RgbInputProps {
color: string;
onChange: ( color: string ) => void;
}

export const RgbInput = ( { color, onChange }: RgbInputProps ) => {
const { r, g, b } = colorize( color ).toRgb();

return (
<>
<InputWithSlider
min={ 0 }
max={ 255 }
label="Red"
abbreviation="R"
value={ r }
onChange={ ( nextR: number ) =>
onChange( colorize( { r: nextR, g, b } ).toHex8String() )
}
/>
<InputWithSlider
min={ 0 }
max={ 255 }
label="Green"
abbreviation="G"
value={ g }
onChange={ ( nextG: number ) =>
onChange( colorize( { r, g: nextG, b } ).toHex8String() )
}
/>
<InputWithSlider
min={ 0 }
max={ 255 }
label="Blue"
abbreviation="B"
value={ b }
onChange={ ( nextB: number ) =>
onChange( colorize( { r, g, b: nextB } ).toHex8String() )
}
/>
</>
);
};
34 changes: 34 additions & 0 deletions packages/components/src/ui/color-picker/stories/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* External dependencies
*/
import { boolean } from '@storybook/addon-knobs';

/**
* Internal dependencies
*/
import { ColorPicker } from '..';

export default {
component: ColorPicker,
title: 'Components (Experimental)/ColorPicker',
};

export const _default = () => {
const props = {
disableAlpha: boolean( 'disableAlpha', true ),
};

return (
<div
style={ {
width: '100vw',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
} }
>
<ColorPicker { ...props } />
</div>
);
};
40 changes: 40 additions & 0 deletions packages/components/src/ui/color-picker/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';

/**
* Internal dependencies
*/
import InnerSelectControl from '../../select-control';
import { space } from '../utils/space';

export const SelectControl = styled( InnerSelectControl )`
width: 5em;
`;

export const ColorfulWrapper = styled.div`
width: 216px;
.react-colorful {
display: flex;
flex-direction: column;
align-items: center;
width: 216px;
height: auto;
}
.react-colorful__saturation {
width: 100%;
border-radius: 0;
height: 216px;
margin-bottom: ${ space( 4 ) };
}
.react-colorful__hue,
.react-colorful__alpha {
width: 184px;
border-radius: 16px;
margin-bottom: ${ space( 2 ) };
}
`;