From aecd5526c3e805ac1682a0f23c1a3824ee863c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=B1=9F=E8=BE=B0?= Date: Thu, 1 Feb 2024 23:55:12 +0800 Subject: [PATCH] feat(color-picker): add color-picker component --- packages/arco-vue-docs/locale/en-us.js | 1 + packages/arco-vue-docs/locale/zh-cn.js | 1 + packages/arco-vue-docs/router.ts | 9 + packages/web-vue/components/_utils/color.ts | 215 +++++++++++ packages/web-vue/components/arco-vue.ts | 2 + .../components/color-picker/README.en-US.md | 47 +++ .../components/color-picker/README.zh-CN.md | 45 +++ .../components/color-picker/TEMPLATE.md | 31 ++ .../components/color-picker/__demo__/basic.md | 31 ++ .../color-picker/__demo__/colors.md | 44 +++ .../color-picker/__demo__/disabled.md | 26 ++ .../color-picker/__demo__/format.md | 35 ++ .../color-picker/__demo__/only-panel.md | 26 ++ .../components/color-picker/__demo__/size.md | 28 ++ .../color-picker/__demo__/trigger-element.md | 38 ++ .../color-picker/__demo__/trigger.md | 40 ++ .../__test__/__snapshots__/demo.test.ts.snap | 355 ++++++++++++++++++ .../color-picker/__test__/demo.test.ts | 3 + .../color-picker/__test__/index.test.ts | 28 ++ .../components/color-picker/color-picker.tsx | 260 +++++++++++++ .../web-vue/components/color-picker/colors.ts | 24 ++ .../components/color-picker/control-bar.tsx | 75 ++++ .../color-picker/hooks/use-control-block.ts | 62 +++ .../web-vue/components/color-picker/index.ts | 18 + .../components/color-picker/input-alpha.tsx | 31 ++ .../components/color-picker/input-hex.tsx | 83 ++++ .../components/color-picker/input-rgb.tsx | 57 +++ .../components/color-picker/interface.ts | 17 + .../components/color-picker/palette.tsx | 48 +++ .../web-vue/components/color-picker/panel.tsx | 176 +++++++++ .../components/color-picker/style/index.less | 308 +++++++++++++++ .../components/color-picker/style/index.ts | 2 + .../components/color-picker/style/token.less | 68 ++++ packages/web-vue/components/components.ts | 1 + packages/web-vue/components/index.less | 1 + packages/web-vue/components/index.ts | 2 + .../web-vue/components/locale/interface.ts | 5 + .../web-vue/components/locale/lang/ar-eg.ts | 5 + .../web-vue/components/locale/lang/de-de.ts | 5 + .../web-vue/components/locale/lang/en-us.ts | 5 + .../web-vue/components/locale/lang/es-es.ts | 5 + .../web-vue/components/locale/lang/fr-fr.ts | 5 + .../web-vue/components/locale/lang/id-id.ts | 5 + .../web-vue/components/locale/lang/it-it.ts | 5 + .../web-vue/components/locale/lang/ja-jp.ts | 5 + .../web-vue/components/locale/lang/km-kh.ts | 5 + .../web-vue/components/locale/lang/ko-kr.ts | 5 + .../web-vue/components/locale/lang/ms-my.ts | 5 + .../web-vue/components/locale/lang/pt-pt.ts | 5 + .../web-vue/components/locale/lang/th-th.ts | 5 + .../web-vue/components/locale/lang/vi-vn.ts | 5 + .../web-vue/components/locale/lang/zh-cn.ts | 5 + .../web-vue/components/locale/lang/zh-tw.ts | 5 + 53 files changed, 2323 insertions(+) create mode 100644 packages/web-vue/components/_utils/color.ts create mode 100644 packages/web-vue/components/color-picker/README.en-US.md create mode 100644 packages/web-vue/components/color-picker/README.zh-CN.md create mode 100644 packages/web-vue/components/color-picker/TEMPLATE.md create mode 100644 packages/web-vue/components/color-picker/__demo__/basic.md create mode 100644 packages/web-vue/components/color-picker/__demo__/colors.md create mode 100644 packages/web-vue/components/color-picker/__demo__/disabled.md create mode 100644 packages/web-vue/components/color-picker/__demo__/format.md create mode 100644 packages/web-vue/components/color-picker/__demo__/only-panel.md create mode 100644 packages/web-vue/components/color-picker/__demo__/size.md create mode 100644 packages/web-vue/components/color-picker/__demo__/trigger-element.md create mode 100644 packages/web-vue/components/color-picker/__demo__/trigger.md create mode 100644 packages/web-vue/components/color-picker/__test__/__snapshots__/demo.test.ts.snap create mode 100644 packages/web-vue/components/color-picker/__test__/demo.test.ts create mode 100644 packages/web-vue/components/color-picker/__test__/index.test.ts create mode 100644 packages/web-vue/components/color-picker/color-picker.tsx create mode 100644 packages/web-vue/components/color-picker/colors.ts create mode 100644 packages/web-vue/components/color-picker/control-bar.tsx create mode 100644 packages/web-vue/components/color-picker/hooks/use-control-block.ts create mode 100644 packages/web-vue/components/color-picker/index.ts create mode 100644 packages/web-vue/components/color-picker/input-alpha.tsx create mode 100644 packages/web-vue/components/color-picker/input-hex.tsx create mode 100644 packages/web-vue/components/color-picker/input-rgb.tsx create mode 100644 packages/web-vue/components/color-picker/interface.ts create mode 100644 packages/web-vue/components/color-picker/palette.tsx create mode 100644 packages/web-vue/components/color-picker/panel.tsx create mode 100644 packages/web-vue/components/color-picker/style/index.less create mode 100644 packages/web-vue/components/color-picker/style/index.ts create mode 100644 packages/web-vue/components/color-picker/style/token.less diff --git a/packages/arco-vue-docs/locale/en-us.js b/packages/arco-vue-docs/locale/en-us.js index 3489462e1..b9f0c9dc0 100644 --- a/packages/arco-vue-docs/locale/en-us.js +++ b/packages/arco-vue-docs/locale/en-us.js @@ -94,6 +94,7 @@ export default { scrollbar: 'Scrollbar', watermark: 'Watermark', verificationCode: 'VerificationCode', + colorPicker: 'ColorPicker', }, footer: { design: 'Design', diff --git a/packages/arco-vue-docs/locale/zh-cn.js b/packages/arco-vue-docs/locale/zh-cn.js index 9ff713667..f12fde02f 100644 --- a/packages/arco-vue-docs/locale/zh-cn.js +++ b/packages/arco-vue-docs/locale/zh-cn.js @@ -94,6 +94,7 @@ export default { scrollbar: '滚动条 Scrollbar', watermark: '水印 Watermark', verificationCode: '验证码输入框 VerificationCode', + colorPicker: '颜色选择器 ColorPicker', }, footer: { design: '设计', diff --git a/packages/arco-vue-docs/router.ts b/packages/arco-vue-docs/router.ts index 71ae186e4..d3c72f762 100644 --- a/packages/arco-vue-docs/router.ts +++ b/packages/arco-vue-docs/router.ts @@ -191,6 +191,10 @@ const VerificationCode = () => import('@web-vue/components/verification-code/README.zh-CN.md'); const VerificationCodeEn = () => import('@web-vue/components/verification-code/README.en-US.md'); +const ColorPicker = () => + import('@web-vue/components/color-picker/README.zh-CN.md'); +const ColorPickerEn = () => + import('@web-vue/components/color-picker/README.en-US.md'); const docs = [ { @@ -459,6 +463,11 @@ const components = [ component: Checkbox, componentEn: CheckboxEn, }, + { + name: 'colorPicker', + component: ColorPicker, + componentEn: ColorPickerEn, + }, { name: 'datePicker', component: DatePicker, diff --git a/packages/web-vue/components/_utils/color.ts b/packages/web-vue/components/_utils/color.ts new file mode 100644 index 000000000..feff96517 --- /dev/null +++ b/packages/web-vue/components/_utils/color.ts @@ -0,0 +1,215 @@ +// https://github.com/scttcper/tinycolor +export const hsvToRgb = (h: number, s: number, v: number) => { + const i = Math.floor(h * 6); + const f = h * 6 - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + const mod = i % 6; + const r = [v, q, p, p, t, v][mod]; + const g = [t, v, v, q, p, p][mod]; + const b = [p, p, t, v, v, q][mod]; + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255), + }; +}; + +export const rgbToHsv = (r: number, g: number, b: number) => { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + const v = max; + const d = max - min; + const s = max === 0 ? 0 : d / max; + + if (max === min) { + h = 0; + } else { + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + default: + break; + } + + h /= 6; + } + + return { h, s, v }; +}; + +// +const CSS_INTEGER = '[-\\+]?\\d+%?'; + +// +const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?'; + +// Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome. +const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`; + +// Actual matching. +// Parentheses and commas are optional, but not required. +// Whitespace can take the place of commas or opening paren +const PERMISSIVE_MATCH3 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`; +const PERMISSIVE_MATCH4 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`; + +const matchers = { + rgb: new RegExp(`rgb${PERMISSIVE_MATCH3}`), + rgba: new RegExp(`rgba${PERMISSIVE_MATCH4}`), + hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, + hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, + hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, + hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, +}; + +export const parseIntFromHex = (val: string): number => { + return parseInt(val, 16); +}; + +export const convertHexToDecimal = (h: string): number => { + return parseIntFromHex(h) / 255; +}; + +export const formatInputToRgb = ( + color: string +): { r: number; g: number; b: number; a?: number } | false => { + let match = matchers.rgb.exec(color); + if (match) { + return { + r: parseInt(match[1], 10), + g: parseInt(match[2], 10), + b: parseInt(match[3], 10), + }; + } + + match = matchers.rgba.exec(color); + if (match) { + return { + r: parseInt(match[1], 10), + g: parseInt(match[2], 10), + b: parseInt(match[3], 10), + a: parseFloat(match[4]), + }; + } + + match = matchers.hex8.exec(color); + if (match) { + return { + r: parseIntFromHex(match[1]), + g: parseIntFromHex(match[2]), + b: parseIntFromHex(match[3]), + a: convertHexToDecimal(match[4]), + }; + } + + match = matchers.hex6.exec(color); + if (match) { + return { + r: parseIntFromHex(match[1]), + g: parseIntFromHex(match[2]), + b: parseIntFromHex(match[3]), + }; + } + + match = matchers.hex4.exec(color); + if (match) { + return { + r: parseIntFromHex(match[1] + match[1]), + g: parseIntFromHex(match[2] + match[2]), + b: parseIntFromHex(match[3] + match[3]), + a: convertHexToDecimal(match[4] + match[4]), + }; + } + + match = matchers.hex3.exec(color); + if (match) { + return { + r: parseIntFromHex(match[1] + match[1]), + g: parseIntFromHex(match[2] + match[2]), + b: parseIntFromHex(match[3] + match[3]), + }; + } + + return false; +}; + +export const formatInputToHSVA = (color: string) => { + const rgba = formatInputToRgb(color); + if (rgba) { + const hsv = rgbToHsv(rgba.r, rgba.g, rgba.b); + return { + ...hsv, + a: rgba.a ?? 1, + }; + } + return { + h: 0, + s: 1, + v: 1, + a: 1, + }; +}; + +export const hexToRgb = (color: string): any => { + color = color.trim().toLowerCase(); + if (color.length === 0) { + return false; + } + + let match = matchers.hex6.exec(color); + if (match) { + return { + r: parseIntFromHex(match[1]), + g: parseIntFromHex(match[2]), + b: parseIntFromHex(match[3]), + }; + } + + match = matchers.hex3.exec(color); + if (match) { + return { + r: parseIntFromHex(match[1] + match[1]), + g: parseIntFromHex(match[2] + match[2]), + b: parseIntFromHex(match[3] + match[3]), + }; + } + + return false; +}; + +export const rgbToHex = (r: number, g: number, b: number) => { + const hex = [ + Math.round(r).toString(16).padStart(2, '0'), + Math.round(g).toString(16).padStart(2, '0'), + Math.round(b).toString(16).padStart(2, '0'), + ]; + + return hex.join('').toUpperCase(); +}; + +export const rgbaToHex = (r: number, g: number, b: number, a: number) => { + const hex = [ + Math.round(r).toString(16).padStart(2, '0'), + Math.round(g).toString(16).padStart(2, '0'), + Math.round(b).toString(16).padStart(2, '0'), + Math.round(a * 255) + .toString(16) + .padStart(2, '0'), + ]; + + return hex.join('').toUpperCase(); +}; diff --git a/packages/web-vue/components/arco-vue.ts b/packages/web-vue/components/arco-vue.ts index 78935a4ff..a45020b85 100644 --- a/packages/web-vue/components/arco-vue.ts +++ b/packages/web-vue/components/arco-vue.ts @@ -16,6 +16,7 @@ import Carousel, { CarouselItem } from './carousel'; import Cascader, { CascaderPanel } from './cascader'; import Checkbox, { CheckboxGroup } from './checkbox'; import Collapse, { CollapseItem } from './collapse'; +import ColorPicker from './color-picker'; import Comment from './comment'; import ConfigProvider from './config-provider'; import DatePicker, { @@ -116,6 +117,7 @@ const components: Record = { Carousel, Collapse, Comment, + ColorPicker, Descriptions, Empty, Image, diff --git a/packages/web-vue/components/color-picker/README.en-US.md b/packages/web-vue/components/color-picker/README.en-US.md new file mode 100644 index 000000000..cba15faf7 --- /dev/null +++ b/packages/web-vue/components/color-picker/README.en-US.md @@ -0,0 +1,47 @@ +```yaml +meta: + type: Component + category: Data Entry +title: ColorPicker +description: Used for select and display colors. +``` + +*Auto translate by google.* + +@import ./__demo__/basic.md +@import ./__demo__/size.md +@import ./__demo__/disabled.md +@import ./__demo__/format.md +@import ./__demo__/colors.md +@import ./__demo__/trigger.md +@import ./__demo__/trigger-element.md +@import ./__demo__/only-panel.md + +## API + + +### `` Props + +|Attribute|Description|Type|Default| +|---|---|---|:---:| +|model-value **(v-model)**|Value|`string`|`-`| +|default-value|Default value (uncontrolled state)|`string`|`-`| +|format|Color value format|`'hex' \| 'rgb'`|`-`| +|size|Size|`'mini' \| 'small' \| 'medium' \| 'large'`|`'medium'`| +|show-text|Show color value|`boolean`|`false`| +|show-history|Show history colors|`boolean`|`false`| +|show-preset|Show preset colors|`boolean`|`false`| +|disabled|disabled|`boolean`|`false`| +|disabled-alpha|Disable transparency channel|`boolean`|`false`| +|hide-trigger|There is no trigger element, only the color panel is displayed|`boolean`|`false`| +|trigger-props|Can accept Props of all [Trigger](/vue/component/trigger) components|`Partial`|`-`| +|history-colors|Color array of historical colors|`string[]`|`-`| +|preset-colors|Color array of preset colors|`string[]`|`() => colors`| +### `` Events + +|Event Name|Description|Parameters| +|---|---|---| +|change|Triggered when the color value changes|value: `string`| +|popup-visible-change|Triggered when the color panel is expanded and collapsed|visible: `boolean`
value: `string`| + + diff --git a/packages/web-vue/components/color-picker/README.zh-CN.md b/packages/web-vue/components/color-picker/README.zh-CN.md new file mode 100644 index 000000000..b460395b7 --- /dev/null +++ b/packages/web-vue/components/color-picker/README.zh-CN.md @@ -0,0 +1,45 @@ +```yaml +meta: + type: 组件 + category: 数据输入 +title: 颜色选择器 ColorPicker +description: 用于选择和展示颜色 +``` + +@import ./__demo__/basic.md +@import ./__demo__/size.md +@import ./__demo__/disabled.md +@import ./__demo__/format.md +@import ./__demo__/colors.md +@import ./__demo__/trigger.md +@import ./__demo__/trigger-element.md +@import ./__demo__/only-panel.md + +## API + + +### `` Props + +|参数名|描述|类型|默认值| +|---|---|---|:---:| +|model-value **(v-model)**|绑定值|`string`|`-`| +|default-value|默认值(非受控状态)|`string`|`-`| +|format|颜色值的格式|`'hex' \| 'rgb'`|`-`| +|size|尺寸|`'mini' \| 'small' \| 'medium' \| 'large'`|`'medium'`| +|show-text|显示颜色值|`boolean`|`false`| +|show-history|显示历史颜色|`boolean`|`false`| +|show-preset|显示预设颜色|`boolean`|`false`| +|disabled|禁用|`boolean`|`false`| +|disabled-alpha|禁用透明通道|`boolean`|`false`| +|hide-trigger|没有触发元素,只显示颜色面板|`boolean`|`false`| +|trigger-props|接受所有 [Trigger](/vue/component/trigger) 组件的Props|`Partial`|`-`| +|history-colors|历史颜色的颜色数组|`string[]`|`-`| +|preset-colors|预设颜色的颜色数组|`string[]`|`() => colors`| +### `` Events + +|事件名|描述|参数| +|---|---|---| +|change|颜色值改变时触发|value: `string`| +|popup-visible-change|颜色面板展开和收起时触发|visible: `boolean`
value: `string`| + + diff --git a/packages/web-vue/components/color-picker/TEMPLATE.md b/packages/web-vue/components/color-picker/TEMPLATE.md new file mode 100644 index 000000000..79f7151b0 --- /dev/null +++ b/packages/web-vue/components/color-picker/TEMPLATE.md @@ -0,0 +1,31 @@ +## zh-CN +```yaml +meta: + type: 组件 + category: 数据输入 +title: 颜色选择器 ColorPicker +description: 用于选择和展示颜色 +``` +--- +## en-US +```yaml +meta: + type: Component + category: Data Entry +title: ColorPicker +description: Used for select and display colors. +``` +--- + +@import ./__demo__/basic.md +@import ./__demo__/size.md +@import ./__demo__/disabled.md +@import ./__demo__/format.md +@import ./__demo__/colors.md +@import ./__demo__/trigger.md +@import ./__demo__/trigger-element.md +@import ./__demo__/only-panel.md + +## API + +%%API(color-picker.tsx)%% diff --git a/packages/web-vue/components/color-picker/__demo__/basic.md b/packages/web-vue/components/color-picker/__demo__/basic.md new file mode 100644 index 000000000..45e1473c5 --- /dev/null +++ b/packages/web-vue/components/color-picker/__demo__/basic.md @@ -0,0 +1,31 @@ +```yaml +title: + zh-CN: 基本使用 + en-US: Basic Usage +``` + +## zh-CN + +基本用法 + +--- + +## en-US + +Basic usage + +--- + +```vue + + + +``` diff --git a/packages/web-vue/components/color-picker/__demo__/colors.md b/packages/web-vue/components/color-picker/__demo__/colors.md new file mode 100644 index 000000000..9fcafcdcb --- /dev/null +++ b/packages/web-vue/components/color-picker/__demo__/colors.md @@ -0,0 +1,44 @@ +```yaml +title: + zh-CN: 预设颜色和历史颜色 + en-US: Preset & History +``` + +## zh-CN + +通过 `showPreset` 和 `showHistory` 开启预设颜色和历史颜色区域。历史颜色需要用户自行控制展示内容。 + +--- + +## en-US + +The preset color and history color areas can be opened through `showPreset` and `showHistory`. Historical colors require users to control the display content themselves. + +--- + +```vue + + + +``` diff --git a/packages/web-vue/components/color-picker/__demo__/disabled.md b/packages/web-vue/components/color-picker/__demo__/disabled.md new file mode 100644 index 000000000..6bde44b19 --- /dev/null +++ b/packages/web-vue/components/color-picker/__demo__/disabled.md @@ -0,0 +1,26 @@ +```yaml +title: + zh-CN: 禁用 + en-US: Disabled +``` + +## zh-CN + +设置 `disabled` 禁用选择器。 + +--- + +## en-US + +Set `disabled` to disable the selector. + +--- + +```vue + +``` diff --git a/packages/web-vue/components/color-picker/__demo__/format.md b/packages/web-vue/components/color-picker/__demo__/format.md new file mode 100644 index 000000000..570c54602 --- /dev/null +++ b/packages/web-vue/components/color-picker/__demo__/format.md @@ -0,0 +1,35 @@ +```yaml +title: + zh-CN: 颜色格式 + en-US: Color format +``` + +## zh-CN + +通过 `format` 设置颜色值的格式,支持 `hex` 和 `rgb`。 + +--- + +## en-US + +Set the format of the color value through `format`, supporting `hex` and `rgb`. + +--- + +```vue + + + +``` diff --git a/packages/web-vue/components/color-picker/__demo__/only-panel.md b/packages/web-vue/components/color-picker/__demo__/only-panel.md new file mode 100644 index 000000000..06b14488c --- /dev/null +++ b/packages/web-vue/components/color-picker/__demo__/only-panel.md @@ -0,0 +1,26 @@ +```yaml +title: + zh-CN: 只使用面板 + en-US: Only Panel +``` + +## zh-CN + +只用颜色选择面板。 + +--- + +## en-US + +Only use the color selection panel. + +--- + +```vue + +``` diff --git a/packages/web-vue/components/color-picker/__demo__/size.md b/packages/web-vue/components/color-picker/__demo__/size.md new file mode 100644 index 000000000..16a908de2 --- /dev/null +++ b/packages/web-vue/components/color-picker/__demo__/size.md @@ -0,0 +1,28 @@ +```yaml +title: + zh-CN: 尺寸 + en-US: Size +``` + +## zh-CN + +颜色选择器定义了四种尺寸(`mini`,`small`, `medium`, `large`),分别为 24px,28px,32px,36px。 + +--- + +## en-US + +ColorPicker defines four sizes (`mini`, `small`, `medium`, `large`), which are 24px, 28px, 32px, and 36px. + +--- + +```vue + +``` diff --git a/packages/web-vue/components/color-picker/__demo__/trigger-element.md b/packages/web-vue/components/color-picker/__demo__/trigger-element.md new file mode 100644 index 000000000..a37495002 --- /dev/null +++ b/packages/web-vue/components/color-picker/__demo__/trigger-element.md @@ -0,0 +1,38 @@ +```yaml +title: + zh-CN: 自定义触发元素 + en-US: Customize trigger element +``` + +## zh-CN + +自定义触发元素。 + +--- + +## en-US + +Customize trigger element. + +--- + +```vue + + + +``` diff --git a/packages/web-vue/components/color-picker/__demo__/trigger.md b/packages/web-vue/components/color-picker/__demo__/trigger.md new file mode 100644 index 000000000..5b910372e --- /dev/null +++ b/packages/web-vue/components/color-picker/__demo__/trigger.md @@ -0,0 +1,40 @@ +```yaml +title: + zh-CN: 触发器 + en-US: Trigger +``` + +## zh-CN + +可以通过 `trigger-props` 设置触发器的所有属性。 + +--- + +## en-US + +You can set the properties of the trigger through `trigger-props`. + +--- + +```vue + + + +``` diff --git a/packages/web-vue/components/color-picker/__test__/__snapshots__/demo.test.ts.snap b/packages/web-vue/components/color-picker/__test__/__snapshots__/demo.test.ts.snap new file mode 100644 index 000000000..c11272c13 --- /dev/null +++ b/packages/web-vue/components/color-picker/__test__/__snapshots__/demo.test.ts.snap @@ -0,0 +1,355 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` demo: render [basic] correctly 1`] = ` +"
+ +
+
+
+ +
+ +
+ +
+
+
+
#165DFF
+
+ +
+
" +`; + +exports[` demo: render [colors] correctly 1`] = ` +"
+
+ +
+" +`; + +exports[` demo: render [disabled] correctly 1`] = ` +"
+ +
+
+
+ +
+ +
+ +
+
+
+
#165DFF
+
+ +
+
" +`; + +exports[` demo: render [format] correctly 1`] = ` +"
+ +
+ +
+
+
+
#165DFF
+
+ +
+
" +`; + +exports[` demo: render [only-panel] correctly 1`] = ` +"
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Hex + + +
+
# + + %
+
+
+
+
+
+
+
最近使用顏色
+
暂无
+
+
+
系统预设颜色
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Hex + + +
+
# + + %
+
+
+
+
+
+ +
+
系统预设颜色
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
" +`; + +exports[` demo: render [size] correctly 1`] = ` +"
+ +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+
" +`; + +exports[` demo: render [trigger] correctly 1`] = ` +"
+ +
+ +
+
+
+ +
+ +
+
" +`; + +exports[` demo: render [trigger-element] correctly 1`] = ` +"
+ +
#165DFF + + + +
+
" +`; diff --git a/packages/web-vue/components/color-picker/__test__/demo.test.ts b/packages/web-vue/components/color-picker/__test__/demo.test.ts new file mode 100644 index 000000000..8aad704a0 --- /dev/null +++ b/packages/web-vue/components/color-picker/__test__/demo.test.ts @@ -0,0 +1,3 @@ +import demoTest from '../../../scripts/demo-test'; + +demoTest('color-picker'); diff --git a/packages/web-vue/components/color-picker/__test__/index.test.ts b/packages/web-vue/components/color-picker/__test__/index.test.ts new file mode 100644 index 000000000..3f19e2368 --- /dev/null +++ b/packages/web-vue/components/color-picker/__test__/index.test.ts @@ -0,0 +1,28 @@ +import { mount } from '@vue/test-utils'; +import ColorPicker from '../index'; + +describe('ColorPicker', () => { + test('Whether the size is rendered correctly', () => { + const wrapper = mount(ColorPicker, { + props: { + size: 'mini', + }, + }); + const colorPickerElement = wrapper.find('.arco-color-picker'); + expect(colorPickerElement.classes()).toContain( + `arco-color-picker-size-mini` + ); + }); + + test('Whether the disabled is rendered correctly', () => { + const wrapper = mount(ColorPicker, { + props: { + disabled: true, + }, + }); + const colorPickerElement = wrapper.find('.arco-color-picker'); + expect(colorPickerElement.classes()).toContain( + `arco-color-picker-disabled` + ); + }); +}); diff --git a/packages/web-vue/components/color-picker/color-picker.tsx b/packages/web-vue/components/color-picker/color-picker.tsx new file mode 100644 index 000000000..d67e44440 --- /dev/null +++ b/packages/web-vue/components/color-picker/color-picker.tsx @@ -0,0 +1,260 @@ +import { defineComponent, PropType, computed, reactive, watch } from 'vue'; +import { Size } from '../_utils/constant'; +import { getPrefixCls } from '../_utils/global-config'; +import { colors } from './colors'; +import { HSV } from './interface'; +import Panel from './panel'; +import Trigger, { TriggerProps } from '../trigger'; +import useMergeState from '../_hooks/use-merge-state'; +import useState from '../_hooks/use-state'; +import { + formatInputToHSVA, + hsvToRgb, + rgbaToHex, + rgbToHex, +} from '../_utils/color'; + +export default defineComponent({ + name: 'ColorPicker', + props: { + /** + * @zh 绑定值 + * @en Value + */ + modelValue: String, + /** + * @zh 默认值(非受控状态) + * @en Default value (uncontrolled state) + */ + defaultValue: { + type: String, + }, + /** + * @zh 颜色值的格式 + * @en Color value format + */ + format: { + type: String as PropType<'hex' | 'rgb'>, + }, + /** + * @zh 尺寸 + * @en Size + * @values 'mini','small','medium','large' + */ + size: { + type: String as PropType, + default: 'medium', + }, + /** + * @zh 显示颜色值 + * @en Show color value + */ + showText: { + type: Boolean, + default: false, + }, + /** + * @zh 显示历史颜色 + * @en Show history colors + */ + showHistory: { + type: Boolean, + default: false, + }, + /** + * @zh 显示预设颜色 + * @en Show preset colors + */ + showPreset: { + type: Boolean, + default: false, + }, + /** + * @zh 禁用 + * @en disabled + */ + disabled: { + type: Boolean, + default: false, + }, + /** + * @zh 禁用透明通道 + * @en Disable transparency channel + */ + disabledAlpha: { + type: Boolean, + default: false, + }, + /** + * @zh 没有触发元素,只显示颜色面板 + * @en There is no trigger element, only the color panel is displayed + * */ + hideTrigger: { + type: Boolean, + }, + /** + * @zh 接受所有 [Trigger](/vue/component/trigger) 组件的Props + * @en Can accept Props of all [Trigger](/vue/component/trigger) components + * */ + triggerProps: { + type: Object as PropType>, + }, + /** + * @zh 历史颜色的颜色数组 + * @en Color array of historical colors + */ + historyColors: { + type: Array as PropType, + }, + /** + * @zh 预设颜色的颜色数组 + * @en Color array of preset colors + */ + presetColors: { + type: Array as PropType, + default: () => colors, + }, + }, + emits: { + 'update:modelValue': (value: string) => true, + /** + * @zh 颜色值改变时触发 + * @en Triggered when the color value changes + * @param {string} value + */ + 'change': (value: string) => true, + /** + * @zh 颜色面板展开和收起时触发 + * @en Triggered when the color panel is expanded and collapsed + * @param {boolean} visible + * @param {string} value + */ + 'popup-visible-change': (visible: boolean, value: string) => true, + }, + setup(props, { emit, slots }) { + const prefixCls = getPrefixCls('color-picker'); + const [mergeValue, setMergeValue] = useMergeState( + props.defaultValue, + reactive({ value: props.modelValue }) + ); + + const formatInput = computed(() => { + return formatInputToHSVA(mergeValue.value || ''); + }); + + const [alpha, setAlpha] = useState(formatInput.value.a); + const [hsv, setHsv] = useState({ + h: formatInput.value.h, + s: formatInput.value.s, + v: formatInput.value.v, + }); + + const color = computed(() => { + const rgb = hsvToRgb(hsv.value.h, hsv.value.s, hsv.value.v); + const hex = rgbToHex(rgb.r, rgb.g, rgb.b); + return { + hsv: hsv.value, + rgb, + hex, + }; + }); + + const colorString = computed(() => { + const { r, g, b } = color.value.rgb; + return `rgba(${r}, ${g}, ${b}, ${alpha.value.toFixed(2)})`; + }); + + const formatValue = computed(() => { + const { r, g, b } = color.value.rgb; + if (props.format === 'rgb') { + return alpha.value < 1 + ? `rgba(${r}, ${g}, ${b}, ${alpha.value.toFixed(2)})` + : `rgb(${r}, ${g}, ${b})`; + } + return alpha.value < 1 + ? `#${rgbaToHex(r, g, b, alpha.value)}` + : `#${rgbToHex(r, g, b)}`; + }); + + watch(formatValue, (value) => { + setMergeValue(value); + emit('update:modelValue', value); + emit('change', value); + }); + + const onHsvChange = (_value: HSV) => { + !props.disabled && setHsv(_value); + }; + + const onAlphaChange = (_value: number) => { + !props.disabled && setAlpha(_value); + }; + + const onPopupVisibleChange = (visible: boolean) => { + emit('popup-visible-change', visible, formatValue.value); + }; + + const renderInput = () => { + return ( +
+
+ {props.showText && ( +
{formatValue.value}
+ )} + +
+ ); + }; + + const renderPanel = () => { + return ( + + ); + }; + + return () => { + return props.hideTrigger ? ( + renderPanel() + ) : ( + + {slots.default ? slots.default() : renderInput()} + + ); + }; + }, +}); diff --git a/packages/web-vue/components/color-picker/colors.ts b/packages/web-vue/components/color-picker/colors.ts new file mode 100644 index 000000000..a8fa5bb01 --- /dev/null +++ b/packages/web-vue/components/color-picker/colors.ts @@ -0,0 +1,24 @@ +export const colors = [ + '#00B42A', + '#3C7EFF', + '#FF7D00', + '#F76965', + '#F7BA1E', + '#F5319D', + '#D91AD9', + '#9FDB1D', + '#FADC19', + '#722ED1', + '#3491FA', + '#7BE188', + '#93BEFF', + '#FFCF8B', + '#FBB0A7', + '#FCE996', + '#FB9DC7', + '#F08EE6', + '#DCF190', + '#FDFA94', + '#C396ED', + '#9FD4FD', +]; diff --git a/packages/web-vue/components/color-picker/control-bar.tsx b/packages/web-vue/components/color-picker/control-bar.tsx new file mode 100644 index 000000000..dfa1b13e4 --- /dev/null +++ b/packages/web-vue/components/color-picker/control-bar.tsx @@ -0,0 +1,75 @@ +import { defineComponent, PropType, toRefs, computed } from 'vue'; +import { getPrefixCls } from '../_utils/global-config'; +import { Color } from './interface'; +import { useControlBlock } from './hooks/use-control-block'; + +export default defineComponent({ + name: 'ControlBar', + props: { + x: { + type: Number, + required: true, + }, + color: { + type: Object as PropType, + required: true, + }, + colorString: String, + type: String as PropType<'hue' | 'alpha'>, + onChange: Function as PropType<(x: number) => void>, + }, + setup(props) { + const prefixCls = getPrefixCls('color-picker'); + const rgb = computed(() => props.color.rgb); + + const { blockRef, handlerRef, onMouseDown } = useControlBlock({ + value: [props.x, 0], + onChange: (pos) => props.onChange?.(pos[0]), + }); + + const renderHandler = () => { + return ( +
+ ); + }; + + return () => { + if (props.type === 'alpha') { + return ( +
+
+ {renderHandler()} +
+
+ ); + } + return ( +
+ {renderHandler()} +
+ ); + }; + }, +}); diff --git a/packages/web-vue/components/color-picker/hooks/use-control-block.ts b/packages/web-vue/components/color-picker/hooks/use-control-block.ts new file mode 100644 index 000000000..442d10d98 --- /dev/null +++ b/packages/web-vue/components/color-picker/hooks/use-control-block.ts @@ -0,0 +1,62 @@ +import { ref } from 'vue'; + +interface ControlBlockParams { + value: [number, number]; + onChange: (value: [number, number]) => void; +} + +export const useControlBlock = ({ value, onChange }: ControlBlockParams) => { + const active = ref(false); + const blockRef = ref(); + const handlerRef = ref(); + + const getPercentNumber = (value: number, max: number) => { + if (value < 0) return 0; + if (value > max) return 1; + return value / max; + }; + + const setCurrentPosition = (ev: MouseEvent) => { + if (!blockRef.value) return; + const { clientX, clientY } = ev; + const rect = blockRef.value.getBoundingClientRect(); + const newValue: [number, number] = [ + getPercentNumber(clientX - rect.x, rect.width), + getPercentNumber(clientY - rect.y, rect.height), + ]; + if (newValue[0] !== value[0] || newValue[1] !== value[1]) { + onChange?.(newValue); + } + }; + + const removeListener = () => { + active.value = false; + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', removeListener); + window.removeEventListener('contextmenu', removeListener); + }; + + const onMouseDown = (ev: MouseEvent) => { + active.value = true; + setCurrentPosition(ev); + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', removeListener); + window.addEventListener('contextmenu', removeListener); + }; + + function onMouseMove(ev: MouseEvent) { + ev.preventDefault(); + if (ev.buttons > 0) { + setCurrentPosition(ev); + } else { + removeListener(); + } + } + + return { + active, + blockRef, + handlerRef, + onMouseDown, + }; +}; diff --git a/packages/web-vue/components/color-picker/index.ts b/packages/web-vue/components/color-picker/index.ts new file mode 100644 index 000000000..038dae9da --- /dev/null +++ b/packages/web-vue/components/color-picker/index.ts @@ -0,0 +1,18 @@ +import type { App } from 'vue'; +import type { ArcoOptions } from '../_utils/types'; +import { setGlobalConfig, getComponentPrefix } from '../_utils/global-config'; +import _ColorPicker from './color-picker'; + +const ColorPicker = Object.assign(_ColorPicker, { + install: (app: App, options?: ArcoOptions) => { + setGlobalConfig(app, options); + const componentPrefix = getComponentPrefix(options); + + app.component(componentPrefix + _ColorPicker.name, _ColorPicker); + }, +}); + +export type { RGB, HSV, Color } from './interface'; +export type ColorPickerInstance = InstanceType; + +export default ColorPicker; diff --git a/packages/web-vue/components/color-picker/input-alpha.tsx b/packages/web-vue/components/color-picker/input-alpha.tsx new file mode 100644 index 000000000..15b6ae8d5 --- /dev/null +++ b/packages/web-vue/components/color-picker/input-alpha.tsx @@ -0,0 +1,31 @@ +import { defineComponent, PropType } from 'vue'; +import { getPrefixCls } from '../_utils/global-config'; +import InputNumber from '../input-number'; + +export default defineComponent({ + name: 'InputAlpha', + props: { + value: { + type: Number, + required: true, + }, + disabled: Boolean, + onChange: Function as PropType<(value: number) => void>, + }, + setup(props) { + const prefixCls = getPrefixCls('color-picker'); + + return () => ( + '%' }} + min={0} + max={100} + disabled={props.disabled} + modelValue={Math.round(props.value * 100)} + onChange={(a = 100) => props.onChange?.(a / 100)} + /> + ); + }, +}); diff --git a/packages/web-vue/components/color-picker/input-hex.tsx b/packages/web-vue/components/color-picker/input-hex.tsx new file mode 100644 index 000000000..08693e44a --- /dev/null +++ b/packages/web-vue/components/color-picker/input-hex.tsx @@ -0,0 +1,83 @@ +import { defineComponent, PropType, toRefs, watch } from 'vue'; +import { getPrefixCls } from '../_utils/global-config'; +import { Color, HSV } from './interface'; +import { hexToRgb, rgbToHsv } from '../_utils/color'; +import useState from '../_hooks/use-state'; +import Input, { InputGroup } from '../input'; +import InputAlpha from './input-alpha'; + +export default defineComponent({ + name: 'InputHex', + props: { + color: { + type: Object as PropType, + required: true, + }, + alpha: { + type: Number, + required: true, + }, + disabled: Boolean, + disabledAlpha: Boolean, + onHsvChange: Function as PropType<(value: HSV) => void>, + onAlphaChange: Function as PropType<(value: number) => void>, + }, + setup(props) { + const prefixCls = getPrefixCls('color-picker'); + const { color } = toRefs(props); + const [hex, setHex] = useState(color.value.hex); + + const handlerChange = (value: string) => { + const _rgb = hexToRgb(value) || { r: 255, g: 0, b: 0 }; + const hsv = rgbToHsv(_rgb.r, _rgb.g, _rgb.b); + props.onHsvChange?.(hsv); + }; + + const onInputChange = (value: string) => { + const matchValue = value.match(/[a-fA-F0-9]*/g)?.join('') ?? ''; + if (matchValue !== color.value.hex) { + handlerChange(matchValue.toUpperCase()); + } + }; + + const onPaste = (ev: ClipboardEvent) => { + if (!ev.clipboardData) return; + let text = ev.clipboardData.getData('Text'); + if (text.startsWith('#')) { + text = text.slice(1); + } + onInputChange(text); + ev.preventDefault(); + }; + + watch(color, () => { + if (color.value.hex !== hex.value) { + setHex(color.value.hex); + } + }); + + return () => ( + + '#' }} + size="mini" + maxLength={6} + disabled={props.disabled} + modelValue={hex.value} + onInput={setHex} + onChange={onInputChange} + onBlur={() => handlerChange} + onPressEnter={() => handlerChange} + // @ts-ignore + onPaste={onPaste} + /> + + + ); + }, +}); diff --git a/packages/web-vue/components/color-picker/input-rgb.tsx b/packages/web-vue/components/color-picker/input-rgb.tsx new file mode 100644 index 000000000..28a4513e4 --- /dev/null +++ b/packages/web-vue/components/color-picker/input-rgb.tsx @@ -0,0 +1,57 @@ +import { defineComponent, PropType, toRefs } from 'vue'; +import { getPrefixCls } from '../_utils/global-config'; +import { Color, HSV, RGB } from './interface'; +import { rgbToHsv } from '../_utils/color'; +import { InputGroup } from '../input'; +import InputNumber from '../input-number'; +import InputAlpha from './input-alpha'; + +export default defineComponent({ + name: 'InputRgb', + props: { + color: { + type: Object as PropType, + required: true, + }, + alpha: { + type: Number, + required: true, + }, + disabled: Boolean, + disabledAlpha: Boolean, + onHsvChange: Function as PropType<(value: HSV) => void>, + onAlphaChange: Function as PropType<(value: number) => void>, + }, + setup(props) { + const prefixCls = getPrefixCls('color-picker'); + const { color } = toRefs(props); + + const handleChange = (value: Partial) => { + const newRGB = { ...color.value.rgb, ...value }; + const hsv = rgbToHsv(newRGB.r, newRGB.g, newRGB.b); + props.onHsvChange?.(hsv); + }; + + return () => ( + + {(['r', 'g', 'b'] as Array).map((channel) => ( + handleChange({ [channel]: val })} + /> + ))} + + + ); + }, +}); diff --git a/packages/web-vue/components/color-picker/interface.ts b/packages/web-vue/components/color-picker/interface.ts new file mode 100644 index 000000000..5b2f0480e --- /dev/null +++ b/packages/web-vue/components/color-picker/interface.ts @@ -0,0 +1,17 @@ +export interface RGB { + r: number; + g: number; + b: number; +} + +export interface HSV { + h: number; + s: number; + v: number; +} + +export interface Color { + hsv: HSV; + rgb: RGB; + hex: string; +} diff --git a/packages/web-vue/components/color-picker/palette.tsx b/packages/web-vue/components/color-picker/palette.tsx new file mode 100644 index 000000000..d7218dbe5 --- /dev/null +++ b/packages/web-vue/components/color-picker/palette.tsx @@ -0,0 +1,48 @@ +import { defineComponent, computed, PropType, toRefs, watch } from 'vue'; +import { getPrefixCls } from '../_utils/global-config'; +import { hsvToRgb } from '../_utils/color'; +import { Color } from './interface'; +import { useControlBlock } from './hooks/use-control-block'; + +export default defineComponent({ + name: 'Palette', + props: { + color: { + type: Object as PropType, + required: true, + }, + onChange: Function as PropType<(s: number, v: number) => void>, + }, + setup(props) { + const prefixCls = getPrefixCls('color-picker'); + const hsv = computed(() => props.color.hsv); + + const { blockRef, handlerRef, onMouseDown } = useControlBlock({ + value: [hsv.value.s, 1 - hsv.value.v], + onChange: (value) => props.onChange?.(value[0], 1 - value[1]), + }); + + const hueColor = computed(() => { + const rgb = hsvToRgb(hsv.value.h, 1, 1); + return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; + }); + + return () => ( +
+
+
+ ); + }, +}); diff --git a/packages/web-vue/components/color-picker/panel.tsx b/packages/web-vue/components/color-picker/panel.tsx new file mode 100644 index 000000000..c6a20459d --- /dev/null +++ b/packages/web-vue/components/color-picker/panel.tsx @@ -0,0 +1,176 @@ +import { PropType, computed, defineComponent, ref } from 'vue'; +import { getPrefixCls } from '../_utils/global-config'; +import { hexToRgb, rgbToHsv } from '../_utils/color'; +import { Color, HSV } from './interface'; +import { useI18n } from '../locale'; +import useState from '../_hooks/use-state'; +import ControlBar from './control-bar'; +import Palette from './palette'; +import Select from '../select'; +import InputRgb from './input-rgb'; +import InputHex from './input-hex'; + +export default defineComponent({ + name: 'Panel', + props: { + color: { + type: Object as PropType, + required: true, + }, + alpha: { + type: Number, + required: true, + }, + colorString: String, + disabled: Boolean, + disabledAlpha: Boolean, + showHistory: Boolean, + showPreset: Boolean, + format: String as PropType<'hex' | 'rgb'>, + historyColors: Array as PropType, + presetColors: Array as PropType, + onAlphaChange: Function as PropType<(alpha: number) => void>, + onHsvChange: Function as PropType<(hsv: HSV) => void>, + }, + setup(props) { + const { t } = useI18n(); + const prefixCls = getPrefixCls('color-picker'); + const hsv = computed(() => props.color.hsv); + const [format, setFormat] = useState<'hex' | 'rgb'>(props.format || 'hex'); + + const onChange = (value: any) => { + setFormat(value); + }; + + const showCopy = ref(false); + + const onHexInputChange = (value: string) => { + const _rgb = hexToRgb(value) || { r: 255, g: 0, b: 0 }; + const _hsv = rgbToHsv(_rgb.r, _rgb.g, _rgb.b); + props.onHsvChange?.(_hsv); + }; + + const renderInput = () => { + const commonProps = { + color: props.color, + alpha: props.alpha, + disabled: props.disabled, + disabledAlpha: props.disabledAlpha, + onHsvChange: props.onHsvChange, + onAlphaChange: props.onAlphaChange, + }; + if (format.value === 'rgb') { + return ; + } + return ; + }; + + const renderColorBlock = (color: string) => { + return ( +
onHexInputChange(color)} + > +
+
+ ); + }; + + const renderColorSection = (text: string, colors: string[] | undefined) => ( +
+
{text}
+
+ {colors?.length ? ( +
+ {colors.map(renderColorBlock)} +
+ ) : ( + + {t('colorPicker.empty')} + + )} +
+
+ ); + + const renderColorSec = () => { + if (props.showHistory || props.showPreset) { + return ( +
+ {props.showHistory && + renderColorSection(t('colorPicker.history'), props.historyColors)} + {props.showPreset && + renderColorSection(t('colorPicker.preset'), props.presetColors)} +
+ ); + } + return null; + }; + + return () => ( +
+ props.onHsvChange?.({ h: hsv.value.h, s, v })} + /> +
+
+
+ + props.onHsvChange?.({ h, s: hsv.value.s, v: hsv.value.v }) + } + /> + +
+
+
+
+