Browse Source

feat: new component ColorPicker (#41990)

* feat: color picker

* chore: improve component logic

* test: update snapshots

* test: update snapshots

* fix: remove unuse code

* fix: adjusting the UI

* fix: hairy edge in dark mode

* fix: demo case

* chore: rollback file

* fix: improve code logic

* perf: improve code

* test: add test case

* refactor: trigger use Popover instead

* test: add test case

* refactor: improve code logic

* test: update snapshots

* refactor: improve code

* docs: add overview img

* feat: style

* chore: update snapshot

* chore: code clean

* chore: code clean

* chore: fix lint

* chore: update snapshot

* feat: color block

* chore: pure render

* refactor: hex input number reduced to 6 digits

* fix: rename React to react

* refactor:  trigger demo

* docs: remove disabled demo

---------

Co-authored-by: MadCcc <1075746765@qq.com>
pull/42315/head
红果汁 2 years ago
committed by GitHub
parent
commit
b95d5674fa
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      components/__tests__/__snapshots__/index.test.ts.snap
  2. 168
      components/color-picker/ColorPicker.tsx
  3. 63
      components/color-picker/ColorPickerPanel.tsx
  4. 4353
      components/color-picker/__tests__/__snapshots__/demo-extend.test.ts.snap
  5. 248
      components/color-picker/__tests__/__snapshots__/demo.test.ts.snap
  6. 41
      components/color-picker/__tests__/__snapshots__/index.test.tsx.snap
  7. 96
      components/color-picker/__tests__/components.test.tsx
  8. 3
      components/color-picker/__tests__/demo-extend.test.ts
  9. 3
      components/color-picker/__tests__/demo.test.ts
  10. 5
      components/color-picker/__tests__/image.test.ts
  11. 249
      components/color-picker/__tests__/index.test.tsx
  12. 45
      components/color-picker/color.ts
  13. 45
      components/color-picker/components/ColorAlphaInput.tsx
  14. 24
      components/color-picker/components/ColorClear.tsx
  15. 47
      components/color-picker/components/ColorHexInput.tsx
  16. 69
      components/color-picker/components/ColorHsbInput.tsx
  17. 75
      components/color-picker/components/ColorInput.tsx
  18. 84
      components/color-picker/components/ColorPresets.tsx
  19. 65
      components/color-picker/components/ColorRgbInput.tsx
  20. 55
      components/color-picker/components/ColorSteppers.tsx
  21. 46
      components/color-picker/components/ColorTrigger.tsx
  22. 7
      components/color-picker/demo/allowClear.md
  23. 4
      components/color-picker/demo/allowClear.tsx
  24. 7
      components/color-picker/demo/base.md
  25. 10
      components/color-picker/demo/base.tsx
  26. 7
      components/color-picker/demo/disabled.md
  27. 4
      components/color-picker/demo/disabled.tsx
  28. 7
      components/color-picker/demo/format.md
  29. 57
      components/color-picker/demo/format.tsx
  30. 7
      components/color-picker/demo/presets.md
  31. 48
      components/color-picker/demo/presets.tsx
  32. 7
      components/color-picker/demo/pure-panel.md
  33. 12
      components/color-picker/demo/pure-panel.tsx
  34. 7
      components/color-picker/demo/trigger.md
  35. 28
      components/color-picker/demo/trigger.tsx
  36. 39
      components/color-picker/hooks/useColorState.ts
  37. 67
      components/color-picker/index.en-US.md
  38. 6
      components/color-picker/index.tsx
  39. 68
      components/color-picker/index.zh-CN.md
  40. 22
      components/color-picker/interface.ts
  41. 27
      components/color-picker/style/color-block.ts
  42. 153
      components/color-picker/style/index.ts
  43. 99
      components/color-picker/style/input.ts
  44. 80
      components/color-picker/style/picker.ts
  45. 113
      components/color-picker/style/presets.ts
  46. 20
      components/color-picker/util.ts
  47. 2
      components/index.ts
  48. 5
      components/locale/en_US.ts
  49. 5
      components/locale/index.tsx
  50. 5
      components/locale/zh_CN.ts
  51. 16
      components/theme/interface/components.ts
  52. 1
      package.json
  53. 1
      tests/__snapshots__/index.test.ts.snap

1
components/__tests__/__snapshots__/index.test.ts.snap

@ -19,6 +19,7 @@ exports[`antd exports modules correctly 1`] = `
"Checkbox",
"Col",
"Collapse",
"ColorPicker",
"ConfigProvider",
"DatePicker",
"Descriptions",

168
components/color-picker/ColorPicker.tsx

@ -0,0 +1,168 @@
import type {
ColorPickerPanelProps as RcColorPickerPanelProps,
TriggerPlacement,
TriggerType,
} from '@rc-component/color-picker';
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import type { CSSProperties } from 'react';
import React, { useContext, useState } from 'react';
import type { ConfigConsumerProps } from '../config-provider/context';
import { ConfigContext } from '../config-provider/context';
import type { PopoverProps } from '../popover';
import Popover from '../popover';
import theme from '../theme';
import ColorPickerPanel from './ColorPickerPanel';
import type { Color } from './color';
import ColorTrigger from './components/ColorTrigger';
import useColorState from './hooks/useColorState';
import type { ColorFormat, ColorPickerBaseProps, PresetsItem } from './interface';
import useStyle from './style/index';
import { customizePrefixCls, generateColor } from './util';
import genPurePanel from '../_util/PurePanel';
export interface ColorPickerProps
extends Omit<
RcColorPickerPanelProps,
'onChange' | 'arrow' | 'value' | 'defaultValue' | 'children' | 'panelRender'
> {
value?: Color | string;
defaultValue?: Color | string;
children?: React.ReactElement;
open?: boolean;
disabled?: boolean;
placement?: TriggerPlacement;
trigger?: TriggerType;
format?: keyof typeof ColorFormat;
allowClear?: boolean;
presets?: PresetsItem[];
arrow?: boolean | { pointAtCenter: boolean };
prefixCls?: string;
className?: string;
style?: CSSProperties;
styles?: { popup?: CSSProperties };
rootClassName?: string;
onOpenChange?: (open: boolean) => void;
onFormatChange?: (format: ColorFormat) => void;
onChange?: (value: Color, hex: string) => void;
getPopupContainer?: PopoverProps['getPopupContainer'];
}
type CompoundedComponent = React.FC<ColorPickerProps> & {
_InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
};
const ColorPicker: CompoundedComponent = (props) => {
const {
value,
defaultValue,
format,
allowClear = false,
presets,
children,
trigger = 'click',
open,
disabled,
placement = 'bottomLeft',
arrow = true,
style,
className,
rootClassName,
styles,
onFormatChange,
onChange,
onOpenChange,
getPopupContainer,
} = props;
const { getPrefixCls, direction } = useContext<ConfigConsumerProps>(ConfigContext);
const { token } = theme.useToken();
const [colorValue, setColorValue] = useColorState(token.colorPrimary, {
value,
defaultValue,
});
const [popupOpen, setPopupOpen] = useMergedState(false, {
value: open,
postState: (openData) => !disabled && openData,
onChange: onOpenChange,
});
const [clearColor, setClearColor] = useState(false);
const prefixCls = getPrefixCls('color-picker', customizePrefixCls);
const [wrapSSR, hashId] = useStyle(prefixCls);
const mergeRootCls = classNames(rootClassName, {
[`${prefixCls}-rtl`]: direction,
});
const mergeCls = classNames(mergeRootCls, className, hashId);
const handleChange = (data: Color) => {
const color: Color = generateColor(data);
if (clearColor && color.toHsb().a > 0) {
setClearColor(false);
}
if (!value) {
setColorValue(color);
}
onChange?.(color, color.toHexString());
};
const handleClear = (clear: boolean) => {
setClearColor(clear);
};
const popoverProps: PopoverProps = {
open: popupOpen,
trigger,
placement,
arrow,
rootClassName,
getPopupContainer,
};
const colorBaseProps: ColorPickerBaseProps = {
prefixCls,
color: colorValue,
allowClear,
clearColor,
disabled,
presets,
format,
onFormatChange,
};
return wrapSSR(
<Popover
style={styles?.popup}
onOpenChange={setPopupOpen}
content={
<ColorPickerPanel {...colorBaseProps} onChange={handleChange} onClear={handleClear} />
}
overlayClassName={prefixCls}
{...popoverProps}
>
{children || (
<ColorTrigger
open={popupOpen}
className={mergeCls}
style={style}
color={colorValue}
prefixCls={prefixCls}
clearColor={clearColor}
disabled={disabled}
/>
)}
</Popover>,
);
};
if (process.env.NODE_ENV !== 'production') {
ColorPicker.displayName = 'ColorPicker';
}
const PurePanel = genPurePanel(ColorPicker, 'color-picker', (prefixCls) => prefixCls);
ColorPicker._InternalPanelDoNotUseOrYouWillBeFired = PurePanel;
export default ColorPicker;

63
components/color-picker/ColorPickerPanel.tsx

@ -0,0 +1,63 @@
import { ColorPickerPanel as RcColorPickerPanel } from '@rc-component/color-picker';
import type { FC } from 'react';
import React from 'react';
import Divider from '../divider';
import type { Color } from './color';
import ColorClear from './components/ColorClear';
import ColorInput from './components/ColorInput';
import ColorPresets from './components/ColorPresets';
import type { ColorPickerBaseProps } from './interface';
interface ColorPickerPanelProps extends ColorPickerBaseProps {
onChange?: (value?: Color) => void;
onClear?: (clear?: boolean) => void;
}
const ColorPickerPanel: FC<ColorPickerPanelProps> = (props) => {
const { prefixCls, allowClear, presets, onChange, onClear, color, ...injectProps } = props;
const colorPickerPanelPrefixCls = `${prefixCls}-inner-panel`;
const extraPanelRender = (panel: React.ReactElement) => (
<div className={colorPickerPanelPrefixCls}>
{allowClear && (
<ColorClear
prefixCls={prefixCls}
value={color}
onChange={(clearColor) => {
onChange?.(clearColor);
onClear?.(true);
}}
{...injectProps}
/>
)}
{panel}
<ColorInput
value={color}
onChange={(value) => onChange?.(value)}
prefixCls={prefixCls}
{...injectProps}
/>
{Array.isArray(presets) && (
<>
<Divider className={`${colorPickerPanelPrefixCls}-divider`} />
<ColorPresets
value={color}
presets={presets}
onChange={(value) => onChange?.(value)}
prefixCls={prefixCls}
/>
</>
)}
</div>
);
return (
<RcColorPickerPanel
prefixCls={prefixCls}
value={color?.toHsb()}
onChange={onChange}
panelRender={extraPanelRender}
/>
);
};
export default ColorPickerPanel;

4353
components/color-picker/__tests__/__snapshots__/demo-extend.test.ts.snap

File diff suppressed because it is too large

248
components/color-picker/__tests__/__snapshots__/demo.test.ts.snap

@ -0,0 +1,248 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders components/color-picker/demo/allowClear.tsx correctly 1`] = `
<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>
`;
exports[`renders components/color-picker/demo/base.tsx correctly 1`] = `
<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>
`;
exports[`renders components/color-picker/demo/disabled.tsx correctly 1`] = `
<div
class="ant-color-picker-trigger ant-color-picker-trigger-disabled"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
/>
</div>
</div>
`;
exports[`renders components/color-picker/demo/format.tsx correctly 1`] = `
<div
class="ant-space ant-space-vertical"
style="display:flex"
>
<div
class="ant-space-item"
style="margin-bottom:16px"
>
<div
class="ant-row ant-row-middle"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<div
class="ant-col"
>
<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
class="ant-space-item"
>
<div
class="ant-col"
>
HEX:
<span>
#1677ff
</span>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-space-item"
style="margin-bottom:16px"
>
<div
class="ant-row ant-row-middle"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<div
class="ant-col"
>
<div
class="ant-color-picker-trigger"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(23, 120, 255)"
/>
</div>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-col"
>
HSB:
<span>
hsb(215,91%,100%)
</span>
</div>
</div>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-row ant-row-middle"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<div
class="ant-col"
>
<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
class="ant-space-item"
>
<div
class="ant-col"
>
RGB:
<span>
rgb(22,119,255)
</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders components/color-picker/demo/presets.tsx correctly 1`] = `
<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>
`;
exports[`renders components/color-picker/demo/pure-panel.tsx correctly 1`] = `
<div
style="padding-bottom:0;position:relative;min-width:0"
>
<div
class="ant-color-picker-trigger"
style="margin:0"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
/>
</div>
</div>
</div>
`;
exports[`renders components/color-picker/demo/trigger.tsx correctly 1`] = `
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<div
style="width:20px;height:20px;border-radius:4px;background:#1677ff"
/>
</div>
<div
class="ant-space-item"
>
<span>
#1677ff
</span>
</div>
</div>
`;

41
components/color-picker/__tests__/__snapshots__/index.test.tsx.snap

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ColorPicker Should disabled work 1`] = `
<div>
<div
class="ant-color-picker-trigger ant-color-picker-trigger-disabled"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background: rgb(22, 119, 255);"
/>
</div>
</div>
</div>
`;
exports[`ColorPicker Should render trigger work 1`] = `
<div>
<div
class="trigger"
/>
</div>
`;
exports[`ColorPicker rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-color-picker-trigger ant-color-picker-rtl"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background: rgb(22, 119, 255);"
/>
</div>
</div>
`;

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

@ -0,0 +1,96 @@
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import ColorAlphaInput from '../components/ColorAlphaInput';
import ColorHexInput from '../components/ColorHexInput';
import ColorHsbInput from '../components/ColorHsbInput';
import ColorRgbInput from '../components/ColorRgbInput';
import ColorSteppers from '../components/ColorSteppers';
describe('ColorPicker Components test', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('Should ColorSteppers work correct', () => {
const handleAlphaChange = jest.fn();
const { container } = render(<ColorSteppers prefixCls="test" onChange={handleAlphaChange} />);
expect(container.querySelector('.test-steppers')).toBeTruthy();
fireEvent.change(container.querySelector('.test-steppers input')!, {
target: { value: 1 },
});
expect(container.querySelector('.test-steppers input')?.getAttribute('value')).toEqual('1');
expect(handleAlphaChange).toHaveBeenCalledTimes(1);
});
it('Should ColorAlphaInput work correct', () => {
const handleAlphaChange = jest.fn();
const { container } = render(<ColorAlphaInput prefixCls="test" onChange={handleAlphaChange} />);
expect(container.querySelector('.test-alpha-input')).toBeTruthy();
fireEvent.change(container.querySelector('.test-alpha-input input')!, {
target: { value: 1 },
});
expect(container.querySelector('.test-alpha-input input')?.getAttribute('value')).toEqual('1%');
expect(handleAlphaChange).toHaveBeenCalledTimes(1);
});
it('Should ColorHexInput work correct', () => {
const handleAlphaChange = jest.fn();
const { container } = render(<ColorHexInput prefixCls="test" onChange={handleAlphaChange} />);
expect(container.querySelector('.test-hex-input')).toBeTruthy();
fireEvent.change(container.querySelector('.test-hex-input input')!, {
target: { value: 631515 },
});
expect(container.querySelector('.test-hex-input input')?.getAttribute('value')).toEqual(
'631515',
);
expect(handleAlphaChange).toHaveBeenCalledTimes(1);
});
it('Should ColorHsbInput work correct', () => {
const handleAlphaChange = jest.fn();
const { container } = render(<ColorHsbInput prefixCls="test" onChange={handleAlphaChange} />);
expect(container.querySelector('.test-hsb-input')).toBeTruthy();
const hsbInputEls = container.querySelectorAll('.test-hsb-input input');
fireEvent.change(hsbInputEls[0], {
target: { value: 139 },
});
expect(hsbInputEls[0]?.getAttribute('value')).toEqual('139');
fireEvent.change(hsbInputEls[1], {
target: { value: 78 },
});
expect(hsbInputEls[1]?.getAttribute('value')).toEqual('78%');
fireEvent.change(hsbInputEls[2], {
target: { value: 39 },
});
expect(hsbInputEls[2]?.getAttribute('value')).toEqual('39%');
expect(handleAlphaChange).toHaveBeenCalledTimes(3);
});
it('Should ColorRgbInput work correct', () => {
const handleAlphaChange = jest.fn();
const { container } = render(<ColorRgbInput prefixCls="test" onChange={handleAlphaChange} />);
expect(container.querySelector('.test-rgb-input')).toBeTruthy();
const rgbInputEls = container.querySelectorAll('.test-rgb-input input');
fireEvent.change(rgbInputEls[0], {
target: { value: 99 },
});
expect(rgbInputEls[0]?.getAttribute('value')).toEqual('99');
fireEvent.change(rgbInputEls[1], {
target: { value: 21 },
});
expect(rgbInputEls[1]?.getAttribute('value')).toEqual('21');
fireEvent.change(rgbInputEls[2], {
target: { value: 21 },
});
expect(rgbInputEls[2]?.getAttribute('value')).toEqual('21');
expect(handleAlphaChange).toHaveBeenCalledTimes(3);
});
});

3
components/color-picker/__tests__/demo-extend.test.ts

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('color-picker');

3
components/color-picker/__tests__/demo.test.ts

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('color-picker');

5
components/color-picker/__tests__/image.test.ts

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('ColorPicker image', () => {
imageDemoTest('color-picker');
});

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

@ -0,0 +1,249 @@
import { fireEvent, render } from '@testing-library/react';
import React, { useMemo, useState } from 'react';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { waitFakeTimer } from '../../../tests/utils';
import ColorPicker from '../ColorPicker';
import type { Color } from '../color';
describe('ColorPicker', () => {
mountTest(ColorPicker);
rtlTest(ColorPicker);
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('Should component render correct', () => {
const { container } = render(<ColorPicker />);
expect(container.querySelector('.ant-color-picker-trigger')).toBeTruthy();
});
it('Should component defaultValue work', () => {
const { container } = render(<ColorPicker defaultValue="#000000" />);
expect(
container.querySelector('.ant-color-picker-color-block-inner')?.getAttribute('style'),
).toEqual('background: rgb(0, 0, 0);');
});
it('Should component custom trigger work', async () => {
const App = () => {
const [color, setColor] = useState<Color | string>('hsb(215, 91%, 100%)');
const colorString = useMemo(
() => (typeof color === 'string' ? color : color.toHsbString()),
[color],
);
return (
<ColorPicker value={color} onChange={setColor} format="hsb">
<span className="custom-trigger">{colorString}</span>
</ColorPicker>
);
};
const { container } = render(<App />);
expect(container.querySelector('.custom-trigger')).toBeTruthy();
fireEvent.click(container.querySelector('.custom-trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-color-picker')).toBeTruthy();
const hsbInputEls = container.querySelectorAll('.ant-color-picker-hsb-input input');
fireEvent.change(hsbInputEls[0], {
target: { value: 0 },
});
fireEvent.change(hsbInputEls[1], {
target: { value: 78 },
});
fireEvent.change(hsbInputEls[2], {
target: { value: 39 },
});
expect(container.querySelector('.custom-trigger')?.innerHTML).toEqual('hsb(0, 78%, 39%)');
});
it('Should popup open work', async () => {
const { container } = render(<ColorPicker />);
fireEvent.click(container.querySelector('.ant-color-picker-trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-color-picker')).toBeTruthy();
fireEvent.click(container.querySelector('.ant-color-picker-trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-popover-hidden')).toBeTruthy();
});
it('Should disabled work', async () => {
const { container } = render(<ColorPicker disabled />);
expect(container.querySelector('.ant-color-picker-trigger-disabled')).toBeTruthy();
expect(container).toMatchSnapshot();
fireEvent.click(container.querySelector('.ant-color-picker-trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-color-picker')).toBeFalsy();
});
it('Should allowClear work', async () => {
const { container } = render(<ColorPicker allowClear />);
fireEvent.click(container.querySelector('.ant-color-picker-trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-color-picker-clear')).toBeTruthy();
fireEvent.click(container.querySelector('.ant-color-picker-clear')!);
expect(
container.querySelector('.ant-color-picker-alpha-input input')?.getAttribute('value'),
).toEqual('0%');
expect(
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
).toBeTruthy();
fireEvent.change(container.querySelector('.ant-color-picker-alpha-input input')!, {
target: { value: 1 },
});
expect(
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
).toBeFalsy();
});
it('Should render trigger work', async () => {
const { container } = render(
<ColorPicker>
<div className="trigger" />
</ColorPicker>,
);
expect(container.querySelector('.trigger')).toBeTruthy();
expect(container).toMatchSnapshot();
fireEvent.click(container.querySelector('.trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-color-picker')).toBeTruthy();
fireEvent.click(container.querySelector('.trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-popover-hidden')).toBeTruthy();
});
it('Should preset color work', async () => {
const handleColorChange = jest.fn();
const { container } = render(
<ColorPicker
onChange={handleColorChange}
presets={[
{
label: 'Recommended',
colors: [
'#000000',
'#000000E0',
'#000000A6',
'#00000073',
'#00000040',
'#00000026',
'#0000001A',
'#00000012',
'#0000000A',
'#00000005',
],
},
{
label: 'Recent',
colors: [],
},
]}
/>,
);
fireEvent.click(container.querySelector('.ant-color-picker-trigger')!);
await waitFakeTimer();
const presetsColors = container
.querySelector('.ant-collapse-content')
?.querySelectorAll('.ant-color-picker-presets-color')!;
expect(container.querySelector('.ant-color-picker-presets')).toBeTruthy();
expect(presetsColors.length).toBe(10);
expect(
container
.querySelectorAll('.ant-collapse-content')[1]
.querySelector('.ant-color-picker-presets-empty'),
).toBeTruthy();
fireEvent.click(presetsColors[0]);
expect(
presetsColors[0].classList.contains('ant-color-picker-presets-color-bright'),
).toBeFalsy();
expect(
container.querySelector('.ant-color-picker-hex-input input')?.getAttribute('value'),
).toEqual('000000');
fireEvent.click(presetsColors[9]);
expect(
presetsColors[9].classList.contains('ant-color-picker-presets-color-bright'),
).toBeTruthy();
expect(
container.querySelector('.ant-color-picker-hex-input input')?.getAttribute('value'),
).toEqual('000000');
expect(
container.querySelector('.ant-color-picker-alpha-input input')?.getAttribute('value'),
).toEqual('2%');
expect(handleColorChange).toHaveBeenCalledTimes(2);
});
it('Should format change work', async () => {
const { container } = render(<ColorPicker />);
fireEvent.click(container.querySelector('.ant-color-picker-trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-color-picker-hex-input')).toBeTruthy();
fireEvent.mouseDown(
container.querySelector('.ant-color-picker-format-select .ant-select-selector')!,
);
await waitFakeTimer();
fireEvent.click(container.querySelector('.ant-select-item[title="HSB"]')!);
await waitFakeTimer();
expect(container.querySelector('.ant-color-picker-hsb-input')).toBeTruthy();
fireEvent.mouseDown(
container.querySelector('.ant-color-picker-format-select .ant-select-selector')!,
);
await waitFakeTimer();
fireEvent.click(container.querySelector('.ant-select-item[title="RGB"]')!);
await waitFakeTimer();
expect(container.querySelector('.ant-color-picker-rgb-input')).toBeTruthy();
});
it('Should hex input work', async () => {
const { container } = render(<ColorPicker open format="hex" />);
fireEvent.change(container.querySelector('.ant-color-picker-hex-input input')!, {
target: { value: 631515 },
});
expect(
container.querySelector('.ant-color-picker-color-block-inner')?.getAttribute('style'),
).toEqual('background: rgb(99, 21, 21);');
});
it('Should rgb input work', async () => {
const { container } = render(<ColorPicker open format="rgb" />);
const rgbInputEls = container.querySelectorAll('.ant-color-picker-rgb-input input');
fireEvent.change(rgbInputEls[0], {
target: { value: 99 },
});
fireEvent.change(rgbInputEls[1], {
target: { value: 21 },
});
fireEvent.change(rgbInputEls[2], {
target: { value: 21 },
});
expect(
container.querySelector('.ant-color-picker-color-block-inner')?.getAttribute('style'),
).toEqual('background: rgb(99, 21, 21);');
});
it('Should hsb input work', async () => {
const { container } = render(<ColorPicker open format="hsb" />);
const hsbInputEls = container.querySelectorAll('.ant-color-picker-hsb-input input');
fireEvent.change(hsbInputEls[0], {
target: { value: 0 },
});
fireEvent.change(hsbInputEls[1], {
target: { value: 78 },
});
fireEvent.change(hsbInputEls[2], {
target: { value: 39 },
});
expect(
container.querySelector('.ant-color-picker-color-block-inner')?.getAttribute('style'),
).toEqual('background: rgb(99, 22, 22);');
});
});

45
components/color-picker/color.ts

@ -0,0 +1,45 @@
/* eslint-disable class-methods-use-this */
import type { ColorGenInput } from '@rc-component/color-picker';
import { Color as RcColor } from '@rc-component/color-picker';
import { getHex } from './util';
export interface Color
extends Pick<
RcColor,
'toHsb' | 'toHsbString' | 'toHex' | 'toHexString' | 'toRgb' | 'toRgbString'
> {}
export class ColorFactory {
/** Original Color object */
private metaColor: RcColor;
constructor(color: ColorGenInput<Color>) {
this.metaColor = new RcColor(color as ColorGenInput);
}
toHsb() {
return this.metaColor.toHsb();
}
toHsbString() {
return this.metaColor.toHsbString();
}
toHex() {
return getHex(this.toHexString(), this.metaColor.getAlpha() < 1);
}
toHexString() {
return this.metaColor.getAlpha() === 1
? this.metaColor.toHexString()
: this.metaColor.toHex8String();
}
toRgb() {
return this.metaColor.toRgb();
}
toRgbString() {
return this.metaColor.toRgbString();
}
}

45
components/color-picker/components/ColorAlphaInput.tsx

@ -0,0 +1,45 @@
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor, getAlphaColor } from '../util';
import ColorSteppers from './ColorSteppers';
interface ColorAlphaInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
}
const ColorAlphaInput: FC<ColorAlphaInputProps> = ({ prefixCls, value, onChange }) => {
const colorAlphaInputPrefixCls = `${prefixCls}-alpha-input`;
const [alphaValue, setAlphaValue] = useState(generateColor(value || '#000'));
// Update step value
useEffect(() => {
if (value) {
setAlphaValue(value);
}
}, [value]);
const handleAlphaChange = (step: number) => {
const hsba = alphaValue.toHsb();
hsba.a = (step || 0) / 100;
const genColor = generateColor(hsba);
if (!value) {
setAlphaValue(genColor);
}
onChange?.(genColor);
};
return (
<ColorSteppers
value={getAlphaColor(alphaValue)}
prefixCls={prefixCls}
formatter={(step) => `${step}%`}
className={colorAlphaInputPrefixCls}
onChange={handleAlphaChange}
/>
);
};
export default ColorAlphaInput;

24
components/color-picker/components/ColorClear.tsx

@ -0,0 +1,24 @@
import type { FC } from 'react';
import React from 'react';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor } from '../util';
interface ColorClearProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
}
const ColorClear: FC<ColorClearProps> = ({ prefixCls, value, onChange }) => {
const handleClick = () => {
if (value) {
const hsba = value.toHsb();
hsba.a = 0;
const genColor = generateColor(hsba);
onChange?.(genColor);
}
};
return <div className={`${prefixCls}-clear`} onClick={handleClick} />;
};
export default ColorClear;

47
components/color-picker/components/ColorHexInput.tsx

@ -0,0 +1,47 @@
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import Input from '../../input';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor, toHexFormat } from '../util';
interface ColorHexInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
}
const hexReg = /(^#[\da-f]{6}$)|(^#[\da-f]{8}$)/i;
const isHexString = (hex?: string) => hexReg.test(`#${hex}`);
const ColorHexInput: FC<ColorHexInputProps> = ({ prefixCls, value, onChange }) => {
const colorHexInputPrefixCls = `${prefixCls}-hex-input`;
const [hexValue, setHexValue] = useState(value?.toHex());
// Update step value
useEffect(() => {
const hex = value?.toHex();
if (isHexString(hex) && value) {
setHexValue(toHexFormat(hex));
}
}, [value]);
const handleHexChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const originValue = e.target.value;
setHexValue(toHexFormat(originValue));
if (isHexString(toHexFormat(originValue, true))) {
onChange?.(generateColor(originValue));
}
};
return (
<Input
className={colorHexInputPrefixCls}
value={hexValue?.toUpperCase()}
prefix="#"
onChange={handleHexChange}
size="small"
/>
);
};
export default ColorHexInput;

69
components/color-picker/components/ColorHsbInput.tsx

@ -0,0 +1,69 @@
import type { HSB } from '@rc-component/color-picker';
import { getRoundNumber } from '@rc-component/color-picker/lib/util';
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor } from '../util';
import ColorSteppers from './ColorSteppers';
interface ColorHsbInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
}
const ColorHsbInput: FC<ColorHsbInputProps> = ({ prefixCls, value, onChange }) => {
const colorHsbInputPrefixCls = `${prefixCls}-hsb-input`;
const [hsbValue, setHsbValue] = useState(generateColor(value || '#000'));
// Update step value
useEffect(() => {
if (value) {
setHsbValue(value);
}
}, [value]);
const handleHsbChange = (step: number, type: keyof HSB) => {
const hsb = hsbValue.toHsb();
hsb[type] = type === 'h' ? step : (step || 0) / 100;
const genColor = generateColor(hsb);
if (!value) {
setHsbValue(genColor);
}
onChange?.(genColor);
};
return (
<div className={colorHsbInputPrefixCls}>
<ColorSteppers
max={360}
min={0}
value={Number(hsbValue.toHsb().h)}
prefixCls={prefixCls}
className={colorHsbInputPrefixCls}
formatter={(step) => getRoundNumber(step || 0).toString()}
onChange={(step) => handleHsbChange(Number(step), 'h')}
/>
<ColorSteppers
max={100}
min={0}
value={Number(hsbValue.toHsb().s) * 100}
prefixCls={prefixCls}
className={colorHsbInputPrefixCls}
formatter={(step) => `${getRoundNumber(step || 0)}%`}
onChange={(step) => handleHsbChange(Number(step), 's')}
/>
<ColorSteppers
max={100}
min={0}
value={Number(hsbValue.toHsb().b) * 100}
prefixCls={prefixCls}
className={colorHsbInputPrefixCls}
formatter={(step) => `${getRoundNumber(step || 0)}%`}
onChange={(step) => handleHsbChange(Number(step), 'b')}
/>
</div>
);
};
export default ColorHsbInput;

75
components/color-picker/components/ColorInput.tsx

@ -0,0 +1,75 @@
import type { FC } from 'react';
import React from 'react';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import Select from '../../select';
import type { ColorPickerBaseProps } from '../interface';
import { ColorFormat } from '../interface';
import type { Color } from '../color';
import ColorAlphaInput from './ColorAlphaInput';
import ColorHexInput from './ColorHexInput';
import ColorHsbInput from './ColorHsbInput';
import ColorRgbInput from './ColorRgbInput';
interface ColorInputProps
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'format' | 'onFormatChange'> {
value?: Color;
onChange?: (value: Color) => void;
}
const ColorInput: FC<ColorInputProps> = (props) => {
const { prefixCls, format, onFormatChange, value, onChange } = props;
const [colorFormat, setColorFormat] = useMergedState('hex', {
value: format,
onChange: onFormatChange,
});
const colorInputPrefixCls = `${prefixCls}-input`;
const handleFormatChange = (newFormat: ColorFormat) => {
setColorFormat(newFormat);
};
const steppersRender = () => {
switch (colorFormat) {
case ColorFormat.hsb:
return <ColorHsbInput value={value} onChange={onChange} prefixCls={prefixCls} />;
case ColorFormat.rgb:
return <ColorRgbInput value={value} onChange={onChange} prefixCls={prefixCls} />;
case ColorFormat.hex:
default:
return <ColorHexInput value={value} onChange={onChange} prefixCls={prefixCls} />;
}
};
return (
<div className={`${colorInputPrefixCls}-container`}>
<Select
value={colorFormat}
bordered={false}
getPopupContainer={(current) => current}
popupMatchSelectWidth={68}
placement="bottomRight"
onChange={handleFormatChange}
className={`${prefixCls}-format-select`}
size="small"
options={[
{
label: ColorFormat.hex.toLocaleUpperCase(),
value: ColorFormat.hex,
},
{
label: ColorFormat.hsb.toLocaleUpperCase(),
value: ColorFormat.hsb,
},
{
label: ColorFormat.rgb.toLocaleUpperCase(),
value: ColorFormat.rgb,
},
]}
/>
<div className={colorInputPrefixCls}>{steppersRender()}</div>
<ColorAlphaInput prefixCls={prefixCls} value={value} onChange={onChange} />
</div>
);
};
export default ColorInput;

84
components/color-picker/components/ColorPresets.tsx

@ -0,0 +1,84 @@
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import type { FC } from 'react';
import React, { useMemo } from 'react';
import { ColorBlock } from '@rc-component/color-picker';
import Collapse from '../../collapse';
import { useLocale } from '../../locale';
import type { Color } from '../color';
import type { ColorPickerBaseProps, PresetsItem } from '../interface';
import { generateColor } from '../util';
const { Panel } = Collapse;
interface ColorPresetsProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
presets: PresetsItem[];
value?: Color;
onChange?: (value: Color) => void;
}
const genPresetColor = (list: PresetsItem[]) =>
list.map((value) => {
value.colors = value.colors.map((color) => generateColor(color));
return value;
});
const isBright = (value: Color) => {
const { r, g, b, a } = value.toRgb();
if (a <= 0.5) {
return true;
}
return r * 0.299 + g * 0.587 + b * 0.114 > 192;
};
const ColorPresets: FC<ColorPresetsProps> = ({ prefixCls, presets, value: color, onChange }) => {
const [locale] = useLocale('ColorPicker');
const [presetsValue] = useMergedState(genPresetColor(presets), {
value: genPresetColor(presets),
postState: (item) => genPresetColor(item),
});
const colorPresetsPrefixCls = `${prefixCls}-presets`;
const activeKey = useMemo(
() => presetsValue.map((preset) => `panel-${preset.label}`),
[presetsValue],
);
const handleClick = (colorValue: Color) => {
onChange?.(colorValue);
};
return (
<div className={colorPresetsPrefixCls}>
<Collapse defaultActiveKey={activeKey} ghost>
{presetsValue.map((preset) => (
<Panel
header={<div className={`${colorPresetsPrefixCls}-label`}>{preset?.label}</div>}
key={`panel-${preset?.label}`}
>
<div className={`${colorPresetsPrefixCls}-items`}>
{Array.isArray(preset?.colors) && preset?.colors.length > 0 ? (
preset.colors.map((presetColor: Color) => (
<ColorBlock
key={`preset-${presetColor.toHexString()}`}
color={generateColor(presetColor).toRgbString()}
prefixCls={prefixCls}
className={classNames(`${colorPresetsPrefixCls}-color`, {
[`${colorPresetsPrefixCls}-color-checked`]:
presetColor.toHexString() === color?.toHexString(),
[`${colorPresetsPrefixCls}-color-bright`]: isBright(presetColor),
})}
onClick={() => handleClick(presetColor)}
/>
))
) : (
<span className={`${colorPresetsPrefixCls}-empty`}>{locale.presetEmpty}</span>
)}
</div>
</Panel>
))}
</Collapse>
</div>
);
};
export default ColorPresets;

65
components/color-picker/components/ColorRgbInput.tsx

@ -0,0 +1,65 @@
import type { RGB } from '@rc-component/color-picker';
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor } from '../util';
import ColorSteppers from './ColorSteppers';
interface ColorRgbInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
}
const ColorRgbInput: FC<ColorRgbInputProps> = ({ prefixCls, value, onChange }) => {
const colorRgbInputPrefixCls = `${prefixCls}-rgb-input`;
const [rgbValue, setRgbValue] = useState(generateColor(value || '#000'));
// Update step value
useEffect(() => {
if (value) {
setRgbValue(value);
}
}, [value]);
const handleRgbChange = (step: number | null, type: keyof RGB) => {
const rgb = rgbValue.toRgb();
rgb[type] = step || 0;
const genColor = generateColor(rgb);
if (!value) {
setRgbValue(genColor);
}
onChange?.(genColor);
};
return (
<div className={colorRgbInputPrefixCls}>
<ColorSteppers
max={255}
min={0}
value={Number(rgbValue.toRgb().r)}
prefixCls={prefixCls}
className={colorRgbInputPrefixCls}
onChange={(step) => handleRgbChange(Number(step), 'r')}
/>
<ColorSteppers
max={255}
min={0}
value={Number(rgbValue.toRgb().g)}
prefixCls={prefixCls}
className={colorRgbInputPrefixCls}
onChange={(step) => handleRgbChange(Number(step), 'g')}
/>
<ColorSteppers
max={255}
min={0}
value={Number(rgbValue.toRgb().b)}
prefixCls={prefixCls}
className={colorRgbInputPrefixCls}
onChange={(step) => handleRgbChange(Number(step), 'b')}
/>
</div>
);
};
export default ColorRgbInput;

55
components/color-picker/components/ColorSteppers.tsx

@ -0,0 +1,55 @@
import classNames from 'classnames';
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import type { InputNumberProps } from '../../input-number';
import InputNumber from '../../input-number';
import type { ColorPickerBaseProps } from '../interface';
interface ColorSteppersProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: number;
min?: number;
max?: number;
onChange?: (value: number | null) => void;
className?: string;
prefix?: (prefixCls: string) => React.ReactNode;
formatter?: InputNumberProps<number>['formatter'];
}
const ColorSteppers: FC<ColorSteppersProps> = ({
prefixCls,
min = 0,
max = 100,
value,
onChange,
className,
formatter,
}) => {
const colorSteppersPrefixCls = `${prefixCls}-steppers`;
const [stepValue, setStepValue] = useState(value);
// Update step value
useEffect(() => {
if (!Number.isNaN(value)) {
setStepValue(value);
}
}, [value]);
return (
<InputNumber
className={classNames(colorSteppersPrefixCls, className)}
min={min}
max={max}
value={stepValue}
formatter={formatter}
size="small"
onChange={(step) => {
if (!value) {
setStepValue(step || 0);
}
onChange?.(step);
}}
/>
);
};
export default ColorSteppers;

46
components/color-picker/components/ColorTrigger.tsx

@ -0,0 +1,46 @@
import classNames from 'classnames';
import type { CSSProperties, MouseEventHandler } from 'react';
import React, { forwardRef, useMemo } from 'react';
import { ColorBlock } from '@rc-component/color-picker';
import type { ColorPickerBaseProps } from '../interface';
import ColorClear from './ColorClear';
interface colorTriggerProps
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'clearColor' | 'disabled'> {
color: Exclude<ColorPickerBaseProps['color'], undefined>;
open?: boolean;
className?: string;
style?: CSSProperties;
onClick?: MouseEventHandler<HTMLDivElement>;
onMouseEnter?: MouseEventHandler<HTMLDivElement>;
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
}
const ColorTrigger = forwardRef<HTMLDivElement, colorTriggerProps>((props, ref) => {
const { color, prefixCls, open, clearColor, disabled, className, ...rest } = props;
const colorTriggerPrefixCls = `${prefixCls}-trigger`;
const containerRender = useMemo(
() =>
clearColor ? (
<ColorClear prefixCls={prefixCls} />
) : (
<ColorBlock color={color.toRgbString()} prefixCls={prefixCls} />
),
[color, clearColor],
);
return (
<div
ref={ref}
className={classNames(colorTriggerPrefixCls, className, {
[`${colorTriggerPrefixCls}-active`]: open,
[`${colorTriggerPrefixCls}-disabled`]: disabled,
})}
{...rest}
>
{containerRender}
</div>
);
});
export default ColorTrigger;

7
components/color-picker/demo/allowClear.md

@ -0,0 +1,7 @@
## zh-CN
清除已选择的颜色。
## en-US
Clear Color.

4
components/color-picker/demo/allowClear.tsx

@ -0,0 +1,4 @@
import { ColorPicker } from 'antd';
import React from 'react';
export default () => <ColorPicker allowClear />;

7
components/color-picker/demo/base.md

@ -0,0 +1,7 @@
## zh-CN
最简单的使用方法。
## en-US
Basic Usage.

10
components/color-picker/demo/base.tsx

@ -0,0 +1,10 @@
import { ColorPicker, theme } from 'antd';
import type { Color } from 'antd/lib/color-picker';
import React, { useState } from 'react';
export default () => {
const { token } = theme.useToken();
const [color, setColor] = useState<Color | string>(token.colorPrimary);
return <ColorPicker value={color} onChange={setColor} />;
};

7
components/color-picker/demo/disabled.md

@ -0,0 +1,7 @@
## zh-CN
设置为禁用状态。
## en-US
Set to disabled state.

4
components/color-picker/demo/disabled.tsx

@ -0,0 +1,4 @@
import { ColorPicker } from 'antd';
import React from 'react';
export default () => <ColorPicker disabled />;

7
components/color-picker/demo/format.md

@ -0,0 +1,7 @@
## zh-CN
编码格式,支持`HEX`、`HSB`、`RGB`。
## en-US
Encoding formats, support `HEX`, `HSB`, `RGB`.

57
components/color-picker/demo/format.tsx

@ -0,0 +1,57 @@
import { Col, ColorPicker, Row, Space } from 'antd';
import type { Color } from 'antd/lib/color-picker';
import React, { useMemo, useState } from 'react';
export default () => {
const [colorHex, setColorHex] = useState<Color | string>('#1677ff');
const [colorHsb, setColorHsb] = useState<Color | string>('hsb(215,91%,100%)');
const [colorRgb, setColorRgb] = useState<Color | string>('rgb(22,119,255)');
const hexString = useMemo(
() => (typeof colorHex === 'string' ? colorHex : colorHex.toHexString()),
[colorHex],
);
const hsbString = useMemo(
() => (typeof colorHsb === 'string' ? colorHsb : colorHsb.toHsbString()),
[colorHsb],
);
const rgbString = useMemo(
() => (typeof colorRgb === 'string' ? colorRgb : colorRgb.toRgbString()),
[colorRgb],
);
return (
<Space direction="vertical" size="middle" style={{ display: 'flex' }}>
<Row align="middle">
<Space>
<Col>
<ColorPicker format="hex" value={colorHex} onChange={setColorHex} />
</Col>
<Col>
HEX: <span>{hexString}</span>
</Col>
</Space>
</Row>
<Row align="middle">
<Space>
<Col>
<ColorPicker format="hsb" value={colorHsb} onChange={setColorHsb} />
</Col>
<Col>
HSB: <span>{hsbString}</span>
</Col>
</Space>
</Row>
<Row align="middle">
<Space>
<Col>
<ColorPicker format="rgb" value={colorRgb} onChange={setColorRgb} />
</Col>
<Col>
RGB: <span>{rgbString}</span>
</Col>
</Space>
</Row>
</Space>
);
};

7
components/color-picker/demo/presets.md

@ -0,0 +1,7 @@
## zh-CN
设置颜色选择器的预设颜色。
## en-US
Set the presets color of the color picker.

48
components/color-picker/demo/presets.tsx

@ -0,0 +1,48 @@
import { ColorPicker } from 'antd';
import React from 'react';
export default () => (
<ColorPicker
presets={[
{
label: 'Recommended',
colors: [
'#000000',
'#000000E0',
'#000000A6',
'#00000073',
'#00000040',
'#00000026',
'#0000001A',
'#00000012',
'#0000000A',
'#00000005',
'#F5222D',
'#FA8C16',
'#FADB14',
'#8BBB11',
'#52C41A',
'#13A8A8',
'#1677FF',
'#2F54EB',
'#722ED1',
'#EB2F96',
'#F5222D4D',
'#FA8C164D',
'#FADB144D',
'#8BBB114D',
'#52C41A4D',
'#13A8A84D',
'#1677FF4D',
'#2F54EB4D',
'#722ED14D',
'#EB2F964D',
],
},
{
label: 'Recent',
colors: [],
},
]}
/>
);

7
components/color-picker/demo/pure-panel.md

@ -0,0 +1,7 @@
## zh-CN
Pure Panel
## en-US
Pure Panel

12
components/color-picker/demo/pure-panel.tsx

@ -0,0 +1,12 @@
import { ColorPicker, theme } from 'antd';
import type { Color } from 'antd/lib/color-picker';
import React, { useState } from 'react';
const PureRenderColorPicker = ColorPicker._InternalPanelDoNotUseOrYouWillBeFired;
export default () => {
const { token } = theme.useToken();
const [color, setColor] = useState<Color | string>(token.colorPrimary);
return <PureRenderColorPicker value={color} onChange={setColor} />;
};

7
components/color-picker/demo/trigger.md

@ -0,0 +1,7 @@
## zh-CN
自定义颜色面板的触发器。
## en-US
Triggers for customizing color panels.

28
components/color-picker/demo/trigger.tsx

@ -0,0 +1,28 @@
import { ColorPicker, Space, theme } from 'antd';
import type { Color } from 'antd/lib/color-picker';
import React, { useMemo, useState } from 'react';
export default () => {
const { token } = theme.useToken();
const [color, setColor] = useState<Color | string>(token.colorPrimary);
const genColor = useMemo(
() => (typeof color === 'string' ? color : color.toHexString()),
[color],
);
return (
<ColorPicker value={color} onChange={setColor}>
<Space>
<div
style={{
width: 20,
height: 20,
borderRadius: 4,
background: genColor,
}}
/>
<span>{genColor}</span>
</Space>
</ColorPicker>
);
};

39
components/color-picker/hooks/useColorState.ts

@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
import type { Color } from '../color';
import { generateColor } from '../util';
function hasValue(value: Color | string | undefined) {
return value !== undefined;
}
const useColorState = (
defaultStateValue: Color | string,
option: {
defaultValue?: Color | string;
value?: Color | string;
},
): [Color, React.Dispatch<React.SetStateAction<Color>>] => {
const { defaultValue, value } = option;
const [colorValue, setColorValue] = useState(() => {
let mergeState;
if (hasValue(value)) {
mergeState = value;
} else if (hasValue(defaultValue)) {
mergeState = defaultValue;
} else {
mergeState = defaultStateValue;
}
const genColor = generateColor(mergeState || '');
return genColor;
});
useEffect(() => {
if (value) {
setColorValue(generateColor(value));
}
}, [value]);
return [colorValue, setColorValue];
};
export default useColorState;

67
components/color-picker/index.en-US.md

@ -0,0 +1,67 @@
---
category: Components
title: ColorPicker
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*PpY4RYNM8UcAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*EHL-QYJofZsAAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
group:
title: Data Entry
---
Components providing color selection
## When To Use
Used when the user needs to customize the color selection
## Examples
<!-- prettier-ignore -->
<code src="./demo/base.tsx">Basic Usage</code>
<code src="./demo/disabled.tsx" debug>Disable</code>
<code src="./demo/allowClear.tsx">Clear Color</code>
<code src="./demo/trigger.tsx">Custom Trigger</code>
<code src="./demo/format.tsx">Color Format</code>
<code src="./demo/presets.tsx">Preset Colors</code>
<code src="./demo/pure-panel.tsx" debug>Pure Render</code>
## API
> This component is available since antd@5.5.0
<!-- prettier-ignore -->
| Property | Description | Type | Default |
| :-- | :-- | :-- | :-- |
| format | Format of color | `rgb` \| `hex` \| `hsb` | `hex` |
| onFormatChange | Callback when `format` is changed | `(format: 'hex' \| 'rgb' \| 'hsb') => void` | - |
| value | Value of color | string \| `Color` | - |
| defaultValue | Default value of color | string \| `Color` | - |
| onChange | Callback when `value` is changed | `(value: Color, hex: string) => void` | - |
| allowClear | Allow clearing color selected | boolean | false |
| presets | Preset colors | `{ label: ReactNode, colors: Array<string \| Color> }[]` | - |
| children | Trigger of ColorPicker | ReactElement | - |
| trigger | ColorPicker trigger mode | `hover` \| `click` | `click` |
| open | Whether to show popup | boolean | - |
| onOpenChange | Callback when `open` is changed | `(open: boolean) => void` | - |
| disabled | Disable ColorPicker | boolean | - |
| placement | Placement of popup | `top` \| `topLeft` \| `topRight` \| `bottom` \| `bottomLeft` \| `bottomRight` | `bottomLeft` |
| arrow | Configuration for popup arrow | boolean | `{ pointAtCenter: boolean }` | - |
### Color
<!-- prettier-ignore -->
| Property | Description | Type | Default |
| :-- | :-- | :-- | :-- |
| toHex | Convert to `hex` format characters | `() => string` | - |
| toHexString | Convert to `hex` format color string | `() => string` | - |
| toHsb | Convert to `hsb` object | `() => ({ h: number, s: number, b: number, a number })` | - |
| toHsbString | Convert to `hsb` format color string | `() => string` | - |
| toRgb | Convert to `rgb` object | `() => ({ r: number, g: number, b: number, a number })` | - |
| toRgbString | Convert to `rgb` format color string | `() => string` | - |
## FAQ
### Questions about color assignment
The value of the color selector supports both string color values and selector-generated `Color` objects. However, since there is a precision error when converting color strings of different formats to each other, it is recommended to use selector-generated `Color` objects for assignment operations in controlled scenarios, so that the precision problem can be avoided and the values are guaranteed to be accurate and the selector can work as expected.

6
components/color-picker/index.tsx

@ -0,0 +1,6 @@
import ColorPicker from './ColorPicker';
export type { Color } from './color';
export type { ColorPickerProps } from './ColorPicker';
export default ColorPicker;

68
components/color-picker/index.zh-CN.md

@ -0,0 +1,68 @@
---
category: Components
subtitle: 颜色选择器
title: ColorPicker
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*PpY4RYNM8UcAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*EHL-QYJofZsAAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
group:
title: 数据录入
---
提供颜色选取的组件
## 何时使用
当用户需要自定义颜色选择的时候使用。
## 代码演示
<!-- prettier-ignore -->
<code src="./demo/base.tsx">基本使用</code>
<code src="./demo/disabled.tsx" debug>禁用</code>
<code src="./demo/allowClear.tsx">清除颜色</code>
<code src="./demo/trigger.tsx">自定义触发器</code>
<code src="./demo/format.tsx">颜色编码</code>
<code src="./demo/presets.tsx">预设颜色</code>
<code src="./demo/pure-panel.tsx" debug>Pure Render</code>
## API
> 自 `antd@5.5.0` 版本开始提供该组件。
<!-- prettier-ignore -->
| 参数 | 说明 | 类型 | 默认值 |
| :-- | :-- | :-- | :-- |
| format | 颜色格式 | `rgb` \| `hex` \| `hsb` | `hex` |
| onFormatChange | 颜色格式变化的回调 | `(format: 'hex' \| 'rgb' \| 'hsb') => void` | - |
| value | 颜色的值 | string \| `Color` | - |
| defaultValue | 颜色默认的值 | string \| `Color` | - |
| onChange | 颜色变化的回调 | `(value: Color, hex: string) => void` | - |
| allowClear | 允许清除选择的颜色 | boolean | false |
| presets | 预设的颜色 | `{ label: ReactNode, colors: Array<string \| Color> }[]` | - |
| children | 颜色选择器的触发器 | ReactElement | - |
| trigger | 颜色选择器的触发模式 | `hover` \| `click` | `click` |
| open | 是否显示弹出窗口 | boolean | - |
| onOpenChange | 当 `open` 被改变时的回调 | `(open: boolean) => void` | - |
| disabled | 禁用颜色选择器 | boolean | - |
| placement | 弹出窗口的位置 | `top` \| `topLeft` \| `topRight` \| `bottom` \| `bottomLeft` \| `bottomRight` | `bottomLeft` |
| arrow | 配置弹出的箭头 | boolean | `{ pointAtCenter: boolean }` | - |
### Color
<!-- prettier-ignore -->
| 参数 | 说明 | 类型 | 默认值 |
| :-- | :-- | :-- | :-- |
| toHex | 转换成 `hex` 格式字符 | `() => string` | - |
| toHexString | 转换成 `hex` 格式颜色字符串 | `() => string` | - |
| toHsb | 转换成 `hsb` 对象 | `() => ({ h: number, s: number, b: number, a number })` | - |
| toHsbString | 转换成 `hsb` 格式颜色字符串 | `() => string` | - |
| toRgb | 转换成 `rgb` 对象 | `() => ({ r: number, g: number, b: number, a number })` | - |
| toRgbString | 转换成 `rgb` 格式颜色字符串 | `() => string` | - |
## FAQ
### 关于颜色赋值的问题
颜色选择器的值同时支持字符串色值和选择器生成的 `Color` 对象,但由于不同格式的颜色字符串互相转换会有精度误差问题,所以受控场景推荐使用选择器生成的 `Color` 对象来进行赋值操作,这样可以避免精度问题,保证取值是精准的,选择器也可以按照预期工作。

22
components/color-picker/interface.ts

@ -0,0 +1,22 @@
import type { ReactNode } from 'react';
import type { ColorPickerProps } from './ColorPicker';
import type { Color } from './color';
export enum ColorFormat {
hex = 'hex',
rgb = 'rgb',
hsb = 'hsb',
}
export type PresetsItem = { label: ReactNode; colors: Array<string | Color> };
export interface ColorPickerBaseProps {
color?: Color;
prefixCls: string;
format?: keyof typeof ColorFormat;
allowClear?: boolean;
clearColor?: boolean;
disabled?: boolean;
presets?: PresetsItem[];
onFormatChange?: ColorPickerProps['onFormatChange'];
}

27
components/color-picker/style/color-block.ts

@ -0,0 +1,27 @@
import type { CSSObject } from '@ant-design/cssinjs';
import type { ColorPickerToken } from './index';
const genColorBlockStyle = (token: ColorPickerToken, size: number): CSSObject => {
const { componentCls, borderRadiusSM, colorPickerInsetShadow, lineWidth, colorFillSecondary } =
token;
return {
[`${componentCls}-color-block`]: {
position: 'relative',
borderRadius: borderRadiusSM,
width: size,
height: size,
boxShadow: colorPickerInsetShadow,
backgroundSize: '100%',
backgroundImage:
'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAFpJREFUWAntljEKADAIA23p6v//qQ+wfUEcCu1yriEgp0FHRJSJcnehmmWm1Dv/lO4HIg1AAAKjTqm03ea88zMCCEDgO4HV5bS757f+7wRoAAIQ4B9gByAAgQ3pfiDmXmAeEwAAAABJRU5ErkJggg==")',
[`${componentCls}-color-block-inner`]: {
width: '100%',
height: '100%',
border: `${lineWidth}px solid ${colorFillSecondary}`,
borderRadius: 'inherit',
},
},
};
};
export default genColorBlockStyle;

153
components/color-picker/style/index.ts

@ -0,0 +1,153 @@
import type { CSSObject } from '@ant-design/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal';
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
import genPickerStyle from './picker';
import genInputStyle from './input';
import genPresetsStyle from './presets';
import genColorBlockStyle from './color-block';
export interface ComponentToken {}
export interface ColorPickerToken extends FullToken<'ColorPicker'> {
colorPickerWidth: number;
colorPickerInsetShadow: string;
colorPickerHandlerSize: number;
colorPickerHandlerSizeSM: number;
colorPickerSliderHeight: number;
colorPickerPreviewSize: number;
colorPickerAlphaInputWidth: number;
colorPickerInputNumberHandleWidth: number;
colorPickerPresetColorSize: number;
}
export const genActiveStyle = (token: ColorPickerToken) => ({
boxShadow: `0 0 0 ${token.controlOutlineWidth}px ${token.controlOutline}`,
borderInlineEndWidth: token.lineWidth,
outline: 0,
});
const genClearStyle = (token: ColorPickerToken, size: number): CSSObject => {
const { componentCls, borderRadiusSM, lineWidth, colorSplit, red6 } = token;
return {
[`${componentCls}-clear`]: {
width: size,
height: size,
borderRadius: borderRadiusSM,
border: `${lineWidth}px solid ${colorSplit}`,
position: 'relative',
cursor: 'pointer',
overflow: 'hidden',
'&::after': {
content: '""',
position: 'absolute',
insetInlineEnd: lineWidth,
top: 0,
display: 'block',
width: 40, // maximum
height: 2, // fixed
transformOrigin: 'right',
transform: 'rotate(-45deg)',
backgroundColor: red6,
},
},
};
};
const genColorPickerStyle: GenerateStyle<ColorPickerToken> = (token) => {
const {
componentCls,
colorPickerWidth,
colorPrimary,
motionDurationMid,
colorBgElevated,
colorTextDisabled,
colorBgContainerDisabled,
borderRadius,
marginXS,
marginSM,
controlHeight,
controlHeightSM,
colorBgTextActive,
colorPickerPresetColorSize,
lineWidth,
colorBorder,
} = token;
return [
{
[componentCls]: {
[`${componentCls}-panel`]: {
display: 'flex',
flexDirection: 'column',
width: colorPickerWidth,
[`${componentCls}-inner-panel`]: {
[`${componentCls}-clear`]: {
marginInlineStart: 'auto',
marginBottom: marginXS,
},
'&-divider': {
margin: `${marginSM}px 0 ${marginXS}px`,
},
},
...genPickerStyle(token),
...genInputStyle(token),
...genPresetsStyle(token),
...genClearStyle(token, colorPickerPresetColorSize),
},
'&-trigger': {
width: controlHeight,
height: controlHeight,
borderRadius,
border: `${lineWidth}px solid ${colorBorder}`,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: `all ${motionDurationMid}`,
background: colorBgElevated,
'&-active': {
...genActiveStyle(token),
borderColor: colorPrimary,
},
'&:hover': {
borderColor: colorPrimary,
},
'&-disabled': {
color: colorTextDisabled,
background: colorBgContainerDisabled,
cursor: 'not-allowed',
'&:hover': {
borderColor: colorBgTextActive,
},
},
...genClearStyle(token, controlHeightSM),
...genColorBlockStyle(token, controlHeightSM),
},
},
},
];
};
export default genComponentStyleHook('ColorPicker', (token) => {
const { colorTextQuaternary, marginSM } = token;
const colorPickerSliderHeight = 8;
const ColorPickerToken = mergeToken<ColorPickerToken>(token, {
colorPickerWidth: 234,
colorPickerHandlerSize: 16,
colorPickerHandlerSizeSM: 12,
colorPickerAlphaInputWidth: 44,
colorPickerInputNumberHandleWidth: 16,
colorPickerPresetColorSize: 18,
colorPickerInsetShadow: `inset 0 0 1px 0 ${colorTextQuaternary}`,
colorPickerSliderHeight,
colorPickerPreviewSize: colorPickerSliderHeight * 2 + marginSM,
});
return [genColorPickerStyle(ColorPickerToken)];
});

99
components/color-picker/style/input.ts

@ -0,0 +1,99 @@
import type { CSSObject } from '@ant-design/cssinjs';
import type { GenerateStyle } from '../../theme/internal';
import type { ColorPickerToken } from './index';
const genInputStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
const {
componentCls,
antCls,
fontSizeSM,
lineHeightSM,
colorPickerAlphaInputWidth,
marginXXS,
paddingXXS,
controlHeightSM,
marginXS,
fontSizeIcon,
paddingXS,
colorTextPlaceholder,
colorPickerInputNumberHandleWidth,
lineWidth,
} = token;
return {
[`${componentCls}-input-container`]: {
display: 'flex',
[`${componentCls}-steppers${antCls}-input-number`]: {
fontSize: fontSizeSM,
lineHeight: lineHeightSM,
[`${antCls}-input-number-input`]: {
paddingInlineStart: paddingXXS,
paddingInlineEnd: 0,
},
[`${antCls}-input-number-handler-wrap`]: {
width: colorPickerInputNumberHandleWidth,
},
},
[`${componentCls}-steppers${componentCls}-alpha-input`]: {
flex: `0 0 ${colorPickerAlphaInputWidth}px`,
marginInlineStart: marginXXS,
},
[`${componentCls}-format-select${antCls}-select`]: {
marginInlineEnd: marginXS,
'&-single': {
[`${antCls}-select-selector`]: {
padding: 0,
border: 0,
},
[`${antCls}-select-arrow`]: {
insetInlineEnd: 0,
},
[`${antCls}-select-selection-item`]: {
paddingInlineEnd: fontSizeIcon + marginXXS,
fontSize: fontSizeSM,
lineHeight: `${controlHeightSM}px`,
},
[`${antCls}-select-item-option-content`]: {
fontSize: fontSizeSM,
lineHeight: lineHeightSM,
},
[`${antCls}-select-dropdown`]: {
[`${antCls}-select-item`]: {
minHeight: 'auto',
},
},
},
},
[`${componentCls}-input`]: {
gap: marginXXS,
alignItems: 'center',
flex: 1,
width: 0,
[`${componentCls}-hsb-input,${componentCls}-rgb-input`]: {
display: 'flex',
gap: marginXXS,
alignItems: 'center',
},
[`${componentCls}-steppers`]: {
flex: 1,
},
[`${componentCls}-hex-input${antCls}-input-affix-wrapper`]: {
flex: 1,
padding: `0 ${paddingXS}px`,
[`${antCls}-input`]: {
fontSize: fontSizeSM,
lineHeight: `${controlHeightSM - 2 * lineWidth}px`,
},
[`${antCls}-input-prefix`]: {
color: colorTextPlaceholder,
},
},
},
},
};
};
export default genInputStyle;

80
components/color-picker/style/picker.ts

@ -0,0 +1,80 @@
import type { CSSObject } from '@ant-design/cssinjs';
import type { GenerateStyle } from '../../theme/internal';
import type { ColorPickerToken } from './index';
import genColorBlockStyle from './color-block';
const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
const {
componentCls,
controlHeightLG,
borderRadiusSM,
colorPickerInsetShadow,
marginSM,
colorBgElevated,
colorFillSecondary,
lineWidthBold,
colorPickerHandlerSize,
colorPickerHandlerSizeSM,
colorPickerSliderHeight,
colorPickerPreviewSize,
} = token;
return {
[`${componentCls}-select`]: {
[`${componentCls}-palette`]: {
minHeight: controlHeightLG * 4,
overflow: 'hidden',
borderRadius: borderRadiusSM,
},
[`${componentCls}-saturation`]: {
position: 'absolute',
borderRadius: 'inherit',
boxShadow: colorPickerInsetShadow,
inset: 0,
},
marginBottom: marginSM,
},
[`${componentCls}-handler`]: {
width: colorPickerHandlerSize,
height: colorPickerHandlerSize,
border: `${lineWidthBold}px solid ${colorBgElevated}`,
position: 'relative',
borderRadius: '50%',
boxShadow: `${colorPickerInsetShadow}, 0 0 0 1px ${colorFillSecondary}`,
'&-sm': {
width: colorPickerHandlerSizeSM,
height: colorPickerHandlerSizeSM,
},
},
[`${componentCls}-slider`]: {
borderRadius: colorPickerSliderHeight / 2,
[`${componentCls}-palette`]: {
height: colorPickerSliderHeight,
},
[`${componentCls}-gradient`]: {
borderRadius: colorPickerSliderHeight / 2,
boxShadow: colorPickerInsetShadow,
},
'&-alpha': {
backgroundSize: colorPickerSliderHeight * 2,
backgroundImage:
'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAFpJREFUWAntljEKADAIA23p6v//qQ+wfUEcCu1yriEgp0FHRJSJcnehmmWm1Dv/lO4HIg1AAAKjTqm03ea88zMCCEDgO4HV5bS757f+7wRoAAIQ4B9gByAAgQ3pfiDmXmAeEwAAAABJRU5ErkJggg==")',
},
marginBottom: marginSM,
},
[`${componentCls}-slider-container`]: {
display: 'flex',
gap: marginSM,
[`${componentCls}-slider-group`]: {
flex: 1,
},
},
...genColorBlockStyle(token, colorPickerPreviewSize),
};
};
export default genPickerStyle;

113
components/color-picker/style/presets.ts

@ -0,0 +1,113 @@
import type { CSSObject } from '@ant-design/cssinjs';
import type { GenerateStyle } from '../../theme/internal';
import type { ColorPickerToken } from './index';
const genPresetsStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
const {
componentCls,
antCls,
colorTextQuaternary,
paddingXXS,
colorPickerPresetColorSize,
fontSizeSM,
colorText,
lineHeightSM,
lineWidth,
borderRadius,
colorFill,
colorWhite,
colorTextTertiary,
marginXXS,
paddingXS,
} = token;
return {
[`${componentCls}-presets`]: {
[`${antCls}-collapse-item > ${antCls}-collapse-header`]: {
padding: 0,
[`${antCls}-collapse-expand-icon`]: {
height: fontSizeSM * lineHeightSM,
color: colorTextQuaternary,
paddingInlineEnd: paddingXXS,
},
},
[`${antCls}-collapse`]: {
display: 'flex',
flexDirection: 'column',
gap: marginXXS,
},
[`${antCls}-collapse-item > ${antCls}-collapse-content > ${antCls}-collapse-content-box`]: {
padding: `${paddingXS}px 0`,
},
'&-label': {
fontSize: fontSizeSM,
color: colorText,
lineHeight: lineHeightSM,
},
'&-items': {
display: 'flex',
flexWrap: 'wrap',
gap: marginXXS * 1.5,
[`${componentCls}-presets-color`]: {
position: 'relative',
cursor: 'pointer',
width: colorPickerPresetColorSize,
height: colorPickerPresetColorSize,
'&::before': {
content: '""',
pointerEvents: 'none',
width: colorPickerPresetColorSize + 4 * lineWidth,
height: colorPickerPresetColorSize + 4 * lineWidth,
position: 'absolute',
top: -2 * lineWidth,
insetInlineStart: -2 * lineWidth,
borderRadius,
border: `${lineWidth}px solid transparent`,
transition: `border-color ${token.motionDurationMid} ${token.motionEaseInBack}`,
},
'&:hover::before': {
borderColor: colorFill,
},
'&::after': {
boxSizing: 'border-box',
position: 'absolute',
top: '50%',
insetInlineStart: '21.5%',
display: 'table',
width: (colorPickerPresetColorSize / 13) * 5,
height: (colorPickerPresetColorSize / 13) * 8,
border: `${token.lineWidthBold}px solid ${token.colorWhite}`,
borderTop: 0,
borderInlineStart: 0,
transform: 'rotate(45deg) scale(0) translate(-50%,-50%)',
opacity: 0,
content: '""',
transition: `all ${token.motionDurationFast} ${token.motionEaseInBack}, opacity ${token.motionDurationFast}`,
},
[`&${componentCls}-presets-color-checked`]: {
'&::after': {
opacity: 1,
borderColor: colorWhite,
transform: 'rotate(45deg) scale(1) translate(-50%,-50%)',
transition: `transform ${token.motionDurationMid} ${token.motionEaseOutBack} ${token.motionDurationFast}`,
},
[`&${componentCls}-presets-color-bright`]: {
'&::after': {
borderColor: colorTextTertiary,
},
},
},
},
},
'&-empty': {
fontSize: fontSizeSM,
color: colorTextQuaternary,
},
},
};
};
export default genPresetsStyle;

20
components/color-picker/util.ts

@ -0,0 +1,20 @@
import type { ColorGenInput } from '@rc-component/color-picker';
import { getRoundNumber } from '@rc-component/color-picker/lib/util';
import type { Color } from './color';
import { ColorFactory } from './color';
export const customizePrefixCls = 'ant-color-picker';
export const generateColor = (color: ColorGenInput<Color>): Color => {
if (color instanceof ColorFactory) {
return color;
}
return new ColorFactory(color);
};
export const getAlphaColor = (color: Color) => getRoundNumber(color.toHsb().a * 100);
export const toHexFormat = (value?: string, alpha?: boolean) =>
value?.replace(/[^\w/]/gi, '').slice(0, alpha ? 8 : 6) || '';
export const getHex = (value?: string, alpha?: boolean) => (value ? toHexFormat(value, alpha) : '');

2
components/index.ts

@ -33,6 +33,8 @@ export { default as Col } from './col';
export type { ColProps } from './col';
export { default as Collapse } from './collapse';
export type { CollapsePanelProps, CollapseProps } from './collapse';
export { default as ColorPicker } from './color-picker';
export type { ColorPickerProps } from './color-picker';
export { default as ConfigProvider } from './config-provider';
export type { ThemeConfig } from './config-provider';
export { default as DatePicker } from './date-picker';

5
components/locale/en_US.ts

@ -1,8 +1,8 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/en_US';
import type { Locale } from '.';
import Calendar from '../calendar/locale/en_US';
import DatePicker from '../date-picker/locale/en_US';
import type { Locale } from '.';
import TimePicker from '../time-picker/locale/en_US';
const typeTemplate = '${label} is not a valid ${type}';
@ -140,6 +140,9 @@ const localeValues: Locale = {
expired: 'QR code expired',
refresh: 'Refresh',
},
ColorPicker: {
presetEmpty: 'Empty',
},
};
export default localeValues;

5
components/locale/index.tsx

@ -4,11 +4,11 @@ import warning from '../_util/warning';
import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker';
import type { TransferLocale as TransferLocaleForEmpty } from '../empty';
import type { ModalLocale } from '../modal/locale';
import type { TourLocale } from '../tour/interface';
import { changeConfirmLocale } from '../modal/locale';
import type { PaginationLocale } from '../pagination/Pagination';
import type { PopconfirmLocale } from '../popconfirm/PurePanel';
import type { TableLocale } from '../table/interface';
import type { TourLocale } from '../tour/interface';
import type { TransferLocale } from '../transfer';
import type { UploadLocale } from '../upload/interface';
import type { LocaleContextProps } from './context';
@ -52,6 +52,9 @@ export interface Locale {
expired: string;
refresh: string;
};
ColorPicker?: {
presetEmpty: string;
};
}
export interface LocaleProviderProps {

5
components/locale/zh_CN.ts

@ -1,8 +1,8 @@
/* eslint-disable no-template-curly-in-string */
import Pagination from 'rc-pagination/lib/locale/zh_CN';
import type { Locale } from '.';
import Calendar from '../calendar/locale/zh_CN';
import DatePicker from '../date-picker/locale/zh_CN';
import type { Locale } from '.';
import TimePicker from '../time-picker/locale/zh_CN';
const typeTemplate = '${label}不是一个有效的${type}';
@ -140,6 +140,9 @@ const localeValues: Locale = {
expired: '二维码过期',
refresh: '点击刷新',
},
ColorPicker: {
presetEmpty: '暂无',
},
};
export default localeValues;

16
components/theme/interface/components.ts

@ -1,20 +1,23 @@
import type { ComponentToken as WaveToken } from '../../_util/wave/style';
import type { ComponentToken as AlertComponentToken } from '../../alert/style';
import type { ComponentToken as AnchorComponentToken } from '../../anchor/style';
import type { ComponentToken as AppComponentToken } from '../../app/style';
import type { ComponentToken as AvatarComponentToken } from '../../avatar/style';
import type { ComponentToken as BackTopComponentToken } from '../../back-top/style';
import type { ComponentToken as ButtonComponentToken } from '../../button/style';
import type { ComponentToken as FloatButtonComponentToken } from '../../float-button/style';
import type { ComponentToken as CalendarComponentToken } from '../../calendar/style';
import type { ComponentToken as CardComponentToken } from '../../card/style';
import type { ComponentToken as CarouselComponentToken } from '../../carousel/style';
import type { ComponentToken as CascaderComponentToken } from '../../cascader/style';
import type { ComponentToken as CheckboxComponentToken } from '../../checkbox/style';
import type { ComponentToken as CollapseComponentToken } from '../../collapse/style';
import type { ComponentToken as ColorPickerComponentToken } from '../../color-picker/style';
import type { ComponentToken as DatePickerComponentToken } from '../../date-picker/style';
import type { ComponentToken as DividerComponentToken } from '../../divider/style';
import type { ComponentToken as DropdownComponentToken } from '../../dropdown/style';
import type { ComponentToken as DrawerComponentToken } from '../../drawer/style';
import type { ComponentToken as DropdownComponentToken } from '../../dropdown/style';
import type { ComponentToken as EmptyComponentToken } from '../../empty/style';
import type { ComponentToken as FloatButtonComponentToken } from '../../float-button/style';
import type { ComponentToken as ImageComponentToken } from '../../image/style';
import type { ComponentToken as InputNumberComponentToken } from '../../input-number/style';
import type { ComponentToken as LayoutComponentToken } from '../../layout/style';
@ -27,6 +30,7 @@ import type { ComponentToken as NotificationComponentToken } from '../../notific
import type { ComponentToken as PopconfirmComponentToken } from '../../popconfirm/style';
import type { ComponentToken as PopoverComponentToken } from '../../popover/style';
import type { ComponentToken as ProgressComponentToken } from '../../progress/style';
import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
import type { ComponentToken as RadioComponentToken } from '../../radio/style';
import type { ComponentToken as RateComponentToken } from '../../rate/style';
import type { ComponentToken as ResultComponentToken } from '../../result/style';
@ -36,20 +40,17 @@ import type { ComponentToken as SkeletonComponentToken } from '../../skeleton/st
import type { ComponentToken as SliderComponentToken } from '../../slider/style';
import type { ComponentToken as SpaceComponentToken } from '../../space/style';
import type { ComponentToken as SpinComponentToken } from '../../spin/style';
import type { ComponentToken as StatisticComponentToken } from '../../statistic/style';
import type { ComponentToken as StepsComponentToken } from '../../steps/style';
import type { ComponentToken as TableComponentToken } from '../../table/style';
import type { ComponentToken as TabsComponentToken } from '../../tabs/style';
import type { ComponentToken as TagComponentToken } from '../../tag/style';
import type { ComponentToken as TimelineComponentToken } from '../../timeline/style';
import type { ComponentToken as TooltipComponentToken } from '../../tooltip/style';
import type { ComponentToken as TourComponentToken } from '../../tour/style';
import type { ComponentToken as TransferComponentToken } from '../../transfer/style';
import type { ComponentToken as TypographyComponentToken } from '../../typography/style';
import type { ComponentToken as UploadComponentToken } from '../../upload/style';
import type { ComponentToken as TourComponentToken } from '../../tour/style';
import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
import type { ComponentToken as AppComponentToken } from '../../app/style';
import type { ComponentToken as StatisticComponentToken } from '../../statistic/style';
import type { ComponentToken as WaveToken } from '../../_util/wave/style';
export interface ComponentTokenMap {
Affix?: {};
@ -64,6 +65,7 @@ export interface ComponentTokenMap {
Carousel?: CarouselComponentToken;
Cascader?: CascaderComponentToken;
Checkbox?: CheckboxComponentToken;
ColorPicker?: ColorPickerComponentToken;
Collapse?: CollapseComponentToken;
DatePicker?: DatePickerComponentToken;
Descriptions?: {};

1
package.json

@ -113,6 +113,7 @@
"@ant-design/react-slick": "~1.0.0",
"@babel/runtime": "^7.18.3",
"@ctrl/tinycolor": "^3.6.0",
"@rc-component/color-picker": "^1.0.0-alpha.11",
"@rc-component/mutate-observer": "^1.0.0",
"@rc-component/tour": "~1.8.0",
"@rc-component/trigger": "^1.12.0",

1
tests/__snapshots__/index.test.ts.snap

@ -19,6 +19,7 @@ exports[`antd dist files exports modules correctly 1`] = `
"Checkbox",
"Col",
"Collapse",
"ColorPicker",
"ConfigProvider",
"DatePicker",
"Descriptions",

Loading…
Cancel
Save