@jsenv/navi 0.30.0 → 0.30.3
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 +291 -114
- package/dist/dev/jsenv_navi.js.map +10 -8
- package/dist/jsenv_navi.js +291 -114
- package/dist/jsenv_navi.js.map +10 -8
- package/docs/dialog_shape.md +3 -0
- package/docs/navigation.md +10 -7
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* using @jsenv/navi as intended.
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, coarsePointerSignal, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
|
-
import { createPubSub, createIterableWeakSet, elementIsFocusable, getScrollContainer, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, 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, trapScrollInside, scrollIntoViewScoped, isPressDisputedByDrag, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, scrollRoomTowards, overflowGoesToViewport, getVirtualKeyboardOverlayHeight,
|
|
6
|
+
import { createPubSub, createIterableWeakSet, elementIsFocusable, getScrollContainer, dispatchInternalCustomEvent, dispatchCustomEvent, findEvent, mergeOneStyle, findFocusable, getElementSignature, chainEvent, isTouchDrivenEvent, getKeyboardEventDefaultAction, isPressDrivenClick, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, createValueEffect, findFocusDelegateTarget, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, 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, trapScrollInside, scrollIntoViewScoped, isPressDisputedByDrag, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, scrollRoomTowards, overflowGoesToViewport, getVirtualKeyboardOverlayHeight, subscribeVisualViewportResizeSettled, subscribeVirtualKeyboardGeometryChange, parsePositionArea, snapToPixel, trapFocusInside, onAncestorReopen, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, setVirtualKeyboardOverlaysContent } 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
9
|
import { useState, useLayoutEffect, useErrorBoundary, useContext, useMemo, useRef, useReducer, useEffect, useCallback } from "preact/hooks";
|
|
@@ -40261,6 +40261,73 @@ const publishRouteRowMove = (move) => {
|
|
|
40261
40261
|
}
|
|
40262
40262
|
};
|
|
40263
40263
|
|
|
40264
|
+
/**
|
|
40265
|
+
* What a hand holding a row of routes leaves the address unable to say.
|
|
40266
|
+
*
|
|
40267
|
+
* A page not built yet builds by the address moving, so the address runs ahead of
|
|
40268
|
+
* the hand. Nothing is decided while the hand holds the row: whatever says where the
|
|
40269
|
+
* reader is answers from here until the hand lets go.
|
|
40270
|
+
*
|
|
40271
|
+
* Apart from route_travel.jsx so that every link can read it without bringing a row
|
|
40272
|
+
* along.
|
|
40273
|
+
*/
|
|
40274
|
+
|
|
40275
|
+
|
|
40276
|
+
/**
|
|
40277
|
+
* The page the reader is still on while a hand holds the row and the address has run
|
|
40278
|
+
* ahead of it, `null` otherwise. A link to a page of the row answers from it by
|
|
40279
|
+
* itself; a counter, a title or a breadcrumb around the box reads it first.
|
|
40280
|
+
*
|
|
40281
|
+
* It goes back to `null` as soon as the hand has let go and the row moves. Nothing
|
|
40282
|
+
* says when the fingers leave a trackpad: there it lasts until the row has come to
|
|
40283
|
+
* rest. A travel no hand set off holds nothing: it goes where it goes.
|
|
40284
|
+
*
|
|
40285
|
+
* One for the document. `route` says which row, for a page holding several boxes.
|
|
40286
|
+
*/
|
|
40287
|
+
const routeTravelSettledPageSignal = signal(null);
|
|
40288
|
+
|
|
40289
|
+
// Where the address ran ahead to. A link has to tell it from any other link that
|
|
40290
|
+
// matches the address: an entry of a bottom bar whose route is a section around the
|
|
40291
|
+
// row stays lit.
|
|
40292
|
+
const headingPageSignal = signal(null);
|
|
40293
|
+
|
|
40294
|
+
const holdPage = (page, headingPage) => {
|
|
40295
|
+
headingPageSignal.value = headingPage;
|
|
40296
|
+
if (routeTravelSettledPageSignal.peek() === null) {
|
|
40297
|
+
routeTravelSettledPageSignal.value = page;
|
|
40298
|
+
}
|
|
40299
|
+
};
|
|
40300
|
+
|
|
40301
|
+
const releaseHeldPage = () => {
|
|
40302
|
+
routeTravelSettledPageSignal.value = null;
|
|
40303
|
+
headingPageSignal.value = null;
|
|
40304
|
+
};
|
|
40305
|
+
|
|
40306
|
+
/**
|
|
40307
|
+
* What a link to `route` with `params` answers because a hand holds a row.
|
|
40308
|
+
*
|
|
40309
|
+
* @returns {boolean|undefined} `undefined` when the hold says nothing about this
|
|
40310
|
+
* link, and the address answers.
|
|
40311
|
+
*/
|
|
40312
|
+
const readHeldLinkCurrent = (route, params) => {
|
|
40313
|
+
const settledPage = routeTravelSettledPageSignal.value;
|
|
40314
|
+
const headingPage = headingPageSignal.value;
|
|
40315
|
+
if (!settledPage) {
|
|
40316
|
+
return undefined;
|
|
40317
|
+
}
|
|
40318
|
+
// By address: a link may name fewer params than the page it leads to.
|
|
40319
|
+
const leadsTo = (page) =>
|
|
40320
|
+
page.route === route &&
|
|
40321
|
+
route.buildUrl(params) === route.buildUrl(page.params);
|
|
40322
|
+
if (leadsTo(settledPage)) {
|
|
40323
|
+
return true;
|
|
40324
|
+
}
|
|
40325
|
+
if (leadsTo(headingPage)) {
|
|
40326
|
+
return false;
|
|
40327
|
+
}
|
|
40328
|
+
return undefined;
|
|
40329
|
+
};
|
|
40330
|
+
|
|
40264
40331
|
/**
|
|
40265
40332
|
* A row of pages the browser scrolls itself, and what is left to read from it.
|
|
40266
40333
|
*
|
|
@@ -40349,12 +40416,21 @@ const readRowStretch = (scroller, pages, page) => {
|
|
|
40349
40416
|
* @param {(event: Event|null) => void} options.onLand - the row may have stopped.
|
|
40350
40417
|
* Read `pageAtRest` to know whether it stopped on a page. Heard more than once
|
|
40351
40418
|
* for one landing, so what it does must find nothing to do the second time.
|
|
40419
|
+
* @param {(forward: boolean) => void} [options.onLeftToSnap] - a finger has lifted
|
|
40420
|
+
* and the row moves: snapping plays out what the browser has decided and does not
|
|
40421
|
+
* turn round, so this is where it is going. Heard once per lift, after `onScroll`.
|
|
40422
|
+
* A trackpad has no lift, and never says this.
|
|
40352
40423
|
* @returns {() => void} stop listening.
|
|
40353
40424
|
*/
|
|
40354
|
-
const watchRowScroll = (
|
|
40425
|
+
const watchRowScroll = (
|
|
40426
|
+
scroller,
|
|
40427
|
+
{ onScroll, onLand, onLeftToSnap },
|
|
40428
|
+
) => {
|
|
40355
40429
|
let stoppedTimeout = null;
|
|
40356
40430
|
let touching = false;
|
|
40431
|
+
let liftedAt = null;
|
|
40357
40432
|
const landOnSilence = () => {
|
|
40433
|
+
liftedAt = null;
|
|
40358
40434
|
// A finger holding the row still: nothing has stopped.
|
|
40359
40435
|
if (!touching) {
|
|
40360
40436
|
onLand(null);
|
|
@@ -40367,14 +40443,27 @@ const watchRowScroll = (scroller, { onScroll, onLand }) => {
|
|
|
40367
40443
|
const onScrollEvent = () => {
|
|
40368
40444
|
onScroll();
|
|
40369
40445
|
armSilence();
|
|
40446
|
+
if (!liftedAt) {
|
|
40447
|
+
return;
|
|
40448
|
+
}
|
|
40449
|
+
// A map moves along one axis at a time, so the two add up to the one it moves on.
|
|
40450
|
+
const moved =
|
|
40451
|
+
scroller.scrollLeft - liftedAt.left + (scroller.scrollTop - liftedAt.top);
|
|
40452
|
+
if (Math.abs(moved) < AT_REST_DISTANCE) {
|
|
40453
|
+
return;
|
|
40454
|
+
}
|
|
40455
|
+
liftedAt = null;
|
|
40456
|
+
onLeftToSnap?.(moved > 0);
|
|
40370
40457
|
};
|
|
40371
40458
|
const onTouchStart = () => {
|
|
40372
40459
|
touching = true;
|
|
40460
|
+
liftedAt = null;
|
|
40373
40461
|
clearTimeout(stoppedTimeout);
|
|
40374
40462
|
};
|
|
40375
40463
|
const onTouchEnd = (touchEvent) => {
|
|
40376
40464
|
touching = touchEvent.touches.length > 0;
|
|
40377
40465
|
if (!touching) {
|
|
40466
|
+
liftedAt = { left: scroller.scrollLeft, top: scroller.scrollTop };
|
|
40378
40467
|
armSilence();
|
|
40379
40468
|
}
|
|
40380
40469
|
};
|
|
@@ -40473,19 +40562,6 @@ const css$16 = /* css */`.navi_route_travel {
|
|
|
40473
40562
|
// neither a navigation nor the requests of a page.
|
|
40474
40563
|
const SET_OFF_PULL = 16;
|
|
40475
40564
|
|
|
40476
|
-
/**
|
|
40477
|
-
* What the URL says while a hand holds the row is where it is heading, for a page
|
|
40478
|
-
* not built yet: it builds by the address moving. A counter, a title or a
|
|
40479
|
-
* breadcrumb around the box must not move for a pull that is let go of: it reads
|
|
40480
|
-
* this first. The page the reader is still on while that lasts, and `null` when
|
|
40481
|
-
* the row has come to rest, which is when `scrollsnapchange` fires and a pager's
|
|
40482
|
-
* `settledPage` changes. A travel no hand set off holds nothing: it goes where it
|
|
40483
|
-
* goes.
|
|
40484
|
-
*
|
|
40485
|
-
* One for the document. `route` says which row, for a page holding several boxes.
|
|
40486
|
-
*/
|
|
40487
|
-
const routeTravelSettledPageSignal = signal(null);
|
|
40488
|
-
|
|
40489
40565
|
/**
|
|
40490
40566
|
* @type {import("preact").FunctionComponent<{
|
|
40491
40567
|
* routes?: Array<object|{route: object, params?: object}>,
|
|
@@ -40631,6 +40707,33 @@ const RouteTravel = ({
|
|
|
40631
40707
|
cause: "drag"
|
|
40632
40708
|
});
|
|
40633
40709
|
};
|
|
40710
|
+
|
|
40711
|
+
// The page the row goes to, once nothing is left to the hand.
|
|
40712
|
+
const askForPageOfRow = (index, movement) => {
|
|
40713
|
+
const row = rowRef.current;
|
|
40714
|
+
const page = latestRef.current.pages[index];
|
|
40715
|
+
if (windowed && !movement.pictures.has(index)) {
|
|
40716
|
+
leavePictureOfLive(movement);
|
|
40717
|
+
}
|
|
40718
|
+
Promise.resolve(askFor(page)).then(() => {
|
|
40719
|
+
if (pageIsCurrent(page)) {
|
|
40720
|
+
return;
|
|
40721
|
+
}
|
|
40722
|
+
// Refused, or sent elsewhere: the row says where the address is.
|
|
40723
|
+
askedPageRef.current = null;
|
|
40724
|
+
const {
|
|
40725
|
+
currentIndex
|
|
40726
|
+
} = latestRef.current;
|
|
40727
|
+
const paneCurrent = panesOf(row)[currentIndex];
|
|
40728
|
+
if (paneCurrent) {
|
|
40729
|
+
movement.travelling = true;
|
|
40730
|
+
row.scrollTo({
|
|
40731
|
+
left: paneCurrent.offsetLeft,
|
|
40732
|
+
behavior: "smooth"
|
|
40733
|
+
});
|
|
40734
|
+
}
|
|
40735
|
+
});
|
|
40736
|
+
};
|
|
40634
40737
|
const sayRowMoves = () => {
|
|
40635
40738
|
const row = rowRef.current;
|
|
40636
40739
|
const panes = panesOf(row);
|
|
@@ -40663,7 +40766,7 @@ const RouteTravel = ({
|
|
|
40663
40766
|
movementRef.current = null;
|
|
40664
40767
|
row.removeAttribute("data-moving");
|
|
40665
40768
|
publishRouteRowMove(null);
|
|
40666
|
-
|
|
40769
|
+
releaseHeldPage();
|
|
40667
40770
|
if (liveRef.current) {
|
|
40668
40771
|
liveRef.current.style.top = "";
|
|
40669
40772
|
}
|
|
@@ -40809,11 +40912,38 @@ const RouteTravel = ({
|
|
|
40809
40912
|
} else if (paneTowards.hasAttribute("data-built")) {
|
|
40810
40913
|
return;
|
|
40811
40914
|
}
|
|
40812
|
-
|
|
40813
|
-
routeTravelSettledPageSignal.value = pages[currentIndex];
|
|
40814
|
-
}
|
|
40915
|
+
holdPage(pages[currentIndex], page);
|
|
40815
40916
|
askFor(page);
|
|
40816
40917
|
};
|
|
40918
|
+
// The address, the tabs and what reads the held page all move here, the way they
|
|
40919
|
+
// do for a tab pressed. If the row ends up elsewhere after all, onLand corrects
|
|
40920
|
+
// the address when it rests.
|
|
40921
|
+
const onLeftToSnap = forward => {
|
|
40922
|
+
const movement = movementRef.current;
|
|
40923
|
+
const {
|
|
40924
|
+
pages,
|
|
40925
|
+
travelByDrag
|
|
40926
|
+
} = latestRef.current;
|
|
40927
|
+
if (!movement || movement.travelling || !travelByDrag) {
|
|
40928
|
+
return;
|
|
40929
|
+
}
|
|
40930
|
+
const panes = panesOf(row);
|
|
40931
|
+
const {
|
|
40932
|
+
base,
|
|
40933
|
+
toward
|
|
40934
|
+
} = readRowStretch(row, panes, panes[0]);
|
|
40935
|
+
const index = panes.indexOf(forward && toward ? toward : base);
|
|
40936
|
+
const page = pages[index];
|
|
40937
|
+
if (!page) {
|
|
40938
|
+
return;
|
|
40939
|
+
}
|
|
40940
|
+
if (!pageIsCurrent(page) && !samePage(askedPageRef.current, page)) {
|
|
40941
|
+
askForPageOfRow(index, movement);
|
|
40942
|
+
}
|
|
40943
|
+
// After the address: a pull coming back to its page must not light the tab the
|
|
40944
|
+
// address ran ahead to, even for a render.
|
|
40945
|
+
releaseHeldPage();
|
|
40946
|
+
};
|
|
40817
40947
|
const onLand = () => {
|
|
40818
40948
|
const movement = movementRef.current;
|
|
40819
40949
|
if (!movement) {
|
|
@@ -40838,37 +40968,19 @@ const RouteTravel = ({
|
|
|
40838
40968
|
if (samePage(askedPageRef.current, page)) {
|
|
40839
40969
|
return;
|
|
40840
40970
|
}
|
|
40841
|
-
|
|
40842
|
-
leavePictureOfLive(movement);
|
|
40843
|
-
}
|
|
40844
|
-
Promise.resolve(askFor(page)).then(() => {
|
|
40845
|
-
if (pageIsCurrent(page)) {
|
|
40846
|
-
return;
|
|
40847
|
-
}
|
|
40848
|
-
// Refused, or sent elsewhere: the row says where the address is.
|
|
40849
|
-
askedPageRef.current = null;
|
|
40850
|
-
const {
|
|
40851
|
-
currentIndex
|
|
40852
|
-
} = latestRef.current;
|
|
40853
|
-
const paneCurrent = panesOf(row)[currentIndex];
|
|
40854
|
-
if (paneCurrent) {
|
|
40855
|
-
movement.travelling = true;
|
|
40856
|
-
row.scrollTo({
|
|
40857
|
-
left: paneCurrent.offsetLeft,
|
|
40858
|
-
behavior: "smooth"
|
|
40859
|
-
});
|
|
40860
|
-
}
|
|
40861
|
-
});
|
|
40971
|
+
askForPageOfRow(panes.indexOf(pane), movement);
|
|
40862
40972
|
};
|
|
40863
40973
|
const stopWatching = watchRowScroll(row, {
|
|
40864
40974
|
onScroll,
|
|
40865
|
-
onLand
|
|
40975
|
+
onLand,
|
|
40976
|
+
onLeftToSnap
|
|
40866
40977
|
});
|
|
40867
40978
|
return () => {
|
|
40868
40979
|
stopWatching();
|
|
40869
40980
|
movementRef.current?.resumeScrollRecording?.();
|
|
40870
40981
|
movementRef.current = null;
|
|
40871
40982
|
publishRouteRowMove(null);
|
|
40983
|
+
releaseHeldPage();
|
|
40872
40984
|
};
|
|
40873
40985
|
}, [windowed]);
|
|
40874
40986
|
|
|
@@ -55315,7 +55427,8 @@ const NavContext = createContext(null);
|
|
|
55315
55427
|
|
|
55316
55428
|
/**
|
|
55317
55429
|
* Whether the page one is on is `route` with `params` — what a link or a button
|
|
55318
|
-
* pointing at a route shows as current.
|
|
55430
|
+
* pointing at a route shows as current. While a hand holds a row of routes, the
|
|
55431
|
+
* page one is on is the one held, wherever the address has run ahead to.
|
|
55319
55432
|
*
|
|
55320
55433
|
* Read through a computed of that one answer, so the component reading it
|
|
55321
55434
|
* re-renders when the answer flips, and not whenever the route's status moves:
|
|
@@ -55367,6 +55480,10 @@ const readRouteCurrent = (route, params, exceptRoutes, alsoRoutes) => {
|
|
|
55367
55480
|
someAlsoMatching = true;
|
|
55368
55481
|
}
|
|
55369
55482
|
}
|
|
55483
|
+
const heldAnswer = readHeldLinkCurrent(route, params);
|
|
55484
|
+
if (heldAnswer !== undefined) {
|
|
55485
|
+
return heldAnswer;
|
|
55486
|
+
}
|
|
55370
55487
|
if (someExceptMatching) {
|
|
55371
55488
|
return false;
|
|
55372
55489
|
}
|
|
@@ -73427,72 +73544,73 @@ const popupCss = /* css */ `
|
|
|
73427
73544
|
`;
|
|
73428
73545
|
|
|
73429
73546
|
/**
|
|
73430
|
-
* A
|
|
73431
|
-
*
|
|
73432
|
-
*
|
|
73433
|
-
*
|
|
73434
|
-
* Two things make an open
|
|
73435
|
-
*
|
|
73436
|
-
* - its ceiling (--dialog-maxmax-height), which CSS keeps
|
|
73437
|
-
* --navi-app-height. Written back resolved, inline. `freezeSize` cannot
|
|
73438
|
-
* a max-* wins over the width/height it writes;
|
|
73547
|
+
* A popup covered by a modal dialog is not in use, so it does not answer the
|
|
73548
|
+
* keyboard: a sheet raising one leaves what is behind it where it is, under the
|
|
73549
|
+
* keyboard like anything else behind that sheet.
|
|
73550
|
+
*
|
|
73551
|
+
* Two things make an open popup follow the keyboard, and both are held here for
|
|
73552
|
+
* as long as it is covered:
|
|
73553
|
+
* - its ceiling (--dialog-maxmax-height, --popover-maxmax-height), which CSS keeps
|
|
73554
|
+
* tracking --navi-app-height. Written back resolved, inline. `freezeSize` cannot
|
|
73555
|
+
* do it: a max-* wins over the width/height it writes;
|
|
73439
73556
|
* - its placement, which reads the live viewport. Deferred, see
|
|
73440
73557
|
* deferPlacementWhileCovered.
|
|
73441
73558
|
*
|
|
73442
|
-
* A
|
|
73443
|
-
* a keyboard on its way out. It is still counted for a while after the
|
|
73444
|
-
* closes (the browser takes its time, then --navi-vvh waits for the resize
|
|
73445
|
-
* settle, see responsive.js), and answering that makes the
|
|
73559
|
+
* A popup uncovered answers the screen as it is, at once, with one
|
|
73560
|
+
* exception: a keyboard on its way out. It is still counted for a while after the
|
|
73561
|
+
* sheet closes (the browser takes its time, then --navi-vvh waits for the resize
|
|
73562
|
+
* to settle, see responsive.js), and answering that makes the popup jump up and
|
|
73446
73563
|
* back down. A keyboard belongs to a focused field, so the focus says which case
|
|
73447
73564
|
* this is: on a field the keyboard stays, anywhere else it is leaving and the
|
|
73448
|
-
*
|
|
73449
|
-
*
|
|
73565
|
+
* popup waits for the viewport to change. No timer: how long a keyboard takes to
|
|
73566
|
+
* leave is unknown, and a duration would also delay a keyboard that stays.
|
|
73450
73567
|
*
|
|
73451
|
-
*
|
|
73452
|
-
* usable, and keeps following the keyboard.
|
|
73568
|
+
* Modal dialogs hold what is below them. What sits under a non-modal one is
|
|
73569
|
+
* still usable, and keeps following the keyboard. A top-layer popover can be
|
|
73570
|
+
* held and holds nothing: it follows its anchor, and that anchor may be inside
|
|
73571
|
+
* the popup below, whose rising is what carries a field in the popover above the
|
|
73572
|
+
* keyboard.
|
|
73453
73573
|
*/
|
|
73454
73574
|
|
|
73455
73575
|
|
|
73456
|
-
const
|
|
73457
|
-
|
|
73458
|
-
const modalStack = [];
|
|
73576
|
+
const coverStack = [];
|
|
73459
73577
|
|
|
73460
73578
|
/**
|
|
73461
|
-
* @param {
|
|
73579
|
+
* @param {HTMLElement} popupEl - The popup being shown.
|
|
73462
73580
|
* @param {object} params
|
|
73463
|
-
* @param {
|
|
73464
|
-
*
|
|
73465
|
-
* @
|
|
73581
|
+
* @param {string} params.ceiling - The custom property capping its height.
|
|
73582
|
+
* @param {boolean} params.holdsBelow - Whether it covers what is below it.
|
|
73583
|
+
* @param {() => void} params.place - Places the popup against the viewport as
|
|
73584
|
+
* it is. Called on a popup that was held once it answers the viewport again.
|
|
73585
|
+
* @returns {() => void} To call when the popup closes.
|
|
73466
73586
|
*/
|
|
73467
|
-
const
|
|
73468
|
-
const entry = {
|
|
73469
|
-
|
|
73470
|
-
|
|
73471
|
-
|
|
73587
|
+
const enterCoverStack = (popupEl, { ceiling, holdsBelow, place }) => {
|
|
73588
|
+
const entry = { popupEl, ceiling, holdsBelow, place, hold: null };
|
|
73589
|
+
if (holdsBelow) {
|
|
73590
|
+
// All of them: one under a popover was not held when that popover opened.
|
|
73591
|
+
for (const entryBelow of coverStack) {
|
|
73592
|
+
holdEntry(entryBelow);
|
|
73593
|
+
}
|
|
73472
73594
|
}
|
|
73473
|
-
|
|
73595
|
+
coverStack.push(entry);
|
|
73474
73596
|
return () => {
|
|
73475
|
-
|
|
73476
|
-
const wasOnTop = index === modalStack.length - 1;
|
|
73477
|
-
modalStack.splice(index, 1);
|
|
73597
|
+
coverStack.splice(coverStack.indexOf(entry), 1);
|
|
73478
73598
|
dropHold(entry);
|
|
73479
|
-
|
|
73480
|
-
releaseBackOnTop(modalStack.at(-1), { closedDialogEl: dialogEl });
|
|
73481
|
-
}
|
|
73599
|
+
releaseUncovered({ closedPopupEl: popupEl });
|
|
73482
73600
|
};
|
|
73483
73601
|
};
|
|
73484
73602
|
|
|
73485
73603
|
/**
|
|
73486
|
-
* Answers true when the
|
|
73604
|
+
* Answers true when the popup must not be placed right now, and remembers to
|
|
73487
73605
|
* place it on release.
|
|
73488
73606
|
*
|
|
73489
73607
|
* Only while the room differs from the one it was covered with: the placement
|
|
73490
73608
|
* reads the live viewport whatever triggered it, so with the same room it lands
|
|
73491
|
-
* where the
|
|
73609
|
+
* where the popup already is, and content growing under the sheet is answered
|
|
73492
73610
|
* as usual.
|
|
73493
73611
|
*/
|
|
73494
|
-
const deferPlacementWhileCovered = (
|
|
73495
|
-
const entry =
|
|
73612
|
+
const deferPlacementWhileCovered = (popupEl) => {
|
|
73613
|
+
const entry = coverStack.find((candidate) => candidate.popupEl === popupEl);
|
|
73496
73614
|
if (!entry || !entry.hold) {
|
|
73497
73615
|
return false;
|
|
73498
73616
|
}
|
|
@@ -73511,35 +73629,63 @@ const holdEntry = (entry) => {
|
|
|
73511
73629
|
entry.hold.stopWaiting = null;
|
|
73512
73630
|
return;
|
|
73513
73631
|
}
|
|
73514
|
-
const {
|
|
73515
|
-
holdCeiling(
|
|
73632
|
+
const { place } = entry;
|
|
73633
|
+
holdCeiling(entry);
|
|
73516
73634
|
entry.hold = {
|
|
73517
73635
|
room: readRoom(),
|
|
73518
73636
|
placementOwed: false,
|
|
73519
73637
|
stopWaiting: null,
|
|
73520
|
-
|
|
73521
|
-
|
|
73522
|
-
|
|
73523
|
-
|
|
73524
|
-
|
|
73525
|
-
|
|
73638
|
+
windowWidth: window.innerWidth,
|
|
73639
|
+
windowHeight: window.innerHeight,
|
|
73640
|
+
// A rotation, which a covered dialog has to fit, is told from a keyboard by
|
|
73641
|
+
// the window's size: the keyboard leaves it alone (see safe_area.md). Read
|
|
73642
|
+
// once the visual viewport settles, not on the window's own resize: that one
|
|
73643
|
+
// can fire first, while --navi-vvh still describes the screen being left.
|
|
73644
|
+
stopWatchingWindow: subscribeVisualViewportResizeSettled(() => {
|
|
73645
|
+
const { hold } = entry;
|
|
73646
|
+
if (
|
|
73647
|
+
window.innerWidth === hold.windowWidth &&
|
|
73648
|
+
window.innerHeight === hold.windowHeight
|
|
73649
|
+
) {
|
|
73650
|
+
return;
|
|
73651
|
+
}
|
|
73652
|
+
hold.windowWidth = window.innerWidth;
|
|
73653
|
+
hold.windowHeight = window.innerHeight;
|
|
73654
|
+
holdCeiling(entry);
|
|
73655
|
+
hold.room = readRoom();
|
|
73656
|
+
hold.placementOwed = false;
|
|
73526
73657
|
place();
|
|
73527
73658
|
}),
|
|
73528
73659
|
};
|
|
73529
73660
|
};
|
|
73530
73661
|
|
|
73531
|
-
|
|
73662
|
+
// From the top down to the first popup that holds what is below it: nothing
|
|
73663
|
+
// above covers any of these.
|
|
73664
|
+
const releaseUncovered = ({ closedPopupEl }) => {
|
|
73665
|
+
for (let i = coverStack.length - 1; i >= 0; i--) {
|
|
73666
|
+
const entry = coverStack[i];
|
|
73667
|
+
releaseBackOnTop(entry, { closedPopupEl });
|
|
73668
|
+
if (entry.holdsBelow) {
|
|
73669
|
+
return;
|
|
73670
|
+
}
|
|
73671
|
+
}
|
|
73672
|
+
};
|
|
73673
|
+
|
|
73674
|
+
const releaseBackOnTop = (entry, { closedPopupEl }) => {
|
|
73532
73675
|
const { hold } = entry;
|
|
73533
73676
|
if (!hold) {
|
|
73534
73677
|
return;
|
|
73535
73678
|
}
|
|
73679
|
+
// Already waiting when a popover above it closes in turn: this close asks again.
|
|
73680
|
+
hold.stopWaiting?.();
|
|
73681
|
+
hold.stopWaiting = null;
|
|
73536
73682
|
// Nothing taken away, or a keyboard that stays: the room read here is the
|
|
73537
|
-
// screen as it is. A field in the
|
|
73538
|
-
//
|
|
73683
|
+
// screen as it is. A field in the popup that closed is one still leaving: a
|
|
73684
|
+
// popup leaving the tree only gives the focus back after the commit.
|
|
73539
73685
|
const { activeElement } = document;
|
|
73540
73686
|
if (
|
|
73541
73687
|
readRoom() >= hold.room ||
|
|
73542
|
-
(isEditableTarget(activeElement) && !
|
|
73688
|
+
(isEditableTarget(activeElement) && !closedPopupEl.contains(activeElement))
|
|
73543
73689
|
) {
|
|
73544
73690
|
release(entry);
|
|
73545
73691
|
return;
|
|
@@ -73585,25 +73731,25 @@ const release = (entry) => {
|
|
|
73585
73731
|
};
|
|
73586
73732
|
|
|
73587
73733
|
const dropHold = (entry) => {
|
|
73588
|
-
const { hold,
|
|
73734
|
+
const { hold, popupEl, ceiling } = entry;
|
|
73589
73735
|
if (!hold) {
|
|
73590
73736
|
return;
|
|
73591
73737
|
}
|
|
73592
73738
|
hold.stopWaiting?.();
|
|
73593
73739
|
hold.stopWatchingWindow();
|
|
73594
|
-
|
|
73740
|
+
popupEl.style.removeProperty(ceiling);
|
|
73595
73741
|
entry.hold = null;
|
|
73596
73742
|
};
|
|
73597
73743
|
|
|
73598
73744
|
// The declared ceiling rather than the used max-height: it is the one cap that
|
|
73599
73745
|
// follows the keyboard, and the other caps (maxHeight) keep their say. Its
|
|
73600
73746
|
// computed value has every var() resolved, so it stops following once written
|
|
73601
|
-
// back. Registered with inherits: false, it does not reach a
|
|
73602
|
-
//
|
|
73603
|
-
const holdCeiling = (
|
|
73604
|
-
|
|
73605
|
-
const
|
|
73606
|
-
|
|
73747
|
+
// back. Registered with inherits: false, it does not reach a popup nested inside
|
|
73748
|
+
// this one.
|
|
73749
|
+
const holdCeiling = ({ popupEl, ceiling }) => {
|
|
73750
|
+
popupEl.style.removeProperty(ceiling);
|
|
73751
|
+
const value = getComputedStyle(popupEl).getPropertyValue(ceiling);
|
|
73752
|
+
popupEl.style.setProperty(ceiling, value);
|
|
73607
73753
|
};
|
|
73608
73754
|
|
|
73609
73755
|
// An overlaying keyboard leaves the viewport alone and arrives on its own.
|
|
@@ -75974,7 +76120,9 @@ const useDialogProps = props => {
|
|
|
75974
76120
|
}
|
|
75975
76121
|
rectEffect.observeSize(dialogEl);
|
|
75976
76122
|
if (isModal) {
|
|
75977
|
-
addCleanup(
|
|
76123
|
+
addCleanup(enterCoverStack(dialogEl, {
|
|
76124
|
+
ceiling: "--dialog-maxmax-height",
|
|
76125
|
+
holdsBelow: true,
|
|
75978
76126
|
// "resize": what it was held from is a viewport change, and it answers
|
|
75979
76127
|
// the way it answers one, animated. Called directly because the
|
|
75980
76128
|
// effect's dedup already saw that viewport go by.
|
|
@@ -76525,6 +76673,10 @@ const css$C = /* css */`
|
|
|
76525
76673
|
syntax: "*";
|
|
76526
76674
|
inherits: false;
|
|
76527
76675
|
}
|
|
76676
|
+
@property --popover-maxmax-height {
|
|
76677
|
+
syntax: "*";
|
|
76678
|
+
inherits: false;
|
|
76679
|
+
}
|
|
76528
76680
|
@property --x-popover-max-width {
|
|
76529
76681
|
syntax: "*";
|
|
76530
76682
|
inherits: false;
|
|
@@ -77886,27 +78038,37 @@ const usePopoverProps = props => {
|
|
|
77886
78038
|
popoverEl.showPopover();
|
|
77887
78039
|
}));
|
|
77888
78040
|
}
|
|
77889
|
-
|
|
77890
|
-
|
|
77891
|
-
}, {
|
|
77892
|
-
event
|
|
77893
|
-
}) => {
|
|
78041
|
+
let anchorVisibilityRatio = 1;
|
|
78042
|
+
const placeUnlessAnchorOutOfView = placeEvent => {
|
|
77894
78043
|
// Only a real anchor can meaningfully go "out of view" — gating on
|
|
77895
78044
|
// document.documentElement's own visibilityRatio (used for
|
|
77896
78045
|
// anchorless/docked popups) would wrongly skip positioning on a
|
|
77897
78046
|
// tall page, since its ratio is often low even when nothing's
|
|
77898
78047
|
// hidden. hasAnchorElement is already false for the custom
|
|
77899
78048
|
// renderer, so this never triggers for it either.
|
|
77900
|
-
if (hasAnchorElement &&
|
|
78049
|
+
if (hasAnchorElement && anchorVisibilityRatio <= 0.2) {
|
|
77901
78050
|
popoverEl.setAttribute("data-anchor-out-of-view", "");
|
|
77902
|
-
return;
|
|
78051
|
+
return false;
|
|
77903
78052
|
}
|
|
77904
78053
|
popoverEl.removeAttribute("data-anchor-out-of-view");
|
|
77905
|
-
positionPopover(
|
|
78054
|
+
positionPopover(placeEvent);
|
|
78055
|
+
return true;
|
|
78056
|
+
};
|
|
78057
|
+
const rectEffect = visibleRectEffect(effectiveAnchor, ({
|
|
78058
|
+
visibilityRatio
|
|
78059
|
+
}, {
|
|
78060
|
+
event
|
|
78061
|
+
}) => {
|
|
78062
|
+
anchorVisibilityRatio = visibilityRatio;
|
|
78063
|
+
// Before the out-of-view rule: a keyboard going over the anchor, behind
|
|
78064
|
+
// the sheet that raised it, hides nothing.
|
|
78065
|
+
if (deferPlacementWhileCovered(popoverEl)) {
|
|
78066
|
+
return;
|
|
78067
|
+
}
|
|
77906
78068
|
// Same as Dialog's own — a popup with a scrolling body and a footer
|
|
77907
78069
|
// swallows the field it just got shorter around. See
|
|
77908
78070
|
// keepFocusedElementVisible's own doc in popup_shared.js.
|
|
77909
|
-
if (mayHaveHiddenFocus(event)) {
|
|
78071
|
+
if (placeUnlessAnchorOutOfView(event) && mayHaveHiddenFocus(event)) {
|
|
77910
78072
|
keepFocusedElementVisible(popoverEl);
|
|
77911
78073
|
}
|
|
77912
78074
|
}, {
|
|
@@ -77919,6 +78081,21 @@ const usePopoverProps = props => {
|
|
|
77919
78081
|
// while open (e.g. an expand/collapse toggle inside it) — not just when
|
|
77920
78082
|
// the anchor itself moves/resizes/re-anchors.
|
|
77921
78083
|
rectEffect.observeSize(popoverEl);
|
|
78084
|
+
// Whatever its own backdrop: a modal dialog above leaves all of the page
|
|
78085
|
+
// inert, a popover without a wall included. layer="local" is left out, its
|
|
78086
|
+
// ceiling answers to its container.
|
|
78087
|
+
if (isTopLayer) {
|
|
78088
|
+
addCleanup(enterCoverStack(popoverEl, {
|
|
78089
|
+
ceiling: "--popover-maxmax-height",
|
|
78090
|
+
holdsBelow: false,
|
|
78091
|
+
// "resize": see Dialog's own.
|
|
78092
|
+
place: () => {
|
|
78093
|
+
if (placeUnlessAnchorOutOfView(new CustomEvent("resize"))) {
|
|
78094
|
+
keepFocusedElementVisible(popoverEl);
|
|
78095
|
+
}
|
|
78096
|
+
}
|
|
78097
|
+
}));
|
|
78098
|
+
}
|
|
77922
78099
|
// Content that sizes itself once on screen (a shrink-wrapped BadgeList)
|
|
77923
78100
|
// says so, after the placement read it: placed again on the spot, where
|
|
77924
78101
|
// observeSize answers a frame late. Stopped here, since a popup further up
|
|
@@ -105845,10 +106022,10 @@ const css = /* css */`.navi_side_panel {
|
|
|
105845
106022
|
height: var(--navi-side-panel-height, auto);
|
|
105846
106023
|
|
|
105847
106024
|
&[data-layer="top"] {
|
|
105848
|
-
--popover-max-height: var(--navi-app-height);
|
|
105849
106025
|
--popover-maxmax-height: var(--navi-app-height);
|
|
106026
|
+
--popover-max-height: var(--popover-maxmax-height);
|
|
105850
106027
|
--popover-maxmax-width: var(--navi-app-width);
|
|
105851
|
-
--dialog-maxmax-height: var(--
|
|
106028
|
+
--dialog-maxmax-height: var(--popover-maxmax-height);
|
|
105852
106029
|
--dialog-maxmax-width: var(--navi-app-width);
|
|
105853
106030
|
|
|
105854
106031
|
&[navi-side="left"], &[navi-side="right"] {
|