Skip to content
Closed
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
22 changes: 20 additions & 2 deletions packages/components/switch/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((originalProps,
defaultValue,
disabled,
loading,
shape,
size,
label,
customValue,
Expand All @@ -40,13 +41,26 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((originalProps,
const [innerChecked, setInnerChecked] = useState(initChecked);

const contentNode = React.useMemo<React.ReactNode>(() => {
if (shape === 'line') return null;
if (Array.isArray(label)) {
const [activeContent = '', inactiveContent = ''] = label;
const content = innerChecked ? activeContent : inactiveContent;
return parseTNode(content, { value });
}
return parseTNode(label, { value });
}, [label, innerChecked, value]);
}, [label, innerChecked, shape, value]);

const derivedAriaLabel = React.useMemo(() => {
if (shape !== 'line') return undefined;
if (Array.isArray(label)) {
const [activeLabel = '已开启', inactiveLabel = '已关闭'] = label;
const active = typeof activeLabel === 'string' ? activeLabel : '已开启';
const inactive = typeof inactiveLabel === 'string' ? inactiveLabel : '已关闭';
return innerChecked ? active : inactive;
}
if (typeof label === 'string') return label;
return undefined;
}, [shape, label, innerChecked]);

const handleChange = (e: React.MouseEvent) => {
!isControlled && setInnerChecked(!innerChecked);
Expand Down Expand Up @@ -84,6 +98,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((originalProps,
const { SIZE, STATUS } = useCommonClassName();
const switchClassName = classNames(
`${classPrefix}-switch`,
`${classPrefix}-switch--shape-${shape}`,
className,
{
[STATUS.checked]: innerChecked,
Expand All @@ -98,13 +113,16 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((originalProps,
{...restProps}
type="button"
role="switch"
aria-checked={innerChecked}
aria-disabled={disabled || loading}
aria-label={restProps['aria-label'] ?? derivedAriaLabel}
disabled={disabled || loading}
className={switchClassName}
ref={ref}
onClick={onInternalClick}
>
<span className={`${classPrefix}-switch__handle`}>{loading && <Loading loading size="small" />}</span>
<div className={`${classPrefix}-switch__content`}>{contentNode}</div>
{shape !== 'line' && <div className={`${classPrefix}-switch__content`}>{contentNode}</div>}
</button>
);
});
Expand Down
51 changes: 51 additions & 0 deletions packages/components/switch/__tests__/switch.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,55 @@ describe('Switch 组件测试', () => {
expect(container.children[0].classList.contains('t-size-s')).toBeTruthy();
});

test('shape', async () => {
const { container, rerender, queryByText } = render(<Switch label={['开', '关']} />);
expect(container.children[0].classList.contains('t-switch--shape-circle')).toBeTruthy();
expect(queryByText('关')).toBeInTheDocument();

rerender(<Switch shape="round" />);
expect(container.children[0].classList.contains('t-switch--shape-round')).toBeTruthy();

rerender(<Switch shape="line" label={['开', '关']} size="small" loading />);
expect(container.children[0].classList.contains('t-switch--shape-line')).toBeTruthy();
expect(container.children[0].classList.contains('t-size-s')).toBeTruthy();
expect(container.children[0].classList.contains('t-is-loading')).toBeTruthy();
expect(queryByText('关')).not.toBeInTheDocument();
expect(container.querySelector('.t-switch__content')).toBeFalsy();

const label = vi.fn(() => '关');
rerender(<Switch shape="line" label={label} />);
expect(label).not.toBeCalled();
});

test('line shape aria-label', async () => {
const { container, rerender } = render(<Switch shape="line" label={['开启', '关闭']} />);
expect(container.firstChild).toHaveAttribute('aria-label', '关闭');

fireEvent.click(container.firstChild);
expect(container.firstChild).toHaveAttribute('aria-label', '开启');

// 独立 render,innerChecked 默认 false
const { container: container2 } = render(<Switch shape="line" />);
expect(container2.firstChild).toHaveAttribute('aria-label', '已关闭');

rerender(<Switch shape="line" label={['开', '关']} aria-label="自定义开关" />);
expect(container.firstChild).toHaveAttribute('aria-label', '自定义开关');

rerender(<Switch shape="circle" label={['开', '关']} />);
expect(container.firstChild).not.toHaveAttribute('aria-label');
});

test('aria-disabled', async () => {
const { container } = render(<Switch disabled />);
expect(container.firstChild).toHaveAttribute('aria-disabled', 'true');

const { container: container2 } = render(<Switch loading />);
expect(container2.firstChild).toHaveAttribute('aria-disabled', 'true');

const { container: container3 } = render(<Switch />);
expect(container3.firstChild).toHaveAttribute('aria-disabled', 'false');
});

test('disabled', async () => {
const clickFn = vi.fn();
const { container } = render(<Switch disabled />);
Expand All @@ -37,7 +86,9 @@ describe('Switch 组件测试', () => {
test('onChange', async () => {
const clickFn = vi.fn();
const { container } = render(<Switch onChange={clickFn} />);
expect(container.firstChild).toHaveAttribute('aria-checked', 'false');
fireEvent.click(container.firstChild);
expect(container.firstChild).toHaveAttribute('aria-checked', 'true');
expect(clickFn).toHaveBeenCalledTimes(1);
});

Expand Down
7 changes: 6 additions & 1 deletion packages/components/switch/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@

import type { TdSwitchProps } from './type';

export const switchDefaultProps: TdSwitchProps = { label: [], loading: false, size: 'medium' };
export const switchDefaultProps: TdSwitchProps = {
label: [],
loading: false,
shape: 'circle',
size: 'medium',
};
5 changes: 5 additions & 0 deletions packages/components/switch/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export interface TdSwitchProps<T = SwitchValue> {
* @default false
*/
loading?: boolean;
/**
* 开关形状。`line` 形态不展示开关内容 `label`
* @default circle
*/
shape?: 'circle' | 'round' | 'line';
/**
* 开关尺寸
* @default medium
Expand Down
Loading