Skip to content

Commit f42332d

Browse files
authored
Color area hsl/hsb (#2887)
support color area hsl/hsb, expose gradient creation
1 parent 3c9ebc9 commit f42332d

File tree

11 files changed

+659
-154
lines changed

11 files changed

+659
-154
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2022 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
export const generateRGB_R = (orientation, dir: boolean, zValue: number) => {
14+
let maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`;
15+
let result = {
16+
colorAreaStyles: {
17+
backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(${zValue},0,0),rgb(${zValue},255,0))`
18+
},
19+
gradientStyles: {
20+
backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(${zValue},0,255),rgb(${zValue},255,255))`,
21+
'WebkitMaskImage': maskImage,
22+
maskImage
23+
}
24+
};
25+
return result;
26+
};
27+
28+
export const generateRGB_G = (orientation, dir: boolean, zValue: number) => {
29+
let maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`;
30+
let result = {
31+
colorAreaStyles: {
32+
backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,${zValue},0),rgb(255,${zValue},0))`
33+
},
34+
gradientStyles: {
35+
backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,${zValue},255),rgb(255,${zValue},255))`,
36+
'WebkitMaskImage': maskImage,
37+
maskImage
38+
}
39+
};
40+
return result;
41+
};
42+
43+
export const generateRGB_B = (orientation, dir: boolean, zValue: number) => {
44+
let maskImage = `linear-gradient(to ${orientation[Number(!dir)]}, transparent, #000)`;
45+
let result = {
46+
colorAreaStyles: {
47+
backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,0,${zValue}),rgb(255,0,${zValue}))`
48+
},
49+
gradientStyles: {
50+
backgroundImage: `linear-gradient(to ${orientation[Number(dir)]},rgb(0,255,${zValue}),rgb(255,255,${zValue}))`,
51+
'WebkitMaskImage': maskImage,
52+
maskImage
53+
}
54+
};
55+
return result;
56+
};
57+
58+
59+
export const generateHSL_H = (orientation, dir: boolean, zValue: number) => {
60+
let result = {
61+
colorAreaStyles: {},
62+
gradientStyles: {
63+
background: [
64+
`linear-gradient(to ${orientation[Number(dir)]}, hsla(0,0%,0%,1) 0%, hsla(0,0%,0%,0) 50%, hsla(0,0%,100%,0) 50%, hsla(0,0%,100%,1) 100%)`,
65+
`linear-gradient(to ${orientation[Number(!dir)]},hsl(0,0%,50%),hsla(0,0%,50%,0))`,
66+
`hsl(${zValue}, 100%, 50%)`
67+
].join(',')
68+
}
69+
};
70+
return result;
71+
};
72+
73+
export const generateHSL_S = (orientation, dir: boolean, alphaValue: number) => {
74+
let result = {
75+
colorAreaStyles: {},
76+
gradientStyles: {
77+
background: [
78+
`linear-gradient(to ${orientation[Number(!dir)]}, hsla(0,0%,0%,${alphaValue}) 0%, hsla(0,0%,0%,0) 50%, hsla(0,0%,100%,0) 50%, hsla(0,0%,100%,${alphaValue}) 100%)`,
79+
`linear-gradient(to ${orientation[Number(dir)]},hsla(0,100%,50%,${alphaValue}),hsla(60,100%,50%,${alphaValue}),hsla(120,100%,50%,${alphaValue}),hsla(180,100%,50%,${alphaValue}),hsla(240,100%,50%,${alphaValue}),hsla(300,100%,50%,${alphaValue}),hsla(359,100%,50%,${alphaValue}))`,
80+
'hsl(0, 0%, 50%)'
81+
].join(',')
82+
}
83+
};
84+
return result;
85+
};
86+
87+
export const generateHSL_L = (orientation, dir: boolean, zValue: number) => {
88+
let result = {
89+
colorAreaStyles: {},
90+
gradientStyles: {
91+
backgroundImage: [
92+
`linear-gradient(to ${orientation[Number(!dir)]},hsl(0,0%,${zValue}%),hsla(0,0%,${zValue}%,0))`,
93+
`linear-gradient(to ${orientation[Number(dir)]},hsl(0,100%,${zValue}%),hsl(60,100%,${zValue}%),hsl(120,100%,${zValue}%),hsl(180,100%,${zValue}%),hsl(240,100%,${zValue}%),hsl(300,100%,${zValue}%),hsl(360,100%,${zValue}%))`
94+
].join(',')
95+
}
96+
};
97+
return result;
98+
};
99+
100+
101+
export const generateHSB_H = (orientation, dir: boolean, zValue: number) => {
102+
let result = {
103+
colorAreaStyles: {},
104+
gradientStyles: {
105+
background: [
106+
`linear-gradient(to ${orientation[Number(dir)]},hsl(0,0%,0%),hsla(0,0%,0%,0))`,
107+
`linear-gradient(to ${orientation[Number(!dir)]},hsl(0,0%,100%),hsla(0,0%,100%,0))`,
108+
`hsl(${zValue}, 100%, 50%)`
109+
].join(',')
110+
}
111+
};
112+
return result;
113+
};
114+
115+
export const generateHSB_S = (orientation, dir: boolean, alphaValue: number) => {
116+
let result = {
117+
colorAreaStyles: {},
118+
gradientStyles: {
119+
background: [
120+
`linear-gradient(to ${orientation[Number(!dir)]},hsla(0,0%,0%,${alphaValue}),hsla(0,0%,0%,0))`,
121+
`linear-gradient(to ${orientation[Number(dir)]},hsla(0,100%,50%,${alphaValue}),hsla(60,100%,50%,${alphaValue}),hsla(120,100%,50%,${alphaValue}),hsla(180,100%,50%,${alphaValue}),hsla(240,100%,50%,${alphaValue}),hsla(300,100%,50%,${alphaValue}),hsla(359,100%,50%,${alphaValue}))`,
122+
`linear-gradient(to ${orientation[Number(!dir)]},hsl(0,0%,0%),hsl(0,0%,100%))`
123+
].join(',')
124+
}
125+
};
126+
return result;
127+
};
128+
129+
export const generateHSB_B = (orientation, dir: boolean, alphaValue: number) => {
130+
let result = {
131+
colorAreaStyles: {},
132+
gradientStyles: {
133+
background: [
134+
`linear-gradient(to ${orientation[Number(!dir)]},hsla(0,0%,100%,${alphaValue}),hsla(0,0%,100%,0))`,
135+
`linear-gradient(to ${orientation[Number(dir)]},hsla(0,100%,50%,${alphaValue}),hsla(60,100%,50%,${alphaValue}),hsla(120,100%,50%,${alphaValue}),hsla(180,100%,50%,${alphaValue}),hsla(240,100%,50%,${alphaValue}),hsla(300,100%,50%,${alphaValue}),hsla(359,100%,50%,${alphaValue}))`,
136+
'#000'
137+
].join(',')
138+
}
139+
};
140+
return result;
141+
};

packages/@react-aria/color/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export * from './useColorArea';
1414
export * from './useColorSlider';
1515
export * from './useColorWheel';
1616
export * from './useColorField';
17+
export * from './gradients';

packages/@react-aria/color/src/useColorArea.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {AriaColorAreaProps} from '@react-types/color';
13+
import {AriaColorAreaProps, ColorChannel} from '@react-types/color';
1414
import {ColorAreaState} from '@react-stately/color';
1515
import {focusWithoutScrolling, isAndroid, isIOS, mergeProps, useGlobalListeners, useLabels} from '@react-aria/utils';
1616
// @ts-ignore
@@ -71,7 +71,7 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
7171
stateRef.current = state;
7272
let {xChannel, yChannel} = stateRef.current.channels;
7373
let xChannelStep = stateRef.current.xChannelStep;
74-
let yChannelStep = stateRef.current.xChannelStep;
74+
let yChannelStep = stateRef.current.yChannelStep;
7575

7676
let currentPosition = useRef<{x: number, y: number}>(null);
7777

@@ -310,11 +310,16 @@ export function useColorArea(props: ColorAreaAriaProps, state: ColorAreaState):
310310

311311
let colorAriaLabellingProps = useLabels(props);
312312

313-
let getValueTitle = () => [
314-
formatMessage('colorNameAndValue', {name: state.value.getChannelName('red', locale), value: state.value.formatChannelValue('red', locale)}),
315-
formatMessage('colorNameAndValue', {name: state.value.getChannelName('green', locale), value: state.value.formatChannelValue('green', locale)}),
316-
formatMessage('colorNameAndValue', {name: state.value.getChannelName('blue', locale), value: state.value.formatChannelValue('blue', locale)})
317-
].join(', ');
313+
let getValueTitle = () => {
314+
const channels: [ColorChannel, ColorChannel, ColorChannel] = state.value.getColorChannels();
315+
const colorNamesAndValues = [];
316+
channels.forEach(channel =>
317+
colorNamesAndValues.push(
318+
formatMessage('colorNameAndValue', {name: state.value.getChannelName(channel, locale), value: state.value.formatChannelValue(channel, locale)})
319+
)
320+
);
321+
return colorNamesAndValues.length ? colorNamesAndValues.join(', ') : null;
322+
};
318323

319324
let ariaRoleDescription = isMobile ? null : formatMessage('twoDimensionalSlider');
320325

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright 2022 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {ColorArea, ColorField, ColorSlider, ColorWheel} from '../';
14+
import {Flex} from '@adobe/react-spectrum';
15+
import {Meta, Story} from '@storybook/react';
16+
import {parseColor} from '@react-stately/color';
17+
import React, {useState} from 'react';
18+
import {SpectrumColorAreaProps} from '@react-types/color';
19+
20+
21+
const meta: Meta<SpectrumColorAreaProps> = {
22+
title: 'ColorArea',
23+
component: ColorArea
24+
};
25+
26+
export default meta;
27+
28+
const Template: Story<SpectrumColorAreaProps> = (args) => (
29+
<ColorAreaExample {...args} />
30+
);
31+
32+
function ColorAreaExample(props: SpectrumColorAreaProps) {
33+
let {xChannel, yChannel, isDisabled} = props;
34+
let defaultValue = typeof props.defaultValue === 'string' ? parseColor(props.defaultValue) : props.defaultValue;
35+
let [color, setColor] = useState(defaultValue || parseColor('#ff00ff'));
36+
let xyChannels = {xChannel, yChannel};
37+
let colorSpace = color.getColorSpace();
38+
let {zChannel} = color.getColorSpaceAxes(xyChannels);
39+
let isHue = zChannel === 'hue';
40+
41+
function onChange(e) {
42+
const newColor = (e || color).toFormat(colorSpace);
43+
if (props.onChange) {
44+
props.onChange(newColor);
45+
}
46+
setColor(newColor);
47+
}
48+
49+
return (
50+
<div role="group" aria-label={`${colorSpace.toUpperCase()} Color Picker`}>
51+
<Flex gap="size-500" alignItems="start">
52+
<Flex direction="column" gap={isHue ? 0 : 'size-50'} alignItems="center">
53+
<ColorArea
54+
size={isHue ? 'size-1200' : null}
55+
{...props}
56+
value={color}
57+
onChange={onChange}
58+
onChangeEnd={props.onChangeEnd} />
59+
{isHue ? (
60+
<ColorWheel
61+
value={color}
62+
onChange={onChange}
63+
onChangeEnd={props.onChangeEnd}
64+
isDisabled={isDisabled}
65+
size={'size-2400'}
66+
UNSAFE_style={{
67+
marginTop: 'calc( -.75 * var(--spectrum-global-dimension-size-2400))'
68+
}} />
69+
) : (
70+
<ColorSlider
71+
value={color}
72+
onChange={onChange}
73+
onChangeEnd={props.onChangeEnd}
74+
channel={zChannel}
75+
isDisabled={isDisabled} />
76+
)}
77+
</Flex>
78+
<Flex direction="column" alignItems="center" gap="size-100" minWidth="size-1200">
79+
<div
80+
role="img"
81+
aria-label={`color swatch: ${color.toString('rgb')}`}
82+
title={`${color.toString('hex')}`}
83+
style={{width: '96px', height: '96px', background: color.toString('css')}} />
84+
<ColorField
85+
label="HEX Color"
86+
value={color}
87+
onChange={onChange}
88+
onKeyDown={event =>
89+
event.key === 'Enter' &&
90+
onChange((event.target as HTMLInputElement).value)
91+
}
92+
isDisabled={isDisabled}
93+
width="size-1200" />
94+
</Flex>
95+
</Flex>
96+
</div>
97+
);
98+
}
99+
100+
export let XBlueYGreen = Template.bind({});
101+
XBlueYGreen.storyName = 'RGB xChannel="blue", yChannel="green"';
102+
XBlueYGreen.args = {xChannel: 'blue', yChannel: 'green'};
103+
104+
export let XGreenYBlue = Template.bind({});
105+
XGreenYBlue.storyName = 'RGB xChannel="green", yChannel="blue"';
106+
XGreenYBlue.args = {...XBlueYGreen.args, xChannel: 'green', yChannel: 'blue'};
107+
108+
export let XBlueYRed = Template.bind({});
109+
XBlueYRed.storyName = 'RGB xChannel="blue", yChannel="red"';
110+
XBlueYRed.args = {...XBlueYGreen.args, xChannel: 'blue', yChannel: 'red'};
111+
112+
export let XRedYBlue = Template.bind({});
113+
XRedYBlue.storyName = 'RGB xChannel="red", yChannel="blue"';
114+
XRedYBlue.args = {...XBlueYGreen.args, xChannel: 'red', yChannel: 'blue'};
115+
116+
export let XRedYGreen = Template.bind({});
117+
XRedYGreen.storyName = 'RGB xChannel="red", yChannel="green"';
118+
XRedYGreen.args = {...XBlueYGreen.args, xChannel: 'red', yChannel: 'green'};
119+
120+
export let XGreenYRed = Template.bind({});
121+
XGreenYRed.storyName = 'RGB xChannel="green", yChannel="red"';
122+
XGreenYRed.args = {...XBlueYGreen.args, xChannel: 'green', yChannel: 'red'};
123+
124+
export let XBlueYGreenisDisabled = Template.bind({});
125+
XBlueYGreenisDisabled.storyName = 'RGB xChannel="blue", yChannel="green", isDisabled';
126+
XBlueYGreenisDisabled.args = {...XBlueYGreen.args, isDisabled: true};
127+
128+
export let XBlueYGreenSize3000 = Template.bind({});
129+
XBlueYGreenSize3000.storyName = 'RGB xChannel="blue", yChannel="green", size="size-3000"';
130+
XBlueYGreenSize3000.args = {...XBlueYGreen.args, size: 'size-3000'};
131+
132+
export let XBlueYGreenSize600 = Template.bind({});
133+
XBlueYGreenSize600.storyName = 'RGB xChannel="blue", yChannel="green", size="size-600"';
134+
XBlueYGreenSize600.args = {...XBlueYGreen.args, size: 'size-600'};
135+
136+
export let XSaturationYLightness = Template.bind({});
137+
XSaturationYLightness.storyName = 'HSL xChannel="saturation", yChannel="lightness"';
138+
XSaturationYLightness.args = {...XBlueYGreen.args, xChannel: 'saturation', yChannel: 'lightness', defaultValue: 'hsl(0, 100%, 50%)'};
139+
140+
export let XLightnessYSaturation = Template.bind({});
141+
XLightnessYSaturation.storyName = 'HSL xChannel="lightness", yChannel="saturation"';
142+
XLightnessYSaturation.args = {...XBlueYGreen.args, xChannel: 'lightness', yChannel: 'saturation', defaultValue: 'hsl(0, 100%, 50%)'};
143+
144+
export let XHueYSaturationHSL = Template.bind({});
145+
XHueYSaturationHSL.storyName = 'HSL xChannel="hue", yChannel="saturation"';
146+
XHueYSaturationHSL.args = {...XSaturationYLightness.args, xChannel: 'hue', yChannel: 'saturation', defaultValue: 'hsl(0, 100%, 50%)'};
147+
148+
export let XSaturationYHueHSL = Template.bind({});
149+
XSaturationYHueHSL.storyName = 'HSL xChannel="saturation", yChannel="hue"';
150+
XSaturationYHueHSL.args = {...XSaturationYLightness.args, xChannel: 'saturation', yChannel: 'hue', defaultValue: 'hsl(0, 100%, 50%)'};
151+
152+
export let XHueYLightnessHSL = Template.bind({});
153+
XHueYLightnessHSL.storyName = 'HSL xChannel="hue", yChannel="lightness"';
154+
XHueYLightnessHSL.args = {...XHueYSaturationHSL.args, xChannel: 'hue', yChannel: 'lightness', defaultValue: 'hsl(0, 100%, 50%)'};
155+
156+
export let XLightnessYHueHSL = Template.bind({});
157+
XLightnessYHueHSL.storyName = 'HSL xChannel="lightness", yChannel="hue"';
158+
XLightnessYHueHSL.args = {...XHueYSaturationHSL.args, xChannel: 'lightness', yChannel: 'hue', defaultValue: 'hsl(0, 100%, 50%)'};
159+
160+
export let XSaturationYBrightness = Template.bind({});
161+
XSaturationYBrightness.storyName = 'HSB xChannel="saturation", yChannel="brightness"';
162+
XSaturationYBrightness.args = {...XHueYSaturationHSL.args, xChannel: 'saturation', yChannel: 'brightness', defaultValue: 'hsb(0, 100%, 100%)'};
163+
164+
export let XBrightnessYSaturation = Template.bind({});
165+
XBrightnessYSaturation.storyName = 'HSB xChannel="brightness", yChannel="saturation"';
166+
XBrightnessYSaturation.args = {...XHueYSaturationHSL.args, xChannel: 'brightness', yChannel: 'saturation', defaultValue: 'hsb(0, 100%, 100%)'};
167+
168+
export let XSaturationYBrightnessisDisabled = Template.bind({});
169+
XSaturationYBrightnessisDisabled.storyName = 'HSB xChannel="saturation", yChannel="brightness", isDisabled';
170+
XSaturationYBrightnessisDisabled.args = {...XSaturationYBrightness.args, isDisabled: true};
171+
172+
export let XHueYSaturationHSB = Template.bind({});
173+
XHueYSaturationHSB.storyName = 'HSB xChannel="hue", yChannel="saturation"';
174+
XHueYSaturationHSB.args = {...XSaturationYBrightness.args, xChannel: 'hue', yChannel: 'saturation', defaultValue: 'hsb(0, 100%, 100%)'};
175+
176+
export let XSaturationYHueHSB = Template.bind({});
177+
XSaturationYHueHSB.storyName = 'HSB xChannel="saturation", yChannel="hue"';
178+
XSaturationYHueHSB.args = {...XSaturationYBrightness.args, xChannel: 'saturation', yChannel: 'hue', defaultValue: 'hsb(0, 100%, 100%)'};
179+
180+
export let XHueYBrightnessHSB = Template.bind({});
181+
XHueYBrightnessHSB.storyName = 'HSB xChannel="hue", yChannel="brightness"';
182+
XHueYBrightnessHSB.args = {...XHueYSaturationHSB.args, xChannel: 'hue', yChannel: 'brightness', defaultValue: 'hsb(0, 100%, 100%)'};
183+
184+
export let XBrightnessYHueHSB = Template.bind({});
185+
XBrightnessYHueHSB.storyName = 'HSB xChannel="brightness", yChannel="hue"';
186+
XBrightnessYHueHSB.args = {...XHueYSaturationHSB.args, xChannel: 'brightness', yChannel: 'hue', defaultValue: 'hsb(0, 100%, 100%)'};

0 commit comments

Comments
 (0)