@uniweb/runtime 1.1.20 → 1.2.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.
- package/dist/ssr.js +57 -43
- package/dist/ssr.js.map +1 -1
- package/package.json +2 -2
- package/src/collect-records.js +7 -1
- package/src/components/BlockRenderer.jsx +10 -2
- package/src/components/Layout.jsx +3 -2
- package/src/default-fetcher.js +4 -1
- package/src/hooks/useHeadMeta.js +42 -7
- package/src/load-page-data.js +6 -1
- package/src/page-renderer.js +21 -40
- package/src/prepare-props.js +31 -19
- package/src/require-transport.js +35 -0
- package/src/ssr-renderer.js +60 -39
- package/src/sticky-check.js +106 -33
- package/src/sticky.js +5 -3
package/src/sticky-check.js
CHANGED
|
@@ -4,13 +4,23 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `position: sticky` keeps an element inside its parent's box. When the parent is no taller than
|
|
6
6
|
* the element (no wider, for one that sticks sideways), the element has no room and scrolls away
|
|
7
|
-
* with the page, and nothing says so.
|
|
7
|
+
* with the page, and nothing says so. Whose box the parent is says what to do (`unmovableAdvice`):
|
|
8
8
|
*
|
|
9
|
-
* - a component's own `sticky top-0
|
|
10
|
-
* never moves. The section's `sticky: true` asks the runtime
|
|
11
|
-
* (`sticky.js`).
|
|
12
|
-
* - a
|
|
13
|
-
* The layout makes its element sticky when `page.isSticky(area)`.
|
|
9
|
+
* - a component's outermost element — a header's own `sticky top-0` — sits in the section's
|
|
10
|
+
* wrapper, as tall as itself, so it never moves. The section's `sticky: true` asks the runtime
|
|
11
|
+
* to make sticky the box that can (`sticky.js`).
|
|
12
|
+
* - a box the runtime made sticky (`data-uw-sticky`) in an element a layout wraps it in, which
|
|
13
|
+
* confines it. The layout makes its element sticky when `page.isSticky(area)`.
|
|
14
|
+
* - an element inside a component sits in a box the component draws. A side panel beside a list
|
|
15
|
+
* sticks within the row the two share, and cannot when its box is only as tall as the panel:
|
|
16
|
+
* the fix is the component's, to give that box room or make it the sticky one.
|
|
17
|
+
*
|
|
18
|
+
* ⭐ MEASURED ON THE SETTLED PAGE. A box's height can wait on the page's data — a list fills its
|
|
19
|
+
* column once its records arrive — so the check runs once nothing is in flight (the layout passes
|
|
20
|
+
* `website.dataStore.whenIdle` as `settled`), after the page's fonts, in the frames after that:
|
|
21
|
+
* React has committed what arrived, and the browser has laid it out. ⛔ Until 2026-10-25 it
|
|
22
|
+
* measured in the first frame after the page mounted, while the page's fetches were still out,
|
|
23
|
+
* and named a side panel whose column was waiting for its records as one that could never move.
|
|
14
24
|
*
|
|
15
25
|
* Table parts are left out: a sticky table header cell sticks within its table, though its row is
|
|
16
26
|
* no taller than it is. So is a child section the runtime made sticky (`data-uw-sticky="child"`):
|
|
@@ -25,6 +35,22 @@
|
|
|
25
35
|
|
|
26
36
|
const warned = new WeakSet()
|
|
27
37
|
|
|
38
|
+
/** Every section wrapper the browser's renderer drew, noted in dev (`BlockRenderer.jsx`). */
|
|
39
|
+
const sectionWrappers = new WeakSet()
|
|
40
|
+
|
|
41
|
+
/** How long the check waits on the page's fetches before it measures anyway. */
|
|
42
|
+
const SETTLE_CAP_MS = 10_000
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Note a section's wrapper, so the check can tell a component's outermost element — the one in
|
|
46
|
+
* it — from an element inside a component. A ref callback's argument: null on unmount.
|
|
47
|
+
*
|
|
48
|
+
* @param {Element|null} element
|
|
49
|
+
*/
|
|
50
|
+
export function noteSectionWrapper(element) {
|
|
51
|
+
if (element) sectionWrappers.add(element)
|
|
52
|
+
}
|
|
53
|
+
|
|
28
54
|
/** The nearest ancestor that draws a box — `display: contents` draws none. */
|
|
29
55
|
function boxParent(el) {
|
|
30
56
|
let parent = el.parentElement
|
|
@@ -73,38 +99,85 @@ export function clippingAncestor(el) {
|
|
|
73
99
|
}
|
|
74
100
|
|
|
75
101
|
/**
|
|
76
|
-
*
|
|
102
|
+
* What to do about a sticky element that cannot move, by whose box holds it (see above).
|
|
77
103
|
*
|
|
78
|
-
* @param {
|
|
79
|
-
* @returns {
|
|
104
|
+
* @param {Element} el
|
|
105
|
+
* @returns {string}
|
|
80
106
|
*/
|
|
81
|
-
export function
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
export function unmovableAdvice(el) {
|
|
108
|
+
if (el.getAttribute('data-uw-sticky')) {
|
|
109
|
+
return (
|
|
110
|
+
'It is the box the runtime makes sticky for a section, and an element of the layout around it ' +
|
|
111
|
+
'confines it: a layout that wraps an area in an element of its own makes that element sticky ' +
|
|
112
|
+
'when `page.isSticky(area)`.'
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
if (sectionWrappers.has(boxParent(el))) {
|
|
116
|
+
return (
|
|
117
|
+
"It is a section's outermost element, and the section's wrapper is as tall as it is. To keep the " +
|
|
118
|
+
'section in view, give it `sticky: true` (its type may default it in meta.js), and the runtime ' +
|
|
119
|
+
'makes sticky the box that can.'
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
return (
|
|
123
|
+
'It sits in a box the component draws. Give that box room — let it stretch with its row rather ' +
|
|
124
|
+
'than fit its content (`items-start` or `self-start` shrinks it) — or make that box the sticky one.'
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Warn, once per element, about each sticky element on the page that cannot move. */
|
|
129
|
+
function checkPage(doc) {
|
|
130
|
+
for (const el of doc.body.querySelectorAll('*')) {
|
|
131
|
+
if (warned.has(el) || getComputedStyle(el).position !== 'sticky') continue
|
|
132
|
+
const clip = clippingAncestor(el)
|
|
133
|
+
if (clip) {
|
|
99
134
|
warned.add(el)
|
|
100
135
|
console.warn(
|
|
101
|
-
'[uniweb] This element is sticky but
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'wraps an area in an element of its own makes that element sticky when `page.isSticky(area)`.',
|
|
136
|
+
'[uniweb] This element is sticky but sits in a box that clips its overflow (`overflow: hidden`, ' +
|
|
137
|
+
'or `overflow-x: hidden`), so it sticks to that box, which does not scroll, and never moves. ' +
|
|
138
|
+
'Clip with `overflow: clip` instead: it clips the same and keeps the element sticking.',
|
|
105
139
|
el,
|
|
140
|
+
clip,
|
|
106
141
|
)
|
|
142
|
+
continue
|
|
107
143
|
}
|
|
108
|
-
|
|
109
|
-
|
|
144
|
+
if (el.getAttribute('data-uw-sticky') === 'child' || canStick(el)) continue
|
|
145
|
+
warned.add(el)
|
|
146
|
+
console.warn(
|
|
147
|
+
'[uniweb] This element is sticky but cannot move: its parent is no taller than it is, so it ' +
|
|
148
|
+
`scrolls away with the page. ${unmovableAdvice(el)}`,
|
|
149
|
+
el,
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check the page once it has settled: when `settled()` resolves (or after `SETTLE_CAP_MS`, so a
|
|
156
|
+
* fetch that never answers does not silence the check), its fonts have loaded, and two frames have
|
|
157
|
+
* passed — the first for React to commit what arrived, the second to lay it out.
|
|
158
|
+
*
|
|
159
|
+
* @param {Document} [doc]
|
|
160
|
+
* @param {{ settled?: () => Promise<unknown> }} [options] - what the page waits on: its data
|
|
161
|
+
* @returns {() => void} cancels a check not yet run
|
|
162
|
+
*/
|
|
163
|
+
export function warnUnmovableSticky(doc = globalThis.document, { settled } = {}) {
|
|
164
|
+
if (!doc?.body || typeof requestAnimationFrame !== 'function') return () => {}
|
|
165
|
+
let cancelled = false
|
|
166
|
+
let frame = 0
|
|
167
|
+
let cap = 0
|
|
168
|
+
const nextFrame = () => new Promise((resolve) => { frame = requestAnimationFrame(resolve) })
|
|
169
|
+
const capped = new Promise((resolve) => { cap = setTimeout(resolve, SETTLE_CAP_MS) })
|
|
170
|
+
;(async () => {
|
|
171
|
+
await Promise.race([Promise.resolve(settled?.()).catch(() => {}), capped])
|
|
172
|
+
clearTimeout(cap)
|
|
173
|
+
await doc.fonts?.ready
|
|
174
|
+
if (!cancelled) await nextFrame()
|
|
175
|
+
if (!cancelled) await nextFrame()
|
|
176
|
+
if (!cancelled) checkPage(doc)
|
|
177
|
+
})()
|
|
178
|
+
return () => {
|
|
179
|
+
cancelled = true
|
|
180
|
+
clearTimeout(cap)
|
|
181
|
+
cancelAnimationFrame(frame)
|
|
182
|
+
}
|
|
110
183
|
}
|
package/src/sticky.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* (`components/Layout.jsx`, `BlockRenderer.jsx`) and the prerender's (`ssr-renderer.js`) make the
|
|
4
4
|
* same boxes sticky, and a prerendered page does not change when the browser renders over it.
|
|
5
5
|
*
|
|
6
|
-
* ⭐
|
|
7
|
-
* parent's box, and a component
|
|
8
|
-
*
|
|
6
|
+
* ⭐ A COMPONENT'S OUTERMOST ELEMENT CANNOT STICK. `position: sticky` moves an element only within
|
|
7
|
+
* its parent's box, and a component sits in boxes as tall as itself: the section's wrapper, the
|
|
8
|
+
* layout area's wrapper (`area-wrappers.js`), the built-in layout's zone (`default-layout.js`). (An
|
|
9
|
+
* element inside a component can stick within a taller box the component draws — a side panel in
|
|
10
|
+
* the row beside a list — which is the component's own layout and asks nothing of this module.)
|
|
9
11
|
* So a section asks — `sticky:`, one of a section's own settings (`@uniweb/schemas/section`),
|
|
10
12
|
* defaulted by its type's `meta.js` — and the runtime makes sticky the box that can:
|
|
11
13
|
*
|