Browse Source
* 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
53 changed files with 6716 additions and 10 deletions
@ -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; |
@ -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; |
File diff suppressed because it is too large
@ -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> |
|||
`; |
@ -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> |
|||
`; |
@ -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); |
|||
}); |
|||
}); |
@ -0,0 +1,3 @@ |
|||
import { extendTest } from '../../../tests/shared/demoTest'; |
|||
|
|||
extendTest('color-picker'); |
@ -0,0 +1,3 @@ |
|||
import demoTest from '../../../tests/shared/demoTest'; |
|||
|
|||
demoTest('color-picker'); |
@ -0,0 +1,5 @@ |
|||
import { imageDemoTest } from '../../../tests/shared/imageTest'; |
|||
|
|||
describe('ColorPicker image', () => { |
|||
imageDemoTest('color-picker'); |
|||
}); |
@ -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);'); |
|||
}); |
|||
}); |
@ -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(); |
|||
} |
|||
} |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -0,0 +1,7 @@ |
|||
## zh-CN |
|||
|
|||
清除已选择的颜色。 |
|||
|
|||
## en-US |
|||
|
|||
Clear Color. |
@ -0,0 +1,4 @@ |
|||
import { ColorPicker } from 'antd'; |
|||
import React from 'react'; |
|||
|
|||
export default () => <ColorPicker allowClear />; |
@ -0,0 +1,7 @@ |
|||
## zh-CN |
|||
|
|||
最简单的使用方法。 |
|||
|
|||
## en-US |
|||
|
|||
Basic Usage. |
@ -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} />; |
|||
}; |
@ -0,0 +1,7 @@ |
|||
## zh-CN |
|||
|
|||
设置为禁用状态。 |
|||
|
|||
## en-US |
|||
|
|||
Set to disabled state. |
@ -0,0 +1,4 @@ |
|||
import { ColorPicker } from 'antd'; |
|||
import React from 'react'; |
|||
|
|||
export default () => <ColorPicker disabled />; |
@ -0,0 +1,7 @@ |
|||
## zh-CN |
|||
|
|||
编码格式,支持`HEX`、`HSB`、`RGB`。 |
|||
|
|||
## en-US |
|||
|
|||
Encoding formats, support `HEX`, `HSB`, `RGB`. |
@ -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> |
|||
); |
|||
}; |
@ -0,0 +1,7 @@ |
|||
## zh-CN |
|||
|
|||
设置颜色选择器的预设颜色。 |
|||
|
|||
## en-US |
|||
|
|||
Set the presets color of the color picker. |
@ -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: [], |
|||
}, |
|||
]} |
|||
/> |
|||
); |
@ -0,0 +1,7 @@ |
|||
## zh-CN |
|||
|
|||
Pure Panel |
|||
|
|||
## en-US |
|||
|
|||
Pure Panel |
@ -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} />; |
|||
}; |
@ -0,0 +1,7 @@ |
|||
## zh-CN |
|||
|
|||
自定义颜色面板的触发器。 |
|||
|
|||
## en-US |
|||
|
|||
Triggers for customizing color panels. |
@ -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> |
|||
); |
|||
}; |
@ -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; |
@ -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. |
@ -0,0 +1,6 @@ |
|||
import ColorPicker from './ColorPicker'; |
|||
|
|||
export type { Color } from './color'; |
|||
export type { ColorPickerProps } from './ColorPicker'; |
|||
|
|||
export default ColorPicker; |
@ -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` 对象来进行赋值操作,这样可以避免精度问题,保证取值是精准的,选择器也可以按照预期工作。 |
@ -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']; |
|||
} |
@ -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; |
@ -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)]; |
|||
}); |
@ -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; |
@ -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; |
@ -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; |
@ -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) : ''); |
Loading…
Reference in new issue