Accessibilty, first pass (#17)

* Placeholder color contrast @ WCAG 2.0 level AA

* Trailing whitespace removed.

* Remove redundant labels.

* Logo is a link in mobile, if not-embedded; otherwise, span. Fix link labels.

* HREF mandatory on A; menu behaves like a popup button.

* Hide icons from screen readers.

* About icon id keyboard-operarable and is labeled.

* Note about inaccessible modal.

* Missing closing tag.

* Fix nesting/indentation.

* Fix nesting/indentation; add missing closing tag.

* Trailing whitespace removed.

* Don't use NAV landmark for non-nav; add heading for page structure.

* Hide icon.

* Load archive form's buttons visibly receive keyboard focus.

* Trailing whitespace removed.

* "Hide" header button is keyboard-operable, and more-verbosely labeled for screenreader users.

* Don't use NAV landmark for non-nav; add heading for page structure.

* Sort order announced to screen reader users.

* Trailing whitespace removed.

* Delete button only a button when it can be pressed; announce in progress deletion.

* Trailing whitespace removed.

* Use a NAV landmark for replay controls.

* Remove "menu" role, to avoid having to implement arrow key support https://www.w3.org/WAI/GL/wiki/Using_ARIA_menus

* Hide icons.

* Label buttons.

* Links in disguise as buttons need HREF, and to respond to spacebar.

* Re-dub "main menu" to distinguish from replay menu

* Replay nav buttons visually get keyboard focus.

* Trailing whitespace removed.

* Hiding icons; button labels; links-as-buttons are operable.

* Use type=search for additional clarity.

* Add appropriate landmarks and a heading for structure and clarity.

* Tab selection is a nav, and is keyboard operable.

* Indentation; missing closing tag.

* Trailing whitespace removed.

* Title iframe.

* Not a navigation.

* Per discussion, rename tab to "URLs" (label only; route etc. the same while experimenting).

* Announce when expanded/collapsed.

* Remove "menu" to avoid having to implement expected keyboard support.

* Use correct heading level when in sidebar and when main.

* Trailing whitespace removed.

* Semantic markup of table; headings and captions; link-buttons are keyboard operable.

* Skip links for keyboard users.

* Title iframe; hide icon.

* Just a list, not an aside.

* Not an article.

* Remove trailing whitespace.

* Make each page in the list a heading.

* link-buttons are keyboard operable.

* Use semantic list for page list.

* Announce changes to search results.

* More wayfinding tweaks.

* Landmark and heading for replay.

* Dynamic page titles.

* Remove redundant labels from main landmarks.

* additional tweaks after rebase
- add 'role' to each of the tabs, dependent if sidebar or not
- add 'role=main' for replay
- add sr-only headers to each tab

* fix typos

* remaining tweaks:
- url-resources: fix sorter getting moved into notification
- url-resources: fix border for mobile view
- add screenreader labels to expand/hide buttons

* Show hover effect on focus.

* Wrap the whole sidebar is role=complementary, including the tab navigation.

* Make buttons keyboard operable; fix typos.

* Programmatically associate sidebar toggle button with sidebar.

* Per discusson, try calling this "URLs"

* Different heading levels for sidebar and main.

* Announce changes to search results.

* Correct dynamic page title, whe not in sidebar.

* Remove hyperlink focus decoration; reveal obscured focus indicator.

* Focus on replay, when replay changes.

* Make buttons keyboard operable.

* Move spacebar helper to misc instead of duplicating.

* Bump compiled assets.

* Use disabled attribute to match css; update label to describe new sidebar.

* Adjust replay focus styles.

* Adjust replay nav button focus, so visuals onclick in Chrome are less prominent.

* Put wordmark with logo on non-embed desktop, so that logo and wordmark make a single link.

Co-authored-by: Ilya Kreymer <ikreymer@gmail.com>
This commit is contained in:
Rebecca Lynn Cremona
2020-09-16 12:46:44 -07:00
committed by GitHub
co-authored by Ilya Kreymer
parent 7d9ce2b952
commit 05adead57c
15 changed files with 947 additions and 685 deletions
+158 -123
View File
@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'lit-element';
import { wrapCss, rwpLogo, IS_APP } from './misc';
import { wrapCss, rwpLogo, IS_APP, clickOnSpacebarPress } from './misc';
import { registerSW, initDBWorker } from './pageutils';
@@ -23,6 +23,7 @@ class App extends LitElement
this.pageParams = {};
this.inited = false;
this.navMenuShown = false;
registerSW(__SW_NAME__);
initDBWorker();
@@ -77,7 +78,7 @@ class App extends LitElement
.tagline {
margin-top: 1.0rem;
}
@media screen and (min-width: 840px) {
.menu-only {
display: none;
@@ -94,6 +95,7 @@ class App extends LitElement
.logo-text {
padding-left: 0px;
margin-left: 8px;
}
a.navbar-item.logo-text:hover {
@@ -103,7 +105,7 @@ class App extends LitElement
@media screen and (max-width: 840px) {
.wide-only {
display: none;
display: none !important;
}
}
@@ -116,133 +118,146 @@ class App extends LitElement
}
return html`
${!this.embed || this.embed === "full" ? html`
<nav class="navbar has-background-info" role="navigation" aria-label="main navigation">
<a href="#skip-main-target" @click=${this.skipMenu} class="skip-link">Skip main navigation</a>
<nav class="navbar has-background-info" aria-label="main">
<div class="navbar-brand">
${!this.embed ? html`
<a class="navbar-item wr-logo-item" title="ReplayWeb.page" href="/">
<fa-icon id="wrlogo" size="2.5rem" .svg=${rwpLogo}></fa-icon>
</a>` : html`
<a class="navbar-item wr-logo-item" title="ReplayWeb.page">
<fa-icon id="wrlogo" size="2.5rem" .svg=${rwpLogo}></fa-icon>
</a>
${!this.embed ? html `
<a href="/" class="navbar-item wr-logo-item" aria-labelledby="home">
<fa-icon id="wrlogo" size="2.5rem" .svg=${rwpLogo} aria-hidden="true"></fa-icon>
<span id="home" class="logo-text has-text-weight-bold is-size-5 has-allcaps wide-only">
<span class="has-text-primary">replay</span>
<span class="has-text-link">web.page</span>
<span class="is-sr-only">Home</span>
</span>
</a>`: html `
<span class="navbar-item wr-logo-item">
<fa-icon id="wrlogo" size="2.5rem" .svg=${rwpLogo} aria-hidden="true"></fa-icon>
</span>
`}
<a role="button" @click="${this.onNavMenu}"
class="navbar-burger burger ${this.navMenuShown ? 'is-active' : ''}" aria-label="menu" aria-expanded="false">
<a href="#" role="button" id="menu-button" @click="${this.onNavMenu}" @keyup="${clickOnSpacebarPress}"
class="navbar-burger burger ${this.navMenuShown ? 'is-active' : ''}" aria-label="main menu" aria-haspopup="true" aria-expanded="${this.navMenuShown}">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu ${this.navMenuShown ? 'is-active' : ''}">
<div class="navbar-start">
${!this.embed ? html`
<a class="navbar-item logo-text has-text-weight-bold is-size-5 has-allcaps" href="/">
<span class="has-text-primary">replay</span>
<span class="has-text-link">web.page</span>
</a>` : ``}
<div class="navbar-start">
${!this.embed ? html`
<a id="home" class="navbar-item logo-text has-text-weight-bold is-size-5 has-allcaps menu-only" href="/">
<span class="has-text-primary">replay</span>
<span class="has-text-link">web.page</span>
<span class="is-sr-only">Home</span>
</a>` : ``}
${IS_APP && !this.collInfo ? html`
<a class="navbar-item arrow-button" title="Go Back" @click="${(e) => window.history.back()}">
<fa-icon .svg="${fasArrowLeft}"></fa-icon><span class="menu-only">&nbsp;Go Back</span>
</a>
<a class="navbar-item arrow-button" title="Go Forward" @click="${(e) => window.history.forward()}">
<fa-icon .svg="${fasArrowRight}"></fa-icon><span class="menu-only">&nbsp;Go Forward</span>
</a>
` : ``}
${this.sourceUrl && this.collInfo ? html`
<div class="navbar-item has-dropdown is-hoverable">
<a class="info-menu navbar-link is-arrowless"><fa-icon size="1.2em" .svg="${fasInfoIcon}"></fa-icon>
<span class="menu-only">About this Archive</span>
</a>
<div class="navbar-dropdown">
<div class="navbar-item wide-only"><i>About this Archive</i></div>
<hr class="navbar-divider">
<div class="navbar-item">
Source:&nbsp;<b>${this.sourceUrl}</b>
${IS_APP && !this.collInfo ? html`
<a role="button" href="#" class="navbar-item arrow-button" title="Go Back" @click="${(e) => window.history.back()}" @keyup="${clickOnSpacebarPress}">
<fa-icon .svg="${fasArrowLeft}" aria-hidden="true"></fa-icon><span class="menu-only">&nbsp;Go Back</span>
</a>
<a role="button" href="#" class="navbar-item arrow-button" title="Go Forward" @click="${(e) => window.history.forward()}" @keyup="${clickOnSpacebarPress}">
<fa-icon .svg="${fasArrowRight}" aria-hidden="true"></fa-icon><span class="menu-only">&nbsp;Go Forward</span>
</a>
` : ``}
${this.sourceUrl && this.collInfo ? html`
<div class="navbar-item has-dropdown is-hoverable">
<span tabindex="0" class="info-menu navbar-link is-arrowless" aria-labelledby="about-this-archive">
<fa-icon size="1.2em" .svg="${fasInfoIcon}" aria-hidden="true"></fa-icon>
<span id="about-this-archive" class="menu-only">About this Archive</span>
</span>
<div class="navbar-dropdown">
<div class="navbar-item wide-only"><i>About this Archive</i></div>
<hr class="navbar-divider">
<div class="navbar-item">
Source:&nbsp;<b>${this.sourceUrl}</b>
</div>
<div class="navbar-item">
Filename:&nbsp;<b>${this.collInfo.filename}</b>
</div>
<div class="navbar-item">
Total Size:&nbsp;<b>${prettyBytes(Number(this.collInfo.size || 0))}</b>
</div>
<div class="navbar-item">
Loading Mode:&nbsp;<b>${this.collInfo.onDemand ? "Download On-Demand" : "Fully Local"}</b>
</div>
<div class="navbar-item">
Collection Id:&nbsp;<b>${this.collInfo.coll}</b>
</div>
<hr class="navbar-divider">
<div class="navbar-item">
ReplayWeb.page Version: ${__VERSION__}
</div>
</div>
</div>
<div class="navbar-item">
Filename:&nbsp;<b>${this.collInfo.filename}</b>
</div>
<div class="navbar-item">
Total Size:&nbsp;<b>${prettyBytes(Number(this.collInfo.size || 0))}</b>
</div>
<div class="navbar-item">
Loading Mode:&nbsp;<b>${this.collInfo.onDemand ? "Download On-Demand" : "Fully Local"}</b>
</div>
<div class="navbar-item">
Collection Id:&nbsp;<b>${this.collInfo.coll}</b>
</div>
<hr class="navbar-divider">
<div class="navbar-item">
ReplayWeb.page Version: ${__VERSION__}
</div>
</div>
` : ``}
</div>
` : ``}
</div>
${!this.embed ? html`
${!this.embed ? html`
<div class="navbar-end">
<a href="/docs" target="_blank" class="navbar-item">
<fa-icon .svg="${fasHelp}"></fa-icon><span>&nbsp;User Docs</span>
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon><span>&nbsp;User Docs</span>
</a>
<!--
-- NB: the Terms modal is currently inaccessible to people using keyboards or screen readers.
-- Should all the JS and infrastructure for accessible modals be added, or should Terms be a normal page?
-->
<a href="?terms" @click="${(e) => { e.preventDefault(); this.showTerms = true} }"class="navbar-item">Terms</a>
</div>` : html``}
</nav>
` : ''}
${this.sourceUrl ? html`
<wr-coll .loadInfo="${this.loadInfo}"
sourceUrl="${this.sourceUrl}"
embed="${this.embed}"
@replay-favicons=${this.onFavIcons}
@coll-loaded=${this.onCollLoaded}></wr-coll>
` : html`
<wr-coll-index>
${!IS_APP ? html`
<p slot="header" class="tagline is-size-5 has-text-centered">Explore and Replay Interactive Archived Webpages Directly in your Browser. <i><a target="_blank" href="./docs/examples">(See Examples)</a></i></p>
` : ``}
<wr-chooser slot="header" @load-start=${this.onStartLoad}></wr-chooser>
</wr-coll-index>
`}
${this.showTerms ? html`
<div class="modal is-active">
<div class="modal-background" @click="${(e) => this.showTerms = false}"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Terms and Privacy</p>
<button class="delete" aria-label="close" @click="${(e) => this.showTerms = false}"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div class="content">
<p>This site is a static browser-based application that loads web archive files provided by the user
and renders them for replay in the browser.</p>
<p>The site is operated by the <a href="https://webrecorder.net/">Webrecorder Project</a></p>
<p>See the <a target="_blank" href="./docs">Docs</a> for more info on how it works.</p>
<h3>Privacy</h3>
<p><b>No data is uploaded anywhere and no information is collected.</b></p>
<p>All content rendered stays directly in your browser. When loading an archive from Google Drive,
the site may ask for account authorization to download the specified file only.</p>
<h4>Disclaimer of Warranties</h4>
<p>The application may not always be available. No guarantees!</p>
<p>Some legalese:</p>
<p style="font-size: 0.8rem">DISCLAIMER OF SOFTWARE WARRANTY. WEBRECORDER SOFTWARE PROVIDES THIS SOFTWARE TO YOU "AS AVAILABLE"
AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE,
INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
</p>
<a class="button is-warning" href="#" @click="${(e) => this.showTerms = false}">Close</a>
</div>
</div>
</section>
</div>
</div>
` : ``} `
</nav><p id="skip-main-target" tabindex="-1" class="is-sr-only">Skipped</p>
` : ''}
${this.sourceUrl ? html`
<wr-coll .loadInfo="${this.loadInfo}"
sourceUrl="${this.sourceUrl}"
embed="${this.embed}"
@replay-favicons=${this.onFavIcons}
@coll-loaded=${this.onCollLoaded}></wr-coll>
` : html`
<wr-coll-index>
${!IS_APP ? html`
<p slot="header" class="tagline is-size-5 has-text-centered">Explore and Replay Interactive Archived Webpages Directly in your Browser. <i><a target="_blank" href="./docs/examples">(See Examples)</a></i></p>
` : ``}
<wr-chooser slot="header" @load-start=${this.onStartLoad}></wr-chooser>
</wr-coll-index>
`}
${this.showTerms ? html`
<div class="modal is-active">
<div class="modal-background" @click="${(e) => this.showTerms = false}"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Terms and Privacy</p>
<button class="delete" aria-label="close" @click="${(e) => this.showTerms = false}"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div class="content">
<p>This site is a static browser-based application that loads web archive files provided by the user
and renders them for replay in the browser.</p>
<p>The site is operated by the <a href="https://webrecorder.net/">Webrecorder Project</a></p>
<p>See the <a target="_blank" href="./docs">Docs</a> for more info on how it works.</p>
<h3>Privacy</h3>
<p><b>No data is uploaded anywhere and no information is collected.</b></p>
<p>All content rendered stays directly in your browser. When loading an archive from Google Drive,
the site may ask for account authorization to download the specified file only.</p>
<h4>Disclaimer of Warranties</h4>
<p>The application may not always be available. No guarantees!</p>
<p>Some legalese:</p>
<p style="font-size: 0.8rem">DISCLAIMER OF SOFTWARE WARRANTY. WEBRECORDER SOFTWARE PROVIDES THIS SOFTWARE TO YOU "AS AVAILABLE"
AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE,
INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
</p>
<a class="button is-warning" href="#" @click="${(e) => this.showTerms = false}">Close</a>
</div>
</div>
</section>
</div>
</div>
</div>
` : ``} `
}
firstUpdated() {
@@ -259,28 +274,48 @@ class App extends LitElement
}
onFavIcons(event) {
const head = document.querySelector('head');
const head = document.querySelector('head');
const oldLinks = document.querySelectorAll("link[rel*='icon']");
for (const link of oldLinks) {
head.removeChild(link);
}
for (const link of oldLinks) {
head.removeChild(link);
}
for (const icon of event.detail.icons) {
const link = document.createElement('link');
link.rel = icon.rel;
link.href = icon.href;
head.appendChild(link);
const link = document.createElement('link');
link.rel = icon.rel;
link.href = icon.href;
head.appendChild(link);
}
}
skipMenu(event){
// This is a workaround, since this app's routing doesn't permit normal
// following of in-page anchors.
event.preventDefault();
this.renderRoot.querySelector("#skip-main-target").focus()
}
onNavMenu(event) {
event.preventDefault();
event.stopPropagation();
this.navMenuShown = !this.navMenuShown;
if (this.navMenuShown) {
document.addEventListener("click", () => {
// Since this menu can be large and obscure significant page content,
// dismiss like a modal, returning keyboard focus to the
// menu button on dismissal.
document.addEventListener("click", (event) => {
event.preventDefault();
this.navMenuShown = false;
this.renderRoot.querySelector("#menu-button").focus();
}, {once: true});
document.addEventListener("keypress", (event) => {
if (event.key == "Escape") {
event.preventDefault();
this.navMenuShown = false;
this.renderRoot.querySelector("#menu-button").focus();
}
}, {once: true});
}
}
@@ -398,4 +433,4 @@ class App extends LitElement
customElements.define("replay-app-main", App);
export { App };
export { App };