@swiftbrowser/web 0.10.0 → 0.10.2
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/package.json +1 -1
- package/src/browserHistory.ts +68 -18
- package/src/renderer.ts +13 -5
package/package.json
CHANGED
package/src/browserHistory.ts
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
* the entry and, once its animation ends, hides the live page until a new
|
|
35
35
|
* frame is committed, so a pop that finished animating by then would stay
|
|
36
36
|
* under the snapshot for Safari's 3 s watchdog, taking the touches unseen.
|
|
37
|
+
* A push is made at the renderer's `sb:precommit`, before the pushed screen
|
|
38
|
+
* is laid out (see `sync`), and this module never forces a layout.
|
|
37
39
|
*/
|
|
38
40
|
import type { Renderer } from './renderer';
|
|
39
41
|
|
|
@@ -50,14 +52,31 @@ interface HistoryState {
|
|
|
50
52
|
const stateOf = (value: unknown): HistoryState | null => (typeof value === 'object' && value !== null && (value as HistoryState).sbHistory === true ? (value as HistoryState) : null);
|
|
51
53
|
|
|
52
54
|
function shown(win: Window, el: HTMLElement): boolean {
|
|
53
|
-
if (el.
|
|
55
|
+
if (!el.isConnected) return false;
|
|
56
|
+
// Style only, never layout: a layout forced here before a push would hand
|
|
57
|
+
// Safari the pushed screen's size for the entry it covers (see `sync`).
|
|
58
|
+
for (let node: HTMLElement | null = el; node; node = node.parentElement) {
|
|
59
|
+
if (node.hidden || win.getComputedStyle(node).display === 'none') return false;
|
|
60
|
+
}
|
|
54
61
|
return win.getComputedStyle(el).visibility !== 'hidden';
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
/** When `sync` runs: the renderer's `sb:precommit` (the pass's ops applied, not yet laid out), its `sb:commit`, or `enable`. */
|
|
65
|
+
export type SyncStage = 'precommit' | 'commit' | 'enabling';
|
|
66
|
+
|
|
67
|
+
/** How long a back the browser made waits for the app to pop a level before what it shows is recorded as it is. */
|
|
68
|
+
const DISMISS_WAIT_MS = 1000;
|
|
69
|
+
|
|
57
70
|
export class BrowserHistory {
|
|
58
71
|
private readonly win: Window;
|
|
59
|
-
/**
|
|
60
|
-
|
|
72
|
+
/**
|
|
73
|
+
* A back the browser made, while the app pops to it: the depth the entry
|
|
74
|
+
* names, the depth the latest dismissal started from, and until when the
|
|
75
|
+
* app's pop is waited for. A pop can take more than one commit (a compact
|
|
76
|
+
* split view clears its sidebar selection first, and the detail goes on
|
|
77
|
+
* the next), so a commit that still shows the level is not a new one.
|
|
78
|
+
*/
|
|
79
|
+
private dismissing: { target: number; from: number; until: number } | null = null;
|
|
61
80
|
/** `popstate` events to ignore: the ones our own `history.go` causes. */
|
|
62
81
|
private ignorePops = 0;
|
|
63
82
|
/**
|
|
@@ -94,13 +113,14 @@ export class BrowserHistory {
|
|
|
94
113
|
if (!current) this.win.history.replaceState({ ...(this.win.history.state ?? {}), ...this.entry(0, this.selection()) }, '');
|
|
95
114
|
this.watchReplaceState();
|
|
96
115
|
this.renderer.interaction.nativeBackGesture = true;
|
|
116
|
+
this.root.addEventListener('sb:precommit', this.onPreCommit);
|
|
97
117
|
this.root.addEventListener('sb:commit', this.onCommit);
|
|
98
118
|
this.win.addEventListener('popstate', this.onPopState);
|
|
99
119
|
// Enabled before the first commit (a mount, or a remount such as React's
|
|
100
120
|
// StrictMode makes in development) the screen is not drawn yet: fewer
|
|
101
121
|
// levels than the entry records is not the app popping, so only the
|
|
102
122
|
// levels already shown are pushed here and the first commit settles the rest.
|
|
103
|
-
this.sync(
|
|
123
|
+
this.sync('enabling');
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
dispose(): void {
|
|
@@ -112,6 +132,7 @@ export class BrowserHistory {
|
|
|
112
132
|
this.landingUrl = null;
|
|
113
133
|
this.syncOnArrival = false;
|
|
114
134
|
this.unwatchReplaceState();
|
|
135
|
+
this.root.removeEventListener('sb:precommit', this.onPreCommit);
|
|
115
136
|
this.root.removeEventListener('sb:commit', this.onCommit);
|
|
116
137
|
this.win.removeEventListener('popstate', this.onPopState);
|
|
117
138
|
}
|
|
@@ -205,6 +226,15 @@ export class BrowserHistory {
|
|
|
205
226
|
return top ? Number(top.dataset['sbDepth'] ?? '0') || 0 : 0;
|
|
206
227
|
}
|
|
207
228
|
|
|
229
|
+
/** Dismisses the level above `depth` for the back in progress, and waits for the app's pop. */
|
|
230
|
+
private dismissFrom(depth: number): void {
|
|
231
|
+
const dismissing = this.dismissing;
|
|
232
|
+
if (!dismissing) return;
|
|
233
|
+
dismissing.from = depth;
|
|
234
|
+
dismissing.until = performance.now() + DISMISS_WAIT_MS;
|
|
235
|
+
if (!this.dismissOne()) this.dismissing = null;
|
|
236
|
+
}
|
|
237
|
+
|
|
208
238
|
/** Asks the app to dismiss one level: the top sheet when nothing is pushed inside it, else the deepest visible stack's top screen. */
|
|
209
239
|
private dismissOne(): boolean {
|
|
210
240
|
const sheets = this.sheets();
|
|
@@ -274,21 +304,40 @@ export class BrowserHistory {
|
|
|
274
304
|
}
|
|
275
305
|
}
|
|
276
306
|
|
|
277
|
-
private readonly
|
|
307
|
+
private readonly onPreCommit = (): void => this.sync('precommit');
|
|
308
|
+
private readonly onCommit = (): void => this.sync('commit');
|
|
278
309
|
|
|
279
|
-
/**
|
|
280
|
-
|
|
310
|
+
/**
|
|
311
|
+
* New levels get entries; levels the app dismissed itself take the history
|
|
312
|
+
* back with them; a moved sidebar selection is an entry too.
|
|
313
|
+
*
|
|
314
|
+
* The entries are pushed at the `precommit`, before the renderer lays the
|
|
315
|
+
* new screen out, and nothing else happens then: Safari sizes the entry
|
|
316
|
+
* being left from the page's last layout at the push (its snapshot's
|
|
317
|
+
* render tree size, more than half of which the page must have again
|
|
318
|
+
* before the snapshot comes down after a swipe back to it), so a push
|
|
319
|
+
* after that layout counted the pushed screen into the entry it covers,
|
|
320
|
+
* and a root lighter than the screen over it stayed under the snapshot
|
|
321
|
+
* until Safari's 3 s watchdog after every swipe back. The rest waits for
|
|
322
|
+
* the `commit`, when the pass is drawn. `enabling`: nothing is drawn yet,
|
|
323
|
+
* so no level is taken as dismissed.
|
|
324
|
+
*/
|
|
325
|
+
sync(stage: SyncStage = 'commit'): void {
|
|
281
326
|
if (!this.enabled) return;
|
|
327
|
+
const dismissing = this.dismissing;
|
|
328
|
+
if (stage === 'precommit' && dismissing) return;
|
|
282
329
|
const depth = this.depth();
|
|
283
330
|
const recorded = this.recorded();
|
|
284
331
|
const selection = this.selection();
|
|
285
|
-
if (
|
|
332
|
+
if (dismissing) {
|
|
286
333
|
// Dismissing for a `popstate`: the entry already names the target depth.
|
|
287
|
-
if (depth >
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
334
|
+
if (depth > dismissing.target && performance.now() < dismissing.until) {
|
|
335
|
+
// The latest dismissal landed: the next level. Otherwise the app has not popped yet.
|
|
336
|
+
if (depth < dismissing.from) this.dismissFrom(depth);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Arrived, or the app kept the level (it is then recorded as any other).
|
|
340
|
+
this.dismissing = null;
|
|
292
341
|
}
|
|
293
342
|
if (depth > recorded) {
|
|
294
343
|
// Under our own pending `go`, a push would land relative to wherever the browser is when it runs (and
|
|
@@ -299,7 +348,7 @@ export class BrowserHistory {
|
|
|
299
348
|
}
|
|
300
349
|
for (let level = recorded + 1; level <= depth; level++) this.win.history.pushState(this.entry(level, selection), '');
|
|
301
350
|
} else if (depth < recorded) {
|
|
302
|
-
if (
|
|
351
|
+
if (stage !== 'commit') return;
|
|
303
352
|
// The app popped or dismissed on its own (back button, swipe, code): leave those entries.
|
|
304
353
|
this.go(depth - recorded, depth);
|
|
305
354
|
} else if (this.movingTo === null && selection !== this.recordedSelection()) {
|
|
@@ -326,11 +375,12 @@ export class BrowserHistory {
|
|
|
326
375
|
// The browser moved on its own (a swipe, a button): keep painting past its transition.
|
|
327
376
|
this.renderer.keepRepainting();
|
|
328
377
|
const depth = this.depth();
|
|
378
|
+
// A move the browser makes replaces any back still in progress.
|
|
379
|
+
this.dismissing = null;
|
|
329
380
|
if (target.sbDepth < depth) {
|
|
330
|
-
// Back: dismiss one level now, the rest as each
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
else this.pendingPops -= 1;
|
|
381
|
+
// Back: dismiss one level now, the rest as each pop lands.
|
|
382
|
+
this.dismissing = { target: target.sbDepth, from: depth, until: 0 };
|
|
383
|
+
this.dismissFrom(depth);
|
|
334
384
|
} else if (target.sbDepth > depth) {
|
|
335
385
|
// Forward: a screen cannot be re-pushed; return to the entry that matches.
|
|
336
386
|
this.go(depth - target.sbDepth, depth);
|
package/src/renderer.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* element tree as `LayoutNode`s; id 0 is the root container supplied by the
|
|
6
6
|
* host page. Every op is applied to the DOM immediately; at `commit` the
|
|
7
7
|
* layout engine (src/layout) computes a frame for every element and
|
|
8
|
-
* `layoutDom.ts` positions the elements absolutely from it (a `sb:
|
|
9
|
-
* CustomEvent is dispatched on the root
|
|
10
|
-
*
|
|
8
|
+
* `layoutDom.ts` positions the elements absolutely from it (a `sb:precommit`
|
|
9
|
+
* CustomEvent is dispatched on the root once the pass's ops are applied and
|
|
10
|
+
* before that layout, and a `sb:commit` after it, so the browser history,
|
|
11
|
+
* tests and tooling can observe pass boundaries). Animated passes are handed
|
|
12
|
+
* to the `Animator`.
|
|
11
13
|
*
|
|
12
14
|
* Most kinds are a single DOM element whose children are the element's
|
|
13
15
|
* children. Compound kinds (`navstack`, `navscreen`, `section`, `toggle`,
|
|
@@ -1062,6 +1064,10 @@ export class Renderer {
|
|
|
1062
1064
|
this.passRootRect = null;
|
|
1063
1065
|
this.preSnapshots.clear();
|
|
1064
1066
|
this.preRects.clear();
|
|
1067
|
+
// Before the layout that first holds this pass's new screens: a
|
|
1068
|
+
// history entry pushed here (browserHistory.ts) is sized by Safari
|
|
1069
|
+
// from the page's last layout, without them.
|
|
1070
|
+
this.root.dispatchEvent(new CustomEvent('sb:precommit', { detail: { commits: this.commits + 1 } }));
|
|
1065
1071
|
const changes = this.layout();
|
|
1066
1072
|
this.animator.commitNextFrame(pass, op.animation, changes);
|
|
1067
1073
|
this.commits += 1;
|
|
@@ -3024,8 +3030,10 @@ function afterAnimation(target: HTMLElement, expectedMs: number, done: () => voi
|
|
|
3024
3030
|
|
|
3025
3031
|
function animatePush(screen: HTMLElement): void {
|
|
3026
3032
|
screen.classList.remove('sb-navscreen--push');
|
|
3027
|
-
//
|
|
3028
|
-
|
|
3033
|
+
// Flush the style (not the layout: a layout forced here, with the screen
|
|
3034
|
+
// just inserted, would size the history entry it covers by it, see
|
|
3035
|
+
// browserHistory.ts) so re-adding the class restarts the animation.
|
|
3036
|
+
void getComputedStyle(screen).animationName;
|
|
3029
3037
|
screen.classList.add('sb-navscreen--push');
|
|
3030
3038
|
afterAnimation(screen, NAV_TRANSITION_MS, () => screen.classList.remove('sb-navscreen--push'));
|
|
3031
3039
|
}
|