Skip to content

Commit 281c092

Browse files
zelliottcopybara-github
authored andcommitted
feat(iconbutton): add soft-disabled attribute for focusable disabled icon buttons
PiperOrigin-RevId: 651858380
1 parent 48124ba commit 281c092

9 files changed

+160
-46
lines changed

iconbutton/demo/demo.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
2323
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
2424
new Knob('icon', {ui: textInput(), defaultValue: ''}),
2525
new Knob('selectedIcon', {ui: textInput(), defaultValue: ''}),
26+
new Knob('softDisabled', {ui: boolInput(), defaultValue: false}),
2627
],
2728
);
2829

iconbutton/demo/stories.ts

+25-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface StoryKnobs {
1919
icon: string;
2020
selectedIcon: string;
2121
disabled: boolean;
22+
softDisabled: boolean;
2223
}
2324

2425
const styles = [
@@ -44,26 +45,35 @@ const styles = [
4445
const buttons: MaterialStoryInit<StoryKnobs> = {
4546
name: 'Icon button variants',
4647
styles,
47-
render({icon, disabled}) {
48+
render({icon, disabled, softDisabled}) {
4849
return html`
4950
<div class="row md-typescale-body-medium">
5051
<div class="column">
5152
<p>Standard</p>
52-
<md-icon-button aria-label="Open settings" ?disabled=${disabled}>
53+
<md-icon-button
54+
aria-label="Open settings"
55+
?disabled=${disabled}
56+
?soft-disabled=${softDisabled}>
5357
<md-icon>${icon || 'settings'}</md-icon>
5458
</md-icon-button>
5559
</div>
5660
5761
<div class="column">
5862
<p>Outlined</p>
59-
<md-outlined-icon-button aria-label="Search" ?disabled=${disabled}>
63+
<md-outlined-icon-button
64+
aria-label="Search"
65+
?disabled=${disabled}
66+
?soft-disabled=${softDisabled}>
6067
<md-icon>${icon || 'search'}</md-icon>
6168
</md-outlined-icon-button>
6269
</div>
6370
6471
<div class="column">
6572
<p>Filled</p>
66-
<md-filled-icon-button aria-label="Complete" ?disabled=${disabled}>
73+
<md-filled-icon-button
74+
aria-label="Complete"
75+
?disabled=${disabled}
76+
?soft-disabled=${softDisabled}>
6777
<md-icon>${icon || 'done'}</md-icon>
6878
</md-filled-icon-button>
6979
</div>
@@ -72,7 +82,8 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
7282
<p>Filled tonal</p>
7383
<md-filled-tonal-icon-button
7484
aria-label="Add new"
75-
?disabled=${disabled}>
85+
?disabled=${disabled}
86+
?soft-disabled=${softDisabled}>
7687
<md-icon>${icon || 'add'}</md-icon>
7788
</md-filled-tonal-icon-button>
7889
</div>
@@ -84,7 +95,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
8495
const toggles: MaterialStoryInit<StoryKnobs> = {
8596
name: 'Toggle icon buttons',
8697
styles,
87-
render({icon, selectedIcon, disabled}) {
98+
render({icon, selectedIcon, disabled, softDisabled}) {
8899
return html`
89100
<div class="row">
90101
<div class="column">
@@ -93,7 +104,8 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
93104
aria-label="Show password"
94105
aria-label-selected="Hide password"
95106
toggle
96-
?disabled=${disabled}>
107+
?disabled=${disabled}
108+
?soft-disabled=${softDisabled}>
97109
<md-icon>${icon || 'visibility'}</md-icon>
98110
<md-icon slot="selected">
99111
${selectedIcon || 'visibility_off'}
@@ -107,7 +119,8 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
107119
aria-label="Play"
108120
aria-label-selected="Pause"
109121
toggle
110-
?disabled=${disabled}>
122+
?disabled=${disabled}
123+
?soft-disabled=${softDisabled}>
111124
<md-icon>${icon || 'play_arrow'}</md-icon>
112125
<md-icon slot="selected">${selectedIcon || 'pause'}</md-icon>
113126
</md-outlined-icon-button>
@@ -119,7 +132,8 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
119132
aria-label="Show more"
120133
aria-label-selected="Show less"
121134
toggle
122-
?disabled=${disabled}>
135+
?disabled=${disabled}
136+
?soft-disabled=${softDisabled}>
123137
<md-icon>${icon || 'expand_more'}</md-icon>
124138
<md-icon slot="selected">${selectedIcon || 'expand_less'}</md-icon>
125139
</md-filled-icon-button>
@@ -131,7 +145,8 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
131145
aria-label="Open menu"
132146
aria-label-selected="Close menu"
133147
toggle
134-
?disabled=${disabled}>
148+
?disabled=${disabled}
149+
?soft-disabled=${softDisabled}>
135150
<md-icon>${icon || 'menu'}</md-icon>
136151
<md-icon slot="selected">${selectedIcon || 'close'}</md-icon>
137152
</md-filled-tonal-icon-button>

iconbutton/icon-button_test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,66 @@ describe('icon button tests', () => {
6161
},
6262
);
6363

64+
it('should not be focusable when disabled', async () => {
65+
// Arrange
66+
const {element} = await setUpTest('button');
67+
element.disabled = true;
68+
await element.updateComplete;
69+
70+
// Act
71+
element.focus();
72+
73+
// Assert
74+
expect(document.activeElement)
75+
.withContext('disabled button should not be focused')
76+
.not.toBe(element);
77+
});
78+
79+
it('should be focusable when soft-disabled', async () => {
80+
// Arrange
81+
const {element} = await setUpTest('button');
82+
element.softDisabled = true;
83+
await element.updateComplete;
84+
85+
// Act
86+
element.focus();
87+
88+
// Assert
89+
expect(document.activeElement)
90+
.withContext('soft-disabled button should be focused')
91+
.toBe(element);
92+
});
93+
94+
it('should not be clickable when disabled', async () => {
95+
// Arrange
96+
const clickListener = jasmine.createSpy('clickListener');
97+
const {element} = await setUpTest('button');
98+
element.disabled = true;
99+
element.addEventListener('click', clickListener);
100+
await element.updateComplete;
101+
102+
// Act
103+
element.click();
104+
105+
// Assert
106+
expect(clickListener).not.toHaveBeenCalled();
107+
});
108+
109+
it('should not be clickable when soft-disabled', async () => {
110+
// Arrange
111+
const clickListener = jasmine.createSpy('clickListener');
112+
const {element} = await setUpTest('button');
113+
element.softDisabled = true;
114+
element.addEventListener('click', clickListener);
115+
await element.updateComplete;
116+
117+
// Act
118+
element.click();
119+
120+
// Assert
121+
expect(clickListener).not.toHaveBeenCalled();
122+
});
123+
64124
it(
65125
'setting `ariaLabel` updates the aria-label attribute on the native ' +
66126
'button element',

iconbutton/internal/_filled-icon-button.scss

+7-7
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
color: var(--_pressed-icon-color);
5454
}
5555

56-
&:disabled {
56+
&:is(:disabled, [aria-disabled='true']) {
5757
color: var(--_disabled-icon-color);
5858
}
5959

@@ -77,17 +77,17 @@
7777
z-index: -1; // place behind content
7878
}
7979

80-
.icon-button:disabled::before {
80+
.icon-button:is(:disabled, [aria-disabled='true'])::before {
8181
background-color: var(--_disabled-container-color);
8282
opacity: var(--_disabled-container-opacity);
8383
}
8484

85-
.icon-button:disabled .icon {
85+
.icon-button:is(:disabled, [aria-disabled='true']) .icon {
8686
opacity: var(--_disabled-icon-opacity);
8787
}
8888

8989
.toggle-filled {
90-
&:not(:disabled) {
90+
&:not(:disabled, [aria-disabled='true']) {
9191
color: var(--_toggle-icon-color);
9292

9393
&:hover {
@@ -111,14 +111,14 @@
111111
);
112112
}
113113

114-
.toggle-filled:not(:disabled)::before {
114+
.toggle-filled:not(:disabled, [aria-disabled='true'])::before {
115115
// Note: filled icon buttons have three container colors,
116116
// "container-color" for regular, then selected/unselected for toggle.
117117
background-color: var(--_unselected-container-color);
118118
}
119119

120120
.selected {
121-
&:not(:disabled) {
121+
&:not(:disabled, [aria-disabled='true']) {
122122
color: var(--_toggle-selected-icon-color);
123123

124124
&:hover {
@@ -142,7 +142,7 @@
142142
);
143143
}
144144

145-
.selected:not(:disabled)::before {
145+
.selected:not(:disabled, [aria-disabled='true'])::before {
146146
background-color: var(--_selected-container-color);
147147
}
148148
}

iconbutton/internal/_filled-tonal-icon-button.scss

+7-9
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
@use '../../tokens';
1313
// go/keep-sorted end
1414

15-
$_custom-property-prefix: 'filled-tonal-icon-button';
16-
1715
@mixin theme($tokens) {
1816
$supported-tokens: tokens.$md-comp-filled-tonal-icon-button-supported-tokens;
1917
@each $token, $value in $tokens {
@@ -55,7 +53,7 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
5553
color: var(--_pressed-icon-color);
5654
}
5755

58-
&:disabled {
56+
&:is(:disabled, [aria-disabled='true']) {
5957
color: var(--_disabled-icon-color);
6058
}
6159

@@ -79,17 +77,17 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
7977
z-index: -1; // place behind content
8078
}
8179

82-
.icon-button:disabled::before {
80+
.icon-button:is(:disabled, [aria-disabled='true'])::before {
8381
background-color: var(--_disabled-container-color);
8482
opacity: var(--_disabled-container-opacity);
8583
}
8684

87-
.icon-button:disabled .icon {
85+
.icon-button:is(:disabled, [aria-disabled='true']) .icon {
8886
opacity: var(--_disabled-icon-opacity);
8987
}
9088

9189
.toggle-filled-tonal {
92-
&:not(:disabled) {
90+
&:not(:disabled, [aria-disabled='true']) {
9391
color: var(--_toggle-icon-color);
9492

9593
&:hover {
@@ -113,14 +111,14 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
113111
);
114112
}
115113

116-
.toggle-filled-tonal:not(:disabled)::before {
114+
.toggle-filled-tonal:not(:disabled, [aria-disabled='true'])::before {
117115
// Note: filled tonal icon buttons have three container colors,
118116
// "container-color" for regular, then selected/unselected for toggle.
119117
background-color: var(--_unselected-container-color);
120118
}
121119

122120
.selected {
123-
&:not(:disabled) {
121+
&:not(:disabled, [aria-disabled='true']) {
124122
color: var(--_toggle-selected-icon-color);
125123

126124
&:hover {
@@ -144,7 +142,7 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
144142
);
145143
}
146144

147-
.selected:not(:disabled)::before {
145+
.selected:not(:disabled, [aria-disabled='true'])::before {
148146
background-color: var(--_selected-container-color);
149147
}
150148
}

iconbutton/internal/_icon-button.scss

+3-3
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
color: var(--_pressed-icon-color);
9292
}
9393

94-
&:disabled {
94+
&:is(:disabled, [aria-disabled='true']) {
9595
color: var(--_disabled-icon-color);
9696
}
9797
}
@@ -100,12 +100,12 @@
100100
border-radius: var(--_state-layer-shape);
101101
}
102102

103-
.standard:disabled .icon {
103+
.standard:is(:disabled, [aria-disabled='true']) {
104104
opacity: var(--_disabled-icon-opacity);
105105
}
106106

107107
.selected {
108-
&:not(:disabled) {
108+
&:not(:disabled, [aria-disabled='true']) {
109109
color: var(--_selected-icon-color);
110110

111111
&:hover {

iconbutton/internal/_outlined-icon-button.scss

+7-7
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
color: var(--_pressed-icon-color);
7070
}
7171

72-
&:disabled {
72+
&:is(:disabled, [aria-disabled='true']) {
7373
color: var(--_disabled-icon-color);
7474

7575
&::before {
@@ -79,7 +79,7 @@
7979
}
8080
}
8181

82-
.outlined:disabled .icon {
82+
.outlined:is(:disabled, [aria-disabled='true']) .icon {
8383
opacity: var(--_disabled-icon-opacity);
8484
}
8585

@@ -103,7 +103,7 @@
103103

104104
// Selected icon button toggle.
105105
.selected {
106-
&:not(:disabled) {
106+
&:not(:disabled, [aria-disabled='true']) {
107107
color: var(--_selected-icon-color);
108108

109109
&:hover {
@@ -129,17 +129,17 @@
129129
);
130130
}
131131

132-
.selected:not(:disabled)::before {
132+
.selected:not(:disabled, [aria-disabled='true'])::before {
133133
background-color: var(--_selected-container-color);
134134
}
135135

136-
.selected:disabled::before {
136+
.selected:is(:disabled, [aria-disabled='true'])::before {
137137
background-color: var(--_disabled-selected-container-color);
138138
opacity: var(--_disabled-selected-container-opacity);
139139
}
140140

141141
@media (forced-colors: active) {
142-
:host([disabled]) {
142+
:host(:is([disabled], [soft-disabled])) {
143143
--_disabled-outline-opacity: 1;
144144
}
145145

@@ -150,7 +150,7 @@
150150
border-width: var(--_outline-width);
151151
}
152152

153-
&:disabled::before {
153+
&:is(:disabled, [aria-disabled='true'])::before {
154154
border-color: GrayText;
155155
opacity: 1;
156156
}

0 commit comments

Comments
 (0)