Skip to content

Commit 1cb644d

Browse files
MadCccyoyo837afc163li-jia-nan
authored
fix: color picker controlled value (ant-design#47816)
* fix: fix control value not show in dom * chore: update title * add test case * change test title * fix: cleared color should change after controlled value changed * chore: code clean * test: change test * comment * fix: should respect empty string * test: add test case * chore: update demo --------- Co-authored-by: tanghui <[email protected]> Co-authored-by: afc163 <[email protected]> Co-authored-by: lijianan <[email protected]>
1 parent 6310bf2 commit 1cb644d

File tree

11 files changed

+200
-92
lines changed

11 files changed

+200
-92
lines changed

components/_util/getAllowClear.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const getAllowClear = (allowClear: AllowClear): AllowClear => {
1313
clearIcon: <CloseCircleFilled />,
1414
};
1515
}
16-
16+
1717
return mergedAllowClear;
1818
};
1919

components/color-picker/ColorPicker.tsx

Lines changed: 9 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import type { CSSProperties, FC } from 'react';
2-
import React, { useContext, useMemo, useRef, useState } from 'react';
3-
import type {
4-
HsbaColorType,
5-
ColorPickerProps as RcColorPickerProps,
6-
} from '@rc-component/color-picker';
1+
import React, { useContext, useMemo, useRef } from 'react';
2+
import type { HsbaColorType } from '@rc-component/color-picker';
73
import classNames from 'classnames';
84
import useMergedState from 'rc-util/lib/hooks/useMergedState';
95

@@ -15,58 +11,17 @@ import { ConfigContext } from '../config-provider/context';
1511
import DisabledContext from '../config-provider/DisabledContext';
1612
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
1713
import useSize from '../config-provider/hooks/useSize';
18-
import type { SizeType } from '../config-provider/SizeContext';
1914
import { FormItemInputContext, NoFormStyle } from '../form/context';
2015
import type { PopoverProps } from '../popover';
2116
import Popover from '../popover';
2217
import type { Color } from './color';
2318
import ColorPickerPanel from './ColorPickerPanel';
2419
import ColorTrigger from './components/ColorTrigger';
2520
import useColorState from './hooks/useColorState';
26-
import type {
27-
ColorFormat,
28-
ColorPickerBaseProps,
29-
ColorValueType,
30-
PresetsItem,
31-
TriggerPlacement,
32-
TriggerType,
33-
} from './interface';
21+
import type { ColorPickerBaseProps, ColorPickerProps, TriggerPlacement } from './interface';
3422
import useStyle from './style';
3523
import { genAlphaColor, generateColor, getAlphaColor } from './util';
3624

37-
export type ColorPickerProps = Omit<
38-
RcColorPickerProps,
39-
'onChange' | 'value' | 'defaultValue' | 'panelRender' | 'disabledAlpha' | 'onChangeComplete'
40-
> & {
41-
value?: ColorValueType;
42-
defaultValue?: ColorValueType;
43-
children?: React.ReactNode;
44-
open?: boolean;
45-
disabled?: boolean;
46-
placement?: TriggerPlacement;
47-
trigger?: TriggerType;
48-
format?: keyof typeof ColorFormat;
49-
defaultFormat?: keyof typeof ColorFormat;
50-
allowClear?: boolean;
51-
presets?: PresetsItem[];
52-
arrow?: boolean | { pointAtCenter: boolean };
53-
panelRender?: (
54-
panel: React.ReactNode,
55-
extra: { components: { Picker: FC; Presets: FC } },
56-
) => React.ReactNode;
57-
showText?: boolean | ((color: Color) => React.ReactNode);
58-
size?: SizeType;
59-
styles?: { popup?: CSSProperties; popupOverlayInner?: CSSProperties };
60-
rootClassName?: string;
61-
disabledAlpha?: boolean;
62-
[key: `data-${string}`]: string;
63-
onOpenChange?: (open: boolean) => void;
64-
onFormatChange?: (format: ColorFormat) => void;
65-
onChange?: (value: Color, hex: string) => void;
66-
onClear?: () => void;
67-
onChangeComplete?: (value: Color) => void;
68-
} & Pick<PopoverProps, 'getPopupContainer' | 'autoAdjustOverflow' | 'destroyTooltipOnHide'>;
69-
7025
type CompoundedComponent = React.FC<ColorPickerProps> & {
7126
_InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
7227
};
@@ -109,7 +64,7 @@ const ColorPicker: CompoundedComponent = (props) => {
10964
const contextDisabled = useContext(DisabledContext);
11065
const mergedDisabled = disabled ?? contextDisabled;
11166

112-
const [colorValue, setColorValue] = useColorState('', {
67+
const [colorValue, setColorValue, prevValue] = useColorState('', {
11368
value,
11469
defaultValue,
11570
});
@@ -124,8 +79,6 @@ const ColorPicker: CompoundedComponent = (props) => {
12479
onChange: onFormatChange,
12580
});
12681

127-
const [colorCleared, setColorCleared] = useState(!value && !defaultValue);
128-
12982
const prefixCls = getPrefixCls('color-picker', customizePrefixCls);
13083

13184
const isAlphaColor = useMemo(() => getAlphaColor(colorValue) < 100, [colorValue]);
@@ -167,14 +120,16 @@ const ColorPicker: CompoundedComponent = (props) => {
167120

168121
const handleChange = (data: Color, type?: HsbaColorType, pickColor?: boolean) => {
169122
let color: Color = generateColor(data);
123+
124+
// If color is cleared, reset alpha to 100
170125
const isNull = value === null || (!value && defaultValue === null);
171-
if (colorCleared || isNull) {
172-
setColorCleared(false);
126+
if (prevValue.current?.cleared || isNull) {
173127
// ignore alpha slider
174128
if (getAlphaColor(colorValue) === 0 && type !== 'alpha') {
175129
color = genAlphaColor(color);
176130
}
177131
}
132+
178133
// ignore alpha color
179134
if (disabledAlpha && isAlphaColor) {
180135
color = genAlphaColor(color);
@@ -192,7 +147,6 @@ const ColorPicker: CompoundedComponent = (props) => {
192147
};
193148

194149
const handleClear = () => {
195-
setColorCleared(true);
196150
onClear?.();
197151
};
198152

@@ -221,7 +175,6 @@ const ColorPicker: CompoundedComponent = (props) => {
221175
prefixCls,
222176
color: colorValue,
223177
allowClear,
224-
colorCleared,
225178
disabled: mergedDisabled,
226179
disabledAlpha,
227180
presets,
@@ -262,13 +215,12 @@ const ColorPicker: CompoundedComponent = (props) => {
262215
open={popupOpen}
263216
className={mergedCls}
264217
style={mergedStyle}
265-
color={value ? generateColor(value) : colorValue}
266218
prefixCls={prefixCls}
267219
disabled={mergedDisabled}
268-
colorCleared={colorCleared}
269220
showText={showText}
270221
format={formatValue}
271222
{...rest}
223+
color={colorValue}
272224
/>
273225
)}
274226
</Popover>,

components/color-picker/__tests__/index.test.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useState } from 'react';
1+
import React, { useEffect, useMemo, useState } from 'react';
22
import { createEvent, fireEvent, render } from '@testing-library/react';
33
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
44

@@ -11,8 +11,9 @@ import ConfigProvider from '../../config-provider';
1111
import Form from '../../form';
1212
import theme from '../../theme';
1313
import type { Color } from '../color';
14-
import type { ColorPickerProps } from '../ColorPicker';
1514
import ColorPicker from '../ColorPicker';
15+
import type { ColorPickerProps, ColorValueType } from '../interface';
16+
import { generateColor } from '../util';
1617

1718
function doMouseMove(
1819
container: HTMLElement,
@@ -607,4 +608,94 @@ describe('ColorPicker', () => {
607608
const { container } = render(<ColorPicker />);
608609
expect(container.querySelector('.ant-color-picker-clear')).toBeTruthy();
609610
});
611+
612+
['', null].forEach((value) => {
613+
it(`When controlled and without an initial value, then changing the controlled value to valid color should be reflected correctly on the DOM. [${String(
614+
value,
615+
)}]`, async () => {
616+
const Demo = () => {
617+
const [color, setColor] = useState<ColorValueType>(value);
618+
useEffect(() => {
619+
setColor(generateColor('red'));
620+
}, []);
621+
return <ColorPicker value={color} />;
622+
};
623+
const { container } = render(<Demo />);
624+
await waitFakeTimer();
625+
expect(container.querySelector('.ant-color-picker-color-block-inner')).toHaveStyle({
626+
background: 'rgb(255, 0, 0)',
627+
});
628+
});
629+
630+
it(`When controlled and has an initial value, then changing the controlled value to cleared color should be reflected correctly on the DOM. [${String(
631+
value,
632+
)}]`, async () => {
633+
const Demo = () => {
634+
const [color, setColor] = useState<ColorValueType>(generateColor('red'));
635+
useEffect(() => {
636+
setColor(value);
637+
}, []);
638+
return <ColorPicker value={color} />;
639+
};
640+
const { container } = render(<Demo />);
641+
await waitFakeTimer();
642+
expect(container.querySelector('.ant-color-picker-clear')).toBeTruthy();
643+
});
644+
});
645+
646+
it('Controlled string value should work with allowClear correctly', async () => {
647+
const Demo = (props: any) => {
648+
const [color, setColor] = useState<ColorValueType>(generateColor('red'));
649+
650+
useEffect(() => {
651+
if (typeof props.value !== 'undefined') {
652+
setColor(props.value);
653+
}
654+
}, [props.value]);
655+
656+
return (
657+
<ColorPicker value={color} onChange={(e) => setColor(e.toHexString())} open allowClear />
658+
);
659+
};
660+
const { container, rerender } = render(<Demo />);
661+
await waitFakeTimer();
662+
expect(
663+
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
664+
).toBeFalsy();
665+
fireEvent.click(container.querySelector('.ant-color-picker-clear')!);
666+
expect(
667+
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
668+
).toBeTruthy();
669+
rerender(<Demo value="#1677ff" />);
670+
expect(
671+
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
672+
).toBeFalsy();
673+
});
674+
675+
it('Controlled value should work with allowClear correctly', async () => {
676+
const Demo = (props: any) => {
677+
const [color, setColor] = useState<ColorValueType>(generateColor('red'));
678+
679+
useEffect(() => {
680+
if (typeof props.value !== 'undefined') {
681+
setColor(props.value);
682+
}
683+
}, [props.value]);
684+
685+
return <ColorPicker value={color} onChange={(e) => setColor(e)} open allowClear />;
686+
};
687+
const { container, rerender } = render(<Demo />);
688+
await waitFakeTimer();
689+
expect(
690+
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
691+
).toBeFalsy();
692+
fireEvent.click(container.querySelector('.ant-color-picker-clear')!);
693+
expect(
694+
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
695+
).toBeTruthy();
696+
rerender(<Demo value="#1677ff" />);
697+
expect(
698+
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
699+
).toBeFalsy();
700+
});
610701
});

components/color-picker/color.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,21 @@ export interface Color
1111
extends Pick<
1212
RcColor,
1313
'toHsb' | 'toHsbString' | 'toHex' | 'toHexString' | 'toRgb' | 'toRgbString'
14-
> {}
14+
> {
15+
cleared: boolean | 'controlled';
16+
}
1517

16-
export class ColorFactory {
18+
export class ColorFactory implements Color {
1719
/** Original Color object */
1820
private metaColor: RcColor;
1921

22+
public cleared: boolean = false;
23+
2024
constructor(color: ColorGenInput<Color>) {
2125
this.metaColor = new RcColor(color as ColorGenInput);
2226
if (!color) {
2327
this.metaColor.setAlpha(0);
28+
this.cleared = true;
2429
}
2530
}
2631

components/color-picker/components/ColorClear.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import type { Color } from '../color';
44
import type { ColorPickerBaseProps } from '../interface';
55
import { generateColor } from '../util';
66

7-
interface ColorClearProps extends Pick<ColorPickerBaseProps, 'prefixCls' | 'colorCleared'> {
7+
interface ColorClearProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
88
value?: Color;
99
onChange?: (value: Color) => void;
1010
}
1111

12-
const ColorClear: FC<ColorClearProps> = ({ prefixCls, value, colorCleared, onChange }) => {
12+
const ColorClear: FC<ColorClearProps> = ({ prefixCls, value, onChange }) => {
1313
const handleClick = () => {
14-
if (value && !colorCleared) {
14+
if (value && !value.cleared) {
1515
const hsba = value.toHsb();
1616
hsba.a = 0;
1717
const genColor = generateColor(hsba);
18+
genColor.cleared = true;
1819
onChange?.(genColor);
1920
}
2021
};

components/color-picker/components/ColorTrigger.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import { ColorBlock } from '@rc-component/color-picker';
22
import classNames from 'classnames';
33
import type { CSSProperties, MouseEventHandler } from 'react';
44
import React, { forwardRef, useMemo } from 'react';
5-
import type { ColorPickerProps } from '../ColorPicker';
6-
import type { ColorPickerBaseProps } from '../interface';
5+
import type { ColorPickerProps, ColorPickerBaseProps } from '../interface';
76
import { getAlphaColor } from '../util';
87
import ColorClear from './ColorClear';
98

10-
interface colorTriggerProps
11-
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'colorCleared' | 'disabled' | 'format'> {
12-
color: Exclude<ColorPickerBaseProps['color'], undefined>;
9+
export interface ColorTriggerProps
10+
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'disabled' | 'format'> {
11+
color: NonNullable<ColorPickerBaseProps['color']>;
1312
open?: boolean;
1413
showText?: ColorPickerProps['showText'];
1514
className?: string;
@@ -19,19 +18,18 @@ interface colorTriggerProps
1918
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
2019
}
2120

22-
const ColorTrigger = forwardRef<HTMLDivElement, colorTriggerProps>((props, ref) => {
23-
const { color, prefixCls, open, colorCleared, disabled, format, className, showText, ...rest } =
24-
props;
21+
const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref) => {
22+
const { color, prefixCls, open, disabled, format, className, showText, ...rest } = props;
2523
const colorTriggerPrefixCls = `${prefixCls}-trigger`;
2624

2725
const containerNode = useMemo<React.ReactNode>(
2826
() =>
29-
colorCleared ? (
27+
color.cleared ? (
3028
<ColorClear prefixCls={prefixCls} />
3129
) : (
3230
<ColorBlock prefixCls={prefixCls} color={color.toRgbString()} />
3331
),
34-
[color, colorCleared, prefixCls],
32+
[color, prefixCls],
3533
);
3634

3735
const genColorString = () => {

components/color-picker/components/PanelPicker.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { PanelPickerContext } from '../context';
77
import type { ColorPickerBaseProps } from '../interface';
88
import ColorClear from './ColorClear';
99
import ColorInput from './ColorInput';
10+
import { generateColor } from '../util';
1011

1112
export interface PanelPickerProps
1213
extends Pick<
1314
ColorPickerBaseProps,
14-
'prefixCls' | 'colorCleared' | 'allowClear' | 'disabledAlpha' | 'onChangeComplete'
15+
'prefixCls' | 'allowClear' | 'disabledAlpha' | 'onChangeComplete'
1516
> {
1617
value?: Color;
1718
onChange?: (value?: Color, type?: HsbaColorType, pickColor?: boolean) => void;
@@ -21,7 +22,6 @@ export interface PanelPickerProps
2122
const PanelPicker: FC = () => {
2223
const {
2324
prefixCls,
24-
colorCleared,
2525
allowClear,
2626
value,
2727
disabledAlpha,
@@ -36,7 +36,6 @@ const PanelPicker: FC = () => {
3636
<ColorClear
3737
prefixCls={prefixCls}
3838
value={value}
39-
colorCleared={colorCleared}
4039
onChange={(clearColor) => {
4140
onChange?.(clearColor);
4241
onClear?.();
@@ -48,8 +47,12 @@ const PanelPicker: FC = () => {
4847
prefixCls={prefixCls}
4948
value={value?.toHsb()}
5049
disabledAlpha={disabledAlpha}
51-
onChange={(colorValue, type) => onChange?.(colorValue, type, true)}
52-
onChangeComplete={onChangeComplete}
50+
onChange={(colorValue, type) => {
51+
onChange?.(generateColor(colorValue), type, true);
52+
}}
53+
onChangeComplete={(colorValue) => {
54+
onChangeComplete?.(generateColor(colorValue));
55+
}}
5356
/>
5457
<ColorInput
5558
value={value}

0 commit comments

Comments
 (0)