diff --git a/script/test/mocks/ResizeObserverMock.js b/script/test/mocks/ResizeObserverMock.js new file mode 100644 index 000000000..2ebede27a --- /dev/null +++ b/script/test/mocks/ResizeObserverMock.js @@ -0,0 +1,16 @@ +import { vi } from 'vitest'; + +class ResizeObserverMock { + constructor(callback) { + this.callback = callback; + this.observe = vi.fn(); + this.unobserve = vi.fn(); + this.disconnect = vi.fn(); + } + + trigger(entries) { + this.callback(entries); + } +} + +export default ResizeObserverMock; diff --git a/script/test/mocks/Touch.js b/script/test/mocks/Touch.js new file mode 100644 index 000000000..e982102ba --- /dev/null +++ b/script/test/mocks/Touch.js @@ -0,0 +1,18 @@ +class Touch { + constructor(init) { + this.identifier = init.identifier; + this.target = init.target; + this.clientX = init.clientX; + this.clientY = init.clientY; + this.pageX = init.pageX; + this.pageY = init.pageY; + this.screenX = init.screenX; + this.screenY = init.screenY; + this.radiusX = init.radiusX; + this.radiusY = init.radiusY; + this.rotationAngle = init.rotationAngle; + this.force = init.force; + } +} + +export default Touch; diff --git a/script/test/setup.js b/script/test/setup.js new file mode 100644 index 000000000..39c3c7f38 --- /dev/null +++ b/script/test/setup.js @@ -0,0 +1,8 @@ +import { vi } from 'vitest'; +import ResizeObserverMock from './mocks/ResizeObserverMock.js'; +import Touch from './mocks/Touch.js'; + +global.Touch = Touch; + +vi.stubGlobal('ResizeObserver', ResizeObserverMock); +vi.stubGlobal('Touch', Touch); diff --git a/src/color-picker/ColorPicker.tsx b/src/color-picker/ColorPicker.tsx index 1c5b0eea6..a6a4e9bb2 100644 --- a/src/color-picker/ColorPicker.tsx +++ b/src/color-picker/ColorPicker.tsx @@ -21,10 +21,8 @@ export interface ColorPickerProps extends TdColorPickerProps, StyledProps {} const ColorPicker: FC = (props) => { const { format, type, enableAlpha, swatchColors, style, value, defaultValue, fixed, onChange, onPaletteBarChange } = useDefaultProps(props, colorPickerDefaultProps); - const [formatList, setFormatList] = useState([]); + const [formatList, setFormatList] = useState<[string, Array]>(['', []]); const [innerSwatchList, setInnerSwatchList] = useState([]); - const [showPrimaryColorPreview] = useState(false); - const [previewColor, setPreviewColor] = useState(''); const [sliderInfo, setSliderInfo] = useState(0); const [panelRect, setPanelRect] = useState({ width: SATURATION_PANEL_DEFAULT_WIDTH, @@ -60,9 +58,7 @@ const ColorPicker: FC = (props) => { const getSliderThumbStyle = useCallback( ({ value, maxValue }) => { const { width } = sliderRect; - if (!width) { - return; - } + if (!width) return; const left = Math.round((value / maxValue) * 100); return { left: `${left}%`, @@ -96,7 +92,6 @@ const ColorPicker: FC = (props) => { value: color.current.value, }), ); - setPreviewColor(color.current.rgba); setFormatList(getFormatList(format, color.current)); }, [getSaturationThumbStyle, getSliderThumbStyle], @@ -162,10 +157,6 @@ const ColorPicker: FC = (props) => { color.current = new Color(value || DEFAULT_COLOR); }, [value]); - useEffect(() => { - setPreviewColor(value); - }, [value]); - useEffect(() => { setCoreStyle(format); }, [format, setCoreStyle]); @@ -210,13 +201,7 @@ const ColorPicker: FC = (props) => { const { x } = coordinate; const maxValue = isAlpha ? ALPHA_MAX : HUE_MAX; - let value = Math.round((x / width) * maxValue * 100) / 100; - if (value < 0) { - value = 0; - } - if (value > maxValue) { - value = maxValue; - } + const value = Math.min(maxValue, Math.max(0, Math.round((x / width) * maxValue * 100) / 100)); handleChangeSlider({ value, isAlpha }); } @@ -249,8 +234,6 @@ const ColorPicker: FC = (props) => { case 'alpha-slider': handleSliderDrag(e, true); break; - default: - break; } } @@ -286,12 +269,6 @@ const ColorPicker: FC = (props) => { }; const renderPicker = () => { - const renderPreviewColorContent = () => ( -
-
-
- ); - const renderAlphaContent = () => (
= (props) => { } : { top: `${saturationThumbStyle.top}`, left: `${saturationThumbStyle.left}` } } - onTouchStart={(e) => { - e.stopPropagation(); - }} - onTouchMove={(e) => { - e.stopPropagation(); - }} - onTouchEnd={(e) => { - e.stopPropagation(); - }} />
@@ -368,15 +336,14 @@ const ColorPicker: FC = (props) => {
{enableAlpha ? renderAlphaContent() : null}
- {showPrimaryColorPreview ? renderPreviewColorContent() : null}
- {format} + {formatList[0]}
- {formatList.map((item, index) => ( + {formatList[1]?.map((item, index) => (
{ + const touchInit = { + changedTouches: [ + new Touch({ + identifier: 0, + target: el, + clientX: touchPosition?.clientX || 0, + clientY: touchPosition?.clientY || 0, + pageX: touchPosition?.pageX || 0, + pageY: touchPosition?.pageY || 0, + }), + ], + bubbles: true, + cancelable: true, + }; + + const event = new TouchEvent(eventName, touchInit); + el.dispatchEvent(event); +}; + +const mockBoundingClientRect = (info) => { + window.HTMLElement.prototype.getBoundingClientRect = () => info; +}; + +const renderColorPicker = (props: ColorPickerProps) => render(); + +describe('ColorPicker', () => { + describe('props', () => { + it(': multiple', () => { + const testCurrentProp = (type: TypeEnum, target: number) => { + const { container } = renderColorPicker({ type }); + const dom = container.querySelectorAll(`${name}__saturation`); + expect(dom).toHaveLength(target); + }; + testCurrentProp(undefined, 0); + testCurrentProp('base', 0); + testCurrentProp('multiple', 1); + }); + + it(': enableAlpha', () => { + const testEnableAlpha = (enableAlpha: boolean) => { + const { container } = renderColorPicker({ enableAlpha, type: 'multiple' }); + const alphaDom = container.querySelectorAll(`${name}__slider-wrapper--alpha-type`); + expect(alphaDom).toHaveLength(enableAlpha ? 1 : 0); + }; + testEnableAlpha(false); + testEnableAlpha(true); + }); + + it(': swatchColors', () => { + const testSwatchColors = (swatchColors: Array | null, target: number) => { + const { container } = renderColorPicker({ swatchColors }); + const dom = container.querySelectorAll(`${name}__swatches-item`); + expect(dom).toHaveLength(target); + }; + testSwatchColors(null, 0); + testSwatchColors([], 0); + testSwatchColors(undefined, 10); + testSwatchColors(['red', 'blur'], 2); + }); + + it(': format', () => { + const testFormat = (format: string, target: ColorFormat) => { + const { container } = renderColorPicker({ format: format as ColorFormat, type: 'multiple' }); + const dom = container.querySelector(`${name}__format-item--first`); + expect(dom.innerHTML).toBe(target); + }; + testFormat('RGB', 'RGB'); + testFormat('123', 'RGB'); + testFormat('HEX', 'HEX'); + }); + }); + + describe('events', () => { + it(': preset change', async () => { + const onChange = vi.fn(); + const { container } = renderColorPicker({ onChange }); + const swatch = container.querySelector(`${name}__swatches-item`); + + fireEvent.click(swatch); + const result = 'rgb(236, 242, 254)'; + + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith(result, { + trigger: 'preset', + color: getColorObject(new Color(result)), + }); + }); + + it(': saturation change', async () => { + const testSaturation = async (fixed = false) => { + const onPaletteBarChange = vi.fn(); + const { container } = renderColorPicker({ onPaletteBarChange, type: 'multiple', fixed }); + const el = container.querySelector('.t-color-picker__saturation'); + + mockBoundingClientRect({ + left: 0, + top: 0, + width: 300, + height: 50, + }); + + act(() => { + makeTouch(el, 'touchstart'); + makeTouch(el, 'touchmove', { pageY: 40, pageX: 0, clientY: 40 }); + makeTouch(el, 'touchmove', { pageY: 40, pageX: 0, clientY: 40 }); + makeTouch(el, 'touchmove', { pageY: 30, pageX: 0, clientY: 30 }); + makeTouch(el, 'touchend', { pageY: 30, pageX: 30, clientY: 20 }); + }); + + expect(onPaletteBarChange).toHaveBeenCalledTimes(3); + const result = 'rgba(80, 80, 80, 1)'; + const color = new Color(result); + color.saturation = 0; + color.value = fixed ? 0.4 : 0.8214285714285714; + + expect(onPaletteBarChange).toHaveBeenLastCalledWith({ + color: getColorObject(color), + }); + }; + + await testSaturation(); + await testSaturation(true); + }); + + it(': hue slider change', async () => { + const onChange = vi.fn(); + const { container } = renderColorPicker({ onChange, type: 'multiple' }); + const el = container.querySelector('.t-color-picker__slider'); + + mockBoundingClientRect({ + left: 0, + top: 0, + width: 300, + height: 50, + }); + + act(() => { + makeTouch(el, 'touchstart', { pageY: 0, pageX: 0, clientY: 30 }); + makeTouch(el, 'touchmove', { pageY: 0, pageX: 30, clientY: 30 }); + makeTouch(el, 'touchend', { pageY: 30, pageX: 40, clientY: 30 }); + }); + + expect(onChange).toHaveBeenCalledTimes(2); + const result = 'rgb(151, 91, 0)'; + expect(onChange).toHaveBeenLastCalledWith(result, { + trigger: 'palette-hue-bar', + color: getColorObject(new Color(result)), + }); + }); + + it(': alpha slider change', async () => { + const onChange = vi.fn(); + const { container } = renderColorPicker({ onChange, type: 'multiple', enableAlpha: true }); + const el = container.querySelector('.t-color-picker__slider-wrapper--alpha-type .t-color-picker__slider'); + + mockBoundingClientRect({ + left: 0, + top: 0, + width: 300, + height: 50, + }); + act(() => { + makeTouch(el, 'touchstart', { pageY: 0, pageX: 0, clientY: 30 }); + makeTouch(el, 'touchmove', { pageY: 0, pageX: 40, clientY: 30 }); + makeTouch(el, 'touchend', { pageY: 30, pageX: 40, clientY: 30 }); + }); + + expect(onChange).toHaveBeenCalledTimes(2); + const result = 'rgb(0, 31, 151)'; + const color = new Color(result); + color.alpha = 0.13; + expect(onChange).toHaveBeenLastCalledWith(result, { + trigger: 'palette-alpha-bar', + color: getColorObject(color), + }); + }); + }); +}); diff --git a/src/color-picker/helper/format.ts b/src/color-picker/helper/format.ts index d4a02781e..4594b1194 100644 --- a/src/color-picker/helper/format.ts +++ b/src/color-picker/helper/format.ts @@ -13,7 +13,7 @@ export const getCoordinate = (e: TouchEvent, rect: PanelRectType, isFixed?: bool }; }; -export const getFormatList = (format: ColorPickerProps['format'], color: Color) => { +export const getFormatList = (format: ColorPickerProps['format'], color: Color): [string, Array] => { const FORMAT_MAP = { HSV: Object.values(color.getHsva()), HSVA: Object.values(color.getHsva()), @@ -32,9 +32,9 @@ export const getFormatList = (format: ColorPickerProps['format'], color: Color) const cur = FORMAT_MAP[format]; if (cur) { - return [...cur.slice(0, cur.length - 1), `${Math.round(color.alpha * 100)}%`]; + return [format, [...cur.slice(0, cur.length - 1), `${Math.round(color.alpha * 100)}%`]]; } - return FORMAT_MAP.RGB; + return ['RGB', FORMAT_MAP.RGB]; }; export const genSwatchList = (prop: ColorPickerProps['swatchColors']) => { diff --git a/src/color-picker/type.ts b/src/color-picker/type.ts index 9472f31e2..372deee19 100644 --- a/src/color-picker/type.ts +++ b/src/color-picker/type.ts @@ -62,6 +62,8 @@ export type TypeEnum = 'base' | 'multiple'; export type ColorPickerChangeTrigger = 'palette-hue-bar' | 'palette-alpha-bar' | 'preset'; +export type ColorFormat = 'RGB' | 'RGBA' | 'HSL' | 'HSLA' | 'HSB' | 'HSV' | 'HSVA' | 'HEX' | 'CMYK' | 'CSS'; + export type ColorPickerTrigger = 'overlay'; export interface ColorObject { diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index b718f4356..550d5cf46 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -31868,7 +31868,7 @@ exports[`csr snapshot test > csr test src/color-picker/_example/index.tsx 1`] = 0
31
@@ -32073,7 +32073,7 @@ exports[`csr snapshot test > csr test src/color-picker/_example/index.tsx 1`] = 0
31
@@ -32544,7 +32544,7 @@ exports[`csr snapshot test > csr test src/color-picker/_example/multiple.tsx 1`] 0
31
@@ -32746,7 +32746,7 @@ exports[`csr snapshot test > csr test src/color-picker/_example/usePopup.tsx 1`] 0
31
@@ -121430,13 +121430,13 @@ exports[`ssr snapshot test > ssr test src/collapse/_example/placement.tsx 1`] = exports[`ssr snapshot test > ssr test src/color-picker/_example/base.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/format.tsx 1`] = `"
CSS
HEX
RGB
HSL
HSV
CMYK
CSS
"`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/format.tsx 1`] = `"
CSS
HEX
RGB
HSL
HSV
CMYK
"`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/index.tsx 1`] = `"

ColorPicker 颜色选择器

用于颜色选择,支持多种格式。

01 类型

RGB

02 组件状态

组件模式选择

CSS
HEX
RGB
HSL
HSV
CMYK
CSS
"`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/index.tsx 1`] = `"

ColorPicker 颜色选择器

用于颜色选择,支持多种格式。

01 类型

02 组件状态

组件模式选择

CSS
HEX
RGB
HSL
HSV
CMYK
"`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/multiple.tsx 1`] = `"
RGB
"`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/multiple.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/usePopup.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/usePopup.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test src/config-provider/_example/index.tsx 1`] = `"

ConfigProvider 全局配置

全局特性配置包含各个组件的文本语言配置及其他通用配置,可以减少重复的通用配置。

Upload

Table

Type
Platform
Property
暂无数据

其他组件

Rating
3 score
Single select date
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index a6a5ee090..634af86dc 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -134,13 +134,13 @@ exports[`ssr snapshot test > ssr test src/collapse/_example/placement.tsx 1`] = exports[`ssr snapshot test > ssr test src/color-picker/_example/base.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/format.tsx 1`] = `"
CSS
HEX
RGB
HSL
HSV
CMYK
CSS
"`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/format.tsx 1`] = `"
CSS
HEX
RGB
HSL
HSV
CMYK
"`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/index.tsx 1`] = `"

ColorPicker 颜色选择器

用于颜色选择,支持多种格式。

01 类型

RGB

02 组件状态

组件模式选择

CSS
HEX
RGB
HSL
HSV
CMYK
CSS
"`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/index.tsx 1`] = `"

ColorPicker 颜色选择器

用于颜色选择,支持多种格式。

01 类型

02 组件状态

组件模式选择

CSS
HEX
RGB
HSL
HSV
CMYK
"`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/multiple.tsx 1`] = `"
RGB
"`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/multiple.tsx 1`] = `"
"`; -exports[`ssr snapshot test > ssr test src/color-picker/_example/usePopup.tsx 1`] = `"
"`; +exports[`ssr snapshot test > ssr test src/color-picker/_example/usePopup.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test src/config-provider/_example/index.tsx 1`] = `"

ConfigProvider 全局配置

全局特性配置包含各个组件的文本语言配置及其他通用配置,可以减少重复的通用配置。

Upload

Table

Type
Platform
Property
暂无数据

其他组件

Rating
3 score
Single select date
"`; diff --git a/vitest.config.mts b/vitest.config.mts index 6a61cd417..30f29d575 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -22,6 +22,7 @@ const testConfig: InlineConfig = { reportsDirectory: 'test/coverage', include: ['src/**/*'], }, + setupFiles: path.resolve(__dirname, `./script/test/setup.js`), }; export default defineConfig({