Skip to content

Commit cd06a20

Browse files
feat(switch): 支持 circle/round/line 三种 shape
issue: Tencent/tdesign-common#2563
1 parent b887818 commit cd06a20

4 files changed

Lines changed: 82 additions & 3 deletions

File tree

packages/components/switch/Switch.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((originalProps,
2626
defaultValue,
2727
disabled,
2828
loading,
29+
shape,
2930
size,
3031
label,
3132
customValue,
@@ -40,13 +41,26 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((originalProps,
4041
const [innerChecked, setInnerChecked] = useState(initChecked);
4142

4243
const contentNode = React.useMemo<React.ReactNode>(() => {
44+
if (shape === 'line') return null;
4345
if (Array.isArray(label)) {
4446
const [activeContent = '', inactiveContent = ''] = label;
4547
const content = innerChecked ? activeContent : inactiveContent;
4648
return parseTNode(content, { value });
4749
}
4850
return parseTNode(label, { value });
49-
}, [label, innerChecked, value]);
51+
}, [label, innerChecked, shape, value]);
52+
53+
const derivedAriaLabel = React.useMemo(() => {
54+
if (shape !== 'line') return undefined;
55+
if (Array.isArray(label)) {
56+
const [activeLabel = '已开启', inactiveLabel = '已关闭'] = label;
57+
const active = typeof activeLabel === 'string' ? activeLabel : '已开启';
58+
const inactive = typeof inactiveLabel === 'string' ? inactiveLabel : '已关闭';
59+
return innerChecked ? active : inactive;
60+
}
61+
if (typeof label === 'string') return label;
62+
return undefined;
63+
}, [shape, label, innerChecked]);
5064

5165
const handleChange = (e: React.MouseEvent) => {
5266
!isControlled && setInnerChecked(!innerChecked);
@@ -84,6 +98,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((originalProps,
8498
const { SIZE, STATUS } = useCommonClassName();
8599
const switchClassName = classNames(
86100
`${classPrefix}-switch`,
101+
`${classPrefix}-switch--shape-${shape}`,
87102
className,
88103
{
89104
[STATUS.checked]: innerChecked,
@@ -98,13 +113,16 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((originalProps,
98113
{...restProps}
99114
type="button"
100115
role="switch"
116+
aria-checked={innerChecked}
117+
aria-disabled={disabled || loading}
118+
aria-label={restProps['aria-label'] ?? derivedAriaLabel}
101119
disabled={disabled || loading}
102120
className={switchClassName}
103121
ref={ref}
104122
onClick={onInternalClick}
105123
>
106124
<span className={`${classPrefix}-switch__handle`}>{loading && <Loading loading size="small" />}</span>
107-
<div className={`${classPrefix}-switch__content`}>{contentNode}</div>
125+
{shape !== 'line' && <div className={`${classPrefix}-switch__content`}>{contentNode}</div>}
108126
</button>
109127
);
110128
});

packages/components/switch/__tests__/switch.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,55 @@ describe('Switch 组件测试', () => {
2727
expect(container.children[0].classList.contains('t-size-s')).toBeTruthy();
2828
});
2929

30+
test('shape', async () => {
31+
const { container, rerender, queryByText } = render(<Switch label={['开', '关']} />);
32+
expect(container.children[0].classList.contains('t-switch--shape-circle')).toBeTruthy();
33+
expect(queryByText('关')).toBeInTheDocument();
34+
35+
rerender(<Switch shape="round" />);
36+
expect(container.children[0].classList.contains('t-switch--shape-round')).toBeTruthy();
37+
38+
rerender(<Switch shape="line" label={['开', '关']} size="small" loading />);
39+
expect(container.children[0].classList.contains('t-switch--shape-line')).toBeTruthy();
40+
expect(container.children[0].classList.contains('t-size-s')).toBeTruthy();
41+
expect(container.children[0].classList.contains('t-is-loading')).toBeTruthy();
42+
expect(queryByText('关')).not.toBeInTheDocument();
43+
expect(container.querySelector('.t-switch__content')).toBeFalsy();
44+
45+
const label = vi.fn(() => '关');
46+
rerender(<Switch shape="line" label={label} />);
47+
expect(label).not.toBeCalled();
48+
});
49+
50+
test('line shape aria-label', async () => {
51+
const { container, rerender } = render(<Switch shape="line" label={['开启', '关闭']} />);
52+
expect(container.firstChild).toHaveAttribute('aria-label', '关闭');
53+
54+
fireEvent.click(container.firstChild);
55+
expect(container.firstChild).toHaveAttribute('aria-label', '开启');
56+
57+
// 独立 render,innerChecked 默认 false
58+
const { container: container2 } = render(<Switch shape="line" />);
59+
expect(container2.firstChild).toHaveAttribute('aria-label', '已关闭');
60+
61+
rerender(<Switch shape="line" label={['开', '关']} aria-label="自定义开关" />);
62+
expect(container.firstChild).toHaveAttribute('aria-label', '自定义开关');
63+
64+
rerender(<Switch shape="circle" label={['开', '关']} />);
65+
expect(container.firstChild).not.toHaveAttribute('aria-label');
66+
});
67+
68+
test('aria-disabled', async () => {
69+
const { container } = render(<Switch disabled />);
70+
expect(container.firstChild).toHaveAttribute('aria-disabled', 'true');
71+
72+
const { container: container2 } = render(<Switch loading />);
73+
expect(container2.firstChild).toHaveAttribute('aria-disabled', 'true');
74+
75+
const { container: container3 } = render(<Switch />);
76+
expect(container3.firstChild).toHaveAttribute('aria-disabled', 'false');
77+
});
78+
3079
test('disabled', async () => {
3180
const clickFn = vi.fn();
3281
const { container } = render(<Switch disabled />);
@@ -37,7 +86,9 @@ describe('Switch 组件测试', () => {
3786
test('onChange', async () => {
3887
const clickFn = vi.fn();
3988
const { container } = render(<Switch onChange={clickFn} />);
89+
expect(container.firstChild).toHaveAttribute('aria-checked', 'false');
4090
fireEvent.click(container.firstChild);
91+
expect(container.firstChild).toHaveAttribute('aria-checked', 'true');
4192
expect(clickFn).toHaveBeenCalledTimes(1);
4293
});
4394

packages/components/switch/defaultProps.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@
44

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

7-
export const switchDefaultProps: TdSwitchProps = { label: [], loading: false, size: 'medium' };
7+
export const switchDefaultProps: TdSwitchProps = {
8+
label: [],
9+
loading: false,
10+
shape: 'circle',
11+
size: 'medium',
12+
};

packages/components/switch/type.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export interface TdSwitchProps<T = SwitchValue> {
2929
* @default false
3030
*/
3131
loading?: boolean;
32+
/**
33+
* 开关形状。`line` 形态不展示开关内容 `label`
34+
* @default circle
35+
*/
36+
shape?: 'circle' | 'round' | 'line';
3237
/**
3338
* 开关尺寸
3439
* @default medium

0 commit comments

Comments
 (0)