@swiftbrowser/web 0.26.6 → 0.26.7
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/pointerEffects.ts +71 -11
- package/src/protocol.ts +15 -0
- package/src/renderer.ts +2 -2
- package/src/styles.css +5 -3
package/package.json
CHANGED
package/src/pointerEffects.ts
CHANGED
|
@@ -1,43 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What a page with a mouse adds to a phone's UI: `hover` events (`.onHover`),
|
|
3
|
-
*
|
|
4
|
-
* (`.
|
|
5
|
-
*
|
|
3
|
+
* `continuoushover` events (`.onContinuousHover`: where the mouse is as it
|
|
4
|
+
* moves), tooltips (`.help`), cursors (`.pointerStyle`), and SF Symbols'
|
|
5
|
+
* effects (`.symbolEffect`) drawn with CSS. The Swift shim draws an iPhone
|
|
6
|
+
* and sends none of these; producers in the page (@swiftbrowser/react) do.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import type { ElementId, Style, SymbolEffectStyle } from './protocol';
|
|
9
|
+
import type { ContinuousHoverEvent, ElementId, HoverEvent, Style, SymbolEffectStyle } from './protocol';
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Hover
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
|
|
14
15
|
const HOVER_ATTR = 'sbHover';
|
|
16
|
+
const CONTINUOUS_HOVER_ATTR = 'sbContinuousHover';
|
|
15
17
|
|
|
16
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Sends `hover` as a mouse enters and leaves the boxes marked `data-sb-hover`
|
|
20
|
+
* under `root`, and `continuoushover` as it moves over the boxes marked
|
|
21
|
+
* `data-sb-continuous-hover` (its value the space of the location: the box's
|
|
22
|
+
* own, or the root's), one per animation frame per box, then `ended` as it
|
|
23
|
+
* leaves each.
|
|
24
|
+
*/
|
|
17
25
|
export class HoverTracker {
|
|
18
26
|
private readonly hovered = new Set<HTMLElement>();
|
|
27
|
+
/** The boxes the mouse is over that follow it, with the location to send at the next frame. */
|
|
28
|
+
private readonly followed = new Map<HTMLElement, { x: number; y: number } | null>();
|
|
29
|
+
private frame: number | null = null;
|
|
19
30
|
|
|
20
31
|
constructor(
|
|
21
32
|
private readonly root: HTMLElement,
|
|
22
|
-
private readonly send: (
|
|
33
|
+
private readonly send: (event: HoverEvent | ContinuousHoverEvent) => void,
|
|
23
34
|
) {
|
|
24
35
|
root.addEventListener('pointerover', this.onOver);
|
|
25
36
|
root.addEventListener('pointerout', this.onOut);
|
|
37
|
+
root.addEventListener('pointermove', this.onMove);
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
dispose(): void {
|
|
29
41
|
this.root.removeEventListener('pointerover', this.onOver);
|
|
30
42
|
this.root.removeEventListener('pointerout', this.onOut);
|
|
43
|
+
this.root.removeEventListener('pointermove', this.onMove);
|
|
31
44
|
this.hovered.clear();
|
|
45
|
+
this.followed.clear();
|
|
46
|
+
if (this.frame !== null) cancelAnimationFrame(this.frame);
|
|
47
|
+
this.frame = null;
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
private readonly onOver = (event: PointerEvent): void => {
|
|
35
51
|
if (event.pointerType === 'touch') return;
|
|
36
52
|
for (let el = event.target as HTMLElement | null; el && el !== this.root; el = el.parentElement) {
|
|
37
|
-
if (el.dataset[HOVER_ATTR]
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
if (el.dataset[HOVER_ATTR] !== undefined && !this.hovered.has(el)) {
|
|
54
|
+
this.hovered.add(el);
|
|
55
|
+
this.report(el, true);
|
|
56
|
+
}
|
|
57
|
+
if (el.dataset[CONTINUOUS_HOVER_ATTR] !== undefined && !this.followed.has(el)) this.followed.set(el, this.locationIn(el, event));
|
|
58
|
+
}
|
|
59
|
+
if (this.followed.size) this.schedule();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
private readonly onMove = (event: PointerEvent): void => {
|
|
63
|
+
if (event.pointerType === 'touch' || !this.followed.size) return;
|
|
64
|
+
for (let el = event.target as HTMLElement | null; el && el !== this.root; el = el.parentElement) {
|
|
65
|
+
if (this.followed.has(el)) this.followed.set(el, this.locationIn(el, event));
|
|
40
66
|
}
|
|
67
|
+
this.schedule();
|
|
41
68
|
};
|
|
42
69
|
|
|
43
70
|
private readonly onOut = (event: PointerEvent): void => {
|
|
@@ -48,11 +75,42 @@ export class HoverTracker {
|
|
|
48
75
|
this.hovered.delete(el);
|
|
49
76
|
this.report(el, false);
|
|
50
77
|
}
|
|
78
|
+
for (const el of [...this.followed.keys()]) {
|
|
79
|
+
if (next && el.contains(next)) continue;
|
|
80
|
+
this.followed.delete(el);
|
|
81
|
+
const id = this.idOf(el);
|
|
82
|
+
if (id !== null) this.send({ type: 'continuoushover', id, phase: 'ended' });
|
|
83
|
+
}
|
|
51
84
|
};
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
/** Where the mouse is, in the box's own coordinates or the root's, as the box's style asks. */
|
|
87
|
+
private locationIn(el: HTMLElement, event: PointerEvent): { x: number; y: number } {
|
|
88
|
+
const rect = (el.dataset[CONTINUOUS_HOVER_ATTR] === 'global' ? this.root : el).getBoundingClientRect();
|
|
89
|
+
return { x: Math.round((event.clientX - rect.left) * 100) / 100, y: Math.round((event.clientY - rect.top) * 100) / 100 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The followed boxes' locations go out at the next frame, however many moves came in. */
|
|
93
|
+
private schedule(): void {
|
|
94
|
+
if (this.frame !== null) return;
|
|
95
|
+
this.frame = requestAnimationFrame(() => {
|
|
96
|
+
this.frame = null;
|
|
97
|
+
for (const [el, location] of this.followed) {
|
|
98
|
+
if (!location) continue;
|
|
99
|
+
this.followed.set(el, null);
|
|
100
|
+
const id = this.idOf(el);
|
|
101
|
+
if (id !== null) this.send({ type: 'continuoushover', id, phase: 'active', location });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private idOf(el: HTMLElement): ElementId | null {
|
|
54
107
|
const id = Number(el.dataset['sbId']);
|
|
55
|
-
|
|
108
|
+
return Number.isInteger(id) && el.isConnected ? id : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private report(el: HTMLElement, value: boolean): void {
|
|
112
|
+
const id = this.idOf(el);
|
|
113
|
+
if (id !== null) this.send({ type: 'hover', id, value });
|
|
56
114
|
}
|
|
57
115
|
}
|
|
58
116
|
|
|
@@ -87,6 +145,8 @@ const triggers = new WeakMap<HTMLElement, number>();
|
|
|
87
145
|
/** Apply the pointer and effect fields of a `styled` element's style (called on create and update). */
|
|
88
146
|
export function applyPointerAndEffects(el: HTMLElement, style: Style): void {
|
|
89
147
|
if (style.hover) el.dataset[HOVER_ATTR] = '';
|
|
148
|
+
if (style.continuousHover) el.dataset[CONTINUOUS_HOVER_ATTR] = style.continuousHover;
|
|
149
|
+
else delete el.dataset[CONTINUOUS_HOVER_ATTR];
|
|
90
150
|
if (style.help) {
|
|
91
151
|
el.title = style.help;
|
|
92
152
|
el.dataset['sbHelp'] = '';
|
package/src/protocol.ts
CHANGED
|
@@ -1593,6 +1593,7 @@ export type RendererEvent =
|
|
|
1593
1593
|
| LazyEvent
|
|
1594
1594
|
| MenuEvent
|
|
1595
1595
|
| HoverEvent
|
|
1596
|
+
| ContinuousHoverEvent
|
|
1596
1597
|
// Scroll geometry and animation completion (the block at the end of this file)
|
|
1597
1598
|
| ScrollGeometryEvent
|
|
1598
1599
|
| AnimationCompleteEvent
|
|
@@ -2694,6 +2695,13 @@ export interface SymbolEffectStyle {
|
|
|
2694
2695
|
export interface Style {
|
|
2695
2696
|
/** The page sends `hover` events as a mouse enters and leaves the box (`.onHover`). */
|
|
2696
2697
|
hover?: boolean;
|
|
2698
|
+
/**
|
|
2699
|
+
* `.onContinuousHover(coordinateSpace:)`: the page sends `continuoushover`
|
|
2700
|
+
* events as a mouse moves over the box, its location in the box's own
|
|
2701
|
+
* coordinates (`local`) or the root's, scrolling included (`global`, the
|
|
2702
|
+
* same a `reportsFrame` geometry event reports), and once as it leaves.
|
|
2703
|
+
*/
|
|
2704
|
+
continuousHover?: 'local' | 'global';
|
|
2697
2705
|
/** `.help(_:)`: a tooltip on the box. */
|
|
2698
2706
|
help?: string;
|
|
2699
2707
|
/** `.pointerStyle(_:)`: the CSS cursor over the box (`pointer`, `text`, `grab`…). */
|
|
@@ -2735,6 +2743,13 @@ export interface HoverEvent {
|
|
|
2735
2743
|
value: boolean;
|
|
2736
2744
|
}
|
|
2737
2745
|
|
|
2746
|
+
/**
|
|
2747
|
+
* A mouse (or pen) over a box whose style has `continuousHover`: `active`
|
|
2748
|
+
* with where it is, coalesced to one per animation frame while it moves,
|
|
2749
|
+
* then `ended` once as it leaves. Touch never hovers.
|
|
2750
|
+
*/
|
|
2751
|
+
export type ContinuousHoverEvent = { type: 'continuoushover'; id: ElementId; phase: 'active'; location: Point } | { type: 'continuoushover'; id: ElementId; phase: 'ended' };
|
|
2752
|
+
|
|
2738
2753
|
export interface ScrollToCommand {
|
|
2739
2754
|
anchor: UnitPoint | null;
|
|
2740
2755
|
/** Increases with every request (across readers); the page carries out each serial once. */
|
package/src/renderer.ts
CHANGED
|
@@ -422,7 +422,7 @@ export class Renderer {
|
|
|
422
422
|
* for size changes, and the measurements of the current layout pass.
|
|
423
423
|
*/
|
|
424
424
|
private readonly hostObserver: ResizeObserver | null = null;
|
|
425
|
-
/** `hover` events for boxes whose style asks for them (pointerEffects.ts). */
|
|
425
|
+
/** `hover` and `continuoushover` events for boxes whose style asks for them (pointerEffects.ts). */
|
|
426
426
|
private readonly hover: HoverTracker;
|
|
427
427
|
/** A frame check of the `reportsFrame` readers is queued for the next animation frame (after a scroll). */
|
|
428
428
|
private frameCheckQueued = false;
|
|
@@ -1171,7 +1171,7 @@ export class Renderer {
|
|
|
1171
1171
|
this.stopRichEditors = listenToRichEditors(root, editorId, (event) => this.onEvent(event));
|
|
1172
1172
|
// After `onInput` above, so a keystroke's `text` goes before its `selection`.
|
|
1173
1173
|
this.stopPlainEditors = listenToPlainEditors(root, editorId, (event) => this.onEvent(event));
|
|
1174
|
-
this.hover = new HoverTracker(root, (
|
|
1174
|
+
this.hover = new HoverTracker(root, (event) => this.onEvent(event));
|
|
1175
1175
|
root.addEventListener('scroll', this.queueFrameCheck, { capture: true, passive: true });
|
|
1176
1176
|
// Scroll transitions and pinned views follow the scroll in the frame it is drawn in.
|
|
1177
1177
|
root.addEventListener('scroll', drawScrollTransitions, { capture: true, passive: true });
|
package/src/styles.css
CHANGED
|
@@ -4499,9 +4499,11 @@ svg.sb-path {
|
|
|
4499
4499
|
height: auto;
|
|
4500
4500
|
}
|
|
4501
4501
|
|
|
4502
|
-
/* `.onHover` (pointerEffects.ts): the whole box
|
|
4503
|
-
view's frame, so a hover box is hit in its
|
|
4504
|
-
.
|
|
4502
|
+
/* `.onHover` and `.onContinuousHover` (pointerEffects.ts): the whole box
|
|
4503
|
+
hovers, as SwiftUI uses the view's frame, so a hover box is hit in its
|
|
4504
|
+
empty space too. */
|
|
4505
|
+
.sb-styled[data-sb-hover],
|
|
4506
|
+
.sb-styled[data-sb-continuous-hover] {
|
|
4505
4507
|
pointer-events: auto;
|
|
4506
4508
|
}
|
|
4507
4509
|
|