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:
co-authored by
Ilya Kreymer
parent
7d9ce2b952
commit
05adead57c
+158
-123
@@ -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"> 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"> 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: <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"> 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"> 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: <b>${this.sourceUrl}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Filename: <b>${this.collInfo.filename}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Total Size: <b>${prettyBytes(Number(this.collInfo.size || 0))}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Loading Mode: <b>${this.collInfo.onDemand ? "Download On-Demand" : "Fully Local"}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Collection Id: <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: <b>${this.collInfo.filename}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Total Size: <b>${prettyBytes(Number(this.collInfo.size || 0))}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Loading Mode: <b>${this.collInfo.onDemand ? "Download On-Demand" : "Fully Local"}</b>
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
Collection Id: <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> User Docs</span>
|
||||
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon><span> 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 };
|
||||
|
||||
Reference in New Issue
Block a user