@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/dev/jsenv_navi.js
CHANGED
|
@@ -6,7 +6,7 @@ import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, win
|
|
|
6
6
|
import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, createEventGroupLogger, 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, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent, subscribeVirtualKeyboardGeometryChange } 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, prefixFirstAndIndentRemainingLines, 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";
|
|
@@ -7107,6 +7107,55 @@ const applyNavigationToNavDepth = (navigationType, state) => {
|
|
|
7107
7107
|
canNavForwardSignal.value = navDepth < navDepthMax;
|
|
7108
7108
|
};
|
|
7109
7109
|
|
|
7110
|
+
/**
|
|
7111
|
+
* Whether the render running now is the one putting a new page on screen.
|
|
7112
|
+
*
|
|
7113
|
+
* A layer drawn over the pages — a popup bound to the address, the slides
|
|
7114
|
+
* inside it — can open, close or change place in the very navigation that
|
|
7115
|
+
* replaces the page beneath it: a link inside the layer leads to another page
|
|
7116
|
+
* and the layer's weak params are dropped, a back to the page it was open over
|
|
7117
|
+
* brings them back. For the layer that is not a movement of its own: the page
|
|
7118
|
+
* changing IS the movement. A route transition already carries it, photographed
|
|
7119
|
+
* as furniture with the page it belongs to (transition_furniture.js), and with
|
|
7120
|
+
* no transition the page cuts and the layer cuts with it. Played on its own as
|
|
7121
|
+
* well, it moves twice — an entrance or an exit over a page sliding past, or
|
|
7122
|
+
* appearing at once.
|
|
7123
|
+
*
|
|
7124
|
+
* So whatever reacts to such a change in a render — a popup's open state, a
|
|
7125
|
+
* slide container's current slide — asks here whether that render carries a
|
|
7126
|
+
* page change, and if so arrives where it goes without travelling there.
|
|
7127
|
+
*
|
|
7128
|
+
* A page is what navi animates between (route_page.js): a route, and the
|
|
7129
|
+
* params telling apart branches of one route. Which routes match is decided by
|
|
7130
|
+
* the path alone, so a navigation that changes the path changes the page; one
|
|
7131
|
+
* that keeps it changes the page only where a param IS a page — and navi knows
|
|
7132
|
+
* those only through the relation or the RouteTravel row that names them, so
|
|
7133
|
+
* that is who says so. A search param a page merely reads is not one: writing
|
|
7134
|
+
* it is the page answering, and a layer opened by it (the `?settings` its door
|
|
7135
|
+
* pushes, the back button taking it out) moves on its own.
|
|
7136
|
+
*
|
|
7137
|
+
* The answer lasts until the render applying the navigation is over: the held
|
|
7138
|
+
* one when somebody photographs the change, Preact's own otherwise, over by the
|
|
7139
|
+
* next frame (see whenPageRendered). Asked from a layout effect it is exact; an
|
|
7140
|
+
* opening deferred past the commit takes the answer with it, read when the
|
|
7141
|
+
* effect ran.
|
|
7142
|
+
*/
|
|
7143
|
+
|
|
7144
|
+
|
|
7145
|
+
let renderingPageChange = false;
|
|
7146
|
+
|
|
7147
|
+
const notePageChange = () => {
|
|
7148
|
+
if (renderingPageChange) {
|
|
7149
|
+
return;
|
|
7150
|
+
}
|
|
7151
|
+
renderingPageChange = true;
|
|
7152
|
+
whenPageRendered(() => {
|
|
7153
|
+
renderingPageChange = false;
|
|
7154
|
+
});
|
|
7155
|
+
};
|
|
7156
|
+
|
|
7157
|
+
const isRenderingPageChange = () => renderingPageChange;
|
|
7158
|
+
|
|
7110
7159
|
// Global signal registry for route template detection
|
|
7111
7160
|
const globalSignalRegistry = new Map();
|
|
7112
7161
|
|
|
@@ -10742,8 +10791,12 @@ This prevents cross-test pollution and ensures clean state.`,
|
|
|
10742
10791
|
// read again.
|
|
10743
10792
|
const path = urlWithoutSearchAndHash(url);
|
|
10744
10793
|
const pathKept = path === previousPath;
|
|
10794
|
+
// A new path is a new page: what the integration tells whatever reacts to
|
|
10795
|
+
// this change in the render it causes (see page_change.js). Not the first
|
|
10796
|
+
// one — there was no page before it, and what it mounts mounts as it is.
|
|
10797
|
+
const pageChanged = previousPath !== null && !pathKept;
|
|
10745
10798
|
previousPath = path;
|
|
10746
|
-
const returnValue = {};
|
|
10799
|
+
const returnValue = { pageChanged };
|
|
10747
10800
|
const routeMatchInfoSet = new Set();
|
|
10748
10801
|
for (const route of routeSet) {
|
|
10749
10802
|
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
@@ -11616,13 +11669,22 @@ const installScrollRestoration = () => {
|
|
|
11616
11669
|
},
|
|
11617
11670
|
{ passive: true },
|
|
11618
11671
|
);
|
|
11672
|
+
const positionOnLoad = positionByUrl.get(window.location.href);
|
|
11673
|
+
if (!positionOnLoad) {
|
|
11674
|
+
// Where the browser landed the first page, written down for the reason
|
|
11675
|
+
// startAtTop writes its top: no scroll event says so.
|
|
11676
|
+
positionByUrl.set(window.location.href, {
|
|
11677
|
+
x: window.scrollX,
|
|
11678
|
+
y: window.scrollY,
|
|
11679
|
+
});
|
|
11680
|
+
return;
|
|
11681
|
+
}
|
|
11619
11682
|
// What a reload asks for, now that the browser has been told not to do it.
|
|
11620
11683
|
// Once, and at the first render of a route: the position is only meaningful
|
|
11621
11684
|
// once there is a page under it.
|
|
11622
11685
|
// Written once the commit that rendered it has ended: said from inside it,
|
|
11623
11686
|
// and what the offset shows may still have to be built (see scrollTo).
|
|
11624
|
-
|
|
11625
|
-
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
11687
|
+
if (positionOnLoad.x || positionOnLoad.y) {
|
|
11626
11688
|
const stopListening = observeRouteRender(() => {
|
|
11627
11689
|
stopListening();
|
|
11628
11690
|
queueMicrotask(() => {
|
|
@@ -11708,6 +11770,11 @@ const startAtTop = (url, { from } = {}) => {
|
|
|
11708
11770
|
return;
|
|
11709
11771
|
}
|
|
11710
11772
|
arrivalCount++;
|
|
11773
|
+
// Written down as well as done: a document already at its top fires no
|
|
11774
|
+
// scroll event, and a page left without ever being scrolled would have no
|
|
11775
|
+
// position to come back to — the return would keep the offset of the page
|
|
11776
|
+
// being left.
|
|
11777
|
+
positionByUrl.set(new URL(url, window.location.href).href, { x: 0, y: 0 });
|
|
11711
11778
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
11712
11779
|
};
|
|
11713
11780
|
const isArrival = (url, { from }) => {
|
|
@@ -12629,11 +12696,14 @@ const applyRouting = (
|
|
|
12629
12696
|
// The routes' actions follow the address by themselves (see routeAction):
|
|
12630
12697
|
// what a navigation leaves to the routing is a reload, the one that asks
|
|
12631
12698
|
// them again with the address unchanged.
|
|
12632
|
-
const { reloadSet } = updateRoutes(url, {
|
|
12699
|
+
const { pageChanged, reloadSet } = updateRoutes(url, {
|
|
12633
12700
|
navigationType,
|
|
12634
12701
|
isVisited,
|
|
12635
12702
|
// state,
|
|
12636
12703
|
});
|
|
12704
|
+
if (pageChanged) {
|
|
12705
|
+
notePageChange();
|
|
12706
|
+
}
|
|
12637
12707
|
if (!reloadSet || reloadSet.size === 0) {
|
|
12638
12708
|
return {
|
|
12639
12709
|
allResult: undefined,
|
|
@@ -16104,8 +16174,8 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
16104
16174
|
// Tracks whether the effect below has ever run before — only the very
|
|
16105
16175
|
// first run gets the "mount already open" treatment (`open` truthy from
|
|
16106
16176
|
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
16107
|
-
// subsequent `open` change is a real, later toggle and
|
|
16108
|
-
//
|
|
16177
|
+
// subsequent `open` change is a real, later toggle and animates like any
|
|
16178
|
+
// interactive open/close — unless a page change carries it (see below).
|
|
16109
16179
|
const isFirstRunRef = useRef(true);
|
|
16110
16180
|
// What `open` was on the previous run of the effect below. preact re-runs
|
|
16111
16181
|
// an effect for a change of its deps — or for none at all, when a
|
|
@@ -16125,6 +16195,12 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
16125
16195
|
isFirstRunRef.current = false;
|
|
16126
16196
|
const openChanged = open !== lastRunOpenRef.current;
|
|
16127
16197
|
lastRunOpenRef.current = open;
|
|
16198
|
+
// Opened or closed by the navigation that puts another page on screen: a
|
|
16199
|
+
// link inside the popup leading away, a back to the page it stood over.
|
|
16200
|
+
// The page changing is its movement, so it has none of its own — a route
|
|
16201
|
+
// transition carries it with its page, and without one it cuts with the
|
|
16202
|
+
// page (see page_change.js).
|
|
16203
|
+
const carriedByPageChange = isRenderingPageChange();
|
|
16128
16204
|
|
|
16129
16205
|
if (isFirstRun) {
|
|
16130
16206
|
// "interaction" is said even over what the caller holds reading open: a
|
|
@@ -16143,10 +16219,12 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
16143
16219
|
// other truthy value means it was simply already open: nothing was ever
|
|
16144
16220
|
// shown as "closed" for the user to see it transition away from, so the
|
|
16145
16221
|
// entrance is skipped (`silent`, see popover.jsx's own openEffect).
|
|
16146
|
-
//
|
|
16222
|
+
// A popup built by an opening a page change carries is silent too,
|
|
16223
|
+
// whatever built it: read now, the answer only holds for this render.
|
|
16224
|
+
const silent = mountOpenReason !== "interaction" || carriedByPageChange;
|
|
16147
16225
|
mountOpenOwedRef.current = () =>
|
|
16148
16226
|
openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
|
|
16149
|
-
silent
|
|
16227
|
+
silent,
|
|
16150
16228
|
});
|
|
16151
16229
|
}
|
|
16152
16230
|
return undefined;
|
|
@@ -16178,11 +16256,13 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
16178
16256
|
});
|
|
16179
16257
|
return undefined;
|
|
16180
16258
|
}
|
|
16181
|
-
openController.open(new CustomEvent("open_by_prop", { detail: {} })
|
|
16259
|
+
openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
|
|
16260
|
+
silent: carriedByPageChange,
|
|
16261
|
+
});
|
|
16182
16262
|
} else {
|
|
16183
16263
|
openController.requestClose(
|
|
16184
16264
|
new CustomEvent("close_by_prop", { detail: {} }),
|
|
16185
|
-
{ isCancel: true },
|
|
16265
|
+
{ isCancel: true, silent: carriedByPageChange },
|
|
16186
16266
|
);
|
|
16187
16267
|
}
|
|
16188
16268
|
// The request can be refused (a busy form denying the close): the popup
|
|
@@ -16923,7 +17003,7 @@ const renderIntoCallout = (jsx$1, calloutMessageElement, {
|
|
|
16923
17003
|
},
|
|
16924
17004
|
children: jsx$1
|
|
16925
17005
|
});
|
|
16926
|
-
render(calloutJsx, calloutMessageElement);
|
|
17006
|
+
render$1(calloutJsx, calloutMessageElement);
|
|
16927
17007
|
};
|
|
16928
17008
|
|
|
16929
17009
|
// An HTML message is rendered through preact rather than innerHTML: what a
|
|
@@ -16946,7 +17026,7 @@ const renderHtmlIntoCallout = (html, calloutMessageElement, {
|
|
|
16946
17026
|
// (a DOM node, an iframe); innerHTML alone would leave preact believing its
|
|
16947
17027
|
// tree is still there.
|
|
16948
17028
|
const clearCalloutMessage = calloutMessageElement => {
|
|
16949
|
-
render(null, calloutMessageElement);
|
|
17029
|
+
render$1(null, calloutMessageElement);
|
|
16950
17030
|
calloutMessageElement.innerHTML = "";
|
|
16951
17031
|
};
|
|
16952
17032
|
const domToVNodes = node => {
|
|
@@ -33144,10 +33224,222 @@ const useUrlSearchParam = (paramName, defaultValue) => {
|
|
|
33144
33224
|
return [value, setSearchParamValue];
|
|
33145
33225
|
};
|
|
33146
33226
|
|
|
33227
|
+
/**
|
|
33228
|
+
* The colour the browser paints around the page — Chrome's address bar on
|
|
33229
|
+
* Android, the status bar of an installed app — read from
|
|
33230
|
+
* `<meta name="theme-color">`. It sits right above the page's top edge, so it
|
|
33231
|
+
* reads as part of whatever is drawn there, and two layers decide it:
|
|
33232
|
+
*
|
|
33233
|
+
* - What the page asks for: the theme-color metas a `<Head>` brings. The last
|
|
33234
|
+
* Head to arrive wins, whatever order they leave in — the title's rule, for
|
|
33235
|
+
* the title's reason (see head.jsx).
|
|
33236
|
+
* - What the top layer lays over that edge: an open popup paints its backdrop
|
|
33237
|
+
* over what is under it, and its own surface too when it is flush with the
|
|
33238
|
+
* top edge. Popups sit above every Head, in opening order, and are painted
|
|
33239
|
+
* again from what is under them each time it changes — a page changing
|
|
33240
|
+
* under an open popup is dimmed by it all the same.
|
|
33241
|
+
*
|
|
33242
|
+
* The browser reads the first theme-color meta whose `media` matches, in tree
|
|
33243
|
+
* order: a Head appending its meta after an earlier one would never be read.
|
|
33244
|
+
* So navi writes the result into metas of its own placed before any other, and
|
|
33245
|
+
* a meta written in the HTML is the bottom of the stack. A popup paints over
|
|
33246
|
+
* every meta of the list, `media` kept, so whichever one the browser picks is
|
|
33247
|
+
* the one it would have picked, painted over.
|
|
33248
|
+
*/
|
|
33249
|
+
|
|
33250
|
+
const headEntries = [];
|
|
33251
|
+
const popupLayers = [];
|
|
33252
|
+
const ownMetas = [];
|
|
33253
|
+
|
|
33254
|
+
/**
|
|
33255
|
+
* @param {Array<{ content: string, media: string|null }>} themeColors
|
|
33256
|
+
* @returns {() => void} Takes them away.
|
|
33257
|
+
*/
|
|
33258
|
+
const pushHeadThemeColors = (themeColors) => {
|
|
33259
|
+
headEntries.push(themeColors);
|
|
33260
|
+
queueRender();
|
|
33261
|
+
return () => {
|
|
33262
|
+
headEntries.splice(headEntries.indexOf(themeColors), 1);
|
|
33263
|
+
queueRender();
|
|
33264
|
+
};
|
|
33265
|
+
};
|
|
33266
|
+
|
|
33267
|
+
/**
|
|
33268
|
+
* @param {object} paint
|
|
33269
|
+
* @param {string} [paint.themeColor] - Forces the colour, nothing is painted.
|
|
33270
|
+
* @param {string|null} [paint.backdropColor] - Laid over the page's colour.
|
|
33271
|
+
* @param {string|null} [paint.surfaceColor] - Laid over that, for a popup
|
|
33272
|
+
* flush with the top edge.
|
|
33273
|
+
* @returns {() => void} Takes the layer away.
|
|
33274
|
+
*/
|
|
33275
|
+
const pushPopupThemeColor = ({
|
|
33276
|
+
themeColor,
|
|
33277
|
+
backdropColor,
|
|
33278
|
+
surfaceColor,
|
|
33279
|
+
}) => {
|
|
33280
|
+
let layer;
|
|
33281
|
+
if (themeColor) {
|
|
33282
|
+
layer = { color: themeColor };
|
|
33283
|
+
} else {
|
|
33284
|
+
const paints = [];
|
|
33285
|
+
for (const color of [backdropColor, surfaceColor]) {
|
|
33286
|
+
if (!color) {
|
|
33287
|
+
continue;
|
|
33288
|
+
}
|
|
33289
|
+
const rgba = parseColor(color);
|
|
33290
|
+
if (rgba[3] > 0) {
|
|
33291
|
+
paints.push(rgba);
|
|
33292
|
+
}
|
|
33293
|
+
}
|
|
33294
|
+
// Nothing laid over the edge: the colour under it stays, as it is.
|
|
33295
|
+
if (paints.length === 0) {
|
|
33296
|
+
return () => {};
|
|
33297
|
+
}
|
|
33298
|
+
layer = { paints };
|
|
33299
|
+
}
|
|
33300
|
+
popupLayers.push(layer);
|
|
33301
|
+
queueRender();
|
|
33302
|
+
return () => {
|
|
33303
|
+
popupLayers.splice(popupLayers.indexOf(layer), 1);
|
|
33304
|
+
queueRender();
|
|
33305
|
+
};
|
|
33306
|
+
};
|
|
33307
|
+
|
|
33308
|
+
let renderQueued = false;
|
|
33309
|
+
// Once per commit: a Head re-rendering leaves and arrives again in the same
|
|
33310
|
+
// commit, and the browser must not be handed the colour from in between.
|
|
33311
|
+
const queueRender = () => {
|
|
33312
|
+
if (renderQueued) {
|
|
33313
|
+
return;
|
|
33314
|
+
}
|
|
33315
|
+
renderQueued = true;
|
|
33316
|
+
queueMicrotask(render);
|
|
33317
|
+
};
|
|
33318
|
+
|
|
33319
|
+
const render = () => {
|
|
33320
|
+
renderQueued = false;
|
|
33321
|
+
const htmlMetas = Array.from(
|
|
33322
|
+
document.head.querySelectorAll('meta[name="theme-color"]'),
|
|
33323
|
+
).filter((meta) => !ownMetas.includes(meta));
|
|
33324
|
+
const htmlColors = htmlMetas.map((meta) => ({
|
|
33325
|
+
content: meta.getAttribute("content"),
|
|
33326
|
+
media: meta.getAttribute("media"),
|
|
33327
|
+
}));
|
|
33328
|
+
let colors = [];
|
|
33329
|
+
for (let i = headEntries.length - 1; i >= 0; i--) {
|
|
33330
|
+
colors.push(...headEntries[i]);
|
|
33331
|
+
}
|
|
33332
|
+
colors.push(...htmlColors);
|
|
33333
|
+
for (const layer of popupLayers) {
|
|
33334
|
+
colors = paintLayer(layer, colors);
|
|
33335
|
+
}
|
|
33336
|
+
// What the HTML metas say reaches the browser without a copy.
|
|
33337
|
+
while (colors.length > 0 && htmlColors.includes(colors[colors.length - 1])) {
|
|
33338
|
+
colors.pop();
|
|
33339
|
+
}
|
|
33340
|
+
|
|
33341
|
+
while (ownMetas.length > colors.length) {
|
|
33342
|
+
ownMetas.pop().remove();
|
|
33343
|
+
}
|
|
33344
|
+
colors.forEach(({ content, media }, index) => {
|
|
33345
|
+
let meta = ownMetas[index];
|
|
33346
|
+
if (!meta) {
|
|
33347
|
+
meta = document.createElement("meta");
|
|
33348
|
+
meta.setAttribute("name", "theme-color");
|
|
33349
|
+
const previousMeta = ownMetas[index - 1];
|
|
33350
|
+
if (previousMeta) {
|
|
33351
|
+
previousMeta.after(meta);
|
|
33352
|
+
} else if (htmlMetas.length > 0) {
|
|
33353
|
+
htmlMetas[0].before(meta);
|
|
33354
|
+
} else {
|
|
33355
|
+
document.head.append(meta);
|
|
33356
|
+
}
|
|
33357
|
+
ownMetas.push(meta);
|
|
33358
|
+
}
|
|
33359
|
+
if (meta.getAttribute("content") !== content) {
|
|
33360
|
+
meta.setAttribute("content", content);
|
|
33361
|
+
}
|
|
33362
|
+
if (meta.getAttribute("media") !== media) {
|
|
33363
|
+
if (media === null) {
|
|
33364
|
+
meta.removeAttribute("media");
|
|
33365
|
+
} else {
|
|
33366
|
+
meta.setAttribute("media", media);
|
|
33367
|
+
}
|
|
33368
|
+
}
|
|
33369
|
+
});
|
|
33370
|
+
};
|
|
33371
|
+
|
|
33372
|
+
const paintLayer = (layer, colors) => {
|
|
33373
|
+
if (layer.color) {
|
|
33374
|
+
return [{ content: layer.color, media: null }];
|
|
33375
|
+
}
|
|
33376
|
+
const underColors =
|
|
33377
|
+
colors.length > 0 ? colors : [{ content: null, media: null }];
|
|
33378
|
+
const painted = [];
|
|
33379
|
+
for (const { content, media } of underColors) {
|
|
33380
|
+
let rgba = content === null ? null : parseUnderColor(content);
|
|
33381
|
+
for (const paint of layer.paints) {
|
|
33382
|
+
rgba = rgba ? composite(paint, rgba) : paint;
|
|
33383
|
+
}
|
|
33384
|
+
// Over no colour at all the browser keeps one of its own, which a
|
|
33385
|
+
// translucent paint cannot be laid over.
|
|
33386
|
+
if (Math.round(rgba[3] * 255) === 255) {
|
|
33387
|
+
painted.push({ content: toHex(rgba), media });
|
|
33388
|
+
}
|
|
33389
|
+
}
|
|
33390
|
+
return painted;
|
|
33391
|
+
};
|
|
33392
|
+
|
|
33393
|
+
// The browser shows a theme-color opaque, whatever alpha it was written with.
|
|
33394
|
+
const parseUnderColor = (color) => {
|
|
33395
|
+
const rgba = parseColor(color);
|
|
33396
|
+
if (rgba[3] === 0) {
|
|
33397
|
+
return null;
|
|
33398
|
+
}
|
|
33399
|
+
return [rgba[0], rgba[1], rgba[2], 1];
|
|
33400
|
+
};
|
|
33401
|
+
|
|
33402
|
+
// Source-over, the way the backdrop is drawn over the page.
|
|
33403
|
+
const composite = (over, under) => {
|
|
33404
|
+
const [r1, g1, b1, a1] = over;
|
|
33405
|
+
const [r2, g2, b2, a2] = under;
|
|
33406
|
+
const alpha = a1 + a2 * (1 - a1);
|
|
33407
|
+
const mix = (c1, c2) => (c1 * a1 + c2 * a2 * (1 - a1)) / alpha;
|
|
33408
|
+
return [mix(r1, r2), mix(g1, g2), mix(b1, b2), alpha];
|
|
33409
|
+
};
|
|
33410
|
+
|
|
33411
|
+
const toHex = ([r, g, b]) =>
|
|
33412
|
+
`#${[r, g, b]
|
|
33413
|
+
.map((channel) => Math.round(channel).toString(16).padStart(2, "0"))
|
|
33414
|
+
.join("")}`;
|
|
33415
|
+
|
|
33416
|
+
// A canvas reads every colour syntax the browser knows — oklch(), color(),
|
|
33417
|
+
// a keyword — and hands back sRGB bytes, which is what a theme-color meta is
|
|
33418
|
+
// sure to be understood in, whatever getComputedStyle answered with.
|
|
33419
|
+
let colorContext = null;
|
|
33420
|
+
const parseColor = (color) => {
|
|
33421
|
+
if (!colorContext) {
|
|
33422
|
+
const canvas = document.createElement("canvas");
|
|
33423
|
+
canvas.width = 1;
|
|
33424
|
+
canvas.height = 1;
|
|
33425
|
+
colorContext = canvas.getContext("2d", { willReadFrequently: true });
|
|
33426
|
+
}
|
|
33427
|
+
colorContext.clearRect(0, 0, 1, 1);
|
|
33428
|
+
// fillStyle ignores a colour it cannot parse: reset first, so that one draws
|
|
33429
|
+
// nothing rather than the previous colour.
|
|
33430
|
+
colorContext.fillStyle = "transparent";
|
|
33431
|
+
colorContext.fillStyle = color;
|
|
33432
|
+
colorContext.fillRect(0, 0, 1, 1);
|
|
33433
|
+
const [r, g, b, a] = colorContext.getImageData(0, 0, 1, 1).data;
|
|
33434
|
+
return [r, g, b, a / 255];
|
|
33435
|
+
};
|
|
33436
|
+
|
|
33147
33437
|
// The title belongs to the last Head to arrive, whatever order they leave in:
|
|
33148
33438
|
// the page being left by a route transition is taken down AFTER the page
|
|
33149
33439
|
// arriving is up (see keepLeavingPages in route.jsx), and putting back the
|
|
33150
|
-
// title it found would put its own page's title over the new one.
|
|
33440
|
+
// title it found would put its own page's title over the new one. A
|
|
33441
|
+
// theme-color meta follows the same rule, kept by theme_color.js, where the
|
|
33442
|
+
// popups of the top layer paint over it.
|
|
33151
33443
|
const titleEntries = [];
|
|
33152
33444
|
let titleWithoutHead = null;
|
|
33153
33445
|
const Head = ({
|
|
@@ -33159,6 +33451,7 @@ const Head = ({
|
|
|
33159
33451
|
}
|
|
33160
33452
|
const childArray = Array.isArray(children) ? children : [children];
|
|
33161
33453
|
const appendedElements = [];
|
|
33454
|
+
const themeColors = [];
|
|
33162
33455
|
let titleEntry = null;
|
|
33163
33456
|
for (const child of childArray) {
|
|
33164
33457
|
if (!child) {
|
|
@@ -33171,6 +33464,13 @@ const Head = ({
|
|
|
33171
33464
|
};
|
|
33172
33465
|
continue;
|
|
33173
33466
|
}
|
|
33467
|
+
if (child.type === "meta" && child.props.name === "theme-color") {
|
|
33468
|
+
themeColors.push({
|
|
33469
|
+
content: child.props.content,
|
|
33470
|
+
media: child.props.media ?? null
|
|
33471
|
+
});
|
|
33472
|
+
continue;
|
|
33473
|
+
}
|
|
33174
33474
|
const el = document.createElement(child.type);
|
|
33175
33475
|
const props = child.props || {};
|
|
33176
33476
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -33186,7 +33486,9 @@ const Head = ({
|
|
|
33186
33486
|
titleEntries.push(titleEntry);
|
|
33187
33487
|
document.title = titleEntry.title;
|
|
33188
33488
|
}
|
|
33489
|
+
const removeThemeColors = themeColors.length > 0 ? pushHeadThemeColors(themeColors) : null;
|
|
33189
33490
|
return () => {
|
|
33491
|
+
removeThemeColors?.();
|
|
33190
33492
|
if (titleEntry) {
|
|
33191
33493
|
titleEntries.splice(titleEntries.indexOf(titleEntry), 1);
|
|
33192
33494
|
const lastEntry = titleEntries[titleEntries.length - 1];
|
|
@@ -36173,6 +36475,9 @@ const rebuildWatcher = () => {
|
|
|
36173
36475
|
if (fromIndex === index) {
|
|
36174
36476
|
return;
|
|
36175
36477
|
}
|
|
36478
|
+
// A page of a relation can be a param: a move between two of them is a
|
|
36479
|
+
// page change the path does not show (see page_change.js).
|
|
36480
|
+
notePageChange();
|
|
36176
36481
|
if (navigationAnimated) {
|
|
36177
36482
|
// The movement on screen was turned round for this very navigation (see
|
|
36178
36483
|
// turnRunningTransitionRound): it is already being answered, by the
|
|
@@ -37537,6 +37842,10 @@ const RouteTravel = ({
|
|
|
37537
37842
|
}
|
|
37538
37843
|
return;
|
|
37539
37844
|
}
|
|
37845
|
+
// Whoever asked for it: a tab of this row can be a param, and a move
|
|
37846
|
+
// between two of them is a page change the path does not show (see
|
|
37847
|
+
// page_change.js).
|
|
37848
|
+
notePageChange();
|
|
37540
37849
|
const page = pages[index];
|
|
37541
37850
|
// A page this box asked for itself — a travel's own navigation, or one
|
|
37542
37851
|
// it had given up waiting on: what arrives here is the answer to a
|
|
@@ -54254,11 +54563,13 @@ const Expandable = props => {
|
|
|
54254
54563
|
openController.openEffect = openEvent => {
|
|
54255
54564
|
const contentContainer = contentContainerRef.current;
|
|
54256
54565
|
// `silent`: the expandable was already open when the page appeared
|
|
54257
|
-
// (`open`/`defaultOpen` at mount
|
|
54258
|
-
//
|
|
54259
|
-
//
|
|
54260
|
-
//
|
|
54261
|
-
//
|
|
54566
|
+
// (`open`/`defaultOpen` at mount, or the navigation putting that page on
|
|
54567
|
+
// screen opening it — see page_change.js). Nothing changed for anyone to
|
|
54568
|
+
// be told about, nothing was ever shown closed to move away from, and a
|
|
54569
|
+
// page must not have its focus stolen by a section that was simply
|
|
54570
|
+
// already open — so an opening that was never an opening plays nothing,
|
|
54571
|
+
// says nothing and takes nothing. The action it carries still runs: its
|
|
54572
|
+
// content is due.
|
|
54262
54573
|
const silent = Boolean(openEvent.detail.silent);
|
|
54263
54574
|
const revealing = animation && !silent && Boolean(contentContainer);
|
|
54264
54575
|
// Where the last paint left the track — 0 when fully closed, partway when
|
|
@@ -54305,7 +54616,9 @@ const Expandable = props => {
|
|
|
54305
54616
|
effectiveAction.abort();
|
|
54306
54617
|
}
|
|
54307
54618
|
cancelSettleWatch();
|
|
54308
|
-
|
|
54619
|
+
// `silent`: closed by the navigation that puts another page on screen,
|
|
54620
|
+
// which is its movement (see page_change.js).
|
|
54621
|
+
const collapsing = animation && !closeEvent.detail.silent && Boolean(contentContainerAtClose);
|
|
54309
54622
|
if (collapsing) {
|
|
54310
54623
|
// Now, while the content is still fully laid out — the collapsing
|
|
54311
54624
|
// track uncovers a content frozen at that size.
|
|
@@ -61661,10 +61974,23 @@ const SlideContainer = ({
|
|
|
61661
61974
|
y: 0
|
|
61662
61975
|
};
|
|
61663
61976
|
const durationMs = durationToMs(duration);
|
|
61977
|
+
// A change of slide the page changing carries is no travel: the page is
|
|
61978
|
+
// the movement, and the slides land where the address puts them (see
|
|
61979
|
+
// page_change.js). A travel still playing is cut on the spot, and the
|
|
61980
|
+
// indicator riding it with it.
|
|
61981
|
+
const landsWithPage = isRenderingPageChange() && (stageRef.current ? stageRef.current.area : drawnAreaRef.current) !== currentArea;
|
|
61982
|
+
if (landsWithPage && trackAnimationRef.current) {
|
|
61983
|
+
trackAnimationRef.current.cancel();
|
|
61984
|
+
trackAnimationRef.current = null;
|
|
61985
|
+
movingRef.current = null;
|
|
61986
|
+
cancelTravelProgressAnimation();
|
|
61987
|
+
paintTravelProgress(0);
|
|
61988
|
+
}
|
|
61989
|
+
const still = noTravel || landsWithPage;
|
|
61664
61990
|
// Nothing is travelling, so nothing is staged: the picture to paint is the
|
|
61665
61991
|
// map itself, and a stage left over from a travel that has just been given
|
|
61666
61992
|
// up on would be painted instead of it.
|
|
61667
|
-
if (
|
|
61993
|
+
if (still) {
|
|
61668
61994
|
stageRef.current = null;
|
|
61669
61995
|
}
|
|
61670
61996
|
let stage = stageRef.current;
|
|
@@ -61672,7 +61998,7 @@ const SlideContainer = ({
|
|
|
61672
61998
|
// between the picture and the slide arriving, on the axis it walks.
|
|
61673
61999
|
let travelStep = null;
|
|
61674
62000
|
const drawnArea = stage ? stage.area : drawnAreaRef.current;
|
|
61675
|
-
const travelStarts = !
|
|
62001
|
+
const travelStarts = !still && durationMs > 0 && drawnArea !== undefined && drawnArea !== currentArea && slideElements.some(slideElement => readArea(slideElement) === drawnArea);
|
|
61676
62002
|
if (travelStarts) {
|
|
61677
62003
|
// Where the slide being left IS — the place the stage gave it if a travel
|
|
61678
62004
|
// was already playing (a press landing mid-travel departs from where the
|
|
@@ -61799,7 +62125,7 @@ const SlideContainer = ({
|
|
|
61799
62125
|
// duration (the ask is zero, so ratioOfOneTravel answers 1), slowing the
|
|
61800
62126
|
// travel at every re-render.
|
|
61801
62127
|
const sameTravelPlaying = travelInFlight && !offsetDragged && offsetTargetBefore === offset;
|
|
61802
|
-
const travels = !sameTravelPlaying && !
|
|
62128
|
+
const travels = !sameTravelPlaying && !still && durationMs > 0 && offsetBefore !== undefined && offsetBefore !== offset;
|
|
61803
62129
|
if (travels) {
|
|
61804
62130
|
// The time it takes is the distance it has left to cover: a travel picked
|
|
61805
62131
|
// up a fifth of the way through goes back in a fifth of the time, so what
|
|
@@ -65795,9 +66121,17 @@ const css$E = /* css */`
|
|
|
65795
66121
|
on its own, sharp, above the wall: any wall on that frame — even a
|
|
65796
66122
|
half-strength wash and blur — is a flash on the anchor. So the wall
|
|
65797
66123
|
arrives from nothing, over the movement: on that frame the page is
|
|
65798
|
-
untouched, and the anchor with it.
|
|
66124
|
+
untouched, and the anchor with it.
|
|
66125
|
+
Keyed on the aria-expanded flip, like the local wall: openEffect reads
|
|
66126
|
+
the layout while the transitions are off, which spends @starting-style
|
|
66127
|
+
with nothing armed. @starting-style stays for an opening that reads
|
|
66128
|
+
nothing before the flip. A silent opening flips before the transitions
|
|
66129
|
+
come back, hence the :not(): its wall lands at once. */
|
|
65799
66130
|
&[data-lifting] {
|
|
65800
|
-
|
|
66131
|
+
&[aria-expanded="false"]::backdrop {
|
|
66132
|
+
opacity: 0;
|
|
66133
|
+
}
|
|
66134
|
+
&[aria-expanded="true"]:not([navi-transitions-off])::backdrop {
|
|
65801
66135
|
opacity: 1;
|
|
65802
66136
|
transition-property: opacity;
|
|
65803
66137
|
transition-duration: var(--navi-popup-lift-duration, 0.25s);
|
|
@@ -65808,6 +66142,12 @@ const css$E = /* css */`
|
|
|
65808
66142
|
}
|
|
65809
66143
|
}
|
|
65810
66144
|
}
|
|
66145
|
+
/* Off while the dialog's own transitions are off (switchTransitionsOff):
|
|
66146
|
+
the inline style that turns them off cannot reach a pseudo-element.
|
|
66147
|
+
After the rules above, which it must win over at equal weight. */
|
|
66148
|
+
&[navi-transitions-off]::backdrop {
|
|
66149
|
+
transition-property: none;
|
|
66150
|
+
}
|
|
65811
66151
|
|
|
65812
66152
|
&[data-focus-visible],
|
|
65813
66153
|
/* …or something filling it holds the keyboard and offers its ring to
|
|
@@ -66251,6 +66591,16 @@ const css$E = /* css */`
|
|
|
66251
66591
|
* `saturate()`/`grayscale()` work too. How far what is behind withdraws is
|
|
66252
66592
|
* a question of paint, not of what an outside click does — a backdrop that
|
|
66253
66593
|
* closes can blur.
|
|
66594
|
+
* @param {string|false} [props.themeColor] - The colour the browser paints
|
|
66595
|
+
* above the page (`<meta name="theme-color">`: Chrome's address bar on
|
|
66596
|
+
* Android, an installed app's status bar) while this dialog is open.
|
|
66597
|
+
* Left out, it is what the dialog lays over the top edge of the screen: its
|
|
66598
|
+
* backdrop over the colour the page asked for (`<Head>`), and its own
|
|
66599
|
+
* background over that when it is flush with the top edge
|
|
66600
|
+
* (`marginWithContainer={0}` with `expand`/`expandY` or a top
|
|
66601
|
+
* `positionArea`). A colour forces it, for a surface whose colour cannot be
|
|
66602
|
+
* read (an image, a gradient); `false` leaves the page's colour alone.
|
|
66603
|
+
* `layer="top"` only.
|
|
66254
66604
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
66255
66605
|
* dialog so the page/container behind it can't scroll while it's open.
|
|
66256
66606
|
* A `layer="local"` dialog always locks its own positioned ancestor's
|
|
@@ -66774,6 +67124,9 @@ const useDialogProps = props => {
|
|
|
66774
67124
|
// so both objects below get them (see this file's CSS).
|
|
66775
67125
|
backdropColor,
|
|
66776
67126
|
backdropFilter,
|
|
67127
|
+
// Read at the opening, with the backdrop and the surface it would
|
|
67128
|
+
// otherwise be worked out from (see openEffect).
|
|
67129
|
+
themeColor,
|
|
66777
67130
|
scrollCapture: scrollCaptureProp,
|
|
66778
67131
|
// "auto" (default) → the dialog follows its content. "frozen" → measured
|
|
66779
67132
|
// once, held at that size while open. See this prop's own JSDoc above.
|
|
@@ -67030,8 +67383,9 @@ const useDialogProps = props => {
|
|
|
67030
67383
|
const dialogEl = ref.current;
|
|
67031
67384
|
// A mount-time opening was never seen closed (see openEffect's own
|
|
67032
67385
|
// `silent`): there is no box it comes from, because nothing was shown
|
|
67033
|
-
// before it.
|
|
67034
|
-
|
|
67386
|
+
// before it. And a change a page change carries moves with that page,
|
|
67387
|
+
// in and out of no anchor (see page_change.js).
|
|
67388
|
+
if (!dialogEl || event.detail.silent || opened && !lifting) {
|
|
67035
67389
|
applyChange();
|
|
67036
67390
|
return;
|
|
67037
67391
|
}
|
|
@@ -67087,8 +67441,9 @@ const useDialogProps = props => {
|
|
|
67087
67441
|
}
|
|
67088
67442
|
|
|
67089
67443
|
// Set by useOpenControllerByProps for the very first open triggered by
|
|
67090
|
-
// `open`/`defaultOpen` already being truthy at mount
|
|
67091
|
-
// own openEffect for the full
|
|
67444
|
+
// `open`/`defaultOpen` already being truthy at mount, and for an open a
|
|
67445
|
+
// page change carries — see popover.jsx's own openEffect for the full
|
|
67446
|
+
// reasoning, mirrored here identically.
|
|
67092
67447
|
const silent = Boolean(e.detail.silent);
|
|
67093
67448
|
|
|
67094
67449
|
// document.documentElement — the shared "no real container, use the
|
|
@@ -67132,7 +67487,7 @@ const useDialogProps = props => {
|
|
|
67132
67487
|
// comment), even though Dialog never needs to measure/flip anything: it
|
|
67133
67488
|
// still needs a genuinely rendered "closed" frame to transition from, not a
|
|
67134
67489
|
// jump straight from not-shown to aria-expanded="true".
|
|
67135
|
-
dialogEl
|
|
67490
|
+
switchTransitionsOff(dialogEl);
|
|
67136
67491
|
if (backdropEl) {
|
|
67137
67492
|
disarmBackdropHideRef.current?.();
|
|
67138
67493
|
disarmBackdropHideRef.current = null;
|
|
@@ -67400,18 +67755,28 @@ const useDialogProps = props => {
|
|
|
67400
67755
|
dialogEl.setAttribute("aria-expanded", "true");
|
|
67401
67756
|
backdropEl?.setAttribute("aria-expanded", "true");
|
|
67402
67757
|
dialogEl.getBoundingClientRect();
|
|
67403
|
-
dialogEl
|
|
67758
|
+
switchTransitionsOn(dialogEl);
|
|
67404
67759
|
if (backdropEl) {
|
|
67405
67760
|
backdropEl.style.transitionProperty = "";
|
|
67406
67761
|
}
|
|
67407
67762
|
} else {
|
|
67408
|
-
dialogEl
|
|
67763
|
+
switchTransitionsOn(dialogEl);
|
|
67409
67764
|
dialogEl.setAttribute("aria-expanded", "true");
|
|
67410
67765
|
backdropEl?.setAttribute("aria-expanded", "true");
|
|
67411
67766
|
if (backdropEl) {
|
|
67412
67767
|
backdropEl.style.transitionProperty = "";
|
|
67413
67768
|
}
|
|
67414
67769
|
}
|
|
67770
|
+
// What the browser paints above the page goes with what is laid over its
|
|
67771
|
+
// top edge, from the start of the opening to the start of the closing: a
|
|
67772
|
+
// local dialog lays nothing there.
|
|
67773
|
+
if (isTopLayer && themeColor !== false) {
|
|
67774
|
+
addCleanup(pushPopupThemeColor({
|
|
67775
|
+
themeColor,
|
|
67776
|
+
backdropColor: backdrop ? getComputedStyle(dialogEl, "::backdrop").backgroundColor : null,
|
|
67777
|
+
surfaceColor: flushEdges.top ? getComputedStyle(dialogEl).backgroundColor : null
|
|
67778
|
+
}));
|
|
67779
|
+
}
|
|
67415
67780
|
const cancelOpenInteractionSuppression = !silent && hasCssTransitionAnimation ? suppressPointerEventsDuringTransition(dialogEl) : null;
|
|
67416
67781
|
// Handing the focus to a field is what raises the on-screen keyboard, and
|
|
67417
67782
|
// the keyboard takes away the very room this dialog was just placed
|
|
@@ -67441,6 +67806,15 @@ const useDialogProps = props => {
|
|
|
67441
67806
|
return closeEvent => {
|
|
67442
67807
|
debugPopup(`"${closeEvent.type}" on ${getElementSignature(closeEvent.target)} -> closeDialog`);
|
|
67443
67808
|
clearTextSelectionInside(dialogEl);
|
|
67809
|
+
// Closed by the navigation that puts another page on screen: it leaves
|
|
67810
|
+
// with that page, and plays no exit of its own (see page_change.js).
|
|
67811
|
+
const silent = Boolean(closeEvent.detail.silent);
|
|
67812
|
+
if (silent) {
|
|
67813
|
+
switchTransitionsOff(dialogEl);
|
|
67814
|
+
if (backdropEl) {
|
|
67815
|
+
backdropEl.style.transitionProperty = "none";
|
|
67816
|
+
}
|
|
67817
|
+
}
|
|
67444
67818
|
dialogEl.setAttribute("aria-expanded", "false");
|
|
67445
67819
|
if (!isTopLayer) {
|
|
67446
67820
|
openLocalDialogCount = Math.max(0, openLocalDialogCount - 1);
|
|
@@ -67480,7 +67854,7 @@ const useDialogProps = props => {
|
|
|
67480
67854
|
// next opening clears it (see openEffect's own unfreezeSize).
|
|
67481
67855
|
freezeSize(dialogEl);
|
|
67482
67856
|
cancelOpenInteractionSuppression?.();
|
|
67483
|
-
if (hasCssTransitionAnimation) {
|
|
67857
|
+
if (hasCssTransitionAnimation && !silent) {
|
|
67484
67858
|
suppressPointerEventsDuringTransition(dialogEl);
|
|
67485
67859
|
}
|
|
67486
67860
|
if (backdropEl) {
|
|
@@ -67491,6 +67865,15 @@ const useDialogProps = props => {
|
|
|
67491
67865
|
}
|
|
67492
67866
|
restoreFocus(closeEvent);
|
|
67493
67867
|
cleanup();
|
|
67868
|
+
if (silent) {
|
|
67869
|
+
// The closed frame committed with nothing transitioning, before
|
|
67870
|
+
// transitions come back for the next opening.
|
|
67871
|
+
dialogEl.getBoundingClientRect();
|
|
67872
|
+
switchTransitionsOn(dialogEl);
|
|
67873
|
+
if (backdropEl) {
|
|
67874
|
+
backdropEl.style.transitionProperty = "";
|
|
67875
|
+
}
|
|
67876
|
+
}
|
|
67494
67877
|
};
|
|
67495
67878
|
};
|
|
67496
67879
|
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
@@ -67760,6 +68143,19 @@ const warnBottomSheetHoldingKeyboardControl = dialogEl => {
|
|
|
67760
68143
|
}
|
|
67761
68144
|
console.warn(`[navi] a Dialog docked to the bottom edge (dockedOnSmallTouchScreen="bottom") holds a field that raises the virtual keyboard. On a phone the keyboard rises from the edge the sheet rests on and reflows the sheet into the strip left above it, at every keystroke. A sheet one types into rests on the top edge: dockedOnSmallTouchScreen without a value. "bottom" is for a sheet one reads and taps.`, controlEl);
|
|
67762
68145
|
};
|
|
68146
|
+
|
|
68147
|
+
// The dialog's own transitions, off while a frame is pinned (the closed one an
|
|
68148
|
+
// entrance starts from, the open one a silent opening lands on, the closed one
|
|
68149
|
+
// a silent close leaves) and back on after it. A modal's wall is its native
|
|
68150
|
+
// ::backdrop, which takes no inline style: it reads the attribute instead.
|
|
68151
|
+
const switchTransitionsOff = dialogEl => {
|
|
68152
|
+
dialogEl.style.transitionProperty = "none";
|
|
68153
|
+
dialogEl.setAttribute("navi-transitions-off", "");
|
|
68154
|
+
};
|
|
68155
|
+
const switchTransitionsOn = dialogEl => {
|
|
68156
|
+
dialogEl.style.transitionProperty = "";
|
|
68157
|
+
dialogEl.removeAttribute("navi-transitions-off");
|
|
68158
|
+
};
|
|
67763
68159
|
const DIALOG_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":focus-within"];
|
|
67764
68160
|
|
|
67765
68161
|
// Lets consumers pass animationDuration="0.5s" as a regular prop; Box maps
|
|
@@ -68210,6 +68606,12 @@ const css$D = /* css */`
|
|
|
68210
68606
|
* `saturate()`/`grayscale()` work too. How far what is behind withdraws is
|
|
68211
68607
|
* a question of paint, not of what an outside click does — a backdrop that
|
|
68212
68608
|
* closes can blur.
|
|
68609
|
+
* @param {string|false} [props.themeColor] - The colour the browser paints
|
|
68610
|
+
* above the page (`<meta name="theme-color">`: Chrome's address bar on
|
|
68611
|
+
* Android, an installed app's status bar) while this popover is open.
|
|
68612
|
+
* Left out, it is the backdrop laid over the colour the page asked for
|
|
68613
|
+
* (`<Head>`), and the page's colour when there is no backdrop. A colour
|
|
68614
|
+
* forces it; `false` leaves the page's colour alone. `layer="top"` only.
|
|
68213
68615
|
* @param {boolean} [props.scrollCapture] - Traps scroll gestures inside the
|
|
68214
68616
|
* popover so the page/container behind it can't scroll while it's open.
|
|
68215
68617
|
* @param {boolean} [props.focusCapture] - Traps Tab navigation inside the
|
|
@@ -68554,6 +68956,9 @@ const usePopoverProps = props => {
|
|
|
68554
68956
|
// which is a sibling element (see this file's top comment).
|
|
68555
68957
|
backdropColor,
|
|
68556
68958
|
backdropFilter,
|
|
68959
|
+
// Read at the opening, with the backdrop it would otherwise be worked out
|
|
68960
|
+
// from (see openEffect).
|
|
68961
|
+
themeColor,
|
|
68557
68962
|
group,
|
|
68558
68963
|
scrollCapture,
|
|
68559
68964
|
focusCapture,
|
|
@@ -68692,8 +69097,10 @@ const usePopoverProps = props => {
|
|
|
68692
69097
|
// `open`/`defaultOpen` already being truthy at mount — there's nothing
|
|
68693
69098
|
// to visually transition away from (nothing was ever shown as "closed"
|
|
68694
69099
|
// to the user), so this open skips the animation entirely instead of
|
|
68695
|
-
// playing it against a closed frame that was never actually seen.
|
|
68696
|
-
//
|
|
69100
|
+
// playing it against a closed frame that was never actually seen. Set
|
|
69101
|
+
// too for an open the page changing carries: that page is its movement
|
|
69102
|
+
// (see page_change.js). See the final commit step below for how that's
|
|
69103
|
+
// done.
|
|
68697
69104
|
const silent = Boolean(e.detail.silent);
|
|
68698
69105
|
const [cleanup, addCleanup] = createPubSub(true);
|
|
68699
69106
|
|
|
@@ -69129,7 +69536,7 @@ const usePopoverProps = props => {
|
|
|
69129
69536
|
// focus in. Inlined rather than a standalone function since it only
|
|
69130
69537
|
// has this one call site.
|
|
69131
69538
|
//
|
|
69132
|
-
// `silent` (mounting already open
|
|
69539
|
+
// `silent` (mounting already open, or carried by a page change) flips the
|
|
69133
69540
|
// order instead: aria-expanded first, *then* re-enable transitions, with
|
|
69134
69541
|
// its own forced reflow in between the two — without that reflow, the
|
|
69135
69542
|
// browser coalesces "flip aria-expanded" and "re-enable transitions"
|
|
@@ -69162,6 +69569,15 @@ const usePopoverProps = props => {
|
|
|
69162
69569
|
backdropEl.style.transitionProperty = "";
|
|
69163
69570
|
}
|
|
69164
69571
|
}
|
|
69572
|
+
// What the browser paints above the page goes with what is laid over its
|
|
69573
|
+
// top edge, from the start of the opening to the start of the closing: a
|
|
69574
|
+
// local popover's backdrop stops at its container.
|
|
69575
|
+
if (isTopLayer && themeColor !== false) {
|
|
69576
|
+
addCleanup(pushPopupThemeColor({
|
|
69577
|
+
themeColor,
|
|
69578
|
+
backdropColor: backdropEl ? getComputedStyle(backdropEl).backgroundColor : null
|
|
69579
|
+
}));
|
|
69580
|
+
}
|
|
69165
69581
|
const cancelOpenInteractionSuppression = !silent && hasCssTransitionAnimation ? suppressPointerEventsDuringTransition(popoverEl) : null;
|
|
69166
69582
|
// autoFocus={false}: opening moves no focus at all — whoever has the
|
|
69167
69583
|
// keyboard keeps it (combobox-style suggestions under an input being
|
|
@@ -69176,6 +69592,15 @@ const usePopoverProps = props => {
|
|
|
69176
69592
|
return closeEvent => {
|
|
69177
69593
|
debugPopup(closeEvent, `closePopover()`);
|
|
69178
69594
|
clearTextSelectionInside(popoverEl);
|
|
69595
|
+
// Closed by the navigation that puts another page on screen: it leaves
|
|
69596
|
+
// with that page, and plays no exit of its own (see page_change.js).
|
|
69597
|
+
const silent = Boolean(closeEvent.detail.silent);
|
|
69598
|
+
if (silent) {
|
|
69599
|
+
popoverEl.style.transitionProperty = "none";
|
|
69600
|
+
if (backdropEl) {
|
|
69601
|
+
backdropEl.style.transitionProperty = "none";
|
|
69602
|
+
}
|
|
69603
|
+
}
|
|
69179
69604
|
popoverEl.setAttribute("aria-expanded", "false");
|
|
69180
69605
|
// Set regardless of isTopLayer — see the open side's own identical
|
|
69181
69606
|
// comment (openEffect above) for why hidePopover() alone isn't
|
|
@@ -69202,7 +69627,7 @@ const usePopoverProps = props => {
|
|
|
69202
69627
|
// open is its own separate call with no way to reach back into this
|
|
69203
69628
|
// one).
|
|
69204
69629
|
cancelOpenInteractionSuppression?.();
|
|
69205
|
-
if (hasCssTransitionAnimation) {
|
|
69630
|
+
if (hasCssTransitionAnimation && !silent) {
|
|
69206
69631
|
suppressPointerEventsDuringTransition(popoverEl);
|
|
69207
69632
|
}
|
|
69208
69633
|
if (backdropEl) {
|
|
@@ -69237,6 +69662,15 @@ const usePopoverProps = props => {
|
|
|
69237
69662
|
popoverEl.removeAttribute("data-position-y-current");
|
|
69238
69663
|
popoverEl.removeAttribute("data-position-x-current");
|
|
69239
69664
|
cleanup();
|
|
69665
|
+
if (silent) {
|
|
69666
|
+
// The closed frame committed with nothing transitioning, before
|
|
69667
|
+
// transitions come back for the next opening.
|
|
69668
|
+
popoverEl.getBoundingClientRect();
|
|
69669
|
+
popoverEl.style.transitionProperty = "";
|
|
69670
|
+
if (backdropEl) {
|
|
69671
|
+
backdropEl.style.transitionProperty = "";
|
|
69672
|
+
}
|
|
69673
|
+
}
|
|
69240
69674
|
};
|
|
69241
69675
|
};
|
|
69242
69676
|
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
@@ -69560,6 +69994,10 @@ const css$C = /* css */`@layer navi {
|
|
|
69560
69994
|
* identically): the wash the backdrop paints over what is behind.
|
|
69561
69995
|
* @param {string} [props.backdropFilter] - Forwarded as-is: what that wash
|
|
69562
69996
|
* does to the picture underneath, `"blur(4px)"` and the like.
|
|
69997
|
+
* @param {string|false} [props.themeColor] - Forwarded as-is: the colour the
|
|
69998
|
+
* browser paints above the page while the popup is open, worked out from
|
|
69999
|
+
* what it lays over the top edge when left out. A colour forces it, `false`
|
|
70000
|
+
* leaves the page's colour alone.
|
|
69563
70001
|
* @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]
|
|
69564
70002
|
* - Forwarded to whichever renders — see either component's own doc.
|
|
69565
70003
|
* `"expanding"`/`"expand-*"` are `Popover`'s own; `"lifting"` and
|
|
@@ -70571,6 +71009,26 @@ const PickerCustom = props => {
|
|
|
70571
71009
|
if (isWithinPickerContent(e.target)) {
|
|
70572
71010
|
return null;
|
|
70573
71011
|
}
|
|
71012
|
+
// Still open at the click of a later press: that press brought no
|
|
71013
|
+
// mousedown to close on (a drag source cancelled its pointerdown),
|
|
71014
|
+
// so it closes here, as it opens here in that case. A mousedown
|
|
71015
|
+
// that came has closed it already; the press that opened it is
|
|
71016
|
+
// only ending.
|
|
71017
|
+
if (openController.opened && !openedDuringThisPress(openController)) {
|
|
71018
|
+
return {
|
|
71019
|
+
name: "click to close picker",
|
|
71020
|
+
intent: "read",
|
|
71021
|
+
prevented: () => {
|
|
71022
|
+
e.preventDefault();
|
|
71023
|
+
},
|
|
71024
|
+
allowed: () => {
|
|
71025
|
+
requestClose(e, {
|
|
71026
|
+
isCancel: true
|
|
71027
|
+
});
|
|
71028
|
+
e.preventDefault();
|
|
71029
|
+
}
|
|
71030
|
+
};
|
|
71031
|
+
}
|
|
70574
71032
|
if (!opensOnPress) {
|
|
70575
71033
|
// Neither the tap, nor the click a hold leaves behind: the press
|
|
70576
71034
|
// is not what opens this picker.
|
|
@@ -71226,12 +71684,12 @@ const isScrolling = (element) => {
|
|
|
71226
71684
|
if (element === undefined) {
|
|
71227
71685
|
return scrollActivitySignal.peek();
|
|
71228
71686
|
}
|
|
71229
|
-
return asScroller(element).hasAttribute("navi-scrolling");
|
|
71687
|
+
return asScroller$1(element).hasAttribute("navi-scrolling");
|
|
71230
71688
|
};
|
|
71231
71689
|
|
|
71232
71690
|
// The page scroll is dispatched on the document; the element carrying the
|
|
71233
71691
|
// attribute is the one CSS can reach, document.scrollingElement.
|
|
71234
|
-
const asScroller = (eventTargetOrElement) => {
|
|
71692
|
+
const asScroller$1 = (eventTargetOrElement) => {
|
|
71235
71693
|
if (eventTargetOrElement === document || eventTargetOrElement === window) {
|
|
71236
71694
|
return document.scrollingElement;
|
|
71237
71695
|
}
|
|
@@ -71242,7 +71700,7 @@ const idleTimeoutMap = new Map();
|
|
|
71242
71700
|
document.addEventListener(
|
|
71243
71701
|
"scroll",
|
|
71244
71702
|
(e) => {
|
|
71245
|
-
const scroller = asScroller(e.target);
|
|
71703
|
+
const scroller = asScroller$1(e.target);
|
|
71246
71704
|
const idleTimeout = idleTimeoutMap.get(scroller);
|
|
71247
71705
|
if (idleTimeout !== undefined) {
|
|
71248
71706
|
clearTimeout(idleTimeout);
|
|
@@ -71264,6 +71722,111 @@ document.addEventListener(
|
|
|
71264
71722
|
{ capture: true, passive: true },
|
|
71265
71723
|
);
|
|
71266
71724
|
|
|
71725
|
+
/*
|
|
71726
|
+
* Whether a scroller is gliding: moving on its own toward a place it was sent
|
|
71727
|
+
* to, with no hand on it — a key (Home, PageDown, Space), a smooth `scrollTo`
|
|
71728
|
+
* or `scrollIntoView`, the status bar tap on iOS. The other scrolls follow a
|
|
71729
|
+
* hand: a finger on the screen, a wheel turning.
|
|
71730
|
+
*
|
|
71731
|
+
* It matters to whoever writes a scroll position while one plays: the write is
|
|
71732
|
+
* an instant scroll, and the browser abandons the glide for it, short of where
|
|
71733
|
+
* it was going. A hand is not abandoned: its next report moves the scroller on
|
|
71734
|
+
* from wherever the write left it.
|
|
71735
|
+
*
|
|
71736
|
+
* A scroll is followed from its first "scroll" event to its "scrollend". It is
|
|
71737
|
+
* a hand's when a finger was down as it started (the fling a finger leaves
|
|
71738
|
+
* behind is still its own), when one is down now, or when a wheel turned
|
|
71739
|
+
* around the time it started or since. A finger is down from its
|
|
71740
|
+
* touchstart to its touchend, heard on what it touched: a list removes the
|
|
71741
|
+
* items scrolled out of its window while the finger is still on one of them,
|
|
71742
|
+
* and the touchend of a removed element never reaches the document.
|
|
71743
|
+
*
|
|
71744
|
+
* In a browser without "scrollend", nothing is said to glide: a scroll would
|
|
71745
|
+
* never be seen ending.
|
|
71746
|
+
*/
|
|
71747
|
+
|
|
71748
|
+
// The scroll a wheel turn causes starts in the frame the turn is read, or the
|
|
71749
|
+
// next one. A turn further from it moved nothing (a wheel at an edge) and is no
|
|
71750
|
+
// hand on a scroll that came from elsewhere.
|
|
71751
|
+
const WHEEL_REACH = 100;
|
|
71752
|
+
|
|
71753
|
+
const flightMap = new WeakMap();
|
|
71754
|
+
let fingersDown = 0;
|
|
71755
|
+
let wheelTimeStamp = -Infinity;
|
|
71756
|
+
|
|
71757
|
+
/**
|
|
71758
|
+
* @param {Element} scrollerEl
|
|
71759
|
+
* @returns {boolean}
|
|
71760
|
+
*/
|
|
71761
|
+
const isScrollGliding = (scrollerEl) => {
|
|
71762
|
+
const flight = flightMap.get(scrollerEl);
|
|
71763
|
+
if (!flight) {
|
|
71764
|
+
return false;
|
|
71765
|
+
}
|
|
71766
|
+
if (flight.fingerDown || fingersDown > 0) {
|
|
71767
|
+
return false;
|
|
71768
|
+
}
|
|
71769
|
+
if (wheelTimeStamp > flight.timeStamp - WHEEL_REACH) {
|
|
71770
|
+
return false;
|
|
71771
|
+
}
|
|
71772
|
+
return true;
|
|
71773
|
+
};
|
|
71774
|
+
|
|
71775
|
+
// The page scroll is dispatched on the document; the scroller asked about is
|
|
71776
|
+
// document.scrollingElement.
|
|
71777
|
+
const asScroller = (eventTarget) => {
|
|
71778
|
+
if (eventTarget === document || eventTarget === window) {
|
|
71779
|
+
return document.scrollingElement;
|
|
71780
|
+
}
|
|
71781
|
+
return eventTarget;
|
|
71782
|
+
};
|
|
71783
|
+
|
|
71784
|
+
const onTouchEnd = (e) => {
|
|
71785
|
+
fingersDown = e.touches.length;
|
|
71786
|
+
};
|
|
71787
|
+
|
|
71788
|
+
if ("onscrollend" in window) {
|
|
71789
|
+
// Capturing: "scroll" and "scrollend" do not bubble from an element.
|
|
71790
|
+
document.addEventListener(
|
|
71791
|
+
"scroll",
|
|
71792
|
+
(e) => {
|
|
71793
|
+
const scroller = asScroller(e.target);
|
|
71794
|
+
if (flightMap.has(scroller)) {
|
|
71795
|
+
return;
|
|
71796
|
+
}
|
|
71797
|
+
flightMap.set(scroller, {
|
|
71798
|
+
timeStamp: e.timeStamp,
|
|
71799
|
+
fingerDown: fingersDown > 0,
|
|
71800
|
+
});
|
|
71801
|
+
},
|
|
71802
|
+
{ capture: true, passive: true },
|
|
71803
|
+
);
|
|
71804
|
+
document.addEventListener(
|
|
71805
|
+
"scrollend",
|
|
71806
|
+
(e) => {
|
|
71807
|
+
flightMap.delete(asScroller(e.target));
|
|
71808
|
+
},
|
|
71809
|
+
{ capture: true, passive: true },
|
|
71810
|
+
);
|
|
71811
|
+
window.addEventListener(
|
|
71812
|
+
"wheel",
|
|
71813
|
+
(e) => {
|
|
71814
|
+
wheelTimeStamp = e.timeStamp;
|
|
71815
|
+
},
|
|
71816
|
+
{ capture: true, passive: true },
|
|
71817
|
+
);
|
|
71818
|
+
window.addEventListener(
|
|
71819
|
+
"touchstart",
|
|
71820
|
+
(e) => {
|
|
71821
|
+
fingersDown = e.touches.length;
|
|
71822
|
+
const touchedEl = e.composedPath()[0];
|
|
71823
|
+
touchedEl.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
71824
|
+
touchedEl.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
71825
|
+
},
|
|
71826
|
+
{ capture: true, passive: true },
|
|
71827
|
+
);
|
|
71828
|
+
}
|
|
71829
|
+
|
|
71267
71830
|
const LoadingDotsSvg = () => {
|
|
71268
71831
|
return jsxs("svg", {
|
|
71269
71832
|
viewBox: "0 0 200 200",
|
|
@@ -74349,7 +74912,9 @@ const useListScrollSync = ({
|
|
|
74349
74912
|
const scrolledFallback = scrolled === undefined || scrolled === null ? "start" : defaultScrolled ?? "start";
|
|
74350
74913
|
const startPlaceRef = useRef({
|
|
74351
74914
|
userTookOver: false,
|
|
74352
|
-
wanted: scrolledWanted
|
|
74915
|
+
wanted: scrolledWanted,
|
|
74916
|
+
way: null,
|
|
74917
|
+
arrived: false
|
|
74353
74918
|
});
|
|
74354
74919
|
if (startPlaceRef.current.wanted !== scrolledWanted) {
|
|
74355
74920
|
startPlaceRef.current.wanted = scrolledWanted;
|
|
@@ -74560,8 +75125,10 @@ const useListScrollSync = ({
|
|
|
74560
75125
|
// keeps moving while the list is still finding out how many items it has and
|
|
74561
75126
|
// how tall one is, so landing there once would land next to it. What ends the
|
|
74562
75127
|
// hold is the user reaching for the list — a wheel, a finger, a key, a hand
|
|
74563
|
-
// on the scrollbar —
|
|
74564
|
-
// as
|
|
75128
|
+
// on the scrollbar — or anything else moving it. A scroll event alone does
|
|
75129
|
+
// not say which: the list provokes as many as the user does, so only one
|
|
75130
|
+
// landing off the way of the list's own placement ends it (see
|
|
75131
|
+
// sendHeldScroll).
|
|
74565
75132
|
const placeWhereHeld = () => {
|
|
74566
75133
|
if (scrolledWanted === "start" || scrolledWanted === undefined || startPlaceRef.current.userTookOver || !ref.current) {
|
|
74567
75134
|
return;
|
|
@@ -74610,12 +75177,7 @@ const useListScrollSync = ({
|
|
|
74610
75177
|
}
|
|
74611
75178
|
const scrollerEl = getScroller();
|
|
74612
75179
|
if (openAt === "end") {
|
|
74613
|
-
|
|
74614
|
-
if (horizontal) {
|
|
74615
|
-
scrollerEl.scrollLeft = scrollerEl.scrollWidth;
|
|
74616
|
-
} else {
|
|
74617
|
-
scrollerEl.scrollTop = scrollerEl.scrollHeight;
|
|
74618
|
-
}
|
|
75180
|
+
sendHeldScroll(horizontal ? scrollerEl.scrollWidth : scrollerEl.scrollHeight);
|
|
74619
75181
|
return;
|
|
74620
75182
|
}
|
|
74621
75183
|
if (typeof openAt === "number") {
|
|
@@ -74639,15 +75201,45 @@ const useListScrollSync = ({
|
|
|
74639
75201
|
const itemRect = itemEl.getBoundingClientRect();
|
|
74640
75202
|
const offsetWanted = resolveOpenOffset(getItemScrollInset(scrollerEl, itemEl, horizontal) + offset, horizontal ? viewportRect.width : viewportRect.height, horizontal ? itemRect.width : itemRect.height);
|
|
74641
75203
|
const offsetNow = horizontal ? itemRect.left - viewportRect.left : itemRect.top - viewportRect.top;
|
|
75204
|
+
const scrollNow = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
74642
75205
|
const delta = offsetNow - offsetWanted;
|
|
74643
75206
|
if (delta > -0.5 && delta < 0.5) {
|
|
75207
|
+
sendHeldScroll(scrollNow);
|
|
74644
75208
|
return;
|
|
74645
75209
|
}
|
|
75210
|
+
sendHeldScroll(scrollNow + delta);
|
|
75211
|
+
};
|
|
75212
|
+
// Every placement goes through here, the ones that find the list in place
|
|
75213
|
+
// too: it keeps the way from where the scroller was to where it is sent,
|
|
75214
|
+
// which is how a scroll event tells the hold's own scrolls from anyone
|
|
75215
|
+
// else's (see letGoUnlessHeldScroll). The whole way, not just its end: under
|
|
75216
|
+
// `scroll-behavior: smooth` the write glides, and every position on the way
|
|
75217
|
+
// is the list's until it gets there.
|
|
75218
|
+
const sendHeldScroll = position => {
|
|
75219
|
+
const scrollerEl = getScroller();
|
|
75220
|
+
const from = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
75221
|
+
const max = horizontal ? scrollerEl.scrollWidth - scrollerEl.clientWidth : scrollerEl.scrollHeight - scrollerEl.clientHeight;
|
|
75222
|
+
let to = position;
|
|
75223
|
+
if (to > max) {
|
|
75224
|
+
to = max;
|
|
75225
|
+
}
|
|
75226
|
+
if (to < 0) {
|
|
75227
|
+
to = 0;
|
|
75228
|
+
}
|
|
74646
75229
|
anchorRef.current = null;
|
|
75230
|
+
const place = startPlaceRef.current;
|
|
75231
|
+
place.way = {
|
|
75232
|
+
from,
|
|
75233
|
+
to
|
|
75234
|
+
};
|
|
75235
|
+
if (to === from) {
|
|
75236
|
+
place.arrived = true;
|
|
75237
|
+
return;
|
|
75238
|
+
}
|
|
74647
75239
|
if (horizontal) {
|
|
74648
|
-
scrollerEl.scrollLeft
|
|
75240
|
+
scrollerEl.scrollLeft = to;
|
|
74649
75241
|
} else {
|
|
74650
|
-
scrollerEl.scrollTop
|
|
75242
|
+
scrollerEl.scrollTop = to;
|
|
74651
75243
|
}
|
|
74652
75244
|
};
|
|
74653
75245
|
useLayoutEffect(placeWhereHeld);
|
|
@@ -74720,6 +75312,19 @@ const useListScrollSync = ({
|
|
|
74720
75312
|
evaluateWindowRef.current("items resized");
|
|
74721
75313
|
return false;
|
|
74722
75314
|
};
|
|
75315
|
+
// The box around a held list changing shape is the hold's to answer too: it
|
|
75316
|
+
// places the list again, the way it writes every scroll of its own (see
|
|
75317
|
+
// sendHeldScroll) — written any other way, the scroll would read as someone
|
|
75318
|
+
// else moving the list, and end the hold.
|
|
75319
|
+
const placeIfHeldRef = useRef(null);
|
|
75320
|
+
placeIfHeldRef.current = () => {
|
|
75321
|
+
// Read again: a scroll may have ended the hold since this render.
|
|
75322
|
+
if (!heldSomewhere || startPlaceRef.current.userTookOver) {
|
|
75323
|
+
return false;
|
|
75324
|
+
}
|
|
75325
|
+
placeWhereHeld();
|
|
75326
|
+
return true;
|
|
75327
|
+
};
|
|
74723
75328
|
useLayoutEffect(() => {
|
|
74724
75329
|
if (scrolledWanted === "start" || scrolledWanted === undefined || !ref.current) {
|
|
74725
75330
|
return undefined;
|
|
@@ -74864,6 +75469,9 @@ const useListScrollSync = ({
|
|
|
74864
75469
|
if (!entries.some(entry => entry.target === scrollerEl)) {
|
|
74865
75470
|
return;
|
|
74866
75471
|
}
|
|
75472
|
+
if (placeIfHeldRef.current()) {
|
|
75473
|
+
return;
|
|
75474
|
+
}
|
|
74867
75475
|
const position = positionRef.current;
|
|
74868
75476
|
if (!position) {
|
|
74869
75477
|
return;
|
|
@@ -74976,6 +75584,15 @@ const useListScrollSync = ({
|
|
|
74976
75584
|
return;
|
|
74977
75585
|
}
|
|
74978
75586
|
anchorRef.current = null;
|
|
75587
|
+
if (isScrollGliding(scrollerEl)) {
|
|
75588
|
+
// Nobody is looking at items standing still: they are passing by, on
|
|
75589
|
+
// their way somewhere the browser was asked to take them (a key, a smooth
|
|
75590
|
+
// scrollTo, the status bar). The write would be an instant scroll, and
|
|
75591
|
+
// the browser abandons the glide for it, short of where it was going.
|
|
75592
|
+
// Left alone, the items drawn above push the rest by `drift` while all
|
|
75593
|
+
// of it moves, and the glide lands.
|
|
75594
|
+
return;
|
|
75595
|
+
}
|
|
74979
75596
|
scrolledByListRef.current = true;
|
|
74980
75597
|
if (horizontal) {
|
|
74981
75598
|
scrollerEl.scrollLeft += drift;
|
|
@@ -75312,9 +75929,44 @@ const useListScrollSync = ({
|
|
|
75312
75929
|
reportPosition();
|
|
75313
75930
|
return;
|
|
75314
75931
|
}
|
|
75932
|
+
letGoUnlessHeldScroll();
|
|
75315
75933
|
reportPosition();
|
|
75316
75934
|
evaluateWindowRef.current("scroll");
|
|
75317
75935
|
};
|
|
75936
|
+
// A scroll the hold did not write is someone else moving the list, which
|
|
75937
|
+
// ends the hold like a wheel does — the ones that come with no input event
|
|
75938
|
+
// at all: the status bar tap on iOS, find in page, a script.
|
|
75939
|
+
// Only once the list has stood where it holds itself, though. Until then
|
|
75940
|
+
// the page is still settling around it, and some of that is written after
|
|
75941
|
+
// the list placed itself: a navigation holding the rendering for its
|
|
75942
|
+
// transition takes the arriving page to its top once the page rendered
|
|
75943
|
+
// (whenRenderingResumes), and the hold puts it back at its next commit.
|
|
75944
|
+
const letGoUnlessHeldScroll = () => {
|
|
75945
|
+
const place = startPlaceRef.current;
|
|
75946
|
+
const way = place.way;
|
|
75947
|
+
if (place.userTookOver || place.wanted === "start" || place.wanted === undefined || !way) {
|
|
75948
|
+
return;
|
|
75949
|
+
}
|
|
75950
|
+
const position = horizontal ? scrollerEl.scrollLeft : scrollerEl.scrollTop;
|
|
75951
|
+
if (position > way.to - 1 && position < way.to + 1) {
|
|
75952
|
+
// From then on only that place is the list's, or a scroll back the
|
|
75953
|
+
// way it came would pass for its own.
|
|
75954
|
+
place.way = {
|
|
75955
|
+
from: way.to,
|
|
75956
|
+
to: way.to
|
|
75957
|
+
};
|
|
75958
|
+
place.arrived = true;
|
|
75959
|
+
return;
|
|
75960
|
+
}
|
|
75961
|
+
if (!place.arrived) {
|
|
75962
|
+
return;
|
|
75963
|
+
}
|
|
75964
|
+
const low = way.from < way.to ? way.from : way.to;
|
|
75965
|
+
const high = way.from < way.to ? way.to : way.from;
|
|
75966
|
+
if (position < low - 1 || position > high + 1) {
|
|
75967
|
+
place.userTookOver = true;
|
|
75968
|
+
}
|
|
75969
|
+
};
|
|
75318
75970
|
// A page-level scroller does not emit "scroll" on the element itself
|
|
75319
75971
|
// (document.scrollingElement); the document does.
|
|
75320
75972
|
const scrollEventTarget = scrollerEl === document.scrollingElement ? document : scrollerEl;
|
|
@@ -78800,7 +79452,8 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
78800
79452
|
*
|
|
78801
79453
|
* In every form the list holds itself there while it is still finding out
|
|
78802
79454
|
* how many items there are and how tall one is, and lets go the moment the
|
|
78803
|
-
* user reaches for the list
|
|
79455
|
+
* user reaches for the list, or anything else scrolls it (a script, find in
|
|
79456
|
+
* page, the status bar tap on iOS).
|
|
78804
79457
|
* @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
|
|
78805
79458
|
* Where the list is, as the user scrolls: the item at the top of the view and
|
|
78806
79459
|
* how far below the place an item lands on its own (see `defaultScrolled`) it
|