import React, { ChangeEvent, CSSProperties, LabelHTMLAttributes, ReactNode, useCallback, useRef, useState } from 'react' import cns from 'classnames' import { useFormSize } from '../Form/defs' import { ComponentSize, usePrefix } from '../ConfigProvider' import { concat } from '../utils' import { WithRef } from '../utils/type' export interface CommonCheckboxProps { /** * 当前选中状态,在 Group 中使用时将被忽略 * @default false */ checked?: boolean /** * 初始选中状态,在 Group 中使用时将被忽略 * @default false */ defaultChecked?: boolean /** 原生 input onchange 时触发,改变后的值作为参数传入 */ onChange?: (checked: boolean) => void /** * 禁用 * @default false */ disabled?: boolean /** 尺寸 */ size?: ComponentSize /** 自定义样式名 */ className?: string /** 自定义样式 */ style?: CSSProperties /** 标签内容 */ children?: ReactNode /** * 根元素的 html 属性 */ rootHtmlProps?: WithRef, HTMLLabelElement> } /** 只给内部组件使用,不对外 */ export interface InternalCheckboxProps extends CommonCheckboxProps { /** Radio 可复用 Checkbox 逻辑,只需要修改下样式 */ type?: 'checkbox' | 'radio' prefixCls?: string /** CheckboxGroup 全选按钮使用 */ partialChecked?: boolean } export function InternalCheckbox({ checked: checkedInProps, defaultChecked = false, disabled = false, onChange, size, className, style, children, type = 'checkbox', prefixCls, partialChecked, rootHtmlProps, ...restProps }: InternalCheckboxProps) { const [innerChecked, setInnerChecked] = useState(checkedInProps ?? defaultChecked) const checked = checkedInProps ?? innerChecked const isControlled = checkedInProps != null const handleChange = useCallback((e: ChangeEvent) => { if (!isControlled) { setInnerChecked(e.target.checked) } onChange?.(e.target.checked) }, [isControlled, onChange]) const inputRef = useRef(null) const labelClickTimeRef = useRef(0) // 解决点击 input 外的 label 区域会触发两次 click 事件的问题 // https://github.com/facebook/react/issues/14295 const handleClick = useCallback((e: React.MouseEvent) => { if (inputRef.current !== e.target) { // e.target 在 input 外的 label 区域,记录时间,用于识别它即将触发的 input 点击事件 labelClickTimeRef.current = Date.now() } else { // e.target 是 input,需要区分是 label 触发的还是它自己被点击了 // 如果是 label 触发的,两次事件间隔大概 0-2 ms(本地测试结果),判定条件保险起见设置为 20ms const isLabelTriggered = Date.now() - labelClickTimeRef.current < 20 if (isLabelTriggered) { e.stopPropagation() return } } rootHtmlProps?.onClick?.(e) }, [rootHtmlProps]) const prefix = usePrefix('checkbox', prefixCls) const formSize = useFormSize(size) const checkboxCns = cns(prefix, concat(prefix, 'size', formSize), { [concat(prefix, 'checked')]: checked, [concat(prefix, 'partial', 'checked')]: partialChecked, [concat(prefix, 'disabled')]: disabled }, className, rootHtmlProps?.className) return ( ) }