mirror of
https://github.com/material-components/material-web.git
synced 2026-01-09 07:21:09 +08:00
Merge 24da379ea657f20b24f2fcaaf25b8953b3555824 into 78676746041b39f014fe9568efbf5ce2e9d7d46a
This commit is contained in:
commit
531b420210
@ -51,6 +51,16 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
|
||||
*/
|
||||
@property({type: Boolean, reflect: true}) disabled = false;
|
||||
|
||||
/**
|
||||
* When true, allows disabled buttons to be focused.
|
||||
*
|
||||
* Add 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: 'always-focusable'})
|
||||
alwaysFocusable = false;
|
||||
|
||||
/**
|
||||
* The URL that the link button points to.
|
||||
*/
|
||||
@ -154,7 +164,8 @@ export abstract class Button extends buttonBaseClass implements FormSubmitter {
|
||||
return html`<button
|
||||
id="button"
|
||||
class="button"
|
||||
?disabled=${this.disabled}
|
||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||
aria-disabled=${this.disabled && this.alwaysFocusable ? 'true' : nothing}
|
||||
aria-label="${ariaLabel || nothing}"
|
||||
aria-haspopup="${ariaHasPopup || nothing}"
|
||||
aria-expanded="${ariaExpanded || nothing}">
|
||||
|
||||
71
button/internal/button_test.ts
Normal file
71
button/internal/button_test.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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 disabled and alwaysFocusable', async () => {
|
||||
const {button} = await setupTest();
|
||||
button.disabled = true;
|
||||
button.alwaysFocusable = 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 disabled and alwaysFocusable', async () => {
|
||||
const clickListener = jasmine.createSpy('clickListener');
|
||||
const {button} = await setupTest();
|
||||
button.disabled = true;
|
||||
button.alwaysFocusable = true;
|
||||
button.addEventListener('click', clickListener);
|
||||
await env.waitForStability();
|
||||
|
||||
button.click();
|
||||
expect(clickListener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -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. 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 disabled always-focusable>Paste</md-text-button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Elevated button
|
||||
|
||||
<!-- go/md-elevated-button -->
|
||||
@ -703,7 +725,6 @@ Token | Default value
|
||||
|
||||
## API
|
||||
|
||||
|
||||
### MdElevatedButton <code><md-elevated-button></code>
|
||||
|
||||
#### Properties
|
||||
@ -713,6 +734,7 @@ Token | Default value
|
||||
| Property | Attribute | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `disabled` | `disabled` | `boolean` | `false` | Whether or not the button is disabled. |
|
||||
| `alwaysFocusable` | `always-focusable` | `boolean` | `false` | Whether the button is still focusable when disabled. |
|
||||
| `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. |
|
||||
|
||||
@ -53,7 +53,8 @@
|
||||
color: var(--_pressed-icon-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:disabled,
|
||||
&[aria-disabled='true'] {
|
||||
color: var(--_disabled-icon-color);
|
||||
}
|
||||
|
||||
@ -77,17 +78,19 @@
|
||||
z-index: -1; // place behind content
|
||||
}
|
||||
|
||||
.icon-button:disabled::before {
|
||||
.icon-button:disabled::before,
|
||||
.icon-button[aria-disabled='true']::before {
|
||||
background-color: var(--_disabled-container-color);
|
||||
opacity: var(--_disabled-container-opacity);
|
||||
}
|
||||
|
||||
.icon-button:disabled .icon {
|
||||
.icon-button:disabled .icon,
|
||||
.icon-button[aria-disabled='true'] .icon {
|
||||
opacity: var(--_disabled-icon-opacity);
|
||||
}
|
||||
|
||||
.toggle-filled {
|
||||
&:not(:disabled) {
|
||||
&:not(:disabled, [aria-disabled='true']) {
|
||||
color: var(--_toggle-icon-color);
|
||||
|
||||
&:hover {
|
||||
@ -111,14 +114,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 {
|
||||
@ -142,7 +145,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
.selected:not(:disabled)::before {
|
||||
.selected:not(:disabled, [aria-disabled='true'])::before {
|
||||
background-color: var(--_selected-container-color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +55,8 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
|
||||
color: var(--_pressed-icon-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:disabled,
|
||||
&[aria-disabled='true'] {
|
||||
color: var(--_disabled-icon-color);
|
||||
}
|
||||
|
||||
@ -79,17 +80,19 @@ $_custom-property-prefix: 'filled-tonal-icon-button';
|
||||
z-index: -1; // place behind content
|
||||
}
|
||||
|
||||
.icon-button:disabled::before {
|
||||
.icon-button:disabled::before,
|
||||
.icon-button[aria-disabled='true']::before {
|
||||
background-color: var(--_disabled-container-color);
|
||||
opacity: var(--_disabled-container-opacity);
|
||||
}
|
||||
|
||||
.icon-button:disabled .icon {
|
||||
.icon-button:disabled .icon,
|
||||
.icon-button[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 {
|
||||
@ -113,14 +116,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 {
|
||||
@ -144,7 +147,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,7 +91,8 @@
|
||||
color: var(--_pressed-icon-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:disabled,
|
||||
&[aria-disabled='true'] {
|
||||
color: var(--_disabled-icon-color);
|
||||
}
|
||||
}
|
||||
@ -100,12 +101,13 @@
|
||||
border-radius: var(--_state-layer-shape);
|
||||
}
|
||||
|
||||
.standard:disabled .icon {
|
||||
.standard:disabled .icon,
|
||||
.standard[aria-disabled='true'] .icon {
|
||||
opacity: var(--_disabled-icon-opacity);
|
||||
}
|
||||
|
||||
.selected {
|
||||
&:not(:disabled) {
|
||||
&:not(:disabled, [aria-disabled='true']) {
|
||||
color: var(--_selected-icon-color);
|
||||
|
||||
&:hover {
|
||||
|
||||
@ -69,7 +69,8 @@
|
||||
color: var(--_pressed-icon-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:disabled,
|
||||
&[aria-disabled='true'] {
|
||||
color: var(--_disabled-icon-color);
|
||||
|
||||
&::before {
|
||||
@ -79,7 +80,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.outlined:disabled .icon {
|
||||
.outlined:disabled .icon,
|
||||
.outlined[aria-disabled='true'] .icon {
|
||||
opacity: var(--_disabled-icon-opacity);
|
||||
}
|
||||
|
||||
@ -103,7 +105,7 @@
|
||||
|
||||
// Selected icon button toggle.
|
||||
.selected {
|
||||
&:not(:disabled) {
|
||||
&:not(:disabled, [aria-disabled='true']) {
|
||||
color: var(--_selected-icon-color);
|
||||
|
||||
&:hover {
|
||||
@ -129,11 +131,12 @@
|
||||
);
|
||||
}
|
||||
|
||||
.selected:not(:disabled)::before {
|
||||
.selected:not(:disabled, [aria-disabled='true'])::before {
|
||||
background-color: var(--_selected-container-color);
|
||||
}
|
||||
|
||||
.selected:disabled::before {
|
||||
.selected:disabled::before,
|
||||
.selected[aria-disabled='true']::before {
|
||||
background-color: var(--_disabled-selected-container-color);
|
||||
opacity: var(--_disabled-selected-container-opacity);
|
||||
}
|
||||
@ -150,7 +153,8 @@
|
||||
border-width: var(--_outline-width);
|
||||
}
|
||||
|
||||
&:disabled::before {
|
||||
&:disabled::before,
|
||||
&[aria-disabled='true']::before {
|
||||
border-color: GrayText;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@ -58,6 +58,16 @@ export class IconButton extends iconButtonBaseClass implements FormSubmitter {
|
||||
*/
|
||||
@property({type: Boolean, reflect: true}) disabled = false;
|
||||
|
||||
/**
|
||||
* When true, allows disabled buttons to be focused.
|
||||
*
|
||||
* Add 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: 'always-focusable'})
|
||||
alwaysFocusable = false;
|
||||
|
||||
/**
|
||||
* Flips the icon if it is in an RTL context at startup.
|
||||
*/
|
||||
@ -156,7 +166,8 @@ export class IconButton extends iconButtonBaseClass implements FormSubmitter {
|
||||
aria-haspopup="${(!this.href && ariaHasPopup) || nothing}"
|
||||
aria-expanded="${(!this.href && ariaExpanded) || nothing}"
|
||||
aria-pressed="${ariaPressedValue}"
|
||||
?disabled="${!this.href && this.disabled}"
|
||||
aria-disabled=${!this.href && this.disabled && this.alwaysFocusable ? 'true' : nothing}
|
||||
?disabled=${!this.href && this.disabled && !this.alwaysFocusable}
|
||||
@click="${this.handleClick}">
|
||||
${this.renderFocusRing()}
|
||||
${this.renderRipple()}
|
||||
|
||||
71
iconbutton/internal/icon-button_test.ts
Normal file
71
iconbutton/internal/icon-button_test.ts
Normal file
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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 {IconButtonHarness} from '../harness.js';
|
||||
|
||||
import {IconButton} from './icon-button.js';
|
||||
|
||||
@customElement('test-icon-button')
|
||||
class TestIconButton extends IconButton {}
|
||||
|
||||
describe('IconButton', () => {
|
||||
const env = new Environment();
|
||||
|
||||
async function setupTest() {
|
||||
const iconButton = new TestIconButton();
|
||||
env.render(html`${iconButton}`);
|
||||
await env.waitForStability();
|
||||
return {iconButton, harness: new IconButtonHarness(iconButton)};
|
||||
}
|
||||
|
||||
it('should not be focusable when disabled', async () => {
|
||||
const {iconButton} = await setupTest();
|
||||
iconButton.disabled = true;
|
||||
await env.waitForStability();
|
||||
|
||||
iconButton.focus();
|
||||
expect(document.activeElement).toEqual(document.body);
|
||||
});
|
||||
|
||||
it('should be focusable when disabled and alwaysFocusable', async () => {
|
||||
const {iconButton} = await setupTest();
|
||||
iconButton.disabled = true;
|
||||
iconButton.alwaysFocusable = true;
|
||||
await env.waitForStability();
|
||||
|
||||
iconButton.focus();
|
||||
expect(document.activeElement).toEqual(iconButton);
|
||||
});
|
||||
|
||||
it('should not be clickable when disabled', async () => {
|
||||
const clickListener = jasmine.createSpy('clickListener');
|
||||
const {iconButton} = await setupTest();
|
||||
iconButton.disabled = true;
|
||||
iconButton.addEventListener('click', clickListener);
|
||||
await env.waitForStability();
|
||||
|
||||
iconButton.click();
|
||||
expect(clickListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not be clickable when disabled and alwaysFocusable', async () => {
|
||||
const clickListener = jasmine.createSpy('clickListener');
|
||||
const {iconButton} = await setupTest();
|
||||
iconButton.disabled = true;
|
||||
iconButton.alwaysFocusable = true;
|
||||
iconButton.addEventListener('click', clickListener);
|
||||
await env.waitForStability();
|
||||
|
||||
iconButton.click();
|
||||
expect(clickListener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user