From 19ec975deb6220d2624f081f9fa3ec115fee2c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=9C=E5=86=BB=E6=A9=99?= Date: Tue, 14 Feb 2023 13:49:06 +0800 Subject: [PATCH] refactor:checkbox (#6248) * refactor:checkbox * docs:update & refactor: checkbox type --- components/checkbox/Checkbox.tsx | 21 +- components/checkbox/Group.tsx | 26 +- components/checkbox/index.en-US.md | 2 +- components/checkbox/index.zh-CN.md | 2 +- components/checkbox/interface.ts | 44 ++-- components/checkbox/style/index.tsx | 294 ++++++++++++++++++++++- components/style.ts | 2 +- components/theme/interface/components.ts | 4 +- 8 files changed, 353 insertions(+), 42 deletions(-) diff --git a/components/checkbox/Checkbox.tsx b/components/checkbox/Checkbox.tsx index 211a0e83f..0a5388c89 100644 --- a/components/checkbox/Checkbox.tsx +++ b/components/checkbox/Checkbox.tsx @@ -11,6 +11,9 @@ import useConfigInject from '../config-provider/hooks/useConfigInject'; import type { CheckboxChangeEvent, CheckboxProps } from './interface'; import { CheckboxGroupContextKey, checkboxProps } from './interface'; +// CSSINJS +import useStyle from './style'; + export default defineComponent({ compatConfig: { MODE: 3 }, name: 'ACheckbox', @@ -22,6 +25,10 @@ export default defineComponent({ const formItemContext = useInjectFormItemContext(); const formItemInputContext = FormItemInputContext.useInject(); const { prefixCls, direction } = useConfigInject('checkbox', props); + + // style + const [wrapSSR, hashId] = useStyle(prefixCls); + const checkboxGroup = inject(CheckboxGroupContextKey, undefined); const uniId = Symbol('checkboxUniId'); @@ -90,12 +97,16 @@ export default defineComponent({ [`${prefixCls.value}-wrapper-in-form-item`]: formItemInputContext.isFormItemInput, }, className, + hashId.value, + ); + const checkboxClass = classNames( + { + [`${prefixCls.value}-indeterminate`]: indeterminate, + }, + hashId.value, ); - const checkboxClass = classNames({ - [`${prefixCls.value}-indeterminate`]: indeterminate, - }); const ariaChecked = indeterminate ? 'mixed' : undefined; - return ( + return wrapSSR( + , ); }; }, diff --git a/components/checkbox/Group.tsx b/components/checkbox/Group.tsx index d341f1fe2..6b89fe12d 100644 --- a/components/checkbox/Group.tsx +++ b/components/checkbox/Group.tsx @@ -5,14 +5,23 @@ import useConfigInject from '../config-provider/hooks/useConfigInject'; import type { CheckboxOptionType } from './interface'; import { CheckboxGroupContextKey, checkboxGroupProps } from './interface'; +// CSSINJS +import useStyle from './style'; + export default defineComponent({ compatConfig: { MODE: 3 }, name: 'ACheckboxGroup', + inheritAttrs: false, props: checkboxGroupProps(), // emits: ['change', 'update:value'], - setup(props, { slots, emit, expose }) { + setup(props, { slots, attrs, emit, expose }) { const formItemContext = useInjectFormItemContext(); const { prefixCls, direction } = useConfigInject('checkbox', props); + const groupPrefixCls = computed(() => `${prefixCls.value}-group`); + + // style + const [wrapSSR, hashId] = useStyle(groupPrefixCls); + const mergedValue = ref((props.value === undefined ? props.defaultValue : props.value) || []); watch( () => props.value, @@ -87,7 +96,6 @@ export default defineComponent({ return () => { const { id = formItemContext.id.value } = props; let children = null; - const groupPrefixCls = `${prefixCls.value}-group`; if (options.value && options.value.length > 0) { children = options.value.map(option => ( {option.label === undefined ? slots.label?.(option) : option.label} )); } - return ( + return wrapSSR(
{children || slots.default?.()} -
+ , ); }; }, diff --git a/components/checkbox/index.en-US.md b/components/checkbox/index.en-US.md index d4f6a5364..3bd1450a6 100644 --- a/components/checkbox/index.en-US.md +++ b/components/checkbox/index.en-US.md @@ -2,7 +2,7 @@ category: Components type: Data Entry title: Checkbox -cover: https://gw.alipayobjects.com/zos/alicdn/8nbVbHEm_/CheckBox.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*DzgiRbW3khIAAAAAAAAAAAAADrJ8AQ/original --- Checkbox component. diff --git a/components/checkbox/index.zh-CN.md b/components/checkbox/index.zh-CN.md index b962a53a4..549cb6dbd 100644 --- a/components/checkbox/index.zh-CN.md +++ b/components/checkbox/index.zh-CN.md @@ -3,7 +3,7 @@ category: Components subtitle: 多选框 type: 数据录入 title: Checkbox -cover: https://gw.alipayobjects.com/zos/alicdn/8nbVbHEm_/CheckBox.svg +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*DzgiRbW3khIAAAAAAAAAAAAADrJ8AQ/original --- 多选框。 diff --git a/components/checkbox/interface.ts b/components/checkbox/interface.ts index e1599dd17..aabe849ba 100644 --- a/components/checkbox/interface.ts +++ b/components/checkbox/interface.ts @@ -1,7 +1,8 @@ -import type { ExtractPropTypes, InjectionKey, PropType, Ref } from 'vue'; +import type { ExtractPropTypes, InjectionKey, Ref } from 'vue'; import type { MouseEventHandler } from '../_util/EventInterface'; import type { VueNode } from '../_util/type'; import PropTypes from '../_util/vue-types'; +import { booleanType, functionType, stringType, arrayType } from '../_util/type'; export type CheckboxValueType = string | number | boolean; export interface CheckboxOptionType { @@ -27,10 +28,9 @@ export const abstractCheckboxGroupProps = () => { return { name: String, prefixCls: String, - options: { - type: Array as PropType>, - default: () => [] as Array, - }, + options: arrayType>( + [] as Array, + ), disabled: Boolean, id: String, }; @@ -39,12 +39,10 @@ export const abstractCheckboxGroupProps = () => { export const checkboxGroupProps = () => { return { ...abstractCheckboxGroupProps(), - defaultValue: { type: Array as PropType> }, - value: { type: Array as PropType> }, - onChange: { type: Function as PropType<(checkedValue: Array) => void> }, - 'onUpdate:value': { - type: Function as PropType<(checkedValue: Array) => void>, - }, + defaultValue: arrayType>(), + value: arrayType>(), + onChange: functionType<(checkedValue: Array) => void>(), + 'onUpdate:value': functionType<(checkedValue: Array) => void>(), }; }; @@ -53,27 +51,27 @@ export type CheckboxGroupProps = Partial { return { prefixCls: String, - defaultChecked: { type: Boolean, default: undefined }, - checked: { type: Boolean, default: undefined }, - disabled: { type: Boolean, default: undefined }, - isGroup: { type: Boolean, default: undefined }, + defaultChecked: booleanType(), + checked: booleanType(), + disabled: booleanType(), + isGroup: booleanType(), value: PropTypes.any, name: String, id: String, - indeterminate: { type: Boolean, default: undefined }, - type: { type: String, default: 'checkbox' }, - autofocus: { type: Boolean, default: undefined }, - onChange: Function as PropType<(e: CheckboxChangeEvent) => void>, - 'onUpdate:checked': Function as PropType<(checked: boolean) => void>, - onClick: Function as PropType, - skipGroup: { type: Boolean, default: false }, + indeterminate: booleanType(), + type: stringType('checkbox'), + autofocus: booleanType(), + onChange: functionType<(e: CheckboxChangeEvent) => void>(), + 'onUpdate:checked': functionType<(checked: boolean) => void>(), + onClick: functionType(), + skipGroup: booleanType(false), }; }; export const checkboxProps = () => { return { ...abstractCheckboxProps(), - indeterminate: { type: Boolean, default: false }, + indeterminate: booleanType(false), }; }; diff --git a/components/checkbox/style/index.tsx b/components/checkbox/style/index.tsx index d53a9fa2e..f6d8906ac 100644 --- a/components/checkbox/style/index.tsx +++ b/components/checkbox/style/index.tsx @@ -1,3 +1,291 @@ -import '../../style/index.less'; -import './index.less'; -// deps-lint-skip: form +import { Keyframes } from '../../_util/cssinjs'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { genFocusOutline, resetComponent } from '../../_style'; + +export interface ComponentToken {} + +interface CheckboxToken extends FullToken<'Checkbox'> { + checkboxCls: string; + checkboxSize: number; +} + +// ============================== Motion ============================== +const antCheckboxEffect = new Keyframes('antCheckboxEffect', { + '0%': { + transform: 'scale(1)', + opacity: 0.5, + }, + + '100%': { + transform: 'scale(1.6)', + opacity: 0, + }, +}); + +// ============================== Styles ============================== +export const genCheckboxStyle: GenerateStyle = token => { + const { checkboxCls } = token; + const wrapperCls = `${checkboxCls}-wrapper`; + + return [ + // ===================== Basic ===================== + { + // Group + [`${checkboxCls}-group`]: { + ...resetComponent(token), + + display: 'inline-flex', + }, + + // Wrapper + [wrapperCls]: { + ...resetComponent(token), + + display: 'inline-flex', + alignItems: 'baseline', + cursor: 'pointer', + + // Fix checkbox & radio in flex align #30260 + '&:after': { + display: 'inline-block', + width: 0, + overflow: 'hidden', + content: "'\\a0'", + }, + + // Checkbox near checkbox + [`& + ${wrapperCls}`]: { + marginInlineStart: token.marginXS, + }, + + [`&${wrapperCls}-in-form-item`]: { + 'input[type="checkbox"]': { + width: 14, // FIXME: magic + height: 14, // FIXME: magic + }, + }, + }, + + // Wrapper > Checkbox + [checkboxCls]: { + ...resetComponent(token), + + top: '0.2em', + position: 'relative', + whiteSpace: 'nowrap', + lineHeight: 1, + cursor: 'pointer', + + // Wrapper > Checkbox > input + [`${checkboxCls}-input`]: { + position: 'absolute', + inset: 0, + zIndex: 1, + width: '100%', + height: '100%', + cursor: 'pointer', + opacity: 0, + + [`&:focus-visible + ${checkboxCls}-inner`]: { + ...genFocusOutline(token), + }, + }, + + // Wrapper > Checkbox > inner + [`${checkboxCls}-inner`]: { + boxSizing: 'border-box', + position: 'relative', + top: 0, + insetInlineStart: 0, + display: 'block', + width: token.checkboxSize, + height: token.checkboxSize, + direction: 'ltr', + backgroundColor: token.colorBgContainer, + border: `${token.lineWidth}px ${token.lineType} ${token.colorBorder}`, + borderRadius: token.borderRadiusSM, + borderCollapse: 'separate', + transition: `all ${token.motionDurationSlow}`, + + '&:after': { + boxSizing: 'border-box', + position: 'absolute', + top: '50%', + insetInlineStart: '21.5%', + display: 'table', + width: (token.checkboxSize / 14) * 5, + height: (token.checkboxSize / 14) * 8, + border: `${token.lineWidthBold}px solid ${token.colorWhite}`, + borderTop: 0, + borderInlineStart: 0, + transform: 'rotate(45deg) scale(0) translate(-50%,-50%)', + opacity: 0, + content: '""', + transition: `all ${token.motionDurationFast} ${token.motionEaseInBack}, opacity ${token.motionDurationFast}`, + }, + }, + + // Wrapper > Checkbox + Text + '& + span': { + paddingInlineStart: token.paddingXS, + paddingInlineEnd: token.paddingXS, + }, + }, + }, + + // ================= Indeterminate ================= + { + [checkboxCls]: { + '&-indeterminate': { + // Wrapper > Checkbox > inner + [`${checkboxCls}-inner`]: { + '&:after': { + top: '50%', + insetInlineStart: '50%', + width: token.fontSizeLG / 2, + height: token.fontSizeLG / 2, + backgroundColor: token.colorPrimary, + border: 0, + transform: 'translate(-50%, -50%) scale(1)', + opacity: 1, + content: '""', + }, + }, + }, + }, + }, + + // ===================== Hover ===================== + { + // Wrapper + [`${wrapperCls}:hover ${checkboxCls}:after`]: { + visibility: 'visible', + }, + + // Wrapper & Wrapper > Checkbox + + [` + ${wrapperCls}:not(${wrapperCls}-disabled), + ${checkboxCls}:not(${checkboxCls}-disabled) + `]: { + [`&:hover ${checkboxCls}-inner`]: { + borderColor: token.colorPrimary, + }, + }, + + [`${wrapperCls}:not(${wrapperCls}-disabled)`]: { + [`&:hover ${checkboxCls}-checked:not(${checkboxCls}-disabled) ${checkboxCls}-inner`]: { + backgroundColor: token.colorPrimaryHover, + borderColor: 'transparent', + }, + [`&:hover ${checkboxCls}-checked:not(${checkboxCls}-disabled):after`]: { + borderColor: token.colorPrimaryHover, + }, + }, + }, + + // ==================== Checked ==================== + { + // Wrapper > Checkbox + [`${checkboxCls}-checked`]: { + [`${checkboxCls}-inner`]: { + backgroundColor: token.colorPrimary, + borderColor: token.colorPrimary, + + '&:after': { + opacity: 1, + transform: 'rotate(45deg) scale(1) translate(-50%,-50%)', + transition: `all ${token.motionDurationMid} ${token.motionEaseOutBack} ${token.motionDurationFast}`, + }, + }, + + // Checked Effect + '&:after': { + position: 'absolute', + top: 0, + insetInlineStart: 0, + width: '100%', + height: '100%', + borderRadius: token.borderRadiusSM, + visibility: 'hidden', + border: `${token.lineWidthBold}px solid ${token.colorPrimary}`, + animationName: antCheckboxEffect, + animationDuration: token.motionDurationSlow, + animationTimingFunction: 'ease-in-out', + animationFillMode: 'backwards', + content: '""', + transition: `all ${token.motionDurationSlow}`, + }, + }, + + [` + ${wrapperCls}-checked:not(${wrapperCls}-disabled), + ${checkboxCls}-checked:not(${checkboxCls}-disabled) + `]: { + [`&:hover ${checkboxCls}-inner`]: { + backgroundColor: token.colorPrimaryHover, + borderColor: 'transparent', + }, + [`&:hover ${checkboxCls}:after`]: { + borderColor: token.colorPrimaryHover, + }, + }, + }, + + // ==================== Disable ==================== + { + // Wrapper + [`${wrapperCls}-disabled`]: { + cursor: 'not-allowed', + }, + + // Wrapper > Checkbox + [`${checkboxCls}-disabled`]: { + // Wrapper > Checkbox > input + [`&, ${checkboxCls}-input`]: { + cursor: 'not-allowed', + // Disabled for native input to enable Tooltip event handler + // ref: https://github.com/ant-design/ant-design/issues/39822#issuecomment-1365075901 + pointerEvents: 'none', + }, + + // Wrapper > Checkbox > inner + [`${checkboxCls}-inner`]: { + background: token.colorBgContainerDisabled, + borderColor: token.colorBorder, + + '&:after': { + borderColor: token.colorTextDisabled, + }, + }, + + '&:after': { + display: 'none', + }, + + '& + span': { + color: token.colorTextDisabled, + }, + + [`&${checkboxCls}-indeterminate ${checkboxCls}-inner::after`]: { + background: token.colorTextDisabled, + }, + }, + }, + ]; +}; + +// ============================== Export ============================== +export function getStyle(prefixCls: string, token: FullToken<'Checkbox'>) { + const checkboxToken: CheckboxToken = mergeToken(token, { + checkboxCls: `.${prefixCls}`, + checkboxSize: token.controlInteractiveSize, + }); + + return [genCheckboxStyle(checkboxToken)]; +} + +export default genComponentStyleHook('Checkbox', (token, { prefixCls }) => [ + getStyle(prefixCls, token), +]); diff --git a/components/style.ts b/components/style.ts index 2db0b31f3..117b4a222 100644 --- a/components/style.ts +++ b/components/style.ts @@ -1,7 +1,7 @@ // import './button/style'; // import './icon/style'; import './radio/style'; -import './checkbox/style'; +// import './checkbox/style'; // import './grid/style'; // import './tag/style'; // import './rate/style'; diff --git a/components/theme/interface/components.ts b/components/theme/interface/components.ts index 65d87fe58..40a4de653 100644 --- a/components/theme/interface/components.ts +++ b/components/theme/interface/components.ts @@ -8,7 +8,7 @@ import type { ComponentToken as ButtonComponentToken } from '../../button/style' import type { ComponentToken as CardComponentToken } from '../../card/style'; import type { ComponentToken as CarouselComponentToken } from '../../carousel/style'; // import type { ComponentToken as CascaderComponentToken } from '../../cascader/style'; -// import type { ComponentToken as CheckboxComponentToken } from '../../checkbox/style'; +import type { ComponentToken as CheckboxComponentToken } from '../../checkbox/style'; // import type { ComponentToken as CollapseComponentToken } from '../../collapse/style'; import type { ComponentToken as DatePickerComponentToken } from '../../date-picker/style'; import type { ComponentToken as DividerComponentToken } from '../../divider/style'; @@ -62,7 +62,7 @@ export interface ComponentTokenMap { Card?: CardComponentToken; Carousel?: CarouselComponentToken; // Cascader?: CascaderComponentToken; - // Checkbox?: CheckboxComponentToken; + Checkbox?: CheckboxComponentToken; // Collapse?: CollapseComponentToken; Comment?: {}; DatePicker?: DatePickerComponentToken;