@jsenv/navi 0.29.431 → 0.29.432
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 +527 -23
- package/dist/dev/jsenv_navi.js.map +14 -8
- package/dist/jsenv_navi.js +527 -23
- package/dist/jsenv_navi.js.map +14 -8
- 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";
|
|
@@ -11241,13 +11241,22 @@ const installScrollRestoration = () => {
|
|
|
11241
11241
|
},
|
|
11242
11242
|
{ passive: true },
|
|
11243
11243
|
);
|
|
11244
|
+
const positionOnLoad = positionByUrl.get(window.location.href);
|
|
11245
|
+
if (!positionOnLoad) {
|
|
11246
|
+
// Where the browser landed the first page, written down for the reason
|
|
11247
|
+
// startAtTop writes its top: no scroll event says so.
|
|
11248
|
+
positionByUrl.set(window.location.href, {
|
|
11249
|
+
x: window.scrollX,
|
|
11250
|
+
y: window.scrollY,
|
|
11251
|
+
});
|
|
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(() => {
|
|
@@ -11333,6 +11342,11 @@ const startAtTop = (url, { from } = {}) => {
|
|
|
11333
11342
|
return;
|
|
11334
11343
|
}
|
|
11335
11344
|
arrivalCount++;
|
|
11345
|
+
// Written down as well as done: a document already at its top fires no
|
|
11346
|
+
// scroll event, and a page left without ever being scrolled would have no
|
|
11347
|
+
// position to come back to — the return would keep the offset of the page
|
|
11348
|
+
// being left.
|
|
11349
|
+
positionByUrl.set(new URL(url, window.location.href).href, { x: 0, y: 0 });
|
|
11336
11350
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
11337
11351
|
};
|
|
11338
11352
|
const isArrival = (url, { from }) => {
|
|
@@ -16346,7 +16360,7 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
|
16346
16360
|
},
|
|
16347
16361
|
children: jsx$1
|
|
16348
16362
|
});
|
|
16349
|
-
render(calloutJsx, calloutMessageElement);
|
|
16363
|
+
render$1(calloutJsx, calloutMessageElement);
|
|
16350
16364
|
};
|
|
16351
16365
|
|
|
16352
16366
|
// An HTML message is rendered through preact rather than innerHTML: what a
|
|
@@ -16369,7 +16383,7 @@ const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
|
16369
16383
|
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
16370
16384
|
// tree is still there.
|
|
16371
16385
|
const clearCalloutMessage = calloutMessageElement => {
|
|
16372
|
-
render(null, calloutMessageElement);
|
|
16386
|
+
render$1(null, calloutMessageElement);
|
|
16373
16387
|
calloutMessageElement.innerHTML = "";
|
|
16374
16388
|
};
|
|
16375
16389
|
const domToVNodes = node => {
|
|
@@ -32262,10 +32276,222 @@ const useUrlSearchParam = (paramName, defaultValue) => {
|
|
|
32262
32276
|
return [value, setSearchParamValue];
|
|
32263
32277
|
};
|
|
32264
32278
|
|
|
32279
|
+
/**
|
|
32280
|
+
* The colour the browser paints around the page — Chrome's address bar on
|
|
32281
|
+
* Android, the status bar of an installed app — read from
|
|
32282
|
+
* `<meta name="theme-color">`. It sits right above the page's top edge, so it
|
|
32283
|
+
* reads as part of whatever is drawn there, and two layers decide it:
|
|
32284
|
+
*
|
|
32285
|
+
* - What the page asks for: the theme-color metas a `<Head>` brings. The last
|
|
32286
|
+
* Head to arrive wins, whatever order they leave in — the title's rule, for
|
|
32287
|
+
* the title's reason (see head.jsx).
|
|
32288
|
+
* - What the top layer lays over that edge: an open popup paints its backdrop
|
|
32289
|
+
* over what is under it, and its own surface too when it is flush with the
|
|
32290
|
+
* top edge. Popups sit above every Head, in opening order, and are painted
|
|
32291
|
+
* again from what is under them each time it changes — a page changing
|
|
32292
|
+
* under an open popup is dimmed by it all the same.
|
|
32293
|
+
*
|
|
32294
|
+
* The browser reads the first theme-color meta whose `media` matches, in tree
|
|
32295
|
+
* order: a Head appending its meta after an earlier one would never be read.
|
|
32296
|
+
* So navi writes the result into metas of its own placed before any other, and
|
|
32297
|
+
* a meta written in the HTML is the bottom of the stack. A popup paints over
|
|
32298
|
+
* every meta of the list, `media` kept, so whichever one the browser picks is
|
|
32299
|
+
* the one it would have picked, painted over.
|
|
32300
|
+
*/
|
|
32301
|
+
|
|
32302
|
+
const headEntries = [];
|
|
32303
|
+
const popupLayers = [];
|
|
32304
|
+
const ownMetas = [];
|
|
32305
|
+
|
|
32306
|
+
/**
|
|
32307
|
+
* @param {Array<{ content: string, media: string|null }>} themeColors
|
|
32308
|
+
* @returns {() => void} Takes them away.
|
|
32309
|
+
*/
|
|
32310
|
+
const pushHeadThemeColors = (themeColors) => {
|
|
32311
|
+
headEntries.push(themeColors);
|
|
32312
|
+
queueRender();
|
|
32313
|
+
return () => {
|
|
32314
|
+
headEntries.splice(headEntries.indexOf(themeColors), 1);
|
|
32315
|
+
queueRender();
|
|
32316
|
+
};
|
|
32317
|
+
};
|
|
32318
|
+
|
|
32319
|
+
/**
|
|
32320
|
+
* @param {object} paint
|
|
32321
|
+
* @param {string} [paint.themeColor] - Forces the colour, nothing is painted.
|
|
32322
|
+
* @param {string|null} [paint.backdropColor] - Laid over the page's colour.
|
|
32323
|
+
* @param {string|null} [paint.surfaceColor] - Laid over that, for a popup
|
|
32324
|
+
* flush with the top edge.
|
|
32325
|
+
* @returns {() => void} Takes the layer away.
|
|
32326
|
+
*/
|
|
32327
|
+
const pushPopupThemeColor = ({
|
|
32328
|
+
themeColor,
|
|
32329
|
+
backdropColor,
|
|
32330
|
+
surfaceColor,
|
|
32331
|
+
}) => {
|
|
32332
|
+
let layer;
|
|
32333
|
+
if (themeColor) {
|
|
32334
|
+
layer = { color: themeColor };
|
|
32335
|
+
} else {
|
|
32336
|
+
const paints = [];
|
|
32337
|
+
for (const color of [backdropColor, surfaceColor]) {
|
|
32338
|
+
if (!color) {
|
|
32339
|
+
continue;
|
|
32340
|
+
}
|
|
32341
|
+
const rgba = parseColor(color);
|
|
32342
|
+
if (rgba[3] > 0) {
|
|
32343
|
+
paints.push(rgba);
|
|
32344
|
+
}
|
|
32345
|
+
}
|
|
32346
|
+
// Nothing laid over the edge: the colour under it stays, as it is.
|
|
32347
|
+
if (paints.length === 0) {
|
|
32348
|
+
return () => {};
|
|
32349
|
+
}
|
|
32350
|
+
layer = { paints };
|
|
32351
|
+
}
|
|
32352
|
+
popupLayers.push(layer);
|
|
32353
|
+
queueRender();
|
|
32354
|
+
return () => {
|
|
32355
|
+
popupLayers.splice(popupLayers.indexOf(layer), 1);
|
|
32356
|
+
queueRender();
|
|
32357
|
+
};
|
|
32358
|
+
};
|
|
32359
|
+
|
|
32360
|
+
let renderQueued = false;
|
|
32361
|
+
// Once per commit: a Head re-rendering leaves and arrives again in the same
|
|
32362
|
+
// commit, and the browser must not be handed the colour from in between.
|
|
32363
|
+
const queueRender = () => {
|
|
32364
|
+
if (renderQueued) {
|
|
32365
|
+
return;
|
|
32366
|
+
}
|
|
32367
|
+
renderQueued = true;
|
|
32368
|
+
queueMicrotask(render);
|
|
32369
|
+
};
|
|
32370
|
+
|
|
32371
|
+
const render = () => {
|
|
32372
|
+
renderQueued = false;
|
|
32373
|
+
const htmlMetas = Array.from(
|
|
32374
|
+
document.head.querySelectorAll('meta[name="theme-color"]'),
|
|
32375
|
+
).filter((meta) => !ownMetas.includes(meta));
|
|
32376
|
+
const htmlColors = htmlMetas.map((meta) => ({
|
|
32377
|
+
content: meta.getAttribute("content"),
|
|
32378
|
+
media: meta.getAttribute("media"),
|
|
32379
|
+
}));
|
|
32380
|
+
let colors = [];
|
|
32381
|
+
for (let i = headEntries.length - 1; i >= 0; i--) {
|
|
32382
|
+
colors.push(...headEntries[i]);
|
|
32383
|
+
}
|
|
32384
|
+
colors.push(...htmlColors);
|
|
32385
|
+
for (const layer of popupLayers) {
|
|
32386
|
+
colors = paintLayer(layer, colors);
|
|
32387
|
+
}
|
|
32388
|
+
// What the HTML metas say reaches the browser without a copy.
|
|
32389
|
+
while (colors.length > 0 && htmlColors.includes(colors[colors.length - 1])) {
|
|
32390
|
+
colors.pop();
|
|
32391
|
+
}
|
|
32392
|
+
|
|
32393
|
+
while (ownMetas.length > colors.length) {
|
|
32394
|
+
ownMetas.pop().remove();
|
|
32395
|
+
}
|
|
32396
|
+
colors.forEach(({ content, media }, index) => {
|
|
32397
|
+
let meta = ownMetas[index];
|
|
32398
|
+
if (!meta) {
|
|
32399
|
+
meta = document.createElement("meta");
|
|
32400
|
+
meta.setAttribute("name", "theme-color");
|
|
32401
|
+
const previousMeta = ownMetas[index - 1];
|
|
32402
|
+
if (previousMeta) {
|
|
32403
|
+
previousMeta.after(meta);
|
|
32404
|
+
} else if (htmlMetas.length > 0) {
|
|
32405
|
+
htmlMetas[0].before(meta);
|
|
32406
|
+
} else {
|
|
32407
|
+
document.head.append(meta);
|
|
32408
|
+
}
|
|
32409
|
+
ownMetas.push(meta);
|
|
32410
|
+
}
|
|
32411
|
+
if (meta.getAttribute("content") !== content) {
|
|
32412
|
+
meta.setAttribute("content", content);
|
|
32413
|
+
}
|
|
32414
|
+
if (meta.getAttribute("media") !== media) {
|
|
32415
|
+
if (media === null) {
|
|
32416
|
+
meta.removeAttribute("media");
|
|
32417
|
+
} else {
|
|
32418
|
+
meta.setAttribute("media", media);
|
|
32419
|
+
}
|
|
32420
|
+
}
|
|
32421
|
+
});
|
|
32422
|
+
};
|
|
32423
|
+
|
|
32424
|
+
const paintLayer = (layer, colors) => {
|
|
32425
|
+
if (layer.color) {
|
|
32426
|
+
return [{ content: layer.color, media: null }];
|
|
32427
|
+
}
|
|
32428
|
+
const underColors =
|
|
32429
|
+
colors.length > 0 ? colors : [{ content: null, media: null }];
|
|
32430
|
+
const painted = [];
|
|
32431
|
+
for (const { content, media } of underColors) {
|
|
32432
|
+
let rgba = content === null ? null : parseUnderColor(content);
|
|
32433
|
+
for (const paint of layer.paints) {
|
|
32434
|
+
rgba = rgba ? composite(paint, rgba) : paint;
|
|
32435
|
+
}
|
|
32436
|
+
// Over no colour at all the browser keeps one of its own, which a
|
|
32437
|
+
// translucent paint cannot be laid over.
|
|
32438
|
+
if (Math.round(rgba[3] * 255) === 255) {
|
|
32439
|
+
painted.push({ content: toHex(rgba), media });
|
|
32440
|
+
}
|
|
32441
|
+
}
|
|
32442
|
+
return painted;
|
|
32443
|
+
};
|
|
32444
|
+
|
|
32445
|
+
// The browser shows a theme-color opaque, whatever alpha it was written with.
|
|
32446
|
+
const parseUnderColor = (color) => {
|
|
32447
|
+
const rgba = parseColor(color);
|
|
32448
|
+
if (rgba[3] === 0) {
|
|
32449
|
+
return null;
|
|
32450
|
+
}
|
|
32451
|
+
return [rgba[0], rgba[1], rgba[2], 1];
|
|
32452
|
+
};
|
|
32453
|
+
|
|
32454
|
+
// Source-over, the way the backdrop is drawn over the page.
|
|
32455
|
+
const composite = (over, under) => {
|
|
32456
|
+
const [r1, g1, b1, a1] = over;
|
|
32457
|
+
const [r2, g2, b2, a2] = under;
|
|
32458
|
+
const alpha = a1 + a2 * (1 - a1);
|
|
32459
|
+
const mix = (c1, c2) => (c1 * a1 + c2 * a2 * (1 - a1)) / alpha;
|
|
32460
|
+
return [mix(r1, r2), mix(g1, g2), mix(b1, b2), alpha];
|
|
32461
|
+
};
|
|
32462
|
+
|
|
32463
|
+
const toHex = ([r, g, b]) =>
|
|
32464
|
+
`#${[r, g, b]
|
|
32465
|
+
.map((channel) => Math.round(channel).toString(16).padStart(2, "0"))
|
|
32466
|
+
.join("")}`;
|
|
32467
|
+
|
|
32468
|
+
// A canvas reads every colour syntax the browser knows — oklch(), color(),
|
|
32469
|
+
// a keyword — and hands back sRGB bytes, which is what a theme-color meta is
|
|
32470
|
+
// sure to be understood in, whatever getComputedStyle answered with.
|
|
32471
|
+
let colorContext = null;
|
|
32472
|
+
const parseColor = (color) => {
|
|
32473
|
+
if (!colorContext) {
|
|
32474
|
+
const canvas = document.createElement("canvas");
|
|
32475
|
+
canvas.width = 1;
|
|
32476
|
+
canvas.height = 1;
|
|
32477
|
+
colorContext = canvas.getContext("2d", { willReadFrequently: true });
|
|
32478
|
+
}
|
|
32479
|
+
colorContext.clearRect(0, 0, 1, 1);
|
|
32480
|
+
// fillStyle ignores a colour it cannot parse: reset first, so that one draws
|
|
32481
|
+
// nothing rather than the previous colour.
|
|
32482
|
+
colorContext.fillStyle = "transparent";
|
|
32483
|
+
colorContext.fillStyle = color;
|
|
32484
|
+
colorContext.fillRect(0, 0, 1, 1);
|
|
32485
|
+
const [r, g, b, a] = colorContext.getImageData(0, 0, 1, 1).data;
|
|
32486
|
+
return [r, g, b, a / 255];
|
|
32487
|
+
};
|
|
32488
|
+
|
|
32265
32489
|
// The title belongs to the last Head to arrive, whatever order they leave in:
|
|
32266
32490
|
// the page being left by a route transition is taken down AFTER the page
|
|
32267
32491
|
// 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.
|
|
32492
|
+
// title it found would put its own page's title over the new one. A
|
|
32493
|
+
// theme-color meta follows the same rule, kept by theme_color.js, where the
|
|
32494
|
+
// popups of the top layer paint over it.
|
|
32269
32495
|
const titleEntries = [];
|
|
32270
32496
|
let titleWithoutHead = null;
|
|
32271
32497
|
const Head = ({
|
|
@@ -32277,6 +32503,7 @@ const Head = ({
|
|
|
32277
32503
|
}
|
|
32278
32504
|
const childArray = Array.isArray(children) ? children : [children];
|
|
32279
32505
|
const appendedElements = [];
|
|
32506
|
+
const themeColors = [];
|
|
32280
32507
|
let titleEntry = null;
|
|
32281
32508
|
for (const child of childArray) {
|
|
32282
32509
|
if (!child) {
|
|
@@ -32289,6 +32516,13 @@ const Head = ({
|
|
|
32289
32516
|
};
|
|
32290
32517
|
continue;
|
|
32291
32518
|
}
|
|
32519
|
+
if (child.type === "meta" && child.props.name === "theme-color") {
|
|
32520
|
+
themeColors.push({
|
|
32521
|
+
content: child.props.content,
|
|
32522
|
+
media: child.props.media ?? null
|
|
32523
|
+
});
|
|
32524
|
+
continue;
|
|
32525
|
+
}
|
|
32292
32526
|
const el = document.createElement(child.type);
|
|
32293
32527
|
const props = child.props || {};
|
|
32294
32528
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -32304,7 +32538,9 @@ const Head = ({
|
|
|
32304
32538
|
titleEntries.push(titleEntry);
|
|
32305
32539
|
document.title = titleEntry.title;
|
|
32306
32540
|
}
|
|
32541
|
+
const removeThemeColors = themeColors.length > 0 ? pushHeadThemeColors(themeColors) : null;
|
|
32307
32542
|
return () => {
|
|
32543
|
+
removeThemeColors?.();
|
|
32308
32544
|
if (titleEntry) {
|
|
32309
32545
|
titleEntries.splice(titleEntries.indexOf(titleEntry), 1);
|
|
32310
32546
|
const lastEntry = titleEntries[titleEntries.length - 1];
|
|
@@ -64607,9 +64843,17 @@ const css$E = /* css */`
|
|
|
64607
64843
|
on its own, sharp, above the wall: any wall on that frame — even a
|
|
64608
64844
|
half-strength wash and blur — is a flash on the anchor. So the wall
|
|
64609
64845
|
arrives from nothing, over the movement: on that frame the page is
|
|
64610
|
-
untouched, and the anchor with it.
|
|
64846
|
+
untouched, and the anchor with it.
|
|
64847
|
+
Keyed on the aria-expanded flip, like the local wall: openEffect reads
|
|
64848
|
+
the layout while the transitions are off, which spends @starting-style
|
|
64849
|
+
with nothing armed. @starting-style stays for an opening that reads
|
|
64850
|
+
nothing before the flip. A silent opening flips before the transitions
|
|
64851
|
+
come back, hence the :not(): its wall lands at once. */
|
|
64611
64852
|
&[data-lifting] {
|
|
64612
|
-
|
|
64853
|
+
&[aria-expanded="false"]::backdrop {
|
|
64854
|
+
opacity: 0;
|
|
64855
|
+
}
|
|
64856
|
+
&[aria-expanded="true"]:not([navi-transitions-off])::backdrop {
|
|
64613
64857
|
opacity: 1;
|
|
64614
64858
|
transition-property: opacity;
|
|
64615
64859
|
transition-duration: var(--navi-popup-lift-duration, 0.25s);
|
|
@@ -65069,6 +65313,16 @@ const css$E = /* css */`
|
|
|
65069
65313
|
* `saturate()`/`grayscale()` work too. How far what is behind withdraws is
|
|
65070
65314
|
* a question of paint, not of what an outside click does — a backdrop that
|
|
65071
65315
|
* closes can blur.
|
|
65316
|
+
* @param {string|false} [props.themeColor] - The colour the browser paints
|
|
65317
|
+
* above the page (`<meta name="theme-color">`: Chrome's address bar on
|
|
65318
|
+
* Android, an installed app's status bar) while this dialog is open.
|
|
65319
|
+
* Left out, it is what the dialog lays over the top edge of the screen: its
|
|
65320
|
+
* backdrop over the colour the page asked for (`<Head>`), and its own
|
|
65321
|
+
* background over that when it is flush with the top edge
|
|
65322
|
+
* (`marginWithContainer={0}` with `expand`/`expandY` or a top
|
|
65323
|
+
* `positionArea`). A colour forces it, for a surface whose colour cannot be
|
|
65324
|
+
* read (an image, a gradient); `false` leaves the page's colour alone.
|
|
65325
|
+
* `layer="top"` only.
|
|
65072
65326
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
65073
65327
|
* dialog so the page/container behind it can't scroll while it's open.
|
|
65074
65328
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
@@ -65590,6 +65844,9 @@ const useDialogProps = props => {
|
|
|
65590
65844
|
// so both objects below get them (see this file's CSS).
|
|
65591
65845
|
backdropColor,
|
|
65592
65846
|
backdropFilter,
|
|
65847
|
+
// Read at the opening, with the backdrop and the surface it would
|
|
65848
|
+
// otherwise be worked out from (see openEffect).
|
|
65849
|
+
themeColor,
|
|
65593
65850
|
scrollCapture: scrollCaptureProp,
|
|
65594
65851
|
// "auto" (default) → the dialog follows its content. "frozen" → measured
|
|
65595
65852
|
// once, held at that size while open. See this prop's own JSDoc above.
|
|
@@ -66173,6 +66430,16 @@ const useDialogProps = props => {
|
|
|
66173
66430
|
backdropEl.style.transitionProperty = "";
|
|
66174
66431
|
}
|
|
66175
66432
|
}
|
|
66433
|
+
// What the browser paints above the page goes with what is laid over its
|
|
66434
|
+
// top edge, from the start of the opening to the start of the closing: a
|
|
66435
|
+
// local dialog lays nothing there.
|
|
66436
|
+
if (isTopLayer && themeColor !== false) {
|
|
66437
|
+
addCleanup(pushPopupThemeColor({
|
|
66438
|
+
themeColor,
|
|
66439
|
+
backdropColor: backdrop ? getComputedStyle(dialogEl, "::backdrop").backgroundColor : null,
|
|
66440
|
+
surfaceColor: flushEdges.top ? getComputedStyle(dialogEl).backgroundColor : null
|
|
66441
|
+
}));
|
|
66442
|
+
}
|
|
66176
66443
|
const cancelOpenInteractionSuppression = !silent && hasCssTransitionAnimation ? suppressPointerEventsDuringTransition(dialogEl) : null;
|
|
66177
66444
|
// Handing the focus to a field is what raises the on-screen keyboard, and
|
|
66178
66445
|
// the keyboard takes away the very room this dialog was just placed
|
|
@@ -66975,6 +67242,12 @@ const css$D = /* css */`
|
|
|
66975
67242
|
* `saturate()`/`grayscale()` work too. How far what is behind withdraws is
|
|
66976
67243
|
* a question of paint, not of what an outside click does — a backdrop that
|
|
66977
67244
|
* closes can blur.
|
|
67245
|
+
* @param {string|false} [props.themeColor] - The colour the browser paints
|
|
67246
|
+
* above the page (`<meta name="theme-color">`: Chrome's address bar on
|
|
67247
|
+
* Android, an installed app's status bar) while this popover is open.
|
|
67248
|
+
* Left out, it is the backdrop laid over the colour the page asked for
|
|
67249
|
+
* (`<Head>`), and the page's colour when there is no backdrop. A colour
|
|
67250
|
+
* forces it; `false` leaves the page's colour alone. `layer="top"` only.
|
|
66978
67251
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
66979
67252
|
* popover so the page/container behind it can't scroll while it's open.
|
|
66980
67253
|
* @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
|
|
@@ -67317,6 +67590,9 @@ const usePopoverProps = props => {
|
|
|
67317
67590
|
// which is a sibling element (see this file's top comment).
|
|
67318
67591
|
backdropColor,
|
|
67319
67592
|
backdropFilter,
|
|
67593
|
+
// Read at the opening, with the backdrop it would otherwise be worked out
|
|
67594
|
+
// from (see openEffect).
|
|
67595
|
+
themeColor,
|
|
67320
67596
|
group,
|
|
67321
67597
|
scrollCapture,
|
|
67322
67598
|
focusCapture,
|
|
@@ -67921,6 +68197,15 @@ const usePopoverProps = props => {
|
|
|
67921
68197
|
backdropEl.style.transitionProperty = "";
|
|
67922
68198
|
}
|
|
67923
68199
|
}
|
|
68200
|
+
// What the browser paints above the page goes with what is laid over its
|
|
68201
|
+
// top edge, from the start of the opening to the start of the closing: a
|
|
68202
|
+
// local popover's backdrop stops at its container.
|
|
68203
|
+
if (isTopLayer && themeColor !== false) {
|
|
68204
|
+
addCleanup(pushPopupThemeColor({
|
|
68205
|
+
themeColor,
|
|
68206
|
+
backdropColor: backdropEl ? getComputedStyle(backdropEl).backgroundColor : null
|
|
68207
|
+
}));
|
|
68208
|
+
}
|
|
67924
68209
|
const cancelOpenInteractionSuppression = !silent && hasCssTransitionAnimation ? suppressPointerEventsDuringTransition(popoverEl) : null;
|
|
67925
68210
|
// autoFocus={false}: opening moves no focus at all — whoever has the
|
|
67926
68211
|
// keyboard keeps it (combobox-style suggestions under an input being
|
|
@@ -68336,6 +68621,10 @@ const css$C = /* css */`@layer navi {
|
|
|
68336
68621
|
* identically): the wash the backdrop paints over what is behind.
|
|
68337
68622
|
* @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
|
|
68338
68623
|
* does to the picture underneath, `"blur(4px)"` and the like.
|
|
68624
|
+
* @param {string|false} [props.themeColor] - Forwarded as-is: the colour the
|
|
68625
|
+
* browser paints above the page while the popup is open, worked out from
|
|
68626
|
+
* what it lays over the top edge when left out. A colour forces it, `false`
|
|
68627
|
+
* leaves the page's colour alone.
|
|
68339
68628
|
* @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
68629
|
* - Forwarded to whichever renders — see either component's own doc.
|
|
68341
68630
|
* `"expanding"`/`"expand-*"` are `Popover`'s own; `"lifting"` and
|
|
@@ -69333,6 +69622,26 @@ const PickerCustom = props => {
|
|
|
69333
69622
|
if (isWithinPickerContent(e.target)) {
|
|
69334
69623
|
return null;
|
|
69335
69624
|
}
|
|
69625
|
+
// Still open at the click of a later press: that press brought no
|
|
69626
|
+
// mousedown to close on (a drag source cancelled its pointerdown),
|
|
69627
|
+
// so it closes here, as it opens here in that case. A mousedown
|
|
69628
|
+
// that came has closed it already; the press that opened it is
|
|
69629
|
+
// only ending.
|
|
69630
|
+
if (openController.opened && !openedDuringThisPress(openController)) {
|
|
69631
|
+
return {
|
|
69632
|
+
name: "click to close picker",
|
|
69633
|
+
intent: "read",
|
|
69634
|
+
prevented: () => {
|
|
69635
|
+
e.preventDefault();
|
|
69636
|
+
},
|
|
69637
|
+
allowed: () => {
|
|
69638
|
+
requestClose(e, {
|
|
69639
|
+
isCancel: true
|
|
69640
|
+
});
|
|
69641
|
+
e.preventDefault();
|
|
69642
|
+
}
|
|
69643
|
+
};
|
|
69644
|
+
}
|
|
69336
69645
|
if (!opensOnPress) {
|
|
69337
69646
|
// Neither the tap, nor the click a hold leaves behind: the press
|
|
69338
69647
|
// is not what opens this picker.
|
|
@@ -69988,12 +70297,12 @@ const isScrolling = (element) => {
|
|
|
69988
70297
|
if (element === undefined) {
|
|
69989
70298
|
return scrollActivitySignal.peek();
|
|
69990
70299
|
}
|
|
69991
|
-
return asScroller(element).hasAttribute("navi-scrolling");
|
|
70300
|
+
return asScroller$1(element).hasAttribute("navi-scrolling");
|
|
69992
70301
|
};
|
|
69993
70302
|
|
|
69994
70303
|
// The page scroll is dispatched on the document; the element carrying the
|
|
69995
70304
|
// attribute is the one CSS can reach, document.scrollingElement.
|
|
69996
|
-
const asScroller = (eventTargetOrElement) => {
|
|
70305
|
+
const asScroller$1 = (eventTargetOrElement) => {
|
|
69997
70306
|
if (eventTargetOrElement === document || eventTargetOrElement === window) {
|
|
69998
70307
|
return document.scrollingElement;
|
|
69999
70308
|
}
|
|
@@ -70004,7 +70313,7 @@ const idleTimeoutMap = new Map();
|
|
|
70004
70313
|
document.addEventListener(
|
|
70005
70314
|
"scroll",
|
|
70006
70315
|
(e) => {
|
|
70007
|
-
const scroller = asScroller(e.target);
|
|
70316
|
+
const scroller = asScroller$1(e.target);
|
|
70008
70317
|
const idleTimeout = idleTimeoutMap.get(scroller);
|
|
70009
70318
|
if (idleTimeout !== undefined) {
|
|
70010
70319
|
clearTimeout(idleTimeout);
|
|
@@ -70026,6 +70335,111 @@ document.addEventListener(
|
|
|
70026
70335
|
{ capture: true, passive: true },
|
|
70027
70336
|
);
|
|
70028
70337
|
|
|
70338
|
+
/*
|
|
70339
|
+
* Whether a scroller is gliding: moving on its own toward a place it was sent
|
|
70340
|
+
* to, with no hand on it — a key (Home, PageDown, Space), a smooth `scrollTo`
|
|
70341
|
+
* or `scrollIntoView`, the status bar tap on iOS. The other scrolls follow a
|
|
70342
|
+
* hand: a finger on the screen, a wheel turning.
|
|
70343
|
+
*
|
|
70344
|
+
* It matters to whoever writes a scroll position while one plays: the write is
|
|
70345
|
+
* an instant scroll, and the browser abandons the glide for it, short of where
|
|
70346
|
+
* it was going. A hand is not abandoned: its next report moves the scroller on
|
|
70347
|
+
* from wherever the write left it.
|
|
70348
|
+
*
|
|
70349
|
+
* A scroll is followed from its first "scroll" event to its "scrollend". It is
|
|
70350
|
+
* a hand's when a finger was down as it started (the fling a finger leaves
|
|
70351
|
+
* behind is still its own), when one is down now, or when a wheel turned
|
|
70352
|
+
* around the time it started or since. A finger is down from its
|
|
70353
|
+
* touchstart to its touchend, heard on what it touched: a list removes the
|
|
70354
|
+
* items scrolled out of its window while the finger is still on one of them,
|
|
70355
|
+
* and the touchend of a removed element never reaches the document.
|
|
70356
|
+
*
|
|
70357
|
+
* In a browser without "scrollend", nothing is said to glide: a scroll would
|
|
70358
|
+
* never be seen ending.
|
|
70359
|
+
*/
|
|
70360
|
+
|
|
70361
|
+
// The scroll a wheel turn causes starts in the frame the turn is read, or the
|
|
70362
|
+
// next one. A turn further from it moved nothing (a wheel at an edge) and is no
|
|
70363
|
+
// hand on a scroll that came from elsewhere.
|
|
70364
|
+
const WHEEL_REACH = 100;
|
|
70365
|
+
|
|
70366
|
+
const flightMap = new WeakMap();
|
|
70367
|
+
let fingersDown = 0;
|
|
70368
|
+
let wheelTimeStamp = -Infinity;
|
|
70369
|
+
|
|
70370
|
+
/**
|
|
70371
|
+
* @param {Element} scrollerEl
|
|
70372
|
+
* @returns {boolean}
|
|
70373
|
+
*/
|
|
70374
|
+
const isScrollGliding = (scrollerEl) => {
|
|
70375
|
+
const flight = flightMap.get(scrollerEl);
|
|
70376
|
+
if (!flight) {
|
|
70377
|
+
return false;
|
|
70378
|
+
}
|
|
70379
|
+
if (flight.fingerDown || fingersDown > 0) {
|
|
70380
|
+
return false;
|
|
70381
|
+
}
|
|
70382
|
+
if (wheelTimeStamp > flight.timeStamp - WHEEL_REACH) {
|
|
70383
|
+
return false;
|
|
70384
|
+
}
|
|
70385
|
+
return true;
|
|
70386
|
+
};
|
|
70387
|
+
|
|
70388
|
+
// The page scroll is dispatched on the document; the scroller asked about is
|
|
70389
|
+
// document.scrollingElement.
|
|
70390
|
+
const asScroller = (eventTarget) => {
|
|
70391
|
+
if (eventTarget === document || eventTarget === window) {
|
|
70392
|
+
return document.scrollingElement;
|
|
70393
|
+
}
|
|
70394
|
+
return eventTarget;
|
|
70395
|
+
};
|
|
70396
|
+
|
|
70397
|
+
const onTouchEnd = (e) => {
|
|
70398
|
+
fingersDown = e.touches.length;
|
|
70399
|
+
};
|
|
70400
|
+
|
|
70401
|
+
if ("onscrollend" in window) {
|
|
70402
|
+
// Capturing: "scroll" and "scrollend" do not bubble from an element.
|
|
70403
|
+
document.addEventListener(
|
|
70404
|
+
"scroll",
|
|
70405
|
+
(e) => {
|
|
70406
|
+
const scroller = asScroller(e.target);
|
|
70407
|
+
if (flightMap.has(scroller)) {
|
|
70408
|
+
return;
|
|
70409
|
+
}
|
|
70410
|
+
flightMap.set(scroller, {
|
|
70411
|
+
timeStamp: e.timeStamp,
|
|
70412
|
+
fingerDown: fingersDown > 0,
|
|
70413
|
+
});
|
|
70414
|
+
},
|
|
70415
|
+
{ capture: true, passive: true },
|
|
70416
|
+
);
|
|
70417
|
+
document.addEventListener(
|
|
70418
|
+
"scrollend",
|
|
70419
|
+
(e) => {
|
|
70420
|
+
flightMap.delete(asScroller(e.target));
|
|
70421
|
+
},
|
|
70422
|
+
{ capture: true, passive: true },
|
|
70423
|
+
);
|
|
70424
|
+
window.addEventListener(
|
|
70425
|
+
"wheel",
|
|
70426
|
+
(e) => {
|
|
70427
|
+
wheelTimeStamp = e.timeStamp;
|
|
70428
|
+
},
|
|
70429
|
+
{ capture: true, passive: true },
|
|
70430
|
+
);
|
|
70431
|
+
window.addEventListener(
|
|
70432
|
+
"touchstart",
|
|
70433
|
+
(e) => {
|
|
70434
|
+
fingersDown = e.touches.length;
|
|
70435
|
+
const touchedEl = e.composedPath()[0];
|
|
70436
|
+
touchedEl.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
70437
|
+
touchedEl.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
70438
|
+
},
|
|
70439
|
+
{ capture: true, passive: true },
|
|
70440
|
+
);
|
|
70441
|
+
}
|
|
70442
|
+
|
|
70029
70443
|
const LoadingDotsSvg = () => {
|
|
70030
70444
|
return jsxs("svg", {
|
|
70031
70445
|
viewBox: "0 0 200 200",
|
|
@@ -73109,7 +73523,9 @@ const useListScrollSync = ({
|
|
|
73109
73523
|
const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
|
|
73110
73524
|
const startPlaceRef = useRef({
|
|
73111
73525
|
userTookOver: false,
|
|
73112
|
-
wanted: scrolledWanted
|
|
73526
|
+
wanted: scrolledWanted,
|
|
73527
|
+
way: null,
|
|
73528
|
+
arrived: false
|
|
73113
73529
|
});
|
|
73114
73530
|
if (startPlaceRef.current.wanted !== scrolledWanted) {
|
|
73115
73531
|
startPlaceRef.current.wanted = scrolledWanted;
|
|
@@ -73319,8 +73735,10 @@ const useListScrollSync = ({
|
|
|
73319
73735
|
// keeps moving while the list is still finding out how many items it has and
|
|
73320
73736
|
// how tall one is, so landing there once would land next to it. What ends the
|
|
73321
73737
|
// hold is the user reaching for the list — a wheel, a finger, a key, a hand
|
|
73322
|
-
// on the scrollbar —
|
|
73323
|
-
// as
|
|
73738
|
+
// on the scrollbar — or anything else moving it. A scroll event alone does
|
|
73739
|
+
// not say which: the list provokes as many as the user does, so only one
|
|
73740
|
+
// landing off the way of the list's own placement ends it (see
|
|
73741
|
+
// sendHeldScroll).
|
|
73324
73742
|
const placeWhereHeld = () => {
|
|
73325
73743
|
if (scrolledWanted === "start" || scrolledWanted === undefined || startPlaceRef.current.userTookOver || !ref.current) {
|
|
73326
73744
|
return;
|
|
@@ -73369,12 +73787,7 @@ const useListScrollSync = ({
|
|
|
73369
73787
|
}
|
|
73370
73788
|
const scrollerEl = getScroller();
|
|
73371
73789
|
if (openAt === "end") {
|
|
73372
|
-
|
|
73373
|
-
if (horizontal) {
|
|
73374
|
-
scrollerEl.scrollLeft = scrollerEl.scrollWidth;
|
|
73375
|
-
} else {
|
|
73376
|
-
scrollerEl.scrollTop = scrollerEl.scrollHeight;
|
|
73377
|
-
}
|
|
73790
|
+
sendHeldScroll(horizontal ? scrollerEl.scrollWidth : scrollerEl.scrollHeight);
|
|
73378
73791
|
return;
|
|
73379
73792
|
}
|
|
73380
73793
|
if (typeof openAt === "number") {
|
|
@@ -73398,15 +73811,45 @@ const useListScrollSync = ({
|
|
|
73398
73811
|
const itemRect = itemEl.getBoundingClientRect();
|
|
73399
73812
|
const offsetWanted = resolveOpenOffset(getItemScrollInset(scrollerEl, itemEl, horizontal) + offset, horizontal ? viewportRect.width : viewportRect.height, horizontal ? itemRect.width : itemRect.height);
|
|
73400
73813
|
const offsetNow = horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top;
|
|
73814
|
+
const scrollNow = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
73401
73815
|
const delta = offsetNow - offsetWanted;
|
|
73402
73816
|
if (delta > -0.5 && delta < 0.5) {
|
|
73817
|
+
sendHeldScroll(scrollNow);
|
|
73403
73818
|
return;
|
|
73404
73819
|
}
|
|
73820
|
+
sendHeldScroll(scrollNow + delta);
|
|
73821
|
+
};
|
|
73822
|
+
// Every placement goes through here, the ones that find the list in place
|
|
73823
|
+
// too: it keeps the way from where the scroller was to where it is sent,
|
|
73824
|
+
// which is how a scroll event tells the hold's own scrolls from anyone
|
|
73825
|
+
// else's (see letGoUnlessHeldScroll). The whole way, not just its end: under
|
|
73826
|
+
// `scroll-behavior: smooth` the write glides, and every position on the way
|
|
73827
|
+
// is the list's until it gets there.
|
|
73828
|
+
const sendHeldScroll = position => {
|
|
73829
|
+
const scrollerEl = getScroller();
|
|
73830
|
+
const from = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
73831
|
+
const max = horizontal ? scrollerEl.scrollWidth - scrollerEl.clientWidth : scrollerEl.scrollHeight - scrollerEl.clientHeight;
|
|
73832
|
+
let to = position;
|
|
73833
|
+
if (to > max) {
|
|
73834
|
+
to = max;
|
|
73835
|
+
}
|
|
73836
|
+
if (to < 0) {
|
|
73837
|
+
to = 0;
|
|
73838
|
+
}
|
|
73405
73839
|
anchorRef.current = null;
|
|
73840
|
+
const place = startPlaceRef.current;
|
|
73841
|
+
place.way = {
|
|
73842
|
+
from,
|
|
73843
|
+
to
|
|
73844
|
+
};
|
|
73845
|
+
if (to === from) {
|
|
73846
|
+
place.arrived = true;
|
|
73847
|
+
return;
|
|
73848
|
+
}
|
|
73406
73849
|
if (horizontal) {
|
|
73407
|
-
scrollerEl.scrollLeft
|
|
73850
|
+
scrollerEl.scrollLeft = to;
|
|
73408
73851
|
} else {
|
|
73409
|
-
scrollerEl.scrollTop
|
|
73852
|
+
scrollerEl.scrollTop = to;
|
|
73410
73853
|
}
|
|
73411
73854
|
};
|
|
73412
73855
|
useLayoutEffect(placeWhereHeld);
|
|
@@ -73479,6 +73922,19 @@ const useListScrollSync = ({
|
|
|
73479
73922
|
evaluateWindowRef.current("items resized");
|
|
73480
73923
|
return false;
|
|
73481
73924
|
};
|
|
73925
|
+
// The box around a held list changing shape is the hold's to answer too: it
|
|
73926
|
+
// places the list again, the way it writes every scroll of its own (see
|
|
73927
|
+
// sendHeldScroll) — written any other way, the scroll would read as someone
|
|
73928
|
+
// else moving the list, and end the hold.
|
|
73929
|
+
const placeIfHeldRef = useRef(null);
|
|
73930
|
+
placeIfHeldRef.current = () => {
|
|
73931
|
+
// Read again: a scroll may have ended the hold since this render.
|
|
73932
|
+
if (!heldSomewhere || startPlaceRef.current.userTookOver) {
|
|
73933
|
+
return false;
|
|
73934
|
+
}
|
|
73935
|
+
placeWhereHeld();
|
|
73936
|
+
return true;
|
|
73937
|
+
};
|
|
73482
73938
|
useLayoutEffect(() => {
|
|
73483
73939
|
if (scrolledWanted === "start" || scrolledWanted === undefined || !ref.current) {
|
|
73484
73940
|
return undefined;
|
|
@@ -73623,6 +74079,9 @@ const useListScrollSync = ({
|
|
|
73623
74079
|
if (!entries.some(entry => entry.target === scrollerEl)) {
|
|
73624
74080
|
return;
|
|
73625
74081
|
}
|
|
74082
|
+
if (placeIfHeldRef.current()) {
|
|
74083
|
+
return;
|
|
74084
|
+
}
|
|
73626
74085
|
const position = positionRef.current;
|
|
73627
74086
|
if (!position) {
|
|
73628
74087
|
return;
|
|
@@ -73735,6 +74194,15 @@ const useListScrollSync = ({
|
|
|
73735
74194
|
return;
|
|
73736
74195
|
}
|
|
73737
74196
|
anchorRef.current = null;
|
|
74197
|
+
if (isScrollGliding(scrollerEl)) {
|
|
74198
|
+
// Nobody is looking at items standing still: they are passing by, on
|
|
74199
|
+
// their way somewhere the browser was asked to take them (a key, a smooth
|
|
74200
|
+
// scrollTo, the status bar). The write would be an instant scroll, and
|
|
74201
|
+
// the browser abandons the glide for it, short of where it was going.
|
|
74202
|
+
// Left alone, the items drawn above push the rest by `drift` while all
|
|
74203
|
+
// of it moves, and the glide lands.
|
|
74204
|
+
return;
|
|
74205
|
+
}
|
|
73738
74206
|
scrolledByListRef.current = true;
|
|
73739
74207
|
if (horizontal) {
|
|
73740
74208
|
scrollerEl.scrollLeft += drift;
|
|
@@ -74062,9 +74530,44 @@ const useListScrollSync = ({
|
|
|
74062
74530
|
reportPosition();
|
|
74063
74531
|
return;
|
|
74064
74532
|
}
|
|
74533
|
+
letGoUnlessHeldScroll();
|
|
74065
74534
|
reportPosition();
|
|
74066
74535
|
evaluateWindowRef.current("scroll");
|
|
74067
74536
|
};
|
|
74537
|
+
// A scroll the hold did not write is someone else moving the list, which
|
|
74538
|
+
// ends the hold like a wheel does — the ones that come with no input event
|
|
74539
|
+
// at all: the status bar tap on iOS, find in page, a script.
|
|
74540
|
+
// Only once the list has stood where it holds itself, though. Until then
|
|
74541
|
+
// the page is still settling around it, and some of that is written after
|
|
74542
|
+
// the list placed itself: a navigation holding the rendering for its
|
|
74543
|
+
// transition takes the arriving page to its top once the page rendered
|
|
74544
|
+
// (whenRenderingResumes), and the hold puts it back at its next commit.
|
|
74545
|
+
const letGoUnlessHeldScroll = () => {
|
|
74546
|
+
const place = startPlaceRef.current;
|
|
74547
|
+
const way = place.way;
|
|
74548
|
+
if (place.userTookOver || place.wanted === "start" || place.wanted === undefined || !way) {
|
|
74549
|
+
return;
|
|
74550
|
+
}
|
|
74551
|
+
const position = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
74552
|
+
if (position > way.to - 1 && position < way.to + 1) {
|
|
74553
|
+
// From then on only that place is the list's, or a scroll back the
|
|
74554
|
+
// way it came would pass for its own.
|
|
74555
|
+
place.way = {
|
|
74556
|
+
from: way.to,
|
|
74557
|
+
to: way.to
|
|
74558
|
+
};
|
|
74559
|
+
place.arrived = true;
|
|
74560
|
+
return;
|
|
74561
|
+
}
|
|
74562
|
+
if (!place.arrived) {
|
|
74563
|
+
return;
|
|
74564
|
+
}
|
|
74565
|
+
const low = way.from < way.to ? way.from : way.to;
|
|
74566
|
+
const high = way.from < way.to ? way.to : way.from;
|
|
74567
|
+
if (position < low - 1 || position > high + 1) {
|
|
74568
|
+
place.userTookOver = true;
|
|
74569
|
+
}
|
|
74570
|
+
};
|
|
74068
74571
|
// A page-level scroller does not emit "scroll" on the element itself
|
|
74069
74572
|
// (document.scrollingElement); the document does.
|
|
74070
74573
|
const scrollEventTarget = scrollerEl === document.scrollingElement ? document : scrollerEl;
|
|
@@ -77453,7 +77956,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
77453
77956
|
*
|
|
77454
77957
|
* In every form the list holds itself there while it is still finding out
|
|
77455
77958
|
* how many items there are and how tall one is, and lets go the moment the
|
|
77456
|
-
* user reaches for the list
|
|
77959
|
+
* user reaches for the list, or anything else scrolls it (a script, find in
|
|
77960
|
+
* page, the status bar tap on iOS).
|
|
77457
77961
|
* @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
|
|
77458
77962
|
* Where the list is, as the user scrolls: the item at the top of the view and
|
|
77459
77963
|
* how far below the place an item lands on its own (see `defaultScrolled`) it
|