Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
feat(snackbar): Accessibility: Announce label text to screen readers (#…
Browse files Browse the repository at this point in the history
…4090)

Fixes #4063
  • Loading branch information
acdvorak authored Nov 17, 2018
1 parent 95c6482 commit ed2b65a
Show file tree
Hide file tree
Showing 20 changed files with 330 additions and 72 deletions.
2 changes: 2 additions & 0 deletions packages/mdc-snackbar/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class MDCSnackbarAdapter {
/** @param {string} className */
removeClass(className) {}

announce() {}

notifyOpening() {}
notifyOpened() {}

Expand Down
13 changes: 12 additions & 1 deletion packages/mdc-snackbar/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,32 @@ const strings = {
LABEL_SELECTOR: '.mdc-snackbar__label',
ACTION_BUTTON_SELECTOR: '.mdc-snackbar__action-button',
ACTION_ICON_SELECTOR: '.mdc-snackbar__action-icon',
LABEL_TEXT_ATTR: 'data-mdc-snackbar-label-text',

OPENING_EVENT: 'MDCSnackbar:opening',
OPENED_EVENT: 'MDCSnackbar:opened',
CLOSING_EVENT: 'MDCSnackbar:closing',
CLOSED_EVENT: 'MDCSnackbar:closed',

REASON_ACTION: 'action',
REASON_DISMISS: 'dismiss',

ARIA_LIVE_LABEL_TEXT_ATTR: 'data-mdc-snackbar-label-text',
};

const numbers = {
MIN_AUTO_DISMISS_TIMEOUT_MS: 4000,
MAX_AUTO_DISMISS_TIMEOUT_MS: 10000,
DEFAULT_AUTO_DISMISS_TIMEOUT_MS: 5000,

SNACKBAR_ANIMATION_OPEN_TIME_MS: 150,
SNACKBAR_ANIMATION_CLOSE_TIME_MS: 225,

/**
* Number of milliseconds to wait between temporarily clearing the label text
* in the DOM and subsequently restoring it. This is necessary to force IE 11
* to pick up the `aria-live` content change and announce it to the user.
*/
ARIA_LIVE_DELAY_MS: 1000,
};

export {cssClasses, strings, numbers};
2 changes: 2 additions & 0 deletions packages/mdc-snackbar/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class MDCSnackbarFoundation extends MDCFoundation {
return /** @type {!MDCSnackbarAdapter} */ ({
addClass: (/* className: string */) => {},
removeClass: (/* className: string */) => {},
announce: () => {},
notifyOpening: () => {},
notifyOpened: () => {},
notifyClosing: (/* reason: string */) => {},
Expand Down Expand Up @@ -99,6 +100,7 @@ class MDCSnackbarFoundation extends MDCFoundation {
this.adapter_.notifyOpening();
this.adapter_.removeClass(CLOSING);
this.adapter_.addClass(OPENING);
this.adapter_.announce();

// Wait a frame once display is no longer "none", to establish basis for animation
this.runNextAnimationFrame_(() => {
Expand Down
28 changes: 24 additions & 4 deletions packages/mdc-snackbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import {MDCComponent} from '@material/base/index';
import MDCSnackbarFoundation from './foundation';
import {strings} from './constants';
import * as util from './util';
import * as ponyfill from '@material/dom/ponyfill';

const {
Expand All @@ -42,15 +43,33 @@ class MDCSnackbar extends MDCComponent {
/** @type {!HTMLElement} */
this.surfaceEl_;

/** @type {!HTMLElement} */
this.labelEl_;

/** @type {!HTMLElement} */
this.actionButtonEl_;

/** @type {function(!HTMLElement, !HTMLElement=): void} */
this.announce_;

/** @private {!Function} */
this.handleKeyDown_;

/** @private {!Function} */
this.handleSurfaceClick_;
}

/**
* @param {function(): function(!HTMLElement, !HTMLElement=):void} announceFactory
*/
initialize(announceFactory = () => util.announce) {
this.announce_ = announceFactory();
}

initialSyncWithDOM() {
this.surfaceEl_ = /** @type {!HTMLElement} */ (this.root_.querySelector(SURFACE_SELECTOR));
this.labelEl_ = /** @type {!HTMLElement} */ (this.root_.querySelector(LABEL_SELECTOR));
this.actionButtonEl_ = /** @type {!HTMLElement} */ (this.root_.querySelector(ACTION_BUTTON_SELECTOR));

this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt);
this.handleSurfaceClick_ = (evt) => {
Expand Down Expand Up @@ -92,6 +111,7 @@ class MDCSnackbar extends MDCComponent {
return new MDCSnackbarFoundation({
addClass: (className) => this.root_.classList.add(className),
removeClass: (className) => this.root_.classList.remove(className),
announce: () => this.announce_(this.labelEl_),
notifyOpening: () => this.emit(OPENING_EVENT, {}),
notifyOpened: () => this.emit(OPENED_EVENT, {}),
notifyClosing: (reason) => this.emit(CLOSING_EVENT, reason ? {reason} : {}),
Expand Down Expand Up @@ -138,28 +158,28 @@ class MDCSnackbar extends MDCComponent {
* @return {string}
*/
get labelText() {
return this.root_.querySelector(LABEL_SELECTOR).textContent;
return this.labelEl_.textContent;
}

/**
* @param {string} labelText
*/
set labelText(labelText) {
this.root_.querySelector(LABEL_SELECTOR).textContent = labelText;
this.labelEl_.textContent = labelText;
}

/**
* @return {string}
*/
get actionButtonText() {
return this.root_.querySelector(ACTION_BUTTON_SELECTOR).textContent;
return this.actionButtonEl_.textContent;
}

/**
* @param {string} actionButtonText
*/
set actionButtonText(actionButtonText) {
this.root_.querySelector(ACTION_BUTTON_SELECTOR).textContent = actionButtonText;
this.actionButtonEl_.textContent = actionButtonText;
}

/**
Expand Down
90 changes: 90 additions & 0 deletions packages/mdc-snackbar/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @license
* Copyright 2018 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

import {numbers, strings} from './constants';

const {ARIA_LIVE_DELAY_MS} = numbers;
const {ARIA_LIVE_LABEL_TEXT_ATTR} = strings;

/**
* @param {!HTMLElement} ariaEl
* @param {!HTMLElement=} labelEl
*/
function announce(ariaEl, labelEl = ariaEl) {
const priority = ariaEl.getAttribute('aria-live');
const labelText = labelEl.textContent.trim(); // Ignore ` ` (see below)
if (!labelText) {
return;
}

// Temporarily disable `aria-live` to prevent JAWS+Firefox from announcing the message twice.
ariaEl.setAttribute('aria-live', 'off');

// Temporarily clear `textContent` to force a DOM mutation event that will be detected by screen readers.
// `aria-live` elements are only announced when the element's `textContent` *changes*, so snackbars
// sent to the browser in the initial HTML response won't be read unless we clear the element's `textContent` first.
// Similarly, displaying the same snackbar message twice in a row doesn't trigger a DOM mutation event,
// so screen readers won't announce the second message unless we first clear `textContent`.
//
// We have to clear the label text two different ways to make it work in all browsers and screen readers:
//
// 1. `textContent = ''` is required for IE11 + JAWS
// 2. `innerHTML = ' '` is required for Chrome + JAWS and NVDA
//
// All other browser/screen reader combinations support both methods.
//
// The wrapper `<span>` visually hides the space character so that it doesn't cause jank when added/removed.
// N.B.: Setting `position: absolute`, `opacity: 0`, or `height: 0` prevents Chrome from detecting the DOM change.
//
// This technique has been tested in:
//
// * JAWS 2019:
// - Chrome 70
// - Firefox 60 (ESR)
// - IE 11
// * NVDA 2018:
// - Chrome 70
// - Firefox 60 (ESR)
// - IE 11
labelEl.textContent = '';
labelEl.innerHTML = '<span style="display: inline-block; width: 0; height: 1px;">&nbsp;</span>';

// Prevent visual jank by temporarily displaying the label text in the ::before pseudo-element.
// CSS generated content is normally announced by screen readers
// (except in IE 11; see https://tink.uk/accessibility-support-for-css-generated-content/);
// however, `aria-live` is turned off, so this DOM update will be ignored by screen readers.
labelEl.setAttribute(ARIA_LIVE_LABEL_TEXT_ATTR, labelText);

setTimeout(() => {
// Allow screen readers to announce changes to the DOM again.
ariaEl.setAttribute('aria-live', priority);

// Remove the message from the ::before pseudo-element.
labelEl.removeAttribute(ARIA_LIVE_LABEL_TEXT_ATTR);

// Restore the original label text, which will be announced by screen readers.
labelEl.textContent = labelText;
}, ARIA_LIVE_DELAY_MS);
}

export {announce};
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@
<button class="test-snackbar-open-button test-font--redact-all" data-test-snackbar-id="test-snackbar--with-action" autofocus>Open snackbar with action</button>

<div class="mdc-snackbar mdc-snackbar--open"
id="test-snackbar--with-action"
role="status"
aria-live="polite"
aria-hidden="true">
id="test-snackbar--with-action">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__actions">
<button type="button" class="mdc-button mdc-snackbar__action-button">Retry</button>
<button class="mdc-icon-button mdc-snackbar__action-icon material-icons" title="Dismiss">close</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@

<div class="mdc-snackbar mdc-snackbar--open"
id="test-snackbar--no-action"
role="status"
aria-live="polite"
aria-hidden="true"
data-test-snackbar-timeout-ms="4000">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">Message sent.</div>
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">Message sent.</div>
<div class="mdc-snackbar__actions">
<button class="mdc-icon-button mdc-snackbar__action-icon material-icons" title="Dismiss">close</button>
</div>
Expand Down
9 changes: 4 additions & 5 deletions test/screenshot/spec/mdc-snackbar/classes/leading.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@
<button class="test-snackbar-open-button test-font--redact-all" data-test-snackbar-id="test-snackbar--leading" autofocus>Open leading snackbar</button>

<div class="mdc-snackbar mdc-snackbar--leading mdc-snackbar--open"
id="test-snackbar--leading"
role="status"
aria-live="polite"
aria-hidden="true">
id="test-snackbar--leading">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">Your photo has been archived.</div>
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">Your photo has been archived.</div>
<div class="mdc-snackbar__actions">
<button type="button" class="mdc-button mdc-snackbar__action-button">Undo</button>
<button class="mdc-icon-button mdc-snackbar__action-icon material-icons" title="Dismiss">close</button>
Expand Down
9 changes: 4 additions & 5 deletions test/screenshot/spec/mdc-snackbar/classes/stacked.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@
<button class="test-snackbar-open-button test-font--redact-all" data-test-snackbar-id="test-snackbar--stacked" autofocus>Open stacked snackbar</button>

<div class="mdc-snackbar mdc-snackbar--stacked mdc-snackbar--open"
id="test-snackbar--stacked"
role="status"
aria-live="polite"
aria-hidden="true">
id="test-snackbar--stacked">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">This item already has the label "travel". You can add a new label.</div>
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">This item already has the label "travel". You can add a new label.</div>
<div class="mdc-snackbar__actions">
<button type="button" class="mdc-button mdc-snackbar__action-button">Add a new label</button>
<button class="mdc-icon-button mdc-snackbar__action-icon material-icons" title="Dismiss">close</button>
Expand Down
7 changes: 3 additions & 4 deletions test/screenshot/spec/mdc-snackbar/classes/wide.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@

<div class="mdc-snackbar mdc-snackbar--wide mdc-snackbar--open"
id="test-snackbar--wide"
role="status"
aria-live="polite"
aria-hidden="true"
data-test-snackbar-timeout-ms="10000">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">
Connection timed out. Showing the latest locally saved version of this document.
Edits made while offline will not be visible to other users until network connectivity is restored.
</div>
Expand Down
9 changes: 4 additions & 5 deletions test/screenshot/spec/mdc-snackbar/mixins/elevation.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@
<button class="test-snackbar-open-button test-font--redact-all" data-test-snackbar-id="test-snackbar" autofocus>Open custom elevation snackbar</button>

<div class="mdc-snackbar mdc-snackbar--open custom-snackbar--elevation"
id="test-snackbar"
role="status"
aria-live="polite"
aria-hidden="true">
id="test-snackbar">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__actions">
<button type="button" class="mdc-button mdc-snackbar__action-button">Retry</button>
<button class="mdc-icon-button mdc-snackbar__action-icon material-icons" title="Dismiss">close</button>
Expand Down
9 changes: 4 additions & 5 deletions test/screenshot/spec/mdc-snackbar/mixins/fill-color.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@
<button class="test-snackbar-open-button test-font--redact-all" data-test-snackbar-id="test-snackbar" autofocus>Open custom fill-color snackbar</button>

<div class="mdc-snackbar mdc-snackbar--open custom-snackbar--fill-color"
id="test-snackbar"
role="status"
aria-live="polite"
aria-hidden="true">
id="test-snackbar">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__actions">
<button type="button" class="mdc-button mdc-snackbar__action-button">Retry</button>
<button class="mdc-icon-button mdc-snackbar__action-icon material-icons" title="Dismiss">close</button>
Expand Down
9 changes: 4 additions & 5 deletions test/screenshot/spec/mdc-snackbar/mixins/label-ink-color.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@
<button class="test-snackbar-open-button test-font--redact-all" data-test-snackbar-id="test-snackbar" autofocus>Open custom label-ink-color snackbar</button>

<div class="mdc-snackbar mdc-snackbar--open custom-snackbar--label-ink-color"
id="test-snackbar"
role="status"
aria-live="polite"
aria-hidden="true">
id="test-snackbar">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__actions">
<button type="button" class="mdc-button mdc-snackbar__action-button">Retry</button>
<button class="mdc-icon-button mdc-snackbar__action-icon material-icons" title="Dismiss">close</button>
Expand Down
9 changes: 4 additions & 5 deletions test/screenshot/spec/mdc-snackbar/mixins/max-width.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,11 @@
<button class="test-snackbar-open-button test-font--redact-all" data-test-snackbar-id="test-snackbar" autofocus>Open custom max-width snackbar</button>

<div class="mdc-snackbar mdc-snackbar--open custom-snackbar--max-width"
id="test-snackbar"
role="status"
aria-live="polite"
aria-hidden="true">
id="test-snackbar">
<div class="mdc-snackbar__surface">
<div class="mdc-snackbar__label">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__label"
role="status"
aria-live="polite">Can't send photo. Retry in 5 seconds.</div>
<div class="mdc-snackbar__actions">
<button type="button" class="mdc-button mdc-snackbar__action-button">Retry</button>
<button class="mdc-icon-button mdc-snackbar__action-icon material-icons" title="Dismiss">close</button>
Expand Down
Loading

0 comments on commit ed2b65a

Please sign in to comment.