@swiftbrowser/web 0.10.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swiftbrowser/web",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "description": "SwiftBrowser web renderer and dev server: applies render ops from a Swift/Wasm app to the DOM inside an iPhone frame.",
6
6
  "repository": {
@@ -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,10 +52,18 @@ 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.hidden || el.getClientRects().length === 0) return false;
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
+
57
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. */
58
68
  const DISMISS_WAIT_MS = 1000;
59
69
 
@@ -103,13 +113,14 @@ export class BrowserHistory {
103
113
  if (!current) this.win.history.replaceState({ ...(this.win.history.state ?? {}), ...this.entry(0, this.selection()) }, '');
104
114
  this.watchReplaceState();
105
115
  this.renderer.interaction.nativeBackGesture = true;
116
+ this.root.addEventListener('sb:precommit', this.onPreCommit);
106
117
  this.root.addEventListener('sb:commit', this.onCommit);
107
118
  this.win.addEventListener('popstate', this.onPopState);
108
119
  // Enabled before the first commit (a mount, or a remount such as React's
109
120
  // StrictMode makes in development) the screen is not drawn yet: fewer
110
121
  // levels than the entry records is not the app popping, so only the
111
122
  // levels already shown are pushed here and the first commit settles the rest.
112
- this.sync(true);
123
+ this.sync('enabling');
113
124
  }
114
125
 
115
126
  dispose(): void {
@@ -121,6 +132,7 @@ export class BrowserHistory {
121
132
  this.landingUrl = null;
122
133
  this.syncOnArrival = false;
123
134
  this.unwatchReplaceState();
135
+ this.root.removeEventListener('sb:precommit', this.onPreCommit);
124
136
  this.root.removeEventListener('sb:commit', this.onCommit);
125
137
  this.win.removeEventListener('popstate', this.onPopState);
126
138
  }
@@ -292,15 +304,31 @@ export class BrowserHistory {
292
304
  }
293
305
  }
294
306
 
295
- private readonly onCommit = (): void => this.sync();
307
+ private readonly onPreCommit = (): void => this.sync('precommit');
308
+ private readonly onCommit = (): void => this.sync('commit');
296
309
 
297
- /** After a commit: new levels get entries; levels the app dismissed itself take the history back with them; a moved sidebar selection is an entry too. `enabling`: nothing is drawn yet, so no level is taken as dismissed. */
298
- sync(enabling = false): void {
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 {
299
326
  if (!this.enabled) return;
327
+ const dismissing = this.dismissing;
328
+ if (stage === 'precommit' && dismissing) return;
300
329
  const depth = this.depth();
301
330
  const recorded = this.recorded();
302
331
  const selection = this.selection();
303
- const dismissing = this.dismissing;
304
332
  if (dismissing) {
305
333
  // Dismissing for a `popstate`: the entry already names the target depth.
306
334
  if (depth > dismissing.target && performance.now() < dismissing.until) {
@@ -320,7 +348,7 @@ export class BrowserHistory {
320
348
  }
321
349
  for (let level = recorded + 1; level <= depth; level++) this.win.history.pushState(this.entry(level, selection), '');
322
350
  } else if (depth < recorded) {
323
- if (enabling) return;
351
+ if (stage !== 'commit') return;
324
352
  // The app popped or dismissed on its own (back button, swipe, code): leave those entries.
325
353
  this.go(depth - recorded, depth);
326
354
  } else if (this.movingTo === null && selection !== this.recordedSelection()) {
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:commit`
9
- * CustomEvent is dispatched on the root so tests and tooling can observe pass
10
- * boundaries). Animated passes are handed to the `Animator`.
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
- // Force a style flush so re-adding the class restarts the animation.
3028
- void screen.offsetWidth;
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
  }