Skip to content

Commit 73b5881

Browse files
committed
feat(radio-button): add aria-disabled accessible disabled state
1 parent 1b2c444 commit 73b5881

File tree

6 files changed

+99
-29
lines changed

6 files changed

+99
-29
lines changed

CHANGELOG.md

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

1920
### Fixed
2021

packages/lumx-core/src/scss/components/radio-button/_index.scss

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,6 @@
7676

7777
// Disabled state
7878
.#{$lumx-base-prefix}-radio-button--is-disabled {
79-
.#{$lumx-base-prefix}-radio-button__input-native {
80-
pointer-events: none;
81-
}
82-
8379
.#{$lumx-base-prefix}-radio-button__input-placeholder {
8480
@include lumx-state-disabled-input;
8581
}
@@ -100,36 +96,38 @@
10096
}
10197

10298
// Hover state
103-
.#{$lumx-base-prefix}-radio-button--theme-light.#{$lumx-base-prefix}-radio-button--is-unchecked
104-
.#{$lumx-base-prefix}-radio-button__input-native:hover
105-
+ .#{$lumx-base-prefix}-radio-button__input-placeholder {
106-
.#{$lumx-base-prefix}-radio-button__input-background {
107-
@include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "dark");
99+
:not(.#{$lumx-base-prefix}-radio-button--is-disabled) {
100+
&.#{$lumx-base-prefix}-radio-button--theme-light.#{$lumx-base-prefix}-radio-button--is-unchecked
101+
.#{$lumx-base-prefix}-radio-button__input-native:hover
102+
+ .#{$lumx-base-prefix}-radio-button__input-placeholder {
103+
.#{$lumx-base-prefix}-radio-button__input-background {
104+
@include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "dark");
105+
}
108106
}
109-
}
110107

111-
.#{$lumx-base-prefix}-radio-button--theme-dark.#{$lumx-base-prefix}-radio-button--is-unchecked
112-
.#{$lumx-base-prefix}-radio-button__input-native:hover
113-
+ .#{$lumx-base-prefix}-radio-button__input-placeholder {
114-
.#{$lumx-base-prefix}-radio-button__input-background {
115-
@include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "light");
108+
&.#{$lumx-base-prefix}-radio-button--theme-dark.#{$lumx-base-prefix}-radio-button--is-unchecked
109+
.#{$lumx-base-prefix}-radio-button__input-native:hover
110+
+ .#{$lumx-base-prefix}-radio-button__input-placeholder {
111+
.#{$lumx-base-prefix}-radio-button__input-background {
112+
@include lumx-state(lumx-base-const("state", "HOVER"), lumx-base-const("emphasis", "LOW"), "light");
113+
}
116114
}
117-
}
118115

119-
// Active state
120-
.#{$lumx-base-prefix}-radio-button--theme-light.#{$lumx-base-prefix}-radio-button--is-unchecked
121-
.#{$lumx-base-prefix}-radio-button__input-native:active
122-
+ .#{$lumx-base-prefix}-radio-button__input-placeholder {
123-
.#{$lumx-base-prefix}-radio-button__input-background {
124-
@include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "dark");
116+
// Active state
117+
&.#{$lumx-base-prefix}-radio-button--theme-light.#{$lumx-base-prefix}-radio-button--is-unchecked
118+
.#{$lumx-base-prefix}-radio-button__input-native:active
119+
+ .#{$lumx-base-prefix}-radio-button__input-placeholder {
120+
.#{$lumx-base-prefix}-radio-button__input-background {
121+
@include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "dark");
122+
}
125123
}
126-
}
127124

128-
.#{$lumx-base-prefix}-radio-button--theme-dark.#{$lumx-base-prefix}-radio-button--is-unchecked
129-
.#{$lumx-base-prefix}-radio-button__input-native:active
130-
+ .#{$lumx-base-prefix}-radio-button__input-placeholder {
131-
.#{$lumx-base-prefix}-radio-button__input-background {
132-
@include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "light");
125+
&.#{$lumx-base-prefix}-radio-button--theme-dark.#{$lumx-base-prefix}-radio-button--is-unchecked
126+
.#{$lumx-base-prefix}-radio-button__input-native:active
127+
+ .#{$lumx-base-prefix}-radio-button__input-placeholder {
128+
.#{$lumx-base-prefix}-radio-button__input-background {
129+
@include lumx-state(lumx-base-const("state", "ACTIVE"), lumx-base-const("emphasis", "LOW"), "light");
130+
}
133131
}
134132
}
135133

packages/lumx-react/src/components/radio-button/RadioButton.stories.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { RadioButton } from '@lumx/react';
22
import { withValueOnChange } from '@lumx/react/stories/decorators/withValueOnChange';
33
import { loremIpsum } from '@lumx/react/stories/utils/lorem';
4+
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
5+
import uniqueId from 'lodash/uniqueId';
46

57
export default {
68
title: 'LumX components/radio-button/Radio button',
@@ -37,3 +39,33 @@ export const LabelAndHelper = {
3739
helper: loremIpsum('tiny'),
3840
},
3941
};
42+
43+
/**
44+
* All state combinations
45+
*/
46+
export const AllStates = {
47+
args: { ...LabelAndHelper.args, helper: 'Radio button helper' },
48+
decorators: [
49+
withCombinations({
50+
combinations: {
51+
rows: {
52+
Default: {},
53+
Checked: { isChecked: true },
54+
},
55+
cols: {
56+
Default: {},
57+
Disabled: { isDisabled: true },
58+
'ARIA Disabled': { 'aria-disabled': true },
59+
},
60+
},
61+
combinator(a, b) {
62+
return Object.assign(a, b, {
63+
// Injecting a unique name for each radio buttons to make sure they can be individually focused
64+
name: uniqueId('name'),
65+
// Disabling
66+
onChange: undefined,
67+
});
68+
},
69+
}),
70+
],
71+
};

packages/lumx-react/src/components/radio-button/RadioButton.test.tsx

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

103+
describe('Disabled state', () => {
104+
it('should be disabled with isDisabled', async () => {
105+
const onChange = jest.fn();
106+
const { radioButton, input } = setup({ isDisabled: true, onChange });
107+
108+
expect(radioButton).toHaveClass('lumx-radio-button--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 { radioButton, input } = setup({ 'aria-disabled': true, onChange });
120+
121+
expect(radioButton).toHaveClass('lumx-radio-button--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/radio-button/RadioButton.tsx

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

1314
/**
1415
* Defines the props of the component.
1516
*/
16-
export interface RadioButtonProps extends GenericProps, HasTheme {
17+
export interface RadioButtonProps extends GenericProps, HasTheme, HasAriaDisabled {
1718
/** Helper text. */
1819
helper?: string;
1920
/** Native input id property. */
@@ -111,6 +112,7 @@ export const RadioButton = forwardRef<RadioButtonProps, HTMLDivElement>((props,
111112
value={value}
112113
checked={isChecked}
113114
onChange={handleChange}
115+
readOnly={inputProps?.readOnly || isAnyDisabled}
114116
aria-describedby={helper ? `${inputId}-helper` : undefined}
115117
{...inputProps}
116118
/>

packages/site-demo/content/product/components/radio-button/index.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ Radio buttons with label are fully accessible by default. If a helper is provide
1010

1111
In contexts where there is no direct label attached to the radio button, 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+
Radio button can disabled in two ways:
14+
- `disabled`/`isDisabled` which completely disables the radio button. 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 radio button is here and
16+
why it is disabled.
17+
- `aria-disabled` only disable changes on the radio button (read only) so it's still focusable and accessible. You'll
18+
have 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="RadioButton" />

0 commit comments

Comments
 (0)