Skip to content

Commit 849bb9a

Browse files
committed
feat(switch): add aria-disabled accessible disabled state
1 parent 73b5881 commit 849bb9a

File tree

6 files changed

+123
-75
lines changed

6 files changed

+123
-75
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- `Link`: handle `aria-disabled=true` for an accessible disabled state
1717
- `ListItem`: handle `aria-disabled=true` for an accessible disabled state
1818
- `RadioButton`: handle `aria-disabled=true` for an accessible disabled state
19+
- `Switch`: handle `aria-disabled=true` for an accessible disabled state
1920

2021
### Fixed
2122

packages/lumx-core/src/scss/components/switch/_index.scss

Lines changed: 71 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,6 @@
112112

113113
// Disabled state
114114
.#{$lumx-base-prefix}-switch--is-disabled {
115-
.#{$lumx-base-prefix}-switch__input-native {
116-
pointer-events: none;
117-
}
118-
119115
.#{$lumx-base-prefix}-switch__input-placeholder {
120116
@include lumx-state-disabled-input;
121117
}
@@ -135,89 +131,91 @@
135131
}
136132
}
137133

138-
// Hover state
139-
.#{$lumx-base-prefix}-switch--theme-light.#{$lumx-base-prefix}-switch--is-unchecked
140-
.#{$lumx-base-prefix}-switch__input-native:hover
141-
+ .#{$lumx-base-prefix}-switch__input-placeholder {
142-
.#{$lumx-base-prefix}-switch__input-background {
143-
@include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "dark");
134+
:not(.#{$lumx-base-prefix}-switch--is-disabled) {
135+
// Hover state
136+
&.#{$lumx-base-prefix}-switch--theme-light.#{$lumx-base-prefix}-switch--is-unchecked
137+
.#{$lumx-base-prefix}-switch__input-native:hover
138+
+ .#{$lumx-base-prefix}-switch__input-placeholder {
139+
.#{$lumx-base-prefix}-switch__input-background {
140+
@include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "dark");
141+
}
144142
}
145-
}
146143

147-
.#{$lumx-base-prefix}-switch--theme-dark.#{$lumx-base-prefix}-switch--is-unchecked
148-
.#{$lumx-base-prefix}-switch__input-native:hover
149-
+ .#{$lumx-base-prefix}-switch__input-placeholder {
150-
.#{$lumx-base-prefix}-switch__input-background {
151-
@include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "light");
144+
&.#{$lumx-base-prefix}-switch--theme-dark.#{$lumx-base-prefix}-switch--is-unchecked
145+
.#{$lumx-base-prefix}-switch__input-native:hover
146+
+ .#{$lumx-base-prefix}-switch__input-placeholder {
147+
.#{$lumx-base-prefix}-switch__input-background {
148+
@include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "light");
149+
}
152150
}
153-
}
154151

155-
.#{$lumx-base-prefix}-switch--theme-light.#{$lumx-base-prefix}-switch--is-checked
156-
.#{$lumx-base-prefix}-switch__input-native:hover
157-
+ .#{$lumx-base-prefix}-switch__input-placeholder {
158-
.#{$lumx-base-prefix}-switch__input-background {
159-
@include lumx-state(
160-
lumx-base-const("state", "HOVER"),
161-
lumx-base-const("emphasis", "HIGH"),
162-
"primary",
163-
lumx-base-const("theme", "LIGHT")
164-
);
152+
&.#{$lumx-base-prefix}-switch--theme-light.#{$lumx-base-prefix}-switch--is-checked
153+
.#{$lumx-base-prefix}-switch__input-native:hover
154+
+ .#{$lumx-base-prefix}-switch__input-placeholder {
155+
.#{$lumx-base-prefix}-switch__input-background {
156+
@include lumx-state(
157+
lumx-base-const("state", "HOVER"),
158+
lumx-base-const("emphasis", "HIGH"),
159+
"primary",
160+
lumx-base-const("theme", "LIGHT")
161+
);
162+
}
165163
}
166-
}
167164

168-
.#{$lumx-base-prefix}-switch--theme-dark.#{$lumx-base-prefix}-switch--is-checked
169-
.#{$lumx-base-prefix}-switch__input-native:hover
170-
+ .#{$lumx-base-prefix}-switch__input-placeholder {
171-
.#{$lumx-base-prefix}-switch__input-background {
172-
@include lumx-state(
173-
lumx-base-const("state", "HOVER"),
174-
lumx-base-const("emphasis", "HIGH"),
175-
"primary",
176-
lumx-base-const("theme", "DARK")
177-
);
165+
&.#{$lumx-base-prefix}-switch--theme-dark.#{$lumx-base-prefix}-switch--is-checked
166+
.#{$lumx-base-prefix}-switch__input-native:hover
167+
+ .#{$lumx-base-prefix}-switch__input-placeholder {
168+
.#{$lumx-base-prefix}-switch__input-background {
169+
@include lumx-state(
170+
lumx-base-const("state", "HOVER"),
171+
lumx-base-const("emphasis", "HIGH"),
172+
"primary",
173+
lumx-base-const("theme", "DARK")
174+
);
175+
}
178176
}
179-
}
180177

181-
// Active state
182-
.#{$lumx-base-prefix}-switch--theme-light.#{$lumx-base-prefix}-switch--is-unchecked
183-
.#{$lumx-base-prefix}-switch__input-native:active
184-
+ .#{$lumx-base-prefix}-switch__input-placeholder {
185-
.#{$lumx-base-prefix}-switch__input-background {
186-
@include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "dark");
178+
// Active state
179+
&.#{$lumx-base-prefix}-switch--theme-light.#{$lumx-base-prefix}-switch--is-unchecked
180+
.#{$lumx-base-prefix}-switch__input-native:active
181+
+ .#{$lumx-base-prefix}-switch__input-placeholder {
182+
.#{$lumx-base-prefix}-switch__input-background {
183+
@include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "dark");
184+
}
187185
}
188-
}
189186

190-
.#{$lumx-base-prefix}-switch--theme-dark.#{$lumx-base-prefix}-switch--is-unchecked
191-
.#{$lumx-base-prefix}-switch__input-native:active
192-
+ .#{$lumx-base-prefix}-switch__input-placeholder {
193-
.#{$lumx-base-prefix}-switch__input-background {
194-
@include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "light");
187+
&.#{$lumx-base-prefix}-switch--theme-dark.#{$lumx-base-prefix}-switch--is-unchecked
188+
.#{$lumx-base-prefix}-switch__input-native:active
189+
+ .#{$lumx-base-prefix}-switch__input-placeholder {
190+
.#{$lumx-base-prefix}-switch__input-background {
191+
@include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "light");
192+
}
195193
}
196-
}
197194

198-
.#{$lumx-base-prefix}-switch--theme-light.#{$lumx-base-prefix}-switch--is-checked
199-
.#{$lumx-base-prefix}-switch__input-native:active
200-
+ .#{$lumx-base-prefix}-switch__input-placeholder {
201-
.#{$lumx-base-prefix}-switch__input-background {
202-
@include lumx-state(
203-
lumx-base-const("state", "ACTIVE"),
204-
lumx-base-const("emphasis", "HIGH"),
205-
"primary",
206-
lumx-base-const("theme", "LIGHT")
207-
);
195+
&.#{$lumx-base-prefix}-switch--theme-light.#{$lumx-base-prefix}-switch--is-checked
196+
.#{$lumx-base-prefix}-switch__input-native:active
197+
+ .#{$lumx-base-prefix}-switch__input-placeholder {
198+
.#{$lumx-base-prefix}-switch__input-background {
199+
@include lumx-state(
200+
lumx-base-const("state", "ACTIVE"),
201+
lumx-base-const("emphasis", "HIGH"),
202+
"primary",
203+
lumx-base-const("theme", "LIGHT")
204+
);
205+
}
208206
}
209-
}
210207

211-
.#{$lumx-base-prefix}-switch--theme-dark.#{$lumx-base-prefix}-switch--is-checked
212-
.#{$lumx-base-prefix}-switch__input-native:active
213-
+ .#{$lumx-base-prefix}-switch__input-placeholder {
214-
.#{$lumx-base-prefix}-switch__input-background {
215-
@include lumx-state(
216-
lumx-base-const("state", "ACTIVE"),
217-
lumx-base-const("emphasis", "HIGH"),
218-
"primary",
219-
lumx-base-const("theme", "DARK")
220-
);
208+
&.#{$lumx-base-prefix}-switch--theme-dark.#{$lumx-base-prefix}-switch--is-checked
209+
.#{$lumx-base-prefix}-switch__input-native:active
210+
+ .#{$lumx-base-prefix}-switch__input-placeholder {
211+
.#{$lumx-base-prefix}-switch__input-background {
212+
@include lumx-state(
213+
lumx-base-const("state", "ACTIVE"),
214+
lumx-base-const("emphasis", "HIGH"),
215+
"primary",
216+
lumx-base-const("theme", "DARK")
217+
);
218+
}
221219
}
222220
}
223221

packages/lumx-react/src/components/switch/Switch.stories.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Alignment, Switch, SwitchProps } from '@lumx/react';
22
import { withValueOnChange } from '@lumx/react/stories/decorators/withValueOnChange';
33
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
4+
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
45

56
export default {
67
title: 'LumX components/switch/Switch',
@@ -28,7 +29,16 @@ export const Default = {};
2829
* Switch disabled
2930
*/
3031
export const Disabled = {
31-
args: { isDisabled: true },
32+
decorators: [
33+
withCombinations({
34+
combinations: {
35+
rows: {
36+
disabled: { isDisabled: true },
37+
'aria-disabled': { 'aria-disabled': true },
38+
},
39+
},
40+
}),
41+
],
3242
};
3343

3444
/**

packages/lumx-react/src/components/switch/Switch.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,36 @@ describe(`<${Switch.displayName}>`, () => {
100100
});
101101
});
102102

103+
describe('Disabled state', () => {
104+
it('should be disabled with isDisabled', async () => {
105+
const onChange = jest.fn();
106+
const { switchWrapper, input } = setup({ isDisabled: true, onChange });
107+
108+
expect(switchWrapper).toHaveClass('lumx-switch--is-disabled');
109+
expect(input).toBeDisabled();
110+
expect(input).toHaveAttribute('readOnly');
111+
112+
// Should not trigger onChange.
113+
await userEvent.click(input);
114+
expect(onChange).not.toHaveBeenCalled();
115+
});
116+
117+
it('should be disabled with aria-disabled', async () => {
118+
const onChange = jest.fn();
119+
const { switchWrapper, input } = setup({ 'aria-disabled': true, onChange });
120+
121+
expect(switchWrapper).toHaveClass('lumx-switch--is-disabled');
122+
// Note: input is not disabled (so it can be focused) but it's readOnly.
123+
expect(input).not.toBeDisabled();
124+
expect(input).toHaveAttribute('aria-disabled', 'true');
125+
expect(input).toHaveAttribute('readOnly');
126+
127+
// Should not trigger onChange.
128+
await userEvent.click(input);
129+
expect(onChange).not.toHaveBeenCalled();
130+
});
131+
});
132+
103133
// Common tests suite.
104134
commonTestsSuiteRTL(setup, {
105135
baseClassName: CLASSNAME,

packages/lumx-react/src/components/switch/Switch.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import { useId } from '@lumx/react/hooks/useId';
1010
import { useTheme } from '@lumx/react/utils/theme/ThemeContext';
1111
import { forwardRef } from '@lumx/react/utils/react/forwardRef';
1212
import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps';
13+
import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled';
1314

1415
/**
1516
* Defines the props of the component.
1617
*/
17-
export interface SwitchProps extends GenericProps, HasTheme {
18+
export interface SwitchProps extends GenericProps, HasTheme, HasAriaDisabled {
1819
/** Helper text. */
1920
helper?: string;
2021
/** Whether it is checked or not. */
@@ -110,6 +111,7 @@ export const Switch = forwardRef<SwitchProps, HTMLDivElement>((props, ref) => {
110111
name={name}
111112
value={value}
112113
{...disabledStateProps}
114+
readOnly={inputProps.readOnly || isAnyDisabled}
113115
checked={isChecked}
114116
aria-checked={Boolean(isChecked)}
115117
onChange={handleChange}

packages/site-demo/content/product/components/switch/index.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ Switches are based on standard checkbox, implement the [WAI-ARIA `switch` patter
1010

1111
In contexts where there is no direct label attached to the checkbox, alternative label should be provided with either [`aria-labelledby`](https://w3c.github.io/aria/#aria-labelledby) (preferred) or [`aria-label`](https://w3c.github.io/aria/#aria-label) (only in last resort) attributes forwarded inside the `inputProps` property.
1212

13+
Switches can be disabled in two ways:
14+
- `disabled`/`isDisabled` which completely disables the switch. It's not focusable and pointer events (hover, click,
15+
etc.) are all disabled too. You'll have to find another way to indicate to users that the switch is here and why it
16+
is disabled.
17+
- `aria-disabled` only disable changes on the switch (read only) so it's still focusable and accessible. You'll have
18+
to setup either a `helper`, a tooltip or an aria description to explain to the user why it is disabled.
19+
1320
### Properties
1421

1522
<PropTable component="Switch" />

0 commit comments

Comments
 (0)