From e4b4e83d3c017636eaceb57a67b0c0efac2769f7 Mon Sep 17 00:00:00 2001 From: Liam Fan Date: Fri, 10 Jul 2026 01:53:13 -0500 Subject: [PATCH 1/3] feat(ColorPicker): support eyedropper color picking --- .../color-picker-panel.test.tsx.snap | 3 + .../__tests__/color-picker.test.tsx | 121 +++++++++++++++++- .../color-picker/_example/eye-dropper.vue | 15 +++ .../components/color-picker/_usage/props.json | 6 + .../color-picker/color-picker-panel-props.ts | 2 + .../color-picker/color-picker.en-US.md | 10 +- .../components/color-picker/color-picker.md | 10 +- .../color-picker/components/panel/header.tsx | 64 +++++++-- .../color-picker/components/panel/index.tsx | 22 +++- packages/components/color-picker/props.ts | 2 + packages/components/color-picker/type.ts | 13 +- .../color-picker/utils/eyedropper.ts | 42 ++++++ 12 files changed, 289 insertions(+), 21 deletions(-) create mode 100644 packages/components/color-picker/_example/eye-dropper.vue create mode 100644 packages/components/color-picker/utils/eyedropper.ts diff --git a/packages/components/color-picker/__tests__/__snapshots__/color-picker-panel.test.tsx.snap b/packages/components/color-picker/__tests__/__snapshots__/color-picker-panel.test.tsx.snap index 5150da8729..abb426210c 100644 --- a/packages/components/color-picker/__tests__/__snapshots__/color-picker-panel.test.tsx.snap +++ b/packages/components/color-picker/__tests__/__snapshots__/color-picker-panel.test.tsx.snap @@ -73,6 +73,7 @@ exports[`ColorPickerPanel > :props > :colorModes[monochrome,linear-gradient] 1`] /> +
:props > :colorModes[monochrome,linear-gradient] 3`] />
+
:ui > :mount 1`] = ` />
+
{ expect(sliderNode2.element.children.length).toBe(2); }); + describe(':eyeDropper[boolean]', () => { + const stubEyeDropper = (hex = '#ff6600') => { + vi.stubGlobal( + 'EyeDropper', + class { + // eslint-disable-next-line class-methods-use-this + open() { + return Promise.resolve({ sRGBHex: hex }); + } + }, + ); + }; + + afterEach(() => { + vi.unstubAllGlobals(); + // 清理本组用例挂载的 Popup 浮层,避免残留 DOM 遮挡后续用例的 trigger + document.body.innerHTML = ''; + }); + + it('does not render eyedropper button by default', async () => { + stubEyeDropper(); + const { panel } = await mountColorPickerAndTriggerPanel({ props: { value: '#0052d9' } }); + expect(panel.find('.t-color-picker__eyedropper').exists()).toBeFalsy(); + }); + + it('picks color and emits change with trigger eyedropper', async () => { + stubEyeDropper('#ff6600'); + const onChange = vi.fn(); + const { panel } = await mountColorPickerAndTriggerPanel({ + props: { value: '#0052d9', eyeDropper: true, onChange }, + }); + const btn = panel.find('.t-color-picker__eyedropper'); + expect(btn.exists()).toBeTruthy(); + expect(btn.classes()).not.toContain('t-is-disabled'); + await btn.trigger('click'); + await nextTick(); + expect(onChange).toHaveBeenCalledTimes(1); + const [, context] = onChange.mock.calls[0]; + expect(context.trigger).toBe('eyedropper'); + expect(context.color.hex).toBe('#ff6600'); + }); + + it('renders disabled button when EyeDropper API is not supported', async () => { + // jsdom 默认无 EyeDropper 全局对象 + const onChange = vi.fn(); + const { panel } = await mountColorPickerAndTriggerPanel({ + props: { value: '#0052d9', eyeDropper: true, onChange }, + }); + const btn = panel.find('.t-color-picker__eyedropper'); + expect(btn.exists()).toBeTruthy(); + expect(btn.classes()).toContain('t-is-disabled'); + expect((btn.element as HTMLButtonElement).disabled).toBe(true); + await btn.trigger('click'); + await nextTick(); + expect(onChange).not.toHaveBeenCalled(); + }); + + it('keeps current alpha when enableAlpha is true', async () => { + stubEyeDropper('#ff6600'); + const onChange = vi.fn(); + const { panel } = await mountColorPickerAndTriggerPanel({ + props: { value: 'rgba(0, 82, 217, 0.5)', format: 'RGBA', enableAlpha: true, eyeDropper: true, onChange }, + }); + await panel.find('.t-color-picker__eyedropper').trigger('click'); + await nextTick(); + const [, context] = onChange.mock.calls[0]; + expect(context.color.hex).toBe('#ff6600'); + expect(context.color.alpha).toBe(0.5); + }); + + it('renders button even when only one color mode', async () => { + stubEyeDropper(); + const { panel } = await mountColorPickerAndTriggerPanel({ + props: { value: '#0052d9', colorModes: ['monochrome'], eyeDropper: true }, + }); + expect(panel.find('.t-radio-group').exists()).toBeFalsy(); + expect(panel.find('.t-color-picker__eyedropper').exists()).toBeTruthy(); + }); + + it('does not emit change when user cancels picking', async () => { + vi.stubGlobal( + 'EyeDropper', + class { + // eslint-disable-next-line class-methods-use-this + open() { + return Promise.reject(new DOMException('User aborted', 'AbortError')); + } + }, + ); + const onChange = vi.fn(); + const { panel } = await mountColorPickerAndTriggerPanel({ + props: { value: '#0052d9', eyeDropper: true, onChange }, + }); + await panel.find('.t-color-picker__eyedropper').trigger('click'); + await nextTick(); + expect(onChange).not.toHaveBeenCalled(); + }); + + it('updates only the selected gradient stop in gradient mode', async () => { + stubEyeDropper('#ff6600'); + const onChange = vi.fn(); + const { panel } = await mountColorPickerAndTriggerPanel({ + props: { + value: 'linear-gradient(45deg, #4facfe 0%, #00f2fe 100%)', + colorModes: ['linear-gradient'], + eyeDropper: true, + onChange, + }, + }); + await panel.find('.t-color-picker__eyedropper').trigger('click'); + await nextTick(); + const [value, context] = onChange.mock.calls[0]; + expect(context.trigger).toBe('eyedropper'); + // 仍为渐变值,仅更新选中的渐变节点,而非把整体颜色替换为单色 + expect(value).toContain('linear-gradient'); + expect(value).toContain('255, 102, 0'); + }); + }); + describe(':format[HEX/HEX8/RGB/RGBA/HSL/HSLA/HSV/HSVA/CMYK/CSS]', async () => { // ps: enableAlpha true: HEX8/RGBA/HSLA/HSVA it('format:validator', () => { diff --git a/packages/components/color-picker/_example/eye-dropper.vue b/packages/components/color-picker/_example/eye-dropper.vue new file mode 100644 index 0000000000..1f7c5c2629 --- /dev/null +++ b/packages/components/color-picker/_example/eye-dropper.vue @@ -0,0 +1,15 @@ + + diff --git a/packages/components/color-picker/_usage/props.json b/packages/components/color-picker/_usage/props.json index 20f5a9ad0b..37134e68bc 100644 --- a/packages/components/color-picker/_usage/props.json +++ b/packages/components/color-picker/_usage/props.json @@ -29,6 +29,12 @@ "defaultValue": true, "options": [] }, + { + "name": "eyeDropper", + "type": "Boolean", + "defaultValue": false, + "options": [] + }, { "name": "size", "type": "enum", diff --git a/packages/components/color-picker/color-picker-panel-props.ts b/packages/components/color-picker/color-picker-panel-props.ts index db17d95655..2a4e69b9f9 100644 --- a/packages/components/color-picker/color-picker-panel-props.ts +++ b/packages/components/color-picker/color-picker-panel-props.ts @@ -22,6 +22,8 @@ export default { type: Boolean, default: true, }, + /** 是否开启吸色(屏幕取色)功能。开启后面板顶部显示吸色按钮,基于浏览器原生 EyeDropper API 实现,浏览器不支持时按钮呈禁用状态 */ + eyeDropper: Boolean, /** 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 */ format: { type: String as PropType, diff --git a/packages/components/color-picker/color-picker.en-US.md b/packages/components/color-picker/color-picker.en-US.md index 5d8a2cbcdb..cdad67a1f9 100644 --- a/packages/components/color-picker/color-picker.en-US.md +++ b/packages/components/color-picker/color-picker.en-US.md @@ -12,6 +12,7 @@ colorModes | Array | ["monochrome", "linear-gradient"] | Typescript:`Array<'mo disabled | Boolean | undefined | \- | N enableAlpha | Boolean | false | \- | N enableMultipleGradient | Boolean | true | \- | N +eyeDropper | Boolean | false | enable screen color sampling. When enabled, an eyedropper button appears in the panel header, implemented with the browser's native EyeDropper API. The button is disabled when the browser does not support the API | N format | String | RGB | When `enableAlpha` is true, `HEX8/RGBA/HSLA/HSVA` are valid。options: HEX/HEX8/RGB/RGBA/HSL/HSLA/HSV/HSVA/CMYK/CSS | N inputProps | Object | - | Typescript:`InputProps`,[Input API Documents](./input?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts) | N popupProps | Object | - | Typescript:`PopupProps`,[Popup API Documents](./popup?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts) | N @@ -23,7 +24,7 @@ size | String | medium | options: small/medium/large。Typescript:`SizeEnum` swatchColors | Array | undefined | swatch colors。Typescript:`Array \| null \| undefined` | N value | String | - | color value。`v-model` and `v-model:value` is supported | N defaultValue | String | - | color value。uncontrolled property | N -onChange | Function | | Typescript:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' `
| N +onChange | Function | | Typescript:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' \| 'eyedropper' `
| N onClear | Function | | Typescript:`(context: { e: MouseEvent }) => void`
| N onPaletteBarChange | Function | | Typescript:`(context: { color: ColorObject }) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
| N onRecentColorsChange | Function | | Typescript:`(value: Array) => void`
| N @@ -32,7 +33,7 @@ onRecentColorsChange | Function | | Typescript:`(value: Array) => voi name | params | description -- | -- | -- -change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' `
+change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' \| 'eyedropper' `
clear | `(context: { e: MouseEvent })` | \- palette-bar-change | `(context: { color: ColorObject })` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
recent-colors-change | `(value: Array)` | \- @@ -46,6 +47,7 @@ colorModes | Array | ["monochrome", "linear-gradient"] | Typescript:`Array<'mo disabled | Boolean | - | \- | N enableAlpha | Boolean | false | \- | N enableMultipleGradient | Boolean | true | \- | N +eyeDropper | Boolean | false | enable screen color sampling. When enabled, an eyedropper button appears in the panel header, implemented with the browser's native EyeDropper API. The button is disabled when the browser does not support the API | N format | String | RGB | When `enableAlpha` is true, `HEX8/RGBA/HSLA/HSVA` are valid。options: HEX/HEX8/RGB/RGBA/HSL/HSLA/HSV/HSVA/CMYK/CSS | N recentColors | Array | [] | used color recently。`v-model:recentColors` is supported。Typescript:`Array \| boolean \| null` | N defaultRecentColors | Array | [] | used color recently。uncontrolled property。Typescript:`Array \| boolean \| null` | N @@ -53,7 +55,7 @@ showPrimaryColorPreview | Boolean | true | \- | N swatchColors | Array | - | swatch colors。Typescript:`Array \| null \| undefined` | N value | String | - | color value。`v-model` and `v-model:value` is supported | N defaultValue | String | - | color value。uncontrolled property | N -onChange | Function | | Typescript:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' `
| N +onChange | Function | | Typescript:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' \| 'eyedropper' `
| N onPaletteBarChange | Function | | Typescript:`(context: { color: ColorObject }) => void`
[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
| N onRecentColorsChange | Function | | Typescript:`(value: Array) => void`
| N @@ -61,6 +63,6 @@ onRecentColorsChange | Function | | Typescript:`(value: Array) => voi name | params | description -- | -- | -- -change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' `
+change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' \| 'eyedropper' `
palette-bar-change | `(context: { color: ColorObject })` | [see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
recent-colors-change | `(value: Array)` | \- diff --git a/packages/components/color-picker/color-picker.md b/packages/components/color-picker/color-picker.md index a5b4f8a4f6..a9edae7d39 100644 --- a/packages/components/color-picker/color-picker.md +++ b/packages/components/color-picker/color-picker.md @@ -12,6 +12,7 @@ colorModes | Array | ["monochrome", "linear-gradient"] | 颜色模式选择。 disabled | Boolean | undefined | 是否禁用组件 | N enableAlpha | Boolean | false | 是否开启透明通道 | N enableMultipleGradient | Boolean | true | 是否允许开启通过点击渐变轴增加渐变梯度,默认开启,关闭时只会存在起始和结束两个颜色 | N +eyeDropper | Boolean | false | 是否开启吸色(屏幕取色)功能。开启后面板顶部显示吸色按钮,基于浏览器原生 EyeDropper API 实现,浏览器不支持时按钮呈禁用状态 | N format | String | RGB | 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效。可选项:HEX/HEX8/RGB/RGBA/HSL/HSLA/HSV/HSVA/CMYK/CSS | N inputProps | Object | - | 透传 Input 输入框组件全部属性。TS 类型:`InputProps`,[Input API Documents](./input?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts) | N popupProps | Object | - | 透传 Popup 组件全部属性,如 `placement` `overlayStyle` `overlayClassName` `trigger`等。TS 类型:`PopupProps`,[Popup API Documents](./popup?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts) | N @@ -23,7 +24,7 @@ size | String | medium | 组件尺寸。可选项:small/medium/large。TS 类 swatchColors | Array | undefined | 系统预设的颜色样例,值为 `null` 或 `[]` 则不显示系统色,值为 `undefined` 会显示组件内置的系统默认色。TS 类型:`Array \| null \| undefined` | N value | String | - | 色值。支持语法糖 `v-model` 或 `v-model:value` | N defaultValue | String | - | 色值。非受控属性 | N -onChange | Function | | TS 类型:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' `
| N +onChange | Function | | TS 类型:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' \| 'eyedropper' `
| N onClear | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
清空按钮点击时触发 | N onPaletteBarChange | Function | | TS 类型:`(context: { color: ColorObject }) => void`
调色板控制器的值变化时触发,`context.color` 指调色板控制器的值。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
| N onRecentColorsChange | Function | | TS 类型:`(value: Array) => void`
最近使用颜色发生变化时触发 | N @@ -32,7 +33,7 @@ onRecentColorsChange | Function | | TS 类型:`(value: Array) => void 名称 | 参数 | 描述 -- | -- | -- -change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' `
+change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' \| 'eyedropper' `
clear | `(context: { e: MouseEvent })` | 清空按钮点击时触发 palette-bar-change | `(context: { color: ColorObject })` | 调色板控制器的值变化时触发,`context.color` 指调色板控制器的值。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
recent-colors-change | `(value: Array)` | 最近使用颜色发生变化时触发 @@ -46,6 +47,7 @@ colorModes | Array | ["monochrome", "linear-gradient"] | 颜色模式选择。 disabled | Boolean | - | 是否禁用组件 | N enableAlpha | Boolean | false | 是否开启透明通道 | N enableMultipleGradient | Boolean | true | 是否允许开启通过点击渐变轴增加渐变梯度,默认开启,关闭时只会存在起始和结束两个颜色 | N +eyeDropper | Boolean | false | 是否开启吸色(屏幕取色)功能。开启后面板顶部显示吸色按钮,基于浏览器原生 EyeDropper API 实现,浏览器不支持时按钮呈禁用状态 | N format | String | RGB | 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效。可选项:HEX/HEX8/RGB/RGBA/HSL/HSLA/HSV/HSVA/CMYK/CSS | N recentColors | Array | [] | 最近使用的颜色。值为 [] 表示以组件内部的“最近使用颜色”为准,值长度大于 0 则以该值为准显示“最近使用颜色”。值为 false 或 null 则完全不显示“最近使用颜色”。支持语法糖 `v-model:recentColors`。TS 类型:`Array \| boolean \| null` | N defaultRecentColors | Array | [] | 最近使用的颜色。值为 [] 表示以组件内部的“最近使用颜色”为准,值长度大于 0 则以该值为准显示“最近使用颜色”。值为 false 或 null 则完全不显示“最近使用颜色”。非受控属性。TS 类型:`Array \| boolean \| null` | N @@ -53,7 +55,7 @@ showPrimaryColorPreview | Boolean | true | 是否展示颜色选择条右侧的 swatchColors | Array | - | 系统预设的颜色样例,值为 `null` 或 `[]` 则不显示系统色,值为 `undefined` 会显示组件内置的系统默认色。TS 类型:`Array \| null \| undefined` | N value | String | - | 色值。支持语法糖 `v-model` 或 `v-model:value` | N defaultValue | String | - | 色值。非受控属性 | N -onChange | Function | | TS 类型:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' `
| N +onChange | Function | | TS 类型:`(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => void`
选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' \| 'eyedropper' `
| N onPaletteBarChange | Function | | TS 类型:`(context: { color: ColorObject }) => void`
调色板控制器的值变化时触发,`context.color` 指调色板控制器的值。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
| N onRecentColorsChange | Function | | TS 类型:`(value: Array) => void`
最近使用颜色发生变化时触发 | N @@ -61,6 +63,6 @@ onRecentColorsChange | Function | | TS 类型:`(value: Array) => void 名称 | 参数 | 描述 -- | -- | -- -change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' `
+change | `(value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger })` | 选中的色值发生变化时触发,第一个参数 `value` 表示新色值,`context.color` 表示当前调色板控制器的色值,`context.trigger` 表示触发颜色变化的来源。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`type ColorPickerChangeTrigger = 'palette-saturation-brightness' \| 'palette-saturation' \| 'palette-brightness' \| 'palette-hue-bar' \| 'palette-alpha-bar' \| 'input' \| 'preset' \| 'recent' \| 'eyedropper' `
palette-bar-change | `(context: { color: ColorObject })` | 调色板控制器的值变化时触发,`context.color` 指调色板控制器的值。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/color-picker-panel/type.ts)。
`interface ColorObject { alpha: number; css: string; hex: string; hex8: string; hsl: string; hsla: string; hsv: string; hsva: string; rgb: string; rgba: string; saturation: number; value: number; isGradient: boolean; linearGradient?: string; }`
recent-colors-change | `(value: Array)` | 最近使用颜色发生变化时触发 diff --git a/packages/components/color-picker/components/panel/header.tsx b/packages/components/color-picker/components/panel/header.tsx index 90426793a8..e6375000c5 100644 --- a/packages/components/color-picker/components/panel/header.tsx +++ b/packages/components/color-picker/components/panel/header.tsx @@ -1,10 +1,12 @@ -import { defineComponent, PropType, ref, watch } from 'vue'; +import { defineComponent, onBeforeUnmount, PropType, ref, watch } from 'vue'; +import { SipIcon as TdSipIcon } from 'tdesign-icons-vue-next'; import { COLOR_MODES } from '@tdesign/common-js/color-picker/constants'; -import { useConfig } from '@tdesign/shared-hooks'; +import { useCommonClassName, useConfig, useGlobalIcon } from '@tdesign/shared-hooks'; import { RadioButton as TRadioButton, RadioGroup as TRadioGroup } from '../../../radio'; import props from '../../color-picker-panel-props'; import { useBaseClassName } from '../../hooks'; +import { isEyeDropperSupported, openEyeDropper } from '../../utils/eyedropper'; import type { TdColorModes } from '../../types'; export default defineComponent({ @@ -21,31 +23,73 @@ export default defineComponent({ return () => {}; }, }, + onEyeDropperPick: { + type: Function as PropType<(hex: string) => void>, + default: () => { + return () => {}; + }, + }, }, setup(props) { const { globalConfig } = useConfig('colorPicker'); const baseClassName = useBaseClassName(); + const { SipIcon } = useGlobalIcon({ SipIcon: TdSipIcon }); + const { STATUS } = useCommonClassName(); + const statusClassNames = STATUS.value; const modeValue = ref(props.mode); const handleModeChange = (v: string) => props.onModeChange(v); + + const eyeDropperSupported = isEyeDropperSupported(); + const eyeDropperAbortController = ref(null); + + const handleEyeDropperClick = async () => { + if (!eyeDropperSupported || props.disabled) return; + eyeDropperAbortController.value?.abort(); + eyeDropperAbortController.value = new AbortController(); + const hex = await openEyeDropper({ signal: eyeDropperAbortController.value.signal }); + // 用户取消或吸色失败时返回 null,不触发变更 + if (hex) props.onEyeDropperPick(hex); + }; + + // 组件卸载时中断进行中的吸色,避免对已销毁组件回调 + onBeforeUnmount(() => { + eyeDropperAbortController.value?.abort(); + }); + watch( () => props.mode, (v) => (modeValue.value = v), ); return () => { - if (props.colorModes?.length === 1) { + const showModeSelect = props.colorModes?.length > 1; + if (!showModeSelect && !props.eyeDropper) { return null; } + const eyeDropperDisabled = !eyeDropperSupported || props.disabled; return (
- - {Object.keys(COLOR_MODES).map((key) => ( - - {Reflect.get(globalConfig.value, COLOR_MODES[key as keyof typeof COLOR_MODES])} - - ))} - + {showModeSelect ? ( + + {Object.keys(COLOR_MODES).map((key) => ( + + {Reflect.get(globalConfig.value, COLOR_MODES[key as keyof typeof COLOR_MODES])} + + ))} + + ) : null}
+ {props.eyeDropper ? ( + + ) : null}
); }; diff --git a/packages/components/color-picker/components/panel/index.tsx b/packages/components/color-picker/components/panel/index.tsx index 9688bab301..89c6f0000f 100644 --- a/packages/components/color-picker/components/panel/index.tsx +++ b/packages/components/color-picker/components/panel/index.tsx @@ -216,6 +216,21 @@ export default defineComponent({ emitColorChange(trigger); }; + /** + * 吸色(EyeDropper)取色 + * 单色模式下更新整体颜色;渐变模式下与输入框行为一致,仅更新当前选中的渐变节点。 + * EyeDropper 返回的始终是不透明 hex,开启透明通道时保留吸色前的 alpha(alpha 仅由透明度滑条控制)。 + * @param hex 吸色器返回的颜色值(形如 #rrggbb) + */ + const handleEyeDropperPick = (hex: string) => { + const prevAlpha = color.value.alpha; + color.value.update(hex); + if (props.enableAlpha) { + color.value.alpha = prevAlpha; + } + emitColorChange('eyedropper'); + }; + /** * 色块点击 * @param type @@ -289,7 +304,12 @@ export default defineComponent({ return (
- +
{isGradient.value ? ( , diff --git a/packages/components/color-picker/type.ts b/packages/components/color-picker/type.ts index dae9992b4b..d8e1b333af 100644 --- a/packages/components/color-picker/type.ts +++ b/packages/components/color-picker/type.ts @@ -39,6 +39,11 @@ export interface TdColorPickerProps { * @default true */ enableMultipleGradient?: boolean; + /** + * 是否开启吸色(屏幕取色)功能。开启后面板顶部显示吸色按钮,基于浏览器原生 EyeDropper API 实现,浏览器不支持时按钮呈禁用状态 + * @default false + */ + eyeDropper?: boolean; /** * 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 * @default RGB @@ -133,6 +138,11 @@ export interface TdColorPickerPanelProps { * @default true */ enableMultipleGradient?: boolean; + /** + * 是否开启吸色(屏幕取色)功能。开启后面板顶部显示吸色按钮,基于浏览器原生 EyeDropper API 实现,浏览器不支持时按钮呈禁用状态 + * @default false + */ + eyeDropper?: boolean; /** * 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 * @default RGB @@ -198,7 +208,8 @@ export type ColorPickerChangeTrigger = | 'palette-alpha-bar' | 'input' | 'preset' - | 'recent'; + | 'recent' + | 'eyedropper'; export interface ColorObject { alpha: number; diff --git a/packages/components/color-picker/utils/eyedropper.ts b/packages/components/color-picker/utils/eyedropper.ts new file mode 100644 index 0000000000..7efa992a1d --- /dev/null +++ b/packages/components/color-picker/utils/eyedropper.ts @@ -0,0 +1,42 @@ +/** + * 浏览器原生 EyeDropper API 封装 + * @see https://developer.mozilla.org/en-US/docs/Web/API/EyeDropper + * + * TODO: 待 tdesign-common 配套 PR(吸色工具函数)合并且 submodule 更新后, + * 删除本文件并改为从 '@tdesign/common-js/color-picker/index' 导入。 + */ + +export interface OpenEyeDropperOptions { + /** 用于中断取色过程(如组件卸载时) */ + signal?: AbortSignal; +} + +interface NativeEyeDropper { + open(options?: { signal?: AbortSignal }): Promise<{ sRGBHex: string }>; +} + +/** + * 检测当前环境是否支持 EyeDropper API。 + * 使用 globalThis 以兼容 SSR / Web Worker 等无 window 环境。 + */ +export function isEyeDropperSupported(): boolean { + return typeof (globalThis as { EyeDropper?: unknown }).EyeDropper === 'function'; +} + +/** + * 调起系统吸色器。 + * @returns 用户选中的颜色(小写 `#rrggbb`,与 `` 的 value 语义一致); + * 环境不支持、用户取消(Esc / AbortSignal)或取色失败时返回 `null`,调用方无需 try/catch。 + */ +export async function openEyeDropper(options?: OpenEyeDropperOptions): Promise { + if (!isEyeDropperSupported()) return null; + const EyeDropperCtor = (globalThis as unknown as { EyeDropper: new () => NativeEyeDropper }).EyeDropper; + try { + const { sRGBHex } = await new EyeDropperCtor().open(options?.signal ? { signal: options.signal } : undefined); + // 规范未强制 sRGBHex 的大小写,统一转小写便于比较与存储 + return sRGBHex.toLowerCase(); + } catch { + // AbortError(用户取消/中断)与并发取色冲突等场景统一按“未取到色”处理 + return null; + } +} From 66741f11890939fb4f6afaf540c7b0c0fa399b41 Mon Sep 17 00:00:00 2001 From: Liam Fan Date: Fri, 10 Jul 2026 03:03:48 -0500 Subject: [PATCH 2/3] refactor(ColorPicker): reuse icon styles for eyedropper button --- .../components/color-picker/components/panel/header.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/color-picker/components/panel/header.tsx b/packages/components/color-picker/components/panel/header.tsx index e6375000c5..5b05c23c7c 100644 --- a/packages/components/color-picker/components/panel/header.tsx +++ b/packages/components/color-picker/components/panel/header.tsx @@ -84,7 +84,11 @@ export default defineComponent({ type="button" aria-label="eyedropper" disabled={eyeDropperDisabled} - class={[`${baseClassName.value}__eyedropper`, { [statusClassNames.disabled]: eyeDropperDisabled }]} + class={[ + `${baseClassName.value}__icon`, + `${baseClassName.value}__eyedropper`, + { [statusClassNames.disabled]: eyeDropperDisabled }, + ]} onClick={handleEyeDropperClick} > From fcc8b4d5379b68a2278cc77a33bbc3a961ced4c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Jul 2026 10:47:22 +0000 Subject: [PATCH 3/3] chore: update common to PR 2624 --- packages/common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common b/packages/common index 5aedfdf7ab..c290715f86 160000 --- a/packages/common +++ b/packages/common @@ -1 +1 @@ -Subproject commit 5aedfdf7ab0457141b0a886d2126e22bbdb42cae +Subproject commit c290715f86dfb66426c3c16ca7efe8d9f3d8715f