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
+71
View File
@@ -3,6 +3,7 @@
/*$wr-green: #64e986;*/ /*$wr-green: #64e986;*/
$wr-green: #55be6f; $wr-green: #55be6f;
$wr-yellow: #ffd975; $wr-yellow: #ffd975;
$wr-dark-yellow: #ffc42e;
$wr-light-blue: #43daff; $wr-light-blue: #43daff;
$wr-med-blue: #27a0ff; $wr-med-blue: #27a0ff;
$wr-dark-blue: #4876ff; $wr-dark-blue: #4876ff;
@@ -22,6 +23,76 @@ $dark: $wr-dark-green;
$navbar-breakpoint: 840px; $navbar-breakpoint: 840px;
// Set placeholder contrast to ~4.5:1 for WCAG AA compliance
$input-placeholder-color: #757575;
$input-disabled-placeholder-color: #707070;
.input::placeholder, .textarea::placeholder, .select select::placeholder {
opacity: 1;
}
// focus styles for links to match
a:focus {
color: #363636;
}
// focus/hover styles for buttons
.file-label:focus-within .file-cta {
background-color: #e8e8e8;
color: #363636;
}
.button.is-primary:focus, .button.is-primary:hover, .button.is-primary:active {
background-color: $dark !important;
color: white !important;
}
.button.is-warning:focus, .button.is-warning:hover, .button.is-warning:active {
background-color: $wr-dark-yellow !important;
color: black !important;
}
.replay-bar .button {
margin: 0 1px;
}
.replay-bar .button:focus {
box-shadow: none !important;
outline: 1px dotted $wr-dark-blue;
outline: -webkit-focus-ring-color auto 1px;
}
.skip-link {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute !important;
width: 1px;
word-wrap: normal !important; /* Many screen reader and browser combinations announce broken words as they would appear visually. */
&:focus {
background-color: black;
border-radius: 3px;
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);
clip: auto !important;
clip-path: none;
color: white;
display: block;
font-family: inherit;
font-size: 1.3em;
font-weight: bold;
height: auto;
left: 5px;
line-height: normal;
padding: 15px 23px 14px;
text-decoration: none;
top: 5px;
width: auto;
z-index: 100000;
}
}
.main-scroll { .main-scroll {
flex: 1; flex: 1;
scroll-behavior: smooth; scroll-behavior: smooth;
+59 -24
View File
@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'lit-element'; 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'; import { registerSW, initDBWorker } from './pageutils';
@@ -23,6 +23,7 @@ class App extends LitElement
this.pageParams = {}; this.pageParams = {};
this.inited = false; this.inited = false;
this.navMenuShown = false;
registerSW(__SW_NAME__); registerSW(__SW_NAME__);
initDBWorker(); initDBWorker();
@@ -94,6 +95,7 @@ class App extends LitElement
.logo-text { .logo-text {
padding-left: 0px; padding-left: 0px;
margin-left: 8px;
} }
a.navbar-item.logo-text:hover { a.navbar-item.logo-text:hover {
@@ -103,7 +105,7 @@ class App extends LitElement
@media screen and (max-width: 840px) { @media screen and (max-width: 840px) {
.wide-only { .wide-only {
display: none; display: none !important;
} }
} }
@@ -116,19 +118,24 @@ class App extends LitElement
} }
return html` return html`
${!this.embed || this.embed === "full" ? 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"> <div class="navbar-brand">
${!this.embed ? html` ${!this.embed ? html `
<a class="navbar-item wr-logo-item" title="ReplayWeb.page" href="/"> <a href="/" class="navbar-item wr-logo-item" aria-labelledby="home">
<fa-icon id="wrlogo" size="2.5rem" .svg=${rwpLogo}></fa-icon> <fa-icon id="wrlogo" size="2.5rem" .svg=${rwpLogo} aria-hidden="true"></fa-icon>
</a>` : html` <span id="home" class="logo-text has-text-weight-bold is-size-5 has-allcaps wide-only">
<a class="navbar-item wr-logo-item" title="ReplayWeb.page"> <span class="has-text-primary">replay</span>
<fa-icon id="wrlogo" size="2.5rem" .svg=${rwpLogo}></fa-icon> <span class="has-text-link">web.page</span>
</a> <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 href="#" role="button" id="menu-button" @click="${this.onNavMenu}" @keyup="${clickOnSpacebarPress}"
<a role="button" @click="${this.onNavMenu}" class="navbar-burger burger ${this.navMenuShown ? 'is-active' : ''}" aria-label="main menu" aria-haspopup="true" aria-expanded="${this.navMenuShown}">
class="navbar-burger burger ${this.navMenuShown ? 'is-active' : ''}" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
@@ -137,24 +144,26 @@ class App extends LitElement
<div class="navbar-menu ${this.navMenuShown ? 'is-active' : ''}"> <div class="navbar-menu ${this.navMenuShown ? 'is-active' : ''}">
<div class="navbar-start"> <div class="navbar-start">
${!this.embed ? html` ${!this.embed ? html`
<a class="navbar-item logo-text has-text-weight-bold is-size-5 has-allcaps" href="/"> <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-primary">replay</span>
<span class="has-text-link">web.page</span> <span class="has-text-link">web.page</span>
<span class="is-sr-only">Home</span>
</a>` : ``} </a>` : ``}
${IS_APP && !this.collInfo ? html` ${IS_APP && !this.collInfo ? html`
<a class="navbar-item arrow-button" title="Go Back" @click="${(e) => window.history.back()}"> <a role="button" href="#" class="navbar-item arrow-button" title="Go Back" @click="${(e) => window.history.back()}" @keyup="${clickOnSpacebarPress}">
<fa-icon .svg="${fasArrowLeft}"></fa-icon><span class="menu-only">&nbsp;Go Back</span> <fa-icon .svg="${fasArrowLeft}" aria-hidden="true"></fa-icon><span class="menu-only">&nbsp;Go Back</span>
</a> </a>
<a class="navbar-item arrow-button" title="Go Forward" @click="${(e) => window.history.forward()}"> <a role="button" href="#" class="navbar-item arrow-button" title="Go Forward" @click="${(e) => window.history.forward()}" @keyup="${clickOnSpacebarPress}">
<fa-icon .svg="${fasArrowRight}"></fa-icon><span class="menu-only">&nbsp;Go Forward</span> <fa-icon .svg="${fasArrowRight}" aria-hidden="true"></fa-icon><span class="menu-only">&nbsp;Go Forward</span>
</a> </a>
` : ``} ` : ``}
${this.sourceUrl && this.collInfo ? html` ${this.sourceUrl && this.collInfo ? html`
<div class="navbar-item has-dropdown is-hoverable"> <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 tabindex="0" class="info-menu navbar-link is-arrowless" aria-labelledby="about-this-archive">
<span class="menu-only">About this Archive</span> <fa-icon size="1.2em" .svg="${fasInfoIcon}" aria-hidden="true"></fa-icon>
</a> <span id="about-this-archive" class="menu-only">About this Archive</span>
</span>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<div class="navbar-item wide-only"><i>About this Archive</i></div> <div class="navbar-item wide-only"><i>About this Archive</i></div>
<hr class="navbar-divider"> <hr class="navbar-divider">
@@ -184,11 +193,16 @@ class App extends LitElement
${!this.embed ? html` ${!this.embed ? html`
<div class="navbar-end"> <div class="navbar-end">
<a href="/docs" target="_blank" class="navbar-item"> <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> </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> <a href="?terms" @click="${(e) => { e.preventDefault(); this.showTerms = true} }"class="navbar-item">Terms</a>
</div>` : html``} </div>` : html``}
</nav> </div>
</nav><p id="skip-main-target" tabindex="-1" class="is-sr-only">Skipped</p>
` : ''} ` : ''}
${this.sourceUrl ? html` ${this.sourceUrl ? html`
@@ -242,6 +256,7 @@ class App extends LitElement
</section> </section>
</div> </div>
</div> </div>
</div>
` : ``} ` ` : ``} `
} }
@@ -274,13 +289,33 @@ class App extends LitElement
} }
} }
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) { onNavMenu(event) {
event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.navMenuShown = !this.navMenuShown; this.navMenuShown = !this.navMenuShown;
if (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.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}); }, {once: true});
} }
} }
+4 -4
View File
@@ -111,8 +111,8 @@ class Chooser extends LitElement
render() { render() {
return html` return html`
<section class="section less-padding"> <section class="section less-padding">
<nav class="panel"> <div class="panel">
<p class="panel-heading">Load Web Archive</p> <h2 class="panel-heading">Load Web Archive</h2>
<div class="extra-padding panel-block file has-name"> <div class="extra-padding panel-block file has-name">
<form class="content is-flex" @submit="${this.onStartLoad}"> <form class="content is-flex" @submit="${this.onStartLoad}">
<label class="file-label"> <label class="file-label">
@@ -121,7 +121,7 @@ class Chooser extends LitElement
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload"> @change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">
<span class="file-cta"> <span class="file-cta">
<span class="file-icon"> <span class="file-icon">
<fa-icon size="0.9em" .svg=${fasUpload}></fa-icon> <fa-icon size="0.9em" .svg=${fasUpload} aria-hidden="true"></fa-icon>
</span> </span>
<span class="file-label is-hidden-touch"> <span class="file-label is-hidden-touch">
Choose File... Choose File...
@@ -146,7 +146,7 @@ class Chooser extends LitElement
</form> </form>
</div> </div>
</nav> </div>
</section>`; </section>`;
} }
} }
+11 -11
View File
@@ -147,14 +147,14 @@ class CollIndex extends LitElement
.no-top-padding { .no-top-padding {
padding-top: 1.0em; padding-top: 1.0em;
} }
button.is-loading { .is-loading {
line-height: 1.5em; line-height: 1.5em;
height: 1.5em; height: 1.5em;
border: 0px; border: 0px;
background-color: transparent !important; background-color: transparent !important;
width: auto; width: auto;
} }
nav.panel.is-light { div.panel.is-light {
margin-bottom: 2em; margin-bottom: 2em;
} }
@@ -251,14 +251,14 @@ class CollIndex extends LitElement
</header> </header>
<section class="section no-top-padding"> <section class="section no-top-padding">
<div class="sort-header is-small"> <div class="sort-header is-small">
<a @click=${(e) => this.hideHeader = !this.hideHeader} class="collapse button is-small"> <button @click=${(e) => this.hideHeader = !this.hideHeader} class="collapse button is-small">
<span class="icon"><fa-icon .svg=${this.hideHeader ? fasArrowDown : fasArrowUp}></span> <span class="icon"><fa-icon .svg=${this.hideHeader ? fasArrowDown : fasArrowUp}></span>
<span>${this.hideHeader ? 'Show' : 'Hide'}</span> <span>${this.hideHeader ? 'Show ' : 'Hide'} <span class="is-sr-only">Header</span></span>
</a> </button>
</div>
<nav class="panel is-light">
<div class="panel-heading"><span>Loaded Archives</span>
</div> </div>
<div class="panel is-light">
<h2 class="panel-heading"><span>Loaded Archives</span>
</h2>
${this.colls.length ? html` ${this.colls.length ? html`
<div class="panel-block sort-header is-small"> <div class="panel-block sort-header is-small">
@@ -296,9 +296,9 @@ class CollIndex extends LitElement
</div> </div>
</div> </div>
${!this._deleting[coll.sourceUrl] ? html` ${!this._deleting[coll.sourceUrl] ? html`
<button class="delete" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button> <button class="delete" aria-label="Unload Collection" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button>
` : html` ` : html`
<button class="button delete is-loading is-static"></button>`} <span class="button delete is-loading is-static">Deleting</span`}
</div> </div>
`)} `)}
</div> </div>
@@ -309,7 +309,7 @@ class CollIndex extends LitElement
<i>No Archives so far! Archives loaded in the section above will appear here.</i> <i>No Archives so far! Archives loaded in the section above will appear here.</i>
</div> </div>
`} `}
</nav> </div>
</section> </section>
`; `;
} }
+108 -63
View File
@@ -1,5 +1,5 @@
import { LitElement, html, css, query } from 'lit-element'; import { LitElement, html, css, query } from 'lit-element';
import { wrapCss, rwpLogo, IS_APP } from './misc'; import { wrapCss, rwpLogo, IS_APP, clickOnSpacebarPress } from './misc';
import { sourceToId, tsToDate, getPageDateTS } from './pageutils'; import { sourceToId, tsToDate, getPageDateTS } from './pageutils';
@@ -138,6 +138,10 @@ class Coll extends LitElement
this._replaceLoc = false; this._replaceLoc = false;
} else { } else {
window.location.hash = this._locationHash; window.location.hash = this._locationHash;
const replay = this.renderRoot.querySelector("wr-coll-replay");
if (replay) {
replay.focus();
}
} }
if (this.embed && window.parent !== window) { if (this.embed && window.parent !== window) {
window.parent.postMessage(this.tabData, '*'); window.parent.postMessage(this.tabData, '*');
@@ -290,7 +294,6 @@ class Coll extends LitElement
updateTabData(data, replaceLoc = false, merge = false) { updateTabData(data, replaceLoc = false, merge = false) {
this.tabData = {...this.tabData, ...data}; this.tabData = {...this.tabData, ...data};
if (this.tabData.url) { if (this.tabData.url) {
this.url = this.tabData.url || ""; this.url = this.tabData.url || "";
} }
@@ -465,7 +468,7 @@ class Coll extends LitElement
cursor: col-resize; cursor: col-resize;
} }
wr-coll-replay { main, wr-coll-replay {
width: 100%; width: 100%;
} }
@@ -483,12 +486,13 @@ class Coll extends LitElement
border-bottom: 0px; border-bottom: 0px;
} }
.sidebar-nav span { .sidebar-nav span.nav-hover {
font-size: smaller; font-size: smaller;
display: none; display: none;
} }
.sidebar-nav:hover span { .sidebar-nav:hover span.nav-hover,
.sidebar-nav:focus-within span.nav-hover {
display: initial; display: initial;
color: rgb(72, 118, 255); color: rgb(72, 118, 255);
} }
@@ -509,6 +513,16 @@ class Coll extends LitElement
right: 8px; right: 8px;
} }
/* Since the replay sometimes programmatically receives keyboard focus,
and that is visually unexpected for mouse-users, and since this won't
particularly trip up keyboard users, just remove the focus style. */
wr-coll-replay:focus {
outline: none;
}
/* Some keyboard-users may see this replacement style */
wr-coll-replay:focus-visible {
outline: 1px solid rgb(72, 118, 255);
}
`; `;
} }
@@ -518,6 +532,10 @@ class Coll extends LitElement
const isReplay = !!this.tabData.url; const isReplay = !!this.tabData.url;
const isSidebar = isReplay && this.showSidebar; const isSidebar = isReplay && this.showSidebar;
if (!isReplay){
document.title = `Browse Contents | ReplayWeb.page`;
}
if (this.collInfo && !this.collInfo.coll) { if (this.collInfo && !this.collInfo.coll) {
return html` return html`
<wr-loader .loadInfo="${this.loadInfo}" embed="${this.embed}" <wr-loader .loadInfo="${this.loadInfo}" embed="${this.embed}"
@@ -525,15 +543,17 @@ class Coll extends LitElement
} else if (this.collInfo) { } else if (this.collInfo) {
return html` return html`
${this.renderLocationBar()} ${this.renderLocationBar()}
<div id="tabContents"> <div id="tabContents">
<nav id="contents" class="is-light ${isSidebar ? 'sidebar' : (isReplay ? 'is-hidden' : 'full-pages')}"> <div id="contents" class="is-light ${isSidebar ? 'sidebar' : (isReplay ? 'is-hidden' : 'full-pages')}"
role="${isSidebar ? "complementary" : ""}" aria-label="${isSidebar ? "Browse Contents" : ""}">
${this.renderTabHeader(isSidebar)} ${this.renderTabHeader(isSidebar)}
${this.renderCollTabs(isSidebar)} ${this.renderCollTabs(isSidebar)}
</nav> </div>
${isReplay ? html` ${isReplay ? html`
<wr-coll-replay <wr-coll-replay
role="main"
tabindex="-1"
.collInfo="${this.collInfo}" .collInfo="${this.collInfo}"
sourceUrl="${this.sourceUrl}" sourceUrl="${this.sourceUrl}"
url="${this.tabData.url || ""}" url="${this.tabData.url || ""}"
@@ -544,7 +564,6 @@ class Coll extends LitElement
</wr-coll-replay> </wr-coll-replay>
` : ``} ` : ``}
</div> </div>
`; `;
} else { } else {
return html``; return html``;
@@ -557,46 +576,49 @@ class Coll extends LitElement
// } // }
return html` return html`
<div class="main tabs is-centered ${isSidebar ? 'sidebar' : ''}"> <nav class="main tabs is-centered ${isSidebar ? 'sidebar' : ''}" aria-label="tabs">
<ul> <ul>
${isSidebar ? html` ${isSidebar ? html`
<li class="sidebar-nav left"> <li class="sidebar-nav left">
<a @click="${this.onHideSidebar}" class="is-marginless is-size-6 is-paddingless"> <a role="button" href="#" @click="${this.onHideSidebar}" @keyup="${clickOnSpacebarPress}" class="is-marginless is-size-6 is-paddingless">
<fa-icon title="Hide" .svg="${fasAngleLeft}"></fa-icon><span>Hide</span> <fa-icon title="Hide" .svg="${fasAngleLeft}" aria-hidden="true"></fa-icon>
<span class="nav-hover" aria-hidden="true">Hide</span>
<span class="is-sr-only">Hide Sidebar</span>
</a> </a>
</li>` : ``} </li>` : ``}
${this.hasStory ? html` ${this.hasStory ? html`
<li class="${this.tabData.view === 'story' ? 'is-active' : ''}"> <li class="${this.tabData.view === 'story' ? 'is-active' : ''}">
<a @click="${this.onTabClick}" href="#story" class="is-size-6"> <a @click="${this.onTabClick}" href="#story" class="is-size-6" aria-label="Story" aria-current="${this.tabData.view === 'story' ? 'location' : ''}">
<span class="icon"><fa-icon .svg="${fasBook}"></fa-icon></span> <span class="icon"><fa-icon .svg="${fasBook}" aria-hidden="true" title="Story"></fa-icon></span>
<span class="tab-label ${isSidebar ? 'is-hidden' : ''}" title="Story">Story</span> <span class="tab-label ${isSidebar ? 'is-hidden' : ''}" title="Story">Story</span>
</a> </a>
</li>` : ``} </li>` : ``}
<li class="${this.tabData.view === 'pages' ? 'is-active' : ''}"> <li class="${this.tabData.view === 'pages' ? 'is-active' : ''}">
<a @click="${this.onTabClick}" href="#pages" class="is-size-6"> <a @click="${this.onTabClick}" href="#pages" class="is-size-6" aria-label="Pages" aria-current="${this.tabData.view === 'pages' ? 'location' : ''}">
<span class="icon"><fa-icon .svg="${farPages}"></fa-icon></span> <span class="icon"><fa-icon .svg="${farPages}" aria-hidden="true" title="Pages"></fa-icon></span>
<span class="tab-label ${isSidebar ? 'is-hidden' : ''}" title="Pages">Pages</span> <span class="tab-label ${isSidebar ? 'is-hidden' : ''}" title="Pages">Pages</span>
</a> </a>
</li> </li>
<li class="${this.tabData.view === 'resources' ? 'is-active' : ''}"> <li class="${this.tabData.view === 'resources' ? 'is-active' : ''}">
<a @click="${this.onTabClick}" href="#resources" class="is-size-6"> <a @click="${this.onTabClick}" href="#resources" class="is-size-6" aria-label="URLs" aria-current="${this.tabData.view === 'resources' ? 'location' : ''}">
<span class="icon"><fa-icon .svg="${farResources}"></fa-icon></span> <span class="icon"><fa-icon .svg="${farResources}" aria-hidden="true" title="URLs"></fa-icon></span>
<span class="tab-label ${isSidebar ? 'is-hidden' : ''}" title="URL Resources">Page Resources</span> <span class="tab-label ${isSidebar ? 'is-hidden' : ''}" title="URLs">URLs</span>
</a> </a>
</li> </li>
${isSidebar ? html` ${isSidebar ? html`
<li class="sidebar-nav right"> <li class="sidebar-nav right">
<a @click="${this.onFullPageView}" class="is-marginless is-size-6 is-paddingless"> <a role="button" href="#" @click="${this.onFullPageView}" @keyup="${clickOnSpacebarPress}" class="is-marginless is-size-6 is-paddingless">
<span>Expand</span><fa-icon title="Expand" .svg="${fasAngleRight}"></fa-icon> <span class="nav-hover" aria-hidden="true">Expand</span>
<span class="is-sr-only">Expand Sidebar to Full View</span>
<fa-icon title="Expand" .svg="${fasAngleRight}" aria-hidden="true"></fa-icon>
</a> </a>
</li>` : ``} </li>` : ``}
</ul> </ul>
</div>`; </nav>`;
} }
renderLocationBar() { renderLocationBar() {
@@ -609,84 +631,91 @@ class Coll extends LitElement
const isReplay = !!this.tabData.url; const isReplay = !!this.tabData.url;
return html` return html`
<div class="replay-bar"> <a class="skip-link" href="#skip-replay-target" @click="${this.skipMenu}">Skip replay navigation</a>
<nav class="replay-bar" aria-label="replay">
<div class="field has-addons"> <div class="field has-addons">
<button id="fullscreen" class="button is-borderless is-hidden-touch" @click="${this.onFullscreenToggle}"> <a href="#" role="button" id="fullscreen" class="button is-borderless is-hidden-touch" @click="${this.onFullscreenToggle}" @keyup="${clickOnSpacebarPress}"
title="${this.isFullscreen ? "Exit Full Screen" : "Full Screen"}" aria-label="${this.isFullscreen ? "Exit Fullscreen" : "Fullscreen"}">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
</span> </span>
</button> </a>
<button class="button is-borderless is-hidden-mobile" @click="${this.onGoBack}"> <a href="#" role="button" class="button is-borderless is-hidden-mobile" @click="${this.onGoBack}" @keyup="${clickOnSpacebarPress}"
title="Back" aria-label="Back">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasLeft}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasLeft}"></fa-icon>
</span> </span>
</button> </a>
<button class="button is-borderless is-hidden-mobile" @click="${this.onGoForward}"> <a href="#" role="button" class="button is-borderless is-hidden-mobile" @click="${this.onGoForward}" @keyup="${clickOnSpacebarPress}"
title="Forward" aria-label="Forward">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasRight}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRight}"></fa-icon>
</span> </span>
</button> </a>
<button id="refresh" class="button is-borderless ${this.isLoading ? 'is-loading' : 'is-hidden-mobile'}" @click="${this.onRefresh}"> <a href="#" role="button" id="refresh" class="button is-borderless ${this.isLoading ? 'is-loading' : 'is-hidden-mobile'}" @click="${this.onRefresh}" @keyup="${clickOnSpacebarPress}"
title="Reload" aria-label="Reload">
<span class="icon is-small"> <span class="icon is-small">
${!this.isLoading ? html` ${!this.isLoading ? html`
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasRefresh}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRefresh}"></fa-icon>
` : ``} ` : ``}
</span> </span>
</button> </a>
<button class="button is-borderless is-hidden-touch ${!isReplay ? 'grey-disabled' : ''}" @click="${this.onShowPages}"> <a href="#" role="button" class="button is-borderless is-hidden-touch ${!isReplay ? 'grey-disabled' : ''}" @click="${this.onShowPages}" @keyup="${clickOnSpacebarPress}"
?disabled="${!isReplay}" title="Browse Contents" aria-label="Browse Contents" aria-controls="contents">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${farListAlt}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${farListAlt}"></fa-icon>
</span> </span>
</button> </a>
<form @submit="${this.onSubmit}"> <form @submit="${this.onSubmit}">
<div class="control is-expanded"> <div class="control is-expanded">
<input id="url" class="input" type="text" @keydown="${this.onKeyDown}" .value="${this.url}" placeholder="Enter text to search or a URL to replay"/> <input id="url" class="input" type="search" @keydown="${this.onKeyDown}" .value="${this.url}" placeholder="Enter text to search or a URL to replay"/>
${isReplay ? html`<p id="datetime" class="control is-hidden-mobile">${dateStr}</p>` : html``} ${isReplay ? html`<p id="datetime" class="control is-hidden-mobile">${dateStr}</p>` : html``}
</div> </div>
</form> </form>
<div class="dropdown is-right ${this.menuActive ? 'is-active' : ''}" @click="${(e) => this.menuActive = false}"> <div class="dropdown is-right ${this.menuActive ? 'is-active' : ''}" @click="${(e) => this.menuActive = false}">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button class="button is-borderless" aria-haspopup="true" aria-controls="menu-dropdown" @click="${this.onMenu}"> <button class="button is-borderless" aria-haspopup="true" aria-controls="menu-dropdown" aria-expanded="${this.menuActive}" @click="${this.onMenu}"
aria-label="more replay controls">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasMenuV}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasMenuV}"></fa-icon>
</span> </span>
</button> </button>
</div> </div>
<div class="dropdown-menu" id="menu-dropdown" role="menu"> <div class="dropdown-menu" id="menu-dropdown">
<div class="dropdown-content"> <div class="dropdown-content">
<a class="dropdown-item is-hidden-desktop" @click="${this.onFullscreenToggle}"> <a href="#" role="button" class="dropdown-item is-hidden-desktop" @click="${this.onFullscreenToggle}" @keyup="${clickOnSpacebarPress}">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
</span> </span>
<span>Full Screen</span> <span>Full Screen</span>
</a> </a>
<a class="dropdown-item is-hidden-tablet" @click="${this.onGoBack}"> <a href="#" role="button" class="dropdown-item is-hidden-tablet" @click="${this.onGoBack}" @keyup="${clickOnSpacebarPress}">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasLeft}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasLeft}"></fa-icon>
</span> </span>
<span>Back</span> <span>Back</span>
</a> </a>
<a class="dropdown-item is-hidden-tablet" @click="${this.onGoForward}"> <a href="#" role="button" class="dropdown-item is-hidden-tablet" @click="${this.onGoForward}" @keyup="${clickOnSpacebarPress}">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasRight}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRight}"></fa-icon>
</span> </span>
<span>Forward</span> <span>Forward</span>
</a> </a>
<a class="dropdown-item is-hidden-tablet" @click="${this.onRefresh}"> <a href="#" role="button" class="dropdown-item is-hidden-tablet" @click="${this.onRefresh}" @keyup="${clickOnSpacebarPress}">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasRefresh}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${fasRefresh}"></fa-icon>
</span> </span>
<span>Reload</span> <span>Reload</span>
</a> </a>
<a class="dropdown-item is-hidden-desktop ${!isReplay ? 'grey-disabled' : ''}" @click="${this.onShowPages}"> <a href="#" role="button" class="dropdown-item is-hidden-desktop ${!isReplay ? 'grey-disabled' : ''}" @click="${this.onShowPages}" @keyup="${clickOnSpacebarPress}">
<span class="icon is-small"> <span class="icon is-small">
<fa-icon size="1.0em" class="has-text-grey" .svg="${farListAlt}"></fa-icon> <fa-icon size="1.0em" class="has-text-grey" aria-hidden="true" .svg="${farListAlt}"></fa-icon>
</span> </span>
<span>Page Search</span> <span>Browse Contents</span>
</a> </a>
<hr class="dropdown-divider is-hidden-desktop"> <hr class="dropdown-divider is-hidden-desktop">
<a class="dropdown-item" @click="${this.onPurgeCache}"> <a href="#" role="button" class="dropdown-item" @click="${this.onPurgeCache}" @keyup="${clickOnSpacebarPress}">
Purge Cache + Full Reload Purge Cache + Full Reload
</a> </a>
${dateStr ? html` ${dateStr ? html`
@@ -697,7 +726,7 @@ class Coll extends LitElement
${this.embed ? html` ${this.embed ? html`
<hr class="dropdown-divider"> <hr class="dropdown-divider">
<a href="https://replayweb.page/" target="_blank" class="dropdown-item info"> <a href="https://replayweb.page/" target="_blank" class="dropdown-item info">
<fa-icon class="menu-logo" size="1.0rem" .svg=${rwpLogo}></fa-icon> <fa-icon class="menu-logo" size="1.0rem" aria-hidden="true" .svg=${rwpLogo}></fa-icon>
<span>&nbsp;About ReplayWeb.page</span> <span>&nbsp;About ReplayWeb.page</span>
<span class="menu-version">(${__VERSION__})</span> <span class="menu-version">(${__VERSION__})</span>
</a> </a>
@@ -708,7 +737,7 @@ class Coll extends LitElement
</div> </div>
</div>`; </nav><p id="skip-replay-target" tabindex="-1" class="is-sr-only">Skipped</p>`;
} }
dragStart(event) { dragStart(event) {
@@ -737,7 +766,9 @@ class Coll extends LitElement
currList="${this.tabData.currList || 0}" currList="${this.tabData.currList || 0}"
@coll-tab-nav="${this.onCollTabNav}" id="story" @coll-tab-nav="${this.onCollTabNav}" id="story"
.isSidebar="${isSidebar}" .isSidebar="${isSidebar}"
class="${isStory ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"> class="${isStory ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"
role="${isSidebar ? '' : 'main'}"
>
</wr-coll-story>` : ''} </wr-coll-story>` : ''}
<wr-coll-resources .collInfo="${this.collInfo}" <wr-coll-resources .collInfo="${this.collInfo}"
@@ -747,7 +778,9 @@ class Coll extends LitElement
.currMime="${this.tabData.currMime || ""}" .currMime="${this.tabData.currMime || ""}"
@coll-tab-nav="${this.onCollTabNav}" id="resources" @coll-tab-nav="${this.onCollTabNav}" id="resources"
.isSidebar="${isSidebar}" .isSidebar="${isSidebar}"
class="is-paddingless ${isResources ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"> class="is-paddingless ${isResources ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"
role="${isSidebar ? '' : 'main'}"
>
</wr-coll-resources> </wr-coll-resources>
<wr-page-view <wr-page-view
@@ -760,11 +793,20 @@ class Coll extends LitElement
.url="${this.tabData.url || ""}" .url="${this.tabData.url || ""}"
.ts="${this.tabData.ts || ""}" .ts="${this.tabData.ts || ""}"
@coll-tab-nav="${this.onCollTabNav}" id="pages" @coll-tab-nav="${this.onCollTabNav}" id="pages"
class="${isPages ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"> class="${isPages ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"
role="${isSidebar ? '' : 'main'}"
>
</wr-page-view> </wr-page-view>
`; `;
} }
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-replay-target").focus()
}
onKeyDown(event) { onKeyDown(event) {
if (event.key === "Esc" || event.key === "Escape") { if (event.key === "Esc" || event.key === "Escape") {
event.target.value = this.url; event.target.value = this.url;
@@ -782,7 +824,8 @@ class Coll extends LitElement
} }
} }
onFullscreenToggle() { onFullscreenToggle(event) {
event.preventDefault();
this.menuActive = false; this.menuActive = false;
if (!this.isFullscreen) { if (!this.isFullscreen) {
this.requestFullscreen(); this.requestFullscreen();
@@ -791,12 +834,14 @@ class Coll extends LitElement
} }
} }
onGoBack() { onGoBack(event) {
event.preventDefault();
this.menuActive = false; this.menuActive = false;
window.history.back(); window.history.back();
} }
onGoForward() { onGoForward(event) {
event.preventDefault();
this.menuActive = false; this.menuActive = false;
window.history.forward(); window.history.forward();
} }
+5 -2
View File
@@ -168,13 +168,16 @@ class Embed extends LitElement
render() { render() {
return html` return html`
${this.paramString && this.hashString && this.swInited ? html` ${this.paramString && this.hashString && this.swInited ? html`
<iframe sandbox="allow-downloads allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-scripts allow-same-origin allow-forms" @load="${this.onLoad}" src="${this.replaybase}?${this.paramString}#${this.hashString}" allow="autoplay *; fullscreen"></iframe> <iframe sandbox="allow-downloads allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-scripts allow-same-origin allow-forms"
@load="${this.onLoad}" src="${this.replaybase}?${this.paramString}#${this.hashString}" allow="autoplay *; fullscreen"
title="Replay of ${this.title ? `${this.title}:` :``} ${this.url}"></iframe>
` : html``} ` : html``}
${this.noSW ? html` ${this.noSW ? html`
<section class="full-width"> <section class="full-width">
<div class="has-text-centered"> <div class="has-text-centered">
<fa-icon class="logo" id="wrlogo" size="2.5rem" .svg=${rwpLogo}></fa-icon> <fa-icon class="logo" id="wrlogo" size="2.5rem" .svg=${rwpLogo} aria-hidden="true"></fa-icon>
</div> </div>
<div class="error"> <div class="error">
Sorry, ReplayWeb.page won't work in this browser as Service Workers are not supported. Sorry, ReplayWeb.page won't work in this browser as Service Workers are not supported.
+12 -1
View File
@@ -18,6 +18,17 @@ function wrapCss(custom) {
const IS_APP = window.IS_APP || window.electron && window.electron.IS_APP || window.matchMedia('(display-mode: standalone)').matches; const IS_APP = window.IS_APP || window.electron && window.electron.IS_APP || window.matchMedia('(display-mode: standalone)').matches;
// ===========================================================================
// Buttons are expected to respond to both enter/return and spacebar.
// If using `<a>` with `role='button'`, assign this handler to keyup.
function clickOnSpacebarPress(event) {
if (event.key == " ") {
event.preventDefault();
event.target.click();
}
}
// =========================================================================== // ===========================================================================
class FaIcon extends LitElement class FaIcon extends LitElement
{ {
@@ -117,4 +128,4 @@ class AnimLogo extends FaIcon
customElements.define("fa-icon", FaIcon); customElements.define("fa-icon", FaIcon);
customElements.define("wr-anim-logo", AnimLogo); customElements.define("wr-anim-logo", AnimLogo);
export { wrapCss, IS_APP, rwpLogo, FaIcon, AnimLogo }; export { wrapCss, IS_APP, clickOnSpacebarPress, rwpLogo, FaIcon, AnimLogo };
+14 -3
View File
@@ -23,6 +23,7 @@ class PageEntry extends LitElement
this.iconValid = false; this.iconValid = false;
this.index = 0; this.index = 0;
this.isCurrent = false; this.isCurrent = false;
this.isSidebar = false;
this.timestamp = ""; this.timestamp = "";
this.date = null; this.date = null;
@@ -40,6 +41,7 @@ class PageEntry extends LitElement
iconValid: { type: Boolean }, iconValid: { type: Boolean },
index: { type: Number }, index: { type: Number },
isCurrent: { type: Boolean }, isCurrent: { type: Boolean },
isSidebar: { type: Boolean },
timestamp: { type: String }, timestamp: { type: String },
date: { type: Object } date: { type: Object }
} }
@@ -75,6 +77,15 @@ class PageEntry extends LitElement
width: 100%; width: 100%;
} }
/* Overrde Bulma to add the tiniest margin, so the focus indicator isn't obscured */
.columns {
margin-top: calc(-0.75rem + 2px);
}
.columns:last-child {
margin-bottom: calc(-0.75rem + 2px);
}
.favicon { .favicon {
width: 24px !important; width: 24px !important;
height: 24px !important; height: 24px !important;
@@ -189,7 +200,7 @@ class PageEntry extends LitElement
<div>${date ? date.toLocaleTimeString() : ""}</div> <div>${date ? date.toLocaleTimeString() : ""}</div>
</div> </div>
<div class="column"> <div class="column">
<article class="media"> <div class="media">
<figure class="media-left"> <figure class="media-left">
<p class=""> <p class="">
${this.iconValid ? html` ${this.iconValid ? html`
@@ -198,7 +209,7 @@ class PageEntry extends LitElement
</p> </p>
</figure> </figure>
<div class="media-content ${this.isCurrent ? 'current' : ''}"> <div class="media-content ${this.isCurrent ? 'current' : ''}">
<a @click="${this.onReplay}" href="#"> <div role="heading" aria-level="${this.isSidebar ? "4": "3"}"><a @click="${this.onReplay}" href="#">
${this.isCurrent ? html`<p class="curr-page is-pulled-right">Current Page</p>` : ``} ${this.isCurrent ? html`<p class="curr-page is-pulled-right">Current Page</p>` : ``}
<p class="is-size-6 has-text-weight-bold has-text-link text"> <p class="is-size-6 has-text-weight-bold has-text-link text">
<keyword-mark keywords="${this.query}">${p.title || p.url}</keyword-mark> <keyword-mark keywords="${this.query}">${p.title || p.url}</keyword-mark>
@@ -215,7 +226,7 @@ class PageEntry extends LitElement
<div class="media-right" style="margin-right: 2em"> <div class="media-right" style="margin-right: 2em">
${prettyBytes(p.size)} ${prettyBytes(p.size)}
</div>` : ``} </div>` : ``}
</article> </div>
</div> </div>
</div> </div>
+34 -42
View File
@@ -1,7 +1,7 @@
"use strict"; "use strict";
import { LitElement, html, css, unsafeCSS } from 'lit-element'; import { LitElement, html, css, unsafeCSS } from 'lit-element';
import { wrapCss } from './misc'; import { wrapCss, clickOnSpacebarPress } from './misc';
import ndjson from 'fetch-ndjson'; import ndjson from 'fetch-ndjson';
import FlexSearch from 'flexsearch'; import FlexSearch from 'flexsearch';
@@ -265,6 +265,10 @@ class Pages extends LitElement
box-sizing: border-box !important; box-sizing: border-box !important;
} }
div[role="main"], #contents div[role="complementary"] {
height: 100%;
}
.main.columns { .main.columns {
width: 100%; width: 100%;
justify-self: stretch; justify-self: stretch;
@@ -343,7 +347,6 @@ class Pages extends LitElement
max-height: 100%; max-height: 100%;
height: 100%; height: 100%;
} }
.index-bar-menu { .index-bar-menu {
max-height: 100%; max-height: 100%;
overflow-y: auto; overflow-y: auto;
@@ -467,36 +470,24 @@ class Pages extends LitElement
const currList = this.currList; const currList = this.currList;
return html` return html`
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "2": "1"}">
Pages in ${this.collInfo.title}
</div>
<div class="search-bar notification is-marginless"> <div class="search-bar notification is-marginless">
${this.isSidebar ? html `<h3 class="is-sr-only">Search and Filter Pages</h3>` : ``}
<div class="field flex-auto"> <div class="field flex-auto">
<div class="control has-icons-left ${this.loading ? 'is-loading' : ''}"> <div class="control has-icons-left ${this.loading ? 'is-loading' : ''}">
<input type="text" class="input" @input="${this.onChangeQuery}" .value="${this.query}" type="text" <input type="search" class="input" @input="${this.onChangeQuery}" .value="${this.query}" type="text"
placeholder="Search by Page URL, Title or Text"> placeholder="Search by Page URL, Title or Text">
<span class="icon is-left"><fa-icon .svg="${fasSearch}"/></span> <span class="icon is-left"><fa-icon .svg="${fasSearch}" aria-hidden="true"></fa-icon></span>
</div> </div>
</div> </div>
${this.isSidebar && this.collInfo.lists.length ? html`
<div class="is-hidden-tablet mobile-lists">
<span class="is-size-7">Filter By List:</span>
<div class="select is-small">
<select id="sort-select" @change=${this.onSelectListDrop}>
<option value="0" ?selected="${this.currList === 0}">All Pages</option>
${this.collInfo.lists.map(list => html`
<option value="${list.id}" ?selected="${this.currList === list.id}">${list.title}</option>
`)}
</select>
</div> </div>
</div>` : ``}
</div>
<div class="main columns"> <div class="main columns">
<div class="column index-bar is-one-fifth is-hidden-mobile"> <div class="column index-bar is-one-fifth is-hidden-mobile">
<div class="index-bar-title">${this.collInfo.title}</div> <div class="index-bar-title">${this.collInfo.title}</div>
<span class="num-results">${this.formatResults()}</span> <span class="num-results" aria-live="polite" aria-atomic="true">${this.formatResults()}</span>
${this.editable ? html` ${this.editable ? html`
<div class="index-bar-actions"> <div class="index-bar-actions">
@@ -505,7 +496,7 @@ class Pages extends LitElement
${this.collInfo.lists.length ? html` ${this.collInfo.lists.length ? html`
<p id="filter-label" class="menu-label">Filter By List:</p> <p id="filter-label" class="menu-label">Filter By List:</p>
<aside class="index-bar-menu menu"> <div class="index-bar-menu menu">
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a href="#list-0" data-list="0" class="${currList === 0 ? 'is-active' : ''}" <a href="#list-0" data-list="0" class="${currList === 0 ? 'is-active' : ''}"
@@ -518,13 +509,13 @@ class Pages extends LitElement
class="${currList === list.id ? 'is-active' : ''}">${list.title}</a> class="${currList === list.id ? 'is-active' : ''}">${list.title}</a>
</li>`)} </li>`)}
</ul> </ul>
</aside> </div>
` : ``} ` : ``}
</div> </div>
<div class="column main-content"> <div class="column main-content">
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "3": "2"}">Page List</div>
${this.renderPages()} ${this.renderPages()}
</div> </div>
</div>`; </div>`;
} }
@@ -532,26 +523,26 @@ class Pages extends LitElement
return html` return html`
<div class="dropdown ${this.menuActive ? 'is-active' : ''}"> <div class="dropdown ${this.menuActive ? 'is-active' : ''}">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button @click="${this.onMenu}" class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu"> <button @click="${this.onMenu}" class="button is-small" aria-haspopup="true" aria-expanded="${this.menuActive}" aria-controls="dropdown-menu">
<span>Download</span> <span>Download</span>
<span class="icon is-small"> <span class="icon is-small">
<fa-icon .svg="${fasAngleDown}"/> <fa-icon .svg="${fasAngleDown} aria-hidden="true"></fa-icon>
</span> </span>
</button> </button>
</div> </div>
<div class="dropdown-menu" id="dropdown-menu" role="menu"> <div class="dropdown-menu" id="dropdown-menu">
<div class="dropdown-content"> <div class="dropdown-content">
<a @click="${(e) => this.onDownload(e, "wacz", true)}" class="dropdown-item"> <a role="button" href="#" @click="${(e) => this.onDownload(e, "wacz", true)}" @keyup="${clickOnSpacebarPress}" class="dropdown-item">
Download Selected as WACZ (Web Archive Collection) Download Selected as WACZ (Web Archive Collection)
</a> </a>
<a @click="${(e) => this.onDownload(e, "warc", true)}" class="dropdown-item"> <a role="button" href="#" @click="${(e) => this.onDownload(e, "warc", true)}" @keyup="${clickOnSpacebarPress}" class="dropdown-item">
Download Selected as WARC Only Download Selected as WARC Only
</a> </a>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
<a @click="${(e) => this.onDownload(e, "wacz", false)}" class="dropdown-item"> <a role="button" href="#" @click="${(e) => this.onDownload(e, "wacz", false)}" @keyup="${clickOnSpacebarPress}" class="dropdown-item">
Download All as WACZ (Web Archive Collection) Download All as WACZ (Web Archive Collection)
</a> </a>
<a @click="${(e) => this.onDownload(e, "warc", false)}" class="dropdown-item"> <a role="button" href="#" @click="${(e) => this.onDownload(e, "warc", false)}" @keyup="${clickOnSpacebarPress}" class="dropdown-item">
Download All as WARC Only Download All as WARC Only
</a> </a>
</div> </div>
@@ -570,16 +561,16 @@ class Pages extends LitElement
<div class="header columns is-hidden-mobile"> <div class="header columns is-hidden-mobile">
${this.query ? html` ${this.query ? html`
<a @click="${this.onSort}" data-key="" class="column is-1 ${this.sortKey === "" ? (this.sortDesc ? "desc" : "asc") : ''}">Match</a>` : ``} <a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="" class="column is-1 ${this.sortKey === "" ? (this.sortDesc ? "desc" : "asc") : ''}">Match</a>` : ``}
<a @click="${this.onSort}" data-key="ts" class="column is-2 ${this.sortKey === "ts" ? (this.sortDesc ? "desc" : "asc") : ''}">Date</a> <a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="ts" class="column is-2 ${this.sortKey === "ts" ? (this.sortDesc ? "desc" : "asc") : ''}">Date</a>
<a @click="${this.onSort}" data-key="title" class="column is-6 pagetitle ${this.sortKey === "title" ? (this.sortDesc ? "desc" : "asc") : ''}">Page Title</a> <a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="title" class="column is-6 pagetitle ${this.sortKey === "title" ? (this.sortDesc ? "desc" : "asc") : ''}">Page Title</a>
</div> </div>
<div class="is-hidden-tablet mobile-header"> <div class="is-hidden-tablet mobile-header">
<div class="num-results">${this.formatResults()}</div> <div class="num-results" aria-live="polite" aria-atomic="true">${this.formatResults()}</div>
<wr-sorter id="pages" <wr-sorter id="pages"
.defaultKey="${this.sortKey ? this.sortKey : (this.query ? '' : 'ts')}" .defaultKey="${this.sortKey ? this.sortKey : (this.query ? '' : 'ts')}"
.defaultDesc="${this.sortDesc !== null ? this.sortDesc : (this.query ? false : true)}" .defaultDesc="${this.sortDesc !== null ? this.sortDesc : (this.query ? false : true)}"
@@ -618,18 +609,19 @@ class Pages extends LitElement
<div class="page-header has-text-weight-bold"> <div class="page-header has-text-weight-bold">
${this.renderPageHeader()} ${this.renderPageHeader()}
</div> </div>
<div class="scroller" @scroll="${this.onScroll}"> <ul class="scroller" @scroll="${this.onScroll}">
${this.sortedPages.length ? html` ${this.sortedPages.length ? html`
${this.sortedPages.map((p, i) => { ${this.sortedPages.map((p, i) => {
const isCurrPage = this.isCurrPage(p); const isCurrPage = this.isCurrPage(p);
return html` return html`
<div class="content ${isCurrPage ? 'current' : ''}"> <li class="content ${isCurrPage ? 'current' : ''}">
<wr-page-entry <wr-page-entry
.index="${this.query || this.isSidebar ? i + 1 : 0}" .index="${this.query || this.isSidebar ? i + 1 : 0}"
.editable="${this.editable}" .editable="${this.editable}"
.selected="${this.selectedPages.has(p.id)}" .selected="${this.selectedPages.has(p.id)}"
.isCurrent="${isCurrPage}" .isCurrent="${isCurrPage}"
.isSidebar="${this.isSidebar}"
.page="${p}" .page="${p}"
@sel-page="${this.onSelectToggle}" @sel-page="${this.onSelectToggle}"
@delete-page="${this.onDeletePage}" @delete-page="${this.onDeletePage}"
@@ -638,8 +630,8 @@ class Pages extends LitElement
class="${this.isSidebar ? 'sidebar' : ''}" class="${this.isSidebar ? 'sidebar' : ''}"
> >
</wr-page-entry> </wr-page-entry>
</div>` })}` : html`<p class="mobile-header">${this.getNoResultsMessage()}</p>`} </li>` })}` : html`<p class="mobile-header">${this.getNoResultsMessage()}</p>`}
</div> </ul>
`; `;
} }
@@ -740,7 +732,7 @@ class Pages extends LitElement
getNoResultsMessage() { getNoResultsMessage() {
if (!this.collInfo || !this.collInfo.pages.length) { if (!this.collInfo || !this.collInfo.pages.length) {
return html`No Pages defined this archive. Check out&nbsp;<a href="#view=resources">Page Resources</a>&nbsp;to search by URL.`; return html`<span class="fix-text-wrapping">No "Pages" are defined in this archive. <a href="#view=resources">Browse by URL</a>.</span>`;
} }
if (this.updatingSearch) { if (this.updatingSearch) {
@@ -751,8 +743,8 @@ class Pages extends LitElement
return "Searching..."; return "Searching...";
} }
if (!this.query) { if (this.query) {
return "No Pages Found. Try changing the search query."; return html `<span class="fix-text-wrapping">No matching pages found. Try changing the search query, or <a href="#view=resources">browse by URL</a>.</span>`;
} }
return "No Pages Found"; return "No Pages Found";
+14 -5
View File
@@ -112,6 +112,10 @@ class Replay extends LitElement
this.title = event.data.title; this.title = event.data.title;
} }
} }
if (this.title){
document.title = `Replay of ${this.title} | ReplayWeb.page`;
}
} }
waitForLoad() { waitForLoad() {
@@ -177,7 +181,7 @@ class Replay extends LitElement
flex-direction: column; flex-direction: column;
} }
nav.intro-panel.panel { div.intro-panel.panel {
min-width: 40%; min-width: 40%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -187,19 +191,23 @@ class Replay extends LitElement
} }
render() { render() {
const title = `Replay of ${this.title ? `${this.title}:` :``} ${this.url}`;
return html` return html`
<h1 id="replay-heading" class="is-sr-only">${title}</h1>
${this.iframeUrl ? html` ${this.iframeUrl ? html`
<iframe @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen <iframe @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
src="${this.iframeUrl}"></iframe> src="${this.iframeUrl}" title="${title}"></iframe>
` : html` ` : html`
<nav class="panel intro-panel"> <div class="panel intro-panel">
<p class="panel-heading">Replay Web Page</p> <p class="panel-heading">Replay Web Page</p>
<div class="panel-block"> <div class="panel-block">
<p>Enter a URL above to replay it from the web archive!</p> <p>Enter a URL above to replay it from the web archive!</p>
<p>(Check out the <a href="#view=pages">Pages</a> or <a href="#view=resources">Page Resources</a> to find URLs in this archive.)</p> <p>(Or, check out <a href="#view=pages">Pages</a> or <a href="#view=resources">URLs</a> to explore the contents of this archive.)</p>
</div>
</div> </div>
</nav>
`} `}
${this.isAuthable ? html` ${this.isAuthable ? html`
@@ -217,6 +225,7 @@ class Replay extends LitElement
</section> </section>
</div> </div>
</div> </div>
</div>
` : ``} ` : ``}
`; `;
} }
+2 -1
View File
@@ -144,7 +144,8 @@ class Sorter extends LitElement
</div> </div>
<button @click=${(e) => this.sortDesc = !this.sortDesc} class="button is-small"> <button @click=${(e) => this.sortDesc = !this.sortDesc} class="button is-small">
<span>Order:</span> <span>Order:</span>
<span class="icon"><fa-icon .svg=${this.sortDesc ? fasSortUp : fasSortDown}></span> <span class="is-sr-only">${this.sortDesc ? "Ascending" : "Descending"}</span>
<span class="icon"><fa-icon aria-hidden="true" .svg=${this.sortDesc ? fasSortUp : fasSortDown}></span>
</button>`; </button>`;
} }
} }
+3 -1
View File
@@ -245,7 +245,9 @@ class Story extends LitElement
const currListNum = this.currList; const currListNum = this.currList;
return html` return html`
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "2": "1"}">
Story for ${this.collInfo.title}
</div>
<div class="columns"> <div class="columns">
<div class="column sidebar is-one-fifth"> <div class="column sidebar is-one-fifth">
<aside class="menu"> <aside class="menu">
+36 -20
View File
@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'lit-element'; import { LitElement, html, css } from 'lit-element';
import { wrapCss } from './misc'; import { wrapCss, clickOnSpacebarPress } from './misc';
import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg'; import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg';
@@ -47,6 +47,7 @@ class URLResources extends LitElement
constructor() { constructor() {
super(); super();
this.collInfo = null; this.collInfo = null;
this.isSidebar = false;
this.currMime = ""; this.currMime = "";
this.query = ""; this.query = "";
@@ -69,6 +70,7 @@ class URLResources extends LitElement
static get properties() { static get properties() {
return { return {
collInfo: { type: Object }, collInfo: { type: Object },
isSidebar: { type: Boolean },
currMime: { type: String }, currMime: { type: String },
query: { type: String }, query: { type: String },
urlSearchType: { type: String }, urlSearchType: { type: String },
@@ -258,6 +260,12 @@ class URLResources extends LitElement
.columns { .columns {
margin: 0px; margin: 0px;
} }
thead {
margin-bottom: 24px;
}
table th:not([align]) {
text-align: left;
}
.result { .result {
border-bottom: 1px #dbdbdb solid; border-bottom: 1px #dbdbdb solid;
min-height: fit-content; min-height: fit-content;
@@ -266,6 +274,8 @@ class URLResources extends LitElement
border-bottom: 2px #dbdbdb solid; border-bottom: 2px #dbdbdb solid;
margin-right: 16px; margin-right: 16px;
min-height: fit-content; min-height: fit-content;
display: block;
width: 100%;
} }
.results-head a { .results-head a {
color: black; color: black;
@@ -298,6 +308,9 @@ class URLResources extends LitElement
render() { render() {
return html` return html`
<div role="heading" aria-level="${this.isSidebar ? "2": "1"}" class="is-sr-only">URLs in ${this.collInfo.title}</div>
<div role="heading" aria-level="${this.isSidebar ? "3": "2"}" class="is-sr-only">Search and Filter</div>
<div class="notification level is-marginless"> <div class="notification level is-marginless">
<div class="level-left flex-auto"> <div class="level-left flex-auto">
<div class="level-item flex-auto"> <div class="level-item flex-auto">
@@ -325,14 +338,13 @@ class URLResources extends LitElement
<label class="radio has-text-left"><input type="radio" name="urltype" value="" ?checked="${this.urlSearchType === 'contains'}" @click="${this.onClickUrlType}">&nbsp;Contains</label> <label class="radio has-text-left"><input type="radio" name="urltype" value="" ?checked="${this.urlSearchType === 'contains'}" @click="${this.onClickUrlType}">&nbsp;Contains</label>
<label class="radio has-text-left"><input type="radio" name="urltype" value="prefix" ?checked="${this.urlSearchType === 'prefix'}" @click="${this.onClickUrlType}">&nbsp;Prefix</label> <label class="radio has-text-left"><input type="radio" name="urltype" value="prefix" ?checked="${this.urlSearchType === 'prefix'}" @click="${this.onClickUrlType}">&nbsp;Prefix</label>
<label class="radio has-text-left"><input type="radio" name="urltype" value="exact" ?checked="${this.urlSearchType === 'exact'}" @click="${this.onClickUrlType}">&nbsp;Exact</label> <label class="radio has-text-left"><input type="radio" name="urltype" value="exact" ?checked="${this.urlSearchType === 'exact'}" @click="${this.onClickUrlType}">&nbsp;Exact</label>
<span class="num-results" is-pulled-right">${this.filteredResults.length} Result(s)</span> <span id="num-results" class="num-results" is-pulled-right" aria-live="polite" aria-atomic="true">${this.filteredResults.length} Result(s)</span>
</div> </div>
</div> </div>
</div> </div>
<div class="sort-header is-hidden-tablet"> <div class="sort-header is-hidden-tablet">
<wr-sorter id="urls" <wr-sorter id="urls"
defaultKey="url"
defaultKey="${this.sortKey ? this.sortKey : "url"}" defaultKey="${this.sortKey ? this.sortKey : "url"}"
?defaultDesc="${this.sortDesc !== null ? this.sortDesc : false}" ?defaultDesc="${this.sortDesc !== null ? this.sortDesc : false}"
.sortKeys="${URLResources.sortKeys}" .sortKeys="${URLResources.sortKeys}"
@@ -341,26 +353,30 @@ class URLResources extends LitElement
</wr-sorter> </wr-sorter>
</div> </div>
<div class="all-results"> <div role="heading" aria-level="${this.isSidebar ? "3": "2"}" id="results-heading" class="is-sr-only">Results</div>
<div class="columns results-head has-text-weight-bold">
<a @click="${this.onSort}" data-key="url" class="column col-url is-6 is-hidden-mobile ${this.sortKey === "url" ? (this.sortDesc ? "desc" : "asc") : ''}">URL</a>
<a @click="${this.onSort}" data-key="ts" class="column col-ts is-2 is-hidden-mobile ${this.sortKey === "ts" ? (this.sortDesc ? "desc" : "asc") : ''}">Date</a>
<a @click="${this.onSort}" data-key="mime" class="column col-mime is-3 is-hidden-mobile ${this.sortKey === "mime" ? (this.sortDesc ? "desc" : "asc") : ''}">Mime Type</a>
<a @click="${this.onSort}" data-key="status" class="column col-status is-1 is-hidden-mobile ${this.sortKey === "status" ? (this.sortDesc ? "desc" : "asc") : ''}">Status</a>
</div>
<div class="main-scroll" @scroll="${this.onScroll}"> <table class="all-results" aria-labelledby="results-heading num-results">
<thead>
<tr class="columns results-head has-text-weight-bold">
<th scope="col" class="column col-url is-6 is-hidden-mobile"><a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="url" class="${this.sortKey === "url" ? (this.sortDesc ? "desc" : "asc") : ''}">URL</a></th>
<th scope="col" class="column col-ts is-2 is-hidden-mobile"><a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="ts" class="${this.sortKey === "ts" ? (this.sortDesc ? "desc" : "asc") : ''}">Date</a></th>
<th scope="col" class="column col-mime is-3 is-hidden-mobile"><a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="mime" class="${this.sortKey === "mime" ? (this.sortDesc ? "desc" : "asc") : ''}">Mime Type</a></th>
<th scope="col" class="column col-status is-1 is-hidden-mobile"><a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="status" class="${this.sortKey === "status" ? (this.sortDesc ? "desc" : "asc") : ''}">Status</a></th>
</tr>
</thead>
<tbody class="main-scroll" @scroll="${this.onScroll}">
${this.sortedResults.length ? ${this.sortedResults.length ?
this.sortedResults.map((result) => html` this.sortedResults.map((result) => html`
<div class="columns result"> <tr class="columns result">
<div class="column col-url is-6"><p class="minihead is-hidden-tablet">URL</p><a @click="${this.onReplay}" data-url="${result.url}" data-ts="${result.ts}" href="#">${result.url}</a></div> <td class="column col-url is-6"><p class="minihead is-hidden-tablet">URL</p><a @click="${this.onReplay}" data-url="${result.url}" data-ts="${result.ts}" href="#">${result.url}</a></td>
<div class="column col-ts is-2"><p class="minihead is-hidden-tablet">Date</p>${new Date(result.date).toLocaleString()}</div> <td class="column col-ts is-2"><p class="minihead is-hidden-tablet">Date</p>${new Date(result.date).toLocaleString()}</td>
<div class="column col-mime is-3"><p class="minihead is-hidden-tablet">Mime Type</p>${result.mime}</div> <td class="column col-mime is-3"><p class="minihead is-hidden-tablet">Mime Type</p>${result.mime}</td>
<div class="column col-status is-1"><p class="minihead is-hidden-tablet">Status</p>${result.status}</div> <td class="column col-status is-1"><p class="minihead is-hidden-tablet">Status</p>${result.status}</td>
</div> </tr>
`) : html`<div class="section"><i>No Results Found.</i></div>`} `) : html`<tr class="section"><td colspan="4"><i>No Results Found.</i></td></tr>`}
</div> </tbody>
</div> </table>
`; `;
} }
+3 -3
View File
File diff suppressed because one or more lines are too long
+279 -213
View File
File diff suppressed because one or more lines are too long