refactor: ColorPicker panel free for uncontrolled (#50785)

* refactor: change on drag will change panel color

* docs: update demo

* test: update snapshot

* chore: wrap with useEvent

* fix: order of events

* fix: sync logic
This commit is contained in:
二货爱吃白萝卜 2024-09-11 10:46:30 +08:00 committed by GitHub
parent 769331dec4
commit 82b05b209d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 824 additions and 325 deletions

View File

@ -108,7 +108,7 @@ const ColorPicker: CompoundedComponent = (props) => {
} }
}; };
const onInternalChange: ColorPickerPanelProps['onChange'] = (data, pickColor) => { const onInternalChange: ColorPickerPanelProps['onChange'] = (data, changeFromPickerDrag) => {
let color: AggregationColor = generateColor(data as AggregationColor); let color: AggregationColor = generateColor(data as AggregationColor);
// ignore alpha color // ignore alpha color
@ -125,7 +125,7 @@ const ColorPicker: CompoundedComponent = (props) => {
} }
// Only for drag-and-drop color picking // Only for drag-and-drop color picking
if (!pickColor) { if (!changeFromPickerDrag) {
onInternalChangeComplete(color); onInternalChangeComplete(color);
} }
}; };

View File

@ -32,15 +32,39 @@ exports[`renders components/color-picker/demo/base.tsx correctly 1`] = `
exports[`renders components/color-picker/demo/controlled.tsx correctly 1`] = ` exports[`renders components/color-picker/demo/controlled.tsx correctly 1`] = `
<div <div
class="ant-color-picker-trigger" class="ant-space ant-space-horizontal ant-space-align-center ant-space-gap-row-small ant-space-gap-col-small"
> >
<div <div
class="ant-color-picker-color-block" class="ant-space-item"
> >
<div <div
class="ant-color-picker-color-block-inner" class="ant-color-picker-trigger"
style="background:rgb(22,119,255)" >
/> <div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22,119,255)"
/>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-color-picker-trigger"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22,119,255)"
/>
</div>
</div>
</div> </div>
</div> </div>
`; `;

View File

@ -20,6 +20,7 @@ function doMouseMove(
start: number, start: number,
end: number, end: number,
element: string | HTMLElement = 'ant-color-picker-handler', element: string | HTMLElement = 'ant-color-picker-handler',
fireMouseUp = true,
) { ) {
const ele = const ele =
element instanceof HTMLElement ? element : container.getElementsByClassName(element)[0]; element instanceof HTMLElement ? element : container.getElementsByClassName(element)[0];
@ -44,8 +45,10 @@ function doMouseMove(
fireEvent(document, mouseMove); fireEvent(document, mouseMove);
} }
const mouseUp = createEvent.mouseUp(document); if (fireMouseUp) {
fireEvent(document, mouseUp); const mouseUp = createEvent.mouseUp(document);
fireEvent(document, mouseUp);
}
} }
describe('ColorPicker', () => { describe('ColorPicker', () => {
@ -879,4 +882,58 @@ describe('ColorPicker', () => {
spyRect.mockRestore(); spyRect.mockRestore();
}); });
describe('controlled with `onChangeComplete`', () => {
let spyRect: ReturnType<typeof spyElementPrototypes>;
beforeEach(() => {
spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
x: 0,
y: 100,
width: 100,
height: 100,
}),
});
});
afterEach(() => {
spyRect.mockRestore();
});
it('lock value', async () => {
const onChange = jest.fn();
const onChangeComplete = jest.fn();
const { container } = render(
<ColorPicker value="#F00" open onChange={onChange} onChangeComplete={onChangeComplete} />,
);
doMouseMove(container, 0, 50, 'ant-color-picker-slider-handle', false);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
// Safe to change with any value but (255/0/0)
'rgb(0,255,255)',
);
expect(onChangeComplete).not.toHaveBeenCalled();
// Inline Color Block (locked)
expect(container.querySelectorAll('.ant-color-picker-color-block-inner')[0]).toHaveStyle({
background: 'rgb(255, 0, 0)',
});
// Popup Color Block (follow operation)
expect(container.querySelectorAll('.ant-color-picker-color-block-inner')[1]).toHaveStyle({
background: 'rgb(0, 255, 255)',
});
// Mouse up
fireEvent.mouseUp(document);
// Lock color back
expect(container.querySelectorAll('.ant-color-picker-color-block-inner')[1]).toHaveStyle({
background: 'rgb(255, 0, 0)',
});
});
});
}); });

View File

@ -85,6 +85,16 @@ const PanelPicker: FC = () => {
return colors[activeIndex]?.color; return colors[activeIndex]?.color;
}, [value, activeIndex, isSingle, lockedColor, gradientDragging]); }, [value, activeIndex, isSingle, lockedColor, gradientDragging]);
// ========================= Picker Color =========================
const [pickerColor, setPickerColor] = React.useState<AggregationColor | null>(activeColor);
const [forceSync, setForceSync] = React.useState(0);
const mergedPickerColor = pickerColor?.equals(activeColor) ? activeColor : pickerColor;
useLayoutEffect(() => {
setPickerColor(activeColor);
}, [forceSync, activeColor?.toHexString()]);
// ============================ Change ============================ // ============================ Change ============================
const fillColor = (nextColor: AggregationColor | Color, info?: Info) => { const fillColor = (nextColor: AggregationColor | Color, info?: Info) => {
let submitColor = generateColor(nextColor); let submitColor = generateColor(nextColor);
@ -121,16 +131,28 @@ const PanelPicker: FC = () => {
return new AggregationColor(nextColors); return new AggregationColor(nextColors);
}; };
const onInternalChange = ( const onPickerChange = (
colorValue: AggregationColor | Color, colorValue: AggregationColor | Color,
fromPicker?: boolean, fromPicker: boolean,
info?: Info, info?: Info,
) => { ) => {
onChange(fillColor(colorValue, info), fromPicker); const nextColor = fillColor(colorValue, info);
setPickerColor(nextColor);
onChange(nextColor, fromPicker);
}; };
const onInternalChangeComplete = (nextColor: Color, info?: Info) => { const onInternalChangeComplete = (nextColor: Color, info?: Info) => {
// Trigger complete event
onChangeComplete(fillColor(nextColor, info)); onChangeComplete(fillColor(nextColor, info));
// Back of origin color in case in controlled
// This will set after `onChangeComplete` to avoid `setState` trigger rerender
// which will make `fillColor` get wrong `color.cleared` state
setForceSync((ori) => ori + 1);
};
const onInputChange = (colorValue: AggregationColor) => {
onChange(fillColor(colorValue));
}; };
// ============================ Render ============================ // ============================ Render ============================
@ -166,10 +188,10 @@ const PanelPicker: FC = () => {
<RcColorPicker <RcColorPicker
prefixCls={prefixCls} prefixCls={prefixCls}
value={activeColor?.toHsb()} value={mergedPickerColor?.toHsb()}
disabledAlpha={disabledAlpha} disabledAlpha={disabledAlpha}
onChange={(colorValue, info) => { onChange={(colorValue, info) => {
onInternalChange(colorValue, true, info); onPickerChange(colorValue, true, info);
}} }}
onChangeComplete={(colorValue, info) => { onChangeComplete={(colorValue, info) => {
onInternalChangeComplete(colorValue, info); onInternalChangeComplete(colorValue, info);
@ -178,7 +200,7 @@ const PanelPicker: FC = () => {
/> />
<ColorInput <ColorInput
value={activeColor} value={activeColor}
onChange={onInternalChange} onChange={onInputChange}
prefixCls={prefixCls} prefixCls={prefixCls}
disabledAlpha={disabledAlpha} disabledAlpha={disabledAlpha}
{...injectProps} {...injectProps}

View File

@ -1,7 +1,7 @@
## zh-CN ## zh-CN
通过 `value``onChange` 设置组件为受控模式。 通过 `value``onChange` 设置组件为受控模式,如果通过 `onChangeComplete` 受控则会锁定展示颜色
## en-US ## en-US
Set the component to controlled mode. Set the component to controlled mode. Will lock the display color if controlled by `onChangeComplete`.

View File

@ -1,12 +1,18 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ColorPicker } from 'antd'; import { ColorPicker, Space } from 'antd';
import type { ColorPickerProps, GetProp } from 'antd'; import type { ColorPickerProps, GetProp } from 'antd';
type Color = GetProp<ColorPickerProps, 'value'>; type Color = GetProp<ColorPickerProps, 'value'>;
const Demo: React.FC = () => { const Demo: React.FC = () => {
const [color, setColor] = useState<Color>('#1677ff'); const [color, setColor] = useState<Color>('#1677ff');
return <ColorPicker value={color} onChange={setColor} />;
return (
<Space>
<ColorPicker value={color} onChange={setColor} />
<ColorPicker value={color} onChangeComplete={setColor} />
</Space>
);
}; };
export default Demo; export default Demo;

View File

@ -61,7 +61,7 @@ Common props ref[Common props](/docs/react/common-props)
| trigger | ColorPicker trigger mode | `hover` \| `click` | `click` | | | trigger | ColorPicker trigger mode | `hover` \| `click` | `click` | |
| value | Value of color | string \| `Color` | - | | | value | Value of color | string \| `Color` | - | |
| onChange | Callback when `value` is changed | `(value: Color, css: string) => void` | - | | | onChange | Callback when `value` is changed | `(value: Color, css: string) => void` | - | |
| onChangeComplete | Called when color pick ends | `(value: Color) => void` | - | 5.7.0 | | onChangeComplete | Called when color pick ends. Will not change the display color when `value` controlled by `onChangeComplete` | `(value: Color) => void` | - | 5.7.0 |
| onFormatChange | Callback when `format` is changed | `(format: 'hex' \| 'rgb' \| 'hsb') => void` | - | | | onFormatChange | Callback when `format` is changed | `(format: 'hex' \| 'rgb' \| 'hsb') => void` | - | |
| onOpenChange | Callback when `open` is changed | `(open: boolean) => void` | - | | | onOpenChange | Callback when `open` is changed | `(open: boolean) => void` | - | |
| onClear | Called when clear | `() => void` | - | 5.6.0 | | onClear | Called when clear | `() => void` | - | 5.6.0 |

View File

@ -62,7 +62,7 @@ group:
| trigger | 颜色选择器的触发模式 | `hover` \| `click` | `click` | | | trigger | 颜色选择器的触发模式 | `hover` \| `click` | `click` | |
| value | 颜色的值 | string \| `Color` | - | | | value | 颜色的值 | string \| `Color` | - | |
| onChange | 颜色变化的回调 | `(value: Color, css: string) => void` | - | | | onChange | 颜色变化的回调 | `(value: Color, css: string) => void` | - | |
| onChangeComplete | 颜色选择完成的回调 | `(value: Color) => void` | - | 5.7.0 | | onChangeComplete | 颜色选择完成的回调,通过 `onChangeComplete``value` 受控时拖拽不会改变展示颜色 | `(value: Color) => void` | - | 5.7.0 |
| onFormatChange | 颜色格式变化的回调 | `(format: 'hex' \| 'rgb' \| 'hsb') => void` | - | | | onFormatChange | 颜色格式变化的回调 | `(format: 'hex' \| 'rgb' \| 'hsb') => void` | - | |
| onOpenChange | 当 `open` 被改变时的回调 | `(open: boolean) => void` | - | | | onOpenChange | 当 `open` 被改变时的回调 | `(open: boolean) => void` | - | |
| onClear | 清除的回调 | `() => void` | - | 5.6.0 | | onClear | 清除的回调 | `() => void` | - | 5.6.0 |