diff --git a/assets/main.scss b/assets/main.scss index 4f8feb9..4b92afe 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -3,6 +3,7 @@ /*$wr-green: #64e986;*/ $wr-green: #55be6f; $wr-yellow: #ffd975; +$wr-dark-yellow: #ffc42e; $wr-light-blue: #43daff; $wr-med-blue: #27a0ff; $wr-dark-blue: #4876ff; @@ -22,6 +23,76 @@ $dark: $wr-dark-green; $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 { flex: 1; scroll-behavior: smooth; diff --git a/src/appmain.js b/src/appmain.js index 590b51b..8c0d905 100644 --- a/src/appmain.js +++ b/src/appmain.js @@ -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` -

Skipped

+ ` : ''} + + ${this.sourceUrl ? html` + + + ` : html` + + + ${!IS_APP ? html` +

Explore and Replay Interactive Archived Webpages Directly in your Browser. (See Examples)

+ ` : ``} + +
+ `} + + ${this.showTerms ? html` + + + ` : ``} ` } 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 }; \ No newline at end of file +export { App }; diff --git a/src/chooser.js b/src/chooser.js index f57ba01..3959d38 100644 --- a/src/chooser.js +++ b/src/chooser.js @@ -45,9 +45,9 @@ class Chooser extends LitElement loadInfo.noCache = this.file.path !== undefined; loadInfo.name = this.fileDisplayName; } - + this.dispatchEvent(new CustomEvent("load-start", {detail: loadInfo})); - + return false; } @@ -111,8 +111,8 @@ class Chooser extends LitElement render() { return html`
- +
`; } } customElements.define("wr-chooser", Chooser); -export { Chooser }; \ No newline at end of file +export { Chooser }; diff --git a/src/coll-index.js b/src/coll-index.js index 5b508e7..8a9fb39 100644 --- a/src/coll-index.js +++ b/src/coll-index.js @@ -94,7 +94,7 @@ class CollIndex extends LitElement const resp = await fetch("./wabac/api/index"); try { const json = await resp.json(); - this.colls = json.colls.map((coll) => { + this.colls = json.colls.map((coll) => { coll.title = coll.title || coll.filename; return coll; }); @@ -147,14 +147,14 @@ class CollIndex extends LitElement .no-top-padding { padding-top: 1.0em; } - button.is-loading { + .is-loading { line-height: 1.5em; height: 1.5em; border: 0px; background-color: transparent !important; width: auto; } - nav.panel.is-light { + div.panel.is-light { margin-bottom: 2em; } @@ -251,14 +251,14 @@ class CollIndex extends LitElement
- this.hideHeader = !this.hideHeader} class="collapse button is-small"> +
- +
`; } @@ -325,4 +325,4 @@ class CollIndex extends LitElement customElements.define("wr-coll-index", CollIndex); -export { CollIndex }; \ No newline at end of file +export { CollIndex }; diff --git a/src/coll.js b/src/coll.js index 6409bfa..aa76d10 100644 --- a/src/coll.js +++ b/src/coll.js @@ -1,5 +1,5 @@ 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'; @@ -84,7 +84,7 @@ class Coll extends LitElement url: { type: String }, ts: { type: String }, - + isFullscreen: { type: Boolean }, menuActive: { type: Boolean }, @@ -138,6 +138,10 @@ class Coll extends LitElement this._replaceLoc = false; } else { window.location.hash = this._locationHash; + const replay = this.renderRoot.querySelector("wr-coll-replay"); + if (replay) { + replay.focus(); + } } if (this.embed && window.parent !== window) { window.parent.postMessage(this.tabData, '*'); @@ -290,7 +294,6 @@ class Coll extends LitElement updateTabData(data, replaceLoc = false, merge = false) { this.tabData = {...this.tabData, ...data}; - if (this.tabData.url) { this.url = this.tabData.url || ""; } @@ -465,7 +468,7 @@ class Coll extends LitElement cursor: col-resize; } - wr-coll-replay { + main, wr-coll-replay { width: 100%; } @@ -483,12 +486,13 @@ class Coll extends LitElement border-bottom: 0px; } - .sidebar-nav span { + .sidebar-nav span.nav-hover { font-size: smaller; display: none; } - .sidebar-nav:hover span { + .sidebar-nav:hover span.nav-hover, + .sidebar-nav:focus-within span.nav-hover { display: initial; color: rgb(72, 118, 255); } @@ -509,6 +513,16 @@ class Coll extends LitElement 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 isSidebar = isReplay && this.showSidebar; + if (!isReplay){ + document.title = `Browse Contents | ReplayWeb.page`; + } + if (this.collInfo && !this.collInfo.coll) { return html` - + ${isReplay ? html` - - + + ` : ``} - `; } else { return html``; @@ -557,46 +576,49 @@ class Coll extends LitElement // } return html` -
+
`; + `; } renderLocationBar() { @@ -609,84 +631,91 @@ class Coll extends LitElement const isReplay = !!this.tabData.url; return html` -
+ +

Skipped

`; } dragStart(event) { @@ -737,7 +766,9 @@ class Coll extends LitElement currList="${this.tabData.currList || 0}" @coll-tab-nav="${this.onCollTabNav}" id="story" .isSidebar="${isSidebar}" - class="${isStory ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}"> + class="${isStory ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}" + role="${isSidebar ? '' : 'main'}" + > ` : ''} + class="is-paddingless ${isResources ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}" + role="${isSidebar ? '' : 'main'}" + > + class="${isPages ? '' : 'is-hidden'} ${isSidebar ? 'sidebar' : ''}" + role="${isSidebar ? '' : 'main'}" + > `; } + 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) { if (event.key === "Esc" || event.key === "Escape") { event.target.value = this.url; @@ -782,7 +824,8 @@ class Coll extends LitElement } } - onFullscreenToggle() { + onFullscreenToggle(event) { + event.preventDefault(); this.menuActive = false; if (!this.isFullscreen) { this.requestFullscreen(); @@ -791,12 +834,14 @@ class Coll extends LitElement } } - onGoBack() { + onGoBack(event) { + event.preventDefault(); this.menuActive = false; window.history.back(); } - onGoForward() { + onGoForward(event) { + event.preventDefault(); this.menuActive = false; window.history.forward(); } @@ -827,7 +872,7 @@ class Coll extends LitElement this.reauthWait = (async () => { const headers = event.detail.headers; - const resp = await fetch(`${this.collInfo.apiPrefix}/updateAuth`, { + const resp = await fetch(`${this.collInfo.apiPrefix}/updateAuth`, { method: 'POST', body: JSON.stringify({headers}) }); @@ -921,4 +966,4 @@ class Coll extends LitElement customElements.define("wr-coll", Coll); -export { Coll }; \ No newline at end of file +export { Coll }; diff --git a/src/embed.js b/src/embed.js index 10fd8a6..12d7320 100644 --- a/src/embed.js +++ b/src/embed.js @@ -113,10 +113,10 @@ class Embed extends LitElement } updated(changedProperties) { - if (changedProperties.has("url") || + if (changedProperties.has("url") || changedProperties.has("ts") || changedProperties.has("query") || - changedProperties.has("view") || + changedProperties.has("view") || changedProperties.has("source")) { this.embed = this.embed || "default"; @@ -128,7 +128,7 @@ class Embed extends LitElement basePageUrl: window.location.href.split("#")[0], embed: this.embed, }).toString(); - + this.hashString = new URLSearchParams({ url: this.url, ts: this.ts, @@ -168,13 +168,16 @@ class Embed extends LitElement render() { return html` ${this.paramString && this.hashString && this.swInited ? html` - + + ` : html``} ${this.noSW ? html`
- +
Sorry, ReplayWeb.page won't work in this browser as Service Workers are not supported. @@ -214,4 +217,4 @@ async function main() { } -main(); \ No newline at end of file +main(); diff --git a/src/misc.js b/src/misc.js index ac6e81b..cd54cf7 100644 --- a/src/misc.js +++ b/src/misc.js @@ -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; +// =========================================================================== +// Buttons are expected to respond to both enter/return and spacebar. +// If using `` with `role='button'`, assign this handler to keyup. +function clickOnSpacebarPress(event) { + if (event.key == " ") { + event.preventDefault(); + event.target.click(); + } +} + + // =========================================================================== class FaIcon extends LitElement { @@ -117,4 +128,4 @@ class AnimLogo extends FaIcon customElements.define("fa-icon", FaIcon); customElements.define("wr-anim-logo", AnimLogo); -export { wrapCss, IS_APP, rwpLogo, FaIcon, AnimLogo }; \ No newline at end of file +export { wrapCss, IS_APP, clickOnSpacebarPress, rwpLogo, FaIcon, AnimLogo }; diff --git a/src/pageentry.js b/src/pageentry.js index c6d3c65..12cf8cc 100644 --- a/src/pageentry.js +++ b/src/pageentry.js @@ -23,6 +23,7 @@ class PageEntry extends LitElement this.iconValid = false; this.index = 0; this.isCurrent = false; + this.isSidebar = false; this.timestamp = ""; this.date = null; @@ -40,6 +41,7 @@ class PageEntry extends LitElement iconValid: { type: Boolean }, index: { type: Number }, isCurrent: { type: Boolean }, + isSidebar: { type: Boolean }, timestamp: { type: String }, date: { type: Object } } @@ -75,6 +77,15 @@ class PageEntry extends LitElement 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 { width: 24px !important; height: 24px !important; @@ -189,7 +200,7 @@ class PageEntry extends LitElement
${date ? date.toLocaleTimeString() : ""}
- + ${this.editable ? html` ${!this.deleting ? html` ` : html` diff --git a/src/pages.js b/src/pages.js index 810e59e..a62948f 100644 --- a/src/pages.js +++ b/src/pages.js @@ -1,7 +1,7 @@ "use strict"; import { LitElement, html, css, unsafeCSS } from 'lit-element'; -import { wrapCss } from './misc'; +import { wrapCss, clickOnSpacebarPress } from './misc'; import ndjson from 'fetch-ndjson'; import FlexSearch from 'flexsearch'; @@ -30,7 +30,7 @@ class Pages extends LitElement this.active = false; this.editable = false; this.changeNeeded = false; - + this.selectedPages = new Set(); this.menuActive = false; @@ -265,6 +265,10 @@ class Pages extends LitElement box-sizing: border-box !important; } + div[role="main"], #contents div[role="complementary"] { + height: 100%; + } + .main.columns { width: 100%; justify-self: stretch; @@ -343,13 +347,12 @@ class Pages extends LitElement max-height: 100%; height: 100%; } - .index-bar-menu { max-height: 100%; overflow-y: auto; } } - + @media screen and (max-width: 768px) { ${Pages.sidebarStyles()} } @@ -382,7 +385,7 @@ class Pages extends LitElement padding-bottom: 1.0em; min-height: 0px; } - + .current { /*background-color: rgb(207, 243, 255);*/ } @@ -467,64 +470,52 @@ class Pages extends LitElement const currList = this.currList; return html` +
+ Pages in ${this.collInfo.title} +
+
+ `; } @@ -532,26 +523,26 @@ class Pages extends LitElement return html` ` : ``} `; @@ -224,4 +233,4 @@ class Replay extends LitElement customElements.define("wr-coll-replay", Replay); -export { Replay }; \ No newline at end of file +export { Replay }; diff --git a/src/sorter.js b/src/sorter.js index e9ddb16..27ecc5c 100644 --- a/src/sorter.js +++ b/src/sorter.js @@ -144,11 +144,12 @@ class Sorter extends LitElement
`; } } customElements.define("wr-sorter", Sorter); -export { Sorter }; \ No newline at end of file +export { Sorter }; diff --git a/src/story.js b/src/story.js index 14952dc..ebbd483 100644 --- a/src/story.js +++ b/src/story.js @@ -245,7 +245,9 @@ class Story extends LitElement const currListNum = this.currList; return html` - +
+ Story for ${this.collInfo.title} +