Skip to content

Commit

Permalink
feat(iconbutton): add soft-disabled attribute for focusable disable…
Browse files Browse the repository at this point in the history
…d icon buttons

PiperOrigin-RevId: 651858380
  • Loading branch information
zelliott authored and copybara-github committed Jul 12, 2024
1 parent 48124ba commit 281c092
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 46 deletions.
1 change: 1 addition & 0 deletions iconbutton/demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
new Knob('icon', {ui: textInput(), defaultValue: ''}),
new Knob('selectedIcon', {ui: textInput(), defaultValue: ''}),
new Knob('softDisabled', {ui: boolInput(), defaultValue: false}),
],
);

Expand Down
35 changes: 25 additions & 10 deletions iconbutton/demo/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface StoryKnobs {
icon: string;
selectedIcon: string;
disabled: boolean;
softDisabled: boolean;
}

const styles = [
Expand All @@ -44,26 +45,35 @@ const styles = [
const buttons: MaterialStoryInit<StoryKnobs> = {
name: 'Icon button variants',
styles,
render({icon, disabled}) {
render({icon, disabled, softDisabled}) {
return html`
<div class="row md-typescale-body-medium">
<div class="column">
<p>Standard</p>
<md-icon-button aria-label="Open settings" ?disabled=${disabled}>
<md-icon-button
aria-label="Open settings"
?disabled=${disabled}
?soft-disabled=${softDisabled}>
<md-icon>${icon || 'settings'}</md-icon>
</md-icon-button>
</div>
<div class="column">
<p>Outlined</p>
<md-outlined-icon-button aria-label="Search" ?disabled=${disabled}>
<md-outlined-icon-button
aria-label="Search"
?disabled=${disabled}
?soft-disabled=${softDisabled}>
<md-icon>${icon || 'search'}</md-icon>
</md-outlined-icon-button>
</div>
<div class="column">
<p>Filled</p>
<md-filled-icon-button aria-label="Complete" ?disabled=${disabled}>
<md-filled-icon-button
aria-label="Complete"
?disabled=${disabled}
?soft-disabled=${softDisabled}>
<md-icon>${icon || 'done'}</md-icon>
</md-filled-icon-button>
</div>
Expand All @@ -72,7 +82,8 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
<p>Filled tonal</p>
<md-filled-tonal-icon-button
aria-label="Add new"
?disabled=${disabled}>
?disabled=${disabled}
?soft-disabled=${softDisabled}>
<md-icon>${icon || 'add'}</md-icon>
</md-filled-tonal-icon-button>
</div>
Expand All @@ -84,7 +95,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
const toggles: MaterialStoryInit<StoryKnobs> = {
name: 'Toggle icon buttons',
styles,
render({icon, selectedIcon, disabled}) {
render({icon, selectedIcon, disabled, softDisabled}) {
return html`
<div class="row">
<div class="column">
Expand All @@ -93,7 +104,8 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
aria-label="Show password"
aria-label-selected="Hide password"
toggle
?disabled=${disabled}>
?disabled=${disabled}
?soft-disabled=${softDisabled}>
<md-icon>${icon || 'visibility'}</md-icon>
<md-icon slot="selected">
${selectedIcon || 'visibility_off'}
Expand All @@ -107,7 +119,8 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
aria-label="Play"
aria-label-selected="Pause"
toggle
?disabled=${disabled}>
?disabled=${disabled}
?soft-disabled=${softDisabled}>
<md-icon>${icon || 'play_arrow'}</md-icon>
<md-icon slot="selected">${selectedIcon || 'pause'}</md-icon>
</md-outlined-icon-button>
Expand All @@ -119,7 +132,8 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
aria-label="Show more"
aria-label-selected="Show less"
toggle
?disabled=${disabled}>
?disabled=${disabled}
?soft-disabled=${softDisabled}>
<md-icon>${icon || 'expand_more'}</md-icon>
<md-icon slot="selected">${selectedIcon || 'expand_less'}</md-icon>
</md-filled-icon-button>
Expand All @@ -131,7 +145,8 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
aria-label="Open menu"
aria-label-selected="Close menu"
toggle
?disabled=${disabled}>
?disabled=${disabled}
?soft-disabled=${softDisabled}>
<md-icon>${icon || 'menu'}</md-icon>
<md-icon slot="selected">${selectedIcon || 'close'}</md-icon>
</md-filled-tonal-icon-button>
Expand Down
60 changes: 60 additions & 0 deletions iconbutton/icon-button_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,66 @@ describe('icon button tests', () => {
},
);

it('should not be focusable when disabled', async () => {
// Arrange
const {element} = await setUpTest('button');
element.disabled = true;
await element.updateComplete;

// Act
element.focus();

// Assert
expect(document.activeElement)
.withContext('disabled button should not be focused')
.not.toBe(element);
});

it('should be focusable when soft-disabled', async () => {
// Arrange
const {element} = await setUpTest('button');
element.softDisabled = true;
await element.updateComplete;

// Act
element.focus();

// Assert
expect(document.activeElement)
.withContext('soft-disabled button should be focused')
.toBe(element);
});

it('should not be clickable when disabled', async () => {
// Arrange
const clickListener = jasmine.createSpy('clickListener');
const {element} = await setUpTest('button');
element.disabled = true;
element.addEventListener('click', clickListener);
await element.updateComplete;

// Act
element.click();

// Assert
expect(clickListener).not.toHaveBeenCalled();
});

it('should not be clickable when soft-disabled', async () => {
// Arrange
const clickListener = jasmine.createSpy('clickListener');
const {element} = await setUpTest('button');
element.softDisabled = true;
element.addEventListener('click', clickListener);
await element.updateComplete;

// Act
element.click();

// Assert
expect(clickListener).not.toHaveBeenCalled();
});

it(
'setting `ariaLabel` updates the aria-label attribute on the native ' +
'button element',
Expand Down
14 changes: 7 additions & 7 deletions iconbutton/internal/_filled-icon-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
color: var(--_pressed-icon-color);
}

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

Expand All @@ -77,17 +77,17 @@
z-index: -1; // place behind content
}

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

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

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

&:hover {
Expand All @@ -111,14 +111,14 @@
);
}

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

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

&:hover {
Expand All @@ -142,7 +142,7 @@
);
}

.selected:not(:disabled)::before {
.selected:not(:disabled, [aria-disabled='true'])::before {
background-color: var(--_selected-container-color);
}
}
16 changes: 7 additions & 9 deletions iconbutton/internal/_filled-tonal-icon-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
@use '../../tokens';
// go/keep-sorted end

$_custom-property-prefix: 'filled-tonal-icon-button';

@mixin theme($tokens) {
$supported-tokens: tokens.$md-comp-filled-tonal-icon-button-supported-tokens;
@each $token, $value in $tokens {
Expand Down Expand Up @@ -55,7 +53,7 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
color: var(--_pressed-icon-color);
}

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

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

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

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

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

&:hover {
Expand All @@ -113,14 +111,14 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
);
}

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

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

&:hover {
Expand All @@ -144,7 +142,7 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
);
}

.selected:not(:disabled)::before {
.selected:not(:disabled, [aria-disabled='true'])::before {
background-color: var(--_selected-container-color);
}
}
6 changes: 3 additions & 3 deletions iconbutton/internal/_icon-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
color: var(--_pressed-icon-color);
}

&:disabled {
&:is(:disabled, [aria-disabled='true']) {
color: var(--_disabled-icon-color);
}
}
Expand All @@ -100,12 +100,12 @@
border-radius: var(--_state-layer-shape);
}

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

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

&:hover {
Expand Down
14 changes: 7 additions & 7 deletions iconbutton/internal/_outlined-icon-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
color: var(--_pressed-icon-color);
}

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

&::before {
Expand All @@ -79,7 +79,7 @@
}
}

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

Expand All @@ -103,7 +103,7 @@

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

&:hover {
Expand All @@ -129,17 +129,17 @@
);
}

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

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

@media (forced-colors: active) {
:host([disabled]) {
:host(:is([disabled], [soft-disabled])) {
--_disabled-outline-opacity: 1;
}

Expand All @@ -150,7 +150,7 @@
border-width: var(--_outline-width);
}

&:disabled::before {
&:is(:disabled, [aria-disabled='true'])::before {
border-color: GrayText;
opacity: 1;
}
Expand Down
Loading

0 comments on commit 281c092

Please sign in to comment.