From 2be235e31a72114ce47e727018c1e8615ea67176 Mon Sep 17 00:00:00 2001 From: yzh1002 Date: Mon, 20 Jul 2026 23:50:18 +0800 Subject: [PATCH 1/3] feat(color-picker): support eyedropper --- package.json | 3 +- .../__tests__/color-picker-panel.test.tsx | 71 ++++++++++++++++++- .../color-picker/_example/eye-dropper.vue | 13 ++++ .../color-picker/color-picker-panel-props.ts | 4 ++ .../color-picker/components/panel/index.tsx | 63 +++++++++++++++- packages/components/color-picker/props.ts | 4 ++ packages/components/color-picker/type.ts | 20 +++++- pnpm-lock.yaml | 39 ++++++++++ 8 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 packages/components/color-picker/_example/eye-dropper.vue diff --git a/package.json b/package.json index cc459f2c6c..d1f6a8f6ee 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@tdesign/internal-utils": "workspace:^", "@tdesign/shared-hooks": "workspace:^", "@tdesign/shared-utils": "workspace:^", + "html2canvas-pro": "2.2.4", "tdesign-vue-next": "workspace:^" }, "devDependencies": { @@ -85,4 +86,4 @@ "eslint --fix --cache" ] } -} \ No newline at end of file +} diff --git a/packages/components/color-picker/__tests__/color-picker-panel.test.tsx b/packages/components/color-picker/__tests__/color-picker-panel.test.tsx index 38f62a94e2..2acfe4695b 100644 --- a/packages/components/color-picker/__tests__/color-picker-panel.test.tsx +++ b/packages/components/color-picker/__tests__/color-picker-panel.test.tsx @@ -1,9 +1,27 @@ import { ColorObject, ColorPickerChangeTrigger, ColorPickerPanel } from '@tdesign/components/color-picker'; import { mount } from '@vue/test-utils'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { ref } from 'vue'; import coloPickerPaneProps from '@tdesign/components/color-picker/color-picker-panel-props'; +afterEach(() => { + vi.unstubAllGlobals(); +}); + +class MockEyeDropper { + // eslint-disable-next-line class-methods-use-this + open() { + return Promise.resolve({ sRGBHex: '#AaBbCc' }); + } +} + +class CancelEyeDropper { + // eslint-disable-next-line class-methods-use-this + open() { + return Promise.reject(new DOMException('User canceled', 'AbortError')); + } +} + /** * 因为在 color-picker 中已经测试过基本的 props 了,因此这里只是做额外的测试 */ @@ -47,6 +65,28 @@ describe('ColorPickerPanel', () => { ); expect(wrapper.find('.t-color-picker__swatches .t-color-picker__icon').exists()).toBeFalsy(); }); + + it(':eyeDropper[boolean]', async () => { + vi.stubGlobal('EyeDropper', MockEyeDropper); + + const wrapper = mount(() => ); + const button = wrapper.find('.t-color-picker__eyedropper'); + + expect(button.exists()).toBeTruthy(); + expect(button.attributes('disabled')).toBeUndefined(); + }); + + it(':eyeDropper disabled when unsupported', async () => { + const wrapper = mount(() => ); + + expect(wrapper.find('.t-color-picker__eyedropper').attributes('disabled')).toBeDefined(); + }); + + it(':eyeDropper fallback mode is enabled without native API', async () => { + const wrapper = mount(() => ); + + expect(wrapper.find('.t-color-picker__eyedropper').attributes('disabled')).toBeUndefined(); + }); }); describe(':event', () => { @@ -75,5 +115,34 @@ describe('ColorPickerPanel', () => { expect(args2[0]).toBe('linear-gradient(80deg,rgb(241, 29, 0) 0%,rgb(73, 106, 220) 100%)'); expect(args2[1].trigger).toBe('input'); }); + + it(':change trigger by eyedropper', async () => { + vi.stubGlobal('EyeDropper', MockEyeDropper); + + const fn = vi.fn((value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => {}); + const wrapper = mount(() => ( + + )); + + await wrapper.find('.t-color-picker__eyedropper').trigger('click'); + await vi.waitFor(() => expect(fn).toHaveBeenCalled()); + + expect(fn.mock.calls[0][0]).toBe('#aabbcc'); + expect(fn.mock.calls[0][1].trigger).toBe('eyedropper'); + }); + + it(':eyeDropper cancel does not trigger change', async () => { + vi.stubGlobal('EyeDropper', CancelEyeDropper); + + const fn = vi.fn((value: string, context: { color: ColorObject; trigger: ColorPickerChangeTrigger }) => {}); + const wrapper = mount(() => ( + + )); + + await wrapper.find('.t-color-picker__eyedropper').trigger('click'); + await Promise.resolve(); + + expect(fn).not.toHaveBeenCalled(); + }); }); }); 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..7f9b8601d6 --- /dev/null +++ b/packages/components/color-picker/_example/eye-dropper.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/components/color-picker/color-picker-panel-props.ts b/packages/components/color-picker/color-picker-panel-props.ts index db17d95655..5b0c1b55fa 100644 --- a/packages/components/color-picker/color-picker-panel-props.ts +++ b/packages/components/color-picker/color-picker-panel-props.ts @@ -23,6 +23,10 @@ export default { default: true, }, /** 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 */ + eyeDropper: { + type: [Boolean, Object] as unknown as PropType, + default: false as TdColorPickerPanelProps['eyeDropper'], + }, format: { type: String as PropType, default: 'RGB' as TdColorPickerPanelProps['format'], diff --git a/packages/components/color-picker/components/panel/index.tsx b/packages/components/color-picker/components/panel/index.tsx index 9688bab301..3b9341ad05 100644 --- a/packages/components/color-picker/components/panel/index.tsx +++ b/packages/components/color-picker/components/panel/index.tsx @@ -1,4 +1,5 @@ -import { computed, defineComponent, ref, toRefs, watch } from 'vue'; +import { computed, defineComponent, onBeforeUnmount, ref, toRefs, watch } from 'vue'; +import { SipIcon as TdSipIcon } from 'tdesign-icons-vue-next'; import { cloneDeep, isNull, isUndefined } from 'lodash-es'; import { Color, @@ -8,12 +9,14 @@ import { getColorObject, GradientColorPoint, initColorFormat, + isEyeDropperSupported, + openEyeDropper, TD_COLOR_USED_COLORS_MAX_SIZE, } from '@tdesign/common-js/color-picker/index'; -import { useCommonClassName, useConfig, useDefaultValue, useVModel } from '@tdesign/shared-hooks'; +import { useCommonClassName, useConfig, useDefaultValue, useGlobalIcon, useVModel } from '@tdesign/shared-hooks'; import props from '../../color-picker-panel-props'; import { useBaseClassName } from '../../hooks'; -import type { ColorPickerChangeTrigger, TdColorPickerProps } from '../../type'; +import type { ColorPickerChangeTrigger, EyeDropperConfig, TdColorPickerProps } from '../../type'; import type { TdColorModes } from '../../types'; import FormatPanel from '../format'; import AlphaSlider from './alpha'; @@ -32,6 +35,7 @@ export default defineComponent({ const baseClassName = useBaseClassName(); const { STATUS } = useCommonClassName(); const { t, globalConfig } = useConfig('colorPicker'); + const { SipIcon } = useGlobalIcon({ SipIcon: TdSipIcon }); const statusClassNames = STATUS.value; const { value: inputValue, modelValue, recentColors } = toRefs(props); const [innerValue, setInnerValue] = useVModel(inputValue, modelValue, props.defaultValue, props.onChange); @@ -56,6 +60,13 @@ export default defineComponent({ const color = ref(new Color(innerValue.value || defaultEmptyColor.value)); const formatModel = ref(initColorFormat(props.format, props.enableAlpha)); + const getEyeDropperOptions = (config: EyeDropperConfig) => { + if (!config || config === true) return {}; + return config; + }; + const eyeDropperOptions = computed(() => getEyeDropperOptions(props.eyeDropper)); + const eyeDropperSupported = computed(() => isEyeDropperSupported(eyeDropperOptions.value)); + const eyeDropperAbortController = ref(null); /** * 添加最近使用颜色 @@ -189,6 +200,34 @@ export default defineComponent({ emitColorChange('input'); }; + const handleEyeDropperPick = (hex: string) => { + const previousAlpha = color.value.alpha; + color.value.update(hex); + if (props.enableAlpha) { + color.value.alpha = previousAlpha; + if (color.value.isGradient) { + color.value.updateCurrentGradientColor(); + } + } + emitColorChange('eyedropper'); + }; + + const handleEyeDropperClick = async () => { + if (!eyeDropperSupported.value || props.disabled) return; + + eyeDropperAbortController.value?.abort(); + eyeDropperAbortController.value = typeof AbortController !== 'undefined' ? new AbortController() : null; + const hex = await openEyeDropper({ + ...eyeDropperOptions.value, + signal: eyeDropperAbortController.value?.signal, + }); + if (hex) handleEyeDropperPick(hex); + }; + + onBeforeUnmount(() => { + eyeDropperAbortController.value?.abort(); + }); + /** * 渐变改变 * @param param0 @@ -257,6 +296,8 @@ export default defineComponent({ systemColors = systemColors.filter((color) => Color.isGradientColor(color)); } const showSystemColors = Array.isArray(systemColors) && systemColors.length; + const showEyeDropper = Boolean(props.eyeDropper); + const eyeDropperDisabled = !eyeDropperSupported.value || props.disabled; const renderSwatches = () => { if (!showSystemColors && !showUsedColors) return null; @@ -302,6 +343,22 @@ export default defineComponent({
+ {showEyeDropper ? ( + + ) : null}
{props.enableAlpha ? : null} diff --git a/packages/components/color-picker/props.ts b/packages/components/color-picker/props.ts index f0757763bc..770065bf04 100644 --- a/packages/components/color-picker/props.ts +++ b/packages/components/color-picker/props.ts @@ -30,6 +30,10 @@ export default { default: true, }, /** 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 */ + eyeDropper: { + type: [Boolean, Object] as unknown as PropType, + default: false as TdColorPickerProps['eyeDropper'], + }, format: { type: String as PropType, default: 'RGB' as TdColorPickerProps['format'], diff --git a/packages/components/color-picker/type.ts b/packages/components/color-picker/type.ts index dae9992b4b..3898e485b9 100644 --- a/packages/components/color-picker/type.ts +++ b/packages/components/color-picker/type.ts @@ -9,6 +9,13 @@ import { PopupProps } from '../popup'; import { SelectInputProps } from '../select-input'; import { SizeEnum } from '../common'; +export type EyeDropperConfig = + | boolean + | { + mode?: 'native' | 'fallback'; + showPreview?: boolean; + }; + export interface TdColorPickerProps { /** * 无边框模式 @@ -39,6 +46,11 @@ export interface TdColorPickerProps { * @default true */ enableMultipleGradient?: boolean; + /** + * 是否开启吸色(屏幕取色)功能。开启后颜色条前显示吸色按钮 + * @default false + */ + eyeDropper?: EyeDropperConfig; /** * 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 * @default RGB @@ -133,6 +145,11 @@ export interface TdColorPickerPanelProps { * @default true */ enableMultipleGradient?: boolean; + /** + * 是否开启吸色(屏幕取色)功能。开启后颜色条前显示吸色按钮 + * @default false + */ + eyeDropper?: EyeDropperConfig; /** * 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 * @default RGB @@ -198,7 +215,8 @@ export type ColorPickerChangeTrigger = | 'palette-alpha-bar' | 'input' | 'preset' - | 'recent'; + | 'recent' + | 'eyedropper'; export interface ColorObject { alpha: number; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffcc78ce87..68be6bbb1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -338,6 +338,9 @@ importers: '@tdesign/shared-utils': specifier: workspace:^ version: link:packages/shared/utils + html2canvas-pro: + specifier: 2.2.4 + version: 2.2.4 tdesign-vue-next: specifier: workspace:^ version: link:packages/tdesign-vue-next @@ -5177,6 +5180,10 @@ packages: bare-url@2.4.3: resolution: {integrity: sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -5658,6 +5665,9 @@ packages: peerDependencies: postcss: ^8.0.9 + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -7229,6 +7239,10 @@ packages: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} + html2canvas-pro@2.2.4: + resolution: {integrity: sha512-fo/VOiQZ3oWykAPvnMNHymUHzLWJE7c7C7VWPh+ydb+NquFTwqY5rHQRoxkgsaXo/iciVryPNffTvLzYg98xhg==} + engines: {node: '>=16.0.0'} + htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} @@ -10265,6 +10279,9 @@ packages: resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} engines: {node: '>=0.10'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -10773,6 +10790,9 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@14.0.1: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true @@ -16581,6 +16601,8 @@ snapshots: dependencies: bare-path: 3.0.0 + base64-arraybuffer@1.0.2: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.30: {} @@ -17107,6 +17129,10 @@ snapshots: dependencies: postcss: 8.5.14 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -19053,6 +19079,11 @@ snapshots: dependencies: whatwg-encoding: 2.0.0 + html2canvas-pro@2.2.4: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 @@ -22779,6 +22810,10 @@ snapshots: text-extensions@1.9.0: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + text-table@0.2.0: {} through2@2.0.5: @@ -23318,6 +23353,10 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.19 + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + uuid@14.0.1: optional: true From 6339054ba971b76e0806b524ac6c147a57db64c5 Mon Sep 17 00:00:00 2001 From: yzh1002 Date: Tue, 21 Jul 2026 22:17:23 +0800 Subject: [PATCH 2/3] feat(color-picker): support eyedropper --- packages/common | 2 +- packages/components/color-picker/_usage/index.vue | 8 ++++---- packages/components/color-picker/_usage/panel-props.json | 6 ++++++ packages/components/color-picker/_usage/props.json | 6 ++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/common b/packages/common index 5aedfdf7ab..f9a99ee018 160000 --- a/packages/common +++ b/packages/common @@ -1 +1 @@ -Subproject commit 5aedfdf7ab0457141b0a886d2126e22bbdb42cae +Subproject commit f9a99ee0185f286e350239a26a1f3b4ca7d34f31 diff --git a/packages/components/color-picker/_usage/index.vue b/packages/components/color-picker/_usage/index.vue index 4fe7f9eb34..8426e3126b 100644 --- a/packages/components/color-picker/_usage/index.vue +++ b/packages/components/color-picker/_usage/index.vue @@ -2,10 +2,10 @@ @@ -24,8 +24,8 @@ const panelList = [ const defaultColor = ref('rgb(0, 82, 217)'); const usageCodeMap = { - colorPicker: '', - colorPickerPanel: '', + colorPicker: '', + colorPickerPanel: '', }; const usageCode = ref(``); diff --git a/packages/components/color-picker/_usage/panel-props.json b/packages/components/color-picker/_usage/panel-props.json index b108508eda..79a91dcc8c 100644 --- a/packages/components/color-picker/_usage/panel-props.json +++ b/packages/components/color-picker/_usage/panel-props.json @@ -17,6 +17,12 @@ "defaultValue": true, "options": [] }, + { + "name": "eyeDropper", + "type": "Boolean", + "defaultValue": true, + "options": [] + }, { "name": "showPrimaryColorPreview", "type": "Boolean", diff --git a/packages/components/color-picker/_usage/props.json b/packages/components/color-picker/_usage/props.json index 20f5a9ad0b..94286109bd 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": true, + "options": [] + }, { "name": "size", "type": "enum", From a7b900a043d8ab2e448ed02897ec39bc895a4df2 Mon Sep 17 00:00:00 2001 From: Uyarn Date: Fri, 31 Jul 2026 11:10:15 +0800 Subject: [PATCH 3/3] chore: update common to common pr --- packages/common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common b/packages/common index f9a99ee018..ec59b0e078 160000 --- a/packages/common +++ b/packages/common @@ -1 +1 @@ -Subproject commit f9a99ee0185f286e350239a26a1f3b4ca7d34f31 +Subproject commit ec59b0e078f115cafbac80f9287aea656ce0e5e1