@jsenv/navi 0.29.431 → 0.29.433
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/dev/jsenv_navi.js +573 -33
- package/dist/dev/jsenv_navi.js.map +15 -9
- package/dist/jsenv_navi.js +573 -33
- package/dist/jsenv_navi.js.map +15 -9
- package/docs/AI_INSTRUCTIONS.md +3 -1
- package/docs/popup_backdrop.md +53 -0
- package/docs/route_transitions.md +3 -2
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -6,7 +6,7 @@ import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, win
|
|
|
6
6
|
import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, ELEMENT_SIZE_CHANGE, getScrollContainer, scrollIntoViewScoped, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, keepTouchRefusable, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createInternalCustomEvent, activeElementSignal, normalizeStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, trapScrollInside, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange, getVirtualKeyboardOverlayHeight } from "@jsenv/dom";
|
|
7
7
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
8
8
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
9
|
-
import { options, isValidElement, createContext, render, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
9
|
+
import { options, isValidElement, createContext, render as render$1, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
10
10
|
import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useEffect, useCallback, useReducer, useId } from "preact/hooks";
|
|
11
11
|
import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
|
|
12
12
|
export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
|
|
@@ -11109,6 +11109,11 @@ const [publishRouteRender, observeRouteRender] = createPubSub();
|
|
|
11109
11109
|
const STORAGE_KEY = "navi_scroll_positions";
|
|
11110
11110
|
|
|
11111
11111
|
const positionByUrl = new Map();
|
|
11112
|
+
// Where the document stands, as its scroll events last said, whoever it
|
|
11113
|
+
// belongs to: what a url shown without the document moving is given (see
|
|
11114
|
+
// stayInPlace). Null until the restoration is installed — nothing is recorded
|
|
11115
|
+
// before.
|
|
11116
|
+
let documentOffset = null;
|
|
11112
11117
|
// url -> (scroller name -> position). The position is whatever the scroller
|
|
11113
11118
|
// handed in: what it can put itself back on, in its own terms.
|
|
11114
11119
|
const scrollerPositionsByUrl = new Map();
|
|
@@ -11225,29 +11230,33 @@ const installScrollRestoration = () => {
|
|
|
11225
11230
|
}
|
|
11226
11231
|
window.history.scrollRestoration = "manual";
|
|
11227
11232
|
loadStore();
|
|
11233
|
+
documentOffset = { x: window.scrollX, y: window.scrollY };
|
|
11228
11234
|
// Read as it happens rather than when leaving: a traverse changes the url
|
|
11229
11235
|
// before anything here is told, so a position read then would be read for
|
|
11230
11236
|
// the wrong page.
|
|
11231
11237
|
window.addEventListener(
|
|
11232
11238
|
"scroll",
|
|
11233
11239
|
() => {
|
|
11240
|
+
documentOffset = { x: window.scrollX, y: window.scrollY };
|
|
11234
11241
|
if (suspendCount) {
|
|
11235
11242
|
return;
|
|
11236
11243
|
}
|
|
11237
|
-
positionByUrl.set(window.location.href,
|
|
11238
|
-
x: window.scrollX,
|
|
11239
|
-
y: window.scrollY,
|
|
11240
|
-
});
|
|
11244
|
+
positionByUrl.set(window.location.href, documentOffset);
|
|
11241
11245
|
},
|
|
11242
11246
|
{ passive: true },
|
|
11243
11247
|
);
|
|
11248
|
+
const positionOnLoad = positionByUrl.get(window.location.href);
|
|
11249
|
+
if (!positionOnLoad) {
|
|
11250
|
+
// The first page, where the browser landed it.
|
|
11251
|
+
stayInPlace(window.location.href);
|
|
11252
|
+
return;
|
|
11253
|
+
}
|
|
11244
11254
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
11245
11255
|
// Once, and at the first render of a route: the position is only meaningful
|
|
11246
11256
|
// once there is a page under it.
|
|
11247
11257
|
// Written once the commit that rendered it has ended: said from inside it,
|
|
11248
11258
|
// and what the offset shows may still have to be built (see scrollTo).
|
|
11249
|
-
|
|
11250
|
-
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
11259
|
+
if (positionOnLoad.x || positionOnLoad.y) {
|
|
11251
11260
|
const stopListening = observeRouteRender(() => {
|
|
11252
11261
|
stopListening();
|
|
11253
11262
|
queueMicrotask(() => {
|
|
@@ -11330,9 +11339,15 @@ const restoreScrollPositionOnReturn = (url) => {
|
|
|
11330
11339
|
// app that scrolls an element of its own scrolls it itself.
|
|
11331
11340
|
const startAtTop = (url, { from } = {}) => {
|
|
11332
11341
|
if (!isArrival(url, { from })) {
|
|
11342
|
+
stayInPlace(url);
|
|
11333
11343
|
return;
|
|
11334
11344
|
}
|
|
11335
11345
|
arrivalCount++;
|
|
11346
|
+
// Written down as well as done: a document already at its top fires no
|
|
11347
|
+
// scroll event, and a page left without ever being scrolled would have no
|
|
11348
|
+
// position to come back to — the return would keep the offset of the page
|
|
11349
|
+
// being left.
|
|
11350
|
+
positionByUrl.set(new URL(url, window.location.href).href, { x: 0, y: 0 });
|
|
11336
11351
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
11337
11352
|
};
|
|
11338
11353
|
const isArrival = (url, { from }) => {
|
|
@@ -11346,6 +11361,23 @@ const isArrival = (url, { from }) => {
|
|
|
11346
11361
|
return true;
|
|
11347
11362
|
};
|
|
11348
11363
|
|
|
11364
|
+
// A url the document is shown under without moving: a replace, a push that
|
|
11365
|
+
// keeps the page (see startAtTop), the first page loaded. No scroll event says
|
|
11366
|
+
// where it stands, so it is written down here, or a return to it would find
|
|
11367
|
+
// nothing and keep the offset of the page being left. The offset the scroll
|
|
11368
|
+
// events last told, not read again: a read here would lay the page out in the
|
|
11369
|
+
// middle of a routing, once per address a settling param writes.
|
|
11370
|
+
//
|
|
11371
|
+
// Deaf with the recording: a row of tabs replaces the url as it travels, and
|
|
11372
|
+
// the offset belongs to nobody until the row says where its tab lands (see
|
|
11373
|
+
// arriveAtScrollPosition).
|
|
11374
|
+
const stayInPlace = (url) => {
|
|
11375
|
+
if (documentOffset === null || suspendCount) {
|
|
11376
|
+
return;
|
|
11377
|
+
}
|
|
11378
|
+
positionByUrl.set(new URL(url, window.location.href).href, documentOffset);
|
|
11379
|
+
};
|
|
11380
|
+
|
|
11349
11381
|
// The same arrival, for the page's own scrollers. The document is scrolled to
|
|
11350
11382
|
// its top once the page is there; a list opens where it decides to in its
|
|
11351
11383
|
// first render, so what it must not find is dropped before the routing
|
|
@@ -11821,14 +11853,18 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
11821
11853
|
// also waits for the page it returns to: an offset is clamped to whatever
|
|
11822
11854
|
// page the document holds when it is written.
|
|
11823
11855
|
//
|
|
11824
|
-
// A replace
|
|
11825
|
-
// settling, a state written — and moving the reader
|
|
11826
|
-
// them out of a page they never left.
|
|
11827
|
-
//
|
|
11828
|
-
//
|
|
11829
|
-
//
|
|
11856
|
+
// A replace leaves the document where it is: it is the same place said
|
|
11857
|
+
// differently — a param settling, a state written — and moving the reader
|
|
11858
|
+
// for it would throw them out of a page they never left. Where they stay
|
|
11859
|
+
// is written down for the new address (see stayInPlace). A push that keeps
|
|
11860
|
+
// the pathname is read the same way, by startAtTop itself. The one replace
|
|
11861
|
+
// that IS an arrival is a row of tabs travelling, and the row says so for
|
|
11862
|
+
// itself (see route_travel.jsx) — from inside the routing above, which is
|
|
11863
|
+
// why the replace is written down after it.
|
|
11830
11864
|
if (navigationType === "push") {
|
|
11831
11865
|
whenRenderingResumes(() => startAtTop(url, { from: urlLeft }));
|
|
11866
|
+
} else if (navigationType === "replace") {
|
|
11867
|
+
stayInPlace(url);
|
|
11832
11868
|
} else if (navigationType === "traverse") {
|
|
11833
11869
|
restoreScrollPositionOnReturn(url);
|
|
11834
11870
|
}
|
|
@@ -16346,7 +16382,7 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
|
16346
16382
|
},
|
|
16347
16383
|
children: jsx$1
|
|
16348
16384
|
});
|
|
16349
|
-
render(calloutJsx, calloutMessageElement);
|
|
16385
|
+
render$1(calloutJsx, calloutMessageElement);
|
|
16350
16386
|
};
|
|
16351
16387
|
|
|
16352
16388
|
// An HTML message is rendered through preact rather than innerHTML: what a
|
|
@@ -16369,7 +16405,7 @@ const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
|
16369
16405
|
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
16370
16406
|
// tree is still there.
|
|
16371
16407
|
const clearCalloutMessage = calloutMessageElement => {
|
|
16372
|
-
render(null, calloutMessageElement);
|
|
16408
|
+
render$1(null, calloutMessageElement);
|
|
16373
16409
|
calloutMessageElement.innerHTML = "";
|
|
16374
16410
|
};
|
|
16375
16411
|
const domToVNodes = node => {
|
|
@@ -32262,10 +32298,222 @@ const useUrlSearchParam = (paramName, defaultValue) => {
|
|
|
32262
32298
|
return [value, setSearchParamValue];
|
|
32263
32299
|
};
|
|
32264
32300
|
|
|
32301
|
+
/**
|
|
32302
|
+
* The colour the browser paints around the page — Chrome's address bar on
|
|
32303
|
+
* Android, the status bar of an installed app — read from
|
|
32304
|
+
* `<meta name="theme-color">`. It sits right above the page's top edge, so it
|
|
32305
|
+
* reads as part of whatever is drawn there, and two layers decide it:
|
|
32306
|
+
*
|
|
32307
|
+
* - What the page asks for: the theme-color metas a `<Head>` brings. The last
|
|
32308
|
+
* Head to arrive wins, whatever order they leave in — the title's rule, for
|
|
32309
|
+
* the title's reason (see head.jsx).
|
|
32310
|
+
* - What the top layer lays over that edge: an open popup paints its backdrop
|
|
32311
|
+
* over what is under it, and its own surface too when it is flush with the
|
|
32312
|
+
* top edge. Popups sit above every Head, in opening order, and are painted
|
|
32313
|
+
* again from what is under them each time it changes — a page changing
|
|
32314
|
+
* under an open popup is dimmed by it all the same.
|
|
32315
|
+
*
|
|
32316
|
+
* The browser reads the first theme-color meta whose `media` matches, in tree
|
|
32317
|
+
* order: a Head appending its meta after an earlier one would never be read.
|
|
32318
|
+
* So navi writes the result into metas of its own placed before any other, and
|
|
32319
|
+
* a meta written in the HTML is the bottom of the stack. A popup paints over
|
|
32320
|
+
* every meta of the list, `media` kept, so whichever one the browser picks is
|
|
32321
|
+
* the one it would have picked, painted over.
|
|
32322
|
+
*/
|
|
32323
|
+
|
|
32324
|
+
const headEntries = [];
|
|
32325
|
+
const popupLayers = [];
|
|
32326
|
+
const ownMetas = [];
|
|
32327
|
+
|
|
32328
|
+
/**
|
|
32329
|
+
* @param {Array<{ content: string, media: string|null }>} themeColors
|
|
32330
|
+
* @returns {() => void} Takes them away.
|
|
32331
|
+
*/
|
|
32332
|
+
const pushHeadThemeColors = (themeColors) => {
|
|
32333
|
+
headEntries.push(themeColors);
|
|
32334
|
+
queueRender();
|
|
32335
|
+
return () => {
|
|
32336
|
+
headEntries.splice(headEntries.indexOf(themeColors), 1);
|
|
32337
|
+
queueRender();
|
|
32338
|
+
};
|
|
32339
|
+
};
|
|
32340
|
+
|
|
32341
|
+
/**
|
|
32342
|
+
* @param {object} paint
|
|
32343
|
+
* @param {string} [paint.themeColor] - Forces the colour, nothing is painted.
|
|
32344
|
+
* @param {string|null} [paint.backdropColor] - Laid over the page's colour.
|
|
32345
|
+
* @param {string|null} [paint.surfaceColor] - Laid over that, for a popup
|
|
32346
|
+
* flush with the top edge.
|
|
32347
|
+
* @returns {() => void} Takes the layer away.
|
|
32348
|
+
*/
|
|
32349
|
+
const pushPopupThemeColor = ({
|
|
32350
|
+
themeColor,
|
|
32351
|
+
backdropColor,
|
|
32352
|
+
surfaceColor,
|
|
32353
|
+
}) => {
|
|
32354
|
+
let layer;
|
|
32355
|
+
if (themeColor) {
|
|
32356
|
+
layer = { color: themeColor };
|
|
32357
|
+
} else {
|
|
32358
|
+
const paints = [];
|
|
32359
|
+
for (const color of [backdropColor, surfaceColor]) {
|
|
32360
|
+
if (!color) {
|
|
32361
|
+
continue;
|
|
32362
|
+
}
|
|
32363
|
+
const rgba = parseColor(color);
|
|
32364
|
+
if (rgba[3] > 0) {
|
|
32365
|
+
paints.push(rgba);
|
|
32366
|
+
}
|
|
32367
|
+
}
|
|
32368
|
+
// Nothing laid over the edge: the colour under it stays, as it is.
|
|
32369
|
+
if (paints.length === 0) {
|
|
32370
|
+
return () => {};
|
|
32371
|
+
}
|
|
32372
|
+
layer = { paints };
|
|
32373
|
+
}
|
|
32374
|
+
popupLayers.push(layer);
|
|
32375
|
+
queueRender();
|
|
32376
|
+
return () => {
|
|
32377
|
+
popupLayers.splice(popupLayers.indexOf(layer), 1);
|
|
32378
|
+
queueRender();
|
|
32379
|
+
};
|
|
32380
|
+
};
|
|
32381
|
+
|
|
32382
|
+
let renderQueued = false;
|
|
32383
|
+
// Once per commit: a Head re-rendering leaves and arrives again in the same
|
|
32384
|
+
// commit, and the browser must not be handed the colour from in between.
|
|
32385
|
+
const queueRender = () => {
|
|
32386
|
+
if (renderQueued) {
|
|
32387
|
+
return;
|
|
32388
|
+
}
|
|
32389
|
+
renderQueued = true;
|
|
32390
|
+
queueMicrotask(render);
|
|
32391
|
+
};
|
|
32392
|
+
|
|
32393
|
+
const render = () => {
|
|
32394
|
+
renderQueued = false;
|
|
32395
|
+
const htmlMetas = Array.from(
|
|
32396
|
+
document.head.querySelectorAll('meta[name="theme-color"]'),
|
|
32397
|
+
).filter((meta) => !ownMetas.includes(meta));
|
|
32398
|
+
const htmlColors = htmlMetas.map((meta) => ({
|
|
32399
|
+
content: meta.getAttribute("content"),
|
|
32400
|
+
media: meta.getAttribute("media"),
|
|
32401
|
+
}));
|
|
32402
|
+
let colors = [];
|
|
32403
|
+
for (let i = headEntries.length - 1; i >= 0; i--) {
|
|
32404
|
+
colors.push(...headEntries[i]);
|
|
32405
|
+
}
|
|
32406
|
+
colors.push(...htmlColors);
|
|
32407
|
+
for (const layer of popupLayers) {
|
|
32408
|
+
colors = paintLayer(layer, colors);
|
|
32409
|
+
}
|
|
32410
|
+
// What the HTML metas say reaches the browser without a copy.
|
|
32411
|
+
while (colors.length > 0 && htmlColors.includes(colors[colors.length - 1])) {
|
|
32412
|
+
colors.pop();
|
|
32413
|
+
}
|
|
32414
|
+
|
|
32415
|
+
while (ownMetas.length > colors.length) {
|
|
32416
|
+
ownMetas.pop().remove();
|
|
32417
|
+
}
|
|
32418
|
+
colors.forEach(({ content, media }, index) => {
|
|
32419
|
+
let meta = ownMetas[index];
|
|
32420
|
+
if (!meta) {
|
|
32421
|
+
meta = document.createElement("meta");
|
|
32422
|
+
meta.setAttribute("name", "theme-color");
|
|
32423
|
+
const previousMeta = ownMetas[index - 1];
|
|
32424
|
+
if (previousMeta) {
|
|
32425
|
+
previousMeta.after(meta);
|
|
32426
|
+
} else if (htmlMetas.length > 0) {
|
|
32427
|
+
htmlMetas[0].before(meta);
|
|
32428
|
+
} else {
|
|
32429
|
+
document.head.append(meta);
|
|
32430
|
+
}
|
|
32431
|
+
ownMetas.push(meta);
|
|
32432
|
+
}
|
|
32433
|
+
if (meta.getAttribute("content") !== content) {
|
|
32434
|
+
meta.setAttribute("content", content);
|
|
32435
|
+
}
|
|
32436
|
+
if (meta.getAttribute("media") !== media) {
|
|
32437
|
+
if (media === null) {
|
|
32438
|
+
meta.removeAttribute("media");
|
|
32439
|
+
} else {
|
|
32440
|
+
meta.setAttribute("media", media);
|
|
32441
|
+
}
|
|
32442
|
+
}
|
|
32443
|
+
});
|
|
32444
|
+
};
|
|
32445
|
+
|
|
32446
|
+
const paintLayer = (layer, colors) => {
|
|
32447
|
+
if (layer.color) {
|
|
32448
|
+
return [{ content: layer.color, media: null }];
|
|
32449
|
+
}
|
|
32450
|
+
const underColors =
|
|
32451
|
+
colors.length > 0 ? colors : [{ content: null, media: null }];
|
|
32452
|
+
const painted = [];
|
|
32453
|
+
for (const { content, media } of underColors) {
|
|
32454
|
+
let rgba = content === null ? null : parseUnderColor(content);
|
|
32455
|
+
for (const paint of layer.paints) {
|
|
32456
|
+
rgba = rgba ? composite(paint, rgba) : paint;
|
|
32457
|
+
}
|
|
32458
|
+
// Over no colour at all the browser keeps one of its own, which a
|
|
32459
|
+
// translucent paint cannot be laid over.
|
|
32460
|
+
if (Math.round(rgba[3] * 255) === 255) {
|
|
32461
|
+
painted.push({ content: toHex(rgba), media });
|
|
32462
|
+
}
|
|
32463
|
+
}
|
|
32464
|
+
return painted;
|
|
32465
|
+
};
|
|
32466
|
+
|
|
32467
|
+
// The browser shows a theme-color opaque, whatever alpha it was written with.
|
|
32468
|
+
const parseUnderColor = (color) => {
|
|
32469
|
+
const rgba = parseColor(color);
|
|
32470
|
+
if (rgba[3] === 0) {
|
|
32471
|
+
return null;
|
|
32472
|
+
}
|
|
32473
|
+
return [rgba[0], rgba[1], rgba[2], 1];
|
|
32474
|
+
};
|
|
32475
|
+
|
|
32476
|
+
// Source-over, the way the backdrop is drawn over the page.
|
|
32477
|
+
const composite = (over, under) => {
|
|
32478
|
+
const [r1, g1, b1, a1] = over;
|
|
32479
|
+
const [r2, g2, b2, a2] = under;
|
|
32480
|
+
const alpha = a1 + a2 * (1 - a1);
|
|
32481
|
+
const mix = (c1, c2) => (c1 * a1 + c2 * a2 * (1 - a1)) / alpha;
|
|
32482
|
+
return [mix(r1, r2), mix(g1, g2), mix(b1, b2), alpha];
|
|
32483
|
+
};
|
|
32484
|
+
|
|
32485
|
+
const toHex = ([r, g, b]) =>
|
|
32486
|
+
`#${[r, g, b]
|
|
32487
|
+
.map((channel) => Math.round(channel).toString(16).padStart(2, "0"))
|
|
32488
|
+
.join("")}`;
|
|
32489
|
+
|
|
32490
|
+
// A canvas reads every colour syntax the browser knows — oklch(), color(),
|
|
32491
|
+
// a keyword — and hands back sRGB bytes, which is what a theme-color meta is
|
|
32492
|
+
// sure to be understood in, whatever getComputedStyle answered with.
|
|
32493
|
+
let colorContext = null;
|
|
32494
|
+
const parseColor = (color) => {
|
|
32495
|
+
if (!colorContext) {
|
|
32496
|
+
const canvas = document.createElement("canvas");
|
|
32497
|
+
canvas.width = 1;
|
|
32498
|
+
canvas.height = 1;
|
|
32499
|
+
colorContext = canvas.getContext("2d", { willReadFrequently: true });
|
|
32500
|
+
}
|
|
32501
|
+
colorContext.clearRect(0, 0, 1, 1);
|
|
32502
|
+
// fillStyle ignores a colour it cannot parse: reset first, so that one draws
|
|
32503
|
+
// nothing rather than the previous colour.
|
|
32504
|
+
colorContext.fillStyle = "transparent";
|
|
32505
|
+
colorContext.fillStyle = color;
|
|
32506
|
+
colorContext.fillRect(0, 0, 1, 1);
|
|
32507
|
+
const [r, g, b, a] = colorContext.getImageData(0, 0, 1, 1).data;
|
|
32508
|
+
return [r, g, b, a / 255];
|
|
32509
|
+
};
|
|
32510
|
+
|
|
32265
32511
|
// The title belongs to the last Head to arrive, whatever order they leave in:
|
|
32266
32512
|
// the page being left by a route transition is taken down AFTER the page
|
|
32267
32513
|
// arriving is up (see keepLeavingPages in route.jsx), and putting back the
|
|
32268
|
-
// title it found would put its own page's title over the new one.
|
|
32514
|
+
// title it found would put its own page's title over the new one. A
|
|
32515
|
+
// theme-color meta follows the same rule, kept by theme_color.js, where the
|
|
32516
|
+
// popups of the top layer paint over it.
|
|
32269
32517
|
const titleEntries = [];
|
|
32270
32518
|
let titleWithoutHead = null;
|
|
32271
32519
|
const Head = ({
|
|
@@ -32277,6 +32525,7 @@ const Head = ({
|
|
|
32277
32525
|
}
|
|
32278
32526
|
const childArray = Array.isArray(children) ? children : [children];
|
|
32279
32527
|
const appendedElements = [];
|
|
32528
|
+
const themeColors = [];
|
|
32280
32529
|
let titleEntry = null;
|
|
32281
32530
|
for (const child of childArray) {
|
|
32282
32531
|
if (!child) {
|
|
@@ -32289,6 +32538,13 @@ const Head = ({
|
|
|
32289
32538
|
};
|
|
32290
32539
|
continue;
|
|
32291
32540
|
}
|
|
32541
|
+
if (child.type === "meta" && child.props.name === "theme-color") {
|
|
32542
|
+
themeColors.push({
|
|
32543
|
+
content: child.props.content,
|
|
32544
|
+
media: child.props.media ?? null
|
|
32545
|
+
});
|
|
32546
|
+
continue;
|
|
32547
|
+
}
|
|
32292
32548
|
const el = document.createElement(child.type);
|
|
32293
32549
|
const props = child.props || {};
|
|
32294
32550
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -32304,7 +32560,9 @@ const Head = ({
|
|
|
32304
32560
|
titleEntries.push(titleEntry);
|
|
32305
32561
|
document.title = titleEntry.title;
|
|
32306
32562
|
}
|
|
32563
|
+
const removeThemeColors = themeColors.length > 0 ? pushHeadThemeColors(themeColors) : null;
|
|
32307
32564
|
return () => {
|
|
32565
|
+
removeThemeColors?.();
|
|
32308
32566
|
if (titleEntry) {
|
|
32309
32567
|
titleEntries.splice(titleEntries.indexOf(titleEntry), 1);
|
|
32310
32568
|
const lastEntry = titleEntries[titleEntries.length - 1];
|
|
@@ -64607,9 +64865,17 @@ const css$E = /* css */`
|
|
|
64607
64865
|
on its own, sharp, above the wall: any wall on that frame — even a
|
|
64608
64866
|
half-strength wash and blur — is a flash on the anchor. So the wall
|
|
64609
64867
|
arrives from nothing, over the movement: on that frame the page is
|
|
64610
|
-
untouched, and the anchor with it.
|
|
64868
|
+
untouched, and the anchor with it.
|
|
64869
|
+
Keyed on the aria-expanded flip, like the local wall: openEffect reads
|
|
64870
|
+
the layout while the transitions are off, which spends @starting-style
|
|
64871
|
+
with nothing armed. @starting-style stays for an opening that reads
|
|
64872
|
+
nothing before the flip. A silent opening flips before the transitions
|
|
64873
|
+
come back, hence the :not(): its wall lands at once. */
|
|
64611
64874
|
&[data-lifting] {
|
|
64612
|
-
|
|
64875
|
+
&[aria-expanded="false"]::backdrop {
|
|
64876
|
+
opacity: 0;
|
|
64877
|
+
}
|
|
64878
|
+
&[aria-expanded="true"]:not([navi-transitions-off])::backdrop {
|
|
64613
64879
|
opacity: 1;
|
|
64614
64880
|
transition-property: opacity;
|
|
64615
64881
|
transition-duration: var(--navi-popup-lift-duration, 0.25s);
|
|
@@ -65069,6 +65335,16 @@ const css$E = /* css */`
|
|
|
65069
65335
|
* `saturate()`/`grayscale()` work too. How far what is behind withdraws is
|
|
65070
65336
|
* a question of paint, not of what an outside click does — a backdrop that
|
|
65071
65337
|
* closes can blur.
|
|
65338
|
+
* @param {string|false} [props.themeColor] - The colour the browser paints
|
|
65339
|
+
* above the page (`<meta name="theme-color">`: Chrome's address bar on
|
|
65340
|
+
* Android, an installed app's status bar) while this dialog is open.
|
|
65341
|
+
* Left out, it is what the dialog lays over the top edge of the screen: its
|
|
65342
|
+
* backdrop over the colour the page asked for (`<Head>`), and its own
|
|
65343
|
+
* background over that when it is flush with the top edge
|
|
65344
|
+
* (`marginWithContainer={0}` with `expand`/`expandY` or a top
|
|
65345
|
+
* `positionArea`). A colour forces it, for a surface whose colour cannot be
|
|
65346
|
+
* read (an image, a gradient); `false` leaves the page's colour alone.
|
|
65347
|
+
* `layer="top"` only.
|
|
65072
65348
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
65073
65349
|
* dialog so the page/container behind it can't scroll while it's open.
|
|
65074
65350
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
@@ -65590,6 +65866,9 @@ const useDialogProps = props => {
|
|
|
65590
65866
|
// so both objects below get them (see this file's CSS).
|
|
65591
65867
|
backdropColor,
|
|
65592
65868
|
backdropFilter,
|
|
65869
|
+
// Read at the opening, with the backdrop and the surface it would
|
|
65870
|
+
// otherwise be worked out from (see openEffect).
|
|
65871
|
+
themeColor,
|
|
65593
65872
|
scrollCapture: scrollCaptureProp,
|
|
65594
65873
|
// "auto" (default) → the dialog follows its content. "frozen" → measured
|
|
65595
65874
|
// once, held at that size while open. See this prop's own JSDoc above.
|
|
@@ -66173,6 +66452,16 @@ const useDialogProps = props => {
|
|
|
66173
66452
|
backdropEl.style.transitionProperty = "";
|
|
66174
66453
|
}
|
|
66175
66454
|
}
|
|
66455
|
+
// What the browser paints above the page goes with what is laid over its
|
|
66456
|
+
// top edge, from the start of the opening to the start of the closing: a
|
|
66457
|
+
// local dialog lays nothing there.
|
|
66458
|
+
if (isTopLayer && themeColor !== false) {
|
|
66459
|
+
addCleanup(pushPopupThemeColor({
|
|
66460
|
+
themeColor,
|
|
66461
|
+
backdropColor: backdrop ? getComputedStyle(dialogEl, "::backdrop").backgroundColor : null,
|
|
66462
|
+
surfaceColor: flushEdges.top ? getComputedStyle(dialogEl).backgroundColor : null
|
|
66463
|
+
}));
|
|
66464
|
+
}
|
|
66176
66465
|
const cancelOpenInteractionSuppression = !silent && hasCssTransitionAnimation ? suppressPointerEventsDuringTransition(dialogEl) : null;
|
|
66177
66466
|
// Handing the focus to a field is what raises the on-screen keyboard, and
|
|
66178
66467
|
// the keyboard takes away the very room this dialog was just placed
|
|
@@ -66975,6 +67264,12 @@ const css$D = /* css */`
|
|
|
66975
67264
|
* `saturate()`/`grayscale()` work too. How far what is behind withdraws is
|
|
66976
67265
|
* a question of paint, not of what an outside click does — a backdrop that
|
|
66977
67266
|
* closes can blur.
|
|
67267
|
+
* @param {string|false} [props.themeColor] - The colour the browser paints
|
|
67268
|
+
* above the page (`<meta name="theme-color">`: Chrome's address bar on
|
|
67269
|
+
* Android, an installed app's status bar) while this popover is open.
|
|
67270
|
+
* Left out, it is the backdrop laid over the colour the page asked for
|
|
67271
|
+
* (`<Head>`), and the page's colour when there is no backdrop. A colour
|
|
67272
|
+
* forces it; `false` leaves the page's colour alone. `layer="top"` only.
|
|
66978
67273
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
66979
67274
|
* popover so the page/container behind it can't scroll while it's open.
|
|
66980
67275
|
* @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
|
|
@@ -67317,6 +67612,9 @@ const usePopoverProps = props => {
|
|
|
67317
67612
|
// which is a sibling element (see this file's top comment).
|
|
67318
67613
|
backdropColor,
|
|
67319
67614
|
backdropFilter,
|
|
67615
|
+
// Read at the opening, with the backdrop it would otherwise be worked out
|
|
67616
|
+
// from (see openEffect).
|
|
67617
|
+
themeColor,
|
|
67320
67618
|
group,
|
|
67321
67619
|
scrollCapture,
|
|
67322
67620
|
focusCapture,
|
|
@@ -67921,6 +68219,15 @@ const usePopoverProps = props => {
|
|
|
67921
68219
|
backdropEl.style.transitionProperty = "";
|
|
67922
68220
|
}
|
|
67923
68221
|
}
|
|
68222
|
+
// What the browser paints above the page goes with what is laid over its
|
|
68223
|
+
// top edge, from the start of the opening to the start of the closing: a
|
|
68224
|
+
// local popover's backdrop stops at its container.
|
|
68225
|
+
if (isTopLayer && themeColor !== false) {
|
|
68226
|
+
addCleanup(pushPopupThemeColor({
|
|
68227
|
+
themeColor,
|
|
68228
|
+
backdropColor: backdropEl ? getComputedStyle(backdropEl).backgroundColor : null
|
|
68229
|
+
}));
|
|
68230
|
+
}
|
|
67924
68231
|
const cancelOpenInteractionSuppression = !silent && hasCssTransitionAnimation ? suppressPointerEventsDuringTransition(popoverEl) : null;
|
|
67925
68232
|
// autoFocus={false}: opening moves no focus at all — whoever has the
|
|
67926
68233
|
// keyboard keeps it (combobox-style suggestions under an input being
|
|
@@ -68336,6 +68643,10 @@ const css$C = /* css */`@layer navi {
|
|
|
68336
68643
|
* identically): the wash the backdrop paints over what is behind.
|
|
68337
68644
|
* @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
|
|
68338
68645
|
* does to the picture underneath, `"blur(4px)"` and the like.
|
|
68646
|
+
* @param {string|false} [props.themeColor] - Forwarded as-is: the colour the
|
|
68647
|
+
* browser paints above the page while the popup is open, worked out from
|
|
68648
|
+
* what it lays over the top edge when left out. A colour forces it, `false`
|
|
68649
|
+
* leaves the page's colour alone.
|
|
68339
68650
|
* @param {boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`|"expanding"|`expand-${string}`|"lifting"|{open: boolean|"auto"|"fading"|"scaling"|"sliding"|`slide-from-${string}`, close: "lifting"}} [props.animation]
|
|
68340
68651
|
* - Forwarded to whichever renders — see either component's own doc.
|
|
68341
68652
|
* `"expanding"`/`"expand-*"` are `Popover`'s own; `"lifting"` and
|
|
@@ -69333,6 +69644,26 @@ const PickerCustom = props => {
|
|
|
69333
69644
|
if (isWithinPickerContent(e.target)) {
|
|
69334
69645
|
return null;
|
|
69335
69646
|
}
|
|
69647
|
+
// Still open at the click of a later press: that press brought no
|
|
69648
|
+
// mousedown to close on (a drag source cancelled its pointerdown),
|
|
69649
|
+
// so it closes here, as it opens here in that case. A mousedown
|
|
69650
|
+
// that came has closed it already; the press that opened it is
|
|
69651
|
+
// only ending.
|
|
69652
|
+
if (openController.opened && !openedDuringThisPress(openController)) {
|
|
69653
|
+
return {
|
|
69654
|
+
name: "click to close picker",
|
|
69655
|
+
intent: "read",
|
|
69656
|
+
prevented: () => {
|
|
69657
|
+
e.preventDefault();
|
|
69658
|
+
},
|
|
69659
|
+
allowed: () => {
|
|
69660
|
+
requestClose(e, {
|
|
69661
|
+
isCancel: true
|
|
69662
|
+
});
|
|
69663
|
+
e.preventDefault();
|
|
69664
|
+
}
|
|
69665
|
+
};
|
|
69666
|
+
}
|
|
69336
69667
|
if (!opensOnPress) {
|
|
69337
69668
|
// Neither the tap, nor the click a hold leaves behind: the press
|
|
69338
69669
|
// is not what opens this picker.
|
|
@@ -69988,12 +70319,12 @@ const isScrolling = (element) => {
|
|
|
69988
70319
|
if (element === undefined) {
|
|
69989
70320
|
return scrollActivitySignal.peek();
|
|
69990
70321
|
}
|
|
69991
|
-
return asScroller(element).hasAttribute("navi-scrolling");
|
|
70322
|
+
return asScroller$1(element).hasAttribute("navi-scrolling");
|
|
69992
70323
|
};
|
|
69993
70324
|
|
|
69994
70325
|
// The page scroll is dispatched on the document; the element carrying the
|
|
69995
70326
|
// attribute is the one CSS can reach, document.scrollingElement.
|
|
69996
|
-
const asScroller = (eventTargetOrElement) => {
|
|
70327
|
+
const asScroller$1 = (eventTargetOrElement) => {
|
|
69997
70328
|
if (eventTargetOrElement === document || eventTargetOrElement === window) {
|
|
69998
70329
|
return document.scrollingElement;
|
|
69999
70330
|
}
|
|
@@ -70004,7 +70335,7 @@ const idleTimeoutMap = new Map();
|
|
|
70004
70335
|
document.addEventListener(
|
|
70005
70336
|
"scroll",
|
|
70006
70337
|
(e) => {
|
|
70007
|
-
const scroller = asScroller(e.target);
|
|
70338
|
+
const scroller = asScroller$1(e.target);
|
|
70008
70339
|
const idleTimeout = idleTimeoutMap.get(scroller);
|
|
70009
70340
|
if (idleTimeout !== undefined) {
|
|
70010
70341
|
clearTimeout(idleTimeout);
|
|
@@ -70026,6 +70357,119 @@ document.addEventListener(
|
|
|
70026
70357
|
{ capture: true, passive: true },
|
|
70027
70358
|
);
|
|
70028
70359
|
|
|
70360
|
+
/*
|
|
70361
|
+
* Whether writing a scroll position now would cost the user the scroll in
|
|
70362
|
+
* progress. A write is an instant scroll, and the browser abandons for it any
|
|
70363
|
+
* scroll it is playing on its own, short of where that one was going: a key
|
|
70364
|
+
* (Home, PageDown, Space), a smooth `scrollTo` or `scrollIntoView`, the status
|
|
70365
|
+
* bar tap on iOS, the fling a finger leaves behind once it lifts. A scroll
|
|
70366
|
+
* following a hand is not abandoned: the wheel's next turn carries on, and on
|
|
70367
|
+
* most engines a finger keeps dragging from wherever the write left it.
|
|
70368
|
+
*
|
|
70369
|
+
* Not on iOS. Its finger places the content from where the drag began, so a
|
|
70370
|
+
* write under it is undone at the finger's next move — or, when the finger
|
|
70371
|
+
* lifts first, lands after the lift and stops the fling dead. And a fling
|
|
70372
|
+
* stops on any write during its momentum, as the status bar's scroll to the
|
|
70373
|
+
* top does (Safari 26.5 on the simulator: flings of 1700px stopped 130 to 340px
|
|
70374
|
+
* after the lift, 3 in 3; a write 50ms before the lift stopped 2 flings in 3).
|
|
70375
|
+
* No finger there is a hand a write could hold to. Chrome flings on through a
|
|
70376
|
+
* write, and loses nothing by not getting one.
|
|
70377
|
+
*
|
|
70378
|
+
* A scroll is followed from its first "scroll" event to its "scrollend", and
|
|
70379
|
+
* is a hand's while a finger is down, or when a wheel turned around the time it
|
|
70380
|
+
* started or since. A finger is down from its touchstart to its touchend,
|
|
70381
|
+
* heard on what it touched: a list removes the items scrolled out of its window
|
|
70382
|
+
* while the finger is still on one of them, and the touchend of a removed
|
|
70383
|
+
* element never reaches the document.
|
|
70384
|
+
*
|
|
70385
|
+
* In a browser without "scrollend", no write is said to interrupt anything: a
|
|
70386
|
+
* scroll would never be seen ending.
|
|
70387
|
+
*/
|
|
70388
|
+
|
|
70389
|
+
// The scroll a wheel turn causes starts in the frame the turn is read, or the
|
|
70390
|
+
// next one. A turn further from it moved nothing (a wheel at an edge) and is no
|
|
70391
|
+
// hand on a scroll that came from elsewhere.
|
|
70392
|
+
const WHEEL_REACH = 100;
|
|
70393
|
+
// iOS's touch handling, the one engine whose finger drops writes (see above).
|
|
70394
|
+
const FINGER_KEEPS_WRITES = !window.CSS.supports(
|
|
70395
|
+
"-webkit-touch-callout",
|
|
70396
|
+
"none",
|
|
70397
|
+
);
|
|
70398
|
+
|
|
70399
|
+
const flightMap = new WeakMap();
|
|
70400
|
+
let fingersDown = 0;
|
|
70401
|
+
let wheelTimeStamp = -Infinity;
|
|
70402
|
+
|
|
70403
|
+
/**
|
|
70404
|
+
* @param {Element} scrollerEl
|
|
70405
|
+
* @returns {boolean}
|
|
70406
|
+
*/
|
|
70407
|
+
const wouldInterruptScroll = (scrollerEl) => {
|
|
70408
|
+
const flight = flightMap.get(scrollerEl);
|
|
70409
|
+
if (!flight) {
|
|
70410
|
+
return false;
|
|
70411
|
+
}
|
|
70412
|
+
if (fingersDown > 0) {
|
|
70413
|
+
return !FINGER_KEEPS_WRITES;
|
|
70414
|
+
}
|
|
70415
|
+
if (wheelTimeStamp > flight.timeStamp - WHEEL_REACH) {
|
|
70416
|
+
return false;
|
|
70417
|
+
}
|
|
70418
|
+
return true;
|
|
70419
|
+
};
|
|
70420
|
+
|
|
70421
|
+
// The page scroll is dispatched on the document; the scroller asked about is
|
|
70422
|
+
// document.scrollingElement.
|
|
70423
|
+
const asScroller = (eventTarget) => {
|
|
70424
|
+
if (eventTarget === document || eventTarget === window) {
|
|
70425
|
+
return document.scrollingElement;
|
|
70426
|
+
}
|
|
70427
|
+
return eventTarget;
|
|
70428
|
+
};
|
|
70429
|
+
|
|
70430
|
+
const onTouchEnd = (e) => {
|
|
70431
|
+
fingersDown = e.touches.length;
|
|
70432
|
+
};
|
|
70433
|
+
|
|
70434
|
+
if ("onscrollend" in window) {
|
|
70435
|
+
// Capturing: "scroll" and "scrollend" do not bubble from an element.
|
|
70436
|
+
document.addEventListener(
|
|
70437
|
+
"scroll",
|
|
70438
|
+
(e) => {
|
|
70439
|
+
const scroller = asScroller(e.target);
|
|
70440
|
+
if (flightMap.has(scroller)) {
|
|
70441
|
+
return;
|
|
70442
|
+
}
|
|
70443
|
+
flightMap.set(scroller, { timeStamp: e.timeStamp });
|
|
70444
|
+
},
|
|
70445
|
+
{ capture: true, passive: true },
|
|
70446
|
+
);
|
|
70447
|
+
document.addEventListener(
|
|
70448
|
+
"scrollend",
|
|
70449
|
+
(e) => {
|
|
70450
|
+
flightMap.delete(asScroller(e.target));
|
|
70451
|
+
},
|
|
70452
|
+
{ capture: true, passive: true },
|
|
70453
|
+
);
|
|
70454
|
+
window.addEventListener(
|
|
70455
|
+
"wheel",
|
|
70456
|
+
(e) => {
|
|
70457
|
+
wheelTimeStamp = e.timeStamp;
|
|
70458
|
+
},
|
|
70459
|
+
{ capture: true, passive: true },
|
|
70460
|
+
);
|
|
70461
|
+
window.addEventListener(
|
|
70462
|
+
"touchstart",
|
|
70463
|
+
(e) => {
|
|
70464
|
+
fingersDown = e.touches.length;
|
|
70465
|
+
const touchedEl = e.composedPath()[0];
|
|
70466
|
+
touchedEl.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
70467
|
+
touchedEl.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
70468
|
+
},
|
|
70469
|
+
{ capture: true, passive: true },
|
|
70470
|
+
);
|
|
70471
|
+
}
|
|
70472
|
+
|
|
70029
70473
|
const LoadingDotsSvg = () => {
|
|
70030
70474
|
return jsxs("svg", {
|
|
70031
70475
|
viewBox: "0 0 200 200",
|
|
@@ -73109,7 +73553,9 @@ const useListScrollSync = ({
|
|
|
73109
73553
|
const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
|
|
73110
73554
|
const startPlaceRef = useRef({
|
|
73111
73555
|
userTookOver: false,
|
|
73112
|
-
wanted: scrolledWanted
|
|
73556
|
+
wanted: scrolledWanted,
|
|
73557
|
+
way: null,
|
|
73558
|
+
arrived: false
|
|
73113
73559
|
});
|
|
73114
73560
|
if (startPlaceRef.current.wanted !== scrolledWanted) {
|
|
73115
73561
|
startPlaceRef.current.wanted = scrolledWanted;
|
|
@@ -73319,8 +73765,10 @@ const useListScrollSync = ({
|
|
|
73319
73765
|
// keeps moving while the list is still finding out how many items it has and
|
|
73320
73766
|
// how tall one is, so landing there once would land next to it. What ends the
|
|
73321
73767
|
// hold is the user reaching for the list — a wheel, a finger, a key, a hand
|
|
73322
|
-
// on the scrollbar —
|
|
73323
|
-
// as
|
|
73768
|
+
// on the scrollbar — or anything else moving it. A scroll event alone does
|
|
73769
|
+
// not say which: the list provokes as many as the user does, so only one
|
|
73770
|
+
// landing off the way of the list's own placement ends it (see
|
|
73771
|
+
// sendHeldScroll).
|
|
73324
73772
|
const placeWhereHeld = () => {
|
|
73325
73773
|
if (scrolledWanted === "start" || scrolledWanted === undefined || startPlaceRef.current.userTookOver || !ref.current) {
|
|
73326
73774
|
return;
|
|
@@ -73369,12 +73817,7 @@ const useListScrollSync = ({
|
|
|
73369
73817
|
}
|
|
73370
73818
|
const scrollerEl = getScroller();
|
|
73371
73819
|
if (openAt === "end") {
|
|
73372
|
-
|
|
73373
|
-
if (horizontal) {
|
|
73374
|
-
scrollerEl.scrollLeft = scrollerEl.scrollWidth;
|
|
73375
|
-
} else {
|
|
73376
|
-
scrollerEl.scrollTop = scrollerEl.scrollHeight;
|
|
73377
|
-
}
|
|
73820
|
+
sendHeldScroll(horizontal ? scrollerEl.scrollWidth : scrollerEl.scrollHeight);
|
|
73378
73821
|
return;
|
|
73379
73822
|
}
|
|
73380
73823
|
if (typeof openAt === "number") {
|
|
@@ -73398,15 +73841,45 @@ const useListScrollSync = ({
|
|
|
73398
73841
|
const itemRect = itemEl.getBoundingClientRect();
|
|
73399
73842
|
const offsetWanted = resolveOpenOffset(getItemScrollInset(scrollerEl, itemEl, horizontal) + offset, horizontal ? viewportRect.width : viewportRect.height, horizontal ? itemRect.width : itemRect.height);
|
|
73400
73843
|
const offsetNow = horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top;
|
|
73844
|
+
const scrollNow = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
73401
73845
|
const delta = offsetNow - offsetWanted;
|
|
73402
73846
|
if (delta > -0.5 && delta < 0.5) {
|
|
73847
|
+
sendHeldScroll(scrollNow);
|
|
73403
73848
|
return;
|
|
73404
73849
|
}
|
|
73850
|
+
sendHeldScroll(scrollNow + delta);
|
|
73851
|
+
};
|
|
73852
|
+
// Every placement goes through here, the ones that find the list in place
|
|
73853
|
+
// too: it keeps the way from where the scroller was to where it is sent,
|
|
73854
|
+
// which is how a scroll event tells the hold's own scrolls from anyone
|
|
73855
|
+
// else's (see letGoUnlessHeldScroll). The whole way, not just its end: under
|
|
73856
|
+
// `scroll-behavior: smooth` the write glides, and every position on the way
|
|
73857
|
+
// is the list's until it gets there.
|
|
73858
|
+
const sendHeldScroll = position => {
|
|
73859
|
+
const scrollerEl = getScroller();
|
|
73860
|
+
const from = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
73861
|
+
const max = horizontal ? scrollerEl.scrollWidth - scrollerEl.clientWidth : scrollerEl.scrollHeight - scrollerEl.clientHeight;
|
|
73862
|
+
let to = position;
|
|
73863
|
+
if (to > max) {
|
|
73864
|
+
to = max;
|
|
73865
|
+
}
|
|
73866
|
+
if (to < 0) {
|
|
73867
|
+
to = 0;
|
|
73868
|
+
}
|
|
73405
73869
|
anchorRef.current = null;
|
|
73870
|
+
const place = startPlaceRef.current;
|
|
73871
|
+
place.way = {
|
|
73872
|
+
from,
|
|
73873
|
+
to
|
|
73874
|
+
};
|
|
73875
|
+
if (to === from) {
|
|
73876
|
+
place.arrived = true;
|
|
73877
|
+
return;
|
|
73878
|
+
}
|
|
73406
73879
|
if (horizontal) {
|
|
73407
|
-
scrollerEl.scrollLeft
|
|
73880
|
+
scrollerEl.scrollLeft = to;
|
|
73408
73881
|
} else {
|
|
73409
|
-
scrollerEl.scrollTop
|
|
73882
|
+
scrollerEl.scrollTop = to;
|
|
73410
73883
|
}
|
|
73411
73884
|
};
|
|
73412
73885
|
useLayoutEffect(placeWhereHeld);
|
|
@@ -73479,6 +73952,19 @@ const useListScrollSync = ({
|
|
|
73479
73952
|
evaluateWindowRef.current("items resized");
|
|
73480
73953
|
return false;
|
|
73481
73954
|
};
|
|
73955
|
+
// The box around a held list changing shape is the hold's to answer too: it
|
|
73956
|
+
// places the list again, the way it writes every scroll of its own (see
|
|
73957
|
+
// sendHeldScroll) — written any other way, the scroll would read as someone
|
|
73958
|
+
// else moving the list, and end the hold.
|
|
73959
|
+
const placeIfHeldRef = useRef(null);
|
|
73960
|
+
placeIfHeldRef.current = () => {
|
|
73961
|
+
// Read again: a scroll may have ended the hold since this render.
|
|
73962
|
+
if (!heldSomewhere || startPlaceRef.current.userTookOver) {
|
|
73963
|
+
return false;
|
|
73964
|
+
}
|
|
73965
|
+
placeWhereHeld();
|
|
73966
|
+
return true;
|
|
73967
|
+
};
|
|
73482
73968
|
useLayoutEffect(() => {
|
|
73483
73969
|
if (scrolledWanted === "start" || scrolledWanted === undefined || !ref.current) {
|
|
73484
73970
|
return undefined;
|
|
@@ -73623,6 +74109,9 @@ const useListScrollSync = ({
|
|
|
73623
74109
|
if (!entries.some(entry => entry.target === scrollerEl)) {
|
|
73624
74110
|
return;
|
|
73625
74111
|
}
|
|
74112
|
+
if (placeIfHeldRef.current()) {
|
|
74113
|
+
return;
|
|
74114
|
+
}
|
|
73626
74115
|
const position = positionRef.current;
|
|
73627
74116
|
if (!position) {
|
|
73628
74117
|
return;
|
|
@@ -73638,6 +74127,13 @@ const useListScrollSync = ({
|
|
|
73638
74127
|
if (delta > -0.5 && delta < 0.5) {
|
|
73639
74128
|
return;
|
|
73640
74129
|
}
|
|
74130
|
+
if (wouldInterruptScroll(scrollerEl)) {
|
|
74131
|
+
// As the anchoring's write would (see holdAnchorStill). The document's
|
|
74132
|
+
// box is the scroller of a list scrolling the page, and it resizes with
|
|
74133
|
+
// every item drawn taller than its filler: while the page scrolls, that
|
|
74134
|
+
// is at every window move.
|
|
74135
|
+
return;
|
|
74136
|
+
}
|
|
73641
74137
|
anchorRef.current = null;
|
|
73642
74138
|
if (horizontal) {
|
|
73643
74139
|
scrollerEl.scrollLeft += delta;
|
|
@@ -73735,6 +74231,14 @@ const useListScrollSync = ({
|
|
|
73735
74231
|
return;
|
|
73736
74232
|
}
|
|
73737
74233
|
anchorRef.current = null;
|
|
74234
|
+
if (wouldInterruptScroll(scrollerEl)) {
|
|
74235
|
+
// Nobody is looking at items standing still: they are passing by, on
|
|
74236
|
+
// their way somewhere the browser takes them (a key, a smooth scrollTo,
|
|
74237
|
+
// the status bar, a fling). The write would stop that scroll short of
|
|
74238
|
+
// where it was going. Left alone, the items drawn above push the rest by
|
|
74239
|
+
// `drift` while all of it moves, and the scroll lands.
|
|
74240
|
+
return;
|
|
74241
|
+
}
|
|
73738
74242
|
scrolledByListRef.current = true;
|
|
73739
74243
|
if (horizontal) {
|
|
73740
74244
|
scrollerEl.scrollLeft += drift;
|
|
@@ -74062,9 +74566,44 @@ const useListScrollSync = ({
|
|
|
74062
74566
|
reportPosition();
|
|
74063
74567
|
return;
|
|
74064
74568
|
}
|
|
74569
|
+
letGoUnlessHeldScroll();
|
|
74065
74570
|
reportPosition();
|
|
74066
74571
|
evaluateWindowRef.current("scroll");
|
|
74067
74572
|
};
|
|
74573
|
+
// A scroll the hold did not write is someone else moving the list, which
|
|
74574
|
+
// ends the hold like a wheel does — the ones that come with no input event
|
|
74575
|
+
// at all: the status bar tap on iOS, find in page, a script.
|
|
74576
|
+
// Only once the list has stood where it holds itself, though. Until then
|
|
74577
|
+
// the page is still settling around it, and some of that is written after
|
|
74578
|
+
// the list placed itself: a navigation holding the rendering for its
|
|
74579
|
+
// transition takes the arriving page to its top once the page rendered
|
|
74580
|
+
// (whenRenderingResumes), and the hold puts it back at its next commit.
|
|
74581
|
+
const letGoUnlessHeldScroll = () => {
|
|
74582
|
+
const place = startPlaceRef.current;
|
|
74583
|
+
const way = place.way;
|
|
74584
|
+
if (place.userTookOver || place.wanted === "start" || place.wanted === undefined || !way) {
|
|
74585
|
+
return;
|
|
74586
|
+
}
|
|
74587
|
+
const position = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
74588
|
+
if (position > way.to - 1 && position < way.to + 1) {
|
|
74589
|
+
// From then on only that place is the list's, or a scroll back the
|
|
74590
|
+
// way it came would pass for its own.
|
|
74591
|
+
place.way = {
|
|
74592
|
+
from: way.to,
|
|
74593
|
+
to: way.to
|
|
74594
|
+
};
|
|
74595
|
+
place.arrived = true;
|
|
74596
|
+
return;
|
|
74597
|
+
}
|
|
74598
|
+
if (!place.arrived) {
|
|
74599
|
+
return;
|
|
74600
|
+
}
|
|
74601
|
+
const low = way.from < way.to ? way.from : way.to;
|
|
74602
|
+
const high = way.from < way.to ? way.to : way.from;
|
|
74603
|
+
if (position < low - 1 || position > high + 1) {
|
|
74604
|
+
place.userTookOver = true;
|
|
74605
|
+
}
|
|
74606
|
+
};
|
|
74068
74607
|
// A page-level scroller does not emit "scroll" on the element itself
|
|
74069
74608
|
// (document.scrollingElement); the document does.
|
|
74070
74609
|
const scrollEventTarget = scrollerEl === document.scrollingElement ? document : scrollerEl;
|
|
@@ -77453,7 +77992,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
77453
77992
|
*
|
|
77454
77993
|
* In every form the list holds itself there while it is still finding out
|
|
77455
77994
|
* how many items there are and how tall one is, and lets go the moment the
|
|
77456
|
-
* user reaches for the list
|
|
77995
|
+
* user reaches for the list, or anything else scrolls it (a script, find in
|
|
77996
|
+
* page, the status bar tap on iOS).
|
|
77457
77997
|
* @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
|
|
77458
77998
|
* Where the list is, as the user scrolls: the item at the top of the view and
|
|
77459
77999
|
* how far below the place an item lands on its own (see `defaultScrolled`) it
|