Chris@0
|
1 /**
|
Chris@0
|
2 * @file
|
Chris@0
|
3 * Adds an HTML element and method to trigger audio UAs to read system messages.
|
Chris@0
|
4 *
|
Chris@0
|
5 * Use {@link Drupal.announce} to indicate to screen reader users that an
|
Chris@0
|
6 * element on the page has changed state. For instance, if clicking a link
|
Chris@0
|
7 * loads 10 more items into a list, one might announce the change like this.
|
Chris@0
|
8 *
|
Chris@0
|
9 * @example
|
Chris@0
|
10 * $('#search-list')
|
Chris@0
|
11 * .on('itemInsert', function (event, data) {
|
Chris@0
|
12 * // Insert the new items.
|
Chris@0
|
13 * $(data.container.el).append(data.items.el);
|
Chris@0
|
14 * // Announce the change to the page contents.
|
Chris@0
|
15 * Drupal.announce(Drupal.t('@count items added to @container',
|
Chris@0
|
16 * {'@count': data.items.length, '@container': data.container.title}
|
Chris@0
|
17 * ));
|
Chris@0
|
18 * });
|
Chris@0
|
19 */
|
Chris@0
|
20
|
Chris@17
|
21 (function(Drupal, debounce) {
|
Chris@0
|
22 let liveElement;
|
Chris@0
|
23 const announcements = [];
|
Chris@0
|
24
|
Chris@0
|
25 /**
|
Chris@0
|
26 * Builds a div element with the aria-live attribute and add it to the DOM.
|
Chris@0
|
27 *
|
Chris@0
|
28 * @type {Drupal~behavior}
|
Chris@0
|
29 *
|
Chris@0
|
30 * @prop {Drupal~behaviorAttach} attach
|
Chris@17
|
31 * Attaches the behavior for drupalAnnounce.
|
Chris@0
|
32 */
|
Chris@0
|
33 Drupal.behaviors.drupalAnnounce = {
|
Chris@0
|
34 attach(context) {
|
Chris@0
|
35 // Create only one aria-live element.
|
Chris@0
|
36 if (!liveElement) {
|
Chris@0
|
37 liveElement = document.createElement('div');
|
Chris@0
|
38 liveElement.id = 'drupal-live-announce';
|
Chris@0
|
39 liveElement.className = 'visually-hidden';
|
Chris@0
|
40 liveElement.setAttribute('aria-live', 'polite');
|
Chris@0
|
41 liveElement.setAttribute('aria-busy', 'false');
|
Chris@0
|
42 document.body.appendChild(liveElement);
|
Chris@0
|
43 }
|
Chris@0
|
44 },
|
Chris@0
|
45 };
|
Chris@0
|
46
|
Chris@0
|
47 /**
|
Chris@0
|
48 * Concatenates announcements to a single string; appends to the live region.
|
Chris@0
|
49 */
|
Chris@0
|
50 function announce() {
|
Chris@0
|
51 const text = [];
|
Chris@0
|
52 let priority = 'polite';
|
Chris@0
|
53 let announcement;
|
Chris@0
|
54
|
Chris@0
|
55 // Create an array of announcement strings to be joined and appended to the
|
Chris@0
|
56 // aria live region.
|
Chris@0
|
57 const il = announcements.length;
|
Chris@0
|
58 for (let i = 0; i < il; i++) {
|
Chris@0
|
59 announcement = announcements.pop();
|
Chris@0
|
60 text.unshift(announcement.text);
|
Chris@0
|
61 // If any of the announcements has a priority of assertive then the group
|
Chris@0
|
62 // of joined announcements will have this priority.
|
Chris@0
|
63 if (announcement.priority === 'assertive') {
|
Chris@0
|
64 priority = 'assertive';
|
Chris@0
|
65 }
|
Chris@0
|
66 }
|
Chris@0
|
67
|
Chris@0
|
68 if (text.length) {
|
Chris@0
|
69 // Clear the liveElement so that repeated strings will be read.
|
Chris@0
|
70 liveElement.innerHTML = '';
|
Chris@0
|
71 // Set the busy state to true until the node changes are complete.
|
Chris@0
|
72 liveElement.setAttribute('aria-busy', 'true');
|
Chris@0
|
73 // Set the priority to assertive, or default to polite.
|
Chris@0
|
74 liveElement.setAttribute('aria-live', priority);
|
Chris@0
|
75 // Print the text to the live region. Text should be run through
|
Chris@0
|
76 // Drupal.t() before being passed to Drupal.announce().
|
Chris@0
|
77 liveElement.innerHTML = text.join('\n');
|
Chris@0
|
78 // The live text area is updated. Allow the AT to announce the text.
|
Chris@0
|
79 liveElement.setAttribute('aria-busy', 'false');
|
Chris@0
|
80 }
|
Chris@0
|
81 }
|
Chris@0
|
82
|
Chris@0
|
83 /**
|
Chris@0
|
84 * Triggers audio UAs to read the supplied text.
|
Chris@0
|
85 *
|
Chris@0
|
86 * The aria-live region will only read the text that currently populates its
|
Chris@0
|
87 * text node. Replacing text quickly in rapid calls to announce results in
|
Chris@0
|
88 * only the text from the most recent call to {@link Drupal.announce} being
|
Chris@0
|
89 * read. By wrapping the call to announce in a debounce function, we allow for
|
Chris@0
|
90 * time for multiple calls to {@link Drupal.announce} to queue up their
|
Chris@0
|
91 * messages. These messages are then joined and append to the aria-live region
|
Chris@0
|
92 * as one text node.
|
Chris@0
|
93 *
|
Chris@0
|
94 * @param {string} text
|
Chris@0
|
95 * A string to be read by the UA.
|
Chris@0
|
96 * @param {string} [priority='polite']
|
Chris@0
|
97 * A string to indicate the priority of the message. Can be either
|
Chris@0
|
98 * 'polite' or 'assertive'.
|
Chris@0
|
99 *
|
Chris@0
|
100 * @return {function}
|
Chris@0
|
101 * The return of the call to debounce.
|
Chris@0
|
102 *
|
Chris@0
|
103 * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops
|
Chris@0
|
104 */
|
Chris@17
|
105 Drupal.announce = function(text, priority) {
|
Chris@0
|
106 // Save the text and priority into a closure variable. Multiple simultaneous
|
Chris@0
|
107 // announcements will be concatenated and read in sequence.
|
Chris@0
|
108 announcements.push({
|
Chris@0
|
109 text,
|
Chris@0
|
110 priority,
|
Chris@0
|
111 });
|
Chris@0
|
112 // Immediately invoke the function that debounce returns. 200 ms is right at
|
Chris@0
|
113 // the cusp where humans notice a pause, so we will wait
|
Chris@0
|
114 // at most this much time before the set of queued announcements is read.
|
Chris@17
|
115 return debounce(announce, 200)();
|
Chris@0
|
116 };
|
Chris@17
|
117 })(Drupal, Drupal.debounce);
|