Skip to content

Commit

Permalink
feat: add soft-disabled property to button and iconbutton
Browse files Browse the repository at this point in the history
Fixes #5672.

PiperOrigin-RevId: 651409230
  • Loading branch information
zelliott authored and copybara-github committed Jul 11, 2024
1 parent 7867674 commit f6c66aa
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 52 deletions.
6 changes: 4 additions & 2 deletions button/internal/_elevation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ $_md-sys-motion: tokens.md-sys-motion-values();
transition-timing-function: map.get($_md-sys-motion, 'emphasized-easing');
}

:host([disabled]) md-elevation {
:host([disabled]) md-elevation,
:host([soft-disabled]) md-elevation {
transition: none;
}

Expand Down Expand Up @@ -59,7 +60,8 @@ $_md-sys-motion: tokens.md-sys-motion-values();
);
}

:host([disabled]) md-elevation {
:host([disabled]) md-elevation,
:host([soft-disabled]) md-elevation {
@include elevation.theme(
(
'level': var(--_disabled-container-elevation),
Expand Down
3 changes: 2 additions & 1 deletion button/internal/_icon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
color: var(--_pressed-icon-color);
}

:host([disabled]) ::slotted([slot='icon']) {
:host([disabled]) ::slotted([slot='icon']),
:host([soft-disabled]) ::slotted([slot='icon']) {
color: var(--_disabled-icon-color);
opacity: var(--_disabled-icon-opacity);
}
Expand Down
9 changes: 6 additions & 3 deletions button/internal/_outlined-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,23 @@
border-color: var(--_pressed-outline-color);
}

:host([disabled]) .outline {
:host([disabled]) .outline,
:host([soft-disabled]) .outline {
border-color: var(--_disabled-outline-color);
opacity: var(--_disabled-outline-opacity);
}

@media (forced-colors: active) {
:host([disabled]) .background {
:host([disabled]) .background,
:host([soft-disabled]) .background {
// Only outlined buttons change their border when disabled to distinguish
// them from other buttons that add a border for increased visibility in
// HCM.
border-color: GrayText;
}

:host([disabled]) .outline {
:host([disabled]) .outline,
:host([soft-disabled]) .outline {
opacity: 1;
}
}
Expand Down
12 changes: 8 additions & 4 deletions button/internal/_shared.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
);
}

:host([disabled]) {
:host([disabled]),
:host([soft-disabled]) {
cursor: default;
pointer-events: none;
}
Expand Down Expand Up @@ -139,12 +140,14 @@
text-overflow: inherit;
}

:host([disabled]) .label {
:host([disabled]) .label,
:host([soft-disabled]) .label {
color: var(--_disabled-label-text-color);
opacity: var(--_disabled-label-text-opacity);
}

:host([disabled]) .background {
:host([disabled]) .background,
:host([soft-disabled]) .background {
background-color: var(--_disabled-container-color);
opacity: var(--_disabled-container-opacity);
}
Expand All @@ -157,7 +160,8 @@
border: 1px solid CanvasText;
}

:host([disabled]) {
:host([disabled]),
:host([soft-disabled]) {
--_disabled-icon-color: GrayText;
--_disabled-icon-opacity: 1;
--_disabled-container-opacity: 1;
Expand Down
38 changes: 34 additions & 4 deletions button/internal/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
*/
@property({type: Boolean, reflect: true}) disabled = false;

/**
* Whether or not the button is "soft-disabled" (disabled but still
* focusable).
*
* Use this when a button needs increased visibility when disabled. See
* https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls
* for more guidance on when this is needed.
*/
@property({type: Boolean, attribute: 'soft-disabled'}) softDisabled = false;

/**
* The URL that the link button points to.
*/
Expand Down Expand Up @@ -111,7 +121,7 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
constructor() {
super();
if (!isServer) {
this.addEventListener('click', this.handleActivationClick);
this.addEventListener('click', this.handleClick);
}
}

Expand All @@ -123,9 +133,19 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
this.buttonElement?.blur();
}

/**
* Link buttons cannot be disabled or soft-disabled.
*/
protected override willUpdate() {
if (this.href) {
this.disabled = false;
this.softDisabled = false;
}
}

protected override render() {
// Link buttons may not be disabled
const isDisabled = this.disabled && !this.href;
const isRippleDisabled = !this.href && (this.disabled || this.softDisabled);
const buttonOrLink = this.href ? this.renderLink() : this.renderButton();
// TODO(b/310046938): due to a limitation in focus ring/ripple, we can't use
// the same ID for different elements, so we change the ID instead.
Expand All @@ -137,7 +157,7 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
<md-ripple
part="ripple"
for=${buttonId}
?disabled="${isDisabled}"></md-ripple>
?disabled="${isRippleDisabled}"></md-ripple>
${buttonOrLink}
`;
}
Expand All @@ -155,6 +175,7 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
id="button"
class="button"
?disabled=${this.disabled}
aria-disabled=${this.softDisabled ? 'true' : nothing}
aria-label="${ariaLabel || nothing}"
aria-haspopup="${ariaHasPopup || nothing}"
aria-expanded="${ariaExpanded || nothing}">
Expand Down Expand Up @@ -190,7 +211,16 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
`;
}

private readonly handleActivationClick = (event: MouseEvent) => {
private readonly handleClick = (event: MouseEvent) => {
// If the button is soft-disabled, we need to explicitly prevent the click
// from propagating to other event listeners as well as prevent the default
// action.
if (!this.href && this.softDisabled) {
event.stopImmediatePropagation();
event.preventDefault();
return;
}

if (!isActivationClick(event) || !this.buttonElement) {
return;
}
Expand Down
69 changes: 69 additions & 0 deletions button/internal/button_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

// import 'jasmine'; (google3-only)

import {html} from 'lit';
import {customElement} from 'lit/decorators.js';

import {Environment} from '../../testing/environment.js';
import {ButtonHarness} from '../harness.js';

import {Button} from './button.js';

@customElement('test-button')
class TestButton extends Button {}

describe('Button', () => {
const env = new Environment();

async function setupTest() {
const button = new TestButton();
env.render(html`${button}`);
await env.waitForStability();
return {button, harness: new ButtonHarness(button)};
}

it('should not be focusable when disabled', async () => {
const {button} = await setupTest();
button.disabled = true;
await env.waitForStability();

button.focus();
expect(document.activeElement).toEqual(document.body);
});

it('should be focusable when soft-disabled', async () => {
const {button} = await setupTest();
button.softDisabled = true;
await env.waitForStability();

button.focus();
expect(document.activeElement).toEqual(button);
});

it('should not be clickable when disabled', async () => {
const clickListener = jasmine.createSpy('clickListener');
const {button} = await setupTest();
button.disabled = true;
button.addEventListener('click', clickListener);
await env.waitForStability();

button.click();
expect(clickListener).not.toHaveBeenCalled();
});

it('should not be clickable when soft-disabled', async () => {
const clickListener = jasmine.createSpy('clickListener');
const {button} = await setupTest();
button.softDisabled = true;
button.addEventListener('click', clickListener);
await env.waitForStability();

button.click();
expect(clickListener).not.toHaveBeenCalled();
});
});
24 changes: 23 additions & 1 deletion docs/components/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,28 @@ attribute to buttons whose labels need a more descriptive label.
<md-elevated-button aria-label="Add a new contact">Add</md-elevated-button>
```

### Focusable and disabled

By default, disabled buttons are not focusable with the keyboard, while
soft-disabled buttons are. Some use cases encourage focusability of disabled
toolbar items to increase their discoverability.

See the
[ARIA guidelines on focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls)<!-- {.external} -->
for guidance on when this is recommended.

```html
<div role="toolbar">
<md-text-button>Copy</md-text-button>
<md-text-button>Cut</md-text-button>
<!--
This button is disabled but kept focusable to improve its discoverability
in the toolbar.
-->
<md-text-button soft-disabled>Paste</md-text-button>
</div>
```

## Elevated button

<!-- go/md-elevated-button -->
Expand Down Expand Up @@ -703,7 +725,6 @@ Token | Default value

## API


### MdElevatedButton <code>&lt;md-elevated-button&gt;</code>

#### Properties
Expand All @@ -713,6 +734,7 @@ Token | Default value
| Property | Attribute | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `disabled` | `disabled` | `boolean` | `false` | Whether or not the button is disabled. |
| `softDisabled` | `soft-disabled` | `boolean` | `false` | Whether the button is "soft-disabled" (disabled but still focusable). |
| `href` | `href` | `string` | `''` | The URL that the link button points to. |
| `target` | `target` | `string` | `''` | Where to display the linked `href` URL for a link button. Common options include `_blank` to open in a new tab. |
| `trailingIcon` | `trailing-icon` | `boolean` | `false` | Whether to render the icon at the inline end of the label rather than the inline start.<br>_Note:_ Link buttons cannot have trailing icons. |
Expand Down
28 changes: 25 additions & 3 deletions docs/components/icon-button.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,28 @@ attribute to icon buttons whose labels need a more descriptive label.
</md-icon-button>
```

### Focusable and disabled

By default, disabled icon buttons are not focusable with the keyboard, while
soft-disabled icon buttons are. Some use cases encourage focusability of
disabled toolbar items to increase their discoverability.

See the
[ARIA guidelines on focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_disabled_controls)<!-- {.external} -->
for guidance on when this is recommended.

```html
<div role="toolbar">
<md-icon-button><md-icon>copy</md-icon></md-icon-button>
<md-icon-button><md-icon>cut</md-icon></md-icon-button>
<!--
This icon button is disabled but kept focusable to improve its
discoverability in the toolbar.
-->
<md-icon-button soft-disabled><md-icon>paste</md-icon></md-icon-button>
</div>
```

### Toggle

Add an `aria-label-selected` attribute to toggle buttons whose labels need a
Expand Down Expand Up @@ -319,7 +341,7 @@ Token | Default value
### Filled Icon Button tokens

Token | Default value
-------------------------------------------------- | ------------------------
-------------------------------------------------- | -------------
`--md-filled-icon-button-selected-container-color` | `--md-sys-color-primary`
`--md-filled-icon-button-container-shape` | `--md-sys-shape-corner-full`
`--md-filled-icon-button-container-width` | `40px`
Expand Down Expand Up @@ -391,7 +413,7 @@ Token | Default value
### Outlined Icon Button tokens

Token | Default value
-------------------------------------------- | ------------------------
-------------------------------------------- | ----------------------------
`--md-outlined-icon-button-outline-color` | `--md-sys-color-outline`
`--md-outlined-icon-button-outline-width` | `1px`
`--md-outlined-icon-button-container-shape` | `--md-sys-shape-corner-full`
Expand Down Expand Up @@ -428,7 +450,6 @@ Token | Default value

## API


### MdIconButton <code>&lt;md-icon-button&gt;</code>

#### Properties
Expand Down Expand Up @@ -472,6 +493,7 @@ Token | Default value
| Property | Attribute | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `disabled` | `disabled` | `boolean` | `false` | Disables the icon button and makes it non-interactive. |
| `softDisabled` | `soft-disabled` | `boolean` | `false` | "Soft-disables" the icon button (disabled but still focusable). |
| `flipIconInRtl` | `flip-icon-in-rtl` | `boolean` | `false` | Flips the icon if it is in an RTL context at startup. |
| `href` | `href` | `string` | `''` | Sets the underlying `HTMLAnchorElement`'s `href` resource attribute. |
| `target` | `target` | `string` | `''` | Sets the underlying `HTMLAnchorElement`'s `target` attribute. |
Expand Down
Loading

0 comments on commit f6c66aa

Please sign in to comment.