Skip to content

Commit

Permalink
Alert Dialog Example: Use icon and button description to reflect save…
Browse files Browse the repository at this point in the history
… status instead of toast alert (pull #2112)

Resolves issue #756 with the following changes to the alert dialog example:
* Remove toast alert that appeared when Save is pressed; replace with visual indicator on button that save is successful.
* Use an off-screen alert to notify screen reader users of successful save.
* Use aria-disabled instead of disabled attribute so save and discard buttons can be focusable when they are disabled, enabling screen reader users to more easily discern the state of the experience.

Co-authored-by: Sarah Higley <sarah.higley@microsoft.com>
Co-authored-by: Matt King <a11yThinker@gmail.com>
  • Loading branch information
3 people committed Nov 15, 2021
1 parent 6d5c495 commit 0c74238
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 69 deletions.
58 changes: 37 additions & 21 deletions examples/dialog-modal/alertdialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,14 @@ <h1>Alert Dialog Example</h1>
<p>To use this example:</p>
<ul>
<li>
Activate the "discard" button to trigger a confirmation dialog.
Activate the "discard" button to trigger a confirmation dialog that has the <code>alertdialog</code> role.
<ul>
<li>Activating the "yes" button removes the contents of both the "Notes" text area and local storage of the notes.</li>
<li>Activating the "yes" button in the confirmation dialog removes the contents of both the "Notes" text area and local storage of the notes.</li>
<li>Activating the "no" button or pressing <kbd>escape</kbd> closes the dialog.</li>
<li>The "discard" button is disabled if the notes text area does not contain any text.</li>
</ul>
</li>
<li>
Activate the "save" button to trigger an alert when it saves the contents of the "Notes" text area to <a href="https://www.w3.org/TR/webstorage/#the-localstorage-attribute">local storage</a>.
<li>Activate the "save" button to trigger an alert when the contents of the "Notes" text area is saved to <a href="https://www.w3.org/TR/webstorage/#the-localstorage-attribute">local storage</a>.
<ul>
<li>A successful save triggers a short alert to notify the user that the notes have been saved.</li>
<li>The "save" button is disabled if the user's local storage value is the same as the "Notes" field.</li>
Expand All @@ -68,9 +67,17 @@ <h2 id="ex_label">Example</h2>
<div id="ex_alertdialog">
<label for="notes">Notes</label>
<textarea class="notes" name="notes" id="notes" rows="7">This is an example text box, which unsurprisingly contains text. The user is given the option to save this text - which triggers a basic alert - or to discard the text - which triggers an alert dialog that prompts the user for confirmation.</textarea>
<button type="button" aria-controls="notes" id="notes_save">Save</button>
<button type="button" aria-controls="notes" id="notes_discard" onclick="openAlertDialog('alert_dialog', this)">Discard</button>
<div role="alert" id="alert_toast" class="toast hidden">Nothing to discard.</div>
<div class="visually-hidden" id="notes_save_status" role="alert"></div>
<button type="button" id="notes_save">
Save
<svg class="icon spinner" viewBox="0 0 32 32" aria-hidden="true">
<path d="M16 32c-4.274 0-8.292-1.664-11.314-4.686s-4.686-7.040-4.686-11.314c0-3.026 0.849-5.973 2.456-8.522 1.563-2.478 3.771-4.48 6.386-5.791l1.344 2.682c-2.126 1.065-3.922 2.693-5.192 4.708-1.305 2.069-1.994 4.462-1.994 6.922 0 7.168 5.832 13 13 13s13-5.832 13-13c0-2.459-0.69-4.853-1.994-6.922-1.271-2.015-3.066-3.643-5.192-4.708l1.344-2.682c2.615 1.31 4.824 3.313 6.386 5.791 1.607 2.549 2.456 5.495 2.456 8.522 0 4.274-1.664 8.292-4.686 11.314s-7.040 4.686-11.314 4.686z"></path>
</svg>
<svg class="icon check" viewBox="0 0 32 32" aria-hidden="true">
<path d="M27 4l-15 15-7-7-5 5 12 12 20-20z"></path>
</svg>
</button>
<button type="button" data-textbox="notes" id="notes_discard" onclick="openAlertDialog('alert_dialog', this)">Discard</button>
<div class="dialog-backdrop no-scroll">
<div id="alert_dialog" role="alertdialog" aria-modal="true" aria-labelledby="dialog_label" aria-describedby="dialog_desc" class="hidden">
<h2 id="dialog_label" class="dialog_label">Confirmation</h2>
Expand All @@ -79,7 +86,7 @@ <h2 id="dialog_label" class="dialog_label">Confirmation</h2>
</div>
<div class="dialog_form_actions">
<button type="button" onclick="closeDialog(this)">No</button>
<button type="button" aria-controls="notes" id="notes_confirm" onclick="discardInput(this)">Yes</button>
<button type="button" id="notes_confirm" onclick="discardInput(this)">Yes</button>
</div>
</div>
</div>
Expand All @@ -88,14 +95,18 @@ <h2 id="dialog_label" class="dialog_label">Confirmation</h2>
</section>
<section>
<h2>Accessibility Features</h2>
<ol>
<li>The accessible label for the alert dialog is set to its heading ("Confirmation").</li>
<ul>
<li>The accessible name of the alert dialog is set to its heading ("Confirmation").</li>
<li>The dialog's prompt ("Are you sure...?") is referenced via <code>aria-describedby</code> to ensure that the user is immediately aware of the prompt.</li>
<li>
Focus is automatically set to the first focusable element inside the dialog, which is the "No" <code>button</code>.
This is the least destructive action, so focusing "No" helps prevent users from accidentally confirming the destructive "Discard" action, which cannot be undone.
</li>
</ol>
<li>
When the buttons are disabled, <code>aria-disabled</code> is used instead of the HTML <code>disabled</code> attribute so the buttons will remain in the page <kbd>Tab</kbd> sequence.
This makes it easier for screen reader users to discover the buttons and discern how the interface works.
</li>
</ul>
</section>
<section>
<h2 id="kbd_label">Keyboard Support</h2>
Expand Down Expand Up @@ -162,23 +173,23 @@ <h2 id="rps_label">Role, Property, State, and Tabindex Attributes</h2>
</tr>
<tr>
<td></td>
<th scope="row"><code>aria-labelledby=<q>IDREF</q></code></th>
<th scope="row"><code>aria-labelledby="ID_REFERENCE"</code></th>
<td><code>div</code></td>
<td>
Gives the alert dialog an accessible name by referring to the element that provides the alert dialog title.
</td>
</tr>
<tr>
<td></td>
<th scope="row"><code>aria-describedby=<q>IDREF</q></code></th>
<th scope="row"><code>aria-describedby="ID_REFERENCE"</code></th>
<td><code>div</code></td>
<td>
Gives the alert dialog an accessible description by referring to the alert dialog content that describes the primary message or purpose of the alert dialog.
</td>
</tr>
<tr>
<td></td>
<th scope="row"><code>aria-modal=<q>true</q></code></th>
<th scope="row"><code>aria-modal="true"</code></th>
<td><code>div</code></td>
<td>
Tells assistive technologies that the windows underneath the current alert dialog are not available for interaction (inert).
Expand All @@ -192,26 +203,31 @@ <h2 id="rps_label">Role, Property, State, and Tabindex Attributes</h2>
Identifies the element that serves as the alert notification.
</td>
</tr>
<tr>
<td></td>
<th scope="row"><code>aria-disabled="true"</code></th>
<td><code>button</code></td>
<td>Tells assistive technology users the button cannot be activated.</td>
</tr>
</tbody>
</table>
<h3>Notes on <code>aria-modal</code> and <code>aria-hidden</code></h3>
<ol>
<ul>
<li>
The <code>aria-modal</code> property was introduced in ARIA 1.1.
As a new property, screen reader users may experience varying degrees of support for it.
As a relatively new property, screen reader users may experience varying degrees of support for it.
</li>
<li>
Applying the <code>aria-modal</code> property to the <code>dialog</code> element
replaces the technique of using <code>aria-hidden</code> on the background for informing assistive technologies that content outside a dialog is inert.
Applying the <code>aria-modal</code> property to the <code>dialog</code> element replaces the technique of using <code>aria-hidden</code> on the background for informing assistive technologies that content outside a dialog is inert.
</li>
<li>
In legacy dialog implementations where <code>aria-hidden</code> is used to make content outside a dialog inert for assistive technology users, it is important that:
<ol>
<ul>
<li><code>aria-hidden</code> is set to <code>true</code> on each element containing a portion of the inert layer.</li>
<li>The dialog element is not a descendant of any element that has <code>aria-hidden</code> set to <code>true</code>.</li>
</ol>
</ul>
</li>
</ol>
</ul>
</section>
<section>
<h2 id="src_label">Javascript and CSS Source Code</h2>
Expand Down
56 changes: 42 additions & 14 deletions examples/dialog-modal/css/dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,48 @@
width: 33%;
}

.toast {
background-color: rgb(0 0 0 / 90%);
color: #fff;
padding: 1rem;
border: none;
border-radius: 0.25rem;
box-shadow: 0 3px 6px rgb(0 0 0 / 16%), 0 3px 6px rgb(0 0 0 / 23%);
position: fixed;
top: 1rem;
right: 1rem;
transform: translateY(-150%);
transition: transform 225ms cubic-bezier(0.4, 0, 0.2, 1);
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

#notes_save {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}

.toast.active {
transform: translateY(0);
#notes_save svg {
display: block;
width: 0.75rem;
}

#notes_save .icon {
display: none;
}

@keyframes rotate {
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
}

#notes_save.loading .spinner {
display: block;
animation: rotate 2s linear infinite;
}

#notes_save.saved .check {
display: block;
}
91 changes: 57 additions & 34 deletions examples/dialog-modal/js/alertdialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,53 @@ var aria = aria || {};
aria.Utils = aria.Utils || {};

aria.Utils.disableCtrl = function (ctrl) {
ctrl.setAttribute('disabled', true);
ctrl.setAttribute('aria-disabled', 'true');
};

aria.Utils.enableCtrl = function (ctrl) {
ctrl.removeAttribute('disabled');
ctrl.removeAttribute('aria-disabled');
};

aria.Utils.triggerAlert = function (alertEl, content) {
return new Promise(function (resolve, reject) {
try {
alertEl.textContent = content || null;
alertEl.classList.remove('hidden');
alertEl.addEventListener(
'transitionend',
function () {
if (!this.classList.contains('active')) {
this.classList.add('hidden');
}
},
true
);
setTimeout(function () {
alertEl.classList.add('active');
}, 1);
setTimeout(function () {
alertEl.classList.remove('active');
resolve();
}, 3000);
} catch (err) {
reject(err);
}
});
aria.Utils.setLoading = function (saveBtn, saveStatusView) {
saveBtn.classList.add('loading');
this.disableCtrl(saveBtn);

// use a timeout for the loading message
// if the saved state happens very quickly,
// we don't need to explicitly announce the intermediate loading state
const loadingTimeout = window.setTimeout(() => {
saveStatusView.textContent = 'Loading';
}, 200);

// set timeout for saved state, to mimic loading
const fakeLoadingTimeout = Math.random() * 2000;
window.setTimeout(() => {
saveBtn.classList.remove('loading');
saveBtn.classList.add('saved');

window.clearTimeout(loadingTimeout);
saveStatusView.textContent = 'Saved successfully';
}, fakeLoadingTimeout);
};

aria.Notes = function Notes(notesId, saveId, discardId, localStorageKey) {
aria.Notes = function Notes(
notesId,
saveId,
saveStatusId,
discardId,
localStorageKey
) {
this.notesInput = document.getElementById(notesId);
this.saveBtn = document.getElementById(saveId);
this.saveStatusView = document.getElementById(saveStatusId);
this.discardBtn = document.getElementById(discardId);
this.localStorageKey = localStorageKey || 'alertdialog-notes';
this.initialized = false;

Object.defineProperty(this, 'controls', {
get: function () {
return document.querySelectorAll(
'[aria-controls=' + this.notesInput.id + ']'
'[data-textbox=' + this.notesInput.id + ']'
);
},
});
Expand Down Expand Up @@ -91,14 +93,16 @@ aria.Notes = function Notes(notesId, saveId, discardId, localStorageKey) {
};

aria.Notes.prototype.save = function (val) {
if (this.alert && !this.isCurrent) {
aria.Utils.triggerAlert(this.alert, 'Saved');
const isDisabled = this.saveBtn.getAttribute('aria-disabled') === 'true';
if (isDisabled) {
return;
}
localStorage.setItem(
this.localStorageKey,
JSON.stringify(val || this.notesInput.value)
);
aria.Utils.disableCtrl(this.saveBtn);
aria.Utils.setLoading(this.saveBtn, this.saveStatusView);
};

aria.Notes.prototype.loadSaved = function () {
Expand All @@ -107,10 +111,19 @@ aria.Notes.prototype.loadSaved = function () {
}
};

aria.Notes.prototype.restoreSaveBtn = function () {
this.saveBtn.classList.remove('loading');
this.saveBtn.classList.remove('saved');
this.saveBtn.removeAttribute('aria-disabled');

this.saveStatusView.textContent = '';
};

aria.Notes.prototype.discard = function () {
localStorage.clear();
this.notesInput.value = '';
this.toggleControls();
this.restoreSaveBtn();
};

aria.Notes.prototype.disableControls = function () {
Expand All @@ -133,6 +146,7 @@ aria.Notes.prototype.toggleCurrent = function () {
if (!this.isCurrent) {
this.notesInput.classList.remove('can-save');
aria.Utils.enableCtrl(this.saveBtn);
this.restoreSaveBtn();
} else {
this.notesInput.classList.add('can-save');
aria.Utils.disableCtrl(this.saveBtn);
Expand Down Expand Up @@ -162,17 +176,26 @@ aria.Notes.prototype.init = function () {

/** initialization */
document.addEventListener('DOMContentLoaded', function initAlertDialog() {
var notes = new aria.Notes('notes', 'notes_save', 'notes_confirm');
notes.alert = document.getElementById('alert_toast');
var notes = new aria.Notes(
'notes',
'notes_save',
'notes_save_status',
'notes_confirm'
);

window.discardInput = function (closeBtn) {
notes.discard.call(notes);
closeDialog(closeBtn);
};

window.openAlertDialog = function (dialogId, triggerBtn, focusFirst) {
// do not proceed if the trigger button is disabled
if (triggerBtn.getAttribute('aria-disabled') === 'true') {
return;
}

var target = document.getElementById(
triggerBtn.getAttribute('aria-controls')
triggerBtn.getAttribute('data-textbox')
);
var dialog = document.getElementById(dialogId);
var desc = document.getElementById(dialog.getAttribute('aria-describedby'));
Expand Down
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ <h2 id="examples_by_props_label">Examples By Properties and States</h2>
<td><code>aria-disabled</code></td>
<td>
<ul>
<li><a href="dialog-modal/alertdialog.html">Alert Dialog</a></li>
<li><a href="menubar/menubar-editor.html">Editor Menubar</a> (<abbr title="High Contrast Support">HC</abbr>)</li>
<li><a href="toolbar/toolbar.html">Toolbar</a></li>
</ul>
Expand Down

0 comments on commit 0c74238

Please sign in to comment.