Skip to content

Commit 02515b8

Browse files
authored
Support null values in ColorField with channel prop (#6722)
1 parent 5079a24 commit 02515b8

File tree

3 files changed

+54
-16
lines changed

3 files changed

+54
-16
lines changed

packages/@react-spectrum/color/docs/ColorField.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ function Example() {
9292
<ColorField label="Saturation" value={color} onChange={setColor} colorSpace="hsl" channel="saturation" />
9393
<ColorField label="Lightness" value={color} onChange={setColor} colorSpace="hsl" channel="lightness" />
9494
</div>
95-
<p>Current color value: {color.toString('hex')}</p>
95+
<p>Current color value: {color?.toString('hex')}</p>
9696
</>
9797
);
9898
}

packages/@react-spectrum/color/test/ColorField.test.js

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -374,17 +374,45 @@ describe('ColorField', function () {
374374
expect(onChangeSpy).toHaveBeenCalledTimes(1);
375375
});
376376

377-
it('should support the channel prop', async function () {
378-
let onChange = jest.fn();
379-
let {getByRole} = renderComponent({label: null, value: '#abc', colorSpace: 'hsl', channel: 'hue', onChange});
380-
let colorField = getByRole('textbox');
381-
expect(colorField.value).toBe('210°');
382-
expect(colorField).toHaveAttribute('aria-label', 'Hue');
377+
describe('channel', function () {
378+
it('should support the channel prop', async function () {
379+
let onChange = jest.fn();
380+
let {getByRole} = renderComponent({label: null, value: '#abc', colorSpace: 'hsl', channel: 'hue', onChange});
381+
let colorField = getByRole('textbox');
382+
expect(colorField.value).toBe('210°');
383+
expect(colorField).toHaveAttribute('aria-label', 'Hue');
384+
385+
await user.tab();
386+
await user.keyboard('100');
387+
await user.tab();
388+
expect(onChange).toHaveBeenCalledWith(parseColor('hsl(100, 25%, 73.33%)'));
389+
});
383390

384-
await user.tab();
385-
await user.keyboard('100');
386-
await user.tab();
387-
expect(onChange).toHaveBeenCalledWith(parseColor('hsl(100, 25%, 73.33%)'));
391+
it('should default to empty', function () {
392+
let {getByRole} = renderComponent({label: null, colorSpace: 'hsl', channel: 'hue'});
393+
let colorField = getByRole('textbox');
394+
expect(colorField).toHaveValue('');
395+
});
396+
397+
it('should support null value', function () {
398+
let {getByRole} = renderComponent({label: null, value: null, colorSpace: 'hsl', channel: 'hue'});
399+
let colorField = getByRole('textbox');
400+
expect(colorField).toHaveValue('');
401+
});
402+
403+
it('should support clearing value', async function () {
404+
let onChange = jest.fn();
405+
let {getByRole} = renderComponent({label: null, defaultValue: '#abc', colorSpace: 'hsl', channel: 'hue', onChange});
406+
let colorField = getByRole('textbox');
407+
expect(colorField).toHaveValue('210°');
408+
409+
await user.tab();
410+
await user.keyboard('{Backspace}');
411+
await user.tab();
412+
413+
expect(colorField).toHaveValue('');
414+
expect(onChange).toHaveBeenCalledWith(null);
415+
});
388416
});
389417

390418
describe('validation', () => {

packages/@react-stately/color/src/useColorChannelFieldState.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,29 @@ export interface ColorChannelFieldState extends NumberFieldState {
2323
*/
2424
export function useColorChannelFieldState(props: ColorChannelFieldStateOptions): ColorChannelFieldState {
2525
let {channel, colorSpace, locale} = props;
26+
let black = useColor('#000')!;
2627
let initialValue = useColor(props.value);
27-
let initialDefaultValue = useColor(props.defaultValue || '#0000')!;
28-
let [colorValue, setColor] = useControlledState(initialValue || undefined, initialDefaultValue, props.onChange);
29-
let color = useMemo(() => colorSpace && colorValue ? colorValue.toFormat(colorSpace) : colorValue, [colorValue, colorSpace]);
28+
let initialDefaultValue = useColor(props.defaultValue);
29+
let [colorValue, setColor] = useControlledState(initialValue, initialDefaultValue ?? null, props.onChange);
30+
let color = useMemo(() => {
31+
let nonNullColorValue = colorValue || black;
32+
return colorSpace && nonNullColorValue ? nonNullColorValue.toFormat(colorSpace) : nonNullColorValue;
33+
}, [black, colorValue, colorSpace]);
3034
let value = color.getChannelValue(channel);
3135
let range = color.getChannelRange(channel);
3236
let formatOptions = useMemo(() => color.getChannelFormatOptions(channel), [color, channel]);
3337
let multiplier = formatOptions.style === 'percent' && range.maxValue === 100 ? 100 : 1;
3438

3539
let numberFieldState = useNumberFieldState({
3640
locale,
37-
value: value / multiplier,
38-
onChange: (v) => setColor(color.withChannelValue(channel, v * multiplier)),
41+
value: colorValue === null ? NaN : value / multiplier,
42+
onChange: (v) => {
43+
if (!Number.isNaN(v)) {
44+
setColor(color.withChannelValue(channel, v * multiplier));
45+
} else {
46+
setColor(null);
47+
}
48+
},
3949
minValue: range.minValue / multiplier,
4050
maxValue: range.maxValue / multiplier,
4151
step: range.step / multiplier,

0 commit comments

Comments
 (0)