@jsenv/navi 0.29.430 → 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 +705 -52
- package/dist/dev/jsenv_navi.js.map +23 -15
- package/dist/jsenv_navi.js +705 -52
- package/dist/jsenv_navi.js.map +23 -15
- package/docs/AI_INSTRUCTIONS.md +3 -1
- package/docs/navigation.md +28 -1
- package/docs/popup_backdrop.md +53 -0
- package/docs/popup_open.md +4 -1
- package/docs/route_transitions.md +8 -3
- 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";
|
|
@@ -6750,6 +6750,55 @@ const applyNavigationToNavDepth = (navigationType, state) => {
|
|
|
6750
6750
|
canNavForwardSignal.value = navDepth < navDepthMax;
|
|
6751
6751
|
};
|
|
6752
6752
|
|
|
6753
|
+
/**
|
|
6754
|
+
* Whether the render running now is the one putting a new page on screen.
|
|
6755
|
+
*
|
|
6756
|
+
* A layer drawn over the pages — a popup bound to the address, the slides
|
|
6757
|
+
* inside it — can open, close or change place in the very navigation that
|
|
6758
|
+
* replaces the page beneath it: a link inside the layer leads to another page
|
|
6759
|
+
* and the layer's weak params are dropped, a back to the page it was open over
|
|
6760
|
+
* brings them back. For the layer that is not a movement of its own: the page
|
|
6761
|
+
* changing IS the movement. A route transition already carries it, photographed
|
|
6762
|
+
* as furniture with the page it belongs to (transition_furniture.js), and with
|
|
6763
|
+
* no transition the page cuts and the layer cuts with it. Played on its own as
|
|
6764
|
+
* well, it moves twice — an entrance or an exit over a page sliding past, or
|
|
6765
|
+
* appearing at once.
|
|
6766
|
+
*
|
|
6767
|
+
* So whatever reacts to such a change in a render — a popup's open state, a
|
|
6768
|
+
* slide container's current slide — asks here whether that render carries a
|
|
6769
|
+
* page change, and if so arrives where it goes without travelling there.
|
|
6770
|
+
*
|
|
6771
|
+
* A page is what navi animates between (route_page.js): a route, and the
|
|
6772
|
+
* params telling apart branches of one route. Which routes match is decided by
|
|
6773
|
+
* the path alone, so a navigation that changes the path changes the page; one
|
|
6774
|
+
* that keeps it changes the page only where a param IS a page — and navi knows
|
|
6775
|
+
* those only through the relation or the RouteTravel row that names them, so
|
|
6776
|
+
* that is who says so. A search param a page merely reads is not one: writing
|
|
6777
|
+
* it is the page answering, and a layer opened by it (the `?settings` its door
|
|
6778
|
+
* pushes, the back button taking it out) moves on its own.
|
|
6779
|
+
*
|
|
6780
|
+
* The answer lasts until the render applying the navigation is over: the held
|
|
6781
|
+
* one when somebody photographs the change, Preact's own otherwise, over by the
|
|
6782
|
+
* next frame (see whenPageRendered). Asked from a layout effect it is exact; an
|
|
6783
|
+
* opening deferred past the commit takes the answer with it, read when the
|
|
6784
|
+
* effect ran.
|
|
6785
|
+
*/
|
|
6786
|
+
|
|
6787
|
+
|
|
6788
|
+
let renderingPageChange = false;
|
|
6789
|
+
|
|
6790
|
+
const notePageChange = () => {
|
|
6791
|
+
if (renderingPageChange) {
|
|
6792
|
+
return;
|
|
6793
|
+
}
|
|
6794
|
+
renderingPageChange = true;
|
|
6795
|
+
whenPageRendered(() => {
|
|
6796
|
+
renderingPageChange = false;
|
|
6797
|
+
});
|
|
6798
|
+
};
|
|
6799
|
+
|
|
6800
|
+
const isRenderingPageChange = () => renderingPageChange;
|
|
6801
|
+
|
|
6753
6802
|
// Global signal registry for route template detection
|
|
6754
6803
|
const globalSignalRegistry = new Map();
|
|
6755
6804
|
|
|
@@ -10333,8 +10382,12 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
10333
10382
|
// read again.
|
|
10334
10383
|
const path = urlWithoutSearchAndHash(url);
|
|
10335
10384
|
const pathKept = path === previousPath;
|
|
10385
|
+
// A new path is a new page: what the integration tells whatever reacts to
|
|
10386
|
+
// this change in the render it causes (see page_change.js). Not the first
|
|
10387
|
+
// one — there was no page before it, and what it mounts mounts as it is.
|
|
10388
|
+
const pageChanged = previousPath !== null && !pathKept;
|
|
10336
10389
|
previousPath = path;
|
|
10337
|
-
const returnValue = {};
|
|
10390
|
+
const returnValue = { pageChanged };
|
|
10338
10391
|
const routeMatchInfoSet = new Set();
|
|
10339
10392
|
for (const route of routeSet) {
|
|
10340
10393
|
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
@@ -11188,13 +11241,22 @@ const installScrollRestoration = () => {
|
|
|
11188
11241
|
},
|
|
11189
11242
|
{ passive: true },
|
|
11190
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
|
+
}
|
|
11191
11254
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
11192
11255
|
// Once, and at the first render of a route: the position is only meaningful
|
|
11193
11256
|
// once there is a page under it.
|
|
11194
11257
|
// Written once the commit that rendered it has ended: said from inside it,
|
|
11195
11258
|
// and what the offset shows may still have to be built (see scrollTo).
|
|
11196
|
-
|
|
11197
|
-
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
11259
|
+
if (positionOnLoad.x || positionOnLoad.y) {
|
|
11198
11260
|
const stopListening = observeRouteRender(() => {
|
|
11199
11261
|
stopListening();
|
|
11200
11262
|
queueMicrotask(() => {
|
|
@@ -11280,6 +11342,11 @@ const startAtTop = (url, { from } = {}) => {
|
|
|
11280
11342
|
return;
|
|
11281
11343
|
}
|
|
11282
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 });
|
|
11283
11350
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
11284
11351
|
};
|
|
11285
11352
|
const isArrival = (url, { from }) => {
|
|
@@ -12148,11 +12215,14 @@ const applyRouting = (
|
|
|
12148
12215
|
// The routes' actions follow the address by themselves (see routeAction):
|
|
12149
12216
|
// what a navigation leaves to the routing is a reload, the one that asks
|
|
12150
12217
|
// them again with the address unchanged.
|
|
12151
|
-
const { reloadSet } = updateRoutes(url, {
|
|
12218
|
+
const { pageChanged, reloadSet } = updateRoutes(url, {
|
|
12152
12219
|
navigationType,
|
|
12153
12220
|
isVisited,
|
|
12154
12221
|
// state,
|
|
12155
12222
|
});
|
|
12223
|
+
if (pageChanged) {
|
|
12224
|
+
notePageChange();
|
|
12225
|
+
}
|
|
12156
12226
|
if (!reloadSet || reloadSet.size === 0) {
|
|
12157
12227
|
return {
|
|
12158
12228
|
allResult: undefined,
|
|
@@ -15499,8 +15569,8 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15499
15569
|
// Tracks whether the effect below has ever run before — only the very
|
|
15500
15570
|
// first run gets the "mount already open" treatment (`open` truthy from
|
|
15501
15571
|
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
15502
|
-
// subsequent `open` change is a real, later toggle and
|
|
15503
|
-
//
|
|
15572
|
+
// subsequent `open` change is a real, later toggle and animates like any
|
|
15573
|
+
// interactive open/close — unless a page change carries it (see below).
|
|
15504
15574
|
const isFirstRunRef = useRef(true);
|
|
15505
15575
|
// What `open` was on the previous run of the effect below. preact re-runs
|
|
15506
15576
|
// an effect for a change of its deps — or for none at all, when a
|
|
@@ -15520,6 +15590,12 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15520
15590
|
isFirstRunRef.current = false;
|
|
15521
15591
|
const openChanged = open !== lastRunOpenRef.current;
|
|
15522
15592
|
lastRunOpenRef.current = open;
|
|
15593
|
+
// Opened or closed by the navigation that puts another page on screen: a
|
|
15594
|
+
// link inside the popup leading away, a back to the page it stood over.
|
|
15595
|
+
// The page changing is its movement, so it has none of its own — a route
|
|
15596
|
+
// transition carries it with its page, and without one it cuts with the
|
|
15597
|
+
// page (see page_change.js).
|
|
15598
|
+
const carriedByPageChange = isRenderingPageChange();
|
|
15523
15599
|
|
|
15524
15600
|
if (isFirstRun) {
|
|
15525
15601
|
// "interaction" is said even over what the caller holds reading open: a
|
|
@@ -15538,10 +15614,12 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15538
15614
|
// other truthy value means it was simply already open: nothing was ever
|
|
15539
15615
|
// shown as "closed" for the user to see it transition away from, so the
|
|
15540
15616
|
// entrance is skipped (`silent`, see popover.jsx's own openEffect).
|
|
15541
|
-
//
|
|
15617
|
+
// A popup built by an opening a page change carries is silent too,
|
|
15618
|
+
// whatever built it: read now, the answer only holds for this render.
|
|
15619
|
+
const silent = mountOpenReason !== "interaction" || carriedByPageChange;
|
|
15542
15620
|
mountOpenOwedRef.current = () =>
|
|
15543
15621
|
openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
|
|
15544
|
-
silent
|
|
15622
|
+
silent,
|
|
15545
15623
|
});
|
|
15546
15624
|
}
|
|
15547
15625
|
return undefined;
|
|
@@ -15573,11 +15651,13 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15573
15651
|
});
|
|
15574
15652
|
return undefined;
|
|
15575
15653
|
}
|
|
15576
|
-
openController.open(new CustomEvent("open_by_prop", { detail: {} })
|
|
15654
|
+
openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
|
|
15655
|
+
silent: carriedByPageChange,
|
|
15656
|
+
});
|
|
15577
15657
|
} else {
|
|
15578
15658
|
openController.requestClose(
|
|
15579
15659
|
new CustomEvent("close_by_prop", { detail: {} }),
|
|
15580
|
-
{ isCancel: true },
|
|
15660
|
+
{ isCancel: true, silent: carriedByPageChange },
|
|
15581
15661
|
);
|
|
15582
15662
|
}
|
|
15583
15663
|
// The request can be refused (a busy form denying the close): the popup
|
|
@@ -16280,7 +16360,7 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
|
16280
16360
|
},
|
|
16281
16361
|
children: jsx$1
|
|
16282
16362
|
});
|
|
16283
|
-
render(calloutJsx, calloutMessageElement);
|
|
16363
|
+
render$1(calloutJsx, calloutMessageElement);
|
|
16284
16364
|
};
|
|
16285
16365
|
|
|
16286
16366
|
// An HTML message is rendered through preact rather than innerHTML: what a
|
|
@@ -16303,7 +16383,7 @@ const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
|
16303
16383
|
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
16304
16384
|
// tree is still there.
|
|
16305
16385
|
const clearCalloutMessage = calloutMessageElement => {
|
|
16306
|
-
render(null, calloutMessageElement);
|
|
16386
|
+
render$1(null, calloutMessageElement);
|
|
16307
16387
|
calloutMessageElement.innerHTML = "";
|
|
16308
16388
|
};
|
|
16309
16389
|
const domToVNodes = node => {
|
|
@@ -32196,10 +32276,222 @@ const useUrlSearchParam = (paramName, defaultValue) => {
|
|
|
32196
32276
|
return [value, setSearchParamValue];
|
|
32197
32277
|
};
|
|
32198
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
|
+
|
|
32199
32489
|
// The title belongs to the last Head to arrive, whatever order they leave in:
|
|
32200
32490
|
// the page being left by a route transition is taken down AFTER the page
|
|
32201
32491
|
// arriving is up (see keepLeavingPages in route.jsx), and putting back the
|
|
32202
|
-
// 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.
|
|
32203
32495
|
const titleEntries = [];
|
|
32204
32496
|
let titleWithoutHead = null;
|
|
32205
32497
|
const Head = ({
|
|
@@ -32211,6 +32503,7 @@ const Head = ({
|
|
|
32211
32503
|
}
|
|
32212
32504
|
const childArray = Array.isArray(children) ? children : [children];
|
|
32213
32505
|
const appendedElements = [];
|
|
32506
|
+
const themeColors = [];
|
|
32214
32507
|
let titleEntry = null;
|
|
32215
32508
|
for (const child of childArray) {
|
|
32216
32509
|
if (!child) {
|
|
@@ -32223,6 +32516,13 @@ const Head = ({
|
|
|
32223
32516
|
};
|
|
32224
32517
|
continue;
|
|
32225
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
|
+
}
|
|
32226
32526
|
const el = document.createElement(child.type);
|
|
32227
32527
|
const props = child.props || {};
|
|
32228
32528
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -32238,7 +32538,9 @@ const Head = ({
|
|
|
32238
32538
|
titleEntries.push(titleEntry);
|
|
32239
32539
|
document.title = titleEntry.title;
|
|
32240
32540
|
}
|
|
32541
|
+
const removeThemeColors = themeColors.length > 0 ? pushHeadThemeColors(themeColors) : null;
|
|
32241
32542
|
return () => {
|
|
32543
|
+
removeThemeColors?.();
|
|
32242
32544
|
if (titleEntry) {
|
|
32243
32545
|
titleEntries.splice(titleEntries.indexOf(titleEntry), 1);
|
|
32244
32546
|
const lastEntry = titleEntries[titleEntries.length - 1];
|
|
@@ -35185,6 +35487,9 @@ const rebuildWatcher = () => {
|
|
|
35185
35487
|
if (fromIndex === index) {
|
|
35186
35488
|
return;
|
|
35187
35489
|
}
|
|
35490
|
+
// A page of a relation can be a param: a move between two of them is a
|
|
35491
|
+
// page change the path does not show (see page_change.js).
|
|
35492
|
+
notePageChange();
|
|
35188
35493
|
if (navigationAnimated) {
|
|
35189
35494
|
// The movement on screen was turned round for this very navigation (see
|
|
35190
35495
|
// turnRunningTransitionRound): it is already being answered, by the
|
|
@@ -36549,6 +36854,10 @@ const RouteTravel = ({
|
|
|
36549
36854
|
}
|
|
36550
36855
|
return;
|
|
36551
36856
|
}
|
|
36857
|
+
// Whoever asked for it: a tab of this row can be a param, and a move
|
|
36858
|
+
// between two of them is a page change the path does not show (see
|
|
36859
|
+
// page_change.js).
|
|
36860
|
+
notePageChange();
|
|
36552
36861
|
const page = pages[index];
|
|
36553
36862
|
// A page this box asked for itself — a travel's own navigation, or one
|
|
36554
36863
|
// it had given up waiting on: what arrives here is the answer to a
|
|
@@ -53059,11 +53368,13 @@ const Expandable = props => {
|
|
|
53059
53368
|
openController.openEffect = openEvent => {
|
|
53060
53369
|
const contentContainer = contentContainerRef.current;
|
|
53061
53370
|
// `silent`: the expandable was already open when the page appeared
|
|
53062
|
-
// (`open`/`defaultOpen` at mount
|
|
53063
|
-
//
|
|
53064
|
-
//
|
|
53065
|
-
//
|
|
53066
|
-
//
|
|
53371
|
+
// (`open`/`defaultOpen` at mount, or the navigation putting that page on
|
|
53372
|
+
// screen opening it — see page_change.js). Nothing changed for anyone to
|
|
53373
|
+
// be told about, nothing was ever shown closed to move away from, and a
|
|
53374
|
+
// page must not have its focus stolen by a section that was simply
|
|
53375
|
+
// already open — so an opening that was never an opening plays nothing,
|
|
53376
|
+
// says nothing and takes nothing. The action it carries still runs: its
|
|
53377
|
+
// content is due.
|
|
53067
53378
|
const silent = Boolean(openEvent.detail.silent);
|
|
53068
53379
|
const revealing = animation && !silent && Boolean(contentContainer);
|
|
53069
53380
|
// Where the last paint left the track — 0 when fully closed, partway when
|
|
@@ -53110,7 +53421,9 @@ const Expandable = props => {
|
|
|
53110
53421
|
effectiveAction.abort();
|
|
53111
53422
|
}
|
|
53112
53423
|
cancelSettleWatch();
|
|
53113
|
-
|
|
53424
|
+
// `silent`: closed by the navigation that puts another page on screen,
|
|
53425
|
+
// which is its movement (see page_change.js).
|
|
53426
|
+
const collapsing = animation && !closeEvent.detail.silent && Boolean(contentContainerAtClose);
|
|
53114
53427
|
if (collapsing) {
|
|
53115
53428
|
// Now, while the content is still fully laid out — the collapsing
|
|
53116
53429
|
// track uncovers a content frozen at that size.
|
|
@@ -60430,10 +60743,23 @@ const SlideContainer = ({
|
|
|
60430
60743
|
y: 0
|
|
60431
60744
|
};
|
|
60432
60745
|
const durationMs = durationToMs(duration);
|
|
60746
|
+
// A change of slide the page changing carries is no travel: the page is
|
|
60747
|
+
// the movement, and the slides land where the address puts them (see
|
|
60748
|
+
// page_change.js). A travel still playing is cut on the spot, and the
|
|
60749
|
+
// indicator riding it with it.
|
|
60750
|
+
const landsWithPage = isRenderingPageChange() && (stageRef.current ? stageRef.current.area : drawnAreaRef.current) !== currentArea;
|
|
60751
|
+
if (landsWithPage && trackAnimationRef.current) {
|
|
60752
|
+
trackAnimationRef.current.cancel();
|
|
60753
|
+
trackAnimationRef.current = null;
|
|
60754
|
+
movingRef.current = null;
|
|
60755
|
+
cancelTravelProgressAnimation();
|
|
60756
|
+
paintTravelProgress(0);
|
|
60757
|
+
}
|
|
60758
|
+
const still = noTravel || landsWithPage;
|
|
60433
60759
|
// Nothing is travelling, so nothing is staged: the picture to paint is the
|
|
60434
60760
|
// map itself, and a stage left over from a travel that has just been given
|
|
60435
60761
|
// up on would be painted instead of it.
|
|
60436
|
-
if (
|
|
60762
|
+
if (still) {
|
|
60437
60763
|
stageRef.current = null;
|
|
60438
60764
|
}
|
|
60439
60765
|
let stage = stageRef.current;
|
|
@@ -60441,7 +60767,7 @@ const SlideContainer = ({
|
|
|
60441
60767
|
// between the picture and the slide arriving, on the axis it walks.
|
|
60442
60768
|
let travelStep = null;
|
|
60443
60769
|
const drawnArea = stage ? stage.area : drawnAreaRef.current;
|
|
60444
|
-
const travelStarts = !
|
|
60770
|
+
const travelStarts = !still && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
|
|
60445
60771
|
if (travelStarts) {
|
|
60446
60772
|
// Where the slide being left IS — the place the stage gave it if a travel
|
|
60447
60773
|
// was already playing (a press landing mid-travel departs from where the
|
|
@@ -60568,7 +60894,7 @@ const SlideContainer = ({
|
|
|
60568
60894
|
// duration (the ask is zero, so ratioOfOneTravel answers 1), slowing the
|
|
60569
60895
|
// travel at every re-render.
|
|
60570
60896
|
const sameTravelPlaying = travelInFlight && !offsetDragged && offsetTargetBefore === offset;
|
|
60571
|
-
const travels = !sameTravelPlaying && !
|
|
60897
|
+
const travels = !sameTravelPlaying && !still && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
|
|
60572
60898
|
if (travels) {
|
|
60573
60899
|
// The time it takes is the distance it has left to cover: a travel picked
|
|
60574
60900
|
// up a fifth of the way through goes back in a fifth of the time, so what
|
|
@@ -64517,9 +64843,17 @@ const css$E = /* css */`
|
|
|
64517
64843
|
on its own, sharp, above the wall: any wall on that frame — even a
|
|
64518
64844
|
half-strength wash and blur — is a flash on the anchor. So the wall
|
|
64519
64845
|
arrives from nothing, over the movement: on that frame the page is
|
|
64520
|
-
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. */
|
|
64521
64852
|
&[data-lifting] {
|
|
64522
|
-
|
|
64853
|
+
&[aria-expanded="false"]::backdrop {
|
|
64854
|
+
opacity: 0;
|
|
64855
|
+
}
|
|
64856
|
+
&[aria-expanded="true"]:not([navi-transitions-off])::backdrop {
|
|
64523
64857
|
opacity: 1;
|
|
64524
64858
|
transition-property: opacity;
|
|
64525
64859
|
transition-duration: var(--navi-popup-lift-duration, 0.25s);
|
|
@@ -64530,6 +64864,12 @@ const css$E = /* css */`
|
|
|
64530
64864
|
}
|
|
64531
64865
|
}
|
|
64532
64866
|
}
|
|
64867
|
+
/* Off while the dialog's own transitions are off (switchTransitionsOff):
|
|
64868
|
+
the inline style that turns them off cannot reach a pseudo-element.
|
|
64869
|
+
After the rules above, which it must win over at equal weight. */
|
|
64870
|
+
&[navi-transitions-off]::backdrop {
|
|
64871
|
+
transition-property: none;
|
|
64872
|
+
}
|
|
64533
64873
|
|
|
64534
64874
|
&[data-focus-visible],
|
|
64535
64875
|
/* …or something filling it holds the keyboard and offers its ring to
|
|
@@ -64973,6 +65313,16 @@ const css$E = /* css */`
|
|
|
64973
65313
|
* `saturate()`/`grayscale()` work too. How far what is behind withdraws is
|
|
64974
65314
|
* a question of paint, not of what an outside click does — a backdrop that
|
|
64975
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.
|
|
64976
65326
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
64977
65327
|
* dialog so the page/container behind it can't scroll while it's open.
|
|
64978
65328
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
@@ -65494,6 +65844,9 @@ const useDialogProps = props => {
|
|
|
65494
65844
|
// so both objects below get them (see this file's CSS).
|
|
65495
65845
|
backdropColor,
|
|
65496
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,
|
|
65497
65850
|
scrollCapture: scrollCaptureProp,
|
|
65498
65851
|
// "auto" (default) → the dialog follows its content. "frozen" → measured
|
|
65499
65852
|
// once, held at that size while open. See this prop's own JSDoc above.
|
|
@@ -65744,8 +66097,9 @@ const useDialogProps = props => {
|
|
|
65744
66097
|
const dialogEl = ref.current;
|
|
65745
66098
|
// A mount-time opening was never seen closed (see openEffect's own
|
|
65746
66099
|
// `silent`): there is no box it comes from, because nothing was shown
|
|
65747
|
-
// before it.
|
|
65748
|
-
|
|
66100
|
+
// before it. And a change a page change carries moves with that page,
|
|
66101
|
+
// in and out of no anchor (see page_change.js).
|
|
66102
|
+
if (!dialogEl || event.detail.silent || opened && !lifting) {
|
|
65749
66103
|
applyChange();
|
|
65750
66104
|
return;
|
|
65751
66105
|
}
|
|
@@ -65785,8 +66139,9 @@ const useDialogProps = props => {
|
|
|
65785
66139
|
}
|
|
65786
66140
|
|
|
65787
66141
|
// Set by useOpenControllerByProps for the very first open triggered by
|
|
65788
|
-
// `open`/`defaultOpen` already being truthy at mount
|
|
65789
|
-
// own openEffect for the full
|
|
66142
|
+
// `open`/`defaultOpen` already being truthy at mount, and for an open a
|
|
66143
|
+
// page change carries — see popover.jsx's own openEffect for the full
|
|
66144
|
+
// reasoning, mirrored here identically.
|
|
65790
66145
|
const silent = Boolean(e.detail.silent);
|
|
65791
66146
|
|
|
65792
66147
|
// document.documentElement — the shared "no real container, use the
|
|
@@ -65830,7 +66185,7 @@ const useDialogProps = props => {
|
|
|
65830
66185
|
// comment), even though Dialog never needs to measure/flip anything: it
|
|
65831
66186
|
// still needs a genuinely rendered "closed" frame to transition from, not a
|
|
65832
66187
|
// jump straight from not-shown to aria-expanded="true".
|
|
65833
|
-
dialogEl
|
|
66188
|
+
switchTransitionsOff(dialogEl);
|
|
65834
66189
|
if (backdropEl) {
|
|
65835
66190
|
disarmBackdropHideRef.current?.();
|
|
65836
66191
|
disarmBackdropHideRef.current = null;
|
|
@@ -66063,18 +66418,28 @@ const useDialogProps = props => {
|
|
|
66063
66418
|
dialogEl.setAttribute("aria-expanded", "true");
|
|
66064
66419
|
backdropEl?.setAttribute("aria-expanded", "true");
|
|
66065
66420
|
dialogEl.getBoundingClientRect();
|
|
66066
|
-
dialogEl
|
|
66421
|
+
switchTransitionsOn(dialogEl);
|
|
66067
66422
|
if (backdropEl) {
|
|
66068
66423
|
backdropEl.style.transitionProperty = "";
|
|
66069
66424
|
}
|
|
66070
66425
|
} else {
|
|
66071
|
-
dialogEl
|
|
66426
|
+
switchTransitionsOn(dialogEl);
|
|
66072
66427
|
dialogEl.setAttribute("aria-expanded", "true");
|
|
66073
66428
|
backdropEl?.setAttribute("aria-expanded", "true");
|
|
66074
66429
|
if (backdropEl) {
|
|
66075
66430
|
backdropEl.style.transitionProperty = "";
|
|
66076
66431
|
}
|
|
66077
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
|
+
}
|
|
66078
66443
|
const cancelOpenInteractionSuppression = !silent && hasCssTransitionAnimation ? suppressPointerEventsDuringTransition(dialogEl) : null;
|
|
66079
66444
|
// Handing the focus to a field is what raises the on-screen keyboard, and
|
|
66080
66445
|
// the keyboard takes away the very room this dialog was just placed
|
|
@@ -66104,6 +66469,15 @@ const useDialogProps = props => {
|
|
|
66104
66469
|
return closeEvent => {
|
|
66105
66470
|
debugPopup(`"${closeEvent.type}" on ${getElementSignature(closeEvent.target)} -> closeDialog`);
|
|
66106
66471
|
clearTextSelectionInside(dialogEl);
|
|
66472
|
+
// Closed by the navigation that puts another page on screen: it leaves
|
|
66473
|
+
// with that page, and plays no exit of its own (see page_change.js).
|
|
66474
|
+
const silent = Boolean(closeEvent.detail.silent);
|
|
66475
|
+
if (silent) {
|
|
66476
|
+
switchTransitionsOff(dialogEl);
|
|
66477
|
+
if (backdropEl) {
|
|
66478
|
+
backdropEl.style.transitionProperty = "none";
|
|
66479
|
+
}
|
|
66480
|
+
}
|
|
66107
66481
|
dialogEl.setAttribute("aria-expanded", "false");
|
|
66108
66482
|
if (!isTopLayer) {
|
|
66109
66483
|
openLocalDialogCount = Math.max(0, openLocalDialogCount - 1);
|
|
@@ -66143,7 +66517,7 @@ const useDialogProps = props => {
|
|
|
66143
66517
|
// next opening clears it (see openEffect's own unfreezeSize).
|
|
66144
66518
|
freezeSize(dialogEl);
|
|
66145
66519
|
cancelOpenInteractionSuppression?.();
|
|
66146
|
-
if (hasCssTransitionAnimation) {
|
|
66520
|
+
if (hasCssTransitionAnimation && !silent) {
|
|
66147
66521
|
suppressPointerEventsDuringTransition(dialogEl);
|
|
66148
66522
|
}
|
|
66149
66523
|
if (backdropEl) {
|
|
@@ -66154,6 +66528,15 @@ const useDialogProps = props => {
|
|
|
66154
66528
|
}
|
|
66155
66529
|
restoreFocus(closeEvent);
|
|
66156
66530
|
cleanup();
|
|
66531
|
+
if (silent) {
|
|
66532
|
+
// The closed frame committed with nothing transitioning, before
|
|
66533
|
+
// transitions come back for the next opening.
|
|
66534
|
+
dialogEl.getBoundingClientRect();
|
|
66535
|
+
switchTransitionsOn(dialogEl);
|
|
66536
|
+
if (backdropEl) {
|
|
66537
|
+
backdropEl.style.transitionProperty = "";
|
|
66538
|
+
}
|
|
66539
|
+
}
|
|
66157
66540
|
};
|
|
66158
66541
|
};
|
|
66159
66542
|
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
@@ -66396,6 +66779,19 @@ const useDialogProps = props => {
|
|
|
66396
66779
|
}
|
|
66397
66780
|
return [!isTopLayer && backdrop ? backdropProps : null, contentProps];
|
|
66398
66781
|
};
|
|
66782
|
+
|
|
66783
|
+
// The dialog's own transitions, off while a frame is pinned (the closed one an
|
|
66784
|
+
// entrance starts from, the open one a silent opening lands on, the closed one
|
|
66785
|
+
// a silent close leaves) and back on after it. A modal's wall is its native
|
|
66786
|
+
// ::backdrop, which takes no inline style: it reads the attribute instead.
|
|
66787
|
+
const switchTransitionsOff = dialogEl => {
|
|
66788
|
+
dialogEl.style.transitionProperty = "none";
|
|
66789
|
+
dialogEl.setAttribute("navi-transitions-off", "");
|
|
66790
|
+
};
|
|
66791
|
+
const switchTransitionsOn = dialogEl => {
|
|
66792
|
+
dialogEl.style.transitionProperty = "";
|
|
66793
|
+
dialogEl.removeAttribute("navi-transitions-off");
|
|
66794
|
+
};
|
|
66399
66795
|
const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
|
|
66400
66796
|
|
|
66401
66797
|
// Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
|
|
@@ -66846,6 +67242,12 @@ const css$D = /* css */`
|
|
|
66846
67242
|
* `saturate()`/`grayscale()` work too. How far what is behind withdraws is
|
|
66847
67243
|
* a question of paint, not of what an outside click does — a backdrop that
|
|
66848
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.
|
|
66849
67251
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
66850
67252
|
* popover so the page/container behind it can't scroll while it's open.
|
|
66851
67253
|
* @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
|
|
@@ -67188,6 +67590,9 @@ const usePopoverProps = props => {
|
|
|
67188
67590
|
// which is a sibling element (see this file's top comment).
|
|
67189
67591
|
backdropColor,
|
|
67190
67592
|
backdropFilter,
|
|
67593
|
+
// Read at the opening, with the backdrop it would otherwise be worked out
|
|
67594
|
+
// from (see openEffect).
|
|
67595
|
+
themeColor,
|
|
67191
67596
|
group,
|
|
67192
67597
|
scrollCapture,
|
|
67193
67598
|
focusCapture,
|
|
@@ -67320,8 +67725,10 @@ const usePopoverProps = props => {
|
|
|
67320
67725
|
// `open`/`defaultOpen` already being truthy at mount — there's nothing
|
|
67321
67726
|
// to visually transition away from (nothing was ever shown as "closed"
|
|
67322
67727
|
// to the user), so this open skips the animation entirely instead of
|
|
67323
|
-
// playing it against a closed frame that was never actually seen.
|
|
67324
|
-
//
|
|
67728
|
+
// playing it against a closed frame that was never actually seen. Set
|
|
67729
|
+
// too for an open the page changing carries: that page is its movement
|
|
67730
|
+
// (see page_change.js). See the final commit step below for how that's
|
|
67731
|
+
// done.
|
|
67325
67732
|
const silent = Boolean(e.detail.silent);
|
|
67326
67733
|
const [cleanup, addCleanup] = createPubSub(true);
|
|
67327
67734
|
|
|
@@ -67757,7 +68164,7 @@ const usePopoverProps = props => {
|
|
|
67757
68164
|
// focus in. Inlined rather than a standalone function since it only
|
|
67758
68165
|
// has this one call site.
|
|
67759
68166
|
//
|
|
67760
|
-
// `silent` (mounting already open
|
|
68167
|
+
// `silent` (mounting already open, or carried by a page change) flips the
|
|
67761
68168
|
// order instead: aria-expanded first, *then* re-enable transitions, with
|
|
67762
68169
|
// its own forced reflow in between the two — without that reflow, the
|
|
67763
68170
|
// browser coalesces "flip aria-expanded" and "re-enable transitions"
|
|
@@ -67790,6 +68197,15 @@ const usePopoverProps = props => {
|
|
|
67790
68197
|
backdropEl.style.transitionProperty = "";
|
|
67791
68198
|
}
|
|
67792
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
|
+
}
|
|
67793
68209
|
const cancelOpenInteractionSuppression = !silent && hasCssTransitionAnimation ? suppressPointerEventsDuringTransition(popoverEl) : null;
|
|
67794
68210
|
// autoFocus={false}: opening moves no focus at all — whoever has the
|
|
67795
68211
|
// keyboard keeps it (combobox-style suggestions under an input being
|
|
@@ -67803,6 +68219,15 @@ const usePopoverProps = props => {
|
|
|
67803
68219
|
// reason: only ever built here.
|
|
67804
68220
|
return closeEvent => {
|
|
67805
68221
|
clearTextSelectionInside(popoverEl);
|
|
68222
|
+
// Closed by the navigation that puts another page on screen: it leaves
|
|
68223
|
+
// with that page, and plays no exit of its own (see page_change.js).
|
|
68224
|
+
const silent = Boolean(closeEvent.detail.silent);
|
|
68225
|
+
if (silent) {
|
|
68226
|
+
popoverEl.style.transitionProperty = "none";
|
|
68227
|
+
if (backdropEl) {
|
|
68228
|
+
backdropEl.style.transitionProperty = "none";
|
|
68229
|
+
}
|
|
68230
|
+
}
|
|
67806
68231
|
popoverEl.setAttribute("aria-expanded", "false");
|
|
67807
68232
|
// Set regardless of isTopLayer — see the open side's own identical
|
|
67808
68233
|
// comment (openEffect above) for why hidePopover() alone isn't
|
|
@@ -67829,7 +68254,7 @@ const usePopoverProps = props => {
|
|
|
67829
68254
|
// open is its own separate call with no way to reach back into this
|
|
67830
68255
|
// one).
|
|
67831
68256
|
cancelOpenInteractionSuppression?.();
|
|
67832
|
-
if (hasCssTransitionAnimation) {
|
|
68257
|
+
if (hasCssTransitionAnimation && !silent) {
|
|
67833
68258
|
suppressPointerEventsDuringTransition(popoverEl);
|
|
67834
68259
|
}
|
|
67835
68260
|
if (backdropEl) {
|
|
@@ -67864,6 +68289,15 @@ const usePopoverProps = props => {
|
|
|
67864
68289
|
popoverEl.removeAttribute("data-position-y-current");
|
|
67865
68290
|
popoverEl.removeAttribute("data-position-x-current");
|
|
67866
68291
|
cleanup();
|
|
68292
|
+
if (silent) {
|
|
68293
|
+
// The closed frame committed with nothing transitioning, before
|
|
68294
|
+
// transitions come back for the next opening.
|
|
68295
|
+
popoverEl.getBoundingClientRect();
|
|
68296
|
+
popoverEl.style.transitionProperty = "";
|
|
68297
|
+
if (backdropEl) {
|
|
68298
|
+
backdropEl.style.transitionProperty = "";
|
|
68299
|
+
}
|
|
68300
|
+
}
|
|
67867
68301
|
};
|
|
67868
68302
|
};
|
|
67869
68303
|
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
@@ -68187,6 +68621,10 @@ const css$C = /* css */`@layer navi {
|
|
|
68187
68621
|
* identically): the wash the backdrop paints over what is behind.
|
|
68188
68622
|
* @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
|
|
68189
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.
|
|
68190
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]
|
|
68191
68629
|
* - Forwarded to whichever renders — see either component's own doc.
|
|
68192
68630
|
* `"expanding"`/`"expand-*"` are `Popover`'s own; `"lifting"` and
|
|
@@ -69184,6 +69622,26 @@ const PickerCustom = props => {
|
|
|
69184
69622
|
if (isWithinPickerContent(e.target)) {
|
|
69185
69623
|
return null;
|
|
69186
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
|
+
}
|
|
69187
69645
|
if (!opensOnPress) {
|
|
69188
69646
|
// Neither the tap, nor the click a hold leaves behind: the press
|
|
69189
69647
|
// is not what opens this picker.
|
|
@@ -69839,12 +70297,12 @@ const isScrolling = (element) => {
|
|
|
69839
70297
|
if (element === undefined) {
|
|
69840
70298
|
return scrollActivitySignal.peek();
|
|
69841
70299
|
}
|
|
69842
|
-
return asScroller(element).hasAttribute("navi-scrolling");
|
|
70300
|
+
return asScroller$1(element).hasAttribute("navi-scrolling");
|
|
69843
70301
|
};
|
|
69844
70302
|
|
|
69845
70303
|
// The page scroll is dispatched on the document; the element carrying the
|
|
69846
70304
|
// attribute is the one CSS can reach, document.scrollingElement.
|
|
69847
|
-
const asScroller = (eventTargetOrElement) => {
|
|
70305
|
+
const asScroller$1 = (eventTargetOrElement) => {
|
|
69848
70306
|
if (eventTargetOrElement === document || eventTargetOrElement === window) {
|
|
69849
70307
|
return document.scrollingElement;
|
|
69850
70308
|
}
|
|
@@ -69855,7 +70313,7 @@ const idleTimeoutMap = new Map();
|
|
|
69855
70313
|
document.addEventListener(
|
|
69856
70314
|
"scroll",
|
|
69857
70315
|
(e) => {
|
|
69858
|
-
const scroller = asScroller(e.target);
|
|
70316
|
+
const scroller = asScroller$1(e.target);
|
|
69859
70317
|
const idleTimeout = idleTimeoutMap.get(scroller);
|
|
69860
70318
|
if (idleTimeout !== undefined) {
|
|
69861
70319
|
clearTimeout(idleTimeout);
|
|
@@ -69877,6 +70335,111 @@ document.addEventListener(
|
|
|
69877
70335
|
{ capture: true, passive: true },
|
|
69878
70336
|
);
|
|
69879
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
|
+
|
|
69880
70443
|
const LoadingDotsSvg = () => {
|
|
69881
70444
|
return jsxs("svg", {
|
|
69882
70445
|
viewBox: "0 0 200 200",
|
|
@@ -72960,7 +73523,9 @@ const useListScrollSync = ({
|
|
|
72960
73523
|
const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
|
|
72961
73524
|
const startPlaceRef = useRef({
|
|
72962
73525
|
userTookOver: false,
|
|
72963
|
-
wanted: scrolledWanted
|
|
73526
|
+
wanted: scrolledWanted,
|
|
73527
|
+
way: null,
|
|
73528
|
+
arrived: false
|
|
72964
73529
|
});
|
|
72965
73530
|
if (startPlaceRef.current.wanted !== scrolledWanted) {
|
|
72966
73531
|
startPlaceRef.current.wanted = scrolledWanted;
|
|
@@ -73170,8 +73735,10 @@ const useListScrollSync = ({
|
|
|
73170
73735
|
// keeps moving while the list is still finding out how many items it has and
|
|
73171
73736
|
// how tall one is, so landing there once would land next to it. What ends the
|
|
73172
73737
|
// hold is the user reaching for the list — a wheel, a finger, a key, a hand
|
|
73173
|
-
// on the scrollbar —
|
|
73174
|
-
// 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).
|
|
73175
73742
|
const placeWhereHeld = () => {
|
|
73176
73743
|
if (scrolledWanted === "start" || scrolledWanted === undefined || startPlaceRef.current.userTookOver || !ref.current) {
|
|
73177
73744
|
return;
|
|
@@ -73220,12 +73787,7 @@ const useListScrollSync = ({
|
|
|
73220
73787
|
}
|
|
73221
73788
|
const scrollerEl = getScroller();
|
|
73222
73789
|
if (openAt === "end") {
|
|
73223
|
-
|
|
73224
|
-
if (horizontal) {
|
|
73225
|
-
scrollerEl.scrollLeft = scrollerEl.scrollWidth;
|
|
73226
|
-
} else {
|
|
73227
|
-
scrollerEl.scrollTop = scrollerEl.scrollHeight;
|
|
73228
|
-
}
|
|
73790
|
+
sendHeldScroll(horizontal ? scrollerEl.scrollWidth : scrollerEl.scrollHeight);
|
|
73229
73791
|
return;
|
|
73230
73792
|
}
|
|
73231
73793
|
if (typeof openAt === "number") {
|
|
@@ -73249,15 +73811,45 @@ const useListScrollSync = ({
|
|
|
73249
73811
|
const itemRect = itemEl.getBoundingClientRect();
|
|
73250
73812
|
const offsetWanted = resolveOpenOffset(getItemScrollInset(scrollerEl, itemEl, horizontal) + offset, horizontal ? viewportRect.width : viewportRect.height, horizontal ? itemRect.width : itemRect.height);
|
|
73251
73813
|
const offsetNow = horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top;
|
|
73814
|
+
const scrollNow = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
73252
73815
|
const delta = offsetNow - offsetWanted;
|
|
73253
73816
|
if (delta > -0.5 && delta < 0.5) {
|
|
73817
|
+
sendHeldScroll(scrollNow);
|
|
73254
73818
|
return;
|
|
73255
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
|
+
}
|
|
73256
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
|
+
}
|
|
73257
73849
|
if (horizontal) {
|
|
73258
|
-
scrollerEl.scrollLeft
|
|
73850
|
+
scrollerEl.scrollLeft = to;
|
|
73259
73851
|
} else {
|
|
73260
|
-
scrollerEl.scrollTop
|
|
73852
|
+
scrollerEl.scrollTop = to;
|
|
73261
73853
|
}
|
|
73262
73854
|
};
|
|
73263
73855
|
useLayoutEffect(placeWhereHeld);
|
|
@@ -73330,6 +73922,19 @@ const useListScrollSync = ({
|
|
|
73330
73922
|
evaluateWindowRef.current("items resized");
|
|
73331
73923
|
return false;
|
|
73332
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
|
+
};
|
|
73333
73938
|
useLayoutEffect(() => {
|
|
73334
73939
|
if (scrolledWanted === "start" || scrolledWanted === undefined || !ref.current) {
|
|
73335
73940
|
return undefined;
|
|
@@ -73474,6 +74079,9 @@ const useListScrollSync = ({
|
|
|
73474
74079
|
if (!entries.some(entry => entry.target === scrollerEl)) {
|
|
73475
74080
|
return;
|
|
73476
74081
|
}
|
|
74082
|
+
if (placeIfHeldRef.current()) {
|
|
74083
|
+
return;
|
|
74084
|
+
}
|
|
73477
74085
|
const position = positionRef.current;
|
|
73478
74086
|
if (!position) {
|
|
73479
74087
|
return;
|
|
@@ -73586,6 +74194,15 @@ const useListScrollSync = ({
|
|
|
73586
74194
|
return;
|
|
73587
74195
|
}
|
|
73588
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
|
+
}
|
|
73589
74206
|
scrolledByListRef.current = true;
|
|
73590
74207
|
if (horizontal) {
|
|
73591
74208
|
scrollerEl.scrollLeft += drift;
|
|
@@ -73913,9 +74530,44 @@ const useListScrollSync = ({
|
|
|
73913
74530
|
reportPosition();
|
|
73914
74531
|
return;
|
|
73915
74532
|
}
|
|
74533
|
+
letGoUnlessHeldScroll();
|
|
73916
74534
|
reportPosition();
|
|
73917
74535
|
evaluateWindowRef.current("scroll");
|
|
73918
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
|
+
};
|
|
73919
74571
|
// A page-level scroller does not emit "scroll" on the element itself
|
|
73920
74572
|
// (document.scrollingElement); the document does.
|
|
73921
74573
|
const scrollEventTarget = scrollerEl === document.scrollingElement ? document : scrollerEl;
|
|
@@ -77304,7 +77956,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
77304
77956
|
*
|
|
77305
77957
|
* In every form the list holds itself there while it is still finding out
|
|
77306
77958
|
* how many items there are and how tall one is, and lets go the moment the
|
|
77307
|
-
* 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).
|
|
77308
77961
|
* @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
|
|
77309
77962
|
* Where the list is, as the user scrolls: the item at the top of the view and
|
|
77310
77963
|
* how far below the place an item lands on its own (see `defaultScrolled`) it
|