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
77 changes: 67 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Accepts string, number, RGB, RGBA, HSB, HSBA, and `Color` inputs.
- Provides hue and alpha controls with change and drag-complete callbacks.
- Exposes `Color` helpers for hex, RGB, and HSB conversions.
- Exposes keyboard-operable, screen-reader-labelled controls with overridable strings.

## Install

Expand Down Expand Up @@ -61,16 +62,45 @@ Then open `http://localhost:8000`.

### ColorPicker

| Property | Description | Type | Default |
| ---------------- | --------------------------------------- | ---------------------------------------------------------------------------- | ----------------- |
| defaultValue | Default color value | `ColorGenInput` | - |
| disabled | Whether the color picker is disabled | boolean | false |
| disabledAlpha | Whether to hide the alpha slider | boolean | false |
| panelRender | Custom panel renderer | `(panel: React.ReactElement) => React.ReactElement` | - |
| prefixCls | Component class name prefix | string | `rc-color-picker` |
| value | Current color value | `ColorGenInput` | - |
| onChange | Callback when color changes | `(color: Color, info?: { type?: 'hue' \| 'alpha'; value?: number }) => void` | - |
| onChangeComplete | Callback when a drag operation finishes | `(color: Color, info?: { type?: 'hue' \| 'alpha'; value?: number }) => void` | - |
| Property | Description | Type | Default |
| ---------------- | -------------------------------------------- | ---------------------------------------------------------------------------- | ----------------- |
| defaultValue | Default color value | `ColorGenInput` | - |
| disabled | Whether the color picker is disabled | boolean | false |
| disabledAlpha | Whether to hide the alpha slider | boolean | false |
| locale | Accessible labels for the picker and sliders | [`Locale`](#locale) | - |
| panelRender | Custom panel renderer | `(panel: React.ReactElement) => React.ReactElement` | - |
| prefixCls | Component class name prefix | string | `rc-color-picker` |
| value | Current color value | `ColorGenInput` | - |
| onChange | Callback when color changes | `(color: Color, info?: { type?: 'hue' \| 'alpha'; value?: number }) => void` | - |
| onChangeComplete | Callback when a drag operation finishes | `(color: Color, info?: { type?: 'hue' \| 'alpha'; value?: number }) => void` | - |

### Locale

Overrides the accessible names the controls expose to assistive technology. Every key is optional and falls back to its default, so pass only the strings you need to translate.

| Property | Description | Type | Default |
| ----------------- | ------------------------------------------------------------- | ------ | -------------- |
| alpha | `aria-label` of the alpha slider | string | `Alpha` |
| brightness | Channel name in the brightness axis `aria-valuetext` | string | `Brightness` |
| hue | `aria-label` of the hue slider | string | `Hue` |
| picker | `aria-label` of both saturation and brightness axes | string | `Color picker` |
| pickerDescription | `aria-roledescription` of both saturation and brightness axes | string | `2D slider` |
| saturation | Channel name in the saturation axis `aria-valuetext` | string | `Saturation` |

```tsx | pure
<ColorPicker
locale={{
picker: 'Sélecteur de couleur',
pickerDescription: 'Curseur 2D',
hue: 'Teinte',
alpha: 'Transparence',
saturation: 'Saturation',
brightness: 'Luminosité',
}}
/>
```

The 2-D area renders one range input per axis. Both share the `picker` name and the `pickerDescription` role description, while each announces its own channel through `aria-valuetext` — `Saturation: 91%` and `Brightness: 100%` — so `saturation` and `brightness` are channel names rather than whole labels.

### Color

Expand All @@ -82,6 +112,33 @@ Then open `http://localhost:8000`.
| toRgb | Convert to RGB object | `() => { r: number; g: number; b: number; a: number }` |
| toRgbString | Convert to RGB color string | `() => string` |

### ColorBlock

A standalone swatch, exported for building custom triggers and panels.

```tsx | pure
import { ColorBlock } from '@rc-component/color-picker';

<ColorBlock
color="#1677ff"
prefixCls="rc-color-picker"
title="Current color"
onClick={handleClick}
/>;
```

| Property | Description | Type | Default |
| --------- | ------------------------------- | -------------------------------------- | ------- |
| color | Color used to fill the swatch | string | - |
| prefixCls | Component class name prefix | string | - |
| className | Class name of the outer element | string | - |
| style | Style of the outer element | `React.CSSProperties` | - |
| ...rest | Forwarded to the outer `div` | `React.HTMLAttributes<HTMLDivElement>` | - |

`ColorBlockProps` extends `React.HTMLAttributes<HTMLDivElement>`, so any standard div attribute or event handler — `onClick`, `title`, `role`, `tabIndex`, `data-*`, `aria-*` — reaches the outer element. `innerClassName` and `innerStyle` also exist on the type, but they are internal to antd's semantic structure; treat them as private.

Passing `onClick` also makes the swatch a keyboard-accessible button: it receives `role="button"`, `tabIndex={0}`, an `aria-label` defaulting to `color`, and `Enter` / `Space` activate it just like a pointer click. Your own `role`, `tabIndex` or `aria-label` still wins over those defaults, and your `onKeyDown` is composed with the built-in activation rather than replacing it — call `event.preventDefault()` in it to suppress activation, as you would on a native button. Without `onClick` the swatch stays a plain, non-focusable, unlabelled `div`.

## Development

```bash
Expand Down
77 changes: 67 additions & 10 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- 支持字符串、数字、RGB、RGBA、HSB、HSBA 和 `Color` 输入。
- 提供带有更改和拖动完成回调的色调和 alpha 控件。
- 暴露 `Color` 辅助方法,用于 hex、RGB 和 HSB 转换。
- 提供可用键盘操作、带屏幕阅读器标签的控件,文案可覆盖。

## 安装

Expand Down Expand Up @@ -61,16 +62,45 @@ npm start

### ColorPicker

| 参数 | 说明 | 类型 | 默认值 |
| ---------------- | -------------------- | ---------------------------------------------------------------------------- | ----------------- |
| defaultValue | 默认颜色值 | `ColorGenInput` | - |
| disabled | 颜色选择器是否禁用 | boolean | false |
| disabledAlpha | 是否隐藏 alpha 滑块 | boolean | false |
| panelRender | 自定义面板渲染器 | `(panel: React.ReactElement) => React.ReactElement` | - |
| prefixCls | 组件 className 前缀 | string | `rc-color-picker` |
| value | 当前颜色值 | `ColorGenInput` | - |
| onChange | 颜色变化时的回调 | `(color: Color, info?: { type?: 'hue' \| 'alpha'; value?: number }) => void` | - |
| onChangeComplete | 拖动操作完成时的回调 | `(color: Color, info?: { type?: 'hue' \| 'alpha'; value?: number }) => void` | - |
| 参数 | 说明 | 类型 | 默认值 |
| ---------------- | ------------------------ | ---------------------------------------------------------------------------- | ----------------- |
| defaultValue | 默认颜色值 | `ColorGenInput` | - |
| disabled | 颜色选择器是否禁用 | boolean | false |
| disabledAlpha | 是否隐藏 alpha 滑块 | boolean | false |
| locale | 选择器与滑块的无障碍标签 | [`Locale`](#locale) | - |
| panelRender | 自定义面板渲染器 | `(panel: React.ReactElement) => React.ReactElement` | - |
| prefixCls | 组件 className 前缀 | string | `rc-color-picker` |
| value | 当前颜色值 | `ColorGenInput` | - |
| onChange | 颜色变化时的回调 | `(color: Color, info?: { type?: 'hue' \| 'alpha'; value?: number }) => void` | - |
| onChangeComplete | 拖动操作完成时的回调 | `(color: Color, info?: { type?: 'hue' \| 'alpha'; value?: number }) => void` | - |

### Locale

覆盖控件向辅助技术暴露的无障碍名称。所有字段均为可选,未传入时回退到默认值,因此只需覆盖需要翻译的字段。

| 参数 | 说明 | 类型 | 默认值 |
| ----------------- | ----------------------------------------- | ------ | -------------- |
| alpha | alpha 滑块的 `aria-label` | string | `Alpha` |
| brightness | 明度轴 `aria-valuetext` 中的通道名称 | string | `Brightness` |
| hue | 色相滑块的 `aria-label` | string | `Hue` |
| picker | 饱和度轴与明度轴的 `aria-label` | string | `Color picker` |
| pickerDescription | 饱和度轴与明度轴的 `aria-roledescription` | string | `2D slider` |
| saturation | 饱和度轴 `aria-valuetext` 中的通道名称 | string | `Saturation` |

```tsx | pure
<ColorPicker
locale={{
picker: '颜色选择器',
pickerDescription: '二维滑块',
hue: '色相',
alpha: '透明度',
saturation: '饱和度',
brightness: '明度',
}}
/>
```

二维选择区域为每个轴渲染一个 range 输入框。两者共享 `picker` 名称与 `pickerDescription` 角色描述,并各自通过 `aria-valuetext` 播报自身通道(如 `Saturation: 91%`、`Brightness: 100%`),因此 `saturation` 与 `brightness` 是通道名称而非完整标签。

### Color

Expand All @@ -82,6 +112,33 @@ npm start
| toRgb | 转换为 RGB 对象 | `() => { r: number; g: number; b: number; a: number }` |
| toRgbString | 转换为 RGB 颜色字符串 | `() => string` |

### ColorBlock

独立的色块组件,用于构建自定义触发器与面板。

```tsx | pure
import { ColorBlock } from '@rc-component/color-picker';

<ColorBlock
color="#1677ff"
prefixCls="rc-color-picker"
title="当前颜色"
onClick={handleClick}
/>;
```

| 参数 | 说明 | 类型 | 默认值 |
| --------- | ---------------------- | -------------------------------------- | ------ |
| color | 用于填充色块的颜色 | string | - |
| prefixCls | 组件 className 前缀 | string | - |
| className | 最外层元素的 className | string | - |
| style | 最外层元素的样式 | `React.CSSProperties` | - |
| ...rest | 透传到最外层 `div` | `React.HTMLAttributes<HTMLDivElement>` | - |

`ColorBlockProps` 继承 `React.HTMLAttributes<HTMLDivElement>`,因此任意标准 div 属性或事件处理器(`onClick`、`title`、`role`、`tabIndex`、`data-*`、`aria-*`)都会透传到最外层元素。类型上还存在 `innerClassName` 与 `innerStyle`,但它们是 antd 语义化结构的内部实现,请视为私有。

传入 `onClick` 时,色块还会变成可通过键盘操作的按钮:自动带上 `role="button"`、`tabIndex={0}`、默认取 `color` 的 `aria-label`,并且 `Enter` / `Space` 与鼠标点击等效。你自行传入的 `role`、`tabIndex` 或 `aria-label` 仍会覆盖这些默认值;你的 `onKeyDown` 会与内置的键盘激活组合执行,而不会将其替换——若要阻止激活,可在其中调用 `event.preventDefault()`,与原生按钮的行为一致。未传 `onClick` 时,色块保持为普通的、不可聚焦且无无障碍名称的 `div`。

## 本地开发

```bash
Expand Down
5 changes: 5 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
border: @handler-border-size solid #fff;
border-radius: 50%;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.06);
&:focus-visible,
&:focus-within {
outline: 2px solid #1677ff;
outline-offset: 1px;
}
}
&-handler-sm {
width: @sm-handler-size;
Expand Down
26 changes: 26 additions & 0 deletions src/ColorPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import useColorState from './hooks/useColorState';
import useComponent, { type Components } from './hooks/useComponent';
import type { BaseColorPickerProps, ColorGenInput } from './interface';

const defaultLocale: Required<BaseColorPickerProps>['locale'] = {
picker: 'Color picker',
pickerDescription: '2D slider',
hue: 'Hue',
alpha: 'Alpha',
saturation: 'Saturation',
brightness: 'Brightness',
};

const HUE_COLORS = [
{
color: 'rgb(255, 0, 0)',
Expand Down Expand Up @@ -67,8 +76,22 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
disabledAlpha = false,
disabled = false,
components,
locale,
} = props;

const mergedLocale = useMemo(
() => ({
picker: locale?.picker ?? defaultLocale.picker,
pickerDescription:
locale?.pickerDescription ?? defaultLocale.pickerDescription,
hue: locale?.hue ?? defaultLocale.hue,
alpha: locale?.alpha ?? defaultLocale.alpha,
saturation: locale?.saturation ?? defaultLocale.saturation,
brightness: locale?.brightness ?? defaultLocale.brightness,
}),
[locale],
);

// ========================== Components ==========================
const [Slider] = useComponent(components);

Expand Down Expand Up @@ -134,6 +157,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
<Picker
onChange={handleChange}
{...sharedSliderProps}
locale={mergedLocale}
onChangeComplete={onChangeComplete}
/>
<div className={`${prefixCls}-slider-container`}>
Expand All @@ -151,6 +175,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
value={colorValue.getHue()}
onChange={onHueChange}
onChangeComplete={onHueChangeComplete}
aria-label={mergedLocale.hue}
/>
{!disabledAlpha && (
<Slider
Expand All @@ -165,6 +190,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
value={colorValue.a * 100}
onChange={onAlphaChange}
onChangeComplete={onAlphaChangeComplete}
aria-label={mergedLocale.alpha}
/>
)}
</div>
Expand Down
31 changes: 23 additions & 8 deletions src/components/ColorBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
import { clsx } from 'clsx';
import React from 'react';

export type ColorBlockProps = {
export type ColorBlockProps = React.HTMLAttributes<HTMLDivElement> & {
color: string;
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
/** Internal usage. Only used in antd ColorPicker semantic structure only */
innerClassName?: string;
/** Internal usage. Only used in antd ColorPicker semantic structure only */
innerStyle?: React.CSSProperties;
onClick?: React.MouseEventHandler<HTMLDivElement>;
};

const ColorBlock: React.FC<ColorBlockProps> = ({
color,
prefixCls,
className,
style,
innerClassName,
innerStyle,
onClick,
...props
}) => {
const colorBlockCls = `${prefixCls}-color-block`;

const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
// Compose instead of replace: a consumer handler still runs, and cancelling the event
// opts out of activation the same way it does on a native button.
props.onKeyDown?.(event);

if (event.defaultPrevented) {
return;
}

if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.currentTarget.click();
}
};

return (
<div
{...(props.onClick
? { role: 'button', tabIndex: 0, 'aria-label': color }
: {})}
{...props}
{...(props.onClick ? { onKeyDown } : {})}
className={clsx(colorBlockCls, className)}
style={style}
onClick={onClick}
>
<div
className={clsx(`${colorBlockCls}-inner`, innerClassName)}
Expand Down
Loading