Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -85,4 +86,4 @@
"eslint --fix --cache"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -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 了,因此这里只是做额外的测试
*/
Expand All @@ -11,7 +29,7 @@
describe(':ui', () => {
it(':mount', () => {
const wrapper = mount(() => <ColorPickerPanel />);
expect(wrapper.element).toMatchSnapshot();

Check failure on line 32 in packages/components/color-picker/__tests__/color-picker-panel.test.tsx

View workflow job for this annotation

GitHub Actions / test

../../components/color-picker/__tests__/color-picker-panel.test.tsx > ColorPickerPanel > :ui > :mount

Error: Snapshot `ColorPickerPanel > :ui > :mount 1` mismatched - Expected + Received @@ -87,10 +87,11 @@ /> </div> <div class="t-color-picker__sliders-wrapper" > + <!----> <div class="t-color-picker__sliders" > <div class="t-color-picker__slider-wrapper t-color-picker__slider-wrapper--hue-type t-color-picker__hue" ❯ ../../components/color-picker/__tests__/color-picker-panel.test.tsx:32:31
});
});

Expand All @@ -23,7 +41,7 @@

it(':colorModes[monochrome,linear-gradient]', async () => {
const wrapper = mount(() => <ColorPickerPanel />);
expect(wrapper.element).toMatchSnapshot();

Check failure on line 44 in packages/components/color-picker/__tests__/color-picker-panel.test.tsx

View workflow job for this annotation

GitHub Actions / test

../../components/color-picker/__tests__/color-picker-panel.test.tsx > ColorPickerPanel > :props > :colorModes[monochrome,linear-gradient]

Error: Snapshot `ColorPickerPanel > :props > :colorModes[monochrome,linear-gradient] 1` mismatched - Expected + Received @@ -87,10 +87,11 @@ /> </div> <div class="t-color-picker__sliders-wrapper" > + <!----> <div class="t-color-picker__sliders" > <div class="t-color-picker__slider-wrapper t-color-picker__slider-wrapper--hue-type t-color-picker__hue" ❯ ../../components/color-picker/__tests__/color-picker-panel.test.tsx:44:31

const wrapper2 = mount(() => <ColorPickerPanel colorModes={['linear-gradient']} />);
expect(wrapper2.element).toMatchSnapshot();
Expand All @@ -47,6 +65,28 @@
);
expect(wrapper.find('.t-color-picker__swatches .t-color-picker__icon').exists()).toBeFalsy();
});

it(':eyeDropper[boolean]', async () => {
vi.stubGlobal('EyeDropper', MockEyeDropper);

const wrapper = mount(() => <ColorPickerPanel eyeDropper />);
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(() => <ColorPickerPanel eyeDropper />);

expect(wrapper.find('.t-color-picker__eyedropper').attributes('disabled')).toBeDefined();
});

it(':eyeDropper fallback mode is enabled without native API', async () => {
const wrapper = mount(() => <ColorPickerPanel eyeDropper={{ mode: 'fallback' }} />);

expect(wrapper.find('.t-color-picker__eyedropper').attributes('disabled')).toBeUndefined();
});
});

describe(':event', () => {
Expand Down Expand Up @@ -75,5 +115,34 @@
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 }) => {});

Check warning on line 122 in packages/components/color-picker/__tests__/color-picker-panel.test.tsx

View workflow job for this annotation

GitHub Actions / lint

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 122 in packages/components/color-picker/__tests__/color-picker-panel.test.tsx

View workflow job for this annotation

GitHub Actions / lint

'value' is defined but never used. Allowed unused args must match /^_/u
const wrapper = mount(() => (
<ColorPickerPanel eyeDropper onChange={fn} colorModes={['monochrome']} format="HEX" />
));

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 }) => {});

Check warning on line 137 in packages/components/color-picker/__tests__/color-picker-panel.test.tsx

View workflow job for this annotation

GitHub Actions / lint

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 137 in packages/components/color-picker/__tests__/color-picker-panel.test.tsx

View workflow job for this annotation

GitHub Actions / lint

'value' is defined but never used. Allowed unused args must match /^_/u
const wrapper = mount(() => (
<ColorPickerPanel eyeDropper onChange={fn} colorModes={['monochrome']} format="HEX" />
));

await wrapper.find('.t-color-picker__eyedropper').trigger('click');
await Promise.resolve();

expect(fn).not.toHaveBeenCalled();
});
});
});
13 changes: 13 additions & 0 deletions packages/components/color-picker/_example/eye-dropper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<t-space direction="vertical">
<t-color-picker v-model="color" eye-dropper format="HEX" />
<t-color-picker-panel v-model="panelColor" :eye-dropper="{ mode: 'fallback' }" format="HEX" />
</t-space>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const color = ref('#0052d9');
const panelColor = ref('#00a870');
</script>
8 changes: 4 additions & 4 deletions packages/components/color-picker/_usage/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<template>
<base-usage :code="usageCode" :config-list="configList" :panel-list="panelList" @panel-change="onPanelChange">
<template #colorPicker="{ configProps }">
<t-color-picker v-bind="configProps" :default-value="defaultColor" />
<t-color-picker v-bind="configProps" eye-dropper :default-value="defaultColor" />
</template>
<template #colorPickerPanel="{ configProps }">
<t-color-picker-panel v-bind="configProps" :default-value="defaultColor" />
<t-color-picker-panel v-bind="configProps" eye-dropper :default-value="defaultColor" />
</template>
</base-usage>
</template>
Expand All @@ -24,8 +24,8 @@ const panelList = [
const defaultColor = ref('rgb(0, 82, 217)');

const usageCodeMap = {
colorPicker: '<t-color-picker v-bind="configProps" />',
colorPickerPanel: '<t-color-picker-panel v-bind="configProps" />',
colorPicker: '<t-color-picker v-bind="configProps" eye-dropper />',
colorPickerPanel: '<t-color-picker-panel v-bind="configProps" eye-dropper />',
};
const usageCode = ref(`<template>${usageCodeMap[panelList[0].value].trim()}</template>`);

Expand Down
6 changes: 6 additions & 0 deletions packages/components/color-picker/_usage/panel-props.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
"defaultValue": true,
"options": []
},
{
"name": "eyeDropper",
"type": "Boolean",
"defaultValue": true,
"options": []
},
{
"name": "showPrimaryColorPreview",
"type": "Boolean",
Expand Down
6 changes: 6 additions & 0 deletions packages/components/color-picker/_usage/props.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
"defaultValue": true,
"options": []
},
{
"name": "eyeDropper",
"type": "Boolean",
"defaultValue": true,
"options": []
},
{
"name": "size",
"type": "enum",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default {
default: true,
},
/** 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 */
eyeDropper: {
type: [Boolean, Object] as unknown as PropType<TdColorPickerPanelProps['eyeDropper']>,
default: false as TdColorPickerPanelProps['eyeDropper'],
},
format: {
type: String as PropType<TdColorPickerPanelProps['format']>,
default: 'RGB' as TdColorPickerPanelProps['format'],
Expand Down
63 changes: 60 additions & 3 deletions packages/components/color-picker/components/panel/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand All @@ -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);
Expand All @@ -56,6 +60,13 @@ export default defineComponent({
const color = ref(new Color(innerValue.value || defaultEmptyColor.value));

const formatModel = ref<TdColorPickerProps['format']>(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<AbortController | null>(null);

/**
* 添加最近使用颜色
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -302,6 +343,22 @@ export default defineComponent({
<SaturationPanel {...baseProps} onChange={handleSatAndValueChange} />

<div class={[`${baseClassName.value}__sliders-wrapper`]}>
{showEyeDropper ? (
<button
type="button"
aria-label="eyedropper"
disabled={eyeDropperDisabled}
class={[
`${baseClassName.value}__icon`,
`${baseClassName.value}__eyedropper`,
`${baseClassName.value}__sliders-eyedropper`,
{ [statusClassNames.disabled]: eyeDropperDisabled },
]}
onClick={handleEyeDropperClick}
>
<SipIcon />
</button>
) : null}
<div class={[`${baseClassName.value}__sliders`]}>
<HueSlider {...baseProps} onChange={handleHueChange} />
{props.enableAlpha ? <AlphaSlider {...baseProps} onChange={handleAlphaChange} /> : null}
Expand Down
4 changes: 4 additions & 0 deletions packages/components/color-picker/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export default {
default: true,
},
/** 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效 */
eyeDropper: {
type: [Boolean, Object] as unknown as PropType<TdColorPickerProps['eyeDropper']>,
default: false as TdColorPickerProps['eyeDropper'],
},
format: {
type: String as PropType<TdColorPickerProps['format']>,
default: 'RGB' as TdColorPickerProps['format'],
Expand Down
20 changes: 19 additions & 1 deletion packages/components/color-picker/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
* 无边框模式
Expand Down Expand Up @@ -39,6 +46,11 @@ export interface TdColorPickerProps {
* @default true
*/
enableMultipleGradient?: boolean;
/**
* 是否开启吸色(屏幕取色)功能。开启后颜色条前显示吸色按钮
* @default false
*/
eyeDropper?: EyeDropperConfig;
/**
* 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效
* @default RGB
Expand Down Expand Up @@ -133,6 +145,11 @@ export interface TdColorPickerPanelProps {
* @default true
*/
enableMultipleGradient?: boolean;
/**
* 是否开启吸色(屏幕取色)功能。开启后颜色条前显示吸色按钮
* @default false
*/
eyeDropper?: EyeDropperConfig;
/**
* 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效
* @default RGB
Expand Down Expand Up @@ -198,7 +215,8 @@ export type ColorPickerChangeTrigger =
| 'palette-alpha-bar'
| 'input'
| 'preset'
| 'recent';
| 'recent'
| 'eyedropper';

export interface ColorObject {
alpha: number;
Expand Down
Loading
Loading