openvibers/openvibe.trade Commits
Trade: data tables that scroll on a phone are keyboard-focusable (#59)
Changes
@@@ -56,5 +56,6 @@ form.inline button { margin: 0; padding: 2px 10px; font-size: .85rem; }@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }@media (max-width: 700px) {h1 { font-size: 1.5rem; }++ /* A table scrolls sideways here, so each has tabindex="0" (axe scrollable-region-focusable). */table.data { display: block; overflow-x: auto; }}@@@ -83,7 +83,7 @@ ${first ? raw(tradeShowcase()) : html`<h1>Sourced market information</h1>`}<button type="submit">Find</button></form><h2>Instruments</h2>--${page.instruments.length ? html`<table class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Kind</th>${exchanges ? html`<th scope="col">Exchange</th>` : ''}<th scope="col">Latest</th></tr></thead><tbody>${rows}</tbody></table>++${page.instruments.length ? html`<table tabindex="0" class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Kind</th>${exchanges ? html`<th scope="col">Exchange</th>` : ''}<th scope="col">Latest</th></tr></thead><tbody>${rows}</tbody></table>${raw(paginationHtml(pager))}`: html`<p class="empty">No instruments have been added yet.</p>`}<h2>Recent documents</h2>@@@ -120,7 +120,7 @@ function observationsTable(instrument, observations) {<td>${when(o.retrieved_at)}</td><td>${staleBadge(o.freshness)}</td></tr>`);-- return html`<table class="data observations">++ return html`<table tabindex="0" class="data observations"><caption>Latest observation per metric. Values are exactly as the source stated them.</caption><thead><tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">Observed</th><th scope="col">Source</th><th scope="col">Retrieved</th><th scope="col">Freshness</th></tr></thead><tbody>${rows}</tbody></table>`;@@@ -135,7 +135,7 @@ function sourcesTable(list, { caption }) {<td>${s.stale_after_sec ? `${Math.round(s.stale_after_sec / 60)} min` : ''}</td><td>${s.upstream_status || html`<span class="muted">not reported</span>`}</td></tr>`);-- return html`<table class="data sources"><caption>${caption}</caption>++ return html`<table tabindex="0" class="data sources"><caption>${caption}</caption><thead><tr><th scope="col">Source</th><th scope="col">Freshness</th><th scope="col">Last successful fetch</th><th scope="col">Stale after</th><th scope="col">Sources reports</th></tr></thead><tbody>${rows}</tbody></table>`;}@@@ -249,7 +249,7 @@ function watchlistsPage({ viewer, lists, rules, deliveries, csrf, loginHref, not}const listHtml = lists.map((w) => html`<section class="card" aria-label="${w.name}"><h2>${w.name}</h2>--${w.items.length ? html`<table class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Note</th><th scope="col"></th></tr></thead><tbody>++${w.items.length ? html`<table tabindex="0" class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Note</th><th scope="col"></th></tr></thead><tbody>${w.items.map((it) => html`<tr><td><a href="${new URL(it.instrument.url).pathname}">${it.instrument.symbol}</a></td><td>${it.instrument.name}</td><td>${it.note || ''}</td><td><form method="post" action="/watchlists/item/remove" class="inline">${csrfField(csrf)}<input type="hidden" name="watchlist_id" value="${w.id}"><input type="hidden" name="symbol" value="${it.instrument.symbol}"><button type="submit">Remove</button></form></td></tr>`)}</tbody></table>` : html`<p class="empty">Empty. Add instruments from their pages or below.</p>`}@@@ -274,7 +274,7 @@ ${listHtml}<label for="wl-name">New watchlist</label><input id="wl-name" name="name" maxlength="80" required><button type="submit">Create</button></form><h2>Your alerts</h2>--${rules.length ? html`<table class="data"><thead><tr><th scope="col">Instrument</th><th scope="col">Condition</th><th scope="col">Created</th><th scope="col"></th></tr></thead><tbody>${ruleRows}</tbody></table>`++${rules.length ? html`<table tabindex="0" class="data"><thead><tr><th scope="col">Instrument</th><th scope="col">Condition</th><th scope="col">Created</th><th scope="col"></th></tr></thead><tbody>${ruleRows}</tbody></table>`: html`<p class="empty">No alerts. Create one from an instrument’s page.</p>`}<h2>Recent alert deliveries</h2>${deliveries.length ? html`<ul>${delRows}</ul>` : html`<p class="empty">None yet.</p>`}`;@@@ -83,6 +83,8 @@ const HOUR = 3600e3;assert.ok(json.observations.find((o) => o.metric === 'price.close').value === '10.60', 'last real value kept, labelled stale');const sourcesPage = await t.get('/sources');assert.ok(/failing since/.test(sourcesPage.text), 'the sources page says the sync is failing');++ // On a phone the table scrolls sideways (trade.css), so it is a keyboard stop (axe scrollable-region-focusable).++ assert.ok(!/<table(?![^>]*tabindex)[^>]*class="data/.test(sourcesPage.text), 'every data table is focusable');const ready = (await t.get('/api/ready')).json();assert.ok(JSON.stringify(ready).includes('last Sources sync failed'));t.sources.setDown(false);
@@ -56,5 +56,6 @@ form.inline button { margin: 0; padding: 2px 10px; font-size: .85rem; }
@@ -56,5 +56,6 @@ form.inline button { margin: 0; padding: 2px 10px; font-size: .85rem; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
@media (max-width: 700px) {
@media (max-width: 700px) {
h1 { font-size: 1.5rem; }
h1 { font-size: 1.5rem; }
+ /* A table scrolls sideways here, so each has tabindex="0" (axe scrollable-region-focusable). */
table.data { display: block; overflow-x: auto; }
table.data { display: block; overflow-x: auto; }
}
}
@@ -83,7 +83,7 @@ ${first ? raw(tradeShowcase()) : html`<h1>Sourced market information</h1>`}
@@ -83,7 +83,7 @@ ${first ? raw(tradeShowcase()) : html`<h1>Sourced market information</h1>`}
<button type="submit">Find</button>
<button type="submit">Find</button>
</form>
</form>
<h2>Instruments</h2>
<h2>Instruments</h2>
-${page.instruments.length ? html`<table class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Kind</th>${exchanges ? html`<th scope="col">Exchange</th>` : ''}<th scope="col">Latest</th></tr></thead><tbody>${rows}</tbody></table>
+${page.instruments.length ? html`<table tabindex="0" class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Kind</th>${exchanges ? html`<th scope="col">Exchange</th>` : ''}<th scope="col">Latest</th></tr></thead><tbody>${rows}</tbody></table>
${raw(paginationHtml(pager))}`
${raw(paginationHtml(pager))}`
: html`<p class="empty">No instruments have been added yet.</p>`}
: html`<p class="empty">No instruments have been added yet.</p>`}
<h2>Recent documents</h2>
<h2>Recent documents</h2>
@@ -120,7 +120,7 @@ function observationsTable(instrument, observations) {
@@ -120,7 +120,7 @@ function observationsTable(instrument, observations) {
<td>${when(o.retrieved_at)}</td>
<td>${when(o.retrieved_at)}</td>
<td>${staleBadge(o.freshness)}</td>
<td>${staleBadge(o.freshness)}</td>
</tr>`);
</tr>`);
- return html`<table class="data observations">
+ return html`<table tabindex="0" class="data observations">
<caption>Latest observation per metric. Values are exactly as the source stated them.</caption>
<caption>Latest observation per metric. Values are exactly as the source stated them.</caption>
<thead><tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">Observed</th><th scope="col">Source</th><th scope="col">Retrieved</th><th scope="col">Freshness</th></tr></thead>
<thead><tr><th scope="col">Metric</th><th scope="col">Value</th><th scope="col">Observed</th><th scope="col">Source</th><th scope="col">Retrieved</th><th scope="col">Freshness</th></tr></thead>
<tbody>${rows}</tbody></table>`;
<tbody>${rows}</tbody></table>`;
@@ -135,7 +135,7 @@ function sourcesTable(list, { caption }) {
@@ -135,7 +135,7 @@ function sourcesTable(list, { caption }) {
<td>${s.stale_after_sec ? `${Math.round(s.stale_after_sec / 60)} min` : ''}</td>
<td>${s.stale_after_sec ? `${Math.round(s.stale_after_sec / 60)} min` : ''}</td>
<td>${s.upstream_status || html`<span class="muted">not reported</span>`}</td>
<td>${s.upstream_status || html`<span class="muted">not reported</span>`}</td>
</tr>`);
</tr>`);
- return html`<table class="data sources"><caption>${caption}</caption>
+ return html`<table tabindex="0" class="data sources"><caption>${caption}</caption>
<thead><tr><th scope="col">Source</th><th scope="col">Freshness</th><th scope="col">Last successful fetch</th><th scope="col">Stale after</th><th scope="col">Sources reports</th></tr></thead>
<thead><tr><th scope="col">Source</th><th scope="col">Freshness</th><th scope="col">Last successful fetch</th><th scope="col">Stale after</th><th scope="col">Sources reports</th></tr></thead>
<tbody>${rows}</tbody></table>`;
<tbody>${rows}</tbody></table>`;
}
}
@@ -249,7 +249,7 @@ function watchlistsPage({ viewer, lists, rules, deliveries, csrf, loginHref, not
@@ -249,7 +249,7 @@ function watchlistsPage({ viewer, lists, rules, deliveries, csrf, loginHref, not
}
}
const listHtml = lists.map((w) => html`<section class="card" aria-label="${w.name}">
const listHtml = lists.map((w) => html`<section class="card" aria-label="${w.name}">
<h2>${w.name}</h2>
<h2>${w.name}</h2>
-${w.items.length ? html`<table class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Note</th><th scope="col"></th></tr></thead><tbody>
+${w.items.length ? html`<table tabindex="0" class="data"><thead><tr><th scope="col">Symbol</th><th scope="col">Name</th><th scope="col">Note</th><th scope="col"></th></tr></thead><tbody>
${w.items.map((it) => html`<tr><td><a href="${new URL(it.instrument.url).pathname}">${it.instrument.symbol}</a></td><td>${it.instrument.name}</td><td>${it.note || ''}</td>
${w.items.map((it) => html`<tr><td><a href="${new URL(it.instrument.url).pathname}">${it.instrument.symbol}</a></td><td>${it.instrument.name}</td><td>${it.note || ''}</td>
<td><form method="post" action="/watchlists/item/remove" class="inline">${csrfField(csrf)}<input type="hidden" name="watchlist_id" value="${w.id}"><input type="hidden" name="symbol" value="${it.instrument.symbol}"><button type="submit">Remove</button></form></td></tr>`)}
<td><form method="post" action="/watchlists/item/remove" class="inline">${csrfField(csrf)}<input type="hidden" name="watchlist_id" value="${w.id}"><input type="hidden" name="symbol" value="${it.instrument.symbol}"><button type="submit">Remove</button></form></td></tr>`)}
</tbody></table>` : html`<p class="empty">Empty. Add instruments from their pages or below.</p>`}
</tbody></table>` : html`<p class="empty">Empty. Add instruments from their pages or below.</p>`}
@@ -274,7 +274,7 @@ ${listHtml}
@@ -274,7 +274,7 @@ ${listHtml}
<label for="wl-name">New watchlist</label><input id="wl-name" name="name" maxlength="80" required>
<label for="wl-name">New watchlist</label><input id="wl-name" name="name" maxlength="80" required>
<button type="submit">Create</button></form>
<button type="submit">Create</button></form>
<h2>Your alerts</h2>
<h2>Your alerts</h2>
-${rules.length ? html`<table class="data"><thead><tr><th scope="col">Instrument</th><th scope="col">Condition</th><th scope="col">Created</th><th scope="col"></th></tr></thead><tbody>${ruleRows}</tbody></table>`
+${rules.length ? html`<table tabindex="0" class="data"><thead><tr><th scope="col">Instrument</th><th scope="col">Condition</th><th scope="col">Created</th><th scope="col"></th></tr></thead><tbody>${ruleRows}</tbody></table>`
: html`<p class="empty">No alerts. Create one from an instrument’s page.</p>`}
: html`<p class="empty">No alerts. Create one from an instrument’s page.</p>`}
<h2>Recent alert deliveries</h2>
<h2>Recent alert deliveries</h2>
${deliveries.length ? html`<ul>${delRows}</ul>` : html`<p class="empty">None yet.</p>`}`;
${deliveries.length ? html`<ul>${delRows}</ul>` : html`<p class="empty">None yet.</p>`}`;
@@ -83,6 +83,8 @@ const HOUR = 3600e3;
@@ -83,6 +83,8 @@ const HOUR = 3600e3;
assert.ok(json.observations.find((o) => o.metric === 'price.close').value === '10.60', 'last real value kept, labelled stale');
assert.ok(json.observations.find((o) => o.metric === 'price.close').value === '10.60', 'last real value kept, labelled stale');
const sourcesPage = await t.get('/sources');
const sourcesPage = await t.get('/sources');
assert.ok(/failing since/.test(sourcesPage.text), 'the sources page says the sync is failing');
assert.ok(/failing since/.test(sourcesPage.text), 'the sources page says the sync is failing');
+ // On a phone the table scrolls sideways (trade.css), so it is a keyboard stop (axe scrollable-region-focusable).
+ assert.ok(!/<table(?![^>]*tabindex)[^>]*class="data/.test(sourcesPage.text), 'every data table is focusable');
const ready = (await t.get('/api/ready')).json();
const ready = (await t.get('/api/ready')).json();
assert.ok(JSON.stringify(ready).includes('last Sources sync failed'));
assert.ok(JSON.stringify(ready).includes('last Sources sync failed'));
t.sources.setDown(false);
t.sources.setDown(false);