From 4135236103caff90148c1fb98b3b1d144d18c9cb Mon Sep 17 00:00:00 2001 From: itiiss Date: Mon, 25 Oct 2021 14:08:28 +0800 Subject: [PATCH] feat(checkbox): add checkbox & checkbox Group (#1371) Co-authored-by: ZhaoChen --- src/checkbox/Checkbox.tsx | 154 ++++++----- src/checkbox/demos/Checkbox.stories.tsx | 43 ++- src/checkbox/group.tsx | 184 ++++++------- src/checkbox/index.tsx | 4 +- src/checkbox/interface.tsx | 104 ++++--- src/checkbox/style/index.less | 258 +++++++++++------- src/legacy/checkbox/Checkbox.tsx | 89 ++++++ src/legacy/checkbox/CheckboxGroupContext.tsx | 14 + .../checkbox/__tests__/Checkbox.test.tsx | 0 .../checkbox/demos/Checkbox.stories.tsx | 51 ++++ src/legacy/checkbox/demos/CheckboxPage.tsx | 38 +++ src/legacy/checkbox/group.tsx | 115 ++++++++ src/legacy/checkbox/index.tsx | 13 + src/legacy/checkbox/interface.tsx | 104 +++++++ src/legacy/checkbox/style/index.less | 185 +++++++++++++ src/legacy/checkbox/style/index.ts | 1 + src/table/FilterList.tsx | 2 +- src/table/hook/useSelection.tsx | 2 +- 18 files changed, 1019 insertions(+), 342 deletions(-) create mode 100644 src/legacy/checkbox/Checkbox.tsx create mode 100644 src/legacy/checkbox/CheckboxGroupContext.tsx rename src/{ => legacy}/checkbox/__tests__/Checkbox.test.tsx (100%) create mode 100644 src/legacy/checkbox/demos/Checkbox.stories.tsx create mode 100644 src/legacy/checkbox/demos/CheckboxPage.tsx create mode 100644 src/legacy/checkbox/group.tsx create mode 100644 src/legacy/checkbox/index.tsx create mode 100644 src/legacy/checkbox/interface.tsx create mode 100644 src/legacy/checkbox/style/index.less create mode 100644 src/legacy/checkbox/style/index.ts diff --git a/src/checkbox/Checkbox.tsx b/src/checkbox/Checkbox.tsx index fbcf913c19..f704fe981b 100644 --- a/src/checkbox/Checkbox.tsx +++ b/src/checkbox/Checkbox.tsx @@ -1,89 +1,109 @@ +/* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable react-hooks/exhaustive-deps */ -import * as React from 'react'; +import React from 'react'; import classNames from 'classnames'; -import RcCheckbox from 'rc-checkbox'; -import { CheckOutlined } from '@gio-design/icons'; -import { usePrefixCls } from '@gio-design/utils'; import CheckboxGroupContext from './CheckboxGroupContext'; -import { CheckboxProps } from './interface'; - -export const Checkbox: React.FC = ({ - prefixCls: customizePrefixCls, - className, - children, - style, - indeterminate, - onChange, - ...restProps -}) => { - const rcCheckbox = React.useRef(null); - - const [check, setChecked] = React.useState(restProps.checked); - - const checkGroup = React.useContext(CheckboxGroupContext); - - const handleChange = React.useCallback( - (e) => { - if (!checkGroup) { - setChecked(!check); - } - if (onChange) onChange(e); - checkGroup?.toggleOption?.({ label: children, value: restProps.value }); - }, - [onChange, check] - ); +import { CheckboxProps, CheckboxValueType } from './interface'; +import WithRef from '../utils/withRef'; +import usePrefixCls from '../utils/hooks/use-prefix-cls'; +import useControlledState from '../utils/hooks/useControlledState'; - const prefixCls = usePrefixCls('checkbox', customizePrefixCls); - const checkProps: CheckboxProps = { ...restProps }; +const Checkbox = WithRef((props, ref) => { + const { + indeterminate = false, + defaultChecked = false, + checked, + disabled = false, + color = 'transparent', + value = '', + children, + className, + style, + onChange, + ...restProps + } = props; - if (checkGroup) { - checkProps.name = checkGroup.name; - checkProps.onChange = handleChange; - checkProps.checked = !!checkGroup.selectedValues.find((_) => _ === restProps.value); - checkProps.disabled = checkProps.disabled || checkGroup.disabled; - } + const checkboxProps: CheckboxProps = { ...restProps }; - React.useEffect(() => { - checkGroup?.registerValue(restProps.value); - return () => { - checkGroup?.unRegisterValue(restProps.value); - }; - }, [restProps.value]); + const prefixCls = usePrefixCls('checkbox'); + + const classes = classNames([className, prefixCls], { + [`${prefixCls}-${checked ? 'checked' : ''}`]: checked, + [`${prefixCls}-${indeterminate ? 'indeterminate' : ''}`]: indeterminate, + [`${prefixCls}-${disabled ? 'disabled' : ''}`]: disabled, + }); const checkboxCls = classNames(className, { [`${prefixCls}-wrapper`]: true, - [`${prefixCls}-wrapper-disabled`]: checkProps.disabled, + [`${prefixCls}-wrapper-disabled`]: checkboxProps.disabled, }); - const checkboxClass = classNames({ - [`${prefixCls}-indeterminate`]: indeterminate, - [`${prefixCls}-cool`]: !(checkProps.disabled || checkProps.checked || checkProps.indeterminate), - }); + const [checkedStatus, setChecked] = useControlledState(checked, defaultChecked); - const checkboxIconClass = classNames({ - [`${prefixCls}-icon`]: true, - [`${prefixCls}-icon-indeterminate`]: indeterminate, - [`${prefixCls}-icon-cool`]: !(checkProps.disabled || checkProps.checked || checkProps.indeterminate), - [`${prefixCls}-icon-disabled`]: checkProps.disabled, - [`${prefixCls}-icon-checked`]: checkProps.checked !== undefined ? checkProps.checked : check, - }); + const checkboxGroup = React.useContext(CheckboxGroupContext); + + const prevValue = React.useRef(value); + + const handleChange = (e: React.ChangeEvent) => { + setChecked(e.target.checked); + onChange?.(e); + }; + + React.useEffect(() => { + checkboxGroup?.registerValue(value); + }, []); + + React.useEffect(() => { + if (value !== prevValue.current) { + checkboxGroup?.unRegisterValue(prevValue.current); + checkboxGroup?.registerValue(value); + } + return () => checkboxGroup?.unRegisterValue(value); + }, [value]); + + if (checkboxGroup) { + checkboxProps.onChange = (...args) => { + if (onChange) { + onChange(...args); + } + if (checkboxGroup.toggleOption) { + checkboxGroup.toggleOption({ label: children, value }); + } + }; + checkboxProps.name = checkboxGroup.name; + checkboxProps.checked = checkboxGroup.selectedValues.indexOf(value) !== -1; + checkboxProps.disabled = disabled || checkboxGroup.disabled; + } return ( // eslint-disable-next-line jsx-a11y/label-has-associated-control - ); +}); + +Checkbox.displayName = 'Checkbox'; + +Checkbox.defaultProps = { + indeterminate: false, + defaultChecked: false, + disabled: false, + color: 'transparent', }; -export default React.memo(Checkbox); +export default Checkbox; diff --git a/src/checkbox/demos/Checkbox.stories.tsx b/src/checkbox/demos/Checkbox.stories.tsx index f647e80fb2..da8c2c08ab 100644 --- a/src/checkbox/demos/Checkbox.stories.tsx +++ b/src/checkbox/demos/Checkbox.stories.tsx @@ -1,27 +1,12 @@ import React from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; -import { withDesign } from 'storybook-addon-designs'; -import Docs from './CheckboxPage'; -import Checkbox from '../Checkbox'; -import CheckboxGroup from '../group'; +import Checkbox from '../index'; import { CheckboxProps, CheckboxGroupProps, CheckboxValueType } from '../interface'; import '../style'; export default { - title: 'Components/Checkbox', + title: 'Upgraded/Checkbox', component: Checkbox, - subcomponents: { CheckboxGroup }, - decorators: [withDesign], - parameters: { - design: { - type: 'figma', - url: 'https://www.figma.com/file/kP3A6S2fLUGVVMBgDuUx0f/GrowingIO-Design-Components?node-id=889%3A1248', - allowFullscreen: true, - }, - docs: { - page: Docs, - }, - }, } as Meta; const Template: Story = (args) => { @@ -31,21 +16,29 @@ const Template: Story = (args) => { }; return ( - Normal + 我已阅读以下条款 ); }; -const TemplateGroup: Story> = (args) => ; -export const Default = Template.bind({}); +const TemplateGroup: Story> = (args) => ; + export const Group = TemplateGroup.bind({}); -Default.args = {}; Group.args = { options: [ - { label: '1', value: 1 }, - { label: '2', value: 2 }, - { label: '3', value: 3 }, - { label: '4', value: 4, disabled: true }, + { label: '我已阅读以下条款一', value: 1 }, + { label: '我已阅读以下条款二', value: 2 }, + { label: '我已阅读以下条款三', value: 3, disabled: true }, ], }; + +export const Default = Template.bind({}); + +export const Indeterminate = Template.bind({}); + +Indeterminate.args = { + indeterminate: true, +}; + +Default.args = {}; diff --git a/src/checkbox/group.tsx b/src/checkbox/group.tsx index a32da7d9c3..779537e828 100644 --- a/src/checkbox/group.tsx +++ b/src/checkbox/group.tsx @@ -1,115 +1,113 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import * as React from 'react'; import classNames from 'classnames'; import { usePrefixCls } from '@gio-design/utils'; import Checkbox from './Checkbox'; +import WithRef from '../utils/withRef'; import CheckboxGroupContext from './CheckboxGroupContext'; import { CheckboxOptionType, CheckboxValueType, CheckboxGroupProps } from './interface'; -function merge(selected: T[], option: CheckboxOptionType, registeredValues: T[]): T[] { - const optionIndex = selected.indexOf(option.value); - const newSelected = [...selected].filter((val) => registeredValues.indexOf(val) !== -1); - if (optionIndex === -1) { - newSelected.push(option.value); - } else { - newSelected.splice(optionIndex, 1); - } - return newSelected; -} - -const emptyValue: any[] = []; - -export function CheckboxGroup({ - options = [], - prefixCls: customizePrefixCls, - defaultValue, - value, - onChange, - disabled = false, - name, - direction = 'horizontal', - children, -}: CheckboxGroupProps) { - const registeredValuesRef = React.useRef([]); +const InternalCheckboxGroup: React.ForwardRefRenderFunction> = ( + props, + ref +) => { + const { + options = [], + prefixCls: customizePrefixCls, + defaultValue, + onChange, + disabled = false, + name, + layout = 'horizontal', + children, + ...restProps + } = props; - const registerValue = React.useCallback((values: T) => { - registeredValuesRef.current.push(values); - }, []); + const [value, setValue] = React.useState(restProps.value || defaultValue || []); + const [registeredValues, setRegisteredValues] = React.useState([]); - const unRegisterValue = React.useCallback((values: T) => { - registeredValuesRef.current = registeredValuesRef.current.filter((_) => _ !== values); - }, []); + React.useEffect(() => { + if ('value' in restProps) { + setValue(restProps.value || []); + } + }, [restProps, restProps.value]); - // self maintained state - const [selected, updateSelected] = React.useState(() => defaultValue || emptyValue); + const getOptions = () => + options.map((option) => { + if (typeof option === 'string') { + return { + label: option, + value: option, + }; + } + return option; + }); - const refValue = React.useRef(value); + const unRegisterValue = (val: string) => { + setRegisteredValues((prevValues) => prevValues.filter((v) => v !== val)); + }; - // update outside maintained state - if (refValue.current !== value && value) { - refValue.current = value; - } + const registerValue = (val: string) => { + setRegisteredValues((prevValues) => [...prevValues, val]); + }; - const toggleOption = React.useCallback( - (option: CheckboxOptionType) => { - if (value === undefined) { - // self maintained state - updateSelected((selecting) => { - const newSelected = merge(selecting, option, registeredValuesRef.current); - onChange?.(newSelected); - return newSelected; - }); - } else { - const newSelected = merge(refValue.current || emptyValue, option, registeredValuesRef.current); - onChange?.(newSelected); - } - }, - [onChange] - ); + const toggleOption = (option: CheckboxOptionType) => { + const optionIndex = value.indexOf(option.value); + const newValue = [...value]; + if (optionIndex === -1) { + newValue.push(option.value); + } else { + newValue.splice(optionIndex, 1); + } + if (!('value' in restProps)) { + setValue(newValue); + } + const opts = getOptions(); + onChange?.( + newValue + .filter((val) => registeredValues.indexOf(val) !== -1) + .sort((a, b) => { + const indexA = opts.findIndex((opt) => opt.value === a); + const indexB = opts.findIndex((opt) => opt.value === b); + return indexA - indexB; + }) + ); + }; const prefixCls = usePrefixCls('checkbox', customizePrefixCls); - const selectedValues = refValue.current || selected; let customChildren = children; - if (options.length > 0) { - customChildren = options - .map((option) => { - if (typeof option === 'string') { - return { - label: option, - value: option, - }; - } - return option; - }) - .map((option) => ( - - {option.label} - - )); + + if (options && options.length > 0) { + customChildren = getOptions().map((option) => ( + + {option.label} + + )); } - const cls = classNames(`${prefixCls}-group`, `${prefixCls}-group--${direction}`); + const cls = classNames(`${prefixCls}-group`, `${prefixCls}-group__${layout}`); + + const context = { + toggleOption, + selectedValues: value, + disabled, + name, + registerValue, + unRegisterValue, + }; return ( - -
{customChildren}
-
+
+ {customChildren} +
); -} +}; + +const CheckboxGroup = WithRef>(InternalCheckboxGroup); -export default CheckboxGroup; +export default React.memo(CheckboxGroup); diff --git a/src/checkbox/index.tsx b/src/checkbox/index.tsx index af625fe011..f2a2db1cf3 100644 --- a/src/checkbox/index.tsx +++ b/src/checkbox/index.tsx @@ -1,13 +1,13 @@ import GIOCheckbox from './Checkbox'; import CheckboxGroup from './group'; +import WithSubComponent from '../utils/withSubComponent'; export type TCheckbox = typeof GIOCheckbox & { Group: typeof CheckboxGroup; }; const Checkbox = GIOCheckbox as TCheckbox; -Checkbox.Group = CheckboxGroup; export { CheckboxProps, CheckboxGroupProps, CheckboxOptionType } from './interface'; export { CheckboxGroup }; -export default Checkbox; +export default WithSubComponent(Checkbox, { Group: CheckboxGroup }); diff --git a/src/checkbox/interface.tsx b/src/checkbox/interface.tsx index 2229bd8e9b..ca920221e7 100644 --- a/src/checkbox/interface.tsx +++ b/src/checkbox/interface.tsx @@ -1,5 +1,48 @@ -export type CheckboxValueType = string | number | boolean; +export type CheckboxValueType = string | number; +export interface CheckboxProps + extends Omit, 'prefix' | 'type' | 'disabled'> { + /** + 是否为不定状态 + */ + indeterminate?: boolean; + /** + 自定义 `CSS` 类前缀 + */ + prefixCls?: string; + /** + 自定义 className + */ + className?: string; + /** + 初始是否选中 + */ + defaultChecked?: boolean; + /** + 指定当前是否选中 + */ + checked?: boolean; + /** + 当前选中值 + */ + value?: CheckboxValueType; + /** + 是否禁止 + */ + disabled?: boolean; + /** + 变化时的回调函数 + */ + onChange?: React.ChangeEventHandler; + /** + 选择框填充后的背景色 + */ + color?: string; + /** + 自定义的`CSS`样式 + */ + style?: React.CSSProperties; +} export interface CheckboxOptionType { label: React.ReactNode; value: T; @@ -36,69 +79,16 @@ export interface CheckboxGroupProps { 自定义的`CSS`样式 */ style?: React.CSSProperties; - /** - 排列的方向 - */ - direction?: 'horizontal' | 'vertical'; - /** - 子元素 - */ - children?: React.ReactNode | React.ReactNode[]; /** 自定义 `CSS` 类前缀 */ prefixCls?: string; -} - -export interface CheckboxProps { /** - 是否部分选中 - */ - indeterminate?: boolean; - /** - 自定义 `CSS` 类前缀 + 自定义子节点 */ - prefixCls?: string; - /** - 自定义 className - */ - className?: string; - /** - 初始是否选中 - */ - defaultChecked?: boolean; - /** - 指定当前是否选中 - */ - checked?: boolean; - /** - 是否禁止 - */ - disabled?: boolean; - /** - 变化时的回调函数 - */ - onChange?: React.ChangeEventHandler; - /** - 点击时的回调函数 - */ - onClick?: React.MouseEventHandler; - /** - 指定的选中项 - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value?: any; children?: React.ReactNode; /** - input[type="checkbox"] 的 id 属性 + checkbox 排列方向 */ - id?: string; - /** - input[type="checkbox"] 的 name 属性 - */ - name?: string; - /** - 自定义的`CSS`样式 - */ - style?: React.CSSProperties; + layout?: 'horizontal' | 'vertical'; } diff --git a/src/checkbox/style/index.less b/src/checkbox/style/index.less index ac417d8c98..f15e094b60 100644 --- a/src/checkbox/style/index.less +++ b/src/checkbox/style/index.less @@ -1,7 +1,9 @@ @import '../../stylesheet/index.less'; @import '../../stylesheet/mixin/index.less'; +@import '../../stylesheet/variables/index.less'; @checkbox-prefix-cls: ~'@{component-prefix}-checkbox'; +@checkbox-indeterminate-prefix-cls: ~'@{component-prefix}-checkbox-indeterminate'; @checkbox-inner-prefix-cls: ~'@{checkbox-prefix-cls}-inner'; @checkbox-icon-cls: ~'@{checkbox-prefix-cls}-icon'; @group-prefix-cls: ~'@{checkbox-prefix-cls}-group'; @@ -14,57 +16,140 @@ white-space: nowrap; cursor: pointer; - &-disabled { - cursor: not-allowed; - } + --active: @blue-3; + --active-inner: @gray-0; + --border: @gray-2; + --border-hover: @blue-3; + --background: @gray-0; + --disabled: @gray-1; + --disabled-inner: @gray-3; - &:hover { - .@{checkbox-prefix-cls} { - &-cool { - .@{checkbox-inner-prefix-cls} { - border-color: @color-border-checkbox-hover; - box-shadow: @shadow-checkbox-hover; + @supports (-webkit-appearance: none) or (-moz-appearance: none) { + input[type='checkbox'].@{checkbox-prefix-cls} { + position: relative; + display: inline-block; + height: 16px; + margin: 0; + background: var(--b, var(--background)); + border: 1px solid var(--bc, var(--border)); + outline: none; + cursor: pointer; + transition: background 0.3s, border-color 0.3s, box-shadow 0.2s; + -webkit-appearance: none; + -moz-appearance: none; + &::after { + position: absolute; + top: 0; + left: 0; + display: block; + transition: transform var(--d-t, 0.3s) var(--d-t-e, ease), opacity var(--d-o, 0.2s); + content: ''; + } + &:checked { + --b: var(--active); + --bc: var(--active); + --d-o: 0.3s; + --d-t: 0.6s; + --d-t-e: cubic-bezier(0.2, 0.85, 0.32, 1.2); + } + &:disabled { + --b: var(--disabled); + cursor: not-allowed; + opacity: 0.9; + &:checked { + --b: var(--disabled-inner); + --bc: var(--border); + } + &::after { + top: 0; + left: 4px; + width: 5px; + height: 9px; + border: 1px solid; + border-color: var(--disabled-inner) !important; + border-top: 0; + border-left: 0; + transform: rotate(var(--r, 43deg)) !important; + } + & + label { + cursor: not-allowed; + } + } + &:hover { + &:not(:checked) { + &:not(:disabled) { + --bc: var(--border-hover); + } + } + } + &:not(.switch) { + width: 16px; + border-radius: 4px; + &::after { + top: 0; + left: 4px; + width: 5px; + height: 9px; + border: 1px solid var(--active-inner); + border-top: 0; + border-left: 0; + transform: rotate(var(--r, 20deg)); } + &:checked { + --r: 43deg; + } + } + & + label { + display: inline-block; + margin-left: 4px; + font-size: 14px; + line-height: 21px; + cursor: pointer; } } - } - - .@{checkbox-prefix-cls} { - &-cool { - .@{checkbox-inner-prefix-cls} { - box-shadow: @shadow-checkbox-inbound; + input[type='checkbox'].@{checkbox-indeterminate-prefix-cls} { + &:not(.switch) { + width: 16px; + border-radius: 4px; + &::after { + top: 0; + left: 2px; + width: 10px; + height: 7px; + border: 1px solid var(--active-inner); + border-top: 0; + border-right: 0; + border-left: 0; + transform: none; + } + &:checked { + --r: 43deg; + } + } + &:disabled { + &::after { + top: 0; + left: 2px; + width: 10px; + height: 7px; + border: 1px solid var(--disable-inner) !important; + border-top: 0; + border-right: 0; + border-left: 0; + transform: none; + } } } } -} - -.@{checkbox-icon-cls} { - position: absolute; - top: 8px; - left: 3px; - z-index: 999; - transform: scale(0); - transition: all 0.5s; - pointer-events: none; - svg { - width: 11px; - height: 10px; - color: @color-background-checkbox-normal; - } - - &-checked { - transform: scale(1); - transition: all 0.5s cubic-bezier(0.12, 0.4, 0.29, 1.46); - } &-disabled { - svg { - color: @color-border-checkbox-disabled; - } + cursor: not-allowed; } - .gio-icon&-indeterminate { - display: none; + &:hover { + .@{checkbox-prefix-cls} { + border-color: @blue-3; + } } } @@ -89,48 +174,29 @@ & + span { margin-left: 8px; + color: @gray-5; + font-size: 14px; + font-style: normal; vertical-align: middle; } - .@{checkbox-inner-prefix-cls} { - position: absolute; - top: calc(-50% + 1px); - right: -50%; - bottom: -50%; - left: calc(-50% + 1px); - z-index: 1; - display: block; - background-color: @color-background-checkbox-normal; - border: 1px solid @color-border-checkbox-normal; - border-radius: 8px; - border-collapse: separate; - transform: scale(0.5); - transition: all 0.9s; - pointer-events: none; - - /** - * 部分选中的横线 - */ - &::after { - position: relative; - top: 50%; - left: 20%; - display: block; - width: 60%; - height: 2px; - margin-top: -1px; - background-color: @color-background-checkbox-normal; - border-radius: 1px; - transform: scale(0); - content: ' '; - } - } + // &::after { + // position: relative; + // top: 50%; + // left: 20%; + // display: block; + // width: 60%; + // height: 2px; + // margin-top: -1px; + // background-color: @color-background-checkbox-normal; + // border-radius: 1px; + // transform: scale(0); + // content: ' '; + // } &-checked { - .@{checkbox-inner-prefix-cls} { - background-color: @color-background-checkbox-checked; - border-color: @color-border-checkbox-checked; - } + background-color: @blue-3; + border-color: @blue-3; } &-disabled&-checked { @@ -150,36 +216,36 @@ } &-disabled { .@{checkbox-inner-prefix-cls} { - background-color: @color-background-checkbox-disabled; - border-color: @color-border-checkbox-disabled !important; + background-color: @gray-1; + border-color: @gray-1 !important; &::after { - background-color: @color-border-checkbox-disabled; + background-color: @gray-1; } } - - & + span { - color: @text-color-disabled; - opacity: 0.3; - } - } - - &-cool { - .@{checkbox-inner-prefix-cls} { - box-shadow: @shadow-checkbox-inbound; - } } } .@{group-prefix-cls} { - &--vertical { + display: inline-flex; + gap: 16px 8px; + justify-content: flex-start; + &__vertical { + flex-direction: column; + align-items: flex-start; .@{checkbox-wrapper-cls} { - display: block; - margin-bottom: 16px; + display: flex; + align-items: flex-start; + justify-content: flex-start; + .@{checkbox-prefix-cls} { + margin-top: 4px !important; + } } } - &--horizontal { + &__horizontal { + flex-direction: row; + align-items: center; .@{checkbox-wrapper-cls} { - margin-right: 24px; + margin-right: 16px; } } } diff --git a/src/legacy/checkbox/Checkbox.tsx b/src/legacy/checkbox/Checkbox.tsx new file mode 100644 index 0000000000..fbcf913c19 --- /dev/null +++ b/src/legacy/checkbox/Checkbox.tsx @@ -0,0 +1,89 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import * as React from 'react'; +import classNames from 'classnames'; +import RcCheckbox from 'rc-checkbox'; +import { CheckOutlined } from '@gio-design/icons'; +import { usePrefixCls } from '@gio-design/utils'; +import CheckboxGroupContext from './CheckboxGroupContext'; +import { CheckboxProps } from './interface'; + +export const Checkbox: React.FC = ({ + prefixCls: customizePrefixCls, + className, + children, + style, + indeterminate, + onChange, + ...restProps +}) => { + const rcCheckbox = React.useRef(null); + + const [check, setChecked] = React.useState(restProps.checked); + + const checkGroup = React.useContext(CheckboxGroupContext); + + const handleChange = React.useCallback( + (e) => { + if (!checkGroup) { + setChecked(!check); + } + if (onChange) onChange(e); + checkGroup?.toggleOption?.({ label: children, value: restProps.value }); + }, + [onChange, check] + ); + + const prefixCls = usePrefixCls('checkbox', customizePrefixCls); + const checkProps: CheckboxProps = { ...restProps }; + + if (checkGroup) { + checkProps.name = checkGroup.name; + checkProps.onChange = handleChange; + checkProps.checked = !!checkGroup.selectedValues.find((_) => _ === restProps.value); + checkProps.disabled = checkProps.disabled || checkGroup.disabled; + } + + React.useEffect(() => { + checkGroup?.registerValue(restProps.value); + return () => { + checkGroup?.unRegisterValue(restProps.value); + }; + }, [restProps.value]); + + const checkboxCls = classNames(className, { + [`${prefixCls}-wrapper`]: true, + [`${prefixCls}-wrapper-disabled`]: checkProps.disabled, + }); + + const checkboxClass = classNames({ + [`${prefixCls}-indeterminate`]: indeterminate, + [`${prefixCls}-cool`]: !(checkProps.disabled || checkProps.checked || checkProps.indeterminate), + }); + + const checkboxIconClass = classNames({ + [`${prefixCls}-icon`]: true, + [`${prefixCls}-icon-indeterminate`]: indeterminate, + [`${prefixCls}-icon-cool`]: !(checkProps.disabled || checkProps.checked || checkProps.indeterminate), + [`${prefixCls}-icon-disabled`]: checkProps.disabled, + [`${prefixCls}-icon-checked`]: checkProps.checked !== undefined ? checkProps.checked : check, + }); + + return ( + // eslint-disable-next-line jsx-a11y/label-has-associated-control + + ); +}; + +export default React.memo(Checkbox); diff --git a/src/legacy/checkbox/CheckboxGroupContext.tsx b/src/legacy/checkbox/CheckboxGroupContext.tsx new file mode 100644 index 0000000000..58673c1a8a --- /dev/null +++ b/src/legacy/checkbox/CheckboxGroupContext.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { CheckboxOptionType, CheckboxValueType } from './interface'; + +export interface CheckboxGroupContextProps { + toggleOption?: (option: CheckboxOptionType) => void; + name?: string; + disabled?: boolean; + selectedValues: CheckboxValueType[]; + registerValue: (value: CheckboxValueType) => void; + unRegisterValue: (value: CheckboxValueType) => void; +} + +const CheckboxGroupContext = React.createContext(null); +export default CheckboxGroupContext; diff --git a/src/checkbox/__tests__/Checkbox.test.tsx b/src/legacy/checkbox/__tests__/Checkbox.test.tsx similarity index 100% rename from src/checkbox/__tests__/Checkbox.test.tsx rename to src/legacy/checkbox/__tests__/Checkbox.test.tsx diff --git a/src/legacy/checkbox/demos/Checkbox.stories.tsx b/src/legacy/checkbox/demos/Checkbox.stories.tsx new file mode 100644 index 0000000000..cc4a29c9af --- /dev/null +++ b/src/legacy/checkbox/demos/Checkbox.stories.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { withDesign } from 'storybook-addon-designs'; +import Docs from './CheckboxPage'; +import Checkbox from '../Checkbox'; +import CheckboxGroup from '../group'; +import { CheckboxProps, CheckboxGroupProps, CheckboxValueType } from '../interface'; +import '../style'; + +export default { + title: 'Data Entry/Checkbox', + component: Checkbox, + subcomponents: { CheckboxGroup }, + decorators: [withDesign], + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/kP3A6S2fLUGVVMBgDuUx0f/GrowingIO-Design-Components?node-id=889%3A1248', + allowFullscreen: true, + }, + docs: { + page: Docs, + }, + }, +} as Meta; + +const Template: Story = (args) => { + const [checked, setChecked] = React.useState(false); + const handleChange = (e: any) => { + setChecked(e.target.checked); + }; + return ( + + Normal + + ); +}; +const TemplateGroup: Story> = (args) => ; + +export const Default = Template.bind({}); +export const Group = TemplateGroup.bind({}); + +Default.args = {}; +Group.args = { + options: [ + { label: '1', value: 1 }, + { label: '2', value: 2 }, + { label: '3', value: 3 }, + { label: '4', value: 4, disabled: true }, + ], +}; diff --git a/src/legacy/checkbox/demos/CheckboxPage.tsx b/src/legacy/checkbox/demos/CheckboxPage.tsx new file mode 100644 index 0000000000..a1c3857022 --- /dev/null +++ b/src/legacy/checkbox/demos/CheckboxPage.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Canvas, Title, Heading, Story, Subheading, ArgsTable } from '@storybook/addon-docs'; +import { useIntl } from 'react-intl'; +import Checkbox from '../Checkbox'; + +export default function ListPage() { + const { formatMessage } = useIntl(); + + return ( + <> + {formatMessage({ defaultMessage: 'Checkbox 多选框' })} +

+ {formatMessage({ + defaultMessage: '在一组可选项中进行多项选择;', + })} +

+

+ {formatMessage({ + defaultMessage: + '单独使用可以表示两种状态之间的切换,和 switch 类似。区别在于切换 switch 会直接触发状态改变,而 checkbox 一般用于状态标记,需要和提交操作配合。', + })} +

+ {formatMessage({ defaultMessage: '代码演示' })} + {formatMessage({ defaultMessage: '基本样式' })} + + + + + {formatMessage({ defaultMessage: '分组' })} + + + + + {formatMessage({ defaultMessage: '参数说明' })} + + + ); +} diff --git a/src/legacy/checkbox/group.tsx b/src/legacy/checkbox/group.tsx new file mode 100644 index 0000000000..a32da7d9c3 --- /dev/null +++ b/src/legacy/checkbox/group.tsx @@ -0,0 +1,115 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import * as React from 'react'; +import classNames from 'classnames'; +import { usePrefixCls } from '@gio-design/utils'; +import Checkbox from './Checkbox'; +import CheckboxGroupContext from './CheckboxGroupContext'; +import { CheckboxOptionType, CheckboxValueType, CheckboxGroupProps } from './interface'; + +function merge(selected: T[], option: CheckboxOptionType, registeredValues: T[]): T[] { + const optionIndex = selected.indexOf(option.value); + const newSelected = [...selected].filter((val) => registeredValues.indexOf(val) !== -1); + if (optionIndex === -1) { + newSelected.push(option.value); + } else { + newSelected.splice(optionIndex, 1); + } + return newSelected; +} + +const emptyValue: any[] = []; + +export function CheckboxGroup({ + options = [], + prefixCls: customizePrefixCls, + defaultValue, + value, + onChange, + disabled = false, + name, + direction = 'horizontal', + children, +}: CheckboxGroupProps) { + const registeredValuesRef = React.useRef([]); + + const registerValue = React.useCallback((values: T) => { + registeredValuesRef.current.push(values); + }, []); + + const unRegisterValue = React.useCallback((values: T) => { + registeredValuesRef.current = registeredValuesRef.current.filter((_) => _ !== values); + }, []); + + // self maintained state + const [selected, updateSelected] = React.useState(() => defaultValue || emptyValue); + + const refValue = React.useRef(value); + + // update outside maintained state + if (refValue.current !== value && value) { + refValue.current = value; + } + + const toggleOption = React.useCallback( + (option: CheckboxOptionType) => { + if (value === undefined) { + // self maintained state + updateSelected((selecting) => { + const newSelected = merge(selecting, option, registeredValuesRef.current); + onChange?.(newSelected); + return newSelected; + }); + } else { + const newSelected = merge(refValue.current || emptyValue, option, registeredValuesRef.current); + onChange?.(newSelected); + } + }, + [onChange] + ); + + const prefixCls = usePrefixCls('checkbox', customizePrefixCls); + const selectedValues = refValue.current || selected; + let customChildren = children; + if (options.length > 0) { + customChildren = options + .map((option) => { + if (typeof option === 'string') { + return { + label: option, + value: option, + }; + } + return option; + }) + .map((option) => ( + + {option.label} + + )); + } + + const cls = classNames(`${prefixCls}-group`, `${prefixCls}-group--${direction}`); + + return ( + +
{customChildren}
+
+ ); +} + +export default CheckboxGroup; diff --git a/src/legacy/checkbox/index.tsx b/src/legacy/checkbox/index.tsx new file mode 100644 index 0000000000..af625fe011 --- /dev/null +++ b/src/legacy/checkbox/index.tsx @@ -0,0 +1,13 @@ +import GIOCheckbox from './Checkbox'; +import CheckboxGroup from './group'; + +export type TCheckbox = typeof GIOCheckbox & { + Group: typeof CheckboxGroup; +}; + +const Checkbox = GIOCheckbox as TCheckbox; +Checkbox.Group = CheckboxGroup; + +export { CheckboxProps, CheckboxGroupProps, CheckboxOptionType } from './interface'; +export { CheckboxGroup }; +export default Checkbox; diff --git a/src/legacy/checkbox/interface.tsx b/src/legacy/checkbox/interface.tsx new file mode 100644 index 0000000000..2229bd8e9b --- /dev/null +++ b/src/legacy/checkbox/interface.tsx @@ -0,0 +1,104 @@ +export type CheckboxValueType = string | number | boolean; + +export interface CheckboxOptionType { + label: React.ReactNode; + value: T; + disabled?: boolean; + onChange?: CheckboxProps['onChange']; +} + +export interface CheckboxGroupProps { + /** + 默认选中的选项 + */ + defaultValue?: T[]; + /** + 整组失效 + */ + disabled?: boolean; + /** + `CheckboxGroup` 下所有 `input[type="checkbox"]` 的 `name` 属性 + */ + name?: string; + /** + 指定选中的选项 + */ + value?: T[]; + /** + 变化时的回调函数 + */ + onChange?: (value: T[]) => void; + /** + 指定可选项 + */ + options?: Array>; + /** + 自定义的`CSS`样式 + */ + style?: React.CSSProperties; + /** + 排列的方向 + */ + direction?: 'horizontal' | 'vertical'; + /** + 子元素 + */ + children?: React.ReactNode | React.ReactNode[]; + /** + 自定义 `CSS` 类前缀 + */ + prefixCls?: string; +} + +export interface CheckboxProps { + /** + 是否部分选中 + */ + indeterminate?: boolean; + /** + 自定义 `CSS` 类前缀 + */ + prefixCls?: string; + /** + 自定义 className + */ + className?: string; + /** + 初始是否选中 + */ + defaultChecked?: boolean; + /** + 指定当前是否选中 + */ + checked?: boolean; + /** + 是否禁止 + */ + disabled?: boolean; + /** + 变化时的回调函数 + */ + onChange?: React.ChangeEventHandler; + /** + 点击时的回调函数 + */ + onClick?: React.MouseEventHandler; + /** + 指定的选中项 + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value?: any; + children?: React.ReactNode; + /** + input[type="checkbox"] 的 id 属性 + */ + id?: string; + /** + input[type="checkbox"] 的 name 属性 + */ + name?: string; + /** + 自定义的`CSS`样式 + */ + style?: React.CSSProperties; +} diff --git a/src/legacy/checkbox/style/index.less b/src/legacy/checkbox/style/index.less new file mode 100644 index 0000000000..a2855545ec --- /dev/null +++ b/src/legacy/checkbox/style/index.less @@ -0,0 +1,185 @@ +@import '../../../stylesheet/index.less'; +@import '../../../stylesheet/mixin/index.less'; + +@checkbox-prefix-cls: ~'@{component-prefix}-checkbox'; +@checkbox-inner-prefix-cls: ~'@{checkbox-prefix-cls}-inner'; +@checkbox-icon-cls: ~'@{checkbox-prefix-cls}-icon'; +@group-prefix-cls: ~'@{checkbox-prefix-cls}-group'; +@checkbox-wrapper-cls: ~'@{checkbox-prefix-cls}-wrapper'; + +.@{checkbox-wrapper-cls} { + .reset-component(); + position: relative; + display: inline-block; + white-space: nowrap; + cursor: pointer; + + &-disabled { + cursor: not-allowed; + } + + &:hover { + .@{checkbox-prefix-cls} { + &-cool { + .@{checkbox-inner-prefix-cls} { + border-color: @color-border-checkbox-hover; + box-shadow: @shadow-checkbox-hover; + } + } + } + } + + .@{checkbox-prefix-cls} { + &-cool { + .@{checkbox-inner-prefix-cls} { + box-shadow: @shadow-checkbox-inbound; + } + } + } +} + +.@{checkbox-icon-cls} { + position: absolute; + top: 8px; + left: 3px; + z-index: 999; + transform: scale(0); + transition: all 0.5s; + pointer-events: none; + svg { + width: 11px; + height: 10px; + color: @color-background-checkbox-normal; + } + + &-checked { + transform: scale(1); + transition: all 0.5s cubic-bezier(0.12, 0.4, 0.29, 1.46); + } + + &-disabled { + svg { + color: @color-border-checkbox-disabled; + } + } + + .gio-icon&-indeterminate { + display: none; + } +} + +.@{checkbox-prefix-cls} { + position: relative; + + &-input { + width: 100%; + height: 100%; + cursor: pointer; + opacity: 0; + + &:disabled { + cursor: not-allowed; + } + } + + display: inline-block; + width: 16px; + height: 16px; + vertical-align: middle; + + & + span { + margin-left: 8px; + vertical-align: middle; + } + + .@{checkbox-inner-prefix-cls} { + position: absolute; + top: calc(-50% + 1px); + right: -50%; + bottom: -50%; + left: calc(-50% + 1px); + z-index: 1; + display: block; + background-color: @color-background-checkbox-normal; + border: 1px solid @color-border-checkbox-normal; + border-radius: 8px; + border-collapse: separate; + transform: scale(0.5); + transition: all 0.9s; + pointer-events: none; + + /** + * 部分选中的横线 + */ + &::after { + position: relative; + top: 50%; + left: 20%; + display: block; + width: 60%; + height: 2px; + margin-top: -1px; + background-color: @color-background-checkbox-normal; + border-radius: 1px; + transform: scale(0); + content: ' '; + } + } + + &-checked { + .@{checkbox-inner-prefix-cls} { + background-color: @color-background-checkbox-checked; + border-color: @color-border-checkbox-checked; + } + } + + &-disabled&-checked { + & + span { + color: unset; + opacity: unset; + } + } + + &-indeterminate { + .@{checkbox-inner-prefix-cls} { + &::after { + transform: scale(1); + transition: all 0.5s; + } + } + } + &-disabled { + .@{checkbox-inner-prefix-cls} { + background-color: @color-background-checkbox-disabled; + border-color: @color-border-checkbox-disabled !important; + &::after { + background-color: @color-border-checkbox-disabled; + } + } + + & + span { + color: @text-color-disabled; + opacity: 0.3; + } + } + + &-cool { + .@{checkbox-inner-prefix-cls} { + box-shadow: @shadow-checkbox-inbound; + } + } +} + +.@{group-prefix-cls} { + &--vertical { + .@{checkbox-wrapper-cls} { + display: block; + margin-bottom: 16px; + } + } + &--horizontal { + .@{checkbox-wrapper-cls} { + margin-right: 24px; + } + } +} diff --git a/src/legacy/checkbox/style/index.ts b/src/legacy/checkbox/style/index.ts new file mode 100644 index 0000000000..d74e52ee9f --- /dev/null +++ b/src/legacy/checkbox/style/index.ts @@ -0,0 +1 @@ +import './index.less'; diff --git a/src/table/FilterList.tsx b/src/table/FilterList.tsx index f0016d562e..2f5c2c29f5 100644 --- a/src/table/FilterList.tsx +++ b/src/table/FilterList.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { without, concat } from 'lodash'; import List from '../components/list'; -import Checkbox from '../checkbox'; +import Checkbox from '../legacy/checkbox'; interface FilterListProps { prefixCls: string; diff --git a/src/table/hook/useSelection.tsx b/src/table/hook/useSelection.tsx index 36d942eae1..9083f4aeb2 100644 --- a/src/table/hook/useSelection.tsx +++ b/src/table/hook/useSelection.tsx @@ -5,7 +5,7 @@ import React, { useMemo, useCallback } from 'react'; import { get, intersection, isUndefined, difference, union, isFunction, isString, flatten, flattenDeep } from 'lodash'; import { ColumnsType, RowSelection, ColumnType } from '../interface'; -import Checkbox from '../../checkbox'; +import Checkbox from '../../legacy/checkbox'; import Tooltip from '../../tooltip'; import useControlledState from '../../utils/hooks/useControlledState';