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
6 changes: 6 additions & 0 deletions packages/components/color-picker/_example/eye-dropper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import { ColorPicker } from 'tdesign-react';

export default function EyeDropperExample() {
return <ColorPicker eyeDropper defaultValue="#0052d9" />;
}
68 changes: 53 additions & 15 deletions packages/components/color-picker/components/panel/header.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { COLOR_MODES } from '@tdesign/common-js/color-picker/constants';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { COLOR_MODES } from '@tdesign/common-js/color-picker/index';

import { useLocaleReceiver } from '../../../locale/LocalReceiver';
import Radio from '../../../radio';
import { isEyeDropperSupported, openEyeDropper } from '../../utils/eyedropper';

import type { RadioValue } from '../../../radio';
import type { TdColorModes } from '../../interface';
Expand All @@ -12,30 +13,67 @@ export interface ColorPanelHeaderProps extends TdColorPickerProps {
mode?: TdColorModes;
onModeChange?: (value: RadioValue, context: { e: React.ChangeEvent<HTMLInputElement> }) => void;
baseClassName?: string;
onEyeDropperPick?: (hex: string) => void;
}

const supported = isEyeDropperSupported();

const Header = (props: ColorPanelHeaderProps) => {
const [local, t] = useLocaleReceiver('colorPicker');
const { baseClassName, mode = 'monochrome', colorModes, onModeChange, eyeDropper, onEyeDropperPick } = props;
const [picking, setPicking] = useState(false);
const abortRef = useRef<AbortController | null>(null);

useEffect(
() => () => {
abortRef.current?.abort();
},
[],
);

const { baseClassName, mode = 'monochrome', colorModes, onModeChange } = props;
const handleEyeDropperClick = useCallback(async () => {
if (picking || !supported) return;
abortRef.current = new AbortController();
setPicking(true);
try {
const hex = await openEyeDropper(abortRef.current.signal);
if (hex) onEyeDropperPick?.(hex);
} finally {
setPicking(false);
abortRef.current = null;
}
}, [picking, onEyeDropperPick]);

const isSingleMode = colorModes?.length === 1;

if (isSingleMode) {
return null;
}
if (isSingleMode && !eyeDropper) return null;

return (
<div className={`${baseClassName}__head`}>
<div className={`${baseClassName}__mode`}>
<Radio.Group variant="default-filled" size="small" value={mode} onChange={onModeChange}>
{Object.keys(COLOR_MODES).map((key) => (
<Radio.Button key={key} value={key}>
{t(local[COLOR_MODES[key]])}
</Radio.Button>
))}
</Radio.Group>
</div>
{!isSingleMode && (
<div className={`${baseClassName}__mode`}>
<Radio.Group variant="default-filled" size="small" value={mode} onChange={onModeChange}>
{Object.keys(COLOR_MODES).map((key) => (
<Radio.Button key={key} value={key}>
{t(local[COLOR_MODES[key]])}
</Radio.Button>
))}
</Radio.Group>
</div>
)}
{eyeDropper && (
<button
className={[`${baseClassName}__eyedropper`, !supported && 't-is-disabled'].filter(Boolean).join(' ')}
title="吸色"
disabled={!supported || picking}
onClick={handleEyeDropperClick}
type="button"
>
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor" aria-hidden="true">
<path d="M13.354.646a2.207 2.207 0 0 0-3.121 0L8.75 2.129l-.604-.604a1 1 0 1 0-1.414 1.414l.25.25L1.47 9.2A2.5 2.5 0 0 0 .75 11v.25H.5a.5.5 0 0 0 0 1h.25V12.5a.5.5 0 0 0 1 0v-.25H2a2.5 2.5 0 0 0 1.8-.72l5.982-5.982.25.25a1 1 0 1 0 1.414-1.414l-.604-.604 1.483-1.483a2.207 2.207 0 0 0 0-3.121zM3.094 11.78A1.5 1.5 0 0 1 2 12.25H1.75v-.25A1.5 1.5 0 0 1 2.22 10.906L8 5.121l.879.879-5.785 5.78z" />
</svg>
</button>
)}
</div>
);
};
Expand Down
18 changes: 17 additions & 1 deletion packages/components/color-picker/components/panel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const Panel = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => {
disabled,
enableAlpha,
enableMultipleGradient,
eyeDropper,
format,
style,
swatchColors,
Expand Down Expand Up @@ -114,6 +115,14 @@ const Panel = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => {
emitColorChange();
};

const handleEyeDropperPick = useCallback(
(hex: string) => {
updateColor(hex);
emitColorChange('eyedropper');
},
[emitColorChange], // eslint-disable-line react-hooks/exhaustive-deps
);

/**
* 最近使用颜色变化
*/
Expand Down Expand Up @@ -281,7 +290,14 @@ const Panel = forwardRef<HTMLDivElement, ColorPickerProps>((props, ref) => {
style={{ ...style }}
ref={ref}
>
<PanelHeader baseClassName={baseClassName} mode={mode} colorModes={colorModes} onModeChange={handleModeChange} />
<PanelHeader
baseClassName={baseClassName}
mode={mode}
colorModes={colorModes}
onModeChange={handleModeChange}
eyeDropper={eyeDropper}
onEyeDropperPick={handleEyeDropperPick}
/>
<div className={`${baseClassName}__body`}>
{isGradient && (
<LinearGradient
Expand Down
8 changes: 7 additions & 1 deletion packages/components/color-picker/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export interface TdColorPickerProps {
* @default true
*/
enableMultipleGradient?: boolean;
/**
* 是否开启吸色功能,启用后面板顶部出现吸色按钮,仅支持 Chrome/Edge 等现代浏览器
* @default false
*/
eyeDropper?: boolean;
/**
* 格式化色值。`enableAlpha` 为真时,`HEX8/RGBA/HSLA/HSVA` 有效
* @default RGB
Expand Down Expand Up @@ -111,7 +116,8 @@ export type ColorPickerChangeTrigger =
| 'palette-alpha-bar'
| 'input'
| 'preset'
| 'recent';
| 'recent'
| 'eyedropper';

export interface ColorObject {
alpha: number;
Expand Down
26 changes: 26 additions & 0 deletions packages/components/color-picker/utils/eyedropper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
interface EyeDropperInstance {
open(options?: { signal?: AbortSignal }): Promise<{ sRGBHex: string }>;
}

interface EyeDropperConstructor {
new (): EyeDropperInstance;
}

function getEyeDropperCtor(): EyeDropperConstructor | undefined {
if (typeof window === 'undefined') return undefined;
const { EyeDropper } = window as Window & { EyeDropper?: unknown };
return typeof EyeDropper === 'function' ? (EyeDropper as EyeDropperConstructor) : undefined;
}

export const isEyeDropperSupported = (): boolean => getEyeDropperCtor() !== undefined;

export const openEyeDropper = async (signal?: AbortSignal): Promise<string | null> => {
const Ctor = getEyeDropperCtor();
if (!Ctor) return null;
try {
const result = await new Ctor().open(signal ? { signal } : undefined);
return result.sRGBHex;
} catch {
return null;
}
};
5 changes: 5 additions & 0 deletions packages/components/config-provider/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,11 @@ export interface ColorPickerConfig {
* @default ''
*/
swatchColorTitle?: string;
/**
* 语言配置,"吸色" 按钮 title 文案
* @default ''
*/
eyeDropper?: string;
}

export interface DatePickerConfig {
Expand Down