Skip to content

Commit e0e3875

Browse files
author
Hector Arce De Las Heras
committed
Enhance Accessibility of ToggleTwoPositionComponent and Update Option Component
This commit improves the accessibility of the ToggleTwoPositionComponent and updates the Option component. Changes include: ToggleTwoPositionComponent in toggleTwoPositions.tsx now includes the aria-labelledby attribute, enhancing its accessibility. Descriptions of aria-describedby and aria-labelledby in argtypes.ts have been updated for clarity. A new content prop has been added to the Option component, and the toggle prop has been marked as deprecated. These changes improve the accessibility of the components and ensure the components stay up-to-date with current best practices.
1 parent 741a6dd commit e0e3875

File tree

9 files changed

+391
-242
lines changed

9 files changed

+391
-242
lines changed

src/components/toggle/__tests__/toggle.test.tsx

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { axe } from 'jest-axe';
77

88
import { InputTypeType } from '@/components/input';
99
import { renderProvider } from '@/tests/renderProvider/renderProvider.utility';
10-
import { POSITIONS } from '@/types';
10+
import { POSITIONS, ROLES } from '@/types';
1111
import * as keyboard from '@/utils/keyboard/keyboard.utility';
1212

1313
import { ToggleControlled } from '../toggleControlled';
@@ -26,6 +26,7 @@ const mockProps = {
2626
leftIconAltText: 'off option',
2727
},
2828
dataTestId: 'toggleId',
29+
['aria-label']: 'toggle',
2930
};
3031

3132
const mockPropsThreePositions = {
@@ -46,7 +47,7 @@ describe('Toggle component', () => {
4647
it('should display an accesible text by default corresponding to the unchecked state', async () => {
4748
const { container } = renderProvider(<Toggle {...mockProps} />);
4849

49-
const toggle = screen.getAllByRole(InputTypeType.RADIO)[0];
50+
const toggle = screen.getByRole(ROLES.SWITCH);
5051

5152
expect(toggle).toBeInTheDocument();
5253
expect(toggle).not.toBeChecked();
@@ -61,23 +62,10 @@ describe('Toggle component', () => {
6162
<Toggle {...mockProps} defaultTogglePosition={POSITIONS.RIGHT} />
6263
);
6364

64-
const toggle = screen.getAllByRole(InputTypeType.RADIO)[0];
65+
const toggle = screen.getByRole(ROLES.SWITCH);
6566

6667
expect(toggle).toBeInTheDocument();
67-
expect(toggle).not.toBeChecked();
68-
69-
const results = await axe(container);
70-
expect(container).toHTMLValidate();
71-
expect(results).toHaveNoViolations();
72-
});
73-
74-
it('should display with disabled state', async () => {
75-
const { container } = renderProvider(<Toggle {...mockProps} disabled={true} />);
76-
77-
const toggle = screen.getAllByRole(InputTypeType.RADIO)[0];
78-
79-
expect(toggle).toBeInTheDocument();
80-
expect(toggle).toBeDisabled();
68+
expect(toggle).toBeChecked();
8169

8270
const results = await axe(container);
8371
expect(container).toHTMLValidate();
@@ -89,11 +77,10 @@ describe('Toggle component', () => {
8977
<Toggle {...mockProps} defaultTogglePosition={POSITIONS.RIGHT} disabled={true} />
9078
);
9179

92-
const toggle = screen.getAllByRole(InputTypeType.RADIO)[0];
80+
const toggle = screen.getByRole(ROLES.SWITCH);
9381

9482
expect(toggle).toBeInTheDocument();
95-
expect(toggle).not.toBeChecked();
96-
expect(toggle).toBeDisabled();
83+
expect(toggle).toBeChecked();
9784

9885
const results = await axe(container);
9986
expect(container).toHTMLValidate();
@@ -103,22 +90,14 @@ describe('Toggle component', () => {
10390
it('should hide said text and display an alternative when clicked', async () => {
10491
const { container } = renderProvider(<Toggle {...mockProps} />);
10592

106-
const toggle = screen.getAllByRole(InputTypeType.RADIO)[0];
93+
const toggle = screen.getByRole(ROLES.SWITCH);
10794

10895
expect(toggle).not.toBeChecked();
10996

11097
await user.click(toggle);
11198

11299
expect(toggle).toBeChecked();
113100

114-
const secondToggle = screen.getAllByRole(InputTypeType.RADIO)[1];
115-
116-
expect(secondToggle).not.toBeChecked();
117-
118-
await user.click(secondToggle);
119-
120-
expect(secondToggle).toBeChecked();
121-
122101
const results = await axe(container);
123102
expect(container).toHTMLValidate();
124103
expect(results).toHaveNoViolations();
@@ -128,8 +107,8 @@ describe('Toggle component', () => {
128107
jest.spyOn(keyboard, 'isKeyEnterPressed').mockReturnValueOnce(true);
129108
const { container } = renderProvider(<Toggle {...mockProps} />);
130109

131-
expect(screen.getByTestId('toggleIdOnLabelOption')).toBeInTheDocument();
132-
expect(screen.getByTestId('toggleIdOffLabelOption')).toBeInTheDocument();
110+
expect(screen.getByTestId('toggleIdOnOption')).toBeInTheDocument();
111+
expect(screen.getByTestId('toggleIdOffOption')).toBeInTheDocument();
133112

134113
const group = screen.getByTestId('toggleId');
135114
expect(group).toBeInTheDocument();
@@ -159,10 +138,13 @@ describe('Toggle component', () => {
159138
const onChange = jest.fn();
160139
renderProvider(<Toggle {...mockProps} onChange={onChange} />);
161140

162-
const secondToggle = screen.getAllByRole(InputTypeType.RADIO)[1];
163-
expect(secondToggle).not.toBeChecked();
164-
await user.click(secondToggle);
165-
expect(secondToggle).toBeChecked();
141+
const toggle = screen.getByRole(ROLES.SWITCH);
142+
expect(toggle).not.toBeChecked();
143+
await user.click(toggle);
144+
expect(toggle).toBeChecked();
145+
146+
await user.click(toggle);
147+
expect(toggle).not.toBeChecked();
166148

167149
const group = screen.getByTestId('toggleId');
168150
expect(group).toBeInTheDocument();
@@ -218,7 +200,14 @@ describe('Toogle Three Positions', () => {
218200
jest.spyOn(keyboard, 'isKeySpacePressed').mockReturnValueOnce(true);
219201
const onClick = jest.fn();
220202
const onKeyDown = jest.fn();
221-
renderProvider(<Toggle {...mockPropsThreePositions} onClick={onClick} onKeyDown={onKeyDown} />);
203+
renderProvider(
204+
<Toggle
205+
{...mockPropsThreePositions}
206+
blockCenter={true}
207+
onClick={onClick}
208+
onKeyDown={onKeyDown}
209+
/>
210+
);
222211

223212
const onLabelOption = screen.getByTestId('toggleIdOnLabelOption');
224213
expect(onLabelOption).toBeInTheDocument();
@@ -231,6 +220,30 @@ describe('Toogle Three Positions', () => {
231220
expect(onKeyDown).toHaveBeenCalled();
232221
});
233222

223+
it('ThreePositions - With disabled true click shouldnt work', async () => {
224+
jest.spyOn(keyboard, 'isKeySpacePressed').mockReturnValueOnce(true);
225+
const onClick = jest.fn();
226+
const onKeyDown = jest.fn();
227+
renderProvider(
228+
<Toggle
229+
{...mockPropsThreePositions}
230+
disabled={true}
231+
onClick={onClick}
232+
onKeyDown={onKeyDown}
233+
/>
234+
);
235+
236+
const onLabelOption = screen.getByTestId('toggleIdOnLabelOption');
237+
expect(onLabelOption).toBeInTheDocument();
238+
fireEvent.click(onLabelOption);
239+
expect(onClick).not.toHaveBeenCalled();
240+
241+
const group = screen.getByTestId('toggleId');
242+
expect(group).toBeInTheDocument();
243+
fireEvent.keyDown(onLabelOption);
244+
expect(onKeyDown).not.toHaveBeenCalled();
245+
});
246+
234247
it('ThreePositions - Should click in offText and called onKeyDown', () => {
235248
jest.spyOn(keyboard, 'isKeySpacePressed').mockReturnValueOnce(true);
236249
renderProvider(<Toggle {...mockPropsThreePositions} />);
@@ -273,7 +286,7 @@ describe('ToggleControlled', () => {
273286
it('should display an accesible text by default corresponding to the unchecked state', async () => {
274287
const { container } = renderProvider(<ToggleControlled {...mockProps} />);
275288

276-
const toggle = screen.getAllByRole(InputTypeType.RADIO)[0];
289+
const toggle = screen.getByRole(ROLES.SWITCH);
277290

278291
expect(toggle).toBeInTheDocument();
279292
expect(toggle).not.toBeChecked();
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React from 'react';
2+
3+
import { InputTypeType } from '@/components/input';
4+
import { Label } from '@/components/label';
5+
import { ScreenReaderOnly } from '@/components/screenReaderOnly';
6+
import { Text, TextComponentType } from '@/components/text';
7+
import { AriaLiveOptionType, POSITIONS } from '@/types';
8+
9+
import {
10+
LabelWrapperStyled,
11+
SliderContainerStyled,
12+
TextLeftWrapperStyled,
13+
TextRightWrapperStyled,
14+
ToggleRadioSwitchStyled,
15+
ToggleWrapperStyled,
16+
} from '../toggle.styled';
17+
import { IToggleStandAlone } from '../types';
18+
19+
interface IToggleThreePosition extends IToggleStandAlone {}
20+
21+
const ToggleThreePositionComponent = (
22+
{
23+
inputValues = {
24+
rightInputValue: 'on option',
25+
centerInputValue: 'undeterminated option',
26+
leftInputValue: 'off option',
27+
},
28+
radioButtonToggleName = 'groupe-toggle',
29+
blockCenter = false,
30+
...props
31+
}: IToggleThreePosition,
32+
ref: React.ForwardedRef<HTMLDivElement> | undefined | null
33+
): JSX.Element => {
34+
const getScreenReaderText = (position: POSITIONS) =>
35+
position === POSITIONS.CENTER
36+
? inputValues.centerInputValue
37+
: position === POSITIONS.RIGHT
38+
? inputValues.rightInputValue ?? props.onIcon?.altText
39+
: inputValues.leftInputValue ?? props.offIcon?.altText;
40+
41+
const buildTextOrIcon = (position: POSITIONS) => {
42+
return (
43+
<>
44+
{position === POSITIONS.CENTER
45+
? inputValues.centerInputValue
46+
: position === POSITIONS.RIGHT
47+
? props.onText?.content
48+
: props.offText?.content}
49+
</>
50+
);
51+
};
52+
53+
const getValueOfRadioButton = (position: POSITIONS) => {
54+
if (position === POSITIONS.RIGHT) {
55+
return inputValues?.rightInputValue;
56+
} else if (position === POSITIONS.LEFT) {
57+
return inputValues?.leftInputValue;
58+
}
59+
return inputValues?.centerInputValue;
60+
};
61+
62+
const buildRadioButton = (position: POSITIONS, block = false) => {
63+
return (
64+
<>
65+
<ToggleRadioSwitchStyled
66+
$height={props.styles?.thumb?.height}
67+
$width={props.styles?.thumb?.width}
68+
aria-describedby={props.screenReaderId}
69+
aria-labelledby={props['aria-describedby']}
70+
disabled={props.disabled}
71+
id={`${position.toLowerCase()}Input`}
72+
name={radioButtonToggleName}
73+
tabIndex={-1}
74+
type={InputTypeType.RADIO}
75+
value={getValueOfRadioButton(position)}
76+
onClick={e => {
77+
if (!block) {
78+
e.persist();
79+
props.onClick?.(position, e);
80+
}
81+
}}
82+
/>
83+
<LabelWrapperStyled
84+
hasThreePositions={props.hasThreePositions}
85+
showLabel={position !== POSITIONS.CENTER && position === props.togglePosition}
86+
styles={props.styles}
87+
togglePosition={props.togglePosition}
88+
>
89+
<Label
90+
color={props.styles?.label?.color}
91+
dataTestId={`${props.dataTestId}${
92+
position === POSITIONS.RIGHT ? 'On' : position === POSITIONS.CENTER ? 'Na' : 'Off'
93+
}LabelOption`}
94+
inputId={`${position.toLowerCase()}Input`}
95+
textVariant={
96+
(position === POSITIONS.LEFT && props.offText?.content) ||
97+
(position === POSITIONS.RIGHT && props.onText?.content)
98+
? props.styles?.label?.font_variant
99+
: props.styles?.labelWithIcons?.font_variant
100+
}
101+
weight={props.styles?.label?.font_weight}
102+
>
103+
{buildTextOrIcon(position)}
104+
</Label>
105+
</LabelWrapperStyled>
106+
</>
107+
);
108+
};
109+
110+
const buildCenterRadioButton = () => (
111+
<>
112+
{props.togglePosition === POSITIONS.CENTER && (
113+
<TextLeftWrapperStyled margin={props.styles?.label?.margin_left}>
114+
<Text
115+
aria-hidden={true}
116+
color={props.styles?.label?.color}
117+
component={TextComponentType.LABEL}
118+
dataTestId={`${props.dataTestId}OffLabel`}
119+
variant={props.styles?.label?.font_variant}
120+
weight={props.styles?.label?.font_weight}
121+
>
122+
{props.offText?.content}
123+
</Text>
124+
</TextLeftWrapperStyled>
125+
)}
126+
{buildRadioButton(
127+
POSITIONS.CENTER,
128+
blockCenter ? props.togglePosition !== POSITIONS.CENTER : false
129+
)}
130+
{props.togglePosition === POSITIONS.CENTER && (
131+
<TextRightWrapperStyled margin={props.styles?.label?.margin_right}>
132+
<Text
133+
aria-hidden={true}
134+
color={props.styles?.label?.color}
135+
component={TextComponentType.LABEL}
136+
dataTestId={`${props.dataTestId}OnLabel`}
137+
variant={props.styles?.label?.font_variant}
138+
weight={props.styles?.label?.font_weight}
139+
>
140+
{props.onText?.content}
141+
</Text>
142+
</TextRightWrapperStyled>
143+
)}
144+
</>
145+
);
146+
147+
return (
148+
<ToggleWrapperStyled
149+
ref={ref}
150+
aria-label={props['aria-label']}
151+
data-testid={props.dataTestId}
152+
disabled={props.disabled}
153+
hasThreePositions={props.hasThreePositions}
154+
id={props.id}
155+
styles={props.styles}
156+
tabIndex={!props.disabled ? props.tabIndex ?? 0 : undefined}
157+
togglePosition={props.togglePosition}
158+
onKeyDown={e => {
159+
e.persist();
160+
props.onKeyDown?.(e);
161+
}}
162+
>
163+
<ScreenReaderOnly ariaLive={AriaLiveOptionType.POLITE}>
164+
{getScreenReaderText(props.togglePosition)}
165+
</ScreenReaderOnly>
166+
{buildRadioButton(POSITIONS.LEFT)}
167+
{props.hasThreePositions && buildCenterRadioButton()}
168+
{buildRadioButton(POSITIONS.RIGHT)}
169+
<SliderContainerStyled
170+
aria-hidden="true"
171+
data-testid={`${props.dataTestId}Thumb`}
172+
hasThreePositions={props.hasThreePositions}
173+
styles={props.styles}
174+
togglePosition={props.togglePosition}
175+
/>
176+
</ToggleWrapperStyled>
177+
);
178+
};
179+
180+
export const ToggleThreePosition = React.forwardRef(ToggleThreePositionComponent);

0 commit comments

Comments
 (0)