@swiftbrowser/web 0.10.2 → 0.10.3
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 +33 -6
- package/src/renderer.ts +30 -1
- package/src/styles.css +7 -0
package/package.json
CHANGED
package/src/browserHistory.ts
CHANGED
|
@@ -61,6 +61,16 @@ function shown(win: Window, el: HTMLElement): boolean {
|
|
|
61
61
|
return win.getComputedStyle(el).visibility !== 'hidden';
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* A back the browser makes after a touch this close to a side of the page is
|
|
66
|
+
* its edge swipe (Safari's, Android's back gesture), which has animated the
|
|
67
|
+
* pop already: the renderer then does not slide the screen out again. The
|
|
68
|
+
* browser's back button sends no touch to the page, so its pop slides.
|
|
69
|
+
*/
|
|
70
|
+
const SWIPE_EDGE_PX = 44;
|
|
71
|
+
/** How long after such a touch a back still counts as the swipe it started. */
|
|
72
|
+
const SWIPE_TOUCH_MS = 2000;
|
|
73
|
+
|
|
64
74
|
/** When `sync` runs: the renderer's `sb:precommit` (the pass's ops applied, not yet laid out), its `sb:commit`, or `enable`. */
|
|
65
75
|
export type SyncStage = 'precommit' | 'commit' | 'enabling';
|
|
66
76
|
|
|
@@ -76,7 +86,9 @@ export class BrowserHistory {
|
|
|
76
86
|
* split view clears its sidebar selection first, and the detail goes on
|
|
77
87
|
* the next), so a commit that still shows the level is not a new one.
|
|
78
88
|
*/
|
|
79
|
-
private dismissing: { target: number; from: number; until: number } | null = null;
|
|
89
|
+
private dismissing: { target: number; from: number; until: number; instant: boolean } | null = null;
|
|
90
|
+
/** When a touch last began at a side of the page (`SWIPE_EDGE_PX`): the start of the browser's edge swipe. */
|
|
91
|
+
private lastEdgeTouch = Number.NEGATIVE_INFINITY;
|
|
80
92
|
/** `popstate` events to ignore: the ones our own `history.go` causes. */
|
|
81
93
|
private ignorePops = 0;
|
|
82
94
|
/**
|
|
@@ -116,6 +128,7 @@ export class BrowserHistory {
|
|
|
116
128
|
this.root.addEventListener('sb:precommit', this.onPreCommit);
|
|
117
129
|
this.root.addEventListener('sb:commit', this.onCommit);
|
|
118
130
|
this.win.addEventListener('popstate', this.onPopState);
|
|
131
|
+
this.win.addEventListener('touchstart', this.onTouchStart, { capture: true, passive: true });
|
|
119
132
|
// Enabled before the first commit (a mount, or a remount such as React's
|
|
120
133
|
// StrictMode makes in development) the screen is not drawn yet: fewer
|
|
121
134
|
// levels than the entry records is not the app popping, so only the
|
|
@@ -135,6 +148,7 @@ export class BrowserHistory {
|
|
|
135
148
|
this.root.removeEventListener('sb:precommit', this.onPreCommit);
|
|
136
149
|
this.root.removeEventListener('sb:commit', this.onCommit);
|
|
137
150
|
this.win.removeEventListener('popstate', this.onPopState);
|
|
151
|
+
this.win.removeEventListener('touchstart', this.onTouchStart, { capture: true });
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
/**
|
|
@@ -232,11 +246,15 @@ export class BrowserHistory {
|
|
|
232
246
|
if (!dismissing) return;
|
|
233
247
|
dismissing.from = depth;
|
|
234
248
|
dismissing.until = performance.now() + DISMISS_WAIT_MS;
|
|
235
|
-
if (!this.dismissOne()) this.dismissing = null;
|
|
249
|
+
if (!this.dismissOne(dismissing.instant)) this.dismissing = null;
|
|
236
250
|
}
|
|
237
251
|
|
|
238
|
-
/**
|
|
239
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Asks the app to dismiss one level: the top sheet when nothing is pushed
|
|
254
|
+
* inside it, else the deepest visible stack's top screen. `instant`: the
|
|
255
|
+
* browser has animated the pop (its swipe), so the renderer does not.
|
|
256
|
+
*/
|
|
257
|
+
private dismissOne(instant: boolean): boolean {
|
|
240
258
|
const sheets = this.sheets();
|
|
241
259
|
const top = sheets.at(-1) ?? this.root;
|
|
242
260
|
const stacks = this.stacks(top)
|
|
@@ -248,6 +266,7 @@ export class BrowserHistory {
|
|
|
248
266
|
const screen = this.topScreen(deepest.stack);
|
|
249
267
|
const id = Number(screen?.dataset['sbId']);
|
|
250
268
|
if (!Number.isInteger(id)) return false;
|
|
269
|
+
if (instant) this.renderer.popWithoutAnimation(id);
|
|
251
270
|
this.renderer.tap(id); // what the back button sends
|
|
252
271
|
return true;
|
|
253
272
|
}
|
|
@@ -364,6 +383,12 @@ export class BrowserHistory {
|
|
|
364
383
|
this.restoringSelection = false;
|
|
365
384
|
}
|
|
366
385
|
|
|
386
|
+
private readonly onTouchStart = (event: TouchEvent): void => {
|
|
387
|
+
const touch = event.touches[0];
|
|
388
|
+
if (!touch) return;
|
|
389
|
+
if (touch.clientX <= SWIPE_EDGE_PX || touch.clientX >= this.win.innerWidth - SWIPE_EDGE_PX) this.lastEdgeTouch = performance.now();
|
|
390
|
+
};
|
|
391
|
+
|
|
367
392
|
private readonly onPopState = (event: PopStateEvent): void => {
|
|
368
393
|
if (this.ignorePops > 0) {
|
|
369
394
|
this.ignorePops -= 1;
|
|
@@ -378,8 +403,10 @@ export class BrowserHistory {
|
|
|
378
403
|
// A move the browser makes replaces any back still in progress.
|
|
379
404
|
this.dismissing = null;
|
|
380
405
|
if (target.sbDepth < depth) {
|
|
381
|
-
// Back: dismiss one level now, the rest as each pop lands.
|
|
382
|
-
|
|
406
|
+
// Back: dismiss one level now, the rest as each pop lands. After a
|
|
407
|
+
// touch at a side of the page it is the browser's swipe, animated already.
|
|
408
|
+
const instant = performance.now() - this.lastEdgeTouch < SWIPE_TOUCH_MS;
|
|
409
|
+
this.dismissing = { target: target.sbDepth, from: depth, until: 0, instant };
|
|
383
410
|
this.dismissFrom(depth);
|
|
384
411
|
} else if (target.sbDepth > depth) {
|
|
385
412
|
// Forward: a screen cannot be re-pushed; return to the entry that matches.
|
package/src/renderer.ts
CHANGED
|
@@ -1310,6 +1310,24 @@ export class Renderer {
|
|
|
1310
1310
|
this.onEvent({ type: 'tap', id });
|
|
1311
1311
|
}
|
|
1312
1312
|
|
|
1313
|
+
/**
|
|
1314
|
+
* Screens the browser's own history move pops (`popWithoutAnimation`):
|
|
1315
|
+
* the browser has animated the pop already, so the renderer does not.
|
|
1316
|
+
*/
|
|
1317
|
+
private instantPops = new Set<ElementId>();
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* The next removal of screen `id` is a pop the browser has already shown
|
|
1321
|
+
* (its back swipe or button, `browserHistory.ts`): the screen goes without
|
|
1322
|
+
* the slide, and the screen it uncovers takes its place at once rather
|
|
1323
|
+
* than sliding back, so the pop does not play a second time.
|
|
1324
|
+
*/
|
|
1325
|
+
popWithoutAnimation(id: ElementId): void {
|
|
1326
|
+
this.instantPops.add(id);
|
|
1327
|
+
// A pop the app refused leaves nothing to skip for long.
|
|
1328
|
+
window.setTimeout(() => this.instantPops.delete(id), NAV_TRANSITION_MS * 5);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1313
1331
|
/** The repaint keep-alive (`keepRepainting`): its box, timer and deadline while it runs. */
|
|
1314
1332
|
private repaintKeepAlive: { el: HTMLElement; timer: number; until: number; on: boolean } | null = null;
|
|
1315
1333
|
|
|
@@ -2221,7 +2239,8 @@ export class Renderer {
|
|
|
2221
2239
|
// screen (`--sb-nav-from`), and the screen below is handed back to the
|
|
2222
2240
|
// stylesheet so it finishes its slide to 0 as the new top screen.
|
|
2223
2241
|
this.interaction.screenRemoved(el);
|
|
2224
|
-
|
|
2242
|
+
if (this.instantPops.delete(id)) settleWithoutTransition(stack);
|
|
2243
|
+
else animatePop(stack, el);
|
|
2225
2244
|
}
|
|
2226
2245
|
const isSheet = el.classList.contains('sb-sheet');
|
|
2227
2246
|
if (isSheet && parent) animateSheetOut(parent, el);
|
|
@@ -3043,6 +3062,16 @@ function animatePush(screen: HTMLElement): void {
|
|
|
3043
3062
|
* visual clone (stripped of element ids so it is invisible to the renderer)
|
|
3044
3063
|
* slides away on top of the stack and is dropped afterwards.
|
|
3045
3064
|
*/
|
|
3065
|
+
/**
|
|
3066
|
+
* A pop the browser has already animated: the stack's screens take their new
|
|
3067
|
+
* places without a transition (styles.css, `data-sb-instant`) for the frames
|
|
3068
|
+
* the change lands in.
|
|
3069
|
+
*/
|
|
3070
|
+
function settleWithoutTransition(stack: HTMLElement): void {
|
|
3071
|
+
stack.dataset['sbInstant'] = '';
|
|
3072
|
+
requestAnimationFrame(() => requestAnimationFrame(() => delete stack.dataset['sbInstant']));
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3046
3075
|
function animatePop(stack: HTMLElement, screen: HTMLElement): void {
|
|
3047
3076
|
const clone = ghostClone(screen);
|
|
3048
3077
|
clone.removeAttribute('inert');
|
package/src/styles.css
CHANGED
|
@@ -1065,6 +1065,13 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1065
1065
|
animation: sb-nav-push 0.4s var(--sb-nav-easing) both;
|
|
1066
1066
|
}
|
|
1067
1067
|
|
|
1068
|
+
/* A pop the browser has animated already (its back swipe, renderer.ts
|
|
1069
|
+
popWithoutAnimation): the uncovered screen takes its place at once. */
|
|
1070
|
+
.sb-navstack[data-sb-instant] > .sb-slot > .sb-navscreen,
|
|
1071
|
+
.sb-navstack[data-sb-instant] > .sb-slot > .sb-navscreen::after {
|
|
1072
|
+
transition: none !important;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1068
1075
|
.sb-navstack > .sb-navscreen--pop {
|
|
1069
1076
|
z-index: 2;
|
|
1070
1077
|
pointer-events: none;
|