Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/components/checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import React from 'react';
import React, { forwardRef } from 'react';

import forwardRefWithStatics from '../_util/forwardRefWithStatics';
import Check from '../common/Check';
import useDefaultProps from '../hooks/useDefaultProps';
import CheckboxGroup from './CheckboxGroup';
import { checkboxDefaultProps } from './defaultProps';

import type { Ref } from 'react';
import type { CheckProps } from '../common/Check';

export type CheckboxProps = Omit<CheckProps, 'type'>;

const Checkbox = forwardRefWithStatics(
(props: CheckboxProps, ref: React.Ref<HTMLLabelElement>) => (
(props: CheckboxProps, ref: Ref<HTMLLabelElement>) => (
<Check ref={ref} type="checkbox" {...useDefaultProps<CheckboxProps>(props, checkboxDefaultProps)} />
),
{ Group: CheckboxGroup },
{
Group: CheckboxGroup,
Button: forwardRef<HTMLLabelElement, CheckboxProps>((props, ref) => (
<Check ref={ref} type="checkbox-button" {...useDefaultProps<CheckboxProps>(props, checkboxDefaultProps)} />
)),
},
);

Checkbox.displayName = 'Checkbox';
Expand Down
110 changes: 66 additions & 44 deletions packages/components/checkbox/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { isNumber } from 'lodash-es';

import { CheckContext } from '../common/Check';
import useCommonClassName from '../hooks/useCommonClassName';
import useConfig from '../hooks/useConfig';
import useControlled from '../hooks/useControlled';
import useDefaultProps from '../hooks/useDefaultProps';
import Checkbox from './Checkbox';
import { checkboxGroupDefaultProps } from './defaultProps';

import type { ReactElement } from 'react';
import type { ReactElement, ReactNode } from 'react';
import type { StyledProps } from '../common';
import type { CheckContextValue, CheckProps } from '../common/Check';
import type { CheckboxProps } from './Checkbox';
Expand All @@ -23,7 +24,7 @@ import type {

export interface CheckboxGroupProps<T extends CheckboxGroupValue = CheckboxGroupValue>
extends TdCheckboxGroupProps<T>, StyledProps {
children?: React.ReactNode;
children?: ReactNode;
}

const getCheckboxValue = (v: CheckboxOption) => {
Expand All @@ -47,6 +48,7 @@ const getCheckboxValue = (v: CheckboxOption) => {
const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props: CheckboxGroupProps<T>) => {
type ItemType = T[number];
const { classPrefix } = useConfig();
const { SIZE: sizeMap } = useCommonClassName();
const {
onChange,
disabled,
Expand All @@ -55,6 +57,10 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
children,
max,
options = [],
size,
variant,
theme,
direction,
...resetProps
} = useDefaultProps<CheckboxGroupProps<T>>(props, checkboxGroupDefaultProps);

Expand All @@ -71,15 +77,15 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
) || [];

const optionsWithoutCheckAll = intervalOptions.filter((t) => typeof t !== 'object' || !t.checkAll);
const optionsWithoutCheckAllValues = [];
const optionsWithoutCheckAllValues: (string | number | boolean | undefined)[] = [];
optionsWithoutCheckAll.forEach((v: string | number) => {
const vs = getCheckboxValue(v);
optionsWithoutCheckAllValues.push(vs);
});

const { enabledValues, disabledValues } = useMemo(() => {
const enabledValues = [];
const disabledValues = [];
const enabledValues: (string | number | boolean | undefined)[] = [];
const disabledValues: (string | number | boolean | undefined)[] = [];
optionsWithoutCheckAll.forEach((option) => {
const isOptionDisabled = typeof option === 'object' && (option.disabled || option.readOnly || option.readonly);
const value = getCheckboxValue(option);
Expand All @@ -96,6 +102,8 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
const [internalValue, setInternalValue] = useControlled(props, 'value', onChange);
const [localMax, setLocalMax] = useState(max);

const checkboxGroupRef = useRef<HTMLDivElement>(null);

const getCheckedSet = useCallback(() => {
if (!Array.isArray(internalValue)) {
return new Set<ItemType>([]);
Expand Down Expand Up @@ -194,45 +202,59 @@ const CheckboxGroup = <T extends CheckboxGroupValue = CheckboxGroupValue>(props:
// options 和 children 的抉择,在未明确说明时,暂时以 options 优先
const useOptions = Array.isArray(options) && options.length !== 0;

// 根据 theme 选择渲染组件
const Comp = theme === 'button' ? Checkbox.Button : Checkbox;

const renderOptions = (): ReactNode => {
return options.map((v: CheckboxOption, index: number) => {
switch (typeof v) {
case 'string':
return (
<Comp key={index} value={v}>
{v}
</Comp>
);
case 'number':
return (
<Comp key={index} value={v}>
{String(v)}
</Comp>
);
case 'object': {
const vs = v as CheckboxOptionObj;
// CheckAll 的 checkBox 不存在 value,故用 checkAll_index 来保证尽量不和用户的 value 冲突.
return vs.checkAll ? (
<Checkbox {...vs} key={`checkAll_${index}`} indeterminate={indeterminate} />
) : (
<Comp
{...vs}
key={index}
disabled={vs.disabled || disabled}
readOnly={vs.readOnly || vs.readonly || readOnly}
/>
);
}
default:
return null;
}
});
};

// 构建 className
const groupClassName = classNames(`${classPrefix}-checkbox-group`, sizeMap[size], className, {
// 边框型
[`${classPrefix}-checkbox-group__outline`]: theme === 'button' && variant === 'outline',
// 填充型
[`${classPrefix}-checkbox-group--filled`]: theme === 'button' && variant?.includes('filled'),
[`${classPrefix}-checkbox-group--default-filled`]: theme === 'button' && variant === 'default-filled',
[`${classPrefix}-checkbox-group--primary-filled`]: theme === 'button' && variant === 'primary-filled',
// 纵向排列
[`${classPrefix}-checkbox-group--vertical`]: direction === 'vertical',
});

return (
<div className={classNames(`${classPrefix}-checkbox-group`, className)} style={style}>
<CheckContext.Provider value={context}>
{useOptions
? options.map((v: any, index) => {
switch (typeof v) {
case 'string':
return (
<Checkbox key={index} label={v} value={v}>
{v}
</Checkbox>
);
case 'number': {
return (
<Checkbox key={index} label={v} value={v}>
{String(v)}
</Checkbox>
);
}
case 'object': {
const vs = v as CheckboxOptionObj;
// CheckAll 的 checkBox 不存在 value,故用 checkAll_index 来保证尽量不和用户的 value 冲突.
return vs.checkAll ? (
<Checkbox {...vs} key={`checkAll_${index}`} indeterminate={indeterminate} />
) : (
<Checkbox
{...vs}
key={index}
disabled={vs.disabled || disabled}
readOnly={vs.readOnly || vs.readonly || readOnly}
/>
);
}
default:
return null;
}
})
: children}
</CheckContext.Provider>
<div ref={checkboxGroupRef} style={style} className={groupClassName}>
<CheckContext.Provider value={context}>{useOptions ? renderOptions() : children}</CheckContext.Provider>
</div>
);
};
Expand Down
109 changes: 109 additions & 0 deletions packages/components/checkbox/__tests__/checkbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,112 @@ describe('CheckboxGroup', () => {
expect(container.firstChild.lastChild).toHaveClass('t-is-disabled');
});
});

describe('Checkbox.Button', () => {
test('button style', () => {
const { container, queryByText } = render(<Checkbox.Button checked={true}>选中按钮</Checkbox.Button>);
expect(container.firstChild).toHaveClass('t-checkbox-button', 't-is-checked');
expect(queryByText('选中按钮')).toBeInTheDocument();
});

test('button disabled', () => {
const fn = vi.fn();
const { container } = render(<Checkbox.Button disabled={true} onChange={fn}></Checkbox.Button>);
expect(container.firstChild).toHaveClass('t-is-disabled');
fireEvent.click(container.firstChild);
expect(fn).toBeCalledTimes(0);
});
});

describe('CheckboxGroup button style', () => {
test('theme=button with children', () => {
const { container } = render(
<Checkbox.Group theme="button" value={['bj']}>
<Checkbox.Button value="bj">北京</Checkbox.Button>
<Checkbox.Button value="sh">上海</Checkbox.Button>
<Checkbox.Button value="gz">广州</Checkbox.Button>
</Checkbox.Group>,
);
expect(container.firstChild).toHaveClass('t-checkbox-group');
expect(container.firstChild.firstChild).toHaveClass('t-checkbox-button', 't-is-checked');
});

test('variant outline', () => {
const { container } = render(
<Checkbox.Group theme="button" variant="outline" value={['bj']}>
<Checkbox.Button value="bj">北京</Checkbox.Button>
<Checkbox.Button value="sh">上海</Checkbox.Button>
</Checkbox.Group>,
);
expect(container.firstChild).toHaveClass('t-checkbox-group__outline');
});

test('variant default-filled', () => {
const { container } = render(
<Checkbox.Group theme="button" variant="default-filled" value={['bj']}>
<Checkbox.Button value="bj">北京</Checkbox.Button>
<Checkbox.Button value="sh">上海</Checkbox.Button>
</Checkbox.Group>,
);
expect(container.firstChild).toHaveClass('t-checkbox-group--filled', 't-checkbox-group--default-filled');
});

test('variant primary-filled', () => {
const { container } = render(
<Checkbox.Group theme="button" variant="primary-filled" value={['bj']}>
<Checkbox.Button value="bj">北京</Checkbox.Button>
<Checkbox.Button value="sh">上海</Checkbox.Button>
</Checkbox.Group>,
);
expect(container.firstChild).toHaveClass('t-checkbox-group--primary-filled');
});

test('direction vertical', () => {
const { container } = render(
<Checkbox.Group theme="button" direction="vertical">
<Checkbox.Button value="bj">北京</Checkbox.Button>
<Checkbox.Button value="sh">上海</Checkbox.Button>
</Checkbox.Group>,
);
expect(container.firstChild).toHaveClass('t-checkbox-group--vertical');
});

test('size small', () => {
const { container } = render(
<Checkbox.Group theme="button" size="small">
<Checkbox.Button value="bj">北京</Checkbox.Button>
</Checkbox.Group>,
);
expect(container.firstChild).toHaveClass('t-size-s');
});

test('size large', () => {
const { container } = render(
<Checkbox.Group theme="button" size="large">
<Checkbox.Button value="bj">北京</Checkbox.Button>
</Checkbox.Group>,
);
expect(container.firstChild).toHaveClass('t-size-l');
});

test('theme=button with options', () => {
const { container } = render(
<Checkbox.Group theme="button" variant="primary-filled" value={['北京']} options={['北京', '上海', '广州']} />,
);
expect(container.firstChild).toHaveClass('t-checkbox-group', 't-checkbox-group--primary-filled');
expect(container.firstChild.firstChild).toHaveClass('t-checkbox-button', 't-is-checked');
});

test('button onChange', () => {
const fn = vi.fn();
const { container } = render(
<Checkbox.Group theme="button" value={['bj']} onChange={fn}>
<Checkbox.Button value="bj">北京</Checkbox.Button>
<Checkbox.Button value="sh">上海</Checkbox.Button>
</Checkbox.Group>,
);
fireEvent.click(container.firstChild.childNodes[1]);
expect(fn).toBeCalledTimes(1);
expect(fn).toBeCalledWith(['bj', 'sh'], expect.any(Object));
});
});
Loading