@swiftbrowser/web 0.3.1 → 0.5.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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * iOS 26's scroll edge effect at the bottom: a list or scroll view that runs
3
+ * down under a tab bar, or under its screen's bottom toolbar or search field,
4
+ * has its content inset by the bar (the engine's `bottomInset` chrome rect,
5
+ * layout/engine.ts). While content is under the bar (more of it below) its
6
+ * screen, or the tab it fills, shows the effect over it, behind the bar:
7
+ * `data-sb-scrolled-bottom` with `--sb-bottom-edge` the bar's height and
8
+ * `--sb-edge-bottom` its strength, which ramps down over the last
9
+ * `EDGE_RAMP` px of content (styles.css). The top edge's effect is the
10
+ * bar's own (largeTitle.ts).
11
+ *
12
+ * `.scrollEdgeEffectStyle(_:for:)` / `.scrollEdgeEffectHidden(_:for:)` reach
13
+ * the scroll view as `edgeEffect` props, which become `data-sb-edge-top` /
14
+ * `data-sb-edge-bottom` on it (`hard` or `hidden`; soft is the default) and
15
+ * are copied to the element that draws that edge's effect.
16
+ */
17
+
18
+ import type { EdgeEffectProps } from './protocol';
19
+ import type { Rect } from './layout';
20
+
21
+ /**
22
+ * How far content has to be under a bar for its edge effect to be at full
23
+ * strength: the effect ramps up over this scroll distance (and back down as
24
+ * the end comes clear of a bottom bar) rather than switching on, which stood
25
+ * out over content that is not the background color (a photo).
26
+ */
27
+ export const EDGE_RAMP = 40;
28
+
29
+ /** The strength (0…1) of an edge effect over `distance` px of content under its bar, as a CSS value. */
30
+ export function edgeStrength(distance: number): string {
31
+ return String(Math.round(Math.min(1, Math.max(0, distance / EDGE_RAMP)) * 1000) / 1000);
32
+ }
33
+
34
+ /** Sets a CSS custom property, untouched when unchanged. */
35
+ export function setProperty(el: HTMLElement, name: string, value: string): void {
36
+ if (el.style.getPropertyValue(name) !== value) el.style.setProperty(name, value);
37
+ }
38
+
39
+ const bound = new WeakSet<HTMLElement>();
40
+ /** The scroll view whose state each screen's (or tab's) bottom effect shows. */
41
+ const claimedBy = new WeakMap<HTMLElement, HTMLElement>();
42
+
43
+ /** A list's or scroll view's `edgeEffect` props, as `data-sb-edge-top` / `data-sb-edge-bottom`. */
44
+ export function applyEdgeEffect(el: HTMLElement, effect: EdgeEffectProps | undefined): void {
45
+ setValue(el, 'sbEdgeTop', edgeValue(effect?.top));
46
+ setValue(el, 'sbEdgeBottom', edgeValue(effect?.bottom));
47
+ }
48
+
49
+ /** Soft is the default effect; only the others are marked. */
50
+ function edgeValue(value: string | undefined): string | null {
51
+ return value === 'hard' || value === 'hidden' ? value : null;
52
+ }
53
+
54
+ /** The element that draws the bottom effect over a scroll view: its screen, else the tab it fills. */
55
+ function ownerOf(scroller: HTMLElement): HTMLElement | null {
56
+ return scroller.closest<HTMLElement>('.sb-navscreen') ?? scroller.closest<HTMLElement>('.sb-tab');
57
+ }
58
+
59
+ /**
60
+ * Called for every list and scroll view the layout reaches with the engine's
61
+ * `bottomInset` rect (absent when no bar covers its bottom).
62
+ */
63
+ export function bindScrollEdgeBottom(scroller: HTMLElement, covered: Rect | undefined): void {
64
+ const owner = ownerOf(scroller);
65
+ if (covered === undefined || covered.height <= 0) {
66
+ if (scroller.dataset['sbUnderBottom'] !== undefined) delete scroller.dataset['sbUnderBottom'];
67
+ if (owner && claimedBy.get(owner) === scroller) release(owner);
68
+ return;
69
+ }
70
+ setValue(scroller, 'sbUnderBottom', '');
71
+ scroller.style.setProperty('--sb-bottom-edge', `${covered.height}px`);
72
+ if (!bound.has(scroller)) {
73
+ bound.add(scroller);
74
+ scroller.addEventListener('scroll', () => syncScrollEdgeBottom(scroller), { passive: true });
75
+ }
76
+ syncScrollEdgeBottom(scroller);
77
+ }
78
+
79
+ export function syncScrollEdgeBottom(scroller: HTMLElement): void {
80
+ if (scroller.dataset['sbUnderBottom'] === undefined) return;
81
+ const owner = ownerOf(scroller);
82
+ if (!owner) return;
83
+ claimedBy.set(owner, scroller);
84
+ // Content is under the bar until the end has scrolled clear of it (the
85
+ // inset is room past the content's end).
86
+ const remaining = scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop;
87
+ setValue(owner, 'sbScrolledBottom', remaining > 1 ? '' : null);
88
+ setProperty(owner, '--sb-edge-bottom', edgeStrength(remaining));
89
+ setValue(owner, 'sbEdgeBottom', scroller.dataset['sbEdgeBottom'] ?? null);
90
+ const height = scroller.style.getPropertyValue('--sb-bottom-edge');
91
+ if (owner.style.getPropertyValue('--sb-bottom-edge') !== height) owner.style.setProperty('--sb-bottom-edge', height);
92
+ }
93
+
94
+ function release(owner: HTMLElement): void {
95
+ claimedBy.delete(owner);
96
+ setValue(owner, 'sbScrolledBottom', null);
97
+ owner.style.removeProperty('--sb-edge-bottom');
98
+ setValue(owner, 'sbEdgeBottom', null);
99
+ owner.style.removeProperty('--sb-bottom-edge');
100
+ }
101
+
102
+ /** Sets a data attribute (by dataset key) to `value`, or removes it for null; untouched when unchanged. */
103
+ export function setValue(el: HTMLElement, key: string, value: string | null): void {
104
+ if (value === null) {
105
+ if (el.dataset[key] !== undefined) delete el.dataset[key];
106
+ } else if (el.dataset[key] !== value) {
107
+ el.dataset[key] = value;
108
+ }
109
+ }
package/src/scrollTo.ts CHANGED
@@ -95,6 +95,12 @@ export function scrollToTarget(
95
95
  const viewport: Rect = { ...frame };
96
96
  const covered = result.chrome.get(at)?.['bottomInset'];
97
97
  if (covered) viewport.height = Math.max(0, covered.y - frame.y);
98
+ // Under its screen's bar the visible area starts below the bar.
99
+ const under = result.chrome.get(at)?.['topInset'];
100
+ if (under) {
101
+ viewport.y += under.height;
102
+ viewport.height = Math.max(0, viewport.height - under.height);
103
+ }
98
104
  let left = el.scrollLeft;
99
105
  let top = el.scrollTop;
100
106
  let changed = false;