@mosaicajs/components 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/aggregate.d.ts +4 -10
- package/dist/aggregate.js +7 -13
- package/dist/artifact-card.d.ts +21 -7
- package/dist/artifact-card.js +136 -79
- package/dist/base.d.ts +44 -5
- package/dist/base.js +54 -5
- package/dist/cluster-filter.d.ts +29 -25
- package/dist/cluster-filter.js +123 -106
- package/dist/colour-editor.d.ts +15 -4
- package/dist/colour-editor.js +107 -34
- package/dist/colour-picker.d.ts +12 -5
- package/dist/colour-picker.js +119 -32
- package/dist/display.js +77 -9
- package/dist/events.d.ts +21 -7
- package/dist/explorer.d.ts +116 -37
- package/dist/explorer.js +476 -210
- package/dist/field-card.d.ts +304 -48
- package/dist/field-card.js +1364 -244
- package/dist/filter-panel.d.ts +54 -10
- package/dist/filter-panel.js +242 -42
- package/dist/filter.d.ts +22 -4
- package/dist/filter.js +113 -34
- package/dist/hierarchy.d.ts +68 -45
- package/dist/hierarchy.js +320 -243
- package/dist/item-card.d.ts +47 -16
- package/dist/item-card.js +241 -108
- package/dist/key-picker.d.ts +2 -1
- package/dist/key-picker.js +4 -3
- package/dist/layer-picker.d.ts +4 -2
- package/dist/layer-picker.js +16 -7
- package/dist/lineage.d.ts +132 -0
- package/dist/lineage.js +307 -0
- package/dist/map.d.ts +135 -24
- package/dist/map.js +387 -117
- package/dist/mosaica-components.js +1837 -833
- package/dist/mosaica-components.js.sri +1 -1
- package/dist/mosaica-components.json +2 -2
- package/dist/parts.d.ts +4 -4
- package/dist/parts.js +18 -4
- package/dist/paths.d.ts +34 -12
- package/dist/paths.js +84 -46
- package/dist/selection.d.ts +22 -3
- package/dist/selection.js +125 -59
- package/dist/states.d.ts +72 -0
- package/dist/states.js +135 -26
- package/dist/status.d.ts +63 -5
- package/dist/status.js +215 -36
- package/dist/tokens.d.ts +8 -4
- package/dist/tokens.js +144 -14
- package/dist/view-picker.js +5 -0
- package/package.json +4 -3
package/dist/selection.js
CHANGED
|
@@ -34,18 +34,19 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
34
34
|
};
|
|
35
35
|
import { css, html, nothing } from 'lit';
|
|
36
36
|
import { property } from 'lit/decorators.js';
|
|
37
|
+
import { formatMasked } from '@mosaicajs/client';
|
|
37
38
|
import { isBusy, isUnanswered } from '@mosaicajs/client/internal';
|
|
38
39
|
import { MosaicaElement, emit, idString, shapeDetail } from './base.js';
|
|
39
40
|
import { attachContextRoot, defineOnce } from './define.js';
|
|
40
41
|
import { icon } from './icons.js';
|
|
41
|
-
import { reasonOf,
|
|
42
|
+
import { ActionFocus, UNSAID, liveLine, reasonOf, refusedLine, stateLine, stateOf } from './states.js';
|
|
42
43
|
import { chrome, tokens } from './tokens.js';
|
|
43
44
|
import './count.js';
|
|
44
45
|
/**
|
|
45
46
|
* Use the selection card beside a map so that viewers can see what is inside the region they have
|
|
46
47
|
* drawn. It reads the store's selected region, or the `region` you set. It counts the items inside
|
|
47
48
|
* that the filters match, the items the viewer may see there and the marks drawn there. The drawn
|
|
48
|
-
* marks are listed below the counts,
|
|
49
|
+
* marks are listed below the counts, each a button that picks it.
|
|
49
50
|
*
|
|
50
51
|
* The region acts as a filter, which means the map and every other count narrow to it once it
|
|
51
52
|
* settles. Outside flips it to everything outside the shape, and Clear removes it. The counts are
|
|
@@ -53,6 +54,12 @@ import './count.js';
|
|
|
53
54
|
* says when that is so. Export and Save as artifact are shown disabled because they are not
|
|
54
55
|
* available yet.
|
|
55
56
|
*
|
|
57
|
+
* Screen readers announce the state line as it changes: that the selection is being counted, a
|
|
58
|
+
* refusal, and, once, when the query has finished loading, the matched and visible counts with
|
|
59
|
+
* whether they are exact for the shape. The counts and that verdict are read out and not drawn on
|
|
60
|
+
* the line, since the figures are below it. With `quiet`, the card shows the same and announces
|
|
61
|
+
* nothing.
|
|
62
|
+
*
|
|
56
63
|
* @summary A card showing the counts and marks inside the selected region.
|
|
57
64
|
* @tagname mosaica-selection
|
|
58
65
|
* @category Elements
|
|
@@ -71,7 +78,7 @@ import './count.js';
|
|
|
71
78
|
* @csspart count-visible - The `<mosaica-count>` of items the viewer may see inside (or outside).
|
|
72
79
|
* @csspart label - The heading above the list.
|
|
73
80
|
* @csspart items - The list of drawn marks inside.
|
|
74
|
-
* @csspart item - One drawn mark, by `mosaica_id`.
|
|
81
|
+
* @csspart item - One drawn mark's row, holding a button named by its `mosaica_id`.
|
|
75
82
|
* @csspart actions - The action buttons.
|
|
76
83
|
* @csspart action - One action button.
|
|
77
84
|
*/
|
|
@@ -80,11 +87,16 @@ let MosaicaSelection = (() => {
|
|
|
80
87
|
let _region_decorators;
|
|
81
88
|
let _region_initializers = [];
|
|
82
89
|
let _region_extraInitializers = [];
|
|
90
|
+
let _quiet_decorators;
|
|
91
|
+
let _quiet_initializers = [];
|
|
92
|
+
let _quiet_extraInitializers = [];
|
|
83
93
|
return class MosaicaSelection extends _classSuper {
|
|
84
94
|
static {
|
|
85
95
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
86
96
|
_region_decorators = [property({ attribute: false })];
|
|
97
|
+
_quiet_decorators = [property({ type: Boolean, reflect: true })];
|
|
87
98
|
__esDecorate(this, null, _region_decorators, { kind: "accessor", name: "region", static: false, private: false, access: { has: obj => "region" in obj, get: obj => obj.region, set: (obj, value) => { obj.region = value; } }, metadata: _metadata }, _region_initializers, _region_extraInitializers);
|
|
99
|
+
__esDecorate(this, null, _quiet_decorators, { kind: "accessor", name: "quiet", static: false, private: false, access: { has: obj => "quiet" in obj, get: obj => obj.quiet, set: (obj, value) => { obj.quiet = value; } }, metadata: _metadata }, _quiet_initializers, _quiet_extraInitializers);
|
|
88
100
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
89
101
|
}
|
|
90
102
|
static styles = [
|
|
@@ -95,7 +107,10 @@ let MosaicaSelection = (() => {
|
|
|
95
107
|
display: block;
|
|
96
108
|
}
|
|
97
109
|
[part='counts'] {
|
|
98
|
-
margin
|
|
110
|
+
margin: 0 0 4px;
|
|
111
|
+
}
|
|
112
|
+
[part='counts'] dd {
|
|
113
|
+
margin: 0;
|
|
99
114
|
}
|
|
100
115
|
[part='counts'] mosaica-count::part(count) {
|
|
101
116
|
font-weight: 500;
|
|
@@ -115,8 +130,26 @@ let MosaicaSelection = (() => {
|
|
|
115
130
|
}
|
|
116
131
|
[part='item'] {
|
|
117
132
|
min-height: 26px;
|
|
133
|
+
}
|
|
134
|
+
/* The row is its button, which fills it and takes the row's own padding. */
|
|
135
|
+
.list > [part='item'] {
|
|
136
|
+
padding: 0;
|
|
137
|
+
}
|
|
138
|
+
[part='item'] button {
|
|
139
|
+
flex: 1 1 0;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
align-self: stretch;
|
|
142
|
+
padding: 0 6px;
|
|
143
|
+
border-radius: var(--_mosaica-radius-control);
|
|
118
144
|
font-family: var(--_mosaica-font-mono);
|
|
119
145
|
font-size: 12px;
|
|
146
|
+
text-align: left;
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
text-overflow: ellipsis;
|
|
150
|
+
}
|
|
151
|
+
[part='item'] button:focus-visible {
|
|
152
|
+
outline-offset: -2px;
|
|
120
153
|
}
|
|
121
154
|
[part='actions'] {
|
|
122
155
|
display: flex;
|
|
@@ -130,74 +163,107 @@ let MosaicaSelection = (() => {
|
|
|
130
163
|
/** A region to show instead of the store's `region` projection. */
|
|
131
164
|
get region() { return this.#region_accessor_storage; }
|
|
132
165
|
set region(value) { this.#region_accessor_storage = value; }
|
|
166
|
+
#quiet_accessor_storage = (__runInitializers(this, _region_extraInitializers), __runInitializers(this, _quiet_initializers, false));
|
|
167
|
+
/**
|
|
168
|
+
* Announces nothing: the state line and the counts are still shown, and none is put in a live
|
|
169
|
+
* region. Set it when another element on the page says the same things.
|
|
170
|
+
*/
|
|
171
|
+
get quiet() { return this.#quiet_accessor_storage; }
|
|
172
|
+
set quiet(value) { this.#quiet_accessor_storage = value; }
|
|
133
173
|
get shown() {
|
|
134
174
|
return this.region ?? this.resolvedStore?.get('region') ?? null;
|
|
135
175
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
${r.visible ? kv(r.shape.outside ? 'Visible outside' : 'Visible inside', html `<mosaica-count part="count-visible" .masked=${r.visible} .stale=${stale}></mosaica-count>`) : nothing}
|
|
150
|
-
</div>`
|
|
151
|
-
: nothing;
|
|
176
|
+
actionFocus = (__runInitializers(this, _quiet_extraInitializers), new ActionFocus());
|
|
177
|
+
willUpdate(changed) {
|
|
178
|
+
super.willUpdate(changed);
|
|
179
|
+
this.actionFocus.note(this.renderRoot);
|
|
180
|
+
}
|
|
181
|
+
updated() {
|
|
182
|
+
this.actionFocus.keep(this.renderRoot.querySelector('[part="state"]'));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The state and its line for a region: what the server said of its count, or how the count
|
|
186
|
+
* stands. Once it is counted, `verdict` says whether the counts are exact for the shape.
|
|
187
|
+
*/
|
|
188
|
+
stateOfRegion(r, status) {
|
|
152
189
|
// A server that cannot be reached, did not answer or is too busy is named as the status strip
|
|
153
190
|
// names it, since nothing about the selection was refused.
|
|
154
191
|
const unreachable = stateOf(status) === 'retrying' && status?.unreachable === true;
|
|
155
192
|
const unanswered = (r.status === 'refused' && isUnanswered(r.refusal)) || stateOf(status) === 'unanswered';
|
|
193
|
+
if (r.status !== 'shown' && (unreachable || unanswered)) {
|
|
194
|
+
const state = unreachable ? 'retrying' : 'unanswered';
|
|
195
|
+
return { state, line: stateLine(state, status) };
|
|
196
|
+
}
|
|
156
197
|
// Busy only where the selection's own count was shed, so the code shown is its own.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
if (r.status === 'refused' && isBusy(r.refusal) && status)
|
|
199
|
+
return { state: 'busy', line: stateLine('busy', { ...status, refusal: r.refusal }, { onRetry: () => this.resolvedStore?.refresh() }) };
|
|
200
|
+
// Counting until the query has finished loading. The figures a frame gives first are an
|
|
201
|
+
// estimate, shown below as they stand with no line above them; the line goes on saying
|
|
202
|
+
// Counting, undrawn, from the same element, so the counts that follow are said once.
|
|
203
|
+
const estimate = r.status === 'shown' && status?.querying === true;
|
|
204
|
+
const counting = html `<span class=${estimate ? 'sr-only' : nothing}>Counting</span>`;
|
|
205
|
+
if (r.status === 'loading')
|
|
206
|
+
return { state: 'loading', line: { dot: 'dot quiet', words: counting, action: nothing } };
|
|
207
|
+
if (r.status === 'refused')
|
|
208
|
+
return { state: 'refused', line: refusedLine('Selection refused', r.refusal?.code, null, reasonOf(r.refusal)) };
|
|
209
|
+
const verdict = r.verdict === null ? 'Not counted yet' : r.verdict.exact ? 'Exact for the shape' : 'Approximate: counted over cells around the shape';
|
|
210
|
+
if (status?.stale)
|
|
211
|
+
return { state: 'stale', line: { dot: 'dot warn', words: 'Data updated', action: nothing }, verdict };
|
|
212
|
+
if (estimate)
|
|
213
|
+
return { state: 'shown', line: { dot: null, words: counting, action: nothing }, verdict };
|
|
214
|
+
// The counts in words and whether they are exact for the shape: heard as they arrive and not
|
|
215
|
+
// drawn, since the figures are below and the verdict is the line's tooltip.
|
|
216
|
+
const side = r.shape.outside ? 'outside' : 'inside';
|
|
217
|
+
const counted = `${formatMasked(r.matched)} matched ${side}${r.visible ? `, ${formatMasked(r.visible)} visible ${side}` : ''}.`;
|
|
218
|
+
return { state: 'shown', line: { dot: null, words: html `<span class="sr-only">${this.quiet ? nothing : counted} ${verdict}</span>`, action: nothing }, verdict };
|
|
219
|
+
}
|
|
220
|
+
render() {
|
|
221
|
+
const r = this.shown;
|
|
222
|
+
const status = this.resolvedStore?.get('status') ?? null;
|
|
223
|
+
const stale = status?.stale ?? false;
|
|
224
|
+
const { state, line, verdict } = r ? this.stateOfRegion(r, status) : { state: 'detached', line: stateLine('detached', null) };
|
|
225
|
+
const pair = (label, count) => html `<dt class="k">${label}</dt><dd class="v">${count}</dd>`;
|
|
226
|
+
const counts = r?.status === 'shown'
|
|
227
|
+
? html `<dl part="counts" class="kv">
|
|
228
|
+
${pair('Shown inside', html `<mosaica-count part="count-served" .count=${r.served} .stale=${stale} figure="shown"></mosaica-count>`)}
|
|
229
|
+
${pair(r.shape.outside ? 'Matched outside' : 'Matched inside', html `<mosaica-count part="count-matched" .masked=${r.matched} .stale=${stale}></mosaica-count>`)}
|
|
230
|
+
${r.visible ? pair(r.shape.outside ? 'Visible outside' : 'Visible inside', html `<mosaica-count part="count-visible" .masked=${r.visible} .stale=${stale}></mosaica-count>`) : nothing}
|
|
231
|
+
</dl>`
|
|
232
|
+
: nothing;
|
|
233
|
+
const ids = r ? Array.from(r.held.ids, idString) : [];
|
|
234
|
+
const unavailable = 'Not available yet';
|
|
235
|
+
const waiting = (label) => html `<button part="action" class="btn off" type="button" disabled title=${unavailable} aria-describedby="unavailable">${label}</button>`;
|
|
236
|
+
// One template whether or not there is a region, so the live element inside the state line is
|
|
237
|
+
// there before its words are. The state's button sits beside it, and the line takes the focus
|
|
238
|
+
// when the button goes.
|
|
239
|
+
return html `<div class="panel">
|
|
240
|
+
<h2 part="title">Selection <span class="summary">${r ? (r.shape.outside ? `outside ${r.shape.kind}` : r.shape.kind) : nothing}</span></h2>
|
|
241
|
+
<span part="state" data-state=${state} tabindex="-1" title=${verdict ?? nothing}>${liveLine(line, this.quiet ? UNSAID : null, state === 'loading' ? html `<span class="skel" aria-hidden="true"></span>` : nothing)}</span>
|
|
242
|
+
${counts}
|
|
243
|
+
${r && ids.length > 0
|
|
244
|
+
? html `<div part="label" id="items-label" class="xs muted items-label">Shown items</div>
|
|
245
|
+
<ul part="items" class="list" aria-labelledby="items-label">
|
|
246
|
+
${ids.map((id) => html `<li part="item" class="item"><button type="button" @click=${() => void this.resolvedStore?.pick(BigInt(id))}>${id}</button></li>`)}
|
|
179
247
|
${r.held.count > ids.length ? html `<li class="muted sm">${(r.held.count - ids.length).toLocaleString('en-GB')} more not listed</li>` : nothing}
|
|
180
248
|
</ul>`
|
|
181
249
|
: nothing}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
250
|
+
${r
|
|
251
|
+
? html `<div part="actions">
|
|
252
|
+
<button part="action" class="btn" type="button" @click=${() => {
|
|
253
|
+
this.resolvedStore?.select(null);
|
|
254
|
+
emit(this, 'mosaica-selectchange', { shape: null });
|
|
255
|
+
}}>${icon('close', 13)}Clear</button>
|
|
256
|
+
<button part="action" class="btn" type="button" title=${r.shape.outside ? 'Filter to the inside of the shape' : 'Filter to the outside of the shape'} @click=${() => {
|
|
257
|
+
const next = { ...r.shape, outside: !r.shape.outside };
|
|
258
|
+
this.resolvedStore?.select(next);
|
|
259
|
+
emit(this, 'mosaica-selectchange', { shape: shapeDetail(next), status: 'loading' });
|
|
260
|
+
}}>${icon(r.shape.outside ? 'filter' : 'outside', 13)}${r.shape.outside ? 'Inside' : 'Outside'}</button>
|
|
261
|
+
${waiting('Export')}${waiting('Save as artifact')}
|
|
262
|
+
<span id="unavailable" class="sr-only">${unavailable}</span>
|
|
263
|
+
</div>`
|
|
264
|
+
: nothing}
|
|
195
265
|
</div>`;
|
|
196
266
|
}
|
|
197
|
-
constructor() {
|
|
198
|
-
super(...arguments);
|
|
199
|
-
__runInitializers(this, _region_extraInitializers);
|
|
200
|
-
}
|
|
201
267
|
};
|
|
202
268
|
})();
|
|
203
269
|
export { MosaicaSelection };
|
package/dist/states.d.ts
CHANGED
|
@@ -47,6 +47,51 @@ export declare function refusalText(words: string, code: string | null | undefin
|
|
|
47
47
|
* server's, such as an error from a request that reached no server.
|
|
48
48
|
*/
|
|
49
49
|
export declare function reasonOf(refusal: Refusal | null | undefined): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* What a state's line is made of: the class of the dot before its words, the words, and the button
|
|
52
|
+
* the state offers. An element whose state is a live region puts the words inside it and the
|
|
53
|
+
* button beside it, so the button's name is not read out as part of the state.
|
|
54
|
+
*/
|
|
55
|
+
export type StateLine = {
|
|
56
|
+
dot: string | null;
|
|
57
|
+
words: TemplateResult | string | typeof nothing;
|
|
58
|
+
action: TemplateResult | typeof nothing;
|
|
59
|
+
/** The dot's tooltip, for a line whose words are not drawn. */
|
|
60
|
+
title?: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The refused state's line: its words with the refusal's code, the server's reason on a line under
|
|
64
|
+
* them where `reason` is given, and Retry where `onRetry` is given.
|
|
65
|
+
*/
|
|
66
|
+
export declare function refusedLine(words: string, code: string | null | undefined, onRetry?: (() => void) | null, reason?: string | null): StateLine;
|
|
67
|
+
/** The line each state needs; `detached` and `shown` need none. */
|
|
68
|
+
export declare function stateLine(state: PanelState, status: StatusProjection | null | undefined, actions?: StateActions): StateLine;
|
|
69
|
+
/**
|
|
70
|
+
* A line for an element that announces its state: the dot, the words in a `role="status"` element,
|
|
71
|
+
* `after`, and the line's button beside them. An element renders this in the same place in every
|
|
72
|
+
* state, so the live element stays and each change of its words is announced. With `unsaid`, the
|
|
73
|
+
* words sit beside the live element instead, read and not announced, and the live element holds
|
|
74
|
+
* `unsaid.heard`, undrawn.
|
|
75
|
+
*/
|
|
76
|
+
export declare function liveLine(line: StateLine, unsaid?: {
|
|
77
|
+
heard: unknown;
|
|
78
|
+
} | null, after?: unknown): TemplateResult;
|
|
79
|
+
/** What {@link liveLine} takes for words that are shown and never announced. */
|
|
80
|
+
export declare const UNSAID: {
|
|
81
|
+
heard: symbol;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Whether the view's emptiness is a query's answer, which an element announces, or where a pan
|
|
85
|
+
* went, which it does not. An element calls {@link note} once for each update.
|
|
86
|
+
*/
|
|
87
|
+
export declare class EmptyAnswer {
|
|
88
|
+
private wasQuerying;
|
|
89
|
+
private answered;
|
|
90
|
+
/** Whether `state` is `empty` and a query ended in it. It stays so until the view is no longer empty. */
|
|
91
|
+
note(state: PanelState, querying: boolean): boolean;
|
|
92
|
+
}
|
|
93
|
+
/** The element with the focus in `doc`, looked for through the shadow roots that hold it. */
|
|
94
|
+
export declare function focusedElement(doc: Document): Element | null;
|
|
50
95
|
/**
|
|
51
96
|
* The refused state: its dot, its words with the refusal's code, the server's reason on a line
|
|
52
97
|
* under them where `reason` is given, and Retry where `onRetry` is given.
|
|
@@ -57,6 +102,33 @@ export declare function refusedState(words: string, code: string | null | undefi
|
|
|
57
102
|
* state needs, such as a skeleton while loading or the refresh control when stale.
|
|
58
103
|
*/
|
|
59
104
|
export declare function renderState(state: PanelState, status: StatusProjection | null | undefined, actions?: StateActions): TemplateResult | typeof nothing;
|
|
105
|
+
/**
|
|
106
|
+
* Where focus goes when a state's button leaves with its state. An element calls {@link note} from
|
|
107
|
+
* `willUpdate` and {@link keep} from `updated`, naming the element that takes the focus.
|
|
108
|
+
*/
|
|
109
|
+
export declare class ActionFocus {
|
|
110
|
+
private held;
|
|
111
|
+
/** Remember the button with focus in a state line inside `root`, if there is one. */
|
|
112
|
+
note(root: DocumentFragment | HTMLElement | null): void;
|
|
113
|
+
/** Focus `to` if the remembered button has left the page. */
|
|
114
|
+
keep(to: HTMLElement | null): void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether a query has run long enough to name. An element calls {@link track} from `updated` with
|
|
118
|
+
* whether its query is running, reads {@link named} when it renders, and is asked to update when
|
|
119
|
+
* the query has run for {@link QUERY_NAMED_AFTER_MS}.
|
|
120
|
+
*/
|
|
121
|
+
export declare class NamedQuery {
|
|
122
|
+
private readonly host;
|
|
123
|
+
named: boolean;
|
|
124
|
+
private timer;
|
|
125
|
+
constructor(host: {
|
|
126
|
+
requestUpdate(): void;
|
|
127
|
+
});
|
|
128
|
+
track(running: boolean): void;
|
|
129
|
+
/** Forget the wait, as an element leaving the page does. */
|
|
130
|
+
stop(): void;
|
|
131
|
+
}
|
|
60
132
|
/** The words for a viewer who can see no item in the view at all, where `empty` would read as "pan elsewhere". */
|
|
61
133
|
export declare const NOTHING_VISIBLE = "Nothing visible to you";
|
|
62
134
|
/**
|
package/dist/states.js
CHANGED
|
@@ -73,45 +73,154 @@ function retryButton(onRetry) {
|
|
|
73
73
|
return onRetry ? html `<button part="retry" class="btn small" type="button" @click=${onRetry}>Retry</button>` : nothing;
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
* The refused state: its
|
|
77
|
-
*
|
|
76
|
+
* The refused state's line: its words with the refusal's code, the server's reason on a line under
|
|
77
|
+
* them where `reason` is given, and Retry where `onRetry` is given.
|
|
78
78
|
*/
|
|
79
|
-
export function
|
|
79
|
+
export function refusedLine(words, code, onRetry, reason) {
|
|
80
80
|
const said = reason ? html `<span class="said">${refusalText(words, code)}<span part="reason">${reason}</span></span>` : refusalText(words, code);
|
|
81
|
-
return
|
|
81
|
+
return { dot: 'dot refuse', words: said, action: retryButton(onRetry) };
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
* state needs, such as a skeleton while loading or the refresh control when stale.
|
|
86
|
-
*/
|
|
87
|
-
export function renderState(state, status, actions = {}) {
|
|
83
|
+
/** The line each state needs; `detached` and `shown` need none. */
|
|
84
|
+
export function stateLine(state, status, actions = {}) {
|
|
88
85
|
const refusal = status?.refusal ?? null;
|
|
86
|
+
const words = stateWord(state, status);
|
|
89
87
|
switch (state) {
|
|
88
|
+
// Empty: "empty" and "refused" are answers, and a detached panel has none.
|
|
90
89
|
case 'detached':
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
case 'loading':
|
|
94
|
-
|
|
95
|
-
return html `<span part="state" data-state="loading"><span class="dot quiet"></span>${words}${status && !status.sessionWarm ? nothing : html `<span class="skel" aria-hidden="true"></span>`}</span>`;
|
|
96
|
-
}
|
|
90
|
+
case 'shown':
|
|
91
|
+
return { dot: null, words: nothing, action: nothing };
|
|
92
|
+
case 'loading':
|
|
93
|
+
return { dot: 'dot quiet', words: actions.first && status?.sessionWarm ? 'Loading' : words, action: nothing };
|
|
97
94
|
case 'retrying':
|
|
98
|
-
return
|
|
95
|
+
return { dot: 'dot warn', words, action: nothing };
|
|
99
96
|
case 'unanswered':
|
|
100
|
-
return
|
|
97
|
+
return { dot: 'dot warn', words, action: retryButton(actions.onRetry) };
|
|
101
98
|
case 'busy':
|
|
102
|
-
return
|
|
103
|
-
case 'shown':
|
|
104
|
-
return html `<span part="state" data-state="shown"></span>`;
|
|
99
|
+
return { dot: 'dot warn', words: refusalText(words, refusal?.code), action: retryButton(actions.onRetry) };
|
|
105
100
|
case 'empty':
|
|
106
|
-
return
|
|
101
|
+
return { dot: null, words, action: nothing };
|
|
107
102
|
case 'refused':
|
|
108
|
-
return
|
|
103
|
+
return refusedLine(words, refusal?.code, actions.onRetry, reasonOf(refusal));
|
|
109
104
|
case 'expired':
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
return {
|
|
106
|
+
dot: 'dot refuse',
|
|
107
|
+
words,
|
|
108
|
+
action: actions.onReauthorise ? html `<button part="reauthorise" class="btn small" type="button" @click=${actions.onReauthorise}>Sign in</button>` : nothing
|
|
109
|
+
};
|
|
113
110
|
case 'stale':
|
|
114
|
-
return
|
|
111
|
+
return { dot: 'dot warn', words, action: html `<button part="refresh" class="btn small" type="button" @click=${actions.onRefresh ?? undefined}>Refresh</button>` };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** The dot before a line's words. */
|
|
115
|
+
function stateDot(line) {
|
|
116
|
+
return line.dot ? html `<span class=${line.dot} title=${line.title ?? nothing}></span>` : nothing;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A line for an element that announces its state: the dot, the words in a `role="status"` element,
|
|
120
|
+
* `after`, and the line's button beside them. An element renders this in the same place in every
|
|
121
|
+
* state, so the live element stays and each change of its words is announced. With `unsaid`, the
|
|
122
|
+
* words sit beside the live element instead, read and not announced, and the live element holds
|
|
123
|
+
* `unsaid.heard`, undrawn.
|
|
124
|
+
*/
|
|
125
|
+
export function liveLine(line, unsaid = null, after = nothing) {
|
|
126
|
+
return html `${stateDot(line)}${unsaid ? line.words : nothing}<span class=${unsaid ? 'sr-only' : nothing} role="status">${unsaid ? unsaid.heard : line.words}</span>${after}${line.action}`;
|
|
127
|
+
}
|
|
128
|
+
/** What {@link liveLine} takes for words that are shown and never announced. */
|
|
129
|
+
export const UNSAID = { heard: nothing };
|
|
130
|
+
/**
|
|
131
|
+
* Whether the view's emptiness is a query's answer, which an element announces, or where a pan
|
|
132
|
+
* went, which it does not. An element calls {@link note} once for each update.
|
|
133
|
+
*/
|
|
134
|
+
export class EmptyAnswer {
|
|
135
|
+
wasQuerying = false;
|
|
136
|
+
answered = false;
|
|
137
|
+
/** Whether `state` is `empty` and a query ended in it. It stays so until the view is no longer empty. */
|
|
138
|
+
note(state, querying) {
|
|
139
|
+
if (state !== 'empty')
|
|
140
|
+
this.answered = false;
|
|
141
|
+
else if (this.wasQuerying && !querying)
|
|
142
|
+
this.answered = true;
|
|
143
|
+
this.wasQuerying = querying;
|
|
144
|
+
return this.answered;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** The element with the focus in `doc`, looked for through the shadow roots that hold it. */
|
|
148
|
+
export function focusedElement(doc) {
|
|
149
|
+
let focused = doc.activeElement;
|
|
150
|
+
while (focused?.shadowRoot?.activeElement)
|
|
151
|
+
focused = focused.shadowRoot.activeElement;
|
|
152
|
+
return focused;
|
|
153
|
+
}
|
|
154
|
+
/** A line as a `part="state"` carrying `data-state`, with `after` between its words and its button. */
|
|
155
|
+
function stateRegion(state, line, after = nothing) {
|
|
156
|
+
return html `<span part="state" data-state=${state}>${stateDot(line)}${line.words}${after}${line.action}</span>`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The refused state: its dot, its words with the refusal's code, the server's reason on a line
|
|
160
|
+
* under them where `reason` is given, and Retry where `onRetry` is given.
|
|
161
|
+
*/
|
|
162
|
+
export function refusedState(words, code, onRetry, reason) {
|
|
163
|
+
return stateRegion('refused', refusedLine(words, code, onRetry, reason));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The state region every panel renders: a `part="state"` carrying `data-state`, and the line the
|
|
167
|
+
* state needs, such as a skeleton while loading or the refresh control when stale.
|
|
168
|
+
*/
|
|
169
|
+
export function renderState(state, status, actions = {}) {
|
|
170
|
+
const skeleton = state === 'loading' && !(status && !status.sessionWarm) ? html `<span class="skel" aria-hidden="true"></span>` : nothing;
|
|
171
|
+
return stateRegion(state, stateLine(state, status, actions), skeleton);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Where focus goes when a state's button leaves with its state. An element calls {@link note} from
|
|
175
|
+
* `willUpdate` and {@link keep} from `updated`, naming the element that takes the focus.
|
|
176
|
+
*/
|
|
177
|
+
export class ActionFocus {
|
|
178
|
+
held = null;
|
|
179
|
+
/** Remember the button with focus in a state line inside `root`, if there is one. */
|
|
180
|
+
note(root) {
|
|
181
|
+
const focused = root ? focusedElement(root.ownerDocument) : null;
|
|
182
|
+
this.held = focused instanceof HTMLButtonElement && root.contains(focused) && focused.closest('[part~="state"]') ? focused : null;
|
|
183
|
+
}
|
|
184
|
+
/** Focus `to` if the remembered button has left the page. */
|
|
185
|
+
keep(to) {
|
|
186
|
+
if (this.held && !this.held.isConnected)
|
|
187
|
+
to?.focus();
|
|
188
|
+
this.held = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** How long a query has run before an element names it, so one answered at once does not flicker it. */
|
|
192
|
+
const QUERY_NAMED_AFTER_MS = 250;
|
|
193
|
+
/**
|
|
194
|
+
* Whether a query has run long enough to name. An element calls {@link track} from `updated` with
|
|
195
|
+
* whether its query is running, reads {@link named} when it renders, and is asked to update when
|
|
196
|
+
* the query has run for {@link QUERY_NAMED_AFTER_MS}.
|
|
197
|
+
*/
|
|
198
|
+
export class NamedQuery {
|
|
199
|
+
host;
|
|
200
|
+
named = false;
|
|
201
|
+
timer = null;
|
|
202
|
+
constructor(host) {
|
|
203
|
+
this.host = host;
|
|
204
|
+
}
|
|
205
|
+
track(running) {
|
|
206
|
+
if (!running) {
|
|
207
|
+
this.stop();
|
|
208
|
+
this.named = false;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (this.named || this.timer !== null)
|
|
212
|
+
return;
|
|
213
|
+
this.timer = setTimeout(() => {
|
|
214
|
+
this.timer = null;
|
|
215
|
+
this.named = true;
|
|
216
|
+
this.host.requestUpdate();
|
|
217
|
+
}, QUERY_NAMED_AFTER_MS);
|
|
218
|
+
}
|
|
219
|
+
/** Forget the wait, as an element leaving the page does. */
|
|
220
|
+
stop() {
|
|
221
|
+
if (this.timer !== null)
|
|
222
|
+
clearTimeout(this.timer);
|
|
223
|
+
this.timer = null;
|
|
115
224
|
}
|
|
116
225
|
}
|
|
117
226
|
/** The words for a viewer who can see no item in the view at all, where `empty` would read as "pan elsewhere". */
|