import React, { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' import cns from 'classnames' import { CommonCheckboxProps, InternalCheckbox, InternalCheckboxProps } from './InternalCheckbox' import { ComponentSize, useLangData, usePrefix } from '../ConfigProvider' import { concat, isDev } from '../utils' import { BaseCommonProps } from '../Input/input' import { useValue } from '../utils/hooks' type Value = string | number | boolean export type CheckboxValue = Value export interface CheckboxGroupProps extends BaseCommonProps{ /** 内容(包含 `Checkbox` / `CheckAll` ) */ children: ReactNode } type Option = { value: T disabled: boolean } interface CheckboxGroupContext { value: T[] disabled?: boolean options: Array> size?: ComponentSize registerOption: (val: T, disabled?: boolean) => void removeOption: (val: T) => void toggleCheck: (val: T) => void toggleCheckAll: (check: boolean) => void } const groupContext = createContext | null>(null) export function CheckboxGroup({ value: valueFromProps, defaultValue = [], disabled, onChange, size, children }: CheckboxGroupProps) { const { value, setValue } = useValue({ valueFromProps, defaultValue, onChange }) const [options, setOptions] = useState>>([]) const registerOption = useCallback((val: T, dis = false) => { setOptions(prevOptions => { const existed = prevOptions.find(option => option.value === val) if (existed) { existed.disabled = dis return prevOptions } return [...prevOptions, { value: val, disabled: dis }] }) }, []) const removeOption = useCallback((val: T) => { setOptions(prevOptions => prevOptions.filter(v => v.value !== val)) }, []) const toggleCheck = useCallback((val: T) => { const valueIndex = value.indexOf(val) const newValue = [...value] if (valueIndex === -1) { newValue.push(val) } else { newValue.splice(valueIndex, 1) } const idxInOptions = (v: T) => options.findIndex(o => o.value === v) newValue.sort((v1, v2) => idxInOptions(v1) - idxInOptions(v2)) setValue(newValue) }, [options, setValue, value]) const toggleCheckAll = useCallback((check: boolean) => { const toToggle = options.filter(o => !o.disabled).map(o => o.value) const newValue = value.filter(v => !toToggle.includes(v)) if (check) newValue.splice(newValue.length, 0, ...toToggle) const idxInOptions = (v: T) => options.findIndex(o => o.value === v) newValue.sort((v1, v2) => idxInOptions(v1) - idxInOptions(v2)) setValue(newValue) }, [options, setValue, value]) const context: CheckboxGroupContext = useMemo(() => ({ value, disabled, options, size, registerOption, removeOption, toggleCheck, toggleCheckAll }), [disabled, options, registerOption, removeOption, size, toggleCheck, toggleCheckAll, value]) return ( {children} ) } export interface CheckboxProps extends CommonCheckboxProps { /** * 是否带边框 * @default false */ bordered?: boolean /** 该选项对应的值,在 Group 中使用时需要设置,Checkbox 单独使用时将被忽略 */ value?: T } export function Checkbox({ bordered, value, ...restProps }: CheckboxProps) { const prefix = usePrefix('checkbox') const checkboxProps: CheckboxProps = { ...restProps } const checkboxGroup = useContext(groupContext) if (!checkboxGroup) { checkboxProps.className = cns({ [concat(prefix, 'bordered')]: bordered }, restProps.className) } else { if (isDev && value === undefined) { // eslint-disable-next-line no-console console.warn('`value` must be set when `` used in ``') } checkboxProps.onChange = checked => { restProps.onChange?.(checked) checkboxGroup.toggleCheck(value) } checkboxProps.checked = checkboxGroup.value.includes(value) checkboxProps.disabled = restProps.disabled || checkboxGroup.disabled checkboxProps.size = checkboxProps.size ?? checkboxGroup.size } const registerOption = checkboxGroup?.registerOption const removeOption = checkboxGroup?.removeOption useEffect(() => { registerOption?.(value, checkboxProps.disabled) return () => removeOption?.(value) }, [value, checkboxProps.disabled, registerOption, removeOption]) return } export function CheckAll(props: Omit) { const checkboxGroup = useContext(groupContext) const checkboxProps: InternalCheckboxProps = { ...props } if (checkboxGroup) { const { value, options } = checkboxGroup checkboxProps.onChange = check => { const nonDisabledOptions = options.filter(o => !o.disabled) if (nonDisabledOptions.every(o => value.includes(o.value))) { // 若每个可能被选中的项都被选中了,那么就应该 check: false check = false } props.onChange?.(check) checkboxGroup.toggleCheckAll(check) } checkboxProps.disabled = (props.disabled || checkboxGroup.disabled) || options.length === 0 // options 为空数组时,options.every 会得到 true,所以这里要判断一下 options.length 必须大于 0 checkboxProps.checked = options.length > 0 && options.every(o => value.includes(o.value)) checkboxProps.partialChecked = !checkboxProps.checked && options.some(o => value.includes(o.value)) } const { checkAllText } = useLangData('checkbox') checkboxProps.children = props.children === undefined ? checkAllText : props.children return }