@issuegraph/viewer 0.1.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/LICENSE +201 -0
- package/README.md +156 -0
- package/dist/clusters.d.ts +34 -0
- package/dist/clusters.d.ts.map +1 -0
- package/dist/clusters.js +195 -0
- package/dist/clusters.js.map +1 -0
- package/dist/document.d.ts +190 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +308 -0
- package/dist/document.js.map +1 -0
- package/dist/element.d.ts +95 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +174 -0
- package/dist/element.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +122 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +297 -0
- package/dist/layout.js.map +1 -0
- package/dist/mount.d.ts +64 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +402 -0
- package/dist/mount.js.map +1 -0
- package/dist/navigation.d.ts +56 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +121 -0
- package/dist/navigation.js.map +1 -0
- package/dist/parts.d.ts +89 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +214 -0
- package/dist/parts.js.map +1 -0
- package/dist/projections/graph.d.ts +30 -0
- package/dist/projections/graph.d.ts.map +1 -0
- package/dist/projections/graph.js +644 -0
- package/dist/projections/graph.js.map +1 -0
- package/dist/projections/linear.d.ts +35 -0
- package/dist/projections/linear.d.ts.map +1 -0
- package/dist/projections/linear.js +157 -0
- package/dist/projections/linear.js.map +1 -0
- package/dist/projections/tree.d.ts +17 -0
- package/dist/projections/tree.d.ts.map +1 -0
- package/dist/projections/tree.js +209 -0
- package/dist/projections/tree.js.map +1 -0
- package/dist/render.d.ts +35 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +45 -0
- package/dist/render.js.map +1 -0
- package/dist/scene.d.ts +94 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +48 -0
- package/dist/scene.js.map +1 -0
- package/dist/styles.d.ts +17 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +389 -0
- package/dist/styles.js.map +1 -0
- package/dist/theme.d.ts +71 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +169 -0
- package/dist/theme.js.map +1 -0
- package/dist/vocabulary.d.ts +107 -0
- package/dist/vocabulary.d.ts.map +1 -0
- package/dist/vocabulary.js +98 -0
- package/dist/vocabulary.js.map +1 -0
- package/package.json +56 -0
package/dist/mount.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DOM shell — the only module in this package that touches nodes.
|
|
3
|
+
*
|
|
4
|
+
* It is deliberately thin. Everything that could be wrong about WHAT is drawn
|
|
5
|
+
* lives in the projections, everything that could be wrong about WHERE
|
|
6
|
+
* navigation goes lives in `navigate`, and what is left here is three
|
|
7
|
+
* listeners, an index from key to element, and a teardown. A shell that grew
|
|
8
|
+
* decisions would be a second implementation of the parts above it, testable
|
|
9
|
+
* only through a DOM.
|
|
10
|
+
*
|
|
11
|
+
* IT REACHES NO GLOBAL. The document comes from the container the caller
|
|
12
|
+
* passed, which is what keeps this module importable on a runtime that has no
|
|
13
|
+
* DOM — the property the workspace smoke test measures against the published
|
|
14
|
+
* entry.
|
|
15
|
+
*/
|
|
16
|
+
import { normalizeDocument } from "./document.js";
|
|
17
|
+
import { materialize } from "./element.js";
|
|
18
|
+
import { initialNavigationState, navigate, reconcile, } from "./navigation.js";
|
|
19
|
+
import { sceneFor } from "./render.js";
|
|
20
|
+
import { GROUP_ATTRIBUTE, KEY_ATTRIBUTE } from "./scene.js";
|
|
21
|
+
import { defaultTheme } from "./theme.js";
|
|
22
|
+
/**
|
|
23
|
+
* Elements that own their own keyboard activation.
|
|
24
|
+
*
|
|
25
|
+
* Every row deliberately carries a deep-link chip, and `keydown` BUBBLES — so
|
|
26
|
+
* the viewer's own Enter/Space handling ran while focus was on the anchor,
|
|
27
|
+
* called `preventDefault()`, and suppressed the link. A projection that exposes
|
|
28
|
+
* a link a keyboard cannot follow has not exposed it.
|
|
29
|
+
*/
|
|
30
|
+
const SELF_ACTIVATING_TAGS = new Set([
|
|
31
|
+
'A',
|
|
32
|
+
'BUTTON',
|
|
33
|
+
'INPUT',
|
|
34
|
+
'SELECT',
|
|
35
|
+
'TEXTAREA',
|
|
36
|
+
]);
|
|
37
|
+
function ownsItsOwnActivation(target, container) {
|
|
38
|
+
let cursor = target ?? null;
|
|
39
|
+
while (cursor !== null && cursor !== container) {
|
|
40
|
+
const tag = cursor.tagName;
|
|
41
|
+
if (tag !== undefined && SELF_ACTIVATING_TAGS.has(tag.toUpperCase()))
|
|
42
|
+
return true;
|
|
43
|
+
cursor = cursor.parentElement;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Walk up from an event target to the nearest element carrying a key.
|
|
49
|
+
*
|
|
50
|
+
* BOUNDED AT THE CONTAINER. An unbounded walk keeps climbing into the host's
|
|
51
|
+
* own page, so an ancestor that happens to carry `data-ig-key` — a host
|
|
52
|
+
* rendering two viewers, or reusing the attribute — would answer for a click
|
|
53
|
+
* that landed on nothing of ours. Stopping at the mount point makes the answer
|
|
54
|
+
* a fact about this viewer.
|
|
55
|
+
*
|
|
56
|
+
* DECORATION ANSWERS TOO, through {@link GROUP_ATTRIBUTE}. The enclosure and
|
|
57
|
+
* its connector are deliberately outside the focus index — one element per key
|
|
58
|
+
* or `focus()` lands on the wrong one — but they are still visible marks a
|
|
59
|
+
* reader can click, and the design states the connector IS a click target.
|
|
60
|
+
* Reading both attributes here separates "what may take focus" from "what may
|
|
61
|
+
* be pointed at" rather than making one attribute answer both.
|
|
62
|
+
*/
|
|
63
|
+
function keyAt(target, container,
|
|
64
|
+
// WHICH IDENTITY IS BEING ASKED FOR. Pointing and focusing are different
|
|
65
|
+
// questions — see the note above — so the caller names the attributes rather
|
|
66
|
+
// than a second near-identical walk being written for the narrower one, which
|
|
67
|
+
// is how two walkers drift apart.
|
|
68
|
+
attributes = [KEY_ATTRIBUTE, GROUP_ATTRIBUTE]) {
|
|
69
|
+
let cursor = target ?? null;
|
|
70
|
+
while (cursor !== null) {
|
|
71
|
+
for (const attribute of attributes) {
|
|
72
|
+
const key = cursor.getAttribute(attribute);
|
|
73
|
+
if (key !== null && key !== '')
|
|
74
|
+
return key;
|
|
75
|
+
}
|
|
76
|
+
if (cursor === container)
|
|
77
|
+
return null;
|
|
78
|
+
cursor = cursor.parentElement;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Mount a viewer into a container.
|
|
84
|
+
*
|
|
85
|
+
* Returns a handle rather than nothing, because a host that cannot tear this
|
|
86
|
+
* down leaks a listener on every re-render — the shell's one genuine hazard.
|
|
87
|
+
*/
|
|
88
|
+
export function mountViewer(container, input, options = {}) {
|
|
89
|
+
const doc = container.ownerDocument;
|
|
90
|
+
let currentOptions = options;
|
|
91
|
+
let projection = options.projection ?? 'linear';
|
|
92
|
+
let normalized = normalizeDocument(input).document;
|
|
93
|
+
// SEEDED FROM THE OPTIONS, not from nothing. `MountOptions` carries
|
|
94
|
+
// `selected` and `focused` because it extends the render options, so a caller
|
|
95
|
+
// restoring a view — after a projection toggle, a route change, a reload —
|
|
96
|
+
// passes them here. Starting empty silently discarded them and dropped the
|
|
97
|
+
// caller straight back to the top of the order.
|
|
98
|
+
let state = {
|
|
99
|
+
focused: options.focused ?? initialNavigationState.focused,
|
|
100
|
+
selected: options.selected ?? initialNavigationState.selected,
|
|
101
|
+
};
|
|
102
|
+
let root = null;
|
|
103
|
+
let keyed = new Map();
|
|
104
|
+
let hovered = null;
|
|
105
|
+
let destroyed = false;
|
|
106
|
+
// Set by the first `draw()` below, which runs before any listener can fire.
|
|
107
|
+
let currentScene = null;
|
|
108
|
+
const theme = options.theme ?? defaultTheme;
|
|
109
|
+
// ONE PLACE THAT CLEARS A HOVER, because forgetting a site is exactly how this
|
|
110
|
+
// kept going wrong: `pointerleave` had it, then a redraw needed it, and now
|
|
111
|
+
// teardown does too. Three call sites open-coding two lines is a fourth site
|
|
112
|
+
// waiting to omit them, so the rule lives here and the sites ask for it.
|
|
113
|
+
const clearHover = () => {
|
|
114
|
+
if (hovered === null)
|
|
115
|
+
return;
|
|
116
|
+
hovered = null;
|
|
117
|
+
currentOptions.onHover?.(null);
|
|
118
|
+
};
|
|
119
|
+
// `announceSelection` is false ONLY for the redraw `emitSelect` runs, which
|
|
120
|
+
// reports the settled selection itself — see there. Every other caller wants
|
|
121
|
+
// the notice, because a reconciliation that moves the selection has moved it
|
|
122
|
+
// out from under whoever set it.
|
|
123
|
+
const draw = (announceSelection = true) => {
|
|
124
|
+
// A DOCUMENT UPDATE CAN REMOVE THE SELECTED ISSUE, and `reconcile` carries
|
|
125
|
+
// the selection through whole — deliberately, because a PROJECTION switch
|
|
126
|
+
// changes representation rather than subject and dropping it there would
|
|
127
|
+
// lose the thing the reader is looking at. A document REPLACEMENT is the
|
|
128
|
+
// other case: the subject can genuinely be gone, and the handle went on
|
|
129
|
+
// reporting a key this document does not carry, against `update`'s own
|
|
130
|
+
// documented promise that a still-present selection survives.
|
|
131
|
+
// THE TEST IS DOCUMENT MEMBERSHIP, WHICH IS WHY IT COSTS THE PROJECTION
|
|
132
|
+
// SWITCH NOTHING. `setProjection` redraws against the SAME `normalized`, so
|
|
133
|
+
// `byKey` is unchanged and a still-present selection passes untouched —
|
|
134
|
+
// preserved across a projection-only change and reconciled on replacement,
|
|
135
|
+
// from one rule rather than from two entry points that can disagree.
|
|
136
|
+
// THE SAME SHAPE AND THE SAME TEST AS THE HOVER RECONCILIATION at the foot
|
|
137
|
+
// of this function, sitting apart from it only because the selection has to
|
|
138
|
+
// be settled BEFORE the scene is built: `sceneFor` is handed
|
|
139
|
+
// `state.selected`, so clearing afterwards would draw one frame marking a
|
|
140
|
+
// selection that no longer exists.
|
|
141
|
+
// WHAT THE SELECTION WAS BEFORE THIS DRAW TOUCHED IT. Two things can move
|
|
142
|
+
// it — the document-membership drop just below, and `reconcile`'s
|
|
143
|
+
// canonicalization to the station this projection draws it under — and the
|
|
144
|
+
// host has to hear about BOTH. Keying the notice on a single `dropped` flag
|
|
145
|
+
// covered only the first, so `setProjection` renamed the selection and told
|
|
146
|
+
// nobody: the handle read the station while the host still held the member.
|
|
147
|
+
const selectedBefore = state.selected;
|
|
148
|
+
if (state.selected !== null && !normalized.byKey.has(state.selected)) {
|
|
149
|
+
state = { ...state, selected: null };
|
|
150
|
+
}
|
|
151
|
+
if (root !== null)
|
|
152
|
+
container.removeChild(root);
|
|
153
|
+
const scene = sceneFor(normalized, projection, {
|
|
154
|
+
...currentOptions,
|
|
155
|
+
theme: currentOptions.theme ?? theme,
|
|
156
|
+
selected: state.selected,
|
|
157
|
+
focused: state.focused,
|
|
158
|
+
});
|
|
159
|
+
state = reconcile(scene, state);
|
|
160
|
+
keyed = new Map();
|
|
161
|
+
// EVERY POINTER IDENTITY THIS SCENE ACTUALLY DREW, which is a WIDER set than
|
|
162
|
+
// `keyed` and a NARROWER one than the document. `keyed` holds focus
|
|
163
|
+
// identities only; decoration announces itself through `GROUP_ATTRIBUTE` and
|
|
164
|
+
// deliberately never enters the focus index, so an enclosure or a connector
|
|
165
|
+
// is absent from `keyed` on every redraw. The hover reconciliation below
|
|
166
|
+
// needs both, and needs them from the SCENE rather than from `byKey`.
|
|
167
|
+
const drawnHoverable = new Set();
|
|
168
|
+
// WHICH KEYS ARE INDEXED BY SOMETHING THAT CAN ACTUALLY TAKE FOCUS. `keyed`
|
|
169
|
+
// is a FOCUS index — its only readers call `focus()` on what it holds — so
|
|
170
|
+
// holding an element a browser ignores makes it silently useless.
|
|
171
|
+
// FIRST-WINS WAS THE DEFECT, and the shape is specific: a RAILED lead is
|
|
172
|
+
// emitted twice with `data-ig-key` — the canvas `<g>`, which carries no
|
|
173
|
+
// `tabindex` because the rail row owns the tab stop, and then the row. The
|
|
174
|
+
// canvas comes first, so every ranked row's `focus()` landed on the group:
|
|
175
|
+
// arrow navigation moved nothing, and focus dropped out of the viewer after
|
|
176
|
+
// a selection redraw. It passed 258 tests because the test double counted
|
|
177
|
+
// every `focus()` call regardless of whether the element could take one.
|
|
178
|
+
// A LATER FOCUSABLE ELEMENT REPLACES A STORED UNFOCUSABLE ONE, rather than
|
|
179
|
+
// indexing only focusable elements: a key whose every element is unfocusable
|
|
180
|
+
// still belongs in the map, so `focus?.()` on it is a no-op rather than a
|
|
181
|
+
// lookup miss, and the pointer identity keeps working exactly as before.
|
|
182
|
+
// TABINDEX **OR** A NATURALLY FOCUSABLE TAG, the same pair the test double
|
|
183
|
+
// now applies — two notions of focusable that can disagree is how this class
|
|
184
|
+
// hides, and it just did.
|
|
185
|
+
const focusableKeys = new Set();
|
|
186
|
+
root = materialize(doc, scene.root, {
|
|
187
|
+
onElement: (spec, node) => {
|
|
188
|
+
const key = spec.attrs?.[KEY_ATTRIBUTE];
|
|
189
|
+
if (typeof key === 'string' && key !== '') {
|
|
190
|
+
const tabindex = spec.attrs?.tabindex;
|
|
191
|
+
const canFocus = (tabindex !== undefined && tabindex !== null) || spec.tag === 'a' || spec.tag === 'button';
|
|
192
|
+
if (!keyed.has(key) || (canFocus && !focusableKeys.has(key))) {
|
|
193
|
+
keyed.set(key, node);
|
|
194
|
+
if (canFocus)
|
|
195
|
+
focusableKeys.add(key);
|
|
196
|
+
}
|
|
197
|
+
drawnHoverable.add(key);
|
|
198
|
+
}
|
|
199
|
+
const group = spec.attrs?.[GROUP_ATTRIBUTE];
|
|
200
|
+
if (typeof group === 'string' && group !== '')
|
|
201
|
+
drawnHoverable.add(group);
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
container.appendChild(root);
|
|
205
|
+
currentScene = scene;
|
|
206
|
+
// A REDRAW CAN REMOVE THE HOVERED ISSUE, and `pointerleave` does not fire
|
|
207
|
+
// when it does: the element under the pointer is destroyed while the
|
|
208
|
+
// pointer stays inside the container, so the handler that clears `hovered`
|
|
209
|
+
// never runs. The host was then left holding a key for an issue this
|
|
210
|
+
// document no longer carries, and never received the documented
|
|
211
|
+
// `onHover(null)` until the pointer left the whole viewer.
|
|
212
|
+
// IN `draw()` RATHER THAN IN `update()`, because the removal is a property
|
|
213
|
+
// of REDRAWING, not of one entry point — `setProjection` and a selection
|
|
214
|
+
// redraw can drop the key just as `update` can.
|
|
215
|
+
// ASK WHAT THIS SCENE DREW, NOT WHAT THE DOCUMENT CARRIES. Both tests reject
|
|
216
|
+
// `keyed` alone, and for the reason that still stands: decoration announces
|
|
217
|
+
// itself through `GROUP_ATTRIBUTE` and never enters the focus index, so
|
|
218
|
+
// clearing on `keyed` would fire constantly on a hover that is perfectly
|
|
219
|
+
// live. The earlier answer — document membership — was the wrong correction,
|
|
220
|
+
// because the document is not what the pointer is over.
|
|
221
|
+
// A PROJECTION SWITCH IS WHERE THE TWO COME APART. An off-order edge endpoint
|
|
222
|
+
// is drawn on the graph canvas and has no row in the linear projection, so
|
|
223
|
+
// switching destroys the element while its issue stays in `byKey` — the
|
|
224
|
+
// document test then preserves a hover on something that is no longer on
|
|
225
|
+
// screen, and the host never receives the documented `onHover(null)` until
|
|
226
|
+
// the pointer leaves the whole viewer. `drawnHoverable` carries both
|
|
227
|
+
// attributes from the scene just materialized, so it answers for decoration
|
|
228
|
+
// and for issues, on every redraw, from the thing the pointer can touch.
|
|
229
|
+
if (hovered !== null && !drawnHoverable.has(hovered))
|
|
230
|
+
clearHover();
|
|
231
|
+
// TOLD, NOT JUST DROPPED. The host was told a selection began, so leaving it
|
|
232
|
+
// to infer the ending from a document it happens to have replaced is the
|
|
233
|
+
// same divergence `clearHover` exists to prevent one line up. Fired AFTER
|
|
234
|
+
// the DOM is in place, exactly as the hover clear is, so a host that redraws
|
|
235
|
+
// from this callback finds the viewer already consistent.
|
|
236
|
+
if (announceSelection && state.selected !== selectedBefore) {
|
|
237
|
+
currentOptions.onSelect?.(state.selected);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const emitSelect = (key) => {
|
|
241
|
+
// A KEY THIS DOCUMENT DOES NOT CARRY IS A SELECTION OF NOTHING, and it has
|
|
242
|
+
// to be resolved HERE, before the state is written. `select()` is documented
|
|
243
|
+
// to fire `onSelect` exactly as a click does — and a click can only ever
|
|
244
|
+
// land on a key the canvas drew, so an unknown key has no click to be
|
|
245
|
+
// equivalent to. A host holding a stale key from its own list is the
|
|
246
|
+
// ordinary way one arrives.
|
|
247
|
+
// WITHOUT THIS, THE TWO NOTIFICATIONS CONTRADICTED EACH OTHER. The state
|
|
248
|
+
// took the unknown key, `draw()` reconciled it away against `byKey` and
|
|
249
|
+
// reported `onSelect(null)`, and then this function reported `onSelect(key)`
|
|
250
|
+
// — so the host was told twice, ending on the key that does not exist,
|
|
251
|
+
// while the handle read `selected: null`. Resolving first means the state
|
|
252
|
+
// and the one notification agree, and the reconciliation in `draw()` finds
|
|
253
|
+
// nothing left to undo.
|
|
254
|
+
const resolved = key !== null && normalized.byKey.has(key) ? key : null;
|
|
255
|
+
state = { ...state, selected: resolved, focused: resolved ?? state.focused };
|
|
256
|
+
// SILENCED, because this function reports the result itself once the draw
|
|
257
|
+
// has settled it. Letting the draw announce too would fire `onSelect` twice
|
|
258
|
+
// for one selection whenever reconciliation renamed it — and a caller
|
|
259
|
+
// cannot tell a doubled notice from a second selection.
|
|
260
|
+
draw(false);
|
|
261
|
+
// FOCUS FOLLOWS THE SUBJECT — the movement branch of `onKeyDown` already
|
|
262
|
+
// does this, and selection needs it for the same reason: `draw()` destroys
|
|
263
|
+
// the subtree holding focus and mounts a replacement. Without it a reader
|
|
264
|
+
// who pressed Enter dropped focus out of the viewer entirely, so no later
|
|
265
|
+
// arrow key reached the container and keyboard navigation was over.
|
|
266
|
+
// HERE AND NOT IN `draw()`, which is the tempting generalization and the
|
|
267
|
+
// wrong one: `update()` redraws too, and a host that refreshes the document
|
|
268
|
+
// while the reader is somewhere else on the page would have focus YANKED
|
|
269
|
+
// into the viewer. Restoring only where the viewer itself moved the subject
|
|
270
|
+
// keeps that impossible. Narrowing `draw()` correctly instead would need to
|
|
271
|
+
// know whether focus was already inside — an `activeElement` this module
|
|
272
|
+
// deliberately does not ask its host for.
|
|
273
|
+
// IT COVERS THE CLICK AND `select()` PATHS TOO, which is what the handle
|
|
274
|
+
// documents: selection "fires onSelect exactly as a click does", and a
|
|
275
|
+
// click moves focus. `:focus-visible` is what keeps the ring off a pointer
|
|
276
|
+
// user, so this costs a mouse reader nothing.
|
|
277
|
+
if (state.focused !== null)
|
|
278
|
+
keyed.get(state.focused)?.focus?.();
|
|
279
|
+
// WHAT THE STATE ACTUALLY HOLDS, not what was asked for. `draw()` reconciles
|
|
280
|
+
// the selection against the scene, which canonicalizes a together unit's
|
|
281
|
+
// partner to the station that represents it — so reporting `resolved` here
|
|
282
|
+
// would tell the host a key the handle does not hold, which is the very
|
|
283
|
+
// disagreement between the two notifications this function was written to
|
|
284
|
+
// end. `select()` stays equivalent to the click it documents itself as.
|
|
285
|
+
currentOptions.onSelect?.(state.selected);
|
|
286
|
+
};
|
|
287
|
+
const onClick = (event) => {
|
|
288
|
+
// AN ACTIVATION ON A CONTROL THAT ACTIVATES ITSELF IS THAT CONTROL'S — the
|
|
289
|
+
// same invariant `onKeyDown` already applies, and the click path needs its
|
|
290
|
+
// own copy because returning from the keydown handler does NOT suppress the
|
|
291
|
+
// click the browser synthesizes afterwards. Following a row's deep link was
|
|
292
|
+
// therefore also selecting the row and firing `onSelect` at the host, for an
|
|
293
|
+
// activation the anchor owned.
|
|
294
|
+
// UNCONDITIONAL HERE, CONDITIONAL THERE, and the asymmetry is the point: a
|
|
295
|
+
// click IS an activation, whereas a keydown may be a MOVEMENT key, which
|
|
296
|
+
// stays the viewer's even while focus rests on a link.
|
|
297
|
+
if (ownsItsOwnActivation(event.target, container))
|
|
298
|
+
return;
|
|
299
|
+
const key = keyAt(event.target, container);
|
|
300
|
+
if (key === null)
|
|
301
|
+
return;
|
|
302
|
+
emitSelect(key);
|
|
303
|
+
};
|
|
304
|
+
const onPointerOver = (event) => {
|
|
305
|
+
const key = keyAt(event.target, container);
|
|
306
|
+
if (key === hovered)
|
|
307
|
+
return;
|
|
308
|
+
hovered = key;
|
|
309
|
+
currentOptions.onHover?.(key);
|
|
310
|
+
};
|
|
311
|
+
const onPointerLeave = () => {
|
|
312
|
+
clearHover();
|
|
313
|
+
};
|
|
314
|
+
const onKeyDown = (event) => {
|
|
315
|
+
if (event.key === undefined || currentScene === null)
|
|
316
|
+
return;
|
|
317
|
+
// An activation key on a control that activates itself is that control's.
|
|
318
|
+
// Movement keys are still the viewer's — a reader on a link must be able to
|
|
319
|
+
// arrow away from it.
|
|
320
|
+
if ((event.key === 'Enter' || event.key === ' ') &&
|
|
321
|
+
ownsItsOwnActivation(event.target, container)) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// SYNC FOCUS FROM THE DOM BEFORE MOVING. `state.focused` tracks the viewer's
|
|
325
|
+
// OWN moves — its arrow keys and its clicks — and native Tab is neither. A
|
|
326
|
+
// reader who tabs into the deep link inside the third row leaves `focused`
|
|
327
|
+
// pointing wherever it was, so ArrowDown moved relative to the OLD row and
|
|
328
|
+
// sent focus BACKWARDS past the row they were standing in.
|
|
329
|
+
// FOCUS IDENTITY ONLY, hence the explicit attribute list: `GROUP_ATTRIBUTE`
|
|
330
|
+
// marks decoration that is deliberately outside the focus index, so adopting
|
|
331
|
+
// one would set `focused` to a key `navigate` cannot find — which resolves
|
|
332
|
+
// to -1 and throws the reader back to the top of the order. That is the same
|
|
333
|
+
// bug this fixes, arriving through the fix.
|
|
334
|
+
// `navigable`, NOT `focusOrder`: a gutter node reachable only sideways is a
|
|
335
|
+
// legitimate place to be standing, and `focusOrder` does not contain it.
|
|
336
|
+
const standingOn = keyAt(event.target, container, [KEY_ATTRIBUTE]);
|
|
337
|
+
if (standingOn !== null && standingOn !== state.focused && currentScene.navigable.includes(standingOn)) {
|
|
338
|
+
state = { ...state, focused: standingOn };
|
|
339
|
+
}
|
|
340
|
+
const result = navigate(currentScene, state, event.key);
|
|
341
|
+
if (result.command.kind === 'none')
|
|
342
|
+
return;
|
|
343
|
+
event.preventDefault?.();
|
|
344
|
+
if (result.command.kind === 'select') {
|
|
345
|
+
emitSelect(result.command.key);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
state = result.state;
|
|
349
|
+
draw();
|
|
350
|
+
keyed.get(result.command.key)?.focus?.();
|
|
351
|
+
};
|
|
352
|
+
container.addEventListener('click', onClick);
|
|
353
|
+
container.addEventListener('pointerover', onPointerOver);
|
|
354
|
+
container.addEventListener('pointerleave', onPointerLeave);
|
|
355
|
+
container.addEventListener('keydown', onKeyDown);
|
|
356
|
+
draw();
|
|
357
|
+
return {
|
|
358
|
+
update(next, nextOptions) {
|
|
359
|
+
if (destroyed)
|
|
360
|
+
return;
|
|
361
|
+
if (nextOptions !== undefined)
|
|
362
|
+
currentOptions = nextOptions;
|
|
363
|
+
if (nextOptions?.projection !== undefined)
|
|
364
|
+
projection = nextOptions.projection;
|
|
365
|
+
normalized = normalizeDocument(next).document;
|
|
366
|
+
draw();
|
|
367
|
+
},
|
|
368
|
+
setProjection(next) {
|
|
369
|
+
if (destroyed)
|
|
370
|
+
return;
|
|
371
|
+
projection = next;
|
|
372
|
+
draw();
|
|
373
|
+
},
|
|
374
|
+
select(key) {
|
|
375
|
+
if (destroyed)
|
|
376
|
+
return;
|
|
377
|
+
emitSelect(key);
|
|
378
|
+
},
|
|
379
|
+
get state() {
|
|
380
|
+
return state;
|
|
381
|
+
},
|
|
382
|
+
destroy() {
|
|
383
|
+
if (destroyed)
|
|
384
|
+
return;
|
|
385
|
+
destroyed = true;
|
|
386
|
+
// BEFORE THE LISTENERS COME OFF, because after that no `pointerleave` can
|
|
387
|
+
// fire and the host is left holding a key for an element about to be
|
|
388
|
+
// removed — a tooltip pinned to nothing. Destruction bypasses `draw()`
|
|
389
|
+
// entirely, so the redraw reconciliation above never sees this path.
|
|
390
|
+
clearHover();
|
|
391
|
+
container.removeEventListener('click', onClick);
|
|
392
|
+
container.removeEventListener('pointerover', onPointerOver);
|
|
393
|
+
container.removeEventListener('pointerleave', onPointerLeave);
|
|
394
|
+
container.removeEventListener('keydown', onKeyDown);
|
|
395
|
+
if (root !== null)
|
|
396
|
+
container.removeChild(root);
|
|
397
|
+
root = null;
|
|
398
|
+
keyed = new Map();
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
//# sourceMappingURL=mount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard navigation, as a pure reducer.
|
|
3
|
+
*
|
|
4
|
+
* It takes a scene, the current state and a key name, and returns the next
|
|
5
|
+
* state plus the one thing the shell should do. No DOM, no events, no
|
|
6
|
+
* side-effects — which is what lets the whole key map be tested exhaustively on
|
|
7
|
+
* a runtime with no DOM at all, and what leaves `mount.ts` with nothing to get
|
|
8
|
+
* wrong except wiring.
|
|
9
|
+
*
|
|
10
|
+
* THE TRAVERSAL IS THE SCENE'S PUBLISHED ORDER, NEVER THE PICTURE. On the spine
|
|
11
|
+
* that order is rank order, so `Tab` and the vertical keys walk the work order
|
|
12
|
+
* even where the drawing places boxes elsewhere. Recovering an order from
|
|
13
|
+
* coordinates is precisely what the design forbids, so this module cannot see
|
|
14
|
+
* a coordinate.
|
|
15
|
+
*/
|
|
16
|
+
import { type Scene } from './scene.ts';
|
|
17
|
+
export interface NavigationState {
|
|
18
|
+
/** The roving tab stop. `null` before anything has been focused. */
|
|
19
|
+
readonly focused: string | null;
|
|
20
|
+
/** The selected key, which survives a projection change. */
|
|
21
|
+
readonly selected: string | null;
|
|
22
|
+
}
|
|
23
|
+
/** What the shell should do. `none` means the host keeps its own shortcut. */
|
|
24
|
+
export type NavigationCommand = {
|
|
25
|
+
readonly kind: 'none';
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'focus';
|
|
28
|
+
readonly key: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: 'select';
|
|
31
|
+
readonly key: string;
|
|
32
|
+
};
|
|
33
|
+
export interface NavigationResult {
|
|
34
|
+
readonly state: NavigationState;
|
|
35
|
+
readonly command: NavigationCommand;
|
|
36
|
+
}
|
|
37
|
+
export declare const initialNavigationState: NavigationState;
|
|
38
|
+
/**
|
|
39
|
+
* Re-resolve state against a scene.
|
|
40
|
+
*
|
|
41
|
+
* Switching projection changes the REPRESENTATION, never the subject, so a
|
|
42
|
+
* selection carries across. Focus follows it when it can; when the new
|
|
43
|
+
* projection does not draw the focused key at all, focus falls to the first
|
|
44
|
+
* item rather than nowhere.
|
|
45
|
+
*/
|
|
46
|
+
export declare function reconcile(scene: Scene, state: NavigationState): NavigationState;
|
|
47
|
+
/**
|
|
48
|
+
* Apply one key.
|
|
49
|
+
*
|
|
50
|
+
* Movement never wraps: the ends of the order are the ends of the work, and a
|
|
51
|
+
* list that silently teleports to the top reads as a jump rather than as a
|
|
52
|
+
* boundary. An unhandled key returns the state untouched and `none`, so a host
|
|
53
|
+
* keeps every shortcut this package does not claim.
|
|
54
|
+
*/
|
|
55
|
+
export declare function navigate(scene: Scene, state: NavigationState, key: string): NavigationResult;
|
|
56
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,KAAK,EAAmB,MAAM,YAAY,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAED,eAAO,MAAM,sBAAsB,EAAE,eAGnC,CAAC;AAeH;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,GAAG,eAAe,CA6B/E;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAyC5F"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard navigation, as a pure reducer.
|
|
3
|
+
*
|
|
4
|
+
* It takes a scene, the current state and a key name, and returns the next
|
|
5
|
+
* state plus the one thing the shell should do. No DOM, no events, no
|
|
6
|
+
* side-effects — which is what lets the whole key map be tested exhaustively on
|
|
7
|
+
* a runtime with no DOM at all, and what leaves `mount.ts` with nothing to get
|
|
8
|
+
* wrong except wiring.
|
|
9
|
+
*
|
|
10
|
+
* THE TRAVERSAL IS THE SCENE'S PUBLISHED ORDER, NEVER THE PICTURE. On the spine
|
|
11
|
+
* that order is rank order, so `Tab` and the vertical keys walk the work order
|
|
12
|
+
* even where the drawing places boxes elsewhere. Recovering an order from
|
|
13
|
+
* coordinates is precisely what the design forbids, so this module cannot see
|
|
14
|
+
* a coordinate.
|
|
15
|
+
*/
|
|
16
|
+
import { resolveFocusKey } from "./scene.js";
|
|
17
|
+
export const initialNavigationState = Object.freeze({
|
|
18
|
+
focused: null,
|
|
19
|
+
selected: null,
|
|
20
|
+
});
|
|
21
|
+
function indexOfFocus(scene, state) {
|
|
22
|
+
if (state.focused === null)
|
|
23
|
+
return -1;
|
|
24
|
+
return scene.focusOrder.indexOf(state.focused);
|
|
25
|
+
}
|
|
26
|
+
function stay(state) {
|
|
27
|
+
return { state, command: { kind: 'none' } };
|
|
28
|
+
}
|
|
29
|
+
function moveTo(state, key) {
|
|
30
|
+
return { state: { ...state, focused: key }, command: { kind: 'focus', key } };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Re-resolve state against a scene.
|
|
34
|
+
*
|
|
35
|
+
* Switching projection changes the REPRESENTATION, never the subject, so a
|
|
36
|
+
* selection carries across. Focus follows it when it can; when the new
|
|
37
|
+
* projection does not draw the focused key at all, focus falls to the first
|
|
38
|
+
* item rather than nowhere.
|
|
39
|
+
*/
|
|
40
|
+
export function reconcile(scene, state) {
|
|
41
|
+
// Selection is carried WHOLE, even when the new projection does not draw it.
|
|
42
|
+
// It is the subject, and a switch changes representation rather than subject;
|
|
43
|
+
// dropping it here would make the toggle lose the thing the reader is looking
|
|
44
|
+
// at, which is the one property the design fixes about switching.
|
|
45
|
+
//
|
|
46
|
+
// Focus goes through the SHARED rule, so what this reports and what the
|
|
47
|
+
// projection rendered a tab stop on cannot diverge.
|
|
48
|
+
//
|
|
49
|
+
// CANONICALIZED THROUGH THE SCENE'S OWN STATIONS FIRST, and that is what makes
|
|
50
|
+
// "carried whole" true rather than merely stated. A key this projection draws
|
|
51
|
+
// under another key's station — a together unit's partner — is not a subject
|
|
52
|
+
// this projection can hold: `navigable` does not list it, so focus could not
|
|
53
|
+
// follow it and fell to the FIRST entry in the order, an unrelated issue.
|
|
54
|
+
// Reporting `selected` as the station keeps the subject and lets focus reach
|
|
55
|
+
// it; the tree publishes no stations, so a partner selected there stays
|
|
56
|
+
// itself, which is correct because the tree gives it its own row.
|
|
57
|
+
// HERE RATHER THAN AT THE ENTRY POINTS, because there are four of them —
|
|
58
|
+
// pointer, `handle.select`, a projection switch, a document update — and only
|
|
59
|
+
// this one sees the scene that decides the answer. Canonicalizing per entry
|
|
60
|
+
// point is what left `handle.select` and `setProjection` behaving differently
|
|
61
|
+
// from the click they are documented to be equivalent to.
|
|
62
|
+
const station = (key) => key === null || key === undefined ? null : (scene.stationOf.get(key) ?? key);
|
|
63
|
+
const selected = station(state.selected);
|
|
64
|
+
return {
|
|
65
|
+
focused: resolveFocusKey(scene.navigable, station(state.focused), selected),
|
|
66
|
+
selected,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Apply one key.
|
|
71
|
+
*
|
|
72
|
+
* Movement never wraps: the ends of the order are the ends of the work, and a
|
|
73
|
+
* list that silently teleports to the top reads as a jump rather than as a
|
|
74
|
+
* boundary. An unhandled key returns the state untouched and `none`, so a host
|
|
75
|
+
* keeps every shortcut this package does not claim.
|
|
76
|
+
*/
|
|
77
|
+
export function navigate(scene, state, key) {
|
|
78
|
+
const order = scene.focusOrder;
|
|
79
|
+
if (order.length === 0)
|
|
80
|
+
return stay(state);
|
|
81
|
+
const current = indexOfFocus(scene, state);
|
|
82
|
+
const first = order[0];
|
|
83
|
+
const last = order[order.length - 1];
|
|
84
|
+
switch (key) {
|
|
85
|
+
case 'ArrowDown': {
|
|
86
|
+
if (current === -1)
|
|
87
|
+
return moveTo(state, first);
|
|
88
|
+
const next = order[Math.min(current + 1, order.length - 1)];
|
|
89
|
+
return next === state.focused ? stay(state) : moveTo(state, next);
|
|
90
|
+
}
|
|
91
|
+
case 'ArrowUp': {
|
|
92
|
+
if (current === -1)
|
|
93
|
+
return moveTo(state, first);
|
|
94
|
+
const next = order[Math.max(current - 1, 0)];
|
|
95
|
+
return next === state.focused ? stay(state) : moveTo(state, next);
|
|
96
|
+
}
|
|
97
|
+
case 'ArrowLeft':
|
|
98
|
+
case 'ArrowRight': {
|
|
99
|
+
if (state.focused === null)
|
|
100
|
+
return moveTo(state, first);
|
|
101
|
+
const neighbours = scene.lateral.get(state.focused);
|
|
102
|
+
const target = key === 'ArrowLeft' ? neighbours?.left : neighbours?.right;
|
|
103
|
+
return target === undefined ? stay(state) : moveTo(state, target);
|
|
104
|
+
}
|
|
105
|
+
case 'Home':
|
|
106
|
+
return state.focused === first ? stay(state) : moveTo(state, first);
|
|
107
|
+
case 'End':
|
|
108
|
+
return state.focused === last ? stay(state) : moveTo(state, last);
|
|
109
|
+
case 'Enter':
|
|
110
|
+
case ' ': {
|
|
111
|
+
const target = state.focused ?? first;
|
|
112
|
+
return {
|
|
113
|
+
state: { focused: target, selected: target },
|
|
114
|
+
command: { kind: 'select', key: target },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
default:
|
|
118
|
+
return stay(state);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=navigation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"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"}
|