@jsenv/navi 0.29.163 → 0.29.165
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/jsenv_navi.js +1667 -1308
- package/dist/jsenv_navi.js.map +14 -12
- package/docs/route_transitions.md +39 -5
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { installImportMetaCssBuild, windowHeightSignal, windowWidthSignal, visualViewportHeightSignal, visualViewportWidthSignal, getAppHeight, getAppWidth, coarsePointerSignal, smallTouchScreenSignal } from "./jsenv_navi_side_effects.js";
|
|
6
6
|
export { disableVirtualKeyboardOverlay } from "./jsenv_navi_side_effects.js";
|
|
7
|
-
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor,
|
|
7
|
+
import { elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, createPubSub, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, markDragSource, startDragTo, createEventGroupLogger, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, watchWheelTravel, scrollRoomTowards, getScrollContainer, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, scrollIntoViewScoped, initFocusGroup, isTouchDrivenEvent, stringifyStyle as stringifyStyle$1, resolveOklchLightness, contrastColor, parsePositionArea, snapToPixel, trapFocusInside, trapScrollInside, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { chainEvent, clickIsSuppressed, contrastColor, findEvent, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
10
10
|
import { isValidElement, h, Fragment, createContext, render, toChildArray, options, cloneElement } from "preact";
|
|
@@ -28064,6 +28064,21 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
28064
28064
|
* the reverse of another, so B → A can be given a movement of its own, or
|
|
28065
28065
|
* silenced with "none", by writing it (see findRelation).
|
|
28066
28066
|
*
|
|
28067
|
+
* One shape of page has no pair to write: the one whose door is in the fixed
|
|
28068
|
+
* furniture — a gear in a top bar, a "+" in a tab bar — and which is therefore
|
|
28069
|
+
* opened from every screen and closed back onto whichever one the reader was
|
|
28070
|
+
* on. The relation is real and it is a single sentence; only the `from` is
|
|
28071
|
+
* missing, so it is left out:
|
|
28072
|
+
*
|
|
28073
|
+
* defineRouteTransition(null, SETTINGS_PAGE, "cover-top");
|
|
28074
|
+
*
|
|
28075
|
+
* Arriving there plays forward from wherever, leaving plays back to wherever,
|
|
28076
|
+
* and that is what lets the back button close it the way it opened — a
|
|
28077
|
+
* traversal carries no request, and nothing has to remember per history entry
|
|
28078
|
+
* what the press that created it had asked for. It is tried after every
|
|
28079
|
+
* written pair, and it is not defineRouteDefaultTransition: a default is about
|
|
28080
|
+
* every navigation nothing was said about, this is about ONE destination.
|
|
28081
|
+
*
|
|
28067
28082
|
* The relation says WHEN something plays and which way; the transition says
|
|
28068
28083
|
* WHAT plays — a movement navi ships, or a name the application defines in its
|
|
28069
28084
|
* own CSS (see the JSDoc below). Said without one, the relation plays the
|
|
@@ -28252,7 +28267,7 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
|
|
|
28252
28267
|
}
|
|
28253
28268
|
}
|
|
28254
28269
|
|
|
28255
|
-
&[data-navi-route-transition-type="cover-
|
|
28270
|
+
&[data-navi-route-transition-type="cover-right"] {
|
|
28256
28271
|
&[data-navi-route-transition="forward"] {
|
|
28257
28272
|
--navi-route-transition-leave: navi-route-transition-still;
|
|
28258
28273
|
--navi-route-transition-enter: navi-route-transition-enter-from-end;
|
|
@@ -28261,14 +28276,22 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
|
|
|
28261
28276
|
&[data-navi-route-transition="back"] {
|
|
28262
28277
|
--navi-route-transition-leave: navi-route-transition-leave-towards-end;
|
|
28263
28278
|
--navi-route-transition-enter: navi-route-transition-still;
|
|
28279
|
+
}
|
|
28280
|
+
}
|
|
28264
28281
|
|
|
28265
|
-
|
|
28266
|
-
|
|
28267
|
-
|
|
28282
|
+
&[data-navi-route-transition-type="cover-left"] {
|
|
28283
|
+
&[data-navi-route-transition="forward"] {
|
|
28284
|
+
--navi-route-transition-leave: navi-route-transition-still;
|
|
28285
|
+
--navi-route-transition-enter: navi-route-transition-enter-from-start;
|
|
28286
|
+
}
|
|
28287
|
+
|
|
28288
|
+
&[data-navi-route-transition="back"] {
|
|
28289
|
+
--navi-route-transition-leave: navi-route-transition-leave-towards-start;
|
|
28290
|
+
--navi-route-transition-enter: navi-route-transition-still;
|
|
28268
28291
|
}
|
|
28269
28292
|
}
|
|
28270
28293
|
|
|
28271
|
-
&[data-navi-route-transition-type="cover-
|
|
28294
|
+
&[data-navi-route-transition-type="cover-bottom"] {
|
|
28272
28295
|
&[data-navi-route-transition="forward"] {
|
|
28273
28296
|
--navi-route-transition-leave: navi-route-transition-still;
|
|
28274
28297
|
--navi-route-transition-enter: navi-route-transition-enter-from-bottom;
|
|
@@ -28277,7 +28300,23 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
|
|
|
28277
28300
|
&[data-navi-route-transition="back"] {
|
|
28278
28301
|
--navi-route-transition-leave: navi-route-transition-leave-towards-bottom;
|
|
28279
28302
|
--navi-route-transition-enter: navi-route-transition-still;
|
|
28303
|
+
}
|
|
28304
|
+
}
|
|
28305
|
+
|
|
28306
|
+
&[data-navi-route-transition-type="cover-top"] {
|
|
28307
|
+
&[data-navi-route-transition="forward"] {
|
|
28308
|
+
--navi-route-transition-leave: navi-route-transition-still;
|
|
28309
|
+
--navi-route-transition-enter: navi-route-transition-enter-from-top;
|
|
28310
|
+
}
|
|
28311
|
+
|
|
28312
|
+
&[data-navi-route-transition="back"] {
|
|
28313
|
+
--navi-route-transition-leave: navi-route-transition-leave-towards-top;
|
|
28314
|
+
--navi-route-transition-enter: navi-route-transition-still;
|
|
28315
|
+
}
|
|
28316
|
+
}
|
|
28280
28317
|
|
|
28318
|
+
&[data-navi-route-transition-type="cover-right"], &[data-navi-route-transition-type="cover-left"], &[data-navi-route-transition-type="cover-bottom"], &[data-navi-route-transition-type="cover-top"] {
|
|
28319
|
+
&[data-navi-route-transition="back"] {
|
|
28281
28320
|
&::view-transition-old(root), &::view-transition-old(navi-route-transition) {
|
|
28282
28321
|
z-index: 1;
|
|
28283
28322
|
}
|
|
@@ -28300,7 +28339,7 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
|
|
|
28300
28339
|
}
|
|
28301
28340
|
}
|
|
28302
28341
|
|
|
28303
|
-
&[data-navi-route-transition-type="slide-x"], &[data-navi-route-transition-type="slide-y"], &[data-navi-route-transition-type="cover-
|
|
28342
|
+
&[data-navi-route-transition-type="slide-x"], &[data-navi-route-transition-type="slide-y"], &[data-navi-route-transition-type="cover-right"], &[data-navi-route-transition-type="cover-left"], &[data-navi-route-transition-type="cover-bottom"], &[data-navi-route-transition-type="cover-top"], &[data-navi-route-transition-type="zoom"] {
|
|
28304
28343
|
&::view-transition-old(root), &::view-transition-old(navi-route-transition) {
|
|
28305
28344
|
animation-name: var(--navi-route-transition-leave);
|
|
28306
28345
|
}
|
|
@@ -28310,7 +28349,7 @@ const css$11 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
|
|
|
28310
28349
|
}
|
|
28311
28350
|
}
|
|
28312
28351
|
|
|
28313
|
-
&[data-navi-route-transition-type="slide-x"], &[data-navi-route-transition-type="slide-y"], &[data-navi-route-transition-type="cover-
|
|
28352
|
+
&[data-navi-route-transition-type="slide-x"], &[data-navi-route-transition-type="slide-y"], &[data-navi-route-transition-type="cover-right"], &[data-navi-route-transition-type="cover-left"], &[data-navi-route-transition-type="cover-bottom"], &[data-navi-route-transition-type="cover-top"] {
|
|
28314
28353
|
&::view-transition-old(root), &::view-transition-new(root), &::view-transition-old(navi-route-transition), &::view-transition-new(navi-route-transition) {
|
|
28315
28354
|
animation-timing-function: ease;
|
|
28316
28355
|
}
|
|
@@ -28435,10 +28474,17 @@ const RouteTransitionArea = ({
|
|
|
28435
28474
|
};
|
|
28436
28475
|
|
|
28437
28476
|
/**
|
|
28438
|
-
* Declare how a pair of routes moves against each other
|
|
28439
|
-
*
|
|
28440
|
-
*
|
|
28441
|
-
*
|
|
28477
|
+
* Declare how a pair of routes moves against each other — or how one route is
|
|
28478
|
+
* entered, from wherever its door happens to be.
|
|
28479
|
+
*
|
|
28480
|
+
* @param {object|null} from - a route, or `{ route, params }` when the page is
|
|
28481
|
+
* a param of a route rather than a route of its own. `null` says the page is
|
|
28482
|
+
* reached FROM ANYWHERE: its door is in the furniture (a gear in the top
|
|
28483
|
+
* bar, a "+" in the tab bar) rather than on a screen, so there is no pair to
|
|
28484
|
+
* write it on. Arriving at `to` from any page then plays forward and leaving
|
|
28485
|
+
* it for any page plays back, which is what makes the back button close it
|
|
28486
|
+
* the way it opened. Tried last, after every written pair: a pair naming the
|
|
28487
|
+
* same destination still owns its crossing.
|
|
28442
28488
|
* @param {object} to - same forms. Going from `from` to `to` plays forward,
|
|
28443
28489
|
* the reverse plays back — unless the reverse is written as a relation of
|
|
28444
28490
|
* its own, which then owns that way (a movement of its own, or `"none"` for
|
|
@@ -28452,8 +28498,11 @@ const RouteTransitionArea = ({
|
|
|
28452
28498
|
* Omitted, the browser's own cross-fade. Shipped with navi:
|
|
28453
28499
|
* - `"slide-x"`, `"slide-y"`: the two pages slide past each other, forward
|
|
28454
28500
|
* towards the start of the axis;
|
|
28455
|
-
* - `"cover-
|
|
28456
|
-
* not move, and slides off it
|
|
28501
|
+
* - `"cover-right"`, `"cover-left"`, `"cover-bottom"`, `"cover-top"`: the
|
|
28502
|
+
* page arriving slides in OVER one that does not move, and slides off it
|
|
28503
|
+
* on the way back. The edge named is the one it comes IN from, which is
|
|
28504
|
+
* where its door is: a page reached from a bottom bar covers from the
|
|
28505
|
+
* bottom, one pulled down from a top bar covers from the top;
|
|
28457
28506
|
* - `"zoom"`: the deeper page is the closer one — it lands from slightly too
|
|
28458
28507
|
* big, and grows away when left;
|
|
28459
28508
|
* - `"none"`: nothing, said out loud — written on one way of a pair, it cuts
|
|
@@ -28484,6 +28533,9 @@ const RouteTransitionArea = ({
|
|
|
28484
28533
|
const defineRouteTransition = (from, to, transition) => {
|
|
28485
28534
|
import.meta.css = [css$11, "@jsenv/navi/src/nav/route_transition.jsx"];
|
|
28486
28535
|
installTransitionWindowCss();
|
|
28536
|
+
if (!to) {
|
|
28537
|
+
throw new TypeError(`defineRouteTransition needs a destination: "to" is ${to}. The page reached from anywhere is written defineRouteTransition(null, THAT_PAGE, ...) — there is no relation the other way round, a page LEFT for anywhere being the back half of that one.`);
|
|
28538
|
+
}
|
|
28487
28539
|
const {
|
|
28488
28540
|
type,
|
|
28489
28541
|
duration
|
|
@@ -28652,13 +28704,15 @@ const rebuildWatcher = () => {
|
|
|
28652
28704
|
}
|
|
28653
28705
|
// Every page any relation mentions, each once: the position of the current
|
|
28654
28706
|
// page in this list is what turns "some signal moved" into "the document
|
|
28655
|
-
// went from page A to page B".
|
|
28707
|
+
// went from page A to page B". A relation written from anywhere mentions one
|
|
28708
|
+
// page only — the other end is whatever the reader was on, and -1 is how it
|
|
28709
|
+
// reads here.
|
|
28656
28710
|
const pages = [];
|
|
28657
28711
|
for (const {
|
|
28658
28712
|
from,
|
|
28659
28713
|
to
|
|
28660
28714
|
} of relations) {
|
|
28661
|
-
if (pageIndexOf$1(pages, from) === -1) {
|
|
28715
|
+
if (from && pageIndexOf$1(pages, from) === -1) {
|
|
28662
28716
|
pages.push(from);
|
|
28663
28717
|
}
|
|
28664
28718
|
if (pageIndexOf$1(pages, to) === -1) {
|
|
@@ -28678,10 +28732,15 @@ const rebuildWatcher = () => {
|
|
|
28678
28732
|
firstReading = false;
|
|
28679
28733
|
return;
|
|
28680
28734
|
}
|
|
28681
|
-
if (
|
|
28735
|
+
if (fromIndex === index) {
|
|
28682
28736
|
return;
|
|
28683
28737
|
}
|
|
28684
|
-
|
|
28738
|
+
// A page in no relation at all is a real end of the crossing, not a
|
|
28739
|
+
// missing one: it is the "anywhere" a page reached from the furniture is
|
|
28740
|
+
// opened over and closed back onto (see findRelation).
|
|
28741
|
+
const fromPage = fromIndex === -1 ? null : pages[fromIndex];
|
|
28742
|
+
const toPage = index === -1 ? null : pages[index];
|
|
28743
|
+
const found = findRelation(fromPage, toPage);
|
|
28685
28744
|
if (!found && !navigationRequest) {
|
|
28686
28745
|
// No relation says anything about these two and this navigation asked
|
|
28687
28746
|
// for nothing: they are side by side, and silence is the fact — not a
|
|
@@ -28700,7 +28759,7 @@ const rebuildWatcher = () => {
|
|
|
28700
28759
|
return;
|
|
28701
28760
|
}
|
|
28702
28761
|
beginTransition({
|
|
28703
|
-
page:
|
|
28762
|
+
page: toPage,
|
|
28704
28763
|
url: navigationUrl,
|
|
28705
28764
|
// Which way it plays: what the navigation itself said first — the link
|
|
28706
28765
|
// being pressed is where the way the app is being walked is known — then
|
|
@@ -28777,13 +28836,23 @@ observeAfterRouting(() => {
|
|
|
28777
28836
|
releaseRoutingRenderingHold();
|
|
28778
28837
|
});
|
|
28779
28838
|
|
|
28780
|
-
// The exact way travelled first, over the whole registry,
|
|
28781
|
-
//
|
|
28782
|
-
//
|
|
28783
|
-
//
|
|
28784
|
-
//
|
|
28839
|
+
// The exact way travelled first, over the whole registry, then the reverses,
|
|
28840
|
+
// and last the pages written from anywhere.
|
|
28841
|
+
//
|
|
28842
|
+
// A relation written B → A owns that way, and being the reverse of one written
|
|
28843
|
+
// A → B never outranks it. This is what makes reciprocity a default rather
|
|
28844
|
+
// than a decree — write the way back to give it a movement of its own, or
|
|
28845
|
+
// "none" to silence it.
|
|
28846
|
+
//
|
|
28847
|
+
// A page reached from anywhere is tried after every pair, so a pair naming the
|
|
28848
|
+
// same destination still owns its crossing — the map, where it was drawn, is
|
|
28849
|
+
// more precise than "from wherever". Arriving is read before leaving: between
|
|
28850
|
+
// two such pages, the one being opened says what plays.
|
|
28785
28851
|
const findRelation = (fromPage, toPage) => {
|
|
28786
28852
|
for (const relation of relations) {
|
|
28853
|
+
if (!relation.from) {
|
|
28854
|
+
continue;
|
|
28855
|
+
}
|
|
28787
28856
|
if (samePage$1(relation.from, fromPage) && samePage$1(relation.to, toPage)) {
|
|
28788
28857
|
return {
|
|
28789
28858
|
direction: "forward",
|
|
@@ -28792,6 +28861,9 @@ const findRelation = (fromPage, toPage) => {
|
|
|
28792
28861
|
}
|
|
28793
28862
|
}
|
|
28794
28863
|
for (const relation of relations) {
|
|
28864
|
+
if (!relation.from) {
|
|
28865
|
+
continue;
|
|
28866
|
+
}
|
|
28795
28867
|
if (samePage$1(relation.from, toPage) && samePage$1(relation.to, fromPage)) {
|
|
28796
28868
|
return {
|
|
28797
28869
|
direction: "back",
|
|
@@ -28799,6 +28871,28 @@ const findRelation = (fromPage, toPage) => {
|
|
|
28799
28871
|
};
|
|
28800
28872
|
}
|
|
28801
28873
|
}
|
|
28874
|
+
for (const relation of relations) {
|
|
28875
|
+
if (relation.from) {
|
|
28876
|
+
continue;
|
|
28877
|
+
}
|
|
28878
|
+
if (samePage$1(relation.to, toPage)) {
|
|
28879
|
+
return {
|
|
28880
|
+
direction: "forward",
|
|
28881
|
+
relation
|
|
28882
|
+
};
|
|
28883
|
+
}
|
|
28884
|
+
}
|
|
28885
|
+
for (const relation of relations) {
|
|
28886
|
+
if (relation.from) {
|
|
28887
|
+
continue;
|
|
28888
|
+
}
|
|
28889
|
+
if (samePage$1(relation.to, fromPage)) {
|
|
28890
|
+
return {
|
|
28891
|
+
direction: "back",
|
|
28892
|
+
relation
|
|
28893
|
+
};
|
|
28894
|
+
}
|
|
28895
|
+
}
|
|
28802
28896
|
return null;
|
|
28803
28897
|
};
|
|
28804
28898
|
|
|
@@ -28906,10 +29000,11 @@ const beginTransition = ({
|
|
|
28906
29000
|
// rendered has already resolved the wait by the next line.
|
|
28907
29001
|
releaseRendering();
|
|
28908
29002
|
if (page === null) {
|
|
28909
|
-
//
|
|
28910
|
-
//
|
|
28911
|
-
//
|
|
28912
|
-
//
|
|
29003
|
+
// Which page is arriving is unknown — a default transition, or a page
|
|
29004
|
+
// reached from anywhere being left for wherever — and some navigations
|
|
29005
|
+
// render no route at all (a search param bound to a signal): waited
|
|
29006
|
+
// on, those would freeze the page until the browser gives up. The wait
|
|
29007
|
+
// is raced with a short timer instead.
|
|
28913
29008
|
await Promise.race([renderWait.rendered, waitMs(50)]);
|
|
28914
29009
|
} else if (pageIsCurrent$1(page)) {
|
|
28915
29010
|
await renderWait.rendered;
|
|
@@ -29034,10 +29129,18 @@ const armRouteRenderWait$1 = () => {
|
|
|
29034
29129
|
};
|
|
29035
29130
|
};
|
|
29036
29131
|
const waitMs = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
29037
|
-
|
|
29038
|
-
|
|
29039
|
-
|
|
29040
|
-
|
|
29132
|
+
|
|
29133
|
+
// A page written as the route itself, and the page written nowhere: `from`
|
|
29134
|
+
// may be left out, which is a relation about arriving at `to` from anywhere.
|
|
29135
|
+
const normalizePage$1 = page => {
|
|
29136
|
+
if (!page) {
|
|
29137
|
+
return null;
|
|
29138
|
+
}
|
|
29139
|
+
return page.isRoute ? {
|
|
29140
|
+
route: page,
|
|
29141
|
+
params: undefined
|
|
29142
|
+
} : page;
|
|
29143
|
+
};
|
|
29041
29144
|
|
|
29042
29145
|
// Two pages are the same page when they select the same thing, not when they
|
|
29043
29146
|
// were written by the same hand.
|
|
@@ -30594,6 +30697,117 @@ const unnameAfterTravel = element => {
|
|
|
30594
30697
|
element.style.viewTransitionName = "";
|
|
30595
30698
|
};
|
|
30596
30699
|
|
|
30700
|
+
/**
|
|
30701
|
+
* What covers the box a route movement plays in, from INSIDE the document: a
|
|
30702
|
+
* sticky row of tabs above a `<RouteTravel>`, a header pinned to the top of a
|
|
30703
|
+
* scroller. The element says it is there, this measures what it takes and
|
|
30704
|
+
* publishes it under --navi-transition-cover-* — the slot transition_window.js
|
|
30705
|
+
* declares and explains, and which nothing but the element covering the box can
|
|
30706
|
+
* fill: the safe area answers for what is pinned to the WINDOW's edges, and a
|
|
30707
|
+
* sticky row is none of those.
|
|
30708
|
+
*
|
|
30709
|
+
* The pictures of a transition are drawn in the top layer, above every z-index
|
|
30710
|
+
* of the document, so a band left uncounted here is a band the pictures slide
|
|
30711
|
+
* over instead of being cut at.
|
|
30712
|
+
*
|
|
30713
|
+
* Same shape as the room a fixed bar gives back (layout/fixed_bar/
|
|
30714
|
+
* fixed_bar_space.js): measured rather than declared, and the largest of the
|
|
30715
|
+
* elements sharing an edge rather than their sum — during a route movement the
|
|
30716
|
+
* outgoing and the incoming row are both mounted, pinned to the same edge, one
|
|
30717
|
+
* over the other. Nothing takes its layout from these variables, though, so the
|
|
30718
|
+
* write happens where it is measured, with no frame to wait for.
|
|
30719
|
+
*/
|
|
30720
|
+
|
|
30721
|
+
|
|
30722
|
+
const sizeMapByEdge = new Map();
|
|
30723
|
+
// What is currently on <html> for each edge (absent = the variable is not set).
|
|
30724
|
+
// Several elements sharing an edge means most calls compute the same largest
|
|
30725
|
+
// size again, and a size that has not changed is a style recalculation of the
|
|
30726
|
+
// whole document for nothing.
|
|
30727
|
+
const writtenValueByEdge = new Map();
|
|
30728
|
+
|
|
30729
|
+
/**
|
|
30730
|
+
* @param {import("preact").RefObject<Element>} elementRef - The element
|
|
30731
|
+
* covering the box.
|
|
30732
|
+
* @param {"top"|"right"|"bottom"|"left"} [edge="top"] - Which side of the box
|
|
30733
|
+
* it covers. Its size across that edge is what gets published; the edge's own
|
|
30734
|
+
* band (the safe area, the fixed bars) is already counted, so an element
|
|
30735
|
+
* states its own size and nothing else.
|
|
30736
|
+
*/
|
|
30737
|
+
const useTransitionCover = (elementRef, edge = "top") => {
|
|
30738
|
+
installTransitionWindowCss();
|
|
30739
|
+
|
|
30740
|
+
const measureCover = (element) => {
|
|
30741
|
+
const { width, height } = element.getBoundingClientRect();
|
|
30742
|
+
return edge === "left" || edge === "right" ? width : height;
|
|
30743
|
+
};
|
|
30744
|
+
|
|
30745
|
+
// Anything a render can change — the row's content, a size prop, a theme
|
|
30746
|
+
// variable — is measured in that same commit: a movement starting on the
|
|
30747
|
+
// render that grew the row is cut at the row as it now is.
|
|
30748
|
+
useLayoutEffect(() => {
|
|
30749
|
+
const element = elementRef.current;
|
|
30750
|
+
if (!element) {
|
|
30751
|
+
return;
|
|
30752
|
+
}
|
|
30753
|
+
writeCover(edge, element, measureCover(element));
|
|
30754
|
+
});
|
|
30755
|
+
|
|
30756
|
+
// What no render caused: a font arriving late, content coming from outside, a
|
|
30757
|
+
// rotation.
|
|
30758
|
+
useLayoutEffect(() => {
|
|
30759
|
+
const element = elementRef.current;
|
|
30760
|
+
if (!element) {
|
|
30761
|
+
return undefined;
|
|
30762
|
+
}
|
|
30763
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
30764
|
+
writeCover(edge, element, measureCover(element));
|
|
30765
|
+
});
|
|
30766
|
+
resizeObserver.observe(element);
|
|
30767
|
+
return () => {
|
|
30768
|
+
resizeObserver.disconnect();
|
|
30769
|
+
writeCover(edge, element, null);
|
|
30770
|
+
};
|
|
30771
|
+
}, [edge]);
|
|
30772
|
+
};
|
|
30773
|
+
|
|
30774
|
+
const writeCover = (edge, element, size) => {
|
|
30775
|
+
let sizeMap = sizeMapByEdge.get(edge);
|
|
30776
|
+
if (!sizeMap) {
|
|
30777
|
+
sizeMap = new Map();
|
|
30778
|
+
sizeMapByEdge.set(edge, sizeMap);
|
|
30779
|
+
}
|
|
30780
|
+
if (size === null) {
|
|
30781
|
+
sizeMap.delete(element);
|
|
30782
|
+
} else {
|
|
30783
|
+
sizeMap.set(element, size);
|
|
30784
|
+
}
|
|
30785
|
+
let largestSize = 0;
|
|
30786
|
+
for (const coverSize of sizeMap.values()) {
|
|
30787
|
+
if (coverSize > largestSize) {
|
|
30788
|
+
largestSize = coverSize;
|
|
30789
|
+
}
|
|
30790
|
+
}
|
|
30791
|
+
const property = `--navi-transition-cover-${edge}`;
|
|
30792
|
+
const { style } = document.documentElement;
|
|
30793
|
+
if (sizeMap.size === 0) {
|
|
30794
|
+
// Back to the zero the sheet declares, rather than a zero written over it:
|
|
30795
|
+
// a page with no sticky row must read what navi says, not what the last one
|
|
30796
|
+
// left behind.
|
|
30797
|
+
if (writtenValueByEdge.has(edge)) {
|
|
30798
|
+
writtenValueByEdge.delete(edge);
|
|
30799
|
+
style.removeProperty(property);
|
|
30800
|
+
}
|
|
30801
|
+
return;
|
|
30802
|
+
}
|
|
30803
|
+
const value = `${largestSize}px`;
|
|
30804
|
+
if (writtenValueByEdge.get(edge) === value) {
|
|
30805
|
+
return;
|
|
30806
|
+
}
|
|
30807
|
+
writtenValueByEdge.set(edge, value);
|
|
30808
|
+
style.setProperty(property, value);
|
|
30809
|
+
};
|
|
30810
|
+
|
|
30597
30811
|
const routeAction = (
|
|
30598
30812
|
routeOrRoutes,
|
|
30599
30813
|
action,
|
|
@@ -39632,6 +39846,12 @@ const shouldInjectSpacingBetween = (left, right) => {
|
|
|
39632
39846
|
* An invisible placeholder rendered with those styles reserves the space; the
|
|
39633
39847
|
* real visible text is layered on top via `position: absolute`.
|
|
39634
39848
|
* Best combined with `noWrap` — does not work reliably on multi-line text.
|
|
39849
|
+
* The placeholder is a SECOND copy of the children: it is `aria-hidden`, so
|
|
39850
|
+
* the accessible name is right, but the text appears twice in `textContent`
|
|
39851
|
+
* — which is what a test asserting on the element's text reads. Where the
|
|
39852
|
+
* element's text must stay single (a tab, a label a test matches on), reserve
|
|
39853
|
+
* the space some other way; `Link`'s `currentEffectBold` paints the weight by
|
|
39854
|
+
* default and comes here only when asked to (`"hold-space"`).
|
|
39635
39855
|
*
|
|
39636
39856
|
* @param {boolean} [boldStable]
|
|
39637
39857
|
* Alternative to `holdSpaceForStyle` for multi-line text. Keeps a consistent
|
|
@@ -40131,6 +40351,7 @@ installImportMetaCssBuild(import.meta);const css$X = /* css */`@layer navi {
|
|
|
40131
40351
|
--link-text-decoration-hover: var(--link-text-decoration);
|
|
40132
40352
|
--link-cursor: pointer;
|
|
40133
40353
|
--link-outline-width: 2px;
|
|
40354
|
+
--link-current-effect-bold-thickness: .02em;
|
|
40134
40355
|
--link-current-indicator-size: 2px;
|
|
40135
40356
|
--link-current-indicator-spacing: 0;
|
|
40136
40357
|
--link-current-indicator-color: var(--navi-link-current-indicator-color);
|
|
@@ -40266,7 +40487,12 @@ installImportMetaCssBuild(import.meta);const css$X = /* css */`@layer navi {
|
|
|
40266
40487
|
--x-link-color: var(--link-color-current);
|
|
40267
40488
|
}
|
|
40268
40489
|
|
|
40269
|
-
&[data-current-effect-bold] {
|
|
40490
|
+
&[data-current-effect-bold="text-shadow"] {
|
|
40491
|
+
text-shadow: var(--link-current-effect-bold-thickness) 0 0 currentColor,
|
|
40492
|
+
calc(-1 * var(--link-current-effect-bold-thickness)) 0 0 currentColor;
|
|
40493
|
+
}
|
|
40494
|
+
|
|
40495
|
+
&[data-current-effect-bold="hold-space"] {
|
|
40270
40496
|
font-weight: bold;
|
|
40271
40497
|
}
|
|
40272
40498
|
|
|
@@ -40479,8 +40705,17 @@ Object.assign(PSEUDO_CLASSES, {
|
|
|
40479
40705
|
* `"tab"` renders a tab-like affordance.
|
|
40480
40706
|
* @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - A
|
|
40481
40707
|
* bar drawn on the given edge (or bottom when `true`) while current.
|
|
40482
|
-
* @param {boolean} [props.currentEffectBold] - Bold
|
|
40483
|
-
*
|
|
40708
|
+
* @param {boolean|"text-shadow"|"hold-space"} [props.currentEffectBold] - Bold
|
|
40709
|
+
* the label while current, without its width changing — a row of tabs must
|
|
40710
|
+
* not jump when one becomes current. Which is a choice of two:
|
|
40711
|
+
* - `"text-shadow"` (what `true` means): the weight is PAINTED, the glyphs
|
|
40712
|
+
* drawn a hair to each side of themselves
|
|
40713
|
+
* (`--link-current-effect-bold-thickness`). The label stays one piece of
|
|
40714
|
+
* text, so `textContent` and anything reading it still see it once.
|
|
40715
|
+
* - `"hold-space"`: the real bold face, its width held by an invisible copy
|
|
40716
|
+
* of the label rendered bold underneath. True bold — and the label then
|
|
40717
|
+
* appears TWICE in `textContent`, which a test asserting on the element's
|
|
40718
|
+
* text reads (see `holdSpaceForStyle` in `text.jsx`).
|
|
40484
40719
|
* @param {boolean} [props.currentEffectShadow] - Inset-shadow effect while
|
|
40485
40720
|
* current (used with `variant="tab"`).
|
|
40486
40721
|
* @param {boolean|import("ignore:preact").ComponentChild} [props.startIcon] - Icon
|
|
@@ -40710,6 +40945,11 @@ const LinkPlain = props => {
|
|
|
40710
40945
|
const startIconEl = startIcon;
|
|
40711
40946
|
const endIconEl = innerEndIcon;
|
|
40712
40947
|
|
|
40948
|
+
// Bold by painting the glyphs unless asked for the bold face itself, because
|
|
40949
|
+
// that one costs a second copy of the label in the element's text (see the
|
|
40950
|
+
// css above).
|
|
40951
|
+
const currentEffectBoldTechnique = currentEffectBold === true ? "text-shadow" : currentEffectBold || undefined;
|
|
40952
|
+
|
|
40713
40953
|
// Where the bar goes: said here, or once for the whole row by the <Nav>
|
|
40714
40954
|
// around this link.
|
|
40715
40955
|
const currentIndicatorAsked = currentIndicator ?? nav?.currentIndicator;
|
|
@@ -40799,7 +41039,7 @@ const LinkPlain = props => {
|
|
|
40799
41039
|
onnavi_value: e => {
|
|
40800
41040
|
e.detail.setValue(value);
|
|
40801
41041
|
},
|
|
40802
|
-
holdSpaceForStyle:
|
|
41042
|
+
holdSpaceForStyle: currentEffectBoldTechnique === "hold-space" ? {
|
|
40803
41043
|
fontWeight: "bold"
|
|
40804
41044
|
} : undefined,
|
|
40805
41045
|
preventSpaceUnderlines: true
|
|
@@ -40813,7 +41053,7 @@ const LinkPlain = props => {
|
|
|
40813
41053
|
,
|
|
40814
41054
|
|
|
40815
41055
|
"data-variant": variant,
|
|
40816
|
-
"data-current-effect-bold":
|
|
41056
|
+
"data-current-effect-bold": currentEffectBoldTechnique,
|
|
40817
41057
|
"data-current-effect-shadow": currentEffectShadow ? "" : undefined,
|
|
40818
41058
|
"data-current-indicator-position": currentIndicatorPosition,
|
|
40819
41059
|
"data-anchor": anchor ? "" : undefined,
|
|
@@ -41051,6 +41291,10 @@ const css$W = /* css */`@layer navi {
|
|
|
41051
41291
|
--nav-current-indicator-size: 2px;
|
|
41052
41292
|
--nav-current-indicator-color: var(--navi-link-current-indicator-color);
|
|
41053
41293
|
}
|
|
41294
|
+
|
|
41295
|
+
.navi_nav .navi_link {
|
|
41296
|
+
--link-current-indicator-color: var(--nav-current-indicator-color);
|
|
41297
|
+
}
|
|
41054
41298
|
}
|
|
41055
41299
|
|
|
41056
41300
|
.navi_nav[data-nav-indicator] {
|
|
@@ -41061,8 +41305,14 @@ const css$W = /* css */`@layer navi {
|
|
|
41061
41305
|
var(--nav-indicator-position-delta));
|
|
41062
41306
|
--x-nav-indicator-length: calc(var(--nav-indicator-length) + var(--slide-travel-progress) *
|
|
41063
41307
|
var(--nav-indicator-length-delta));
|
|
41308
|
+
--x-nav-indicator-color: var(--nav-indicator-color, var(--nav-current-indicator-color));
|
|
41309
|
+
--x-nav-indicator-color-toward: var(--nav-indicator-color-toward, var(--x-nav-indicator-color));
|
|
41064
41310
|
z-index: 1;
|
|
41065
|
-
background:
|
|
41311
|
+
background: color-mix(in srgb,
|
|
41312
|
+
var(--x-nav-indicator-color-toward)
|
|
41313
|
+
calc(var(--slide-travel-progress) * var(--nav-indicator-toward-sign, 0) *
|
|
41314
|
+
100%),
|
|
41315
|
+
var(--x-nav-indicator-color));
|
|
41066
41316
|
pointer-events: none;
|
|
41067
41317
|
border-radius: .1px;
|
|
41068
41318
|
position: absolute;
|
|
@@ -41127,6 +41377,11 @@ const css$W = /* css */`@layer navi {
|
|
|
41127
41377
|
|
|
41128
41378
|
&[data-scrollable] {
|
|
41129
41379
|
max-width: 100%;
|
|
41380
|
+
|
|
41381
|
+
& .navi_link {
|
|
41382
|
+
white-space: nowrap;
|
|
41383
|
+
flex: none;
|
|
41384
|
+
}
|
|
41130
41385
|
}
|
|
41131
41386
|
|
|
41132
41387
|
&[data-scrollable][data-vertical] {
|
|
@@ -41162,13 +41417,13 @@ const css$W = /* css */`@layer navi {
|
|
|
41162
41417
|
&[data-expand-x] {
|
|
41163
41418
|
flex-grow: 1;
|
|
41164
41419
|
|
|
41165
|
-
&:not([data-vertical]) .navi_link {
|
|
41420
|
+
&:not([data-vertical]):not([data-scrollable]) .navi_link {
|
|
41166
41421
|
flex: 1;
|
|
41167
41422
|
justify-content: center;
|
|
41168
41423
|
}
|
|
41169
41424
|
}
|
|
41170
41425
|
|
|
41171
|
-
&[data-expand-y][data-vertical] {
|
|
41426
|
+
&[data-expand-y][data-vertical]:not([data-scrollable]) {
|
|
41172
41427
|
& .navi_link {
|
|
41173
41428
|
flex: 1;
|
|
41174
41429
|
}
|
|
@@ -41256,6 +41511,10 @@ const positionOfCurrentIndicator = (currentIndicator, vertical) => {
|
|
|
41256
41511
|
* @param {boolean|"top"|"bottom"|"left"|"right"} [props.currentIndicator] - the
|
|
41257
41512
|
* bar that says which tab one is on, said once here rather than on every
|
|
41258
41513
|
* `<Link>`. A link may still say otherwise for itself.
|
|
41514
|
+
* Its colour belongs to the tab it is under: `currentIndicatorColor` here is
|
|
41515
|
+
* what every tab inherits, and a tab declaring
|
|
41516
|
+
* `--nav-current-indicator-color` for itself is arrived at in its own colour
|
|
41517
|
+
* — the bar changes colour as it travels rather than once it lands.
|
|
41259
41518
|
* @param {boolean} [props.currentIndicatorSlides=true] - whether that bar
|
|
41260
41519
|
* travels from the tab it was under to the tab it is under now, instead of
|
|
41261
41520
|
* going out on one and coming back on the other. For a nav made of routes it
|
|
@@ -41334,11 +41593,11 @@ const Nav = ({
|
|
|
41334
41593
|
return navElement.querySelector(`[data-slide-target="${CSS.escape(towardArea)}"]`);
|
|
41335
41594
|
};
|
|
41336
41595
|
|
|
41337
|
-
// Where the trait is
|
|
41338
|
-
// CSS above interpolates between (see the .navi_nav_indicator
|
|
41339
|
-
// by hand rather than rendered: it is read off the row as it
|
|
41340
|
-
// travel it must agree with starts in the same frame the
|
|
41341
|
-
// it — a render would land after the movement had begun.
|
|
41596
|
+
// Where the trait is, where it is headed, and in which colour at each end —
|
|
41597
|
+
// the numbers the CSS above interpolates between (see the .navi_nav_indicator
|
|
41598
|
+
// rules). Written by hand rather than rendered: it is read off the row as it
|
|
41599
|
+
// stands, and the travel it must agree with starts in the same frame the
|
|
41600
|
+
// container publishes it — a render would land after the movement had begun.
|
|
41342
41601
|
const paintIndicatorGeometry = () => {
|
|
41343
41602
|
const navElement = navRef.current;
|
|
41344
41603
|
const containerElement = slideContainerElementRef.current;
|
|
@@ -41360,13 +41619,29 @@ const Nav = ({
|
|
|
41360
41619
|
position: tabElement.offsetLeft,
|
|
41361
41620
|
length: tabElement.offsetWidth
|
|
41362
41621
|
};
|
|
41622
|
+
// The colour a tab asks the bar to be under it. Read from the tab and not
|
|
41623
|
+
// from the row, so a tab that says nothing gives the row's value back —
|
|
41624
|
+
// it inherits it (see the css above) — and one that says something gives
|
|
41625
|
+
// its own. Empty when neither resolves to anything: the css then falls
|
|
41626
|
+
// back on its own, rather than being handed a colour that is not one.
|
|
41627
|
+
const colorOf = tabElement => getComputedStyle(tabElement).getPropertyValue("--link-current-indicator-color").trim();
|
|
41628
|
+
const writeColor = (property, color) => {
|
|
41629
|
+
if (color) {
|
|
41630
|
+
navElement.style.setProperty(property, color);
|
|
41631
|
+
} else {
|
|
41632
|
+
navElement.style.removeProperty(property);
|
|
41633
|
+
}
|
|
41634
|
+
};
|
|
41363
41635
|
const currentMeasure = measure(tabElements[currentIndex]);
|
|
41364
41636
|
const towardArea = containerElement.getAttribute("data-slide-travel-toward");
|
|
41365
41637
|
const towardIndex = tabElements.findIndex(tabElement => areaOf(tabElement) === towardArea);
|
|
41366
41638
|
let positionDelta = 0;
|
|
41367
41639
|
let lengthDelta = 0;
|
|
41640
|
+
let towardSign = 0;
|
|
41641
|
+
let towardColor = "";
|
|
41368
41642
|
if (towardIndex !== -1 && towardIndex !== currentIndex) {
|
|
41369
|
-
const
|
|
41643
|
+
const towardTabElement = tabElements[towardIndex];
|
|
41644
|
+
const towardMeasure = measure(towardTabElement);
|
|
41370
41645
|
// What one box of travel is worth in pixels of this row, signed so that
|
|
41371
41646
|
// the trait is exactly on the other tab when the progress is at its own
|
|
41372
41647
|
// end: the container counts +1 when the picture leans on a slide sitting
|
|
@@ -41374,6 +41649,8 @@ const Nav = ({
|
|
|
41374
41649
|
const sign = towardIndex > currentIndex ? -1 : 1;
|
|
41375
41650
|
positionDelta = (towardMeasure.position - currentMeasure.position) * sign;
|
|
41376
41651
|
lengthDelta = (towardMeasure.length - currentMeasure.length) * sign;
|
|
41652
|
+
towardSign = sign;
|
|
41653
|
+
towardColor = colorOf(towardTabElement);
|
|
41377
41654
|
}
|
|
41378
41655
|
const {
|
|
41379
41656
|
style
|
|
@@ -41382,6 +41659,9 @@ const Nav = ({
|
|
|
41382
41659
|
style.setProperty("--nav-indicator-length", currentMeasure.length);
|
|
41383
41660
|
style.setProperty("--nav-indicator-position-delta", positionDelta);
|
|
41384
41661
|
style.setProperty("--nav-indicator-length-delta", lengthDelta);
|
|
41662
|
+
style.setProperty("--nav-indicator-toward-sign", towardSign);
|
|
41663
|
+
writeColor("--nav-indicator-color", colorOf(tabElements[currentIndex]));
|
|
41664
|
+
writeColor("--nav-indicator-color-toward", towardColor);
|
|
41385
41665
|
navElement.setAttribute("data-nav-indicator-measured", "");
|
|
41386
41666
|
};
|
|
41387
41667
|
// Reached through a ref by everything watching the DOM below: those watchers
|
|
@@ -42857,7 +43137,15 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
42857
43137
|
* keeps its bars against its own edges.
|
|
42858
43138
|
* 2. **It gives its space back.** Being fixed it covers the content: without a
|
|
42859
43139
|
* reserve the end of a long page stays under it, unreachable. It publishes
|
|
42860
|
-
* what it takes on <html> — see fixed_bar_space.js.
|
|
43140
|
+
* what it takes on <html> — see fixed_bar_space.js. An edge holds ONE bar:
|
|
43141
|
+
* bars sharing an edge are pinned to it one over the other, so the room
|
|
43142
|
+
* given back is the largest of them and never their sum — which is what the
|
|
43143
|
+
* two bars of a page transition need, and what makes a deliberately stacked
|
|
43144
|
+
* second bar unexpressible. A second strip under the first belongs to the
|
|
43145
|
+
* flow: `position: sticky` at `top: var(--navi-fixed-bar-space-top)`, which
|
|
43146
|
+
* puts it under whatever the bar of the moment is worth, and
|
|
43147
|
+
* `useTransitionCover` (nav/transition_cover.js) so the pictures of a route
|
|
43148
|
+
* movement are cut at it.
|
|
42861
43149
|
* 3. **It reaches under the notch.** `env(safe-area-inset-*)` is padding, so
|
|
42862
43150
|
* the content is pushed in while the background keeps running to the edge of
|
|
42863
43151
|
* the screen. Across the bar that inset is added to `width`/`height` too, so
|
|
@@ -42972,7 +43260,9 @@ const FixedBarStyleCSSVars = {
|
|
|
42972
43260
|
* borderColor?: string,
|
|
42973
43261
|
* }>}
|
|
42974
43262
|
* @param {"top"|"bottom"|"left"|"right"} [props.area="top"] - Which edge of
|
|
42975
|
-
* the window it is pinned to.
|
|
43263
|
+
* the window it is pinned to. One bar per edge: a second one does not stack
|
|
43264
|
+
* under the first, it sits on it (see this file's top comment for the strip
|
|
43265
|
+
* that does stack).
|
|
42976
43266
|
* @param {string|number} [props.height] - For a bar on the top or bottom
|
|
42977
43267
|
* @param {string|number} [props.width] - …and for one on a side. The safe-area
|
|
42978
43268
|
* inset is NOT part of it: it is added on top, so the content keeps the size
|
|
@@ -43610,255 +43900,868 @@ const DetailsFieldContent = ({
|
|
|
43610
43900
|
});
|
|
43611
43901
|
};
|
|
43612
43902
|
|
|
43613
|
-
|
|
43614
|
-
|
|
43615
|
-
|
|
43616
|
-
|
|
43617
|
-
|
|
43618
|
-
|
|
43619
|
-
|
|
43620
|
-
|
|
43621
|
-
|
|
43622
|
-
|
|
43623
|
-
|
|
43624
|
-
|
|
43625
|
-
|
|
43626
|
-
|
|
43627
|
-
* batching that makes several state changes one commit.
|
|
43628
|
-
*/
|
|
43629
|
-
const flushSyncRendering = (fn) => {
|
|
43630
|
-
const debounceRenderingPrevious = options.debounceRendering;
|
|
43631
|
-
options.debounceRendering = (drainRenderQueue) => {
|
|
43632
|
-
drainRenderQueue();
|
|
43633
|
-
};
|
|
43634
|
-
try {
|
|
43635
|
-
fn();
|
|
43636
|
-
} finally {
|
|
43637
|
-
options.debounceRendering = debounceRenderingPrevious;
|
|
43638
|
-
}
|
|
43639
|
-
};
|
|
43640
|
-
|
|
43641
|
-
/**
|
|
43642
|
-
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
43643
|
-
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
43644
|
-
* of which of the two owns it.
|
|
43645
|
-
*/
|
|
43646
|
-
|
|
43903
|
+
// How long a popup waits before handing the focus to a field, when giving it
|
|
43904
|
+
// is what raises the on-screen keyboard.
|
|
43905
|
+
//
|
|
43906
|
+
// The focus is normally given as early as possible. But a popup places itself
|
|
43907
|
+
// against the viewport, and on a phone the keyboard takes a third of that
|
|
43908
|
+
// viewport away the moment a field receives focus — so the two landing in the
|
|
43909
|
+
// same tick means the popup is still arriving when the room under it changes,
|
|
43910
|
+
// and it re-places itself mid-entrance. Waiting lets it settle first, and the
|
|
43911
|
+
// keyboard then shrinks a box that has stopped moving.
|
|
43912
|
+
//
|
|
43913
|
+
// Long enough to outlast an entrance transition rather than merely reaching
|
|
43914
|
+
// the next frame: what has to be over is the popup MOVING, not one paint of
|
|
43915
|
+
// it.
|
|
43916
|
+
const FOCUS_DELAY_ON_KEYBOARD_MS = 250;
|
|
43647
43917
|
|
|
43648
43918
|
/**
|
|
43649
|
-
*
|
|
43650
|
-
*
|
|
43651
|
-
* - "resize": the window/visual viewport settled — which is also how the
|
|
43652
|
-
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
43653
|
-
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
43654
|
-
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
43655
|
-
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
43919
|
+
* Owns open/close decision-making for a popup (Dialog or Popover): guards
|
|
43920
|
+
* against duplicate requests and notifies the popup owner's own reactions.
|
|
43656
43921
|
*
|
|
43657
|
-
*
|
|
43658
|
-
*
|
|
43659
|
-
*
|
|
43660
|
-
|
|
43661
|
-
|
|
43662
|
-
|
|
43663
|
-
|
|
43664
|
-
|
|
43665
|
-
|
|
43666
|
-
};
|
|
43667
|
-
|
|
43668
|
-
/**
|
|
43669
|
-
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
43670
|
-
* getting shorter has pushed it out.
|
|
43922
|
+
* `controller.openEffect` is implemented by the controlled element (Dialog or
|
|
43923
|
+
* Popover), reassigned on every render so it always closes over the latest
|
|
43924
|
+
* props (scrollCapture, anchor, etc.). It performs whatever DOM side effects
|
|
43925
|
+
* are needed to make the element actually open (`showModal()`/`showPopover()`,
|
|
43926
|
+
* focus transfer, positioning, traps...) and returns its cleanup —
|
|
43927
|
+
* the matching side effects to sync back to closed (`close()`/
|
|
43928
|
+
* `hidePopover()`, releasing traps...). That cleanup is kept private to the
|
|
43929
|
+
* controller (not exposed as a property) and invoked when the popup actually
|
|
43930
|
+
* closes, however that happens.
|
|
43671
43931
|
*
|
|
43672
|
-
*
|
|
43673
|
-
*
|
|
43674
|
-
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
43675
|
-
* field makes the browser scroll it into view, which it does against the
|
|
43676
|
-
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
43677
|
-
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
43678
|
-
* content slides down relative to the shorter scrollport and the field ends up
|
|
43679
|
-
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
43680
|
-
* not redo a scroll-into-view it already answered, so this does.
|
|
43932
|
+
* Dialog/Popover also call `openController.requestClose(e, { isCancel })` for
|
|
43933
|
+
* their own internal triggers (backdrop click, Escape).
|
|
43681
43934
|
*
|
|
43682
|
-
*
|
|
43683
|
-
*
|
|
43684
|
-
*
|
|
43685
|
-
*
|
|
43935
|
+
* `openHandler` is the popup owner's own business logic, passed once to
|
|
43936
|
+
* `createOpenController`. Its return value is `{ onRequestClose, onClose }`,
|
|
43937
|
+
* in the spirit of CloseWatcher
|
|
43938
|
+
* (https://developer.mozilla.org/en-US/docs/Web/API/CloseWatcher) but with
|
|
43939
|
+
* clearer naming than its cancel/close pair:
|
|
43940
|
+
* - `onRequestClose(e)`: about to close — call `e.preventDefault()` to stay
|
|
43941
|
+
* open. Validation lives here.
|
|
43942
|
+
* - `onClose(e)`: actually closing, not preventable — final reactions live here.
|
|
43686
43943
|
*
|
|
43687
|
-
*
|
|
43688
|
-
*
|
|
43689
|
-
*
|
|
43944
|
+
* The controller exposes matching action methods:
|
|
43945
|
+
* - `open()`: requests opening — calls the caller's `onOpen` (see below), then
|
|
43946
|
+
* `mountContent`/`openEffect`, then `openHandler`.
|
|
43947
|
+
* - `requestClose()`: requests closing — calls `onRequestClose` then `onClose`,
|
|
43948
|
+
* stopping after the first if denied. The popup may choose to stay open,
|
|
43949
|
+
* which is what a `false` return says (`true`: closed, or closed already).
|
|
43950
|
+
* - `close()`: closes for real — calls only `onClose`, skipping
|
|
43951
|
+
* `onRequestClose` entirely. Used when there really is no choice (e.g. the
|
|
43952
|
+
* popup unmounting).
|
|
43690
43953
|
*/
|
|
43691
|
-
const
|
|
43692
|
-
|
|
43693
|
-
|
|
43694
|
-
|
|
43695
|
-
}
|
|
43696
|
-
|
|
43697
|
-
|
|
43698
|
-
}
|
|
43699
|
-
scrollIntoViewScoped(activeElement, { block: "nearest" });
|
|
43700
|
-
};
|
|
43954
|
+
const createOpenController = (
|
|
43955
|
+
openHandler,
|
|
43956
|
+
{ debugInteraction } = {},
|
|
43957
|
+
) => {
|
|
43958
|
+
let closeHandlers = null; // { onRequestClose, onClose } returned by openHandler
|
|
43959
|
+
let openEffectCleanup = null; // function returned by openEffect, undoes its DOM side effects
|
|
43960
|
+
let focusedAtClose = null; // what held the focus when the close was decided, see performClose
|
|
43701
43961
|
|
|
43702
|
-
|
|
43703
|
-
|
|
43704
|
-
|
|
43705
|
-
|
|
43706
|
-
|
|
43707
|
-
|
|
43708
|
-
|
|
43709
|
-
|
|
43710
|
-
|
|
43711
|
-
|
|
43712
|
-
|
|
43713
|
-
|
|
43714
|
-
|
|
43715
|
-
|
|
43716
|
-
|
|
43717
|
-
|
|
43718
|
-
|
|
43719
|
-
|
|
43720
|
-
|
|
43721
|
-
|
|
43722
|
-
|
|
43723
|
-
|
|
43724
|
-
|
|
43725
|
-
|
|
43726
|
-
|
|
43727
|
-
|
|
43728
|
-
|
|
43729
|
-
|
|
43730
|
-
|
|
43731
|
-
|
|
43732
|
-
|
|
43733
|
-
|
|
43734
|
-
|
|
43735
|
-
|
|
43736
|
-
|
|
43737
|
-
|
|
43738
|
-
|
|
43739
|
-
|
|
43740
|
-
|
|
43741
|
-
|
|
43742
|
-
|
|
43962
|
+
// Set true while we're waiting to see whether the click that follows a
|
|
43963
|
+
// mousedown-close will land back on whatever would reopen us — see
|
|
43964
|
+
// armSuppressNextOpenRequest below.
|
|
43965
|
+
let suppressNextOpenRequest = false;
|
|
43966
|
+
let disarmSuppressNextOpenRequest = null;
|
|
43967
|
+
|
|
43968
|
+
// When the popup closes because of a mousedown (e.g. clicking the
|
|
43969
|
+
// backdrop), the browser still dispatches the matching "click" afterward.
|
|
43970
|
+
// If that click lands back on the element that triggers open() (e.g. the
|
|
43971
|
+
// picker button), it would immediately reopen the popup. We cannot
|
|
43972
|
+
// preventDefault/stopPropagation the mousedown to stop that — the browser
|
|
43973
|
+
// dispatches the click regardless.
|
|
43974
|
+
//
|
|
43975
|
+
// Instead: arm a capture-phase "click" listener on document. Capture fires
|
|
43976
|
+
// before the click reaches its target, so by the time any bubble-phase
|
|
43977
|
+
// click handler (e.g. the trigger button's onClick, which calls
|
|
43978
|
+
// controller.open()) runs, `suppressNextOpenRequest` is already true and
|
|
43979
|
+
// open() ignores the request — no need to know *which* element triggers
|
|
43980
|
+
// it. A bubble-phase listener (runs after everything else, once the click
|
|
43981
|
+
// reaches document) clears the flag if nothing consumed it, meaning this
|
|
43982
|
+
// click never resulted in an open() call. A timeout is a last-resort safety
|
|
43983
|
+
// net in case the click never reaches document at all (e.g. some ancestor
|
|
43984
|
+
// called stopPropagation()) — a *task*, never a microtask: a microtask
|
|
43985
|
+
// checkpoint runs between two listeners of the same trusted event dispatch,
|
|
43986
|
+
// so it would clear the flag before the bubble-phase handler this is meant
|
|
43987
|
+
// to block ever runs, which is precisely the case it exists for.
|
|
43988
|
+
const armSuppressNextOpenRequest = () => {
|
|
43989
|
+
disarmSuppressNextOpenRequest?.();
|
|
43990
|
+
let safetyTimeout = null;
|
|
43991
|
+
const onCaptureClick = () => {
|
|
43992
|
+
document.removeEventListener("click", onCaptureClick, {
|
|
43993
|
+
capture: true,
|
|
43994
|
+
});
|
|
43995
|
+
suppressNextOpenRequest = true;
|
|
43996
|
+
document.addEventListener("click", onBubbleClick);
|
|
43997
|
+
safetyTimeout = setTimeout(() => {
|
|
43998
|
+
suppressNextOpenRequest = false;
|
|
43999
|
+
});
|
|
44000
|
+
};
|
|
44001
|
+
const onBubbleClick = () => {
|
|
44002
|
+
document.removeEventListener("click", onBubbleClick);
|
|
44003
|
+
clearTimeout(safetyTimeout);
|
|
44004
|
+
suppressNextOpenRequest = false;
|
|
44005
|
+
};
|
|
44006
|
+
disarmSuppressNextOpenRequest = () => {
|
|
44007
|
+
clearTimeout(safetyTimeout);
|
|
44008
|
+
document.removeEventListener("click", onCaptureClick, {
|
|
44009
|
+
capture: true,
|
|
44010
|
+
});
|
|
44011
|
+
document.removeEventListener("click", onBubbleClick);
|
|
44012
|
+
};
|
|
44013
|
+
document.addEventListener("click", onCaptureClick, { capture: true });
|
|
43743
44014
|
};
|
|
43744
|
-
};
|
|
43745
44015
|
|
|
43746
|
-
|
|
43747
|
-
|
|
43748
|
-
|
|
43749
|
-
|
|
43750
|
-
|
|
43751
|
-
|
|
43752
|
-
|
|
43753
|
-
* its own state.
|
|
43754
|
-
*/
|
|
43755
|
-
const suppressPointerEventsDuringTransition = (el) => {
|
|
43756
|
-
el.style.pointerEvents = "none";
|
|
43757
|
-
return whenTransitionSettles(el, () => {
|
|
43758
|
-
el.style.pointerEvents = "";
|
|
43759
|
-
});
|
|
43760
|
-
};
|
|
44016
|
+
const performClose = (closeEvent) => {
|
|
44017
|
+
controller.opened = false;
|
|
44018
|
+
// Read before any close effect touches the DOM: closing a native <dialog>
|
|
44019
|
+
// hands the focus back to whatever held it at showModal() time, so by the
|
|
44020
|
+
// time the close cleanup runs, the popup's content has already lost the
|
|
44021
|
+
// focus and could not be remembered for the next open.
|
|
44022
|
+
focusedAtClose = document.activeElement;
|
|
43761
44023
|
|
|
43762
|
-
|
|
43763
|
-
|
|
43764
|
-
|
|
43765
|
-
|
|
43766
|
-
|
|
43767
|
-
|
|
43768
|
-
|
|
43769
|
-
|
|
43770
|
-
|
|
43771
|
-
|
|
43772
|
-
* downstream, so no fallback timer is needed.
|
|
43773
|
-
*
|
|
43774
|
-
* `hide` is the caller's own way to actually hide the backdrop
|
|
43775
|
-
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
43776
|
-
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
43777
|
-
*
|
|
43778
|
-
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
43779
|
-
* fresh open can cancel a pending hide it's about to make redundant.
|
|
43780
|
-
*/
|
|
43781
|
-
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
43782
|
-
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
43783
|
-
if (!mousedownEvent) {
|
|
43784
|
-
hide();
|
|
43785
|
-
return undefined;
|
|
43786
|
-
}
|
|
43787
|
-
const onClick = () => {
|
|
43788
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
43789
|
-
hide();
|
|
43790
|
-
};
|
|
43791
|
-
document.addEventListener("click", onClick, { capture: true });
|
|
43792
|
-
return () => {
|
|
43793
|
-
document.removeEventListener("click", onClick, { capture: true });
|
|
43794
|
-
};
|
|
43795
|
-
};
|
|
44024
|
+
prevent_reopen: {
|
|
44025
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
44026
|
+
if (mousedownEvent) {
|
|
44027
|
+
debugInteraction(
|
|
44028
|
+
closeEvent,
|
|
44029
|
+
`closed by mousedown -> ignore next click`,
|
|
44030
|
+
);
|
|
44031
|
+
armSuppressNextOpenRequest();
|
|
44032
|
+
break prevent_reopen;
|
|
44033
|
+
}
|
|
43796
44034
|
|
|
43797
|
-
|
|
43798
|
-
|
|
43799
|
-
|
|
43800
|
-
|
|
43801
|
-
|
|
43802
|
-
|
|
43803
|
-
|
|
43804
|
-
* when docked) keeps the word as the compass direction the popup comes
|
|
43805
|
-
* from: placed "top" (a point/corner), it slides in from the top.
|
|
43806
|
-
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
43807
|
-
* direction instead, the opposite compass point: placed "top" of the
|
|
43808
|
-
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
43809
|
-
*
|
|
43810
|
-
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
43811
|
-
*/
|
|
43812
|
-
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
43813
|
-
const yWord =
|
|
43814
|
-
y === "top"
|
|
43815
|
-
? prefix === "expand"
|
|
43816
|
-
? "up"
|
|
43817
|
-
: "top"
|
|
43818
|
-
: y === "bottom"
|
|
43819
|
-
? prefix === "expand"
|
|
43820
|
-
? "down"
|
|
43821
|
-
: "bottom"
|
|
43822
|
-
: null;
|
|
43823
|
-
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
43824
|
-
if (!yWord && !xWord) {
|
|
43825
|
-
return null;
|
|
43826
|
-
}
|
|
43827
|
-
return yWord && xWord
|
|
43828
|
-
? `${prefix}-${yWord}-${xWord}`
|
|
43829
|
-
: `${prefix}-${yWord || xWord}`;
|
|
43830
|
-
};
|
|
44035
|
+
// The keyboard counterpart of the mousedown case above: a key press that
|
|
44036
|
+
// closes the popup and then goes on to activate the trigger, reopening it
|
|
44037
|
+
// on the spot. Space and Enter both get there, but not the same way and
|
|
44038
|
+
// not always — preventing the key unconditionally would eat presses that
|
|
44039
|
+
// were never going to activate anything (a space typed in a field, an
|
|
44040
|
+
// Enter the popup's own handler already consumed), so each is verified
|
|
44041
|
+
// before being prevented.
|
|
43831
44042
|
|
|
43832
|
-
|
|
43833
|
-
|
|
43834
|
-
|
|
43835
|
-
|
|
43836
|
-
|
|
43837
|
-
|
|
43838
|
-
|
|
43839
|
-
|
|
43840
|
-
|
|
43841
|
-
|
|
43842
|
-
|
|
43843
|
-
|
|
43844
|
-
|
|
43845
|
-
|
|
43846
|
-
|
|
43847
|
-
|
|
43848
|
-
|
|
43849
|
-
|
|
43850
|
-
|
|
43851
|
-
|
|
43852
|
-
|
|
44043
|
+
// Space: pressed on the trigger itself, which still has focus (closing
|
|
44044
|
+
// does not move it away from an element outside the popup). The browser
|
|
44045
|
+
// turns that press into a click on keyup, and that click lands back on
|
|
44046
|
+
// the trigger. Asked of the browser's own default action rather than
|
|
44047
|
+
// guessed from the tag name: a space that scrolls, or types into a field
|
|
44048
|
+
// inside the popup, has no activation to prevent and preventing it would
|
|
44049
|
+
// swallow the scroll / the character.
|
|
44050
|
+
const spaceKeyEvent = findEvent(
|
|
44051
|
+
closeEvent,
|
|
44052
|
+
(e) => e.type === "keydown" && e.key === " ",
|
|
44053
|
+
);
|
|
44054
|
+
if (
|
|
44055
|
+
spaceKeyEvent &&
|
|
44056
|
+
getKeyboardEventDefaultAction(spaceKeyEvent) === "activate"
|
|
44057
|
+
) {
|
|
44058
|
+
debugInteraction(
|
|
44059
|
+
closeEvent,
|
|
44060
|
+
`closed by space on <${spaceKeyEvent.target.tagName.toLowerCase()}> -> prevent the click it would produce (space.preventDefault())`,
|
|
44061
|
+
);
|
|
44062
|
+
// The browser won't dispatch the click, and our "space_to_open" sees
|
|
44063
|
+
// defaultPrevented too so it won't try to open the picker either.
|
|
44064
|
+
spaceKeyEvent.preventDefault();
|
|
44065
|
+
break prevent_reopen;
|
|
44066
|
+
}
|
|
43853
44067
|
|
|
43854
|
-
|
|
43855
|
-
|
|
43856
|
-
|
|
43857
|
-
|
|
43858
|
-
|
|
43859
|
-
|
|
43860
|
-
|
|
43861
|
-
|
|
44068
|
+
// Enter: pressed inside the popup (its own submit button, or implicit
|
|
44069
|
+
// submission from a field it contains). The popup closes synchronously
|
|
44070
|
+
// and focus is restored to the trigger, so the activation the browser
|
|
44071
|
+
// still owes this press is delivered to the trigger instead.
|
|
44072
|
+
//
|
|
44073
|
+
// Verified on two counts: the press still owes an activation (the
|
|
44074
|
+
// browser's default action for it is one — "activate" on a submit button,
|
|
44075
|
+
// "form_submit" on a field — and nothing has consumed it yet), and it came
|
|
44076
|
+
// from inside the popup. An Enter from outside is not this case at all.
|
|
44077
|
+
const enterKeyEvent = findEvent(
|
|
44078
|
+
closeEvent,
|
|
44079
|
+
(e) => e.type === "keydown" && e.key === "Enter",
|
|
44080
|
+
);
|
|
44081
|
+
if (
|
|
44082
|
+
enterKeyEvent &&
|
|
44083
|
+
!enterKeyEvent.defaultPrevented &&
|
|
44084
|
+
ENTER_ACTIVATING_DEFAULT_ACTION_SET.has(
|
|
44085
|
+
getKeyboardEventDefaultAction(enterKeyEvent),
|
|
44086
|
+
) &&
|
|
44087
|
+
isInsideOpenPopup(enterKeyEvent.target)
|
|
44088
|
+
) {
|
|
44089
|
+
debugInteraction(
|
|
44090
|
+
closeEvent,
|
|
44091
|
+
`closed by enter from inside the popup -> prevent the activation it would deliver to the trigger (enter.preventDefault())`,
|
|
44092
|
+
);
|
|
44093
|
+
enterKeyEvent.preventDefault();
|
|
44094
|
+
break prevent_reopen;
|
|
44095
|
+
}
|
|
44096
|
+
}
|
|
44097
|
+
|
|
44098
|
+
// Sync the DOM closed first (releasing the focus trap) — only then run
|
|
44099
|
+
// the owner's own reaction (onClose may restore focus to an element
|
|
44100
|
+
// outside the popup, which the focus trap would otherwise fight while
|
|
44101
|
+
// still active).
|
|
44102
|
+
openEffectCleanup?.(closeEvent);
|
|
44103
|
+
openEffectCleanup = null;
|
|
44104
|
+
closeHandlers?.onClose?.(closeEvent);
|
|
44105
|
+
closeHandlers = null;
|
|
44106
|
+
// Last: the close effects above are what starts the exit transition the
|
|
44107
|
+
// content must outlive (see popup_content_mount.js).
|
|
44108
|
+
controller.unmountContent?.();
|
|
44109
|
+
controller.onOpenedChange?.(false, closeEvent);
|
|
44110
|
+
};
|
|
44111
|
+
const controller = {
|
|
44112
|
+
opened: false,
|
|
44113
|
+
openEffect: null,
|
|
44114
|
+
// Set by the controlled element (see popup_content_mount.js) when its
|
|
44115
|
+
// content is still waiting for a first open to be built. Called below,
|
|
44116
|
+
// before openEffect, so the popup measures and positions the real thing.
|
|
44117
|
+
mountContent: null,
|
|
44118
|
+
// The caller's own `onOpen`, set by Dialog/Popover from their props on
|
|
44119
|
+
// every render (like openEffect). Called BEFORE mountContent, so whatever
|
|
44120
|
+
// it decides — which record this dialog is opening on — is already true by
|
|
44121
|
+
// the time the content is built, positioned and shown. That order is the
|
|
44122
|
+
// whole point: learning it afterwards means the content mounted on the
|
|
44123
|
+
// previous subject first.
|
|
44124
|
+
onOpen: null,
|
|
44125
|
+
// The counterpart, set only when the popup was told to throw its content
|
|
44126
|
+
// away on close (`mount="while-opened"`). Called from performClose above.
|
|
44127
|
+
unmountContent: null,
|
|
44128
|
+
// Told whenever `opened` actually changes, whatever asked for it — an
|
|
44129
|
+
// interaction, a command, a prop — with the event that asked. What lets a
|
|
44130
|
+
// `signal` prop reflect the popup's real state, and a `navState` prop write
|
|
44131
|
+
// it into the history entry (see useOpenPropsEffectOnOpenController);
|
|
44132
|
+
// called once the open/close has fully happened rather than mid-sequence.
|
|
44133
|
+
onOpenedChange: null,
|
|
44134
|
+
open: (e, detail) => {
|
|
44135
|
+
if (controller.opened || !controller.openEffect) {
|
|
44136
|
+
return;
|
|
44137
|
+
}
|
|
44138
|
+
if (suppressNextOpenRequest) {
|
|
44139
|
+
suppressNextOpenRequest = false;
|
|
44140
|
+
return;
|
|
44141
|
+
}
|
|
44142
|
+
const requestOpenEvent = new CustomEvent("navi_request_open", {
|
|
44143
|
+
detail: { event: e, ...detail },
|
|
44144
|
+
cancelable: true,
|
|
44145
|
+
});
|
|
44146
|
+
chainEvent(requestOpenEvent, e);
|
|
44147
|
+
// we prepare focus transfer before actually opening the popover/dialog
|
|
44148
|
+
// because opnening dialog makes browser try to transfer focus (which ends up in document.body for instance)
|
|
44149
|
+
const focusTransfer = prepareFocusTransfer(
|
|
44150
|
+
requestOpenEvent,
|
|
44151
|
+
debugInteraction,
|
|
44152
|
+
);
|
|
44153
|
+
controller.transferFocusOnOpen = (el) => {
|
|
44154
|
+
// requestOpenEvent, not the raw `e` — getFocusedBeforeTransfer needs
|
|
44155
|
+
// e.detail.eventChain (built by chainEvent above) to recover the
|
|
44156
|
+
// element a mousedown/click landed on. `e` itself is usually the raw
|
|
44157
|
+
// native event: its own `.detail` is a number (click count) on a
|
|
44158
|
+
// MouseEvent, so `e.detail.eventChain` is always undefined and the
|
|
44159
|
+
// mousedown/click branches below never matched — silently falling
|
|
44160
|
+
// back to `document.activeElement`, which is often `document.body`
|
|
44161
|
+
// once mousedown.preventDefault() has kept focus from landing
|
|
44162
|
+
// anywhere yet.
|
|
44163
|
+
|
|
44164
|
+
// Two conditions, and both are about THIS opening rather than about
|
|
44165
|
+
// the device:
|
|
44166
|
+
// - the interaction: only a finger raises a virtual keyboard, and a
|
|
44167
|
+
// hybrid tablet answers "coarse" to every device-level signal
|
|
44168
|
+
// whichever of its two inputs was just used — the open event still
|
|
44169
|
+
// remembers which one it was. An opening with no pointer in it at
|
|
44170
|
+
// all (a keyboard shortcut, defaultOpen, an app calling open()) is
|
|
44171
|
+
// not one either.
|
|
44172
|
+
// - the target: focusing a button raises nothing, so there is nothing
|
|
44173
|
+
// to wait for and the focus stays immediate. Only a field the
|
|
44174
|
+
// keyboard comes up for is worth delaying — which is why the
|
|
44175
|
+
// decision is taken on the resolved target, inside transferFocus.
|
|
44176
|
+
const openedByTouch = Boolean(
|
|
44177
|
+
findEvent(requestOpenEvent, isTouchDrivenEvent),
|
|
44178
|
+
);
|
|
44179
|
+
const cancelPendingFocus = focusTransfer.transferFocus(e, el, {
|
|
44180
|
+
getDelay: (target) =>
|
|
44181
|
+
openedByTouch && isEditableTarget(target)
|
|
44182
|
+
? FOCUS_DELAY_ON_KEYBOARD_MS
|
|
44183
|
+
: 0,
|
|
44184
|
+
});
|
|
44185
|
+
return (closeEvent) => {
|
|
44186
|
+
// Closed before the delay was up: the focus was never given, so it
|
|
44187
|
+
// must not be given now — to a field inside a popup on its way out,
|
|
44188
|
+
// raising the keyboard as it goes.
|
|
44189
|
+
cancelPendingFocus?.();
|
|
44190
|
+
markAutofocusRestoreOnClose(el, closeEvent, focusedAtClose);
|
|
44191
|
+
const focusoutEvent = findEvent(closeEvent, "focusout");
|
|
44192
|
+
if (focusoutEvent) {
|
|
44193
|
+
debugInteraction(
|
|
44194
|
+
closeEvent,
|
|
44195
|
+
`closed by focusout -> let focus go away`,
|
|
44196
|
+
);
|
|
44197
|
+
} else {
|
|
44198
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
44199
|
+
if (mousedownEvent) {
|
|
44200
|
+
debugInteraction(
|
|
44201
|
+
closeEvent,
|
|
44202
|
+
"closed by mousedown -> prevent browser focus (mousedown.preventDefault())",
|
|
44203
|
+
);
|
|
44204
|
+
mousedownEvent.preventDefault();
|
|
44205
|
+
}
|
|
44206
|
+
focusTransfer.restoreFocus();
|
|
44207
|
+
}
|
|
44208
|
+
};
|
|
44209
|
+
};
|
|
44210
|
+
// Before mountContent, which builds the content, and before openEffect,
|
|
44211
|
+
// which shows it: what the popup opens ON has to be known before either
|
|
44212
|
+
// (see `onOpen` above).
|
|
44213
|
+
controller.onOpen?.(requestOpenEvent);
|
|
44214
|
+
// After prepareFocusTransfer, which has to record what held the focus
|
|
44215
|
+
// before anything inside the popup can claim it, and before openEffect,
|
|
44216
|
+
// which measures the popup to place it.
|
|
44217
|
+
controller.mountContent?.();
|
|
44218
|
+
// Only now — after the content has been built, before openEffect shows
|
|
44219
|
+
// it. Dialog/Popover recompute aria-expanded and navi-hidden from this
|
|
44220
|
+
// flag on every render, and mountContent above renders synchronously:
|
|
44221
|
+
// flipping it any earlier commits an already-open DOM (aria-expanded
|
|
44222
|
+
// "true", navi-hidden gone) before openEffect has run a single
|
|
44223
|
+
// statement, so the "closed" frame it pins to transition from is in
|
|
44224
|
+
// fact the open one and the entrance animation has nothing to play.
|
|
44225
|
+
// It also gives the content it just built the opening it is documented
|
|
44226
|
+
// to observe — mounted while the popup reads as closed, told it opened
|
|
44227
|
+
// right after (see popup_content_mount.js and
|
|
44228
|
+
// use_displayed_layout_effect.js).
|
|
44229
|
+
controller.opened = true;
|
|
44230
|
+
const openEffectReturnValue =
|
|
44231
|
+
controller.openEffect(requestOpenEvent) || null;
|
|
44232
|
+
openEffectCleanup = (closeEvent) => {
|
|
44233
|
+
openEffectReturnValue?.(closeEvent);
|
|
44234
|
+
};
|
|
44235
|
+
closeHandlers = openHandler(requestOpenEvent) || null;
|
|
44236
|
+
controller.onOpenedChange?.(true, requestOpenEvent);
|
|
44237
|
+
},
|
|
44238
|
+
requestClose: (
|
|
44239
|
+
e = new CustomEvent("programmatic", { detail: {} }),
|
|
44240
|
+
detail,
|
|
44241
|
+
) => {
|
|
44242
|
+
if (!controller.opened) {
|
|
44243
|
+
return true;
|
|
44244
|
+
}
|
|
44245
|
+
const requestCloseEvent = new CustomEvent("navi_request_close", {
|
|
44246
|
+
detail: { event: e, ...detail },
|
|
44247
|
+
cancelable: true,
|
|
44248
|
+
});
|
|
44249
|
+
chainEvent(requestCloseEvent, e);
|
|
44250
|
+
closeHandlers?.onRequestClose?.(requestCloseEvent);
|
|
44251
|
+
if (requestCloseEvent.defaultPrevented) {
|
|
44252
|
+
// The native <dialog> "cancel" event (Escape key) closes the dialog
|
|
44253
|
+
// by default; prevent that default so denial actually keeps it open.
|
|
44254
|
+
const nativeCancelEvent = findEvent(requestCloseEvent, "cancel");
|
|
44255
|
+
if (nativeCancelEvent) {
|
|
44256
|
+
nativeCancelEvent.preventDefault();
|
|
44257
|
+
}
|
|
44258
|
+
return false;
|
|
44259
|
+
}
|
|
44260
|
+
performClose(requestCloseEvent);
|
|
44261
|
+
return true;
|
|
44262
|
+
},
|
|
44263
|
+
close: (e = new CustomEvent("programmatic", { detail: {} }), detail) => {
|
|
44264
|
+
if (!controller.opened) {
|
|
44265
|
+
return;
|
|
44266
|
+
}
|
|
44267
|
+
const closeEvent = new CustomEvent("navi_close", {
|
|
44268
|
+
detail: { event: e, ...detail },
|
|
44269
|
+
});
|
|
44270
|
+
chainEvent(closeEvent, e);
|
|
44271
|
+
// Skips onRequestClose entirely — there is no choice here.
|
|
44272
|
+
performClose(closeEvent);
|
|
44273
|
+
},
|
|
44274
|
+
};
|
|
44275
|
+
return controller;
|
|
44276
|
+
};
|
|
44277
|
+
|
|
44278
|
+
// Inside a popup that is open right now — the popup being closed, in practice,
|
|
44279
|
+
// since that is the one the key press was delivered to.
|
|
44280
|
+
const isInsideOpenPopup = (element) => {
|
|
44281
|
+
if (!element || element.nodeType !== 1) {
|
|
44282
|
+
return false;
|
|
44283
|
+
}
|
|
44284
|
+
return Boolean(element.closest("dialog[open], [popover]:popover-open"));
|
|
44285
|
+
};
|
|
44286
|
+
|
|
44287
|
+
// What Enter is about to do when it is about to activate something: press the
|
|
44288
|
+
// focused control, or submit the form around it. Anything else it can do
|
|
44289
|
+
// (typing a newline, nothing at all) leaves no activation behind to land on the
|
|
44290
|
+
// trigger once focus is restored.
|
|
44291
|
+
const ENTER_ACTIVATING_DEFAULT_ACTION_SET = new Set([
|
|
44292
|
+
"activate",
|
|
44293
|
+
"form_submit",
|
|
44294
|
+
]);
|
|
44295
|
+
|
|
44296
|
+
// Created once per popup instance: openHandler is wrapped in a stable callback
|
|
44297
|
+
// so the controller identity never changes across renders, even though
|
|
44298
|
+
// Dialog/Popover read fresh closures (scrollTrap, etc.) via
|
|
44299
|
+
// openController.openEffect on every render.
|
|
44300
|
+
const useOpenController = (openHandler) => {
|
|
44301
|
+
const debugInteraction = useDebugInteraction();
|
|
44302
|
+
const stableOpenHandler = useStableCallback(openHandler);
|
|
44303
|
+
const controllerRef = useRef(null);
|
|
44304
|
+
if (!controllerRef.current) {
|
|
44305
|
+
controllerRef.current = createOpenController(stableOpenHandler, {
|
|
44306
|
+
debugInteraction,
|
|
44307
|
+
});
|
|
44308
|
+
}
|
|
44309
|
+
// Unmount safety net: if Dialog/Popover unmounts while still open (parent
|
|
44310
|
+
// removes it from the tree without going through requestClose()), there is
|
|
44311
|
+
// no choice to leave open — close it for real.
|
|
44312
|
+
useLayoutEffect(() => {
|
|
44313
|
+
return () => {
|
|
44314
|
+
controllerRef.current.close();
|
|
44315
|
+
};
|
|
44316
|
+
}, []);
|
|
44317
|
+
return controllerRef.current;
|
|
44318
|
+
};
|
|
44319
|
+
|
|
44320
|
+
// Nested popups that both mount already-open (`open`/`defaultOpen`) would
|
|
44321
|
+
// otherwise stack in the wrong order: Preact fires layout effects
|
|
44322
|
+
// child-first on mount, so a nested popup's own mount-open would call
|
|
44323
|
+
// showPopover() before its ancestor's — and the top layer stacks *later*
|
|
44324
|
+
// showPopover() calls above *earlier* ones (see popover.jsx's own openEffect
|
|
44325
|
+
// comment) — leaving the ancestor on top instead of the nested popup, the
|
|
44326
|
+
// opposite of what opening them one at a time (ancestor first, by real user
|
|
44327
|
+
// interaction) would produce. Batching every mount-time silent open queued
|
|
44328
|
+
// during the same commit's layout-effect phase into one microtask flush,
|
|
44329
|
+
// then simply running them in *reverse* of their registration order fixes
|
|
44330
|
+
// this — no need to compare DOM positions: since effects already fire
|
|
44331
|
+
// child-first, tree-wide, for *any* ancestor/descendant pair the descendant
|
|
44332
|
+
// is always queued before the ancestor, regardless of what else is in the
|
|
44333
|
+
// tree, so reversing the whole batch always puts every ancestor before its
|
|
44334
|
+
// own descendants. Works for any nesting depth for the same reason. Two
|
|
44335
|
+
// unrelated (sibling) popups both mounting open also get reordered
|
|
44336
|
+
// relative to each other, but there's no meaningful "correct" order between
|
|
44337
|
+
// those anyway.
|
|
44338
|
+
let pendingMountOpens = [];
|
|
44339
|
+
let mountOpenFlushScheduled = false;
|
|
44340
|
+
const scheduleMountOpen = (run) => {
|
|
44341
|
+
pendingMountOpens.push(run);
|
|
44342
|
+
if (mountOpenFlushScheduled) {
|
|
44343
|
+
return;
|
|
44344
|
+
}
|
|
44345
|
+
mountOpenFlushScheduled = true;
|
|
44346
|
+
queueMicrotask(() => {
|
|
44347
|
+
const entries = pendingMountOpens;
|
|
44348
|
+
pendingMountOpens = [];
|
|
44349
|
+
mountOpenFlushScheduled = false;
|
|
44350
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
44351
|
+
entries[i]();
|
|
44352
|
+
}
|
|
44353
|
+
});
|
|
44354
|
+
};
|
|
44355
|
+
|
|
44356
|
+
// Where the popup's open state is kept, when it is kept anywhere: `navState`
|
|
44357
|
+
// resolved to the `{ id, type }` useNavState wants.
|
|
44358
|
+
//
|
|
44359
|
+
// `true` takes the popup's own id — a popup a `--navi-open` command can name is
|
|
44360
|
+
// a popup that already has a stable one, and that id is what identifies its
|
|
44361
|
+
// open state too.
|
|
44362
|
+
const NO_NAV_STATE = { id: undefined, type: "replace" };
|
|
44363
|
+
const resolveNavStateProp = (navState, popupId, name) => {
|
|
44364
|
+
if (!navState) {
|
|
44365
|
+
return NO_NAV_STATE;
|
|
44366
|
+
}
|
|
44367
|
+
if (navState === true) {
|
|
44368
|
+
return { id: popupId, type: "replace" };
|
|
44369
|
+
}
|
|
44370
|
+
if (typeof navState === "string") {
|
|
44371
|
+
return { id: navState, type: "replace" };
|
|
44372
|
+
}
|
|
44373
|
+
return { id: navState.id || popupId, type: navState.type || "replace" };
|
|
44374
|
+
};
|
|
44375
|
+
|
|
44376
|
+
/**
|
|
44377
|
+
* Keeps an open controller in sync with where the caller says the popup should
|
|
44378
|
+
* be: an `open`/`defaultOpen` pair, a `signal`, or a `navState` — the open
|
|
44379
|
+
* state written into the history entry, so a screen left and come back to finds
|
|
44380
|
+
* its popup as it was.
|
|
44381
|
+
*
|
|
44382
|
+
* Shared between `useOpenControllerByProps` below (Dialog/Popover driving their
|
|
44383
|
+
* own controller), `picker_custom.jsx` (which owns its controller but wants
|
|
44384
|
+
* the same skip-if-already-matching / open-or-requestClose control flow) and
|
|
44385
|
+
* `expandable.jsx` (open in flow rather than on a layer, same decision).
|
|
44386
|
+
*
|
|
44387
|
+
* @param {{ open: (e: Event, detail?: object) => void, requestClose: (e: Event, detail?: object) => void, opened: boolean }} openController
|
|
44388
|
+
* @param {{ id?: string, open?: boolean|"interaction", defaultOpen?: boolean|"interaction", signal?: import("@preact/signals").Signal<boolean>, navState?: boolean|string|{id?: string, type?: "push"|"replace"} }} props
|
|
44389
|
+
* @param {string} [name] What the dev warnings call the thing being opened.
|
|
44390
|
+
*/
|
|
44391
|
+
const useOpenPropsEffectOnOpenController = (
|
|
44392
|
+
openController,
|
|
44393
|
+
props,
|
|
44394
|
+
name = "popup",
|
|
44395
|
+
) => {
|
|
44396
|
+
const { signal, defaultOpen, navState } = props;
|
|
44397
|
+
const { id: navStateId, type: navStateType } = resolveNavStateProp(
|
|
44398
|
+
navState,
|
|
44399
|
+
props.id);
|
|
44400
|
+
// Called unconditionally (it answers with no-ops for an absent id), like
|
|
44401
|
+
// every other hook here.
|
|
44402
|
+
const [navStateValue, enterNavState, leaveNavState] = useNavState(
|
|
44403
|
+
navStateId,
|
|
44404
|
+
{ type: navStateType },
|
|
44405
|
+
);
|
|
44406
|
+
// What the caller holds, however they hold it: the history entry when there
|
|
44407
|
+
// is a `navState`, an `open` they re-render themselves, or a `signal` this
|
|
44408
|
+
// hook also writes (see onOpenedChange below). Reading .value during render
|
|
44409
|
+
// is what subscribes the popup to a signal; reading the document state is
|
|
44410
|
+
// what subscribes it to the history entry, back button included.
|
|
44411
|
+
const open = navStateId
|
|
44412
|
+
? Boolean(navStateValue)
|
|
44413
|
+
: signal
|
|
44414
|
+
? signal.value
|
|
44415
|
+
: props.open;
|
|
44416
|
+
// Assigned on every render, like openEffect, so it always closes over the
|
|
44417
|
+
// latest prop: a popup that opens or closes on its own (Escape, backdrop, a
|
|
44418
|
+
// --navi-close command) writes what happened where the caller keeps it, so
|
|
44419
|
+
// whoever holds it always reads where the popup is.
|
|
44420
|
+
openController.onOpenedChange =
|
|
44421
|
+
navStateId || signal
|
|
44422
|
+
? (opened, event) => {
|
|
44423
|
+
if (navStateId) {
|
|
44424
|
+
if (opened) {
|
|
44425
|
+
enterNavState();
|
|
44426
|
+
} else {
|
|
44427
|
+
// Under type "push" a cancel goes back rather than rewriting the
|
|
44428
|
+
// entry, so everything else written to the url while the popup
|
|
44429
|
+
// was open goes back with it (see useNavState's own leave()).
|
|
44430
|
+
leaveNavState({ isBack: Boolean(event?.detail?.isCancel) });
|
|
44431
|
+
}
|
|
44432
|
+
}
|
|
44433
|
+
if (signal) {
|
|
44434
|
+
signal.value = opened;
|
|
44435
|
+
}
|
|
44436
|
+
}
|
|
44437
|
+
: null;
|
|
44438
|
+
// Tracks whether the effect below has ever run before — only the very
|
|
44439
|
+
// first run gets the "mount already open" treatment (`open` truthy from
|
|
44440
|
+
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
44441
|
+
// subsequent `open` change is a real, later toggle and should animate
|
|
44442
|
+
// normally like any other interactive open/close.
|
|
44443
|
+
const isFirstRunRef = useRef(true);
|
|
44444
|
+
|
|
44445
|
+
useLayoutEffect(() => {
|
|
44446
|
+
const isFirstRun = isFirstRunRef.current;
|
|
44447
|
+
isFirstRunRef.current = false;
|
|
44448
|
+
|
|
44449
|
+
if (isFirstRun) {
|
|
44450
|
+
const mountOpenReason = open || defaultOpen;
|
|
44451
|
+
if (mountOpenReason) {
|
|
44452
|
+
// Whether this popup being open is something that just happened, or
|
|
44453
|
+
// something that was already true when the page appeared. "interaction"
|
|
44454
|
+
// says the mount IS the opening — the popup exists because the user
|
|
44455
|
+
// just asked for it — so the entrance plays like any other open. Any
|
|
44456
|
+
// other truthy value means it was simply already open: nothing was ever
|
|
44457
|
+
// shown as "closed" for the user to see it transition away from, so the
|
|
44458
|
+
// entrance is skipped (`silent`, see popover.jsx's own openEffect).
|
|
44459
|
+
//
|
|
44460
|
+
// Deferred + batched (see scheduleMountOpen above) rather than called
|
|
44461
|
+
// directly, so nested popups that both mount already-open end up
|
|
44462
|
+
// stacked ancestor-first instead of Preact's own child-first effect
|
|
44463
|
+
// order.
|
|
44464
|
+
scheduleMountOpen(() =>
|
|
44465
|
+
openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
|
|
44466
|
+
silent: mountOpenReason !== "interaction",
|
|
44467
|
+
}),
|
|
44468
|
+
);
|
|
44469
|
+
}
|
|
44470
|
+
return;
|
|
44471
|
+
}
|
|
44472
|
+
|
|
44473
|
+
if (open === undefined) {
|
|
44474
|
+
return;
|
|
44475
|
+
}
|
|
44476
|
+
// Skip when the controller is already in the desired state.
|
|
44477
|
+
// openController.opened tracks actual open/close (updated by onopen/onclose,
|
|
44478
|
+
// not by renders) so it is the authoritative check against feedback loops.
|
|
44479
|
+
if (open === openController.opened) {
|
|
44480
|
+
return;
|
|
44481
|
+
}
|
|
44482
|
+
if (open) {
|
|
44483
|
+
openController.open(new CustomEvent("open_by_prop", { detail: {} }));
|
|
44484
|
+
} else {
|
|
44485
|
+
openController.requestClose(
|
|
44486
|
+
new CustomEvent("close_by_prop", { detail: {} }),
|
|
44487
|
+
{ isCancel: true },
|
|
44488
|
+
);
|
|
44489
|
+
}
|
|
44490
|
+
// The request can be refused (a busy form denying the close): the popup
|
|
44491
|
+
// then stays where it was, and whoever holds the open state is told so —
|
|
44492
|
+
// otherwise it would keep saying "closed" about a popup still open.
|
|
44493
|
+
if (signal) {
|
|
44494
|
+
signal.value = openController.opened;
|
|
44495
|
+
}
|
|
44496
|
+
if (navStateId && openController.opened) {
|
|
44497
|
+
enterNavState();
|
|
44498
|
+
}
|
|
44499
|
+
}, [open]);
|
|
44500
|
+
};
|
|
44501
|
+
|
|
44502
|
+
const useOpenControllerByProps = (props, name) => {
|
|
44503
|
+
const { onClose } = props;
|
|
44504
|
+
// Lets an uncontrolled consumer (no openController of its own) still react
|
|
44505
|
+
// to a self-initiated close (Escape, backdrop click, its own close button)
|
|
44506
|
+
// without having to own a controller just to observe it — onClose is
|
|
44507
|
+
// called on every real close, matching createOpenController's own
|
|
44508
|
+
// { onRequestClose, onClose } contract (never denies the close itself).
|
|
44509
|
+
const openController = useOpenController(() =>
|
|
44510
|
+
onClose ? { onClose } : undefined,
|
|
44511
|
+
);
|
|
44512
|
+
useOpenPropsEffectOnOpenController(openController, props, name);
|
|
44513
|
+
return openController;
|
|
44514
|
+
};
|
|
44515
|
+
|
|
44516
|
+
/**
|
|
44517
|
+
* Runs `fn` and commits whatever it re-renders before returning, instead of
|
|
44518
|
+
* letting Preact batch it into the next microtask. Layout effects of what gets
|
|
44519
|
+
* mounted run inside the call too, exactly as they would on any other commit.
|
|
44520
|
+
*
|
|
44521
|
+
* For the caller that has to read the DOM it just asked for — measuring an
|
|
44522
|
+
* element whose content it mounts in the same breath — and cannot wait a tick
|
|
44523
|
+
* to do it, because what comes after is a browser event still in flight
|
|
44524
|
+
* (preventDefault, focus placement) that no longer accepts being answered late.
|
|
44525
|
+
*
|
|
44526
|
+
* `options.debounceRendering` is Preact's own hook for deciding *when* the
|
|
44527
|
+
* render queue drains; swapping it for "right now" for the duration of the call
|
|
44528
|
+
* is exactly how preact/compat implements React's flushSync. Reserve it for the
|
|
44529
|
+
* case above: rendering synchronously in the middle of an event gives up the
|
|
44530
|
+
* batching that makes several state changes one commit.
|
|
44531
|
+
*/
|
|
44532
|
+
const flushSyncRendering = (fn) => {
|
|
44533
|
+
const debounceRenderingPrevious = options.debounceRendering;
|
|
44534
|
+
options.debounceRendering = (drainRenderQueue) => {
|
|
44535
|
+
drainRenderQueue();
|
|
44536
|
+
};
|
|
44537
|
+
try {
|
|
44538
|
+
fn();
|
|
44539
|
+
} finally {
|
|
44540
|
+
options.debounceRendering = debounceRenderingPrevious;
|
|
44541
|
+
}
|
|
44542
|
+
};
|
|
44543
|
+
|
|
44544
|
+
/**
|
|
44545
|
+
* Small, renderer-agnostic helpers shared by Popover and Dialog's own custom
|
|
44546
|
+
* (non-top-layer) renderers — operate on a plain DOM element, no knowledge
|
|
44547
|
+
* of which of the two owns it.
|
|
44548
|
+
*/
|
|
44549
|
+
|
|
44550
|
+
|
|
44551
|
+
/**
|
|
44552
|
+
* Whether a visibleRectEffect delivery is one that can have taken height away
|
|
44553
|
+
* from a popup, and so pushed whatever holds focus out of sight:
|
|
44554
|
+
* - "resize": the window/visual viewport settled — which is also how the
|
|
44555
|
+
* on-screen keyboard arrives, overlay or not (window_size.js);
|
|
44556
|
+
* - ELEMENT_SIZE_CHANGE: the popup's own box measured different;
|
|
44557
|
+
* - "focusin": the focus-settled re-measure, for the room that changes with
|
|
44558
|
+
* nothing announcing it (subscribeFocusSettled in window_size.js).
|
|
44559
|
+
*
|
|
44560
|
+
* Everything else is a scroll of one kind or another, where nothing got
|
|
44561
|
+
* smaller and scrolling the focused element back would fight the very gesture
|
|
44562
|
+
* that fired it.
|
|
44563
|
+
*/
|
|
44564
|
+
const mayHaveHiddenFocus = (event) => {
|
|
44565
|
+
const type = event?.type;
|
|
44566
|
+
return (
|
|
44567
|
+
type === "resize" || type === ELEMENT_SIZE_CHANGE || type === "focusin"
|
|
44568
|
+
);
|
|
44569
|
+
};
|
|
44570
|
+
|
|
44571
|
+
/**
|
|
44572
|
+
* Scrolls whatever holds focus inside `popupEl` back into view, if the popup
|
|
44573
|
+
* getting shorter has pushed it out.
|
|
44574
|
+
*
|
|
44575
|
+
* The case this exists for: a field low in the scrolling body of a popup that
|
|
44576
|
+
* also has a footer (box.jsx — with a body, the body is the only thing that
|
|
44577
|
+
* scrolls and the footer is a sibling sitting right under it). Focusing the
|
|
44578
|
+
* field makes the browser scroll it into view, which it does against the
|
|
44579
|
+
* popup's height AT THAT MOMENT; the on-screen keyboard then opens and takes
|
|
44580
|
+
* that height away. The body shrinks, its scrollTop does not move, so the
|
|
44581
|
+
* content slides down relative to the shorter scrollport and the field ends up
|
|
44582
|
+
* past its bottom edge — visually, swallowed by the footer. The browser does
|
|
44583
|
+
* not redo a scroll-into-view it already answered, so this does.
|
|
44584
|
+
*
|
|
44585
|
+
* Scoped to the field's own scroll container (never the page): a popup traps
|
|
44586
|
+
* scrolling precisely so the document underneath cannot move, and a plain
|
|
44587
|
+
* scrollIntoView walks past a container whose scrollbar isn't visible — see
|
|
44588
|
+
* scrollIntoViewScoped's own doc.
|
|
44589
|
+
*
|
|
44590
|
+
* "nearest": the smallest scroll that makes it visible, and none at all when
|
|
44591
|
+
* it already is — so this is free to call on every resize, and never fights
|
|
44592
|
+
* where the user had scrolled to.
|
|
44593
|
+
*/
|
|
44594
|
+
const keepFocusedElementVisible = (popupEl) => {
|
|
44595
|
+
const { activeElement } = document;
|
|
44596
|
+
if (!activeElement || activeElement === popupEl) {
|
|
44597
|
+
return;
|
|
44598
|
+
}
|
|
44599
|
+
if (!popupEl.contains(activeElement)) {
|
|
44600
|
+
return;
|
|
44601
|
+
}
|
|
44602
|
+
scrollIntoViewScoped(activeElement, { block: "nearest" });
|
|
44603
|
+
};
|
|
44604
|
+
|
|
44605
|
+
/**
|
|
44606
|
+
* Calls `onSettled` once `el`'s current CSS transition is over — via
|
|
44607
|
+
* `transitionend`, with a safety `setTimeout` fallback matching the longest
|
|
44608
|
+
* `transition-duration`, in case nothing actually transitions or an event is
|
|
44609
|
+
* missed.
|
|
44610
|
+
*
|
|
44611
|
+
* Returns a "cancel" function, so a caller whose instance has been superseded
|
|
44612
|
+
* (a fresh open/close about to set its own state) can keep this stale one from
|
|
44613
|
+
* firing later. Cancelling only stops `onSettled`: undoing whatever the caller
|
|
44614
|
+
* did up front is that fresh call's business, not this one's.
|
|
44615
|
+
*/
|
|
44616
|
+
const whenTransitionSettles = (el, onSettled) => {
|
|
44617
|
+
let settled = false;
|
|
44618
|
+
const onTransitionEnd = (transitionEvent) => {
|
|
44619
|
+
if (transitionEvent.target === el) {
|
|
44620
|
+
finish();
|
|
44621
|
+
}
|
|
44622
|
+
};
|
|
44623
|
+
const stopWatching = () => {
|
|
44624
|
+
settled = true;
|
|
44625
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
44626
|
+
clearTimeout(safetyTimeoutId);
|
|
44627
|
+
};
|
|
44628
|
+
const finish = () => {
|
|
44629
|
+
if (settled) {
|
|
44630
|
+
return;
|
|
44631
|
+
}
|
|
44632
|
+
stopWatching();
|
|
44633
|
+
onSettled();
|
|
44634
|
+
};
|
|
44635
|
+
el.addEventListener("transitionend", onTransitionEnd);
|
|
44636
|
+
const durationsInSeconds = getComputedStyle(el)
|
|
44637
|
+
.transitionDuration.split(",")
|
|
44638
|
+
.map((value) => parseFloat(value) || 0);
|
|
44639
|
+
const longestDurationMs = Math.max(0, ...durationsInSeconds) * 1000;
|
|
44640
|
+
const safetyTimeoutId = setTimeout(finish, longestDurationMs + 50);
|
|
44641
|
+
return () => {
|
|
44642
|
+
if (settled) {
|
|
44643
|
+
return;
|
|
44644
|
+
}
|
|
44645
|
+
stopWatching();
|
|
44646
|
+
};
|
|
44647
|
+
};
|
|
44648
|
+
|
|
44649
|
+
/**
|
|
44650
|
+
* Disables pointer-events on `el` until its current CSS transition settles —
|
|
44651
|
+
* avoids the cursor changing/something becoming clickable while the popup is
|
|
44652
|
+
* still visually moving into or out of place.
|
|
44653
|
+
*
|
|
44654
|
+
* Returns whenTransitionSettles' own "cancel" function: it doesn't restore
|
|
44655
|
+
* pointer-events, since a fresh call for the next open/close is about to set
|
|
44656
|
+
* its own state.
|
|
44657
|
+
*/
|
|
44658
|
+
const suppressPointerEventsDuringTransition = (el) => {
|
|
44659
|
+
el.style.pointerEvents = "none";
|
|
44660
|
+
return whenTransitionSettles(el, () => {
|
|
44661
|
+
el.style.pointerEvents = "";
|
|
44662
|
+
});
|
|
44663
|
+
};
|
|
44664
|
+
|
|
44665
|
+
/**
|
|
44666
|
+
* Hides the backdrop, deferring until the browser's matching "click" fires
|
|
44667
|
+
* when `closeEvent` was triggered by a mousedown (see popover.jsx's top
|
|
44668
|
+
* comment for why) — same capture-phase-on-document pattern as
|
|
44669
|
+
* armSuppressNextOpenRequest in open_controller.js, which a plain timeout
|
|
44670
|
+
* can't safely replace: mouseup (and the click that follows it) can land an
|
|
44671
|
+
* arbitrarily long time after mousedown (the user is still holding the
|
|
44672
|
+
* button down), so a short timeout can fire first and hide the backdrop
|
|
44673
|
+
* before its own click ever arrives. A capture-phase listener on document
|
|
44674
|
+
* fires for every click regardless of what any bubble-phase handler does
|
|
44675
|
+
* downstream, so no fallback timer is needed.
|
|
44676
|
+
*
|
|
44677
|
+
* `hide` is the caller's own way to actually hide the backdrop
|
|
44678
|
+
* (`hidePopover()` for a top-layer backdrop, a plain `style.display = "none"`
|
|
44679
|
+
* for a plain div) — this helper only owns the mousedown/click timing.
|
|
44680
|
+
*
|
|
44681
|
+
* Returns a disarm function (or undefined if hidden immediately), so a
|
|
44682
|
+
* fresh open can cancel a pending hide it's about to make redundant.
|
|
44683
|
+
*/
|
|
44684
|
+
const armPointerDownOutsideClose = (closeEvent, hide) => {
|
|
44685
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
44686
|
+
if (!mousedownEvent) {
|
|
44687
|
+
hide();
|
|
44688
|
+
return undefined;
|
|
44689
|
+
}
|
|
44690
|
+
const onClick = () => {
|
|
44691
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
44692
|
+
hide();
|
|
44693
|
+
};
|
|
44694
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
44695
|
+
return () => {
|
|
44696
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
44697
|
+
};
|
|
44698
|
+
};
|
|
44699
|
+
|
|
44700
|
+
/**
|
|
44701
|
+
* Maps a positionArea y/x pair to a concrete `navi-animation` value (a
|
|
44702
|
+
* `prefix` plus a direction word), or `null` if both axes overlap the anchor
|
|
44703
|
+
* (no direction at all — that's `resolvedAnimationKind === "scaling"`
|
|
44704
|
+
* territory instead, see resolveAutoAnimationKind below).
|
|
44705
|
+
*
|
|
44706
|
+
* `prefix: "slide-from"` (used with no real anchor — Dialog always, Popover
|
|
44707
|
+
* when docked) keeps the word as the compass direction the popup comes
|
|
44708
|
+
* from: placed "top" (a point/corner), it slides in from the top.
|
|
44709
|
+
* `prefix: "expand"` (a real anchor, Popover-only) uses the motion/growth
|
|
44710
|
+
* direction instead, the opposite compass point: placed "top" of the
|
|
44711
|
+
* anchor, it moves/grows up, away from the anchor (which sits below it).
|
|
44712
|
+
*
|
|
44713
|
+
* "inset-*"/"center" contribute no direction on their axis either way.
|
|
44714
|
+
*/
|
|
44715
|
+
const resolveDirectionValue = (y, x, { prefix }) => {
|
|
44716
|
+
const yWord =
|
|
44717
|
+
y === "top"
|
|
44718
|
+
? prefix === "expand"
|
|
44719
|
+
? "up"
|
|
44720
|
+
: "top"
|
|
44721
|
+
: y === "bottom"
|
|
44722
|
+
? prefix === "expand"
|
|
44723
|
+
? "down"
|
|
44724
|
+
: "bottom"
|
|
44725
|
+
: null;
|
|
44726
|
+
const xWord = x === "left" ? "left" : x === "right" ? "right" : null;
|
|
44727
|
+
if (!yWord && !xWord) {
|
|
44728
|
+
return null;
|
|
44729
|
+
}
|
|
44730
|
+
return yWord && xWord
|
|
44731
|
+
? `${prefix}-${yWord}-${xWord}`
|
|
44732
|
+
: `${prefix}-${yWord || xWord}`;
|
|
44733
|
+
};
|
|
44734
|
+
|
|
44735
|
+
/**
|
|
44736
|
+
* Shared `animation="auto"`/`true` resolution: "scaling" reads best overall
|
|
44737
|
+
* — picked for any real anchor, or for a point/corner placed dead-center
|
|
44738
|
+
* (both positionArea axes overlapping — there's no sensible direction to
|
|
44739
|
+
* slide from in that case). "sliding" otherwise. `anchor` is `undefined`
|
|
44740
|
+
* for any no-anchor/docked case (Dialog always, Popover's own custom
|
|
44741
|
+
* renderer when there's no real anchor), so this collapses to "scaling"
|
|
44742
|
+
* there only for the dead-center case, "sliding" otherwise. The two
|
|
44743
|
+
* "overlapping" booleans below describe the *positionArea* itself (a bare
|
|
44744
|
+
* word vs. "inset-"/"center"), not anything about the anchor — they'd
|
|
44745
|
+
* mean exactly the same thing even with no anchor at all, since it's the
|
|
44746
|
+
* position strategy, not the anchor, that decides whether there's a
|
|
44747
|
+
* direction to slide from.
|
|
44748
|
+
*/
|
|
44749
|
+
const resolveAutoAnimationKind = (anchor, parsedPositionArea) => {
|
|
44750
|
+
const yIsOverlapping =
|
|
44751
|
+
parsedPositionArea.y !== "top" && parsedPositionArea.y !== "bottom";
|
|
44752
|
+
const xIsOverlapping =
|
|
44753
|
+
parsedPositionArea.x !== "left" && parsedPositionArea.x !== "right";
|
|
44754
|
+
return anchor || (yIsOverlapping && xIsOverlapping) ? "scaling" : "sliding";
|
|
44755
|
+
};
|
|
44756
|
+
|
|
44757
|
+
/**
|
|
44758
|
+
* When a popup builds what it holds, and when it throws it away.
|
|
44759
|
+
*
|
|
44760
|
+
* A closed popup shows nothing, focuses nothing, and answers nothing: what it
|
|
44761
|
+
* holds is out of reach until it opens. Building that content at mount time
|
|
44762
|
+
* means a page carrying a handful of closed popups pays, on the very render
|
|
44763
|
+
* that decides how fast it appears, for content nobody has asked for — and
|
|
44764
|
+
* pays again on every subsequent measurement, since each of those nodes makes
|
|
43862
44765
|
* the document the rest of the page queries bigger.
|
|
43863
44766
|
*
|
|
43864
44767
|
* So the content is built when the popup first opens, and stays built from
|
|
@@ -43898,7 +44801,7 @@ const MOUNT_DEFAULT = "from-first-open";
|
|
|
43898
44801
|
const usePopupContentMount = (
|
|
43899
44802
|
openController,
|
|
43900
44803
|
ref,
|
|
43901
|
-
{
|
|
44804
|
+
{ mount = MOUNT_DEFAULT },
|
|
43902
44805
|
) => {
|
|
43903
44806
|
const mountedAlways = mount === "always";
|
|
43904
44807
|
const [contentMounted, setContentMounted] = useState(
|
|
@@ -43938,30 +44841,48 @@ const usePopupContentMount = (
|
|
|
43938
44841
|
}
|
|
43939
44842
|
}, [mountedAlways]);
|
|
43940
44843
|
|
|
43941
|
-
return contentMounted
|
|
44844
|
+
return contentMounted;
|
|
43942
44845
|
};
|
|
43943
44846
|
|
|
43944
44847
|
installImportMetaCssBuild(import.meta);/**
|
|
43945
44848
|
* Expandable: an in-flow disclosure — a UI part that reveals a content part.
|
|
43946
44849
|
* It covers the same ground as <Details> with structural differences:
|
|
43947
44850
|
*
|
|
43948
|
-
* - the two parts are explicit and
|
|
44851
|
+
* - the two parts are explicit, and are Boxes:
|
|
43949
44852
|
*
|
|
43950
44853
|
* <Expandable>
|
|
43951
44854
|
* <Expandable.UI>See more</Expandable.UI>
|
|
43952
44855
|
* <Expandable.Content>…</Expandable.Content>
|
|
43953
44856
|
* </Expandable>
|
|
43954
44857
|
*
|
|
43955
|
-
*
|
|
43956
|
-
*
|
|
43957
|
-
*
|
|
43958
|
-
*
|
|
43959
|
-
*
|
|
43960
|
-
*
|
|
44858
|
+
* `openDirection` decides where the content is revealed — down (the
|
|
44859
|
+
* <details> shape) or up, left or right in `layout="column"`, where the
|
|
44860
|
+
* parts sit side by side sharing their height. It places the parts too
|
|
44861
|
+
* (CSS `order`), so an expandable revealing upward can still be written UI
|
|
44862
|
+
* first, and an expandable with no UI part at all — driven from
|
|
44863
|
+
* `open`/`signal` by a toggle of the app's own — can say which way it
|
|
44864
|
+
* opens. The marker chevron follows: it points right while closed and
|
|
44865
|
+
* toward where the content went while open (down, up, or left). The common
|
|
44866
|
+
* shape has a shorthand: `ui` prop + children as content.
|
|
44867
|
+
*
|
|
43961
44868
|
* - the UI part is the focusable toggle itself (role button, Space/Enter,
|
|
43962
44869
|
* arrow keys) and accepts any markup: controls inside it keep their own
|
|
43963
44870
|
* behavior, the marker is purely decorative.
|
|
43964
44871
|
*
|
|
44872
|
+
* The root never looks at its children: which parts are there, in which order,
|
|
44873
|
+
* is none of its business. It publishes a context and the parts take what they
|
|
44874
|
+
* need from it.
|
|
44875
|
+
*
|
|
44876
|
+
* Deciding to open or to close is the same decision a Dialog or a Popover
|
|
44877
|
+
* takes — the same props behind it (`open`/`defaultOpen`/`signal`/`navState`),
|
|
44878
|
+
* the same commands in front of it, the same refusable close — so it is taken
|
|
44879
|
+
* in the same place: an open controller (see open_controller.js). What belongs
|
|
44880
|
+
* to an expandable is only what opening LOOKS like, which is `openEffect` and
|
|
44881
|
+
* the cleanup it returns. Both render synchronously (flushSyncRendering)
|
|
44882
|
+
* before measuring, so the DOM is in the state they are about to measure —
|
|
44883
|
+
* that, and not any inspection of the children, is what makes a part free to
|
|
44884
|
+
* re-render later than the root (a memoized subtree does exactly that).
|
|
44885
|
+
*
|
|
43965
44886
|
* Reach for it knowingly: expanding in-flow SHIFTS the layout — everything
|
|
43966
44887
|
* below (or beside) moves when it opens. A Popover, Dialog, Picker or Callout
|
|
43967
44888
|
* answers the same click on its own layer, moving nothing, which is usually
|
|
@@ -43980,17 +44901,18 @@ installImportMetaCssBuild(import.meta);/**
|
|
|
43980
44901
|
* and answer the `navi_command`/`navi_request_open`/`navi_request_close`
|
|
43981
44902
|
* events.
|
|
43982
44903
|
*
|
|
43983
|
-
* Content is not built until the first expansion and stays built afterwards
|
|
43984
|
-
*
|
|
43985
|
-
* `"always"` builds it right away, `"while-opened"`
|
|
43986
|
-
* collapse settles
|
|
44904
|
+
* Content is not built until the first expansion and stays built afterwards:
|
|
44905
|
+
* a popup's policy, and its code (`usePopupContentMount`, see
|
|
44906
|
+
* popup_content_mount.js) — `"always"` builds it right away, `"while-opened"`
|
|
44907
|
+
* throws it away once the collapse settles, so a closing animation still plays
|
|
44908
|
+
* on real content.
|
|
43987
44909
|
*
|
|
43988
44910
|
* The animation is a REVEAL, not a resize: the expandable's own footprint
|
|
43989
44911
|
* grows/shrinks progressively (the content's grid track interpolates
|
|
43990
44912
|
* 0fr <-> 1fr — rows for the stacked layout, columns for `layout="column"`),
|
|
43991
44913
|
* but the content inside is laid out at its final size for the whole movement
|
|
43992
|
-
* (its animated dimension is frozen to the measured final value, see
|
|
43993
|
-
*
|
|
44914
|
+
* (its animated dimension is frozen to the measured final value, see
|
|
44915
|
+
* freezeContentSize) and the container simply uncovers it. Text never rewraps
|
|
43994
44916
|
* mid-animation. The content is revealed from its UI side (pinned against the
|
|
43995
44917
|
* UI when it comes first). Once settled open the clipping is released, so a
|
|
43996
44918
|
* popover or focus ring inside is not cut at the edges.
|
|
@@ -44001,6 +44923,10 @@ const css$Q = /* css */`.navi_expandable {
|
|
|
44001
44923
|
display: flex;
|
|
44002
44924
|
position: relative;
|
|
44003
44925
|
|
|
44926
|
+
&[data-open-direction="down"] > .navi_expandable_content_container, &[data-open-direction="right"] > .navi_expandable_content_container, &[data-open-direction="up"] > .navi_expandable_ui, &[data-open-direction="left"] > .navi_expandable_ui {
|
|
44927
|
+
order: 1;
|
|
44928
|
+
}
|
|
44929
|
+
|
|
44004
44930
|
& > .navi_expandable_ui {
|
|
44005
44931
|
cursor: pointer;
|
|
44006
44932
|
user-select: none;
|
|
@@ -44046,7 +44972,7 @@ const css$Q = /* css */`.navi_expandable {
|
|
|
44046
44972
|
grid-template-rows: 1fr;
|
|
44047
44973
|
}
|
|
44048
44974
|
|
|
44049
|
-
&[data-
|
|
44975
|
+
&[data-open-direction="up"] > .navi_expandable_content_container {
|
|
44050
44976
|
clip-path: inset(0 -9999px -9999px);
|
|
44051
44977
|
align-content: end;
|
|
44052
44978
|
|
|
@@ -44087,7 +45013,7 @@ const css$Q = /* css */`.navi_expandable {
|
|
|
44087
45013
|
grid-template-rows: none;
|
|
44088
45014
|
}
|
|
44089
45015
|
|
|
44090
|
-
&[data-
|
|
45016
|
+
&[data-open-direction="left"] > .navi_expandable_content_container {
|
|
44091
45017
|
clip-path: inset(-9999px -9999px 0 0);
|
|
44092
45018
|
justify-content: end;
|
|
44093
45019
|
|
|
@@ -44138,11 +45064,14 @@ const useExpandableContext = partName => {
|
|
|
44138
45064
|
* open?: boolean,
|
|
44139
45065
|
* defaultOpen?: boolean,
|
|
44140
45066
|
* signal?: import("@preact/signals").Signal<boolean>,
|
|
45067
|
+
* navState?: boolean | string | { id?: string, type?: "push" | "replace" },
|
|
45068
|
+
* onClose?: (event: Event) => void,
|
|
44141
45069
|
* onToggle?: (event: Event) => void,
|
|
44142
45070
|
* action?: Function,
|
|
44143
45071
|
* loading?: boolean,
|
|
44144
45072
|
* animation?: boolean,
|
|
44145
45073
|
* layout?: "row" | "column",
|
|
45074
|
+
* openDirection?: "down" | "up" | "right" | "left",
|
|
44146
45075
|
* autoFocus?: boolean,
|
|
44147
45076
|
* maxContentHeight?: string | number,
|
|
44148
45077
|
* mount?: "always" | "from-first-open" | "while-opened",
|
|
@@ -44158,10 +45087,19 @@ const useExpandableContext = partName => {
|
|
|
44158
45087
|
* `<Expandable.Content>` as children instead.
|
|
44159
45088
|
* @param open - Drives the state from outside: the expandable opens/closes to
|
|
44160
45089
|
* match every change of this prop, but user interaction can still toggle it
|
|
44161
|
-
* in between
|
|
45090
|
+
* in between. Same open controller as Dialog/Popover, so the four props
|
|
45091
|
+
* below behave exactly as they do there (see open_controller.js).
|
|
44162
45092
|
* @param defaultOpen - Uncontrolled, mount-only initial state.
|
|
44163
45093
|
* @param signal - Two-way binding: the expandable follows the signal and
|
|
44164
45094
|
* writes back into it whenever it toggles on its own. Excludes `open`.
|
|
45095
|
+
* @param navState - Keeps the open state in the history entry, so a screen
|
|
45096
|
+
* left and come back to finds its sections as they were: `true` uses the
|
|
45097
|
+
* expandable's own `id`, a string names the key, `{ id, type }` chooses
|
|
45098
|
+
* between rewriting the entry ("replace", the default) and pushing one
|
|
45099
|
+
* ("push"), where closing goes back. Source of truth — excludes
|
|
45100
|
+
* `open`/`signal`.
|
|
45101
|
+
* @param onClose - Called when the expandable actually closes, whatever asked
|
|
45102
|
+
* for it. Not preventable.
|
|
44165
45103
|
* @param onToggle - Listens the "toggle" event dispatched on the root (a
|
|
44166
45104
|
* ToggleEvent with newState/oldState where supported). Fires on every actual
|
|
44167
45105
|
* state change, never on mount.
|
|
@@ -44175,15 +45113,23 @@ const useExpandableContext = partName => {
|
|
|
44175
45113
|
* @param layout - `"row"` (default): the parts stack, the content expands
|
|
44176
45114
|
* vertically. `"column"`: the parts sit side by side sharing their height,
|
|
44177
45115
|
* the content expands horizontally next to the UI part.
|
|
44178
|
-
* @param
|
|
44179
|
-
*
|
|
44180
|
-
* `
|
|
44181
|
-
*
|
|
44182
|
-
*
|
|
45116
|
+
* @param openDirection - Where the content is revealed: `"down"` (default) or
|
|
45117
|
+
* `"up"` in the stacked layout, `"right"` (default) or `"left"` in
|
|
45118
|
+
* `layout="column"`. It also places the parts, so the direction holds
|
|
45119
|
+
* whatever order they are written in — and an expandable with no
|
|
45120
|
+
* `<Expandable.UI>` at all, driven from `open`/`signal` by a toggle of the
|
|
45121
|
+
* app's own, can still say which way it opens.
|
|
45122
|
+
* @param autoFocus - Off by default (the keyboard stays where it is when the
|
|
45123
|
+
* expandable opens). `true` hands the focus to the content through the same
|
|
45124
|
+
* transfer a popup uses — the ladder picks the target (`navi-autofocus`
|
|
45125
|
+
* first, see focus_transfer.js) and closing gives the keyboard back where it
|
|
45126
|
+
* came from. An expandable that mounts already open never takes it. Whatever
|
|
45127
|
+
* the setting, closing while the focus sits inside the content hands it back
|
|
45128
|
+
* to the UI part (it would otherwise be lost to the closed, inert content).
|
|
44183
45129
|
* @param maxContentHeight - Caps the content height; taller content scrolls
|
|
44184
45130
|
* inside the expandable instead of growing it.
|
|
44185
|
-
* @param mount - When the content is built and thrown away,
|
|
44186
|
-
*
|
|
45131
|
+
* @param mount - When the content is built and thrown away, a popup's own
|
|
45132
|
+
* three values and a popup's own code (see popup_content_mount.js). `"from-first-open"` (the
|
|
44187
45133
|
* default) builds it on the first expansion and keeps it afterwards.
|
|
44188
45134
|
* `"always"` builds it right away; in layout="column" it also gives the
|
|
44189
45135
|
* closed expandable its content's height (the content is kept laid out at
|
|
@@ -44194,16 +45140,22 @@ const useExpandableContext = partName => {
|
|
|
44194
45140
|
*/
|
|
44195
45141
|
const Expandable = props => {
|
|
44196
45142
|
import.meta.css = [css$Q, "@jsenv/navi/src/control/expandable/expandable.jsx"];
|
|
45143
|
+
/* The open props are read from `props` by the open controller below; they
|
|
45144
|
+
are named here only to keep them out of `rest`, and so out of the DOM. */
|
|
45145
|
+
/* eslint-disable no-unused-vars */
|
|
44197
45146
|
const {
|
|
44198
45147
|
ref,
|
|
44199
45148
|
ui,
|
|
44200
45149
|
open,
|
|
44201
45150
|
defaultOpen,
|
|
44202
45151
|
signal,
|
|
45152
|
+
navState,
|
|
45153
|
+
onClose,
|
|
44203
45154
|
action,
|
|
44204
45155
|
loading,
|
|
44205
45156
|
animation = false,
|
|
44206
45157
|
layout,
|
|
45158
|
+
openDirection,
|
|
44207
45159
|
autoFocus,
|
|
44208
45160
|
maxContentHeight,
|
|
44209
45161
|
mount = MOUNT_DEFAULT,
|
|
@@ -44213,35 +45165,43 @@ const Expandable = props => {
|
|
|
44213
45165
|
children,
|
|
44214
45166
|
...rest
|
|
44215
45167
|
} = props;
|
|
45168
|
+
/* eslint-enable no-unused-vars */
|
|
45169
|
+
|
|
44216
45170
|
const defaultRef = useRef();
|
|
44217
45171
|
const rootRef = ref || defaultRef;
|
|
44218
45172
|
const uiRef = useRef();
|
|
44219
45173
|
const contentContainerRef = useRef();
|
|
44220
45174
|
const contentId = useId();
|
|
44221
45175
|
const isColumn = layout === "column";
|
|
44222
|
-
const
|
|
44223
|
-
const mountedWhileOpened = mount === "while-opened";
|
|
44224
|
-
const closedContentSized = isColumn && mountedAlways;
|
|
44225
|
-
// Reading .value during render is what subscribes the expandable to it.
|
|
44226
|
-
const openRequested = signal ? signal.value : open;
|
|
44227
|
-
const [opened, setOpened] = useState(() => Boolean(openRequested === undefined ? defaultOpen : openRequested));
|
|
44228
|
-
const openedRef = useRef(opened);
|
|
44229
|
-
openedRef.current = opened;
|
|
45176
|
+
const closedContentSized = isColumn && mount === "always";
|
|
44230
45177
|
const hasAction = Boolean(action);
|
|
44231
45178
|
const effectiveAction = useAction(action);
|
|
44232
45179
|
const {
|
|
44233
45180
|
loading: actionLoading
|
|
44234
45181
|
} = useActionStatus(effectiveAction);
|
|
44235
|
-
const [contentMounted, setContentMounted] = useState(() => mountedAlways || opened);
|
|
44236
45182
|
|
|
44237
|
-
//
|
|
44238
|
-
// (see
|
|
45183
|
+
// Registered before the open controller, so this cleanup runs before its own
|
|
45184
|
+
// unmount safety net (see useOpenController): a close fired on the way out
|
|
45185
|
+
// still does its bookkeeping, it just has no tree left to move.
|
|
45186
|
+
const unmountedRef = useRef(false);
|
|
45187
|
+
useLayoutEffect(() => {
|
|
45188
|
+
return () => {
|
|
45189
|
+
unmountedRef.current = true;
|
|
45190
|
+
};
|
|
45191
|
+
}, []);
|
|
45192
|
+
const openController = useOpenControllerByProps(props, "expandable");
|
|
45193
|
+
// What the controller decided, mirrored for the render (aria-expanded, the
|
|
45194
|
+
// content's inert, the marker). Written only from `openEffect` and the
|
|
45195
|
+
// cleanup it returns, and synchronously (see flushSyncRendering), so the DOM
|
|
45196
|
+
// is already in the new state when what follows measures it.
|
|
45197
|
+
const [opened, setOpened] = useState(false);
|
|
45198
|
+
// Fully open (or fully closed) and no longer moving — what releases the
|
|
45199
|
+
// clipping, see the CSS.
|
|
44239
45200
|
const [settled, setSettled] = useState(true);
|
|
45201
|
+
const contentMounted = usePopupContentMount(openController, contentContainerRef, {
|
|
45202
|
+
mount
|
|
45203
|
+
});
|
|
44240
45204
|
|
|
44241
|
-
// Read before the close touches the DOM: flipping the content to inert can
|
|
44242
|
-
// blur what it held, so by effect time the focus to hand back to the UI part
|
|
44243
|
-
// could already be gone.
|
|
44244
|
-
const focusedBeforeCloseRef = useRef(null);
|
|
44245
45205
|
// The pointer press that is about to toggle can blur the focused field
|
|
44246
45206
|
// before the click ever fires — so what held the focus has to be remembered
|
|
44247
45207
|
// at pointerdown time.
|
|
@@ -44255,10 +45215,7 @@ const Expandable = props => {
|
|
|
44255
45215
|
// released once the movement settles.
|
|
44256
45216
|
const freezeContentSize = () => {
|
|
44257
45217
|
const contentContainer = contentContainerRef.current;
|
|
44258
|
-
const contentElement = contentContainer
|
|
44259
|
-
if (!contentElement) {
|
|
44260
|
-
return;
|
|
44261
|
-
}
|
|
45218
|
+
const contentElement = contentContainer.firstElementChild.firstElementChild;
|
|
44262
45219
|
const rect = contentElement.getBoundingClientRect();
|
|
44263
45220
|
if (isColumn) {
|
|
44264
45221
|
// Both, not just the width: a max-height-capped content otherwise
|
|
@@ -44270,50 +45227,147 @@ const Expandable = props => {
|
|
|
44270
45227
|
}
|
|
44271
45228
|
};
|
|
44272
45229
|
|
|
44273
|
-
//
|
|
44274
|
-
//
|
|
44275
|
-
//
|
|
44276
|
-
|
|
44277
|
-
|
|
44278
|
-
|
|
44279
|
-
|
|
44280
|
-
|
|
44281
|
-
|
|
44282
|
-
|
|
44283
|
-
|
|
45230
|
+
// A movement still playing: what a new one cancels before starting, so a
|
|
45231
|
+
// reveal interrupted halfway does not have the previous settle release its
|
|
45232
|
+
// frozen size under it.
|
|
45233
|
+
const cancelSettleWatchRef = useRef(null);
|
|
45234
|
+
const cancelSettleWatch = () => {
|
|
45235
|
+
cancelSettleWatchRef.current?.();
|
|
45236
|
+
cancelSettleWatchRef.current = null;
|
|
45237
|
+
};
|
|
45238
|
+
const watchSettle = () => {
|
|
45239
|
+
const contentContainer = contentContainerRef.current;
|
|
45240
|
+
const contentElement = contentContainer.firstElementChild.firstElementChild;
|
|
45241
|
+
cancelSettleWatchRef.current = whenTransitionSettles(contentContainer, () => {
|
|
45242
|
+
cancelSettleWatchRef.current = null;
|
|
45243
|
+
contentElement.style.width = "";
|
|
45244
|
+
contentElement.style.height = "";
|
|
45245
|
+
setSettled(true);
|
|
45246
|
+
});
|
|
45247
|
+
};
|
|
45248
|
+
|
|
45249
|
+
// The reveal needs the content at its final size before the track starts
|
|
45250
|
+
// moving, and that size only exists in the open state — the reflow trick
|
|
45251
|
+
// (see instructions.md, CSS section), with transitions suppressed BEFORE the
|
|
45252
|
+
// first layout read: this runs pre-paint, so any earlier read would itself be
|
|
45253
|
+
// the first recalc of the open state and would start the track transition.
|
|
45254
|
+
const armReveal = startRect => {
|
|
45255
|
+
const contentContainer = contentContainerRef.current;
|
|
45256
|
+
const contentElement = contentContainer.firstElementChild.firstElementChild;
|
|
45257
|
+
contentContainer.style.transitionProperty = "none";
|
|
45258
|
+
const finalRect = contentElement.getBoundingClientRect();
|
|
45259
|
+
if (isColumn) {
|
|
45260
|
+
contentElement.style.width = `${finalRect.width}px`;
|
|
45261
|
+
contentElement.style.height = `${finalRect.height}px`;
|
|
45262
|
+
} else {
|
|
45263
|
+
contentElement.style.height = `${finalRect.height}px`;
|
|
44284
45264
|
}
|
|
44285
|
-
|
|
44286
|
-
|
|
44287
|
-
|
|
44288
|
-
|
|
44289
|
-
|
|
45265
|
+
// Put the tracks back where the last paint left them and let the
|
|
45266
|
+
// transition play from there. In fr — px does not interpolate with fr.
|
|
45267
|
+
const startFrOf = (startSize, finalSize) => finalSize > 0 ? startSize / finalSize : 0;
|
|
45268
|
+
if (isColumn) {
|
|
45269
|
+
contentContainer.style.gridTemplateColumns = `${startFrOf(startRect ? startRect.width : 0, finalRect.width)}fr`;
|
|
45270
|
+
if (!closedContentSized) {
|
|
45271
|
+
// The height opens alongside the width (a closed column expandable is
|
|
45272
|
+
// only as tall as its UI) — unless the closed content already sizes
|
|
45273
|
+
// it, where only the width has anywhere to go.
|
|
45274
|
+
contentContainer.style.gridTemplateRows = `${startFrOf(startRect ? startRect.height : 0, finalRect.height)}fr`;
|
|
44290
45275
|
}
|
|
44291
|
-
setContentMounted(true);
|
|
44292
45276
|
} else {
|
|
44293
|
-
|
|
44294
|
-
focusedBeforeCloseRef.current = !activeElement || activeElement === document.body ? focusedAtPointerDownRef.current : activeElement;
|
|
44295
|
-
if (animation) {
|
|
44296
|
-
// Now, while the content is still fully laid out — by effect time the
|
|
44297
|
-
// track is already heading to 0 (the opening case measures in the
|
|
44298
|
-
// effect instead, where the just-mounted content exists).
|
|
44299
|
-
freezeContentSize();
|
|
44300
|
-
}
|
|
44301
|
-
}
|
|
44302
|
-
focusedAtPointerDownRef.current = null;
|
|
44303
|
-
setOpened(nextOpen);
|
|
44304
|
-
// Flipped here, before the closing/opening commit, so effects of that very
|
|
44305
|
-
// commit already see the movement as started — mount="while-opened" must not
|
|
44306
|
-
// read a stale "settled" and empty the content under a closing animation.
|
|
44307
|
-
setSettled(!animation);
|
|
44308
|
-
if (signal) {
|
|
44309
|
-
signal.value = nextOpen;
|
|
45277
|
+
contentContainer.style.gridTemplateRows = `${startFrOf(startRect ? startRect.height : 0, finalRect.height)}fr`;
|
|
44310
45278
|
}
|
|
45279
|
+
// That starting frame must be genuinely rendered to transition from it,
|
|
45280
|
+
// and transitions re-enabled BEFORE the flip back to the open value —
|
|
45281
|
+
// same order as popover.jsx's own reflow trick.
|
|
45282
|
+
contentContainer.getBoundingClientRect();
|
|
45283
|
+
contentContainer.style.transitionProperty = "";
|
|
45284
|
+
contentContainer.style.gridTemplateColumns = "";
|
|
45285
|
+
contentContainer.style.gridTemplateRows = "";
|
|
45286
|
+
};
|
|
45287
|
+
|
|
45288
|
+
// What opening LOOKS like here, and how to undo it — the one thing an
|
|
45289
|
+
// expandable owns that a popup does not (see open_controller.js). Reassigned
|
|
45290
|
+
// on every render so it always closes over the latest props.
|
|
45291
|
+
openController.openEffect = openEvent => {
|
|
45292
|
+
const contentContainer = contentContainerRef.current;
|
|
45293
|
+
// `silent`: the expandable was already open when the page appeared
|
|
45294
|
+
// (`open`/`defaultOpen` at mount). Nothing changed for anyone to be told
|
|
45295
|
+
// about, nothing was ever shown closed to move away from, and a page must
|
|
45296
|
+
// not have its focus stolen by a section that was simply already open — so
|
|
45297
|
+
// an opening that was never an opening plays nothing, says nothing and
|
|
45298
|
+
// takes nothing. The action it carries still runs: its content is due.
|
|
45299
|
+
const silent = Boolean(openEvent.detail.silent);
|
|
45300
|
+
const revealing = animation && !silent && Boolean(contentContainer);
|
|
45301
|
+
// Where the last paint left the track — 0 when fully closed, partway when
|
|
45302
|
+
// reopening during a collapse — read while the DOM still says closed.
|
|
45303
|
+
const startRect = revealing ? contentContainer.getBoundingClientRect() : null;
|
|
45304
|
+
cancelSettleWatch();
|
|
45305
|
+
flushSyncRendering(() => {
|
|
45306
|
+
setOpened(true);
|
|
45307
|
+
setSettled(!revealing);
|
|
45308
|
+
});
|
|
44311
45309
|
if (hasAction) {
|
|
44312
|
-
|
|
44313
|
-
|
|
44314
|
-
|
|
45310
|
+
runUnwatched(() => effectiveAction.run());
|
|
45311
|
+
}
|
|
45312
|
+
// autoFocus off (the default): the keyboard stays where it is, and with no
|
|
45313
|
+
// transfer there is nothing to restore on close either.
|
|
45314
|
+
const restoreFocus = autoFocus && !silent && contentContainer ? openController.transferFocusOnOpen(contentContainer) : null;
|
|
45315
|
+
if (revealing) {
|
|
45316
|
+
armReveal(startRect);
|
|
45317
|
+
watchSettle();
|
|
45318
|
+
}
|
|
45319
|
+
if (!silent) {
|
|
45320
|
+
rootRef.current.dispatchEvent(createToggleEvent(true));
|
|
45321
|
+
}
|
|
45322
|
+
return closeEvent => {
|
|
45323
|
+
if (unmountedRef.current) {
|
|
45324
|
+
return;
|
|
45325
|
+
}
|
|
45326
|
+
const contentContainerAtClose = contentContainerRef.current;
|
|
45327
|
+
// Read while the content still holds what it holds: the close is about
|
|
45328
|
+
// to make it inert, which blurs whatever is inside it.
|
|
45329
|
+
const activeElement = document.activeElement;
|
|
45330
|
+
const focusedBeforeClose = !activeElement || activeElement === document.body ? focusedAtPointerDownRef.current : activeElement;
|
|
45331
|
+
focusedAtPointerDownRef.current = null;
|
|
45332
|
+
if (hasAction) {
|
|
44315
45333
|
effectiveAction.abort();
|
|
44316
45334
|
}
|
|
45335
|
+
cancelSettleWatch();
|
|
45336
|
+
const collapsing = animation && Boolean(contentContainerAtClose);
|
|
45337
|
+
if (collapsing) {
|
|
45338
|
+
// Now, while the content is still fully laid out — the collapsing
|
|
45339
|
+
// track uncovers a content frozen at that size.
|
|
45340
|
+
freezeContentSize();
|
|
45341
|
+
}
|
|
45342
|
+
flushSyncRendering(() => {
|
|
45343
|
+
setOpened(false);
|
|
45344
|
+
setSettled(!collapsing);
|
|
45345
|
+
});
|
|
45346
|
+
if (restoreFocus) {
|
|
45347
|
+
restoreFocus(closeEvent);
|
|
45348
|
+
} else if (focusedBeforeClose && contentContainerAtClose && contentContainerAtClose.contains(focusedBeforeClose)) {
|
|
45349
|
+
const uiElement = uiRef.current;
|
|
45350
|
+
if (uiElement) {
|
|
45351
|
+
moveFocusTo(uiElement);
|
|
45352
|
+
} else {
|
|
45353
|
+
// Nothing of the expandable's own can hold the keyboard (no UI
|
|
45354
|
+
// part): the focus only has to leave the content becoming inert.
|
|
45355
|
+
focusedBeforeClose.blur();
|
|
45356
|
+
}
|
|
45357
|
+
}
|
|
45358
|
+
if (collapsing) {
|
|
45359
|
+
watchSettle();
|
|
45360
|
+
}
|
|
45361
|
+
rootRef.current.dispatchEvent(createToggleEvent(false));
|
|
45362
|
+
};
|
|
45363
|
+
};
|
|
45364
|
+
const toggle = event => {
|
|
45365
|
+
if (openController.opened) {
|
|
45366
|
+
openController.requestClose(event, {
|
|
45367
|
+
isCancel: true
|
|
45368
|
+
});
|
|
45369
|
+
} else {
|
|
45370
|
+
openController.open(event);
|
|
44317
45371
|
}
|
|
44318
45372
|
};
|
|
44319
45373
|
const findFirstFocusableInContent = () => {
|
|
@@ -44321,119 +45375,11 @@ const Expandable = props => {
|
|
|
44321
45375
|
if (!contentContainer) {
|
|
44322
45376
|
return null;
|
|
44323
45377
|
}
|
|
44324
|
-
const autofocusElement = contentContainer.querySelector("[autofocus]");
|
|
44325
|
-
if (autofocusElement) {
|
|
44326
|
-
return autofocusElement;
|
|
44327
|
-
}
|
|
44328
45378
|
return findAfter(contentContainer, elementIsFocusable, {
|
|
44329
45379
|
root: contentContainer
|
|
44330
45380
|
});
|
|
44331
45381
|
};
|
|
44332
45382
|
|
|
44333
|
-
// Follow `open`/`signal` changes after mount (the initial value is already
|
|
44334
|
-
// in the state above). A self-initiated toggle that wrote the signal lands
|
|
44335
|
-
// here too and no-ops, since the state already matches.
|
|
44336
|
-
const isFirstOpenRequestedRunRef = useRef(true);
|
|
44337
|
-
useLayoutEffect(() => {
|
|
44338
|
-
if (isFirstOpenRequestedRunRef.current) {
|
|
44339
|
-
isFirstOpenRequestedRunRef.current = false;
|
|
44340
|
-
return;
|
|
44341
|
-
}
|
|
44342
|
-
if (openRequested === undefined) {
|
|
44343
|
-
return;
|
|
44344
|
-
}
|
|
44345
|
-
toggleTo(openRequested);
|
|
44346
|
-
}, [openRequested]);
|
|
44347
|
-
|
|
44348
|
-
// A state change: tell the world (the "toggle" event), move the focus, and
|
|
44349
|
-
// set up the reveal. Skipped on mount — nothing changed, so neither the
|
|
44350
|
-
// event nor a transition exists (and a page must not have its focus stolen
|
|
44351
|
-
// by an expandable that was simply already open).
|
|
44352
|
-
const isFirstOpenedRunRef = useRef(true);
|
|
44353
|
-
useLayoutEffect(() => {
|
|
44354
|
-
if (isFirstOpenedRunRef.current) {
|
|
44355
|
-
isFirstOpenedRunRef.current = false;
|
|
44356
|
-
return undefined;
|
|
44357
|
-
}
|
|
44358
|
-
const root = rootRef.current;
|
|
44359
|
-
root.dispatchEvent(createToggleEvent(opened));
|
|
44360
|
-
if (opened) {
|
|
44361
|
-
if (autoFocus) {
|
|
44362
|
-
const firstFocusableElement = findFirstFocusableInContent();
|
|
44363
|
-
if (firstFocusableElement) {
|
|
44364
|
-
firstFocusableElement.focus();
|
|
44365
|
-
}
|
|
44366
|
-
}
|
|
44367
|
-
} else {
|
|
44368
|
-
const focusedBeforeClose = focusedBeforeCloseRef.current;
|
|
44369
|
-
focusedBeforeCloseRef.current = null;
|
|
44370
|
-
if (focusedBeforeClose && contentContainerRef.current && contentContainerRef.current.contains(focusedBeforeClose)) {
|
|
44371
|
-
uiRef.current.focus();
|
|
44372
|
-
}
|
|
44373
|
-
}
|
|
44374
|
-
if (!animation) {
|
|
44375
|
-
return undefined;
|
|
44376
|
-
}
|
|
44377
|
-
const contentContainer = contentContainerRef.current;
|
|
44378
|
-
const contentElement = contentContainer.firstElementChild.firstElementChild;
|
|
44379
|
-
if (opened) {
|
|
44380
|
-
// The reveal needs the content at its final size before the track
|
|
44381
|
-
// starts moving, and the final size only exists in the open state —
|
|
44382
|
-
// the reflow trick (see instructions.md, CSS section), with transitions
|
|
44383
|
-
// suppressed BEFORE the first layout read: this effect runs pre-paint,
|
|
44384
|
-
// so any earlier read would itself be the first recalc of the open
|
|
44385
|
-
// state and would start the track transition (see revealStartSizeRef).
|
|
44386
|
-
contentContainer.style.transitionProperty = "none";
|
|
44387
|
-
const finalRect = contentElement.getBoundingClientRect();
|
|
44388
|
-
if (isColumn) {
|
|
44389
|
-
contentElement.style.width = `${finalRect.width}px`;
|
|
44390
|
-
contentElement.style.height = `${finalRect.height}px`;
|
|
44391
|
-
} else {
|
|
44392
|
-
contentElement.style.height = `${finalRect.height}px`;
|
|
44393
|
-
}
|
|
44394
|
-
// Put the tracks back where the last paint left them and let the
|
|
44395
|
-
// transition play from there. In fr — px does not interpolate with fr.
|
|
44396
|
-
const startRect = revealStartSizeRef.current;
|
|
44397
|
-
revealStartSizeRef.current = null;
|
|
44398
|
-
const startFrOf = (startSize, finalSize) => finalSize > 0 ? startSize / finalSize : 0;
|
|
44399
|
-
if (isColumn) {
|
|
44400
|
-
contentContainer.style.gridTemplateColumns = `${startFrOf(startRect ? startRect.width : 0, finalRect.width)}fr`;
|
|
44401
|
-
if (!closedContentSized) {
|
|
44402
|
-
// The height opens alongside the width (a closed column expandable
|
|
44403
|
-
// is only as tall as its UI) — unless the closed content already
|
|
44404
|
-
// sizes it, where only the width has anywhere to go.
|
|
44405
|
-
contentContainer.style.gridTemplateRows = `${startFrOf(startRect ? startRect.height : 0, finalRect.height)}fr`;
|
|
44406
|
-
}
|
|
44407
|
-
} else {
|
|
44408
|
-
contentContainer.style.gridTemplateRows = `${startFrOf(startRect ? startRect.height : 0, finalRect.height)}fr`;
|
|
44409
|
-
}
|
|
44410
|
-
// That starting frame must be genuinely rendered to transition from it,
|
|
44411
|
-
// and transitions re-enabled BEFORE the flip back to the open value —
|
|
44412
|
-
// same order as popover.jsx's own reflow trick.
|
|
44413
|
-
contentContainer.getBoundingClientRect();
|
|
44414
|
-
contentContainer.style.transitionProperty = "";
|
|
44415
|
-
contentContainer.style.gridTemplateColumns = "";
|
|
44416
|
-
contentContainer.style.gridTemplateRows = "";
|
|
44417
|
-
}
|
|
44418
|
-
// (closing froze the content in toggleTo, while it was still laid out)
|
|
44419
|
-
const cancel = whenTransitionSettles(contentContainer, () => {
|
|
44420
|
-
contentElement.style.width = "";
|
|
44421
|
-
contentElement.style.height = "";
|
|
44422
|
-
setSettled(true);
|
|
44423
|
-
});
|
|
44424
|
-
return cancel;
|
|
44425
|
-
}, [opened]);
|
|
44426
|
-
useLayoutEffect(() => {
|
|
44427
|
-
if (settled && !opened && mountedWhileOpened) {
|
|
44428
|
-
setContentMounted(false);
|
|
44429
|
-
}
|
|
44430
|
-
}, [settled, opened, mountedWhileOpened]);
|
|
44431
|
-
useLayoutEffect(() => {
|
|
44432
|
-
if (mountedAlways) {
|
|
44433
|
-
setContentMounted(true);
|
|
44434
|
-
}
|
|
44435
|
-
}, [mountedAlways]);
|
|
44436
|
-
|
|
44437
45383
|
// closedContentSized (column + mount="always"): the closed content sizes
|
|
44438
45384
|
// the height (see the CSS), which is only right if it lies at its OPEN
|
|
44439
45385
|
// width — at its natural closed width (a 0-wide track) it would wrap
|
|
@@ -44457,17 +45403,16 @@ const Expandable = props => {
|
|
|
44457
45403
|
contentContainer.getBoundingClientRect();
|
|
44458
45404
|
contentContainer.style.transitionProperty = "";
|
|
44459
45405
|
}, [closedContentSized, opened, settled, contentMounted]);
|
|
44460
|
-
|
|
44461
|
-
// Mounted already open: the content is visible, its data is due.
|
|
44462
|
-
useEffect(() => {
|
|
44463
|
-
if (openedRef.current && hasAction) {
|
|
44464
|
-
runUnwatched(() => effectiveAction.run());
|
|
44465
|
-
}
|
|
44466
|
-
}, []);
|
|
44467
45406
|
const onRootKeyDown = keyboardEvent => {
|
|
44468
45407
|
if (!arrowKeyShortcuts) {
|
|
44469
45408
|
return;
|
|
44470
45409
|
}
|
|
45410
|
+
// The shortcuts all speak about the UI part — opening from it, stepping
|
|
45411
|
+
// into the content, stepping back to it. Without one the expandable is
|
|
45412
|
+
// driven from outside and the keys belong to whatever drives it.
|
|
45413
|
+
if (!uiRef.current) {
|
|
45414
|
+
return;
|
|
45415
|
+
}
|
|
44471
45416
|
// A nested expandable (deeper, so heard first) already answered this key.
|
|
44472
45417
|
if (keyboardEvent.defaultPrevented) {
|
|
44473
45418
|
return;
|
|
@@ -44486,9 +45431,9 @@ const Expandable = props => {
|
|
|
44486
45431
|
if (document.activeElement !== uiRef.current) {
|
|
44487
45432
|
return;
|
|
44488
45433
|
}
|
|
44489
|
-
if (!
|
|
45434
|
+
if (!openController.opened) {
|
|
44490
45435
|
keyboardEvent.preventDefault();
|
|
44491
|
-
|
|
45436
|
+
openController.open(keyboardEvent);
|
|
44492
45437
|
return;
|
|
44493
45438
|
}
|
|
44494
45439
|
const firstFocusableElementInContent = findFirstFocusableInContent();
|
|
@@ -44496,20 +45441,21 @@ const Expandable = props => {
|
|
|
44496
45441
|
return;
|
|
44497
45442
|
}
|
|
44498
45443
|
keyboardEvent.preventDefault();
|
|
44499
|
-
firstFocusableElementInContent
|
|
45444
|
+
moveFocusTo(firstFocusableElementInContent);
|
|
44500
45445
|
return;
|
|
44501
45446
|
}
|
|
44502
45447
|
if (key === closeKeyShortcut) {
|
|
44503
|
-
if (!
|
|
45448
|
+
if (!openController.opened) {
|
|
44504
45449
|
return;
|
|
44505
45450
|
}
|
|
44506
45451
|
const uiElement = uiRef.current;
|
|
45452
|
+
keyboardEvent.preventDefault();
|
|
44507
45453
|
if (document.activeElement === uiElement) {
|
|
44508
|
-
|
|
44509
|
-
|
|
45454
|
+
openController.requestClose(keyboardEvent, {
|
|
45455
|
+
isCancel: true
|
|
45456
|
+
});
|
|
44510
45457
|
} else {
|
|
44511
|
-
|
|
44512
|
-
uiElement.focus();
|
|
45458
|
+
moveFocusTo(uiElement);
|
|
44513
45459
|
}
|
|
44514
45460
|
}
|
|
44515
45461
|
};
|
|
@@ -44528,7 +45474,7 @@ const Expandable = props => {
|
|
|
44528
45474
|
return;
|
|
44529
45475
|
}
|
|
44530
45476
|
}
|
|
44531
|
-
|
|
45477
|
+
toggle(clickEvent);
|
|
44532
45478
|
};
|
|
44533
45479
|
|
|
44534
45480
|
// Space/Enter on the UI part itself (role button) — a key pressed on a
|
|
@@ -44545,27 +45491,25 @@ const Expandable = props => {
|
|
|
44545
45491
|
} = keyboardEvent;
|
|
44546
45492
|
if (key === " " || key === "Enter") {
|
|
44547
45493
|
keyboardEvent.preventDefault();
|
|
44548
|
-
|
|
45494
|
+
toggle(keyboardEvent);
|
|
44549
45495
|
}
|
|
44550
45496
|
};
|
|
44551
45497
|
|
|
44552
|
-
//
|
|
44553
|
-
//
|
|
44554
|
-
|
|
44555
|
-
// the
|
|
44556
|
-
|
|
44557
|
-
|
|
44558
|
-
const
|
|
44559
|
-
const contentFirst = hasParts && firstPart.type === ExpandableContent;
|
|
44560
|
-
const openDirection = isColumn ? "left" : contentFirst ? "up" : "down";
|
|
45498
|
+
// Only the two directions the layout has room for: a column layout reveals
|
|
45499
|
+
// sideways, a stacked one vertically.
|
|
45500
|
+
const revealDirection = isColumn ? openDirection === "left" ? "left" : "right" : openDirection === "up" ? "up" : "down";
|
|
45501
|
+
// While open the marker points at the content, while closed always right.
|
|
45502
|
+
// In a column layout it points back toward the UI part whichever side the
|
|
45503
|
+
// content took, "right" being the closed direction already.
|
|
45504
|
+
const markerDirection = isColumn ? "left" : revealDirection;
|
|
44561
45505
|
const expandableContextValue = {
|
|
44562
45506
|
opened,
|
|
44563
45507
|
loading: loading || hasAction && actionLoading,
|
|
44564
45508
|
contentMounted,
|
|
44565
45509
|
hasAction,
|
|
44566
45510
|
effectiveAction,
|
|
44567
|
-
|
|
44568
|
-
|
|
45511
|
+
markerDirection,
|
|
45512
|
+
openController,
|
|
44569
45513
|
onUIClick,
|
|
44570
45514
|
onUIPointerDown,
|
|
44571
45515
|
onUIKeyDown,
|
|
@@ -44574,12 +45518,9 @@ const Expandable = props => {
|
|
|
44574
45518
|
contentId
|
|
44575
45519
|
};
|
|
44576
45520
|
|
|
44577
|
-
//
|
|
44578
|
-
//
|
|
44579
|
-
|
|
44580
|
-
// their context subscription to re-render them asynchronously — after the
|
|
44581
|
-
// [opened] effect above, which measures the content they render.
|
|
44582
|
-
const body = hasParts ? childArray.map(child => child && (child.type === ExpandableUI || child.type === ExpandableContent) ? cloneElement(child) : child) : jsxs(Fragment$1, {
|
|
45521
|
+
// The `ui` prop is the shorthand for the common shape: it names the UI part
|
|
45522
|
+
// and children are the content. Without it the children ARE the parts.
|
|
45523
|
+
const body = ui === undefined ? children : jsxs(Fragment$1, {
|
|
44583
45524
|
children: [jsx(ExpandableUI, {
|
|
44584
45525
|
children: ui
|
|
44585
45526
|
}), jsx(ExpandableContent, {
|
|
@@ -44591,7 +45532,7 @@ const Expandable = props => {
|
|
|
44591
45532
|
baseClassName: "navi_expandable",
|
|
44592
45533
|
"aria-expanded": opened ? "true" : "false",
|
|
44593
45534
|
"data-layout": isColumn ? "column" : undefined,
|
|
44594
|
-
"data-
|
|
45535
|
+
"data-open-direction": revealDirection,
|
|
44595
45536
|
"data-animation": animation ? "" : undefined,
|
|
44596
45537
|
"data-settled": settled ? "" : undefined,
|
|
44597
45538
|
"data-content-scrolls": maxContentHeight === undefined ? undefined : "",
|
|
@@ -44606,11 +45547,17 @@ const Expandable = props => {
|
|
|
44606
45547
|
},
|
|
44607
45548
|
onnavi_request_open: e => {
|
|
44608
45549
|
rest.onnavi_request_open?.(e);
|
|
44609
|
-
|
|
45550
|
+
openController.open(e);
|
|
44610
45551
|
},
|
|
44611
45552
|
onnavi_request_close: e => {
|
|
44612
45553
|
rest.onnavi_request_close?.(e);
|
|
44613
|
-
|
|
45554
|
+
const closing = openController.requestClose(e, {
|
|
45555
|
+
isCancel: e.detail?.isCancel
|
|
45556
|
+
});
|
|
45557
|
+
if (!closing) {
|
|
45558
|
+
// Said back to whoever asked: --navi-close:all stops climbing here.
|
|
45559
|
+
e.preventDefault();
|
|
45560
|
+
}
|
|
44614
45561
|
},
|
|
44615
45562
|
onKeyDown: e => {
|
|
44616
45563
|
rest.onKeyDown?.(e);
|
|
@@ -44631,32 +45578,35 @@ const Expandable = props => {
|
|
|
44631
45578
|
* The always-visible part that reveals the content: the focusable toggle
|
|
44632
45579
|
* itself (role button — click, Space/Enter, arrow keys), holding the marker
|
|
44633
45580
|
* plus whatever it is given — any markup, a function of `{ open }` included.
|
|
44634
|
-
* Controls inside it keep their own behavior and do not toggle.
|
|
44635
|
-
*
|
|
44636
|
-
* below/right; after it: content above/left).
|
|
45581
|
+
* Controls inside it keep their own behavior and do not toggle. A Box, so it
|
|
45582
|
+
* takes the layout and style props every Box takes.
|
|
44637
45583
|
*
|
|
44638
45584
|
* @type {import("ignore:preact").FunctionComponent<{
|
|
45585
|
+
* marker?: false | import("ignore:preact").ComponentChildren,
|
|
44639
45586
|
* children?: import("ignore:preact").ComponentChildren | ((state: { open: boolean }) => import("ignore:preact").ComponentChildren),
|
|
44640
45587
|
* }>}
|
|
45588
|
+
* @param marker - The chevron drawn before the label. `false` removes it (a UI
|
|
45589
|
+
* part that is already an icon has no room for one); any node replaces it.
|
|
44641
45590
|
*/
|
|
44642
45591
|
const ExpandableUI = ({
|
|
45592
|
+
marker,
|
|
44643
45593
|
children,
|
|
44644
45594
|
...rest
|
|
44645
45595
|
}) => {
|
|
44646
45596
|
const {
|
|
44647
45597
|
opened,
|
|
44648
45598
|
loading,
|
|
44649
|
-
|
|
44650
|
-
|
|
45599
|
+
markerDirection,
|
|
45600
|
+
openController,
|
|
44651
45601
|
onUIClick,
|
|
44652
45602
|
onUIPointerDown,
|
|
44653
45603
|
onUIKeyDown,
|
|
44654
45604
|
uiRef,
|
|
44655
45605
|
contentId
|
|
44656
45606
|
} = useExpandableContext("UI");
|
|
44657
|
-
return jsxs(
|
|
45607
|
+
return jsxs(Box, {
|
|
44658
45608
|
ref: uiRef,
|
|
44659
|
-
|
|
45609
|
+
baseClassName: "navi_expandable_ui",
|
|
44660
45610
|
role: "button",
|
|
44661
45611
|
tabIndex: 0,
|
|
44662
45612
|
"aria-expanded": opened,
|
|
@@ -44674,21 +45624,23 @@ const ExpandableUI = ({
|
|
|
44674
45624
|
onnavi_command: e => {
|
|
44675
45625
|
onNaviCommand(e);
|
|
44676
45626
|
},
|
|
44677
|
-
onnavi_request_open:
|
|
44678
|
-
|
|
45627
|
+
onnavi_request_open: e => {
|
|
45628
|
+
openController.open(e);
|
|
44679
45629
|
},
|
|
44680
|
-
onnavi_request_close:
|
|
44681
|
-
|
|
45630
|
+
onnavi_request_close: e => {
|
|
45631
|
+
openController.requestClose(e, {
|
|
45632
|
+
isCancel: e.detail?.isCancel
|
|
45633
|
+
});
|
|
44682
45634
|
},
|
|
44683
45635
|
...rest,
|
|
44684
|
-
children: [jsx("span", {
|
|
45636
|
+
children: [marker === false ? null : jsx("span", {
|
|
44685
45637
|
className: "navi_expandable_marker",
|
|
44686
45638
|
"aria-hidden": "true",
|
|
44687
|
-
children: jsx(SummaryMarker, {
|
|
45639
|
+
children: marker === undefined ? jsx(SummaryMarker, {
|
|
44688
45640
|
open: opened,
|
|
44689
45641
|
loading: loading,
|
|
44690
|
-
openDirection:
|
|
44691
|
-
})
|
|
45642
|
+
openDirection: markerDirection
|
|
45643
|
+
}) : marker
|
|
44692
45644
|
}), jsx("div", {
|
|
44693
45645
|
className: "navi_expandable_ui_label",
|
|
44694
45646
|
children: typeof children === "function" ? children({
|
|
@@ -44699,8 +45651,9 @@ const ExpandableUI = ({
|
|
|
44699
45651
|
};
|
|
44700
45652
|
|
|
44701
45653
|
/**
|
|
44702
|
-
* The revealed part. With an `action` on the
|
|
44703
|
-
* function `(data) => ui` or a branches object —
|
|
45654
|
+
* The revealed part, a Box like the UI part. With an `action` on the
|
|
45655
|
+
* Expandable, children may be a function `(data) => ui` or a branches object —
|
|
45656
|
+
* see ActionRenderer.
|
|
44704
45657
|
*
|
|
44705
45658
|
* @type {import("ignore:preact").FunctionComponent<{}>}
|
|
44706
45659
|
*/
|
|
@@ -44723,10 +45676,10 @@ const ExpandableContent = ({
|
|
|
44723
45676
|
children: children
|
|
44724
45677
|
});
|
|
44725
45678
|
}
|
|
44726
|
-
return jsx(
|
|
45679
|
+
return jsx(Box, {
|
|
44727
45680
|
ref: contentContainerRef,
|
|
44728
45681
|
id: contentId,
|
|
44729
|
-
|
|
45682
|
+
baseClassName: "navi_expandable_content_container",
|
|
44730
45683
|
inert: opened ? undefined : true,
|
|
44731
45684
|
...rest,
|
|
44732
45685
|
children: jsx("div", {
|
|
@@ -54346,806 +55299,212 @@ const useInputGroup = (ref) => {
|
|
|
54346
55299
|
const allSelected =
|
|
54347
55300
|
active.selectionStart === 0 &&
|
|
54348
55301
|
active.selectionEnd === active.value.length;
|
|
54349
|
-
const atEnd =
|
|
54350
|
-
allSelected ||
|
|
54351
|
-
(active.selectionStart === active.value.length &&
|
|
54352
|
-
active.selectionEnd === active.value.length);
|
|
54353
|
-
if (!atEnd) {
|
|
54354
|
-
return;
|
|
54355
|
-
}
|
|
54356
|
-
const inputs = getInputs();
|
|
54357
|
-
const idx = inputs.indexOf(active);
|
|
54358
|
-
if (idx === -1) {
|
|
54359
|
-
debugFocus(
|
|
54360
|
-
e,
|
|
54361
|
-
"InputGroup ArrowRight on non group input → do nothing",
|
|
54362
|
-
);
|
|
54363
|
-
return;
|
|
54364
|
-
}
|
|
54365
|
-
if (idx === inputs.length - 1) {
|
|
54366
|
-
debugFocus(
|
|
54367
|
-
e,
|
|
54368
|
-
"InputGroup ArrowRight at end of last input → do nothing",
|
|
54369
|
-
);
|
|
54370
|
-
return;
|
|
54371
|
-
}
|
|
54372
|
-
|
|
54373
|
-
debugFocus(
|
|
54374
|
-
e,
|
|
54375
|
-
"InputGroup ArrowRight at end of input[%d] → focus input[%d]",
|
|
54376
|
-
idx,
|
|
54377
|
-
idx + 1,
|
|
54378
|
-
);
|
|
54379
|
-
e.preventDefault();
|
|
54380
|
-
focusInput(inputs[idx + 1]);
|
|
54381
|
-
return;
|
|
54382
|
-
}
|
|
54383
|
-
const allSelected =
|
|
54384
|
-
active.selectionStart === 0 &&
|
|
54385
|
-
active.selectionEnd === active.value.length;
|
|
54386
|
-
const atStart =
|
|
54387
|
-
allSelected ||
|
|
54388
|
-
(active.selectionStart === 0 && active.selectionEnd === 0);
|
|
54389
|
-
if (!atStart) {
|
|
54390
|
-
return;
|
|
54391
|
-
}
|
|
54392
|
-
const inputs = getInputs();
|
|
54393
|
-
const idx = inputs.indexOf(active);
|
|
54394
|
-
if (idx === 0) {
|
|
54395
|
-
return;
|
|
54396
|
-
}
|
|
54397
|
-
debugFocus(
|
|
54398
|
-
e,
|
|
54399
|
-
"InputGroup ArrowLeft at start of input[%d] → focus input[%d]",
|
|
54400
|
-
idx,
|
|
54401
|
-
idx - 1,
|
|
54402
|
-
);
|
|
54403
|
-
e.preventDefault();
|
|
54404
|
-
focusInput(inputs[idx - 1]);
|
|
54405
|
-
};
|
|
54406
|
-
|
|
54407
|
-
const handleNaviInputFull = (e) => {
|
|
54408
|
-
if (!e.detail.event?.isTrusted) {
|
|
54409
|
-
// Programmatic value change (e.g. ArrowUp/Down) — don't auto-advance.
|
|
54410
|
-
return;
|
|
54411
|
-
}
|
|
54412
|
-
const input = e.detail.event.currentTarget;
|
|
54413
|
-
if (!el.contains(input)) {
|
|
54414
|
-
return;
|
|
54415
|
-
}
|
|
54416
|
-
const inputs = getInputs();
|
|
54417
|
-
const idx = inputs.indexOf(input);
|
|
54418
|
-
if (idx === -1) {
|
|
54419
|
-
return;
|
|
54420
|
-
}
|
|
54421
|
-
if (idx === inputs.length - 1) {
|
|
54422
|
-
return;
|
|
54423
|
-
}
|
|
54424
|
-
const nextInput = inputs[idx + 1];
|
|
54425
|
-
debugFocus(
|
|
54426
|
-
e,
|
|
54427
|
-
"InputGroup navi_input_full on input -> move to next input",
|
|
54428
|
-
input,
|
|
54429
|
-
nextInput,
|
|
54430
|
-
);
|
|
54431
|
-
e.preventDefault();
|
|
54432
|
-
focusInput(nextInput);
|
|
54433
|
-
};
|
|
54434
|
-
|
|
54435
|
-
// const handlePaste = (e) => {
|
|
54436
|
-
// const active = document.activeElement;
|
|
54437
|
-
// if (!isTextInputElement(active) || !el.contains(active)) {
|
|
54438
|
-
// return;
|
|
54439
|
-
// }
|
|
54440
|
-
// const inputs = getInputs();
|
|
54441
|
-
// const startIdx = inputs.indexOf(active);
|
|
54442
|
-
// if (startIdx === -1) {
|
|
54443
|
-
// return;
|
|
54444
|
-
// }
|
|
54445
|
-
// const pastedText = e.clipboardData?.getData("text") ?? "";
|
|
54446
|
-
// if (!pastedText) {
|
|
54447
|
-
// return;
|
|
54448
|
-
// }
|
|
54449
|
-
// // Only intercept when the pasted text contains at least one separator
|
|
54450
|
-
// // from the inputs starting at the focused position.
|
|
54451
|
-
// const remainingInputs = inputs.slice(startIdx);
|
|
54452
|
-
// const hasSeparatorMatch = remainingInputs.some(
|
|
54453
|
-
// (input) =>
|
|
54454
|
-
// input.dataset.separator &&
|
|
54455
|
-
// pastedText.includes(input.dataset.separator),
|
|
54456
|
-
// );
|
|
54457
|
-
// if (!hasSeparatorMatch) {
|
|
54458
|
-
// return;
|
|
54459
|
-
// }
|
|
54460
|
-
// e.preventDefault();
|
|
54461
|
-
// let remaining = pastedText;
|
|
54462
|
-
// let lastFilledIdx = startIdx;
|
|
54463
|
-
// for (let i = 0; i < remainingInputs.length; i++) {
|
|
54464
|
-
// const input = remainingInputs[i];
|
|
54465
|
-
// const separator = input.dataset.separator;
|
|
54466
|
-
// let part;
|
|
54467
|
-
// if (separator && remaining.includes(separator)) {
|
|
54468
|
-
// const sepIdx = remaining.indexOf(separator);
|
|
54469
|
-
// part = remaining.slice(0, sepIdx);
|
|
54470
|
-
// remaining = remaining.slice(sepIdx + separator.length);
|
|
54471
|
-
// } else {
|
|
54472
|
-
// part = remaining;
|
|
54473
|
-
// remaining = "";
|
|
54474
|
-
// }
|
|
54475
|
-
// requestSubPaste(input, part, e);
|
|
54476
|
-
// lastFilledIdx = startIdx + i;
|
|
54477
|
-
// if (remaining === "") {
|
|
54478
|
-
// break;
|
|
54479
|
-
// }
|
|
54480
|
-
// }
|
|
54481
|
-
// focusInput(inputs[lastFilledIdx]);
|
|
54482
|
-
// };
|
|
54483
|
-
|
|
54484
|
-
el.addEventListener("keydown", handleKeyDown, { capture: true });
|
|
54485
|
-
el.addEventListener("navi_input_full", handleNaviInputFull);
|
|
54486
|
-
// el.addEventListener("paste", handlePaste, { capture: true });
|
|
54487
|
-
return () => {
|
|
54488
|
-
el.removeEventListener("keydown", handleKeyDown, { capture: true });
|
|
54489
|
-
el.removeEventListener("navi_input_full", handleNaviInputFull);
|
|
54490
|
-
// el.removeEventListener("paste", handlePaste, { capture: true });
|
|
54491
|
-
};
|
|
54492
|
-
}, [debugFocus]);
|
|
54493
|
-
};
|
|
54494
|
-
|
|
54495
|
-
// const requestSubPaste = (input, value, event) => {
|
|
54496
|
-
// dispatchRequestInteraction(input, {
|
|
54497
|
-
// event,
|
|
54498
|
-
// name: "subpaste",
|
|
54499
|
-
// allowed: () => {
|
|
54500
|
-
// dispatchRequestSetUIState(input, value, { event });
|
|
54501
|
-
// },
|
|
54502
|
-
// });
|
|
54503
|
-
// };
|
|
54504
|
-
|
|
54505
|
-
const isTextInputElement = (el) => {
|
|
54506
|
-
if (!el) {
|
|
54507
|
-
return false;
|
|
54508
|
-
}
|
|
54509
|
-
if (el.tagName === "TEXTAREA") {
|
|
54510
|
-
return true;
|
|
54511
|
-
}
|
|
54512
|
-
if (el.tagName !== "INPUT") {
|
|
54513
|
-
return false;
|
|
54514
|
-
}
|
|
54515
|
-
const type = el.type || "text";
|
|
54516
|
-
return (
|
|
54517
|
-
type === "text" ||
|
|
54518
|
-
type === "search" ||
|
|
54519
|
-
type === "url" ||
|
|
54520
|
-
type === "tel" ||
|
|
54521
|
-
type === "email" ||
|
|
54522
|
-
type === "password" ||
|
|
54523
|
-
type === "number"
|
|
54524
|
-
);
|
|
54525
|
-
};
|
|
54526
|
-
|
|
54527
|
-
// How many lines the thing around a component gives it, when that thing caps
|
|
54528
|
-
// its own height and cannot cap what it holds.
|
|
54529
|
-
//
|
|
54530
|
-
// A <Picker> is the case it exists for: its value is clamped with maxLines,
|
|
54531
|
-
// which is CSS line-clamp — it counts line boxes of inline text and has no idea
|
|
54532
|
-
// what a wrapped flex row is. So a <BadgeList> rendered as a picker's ui reads
|
|
54533
|
-
// the number from here and caps its own rows to it, and the picker turns its
|
|
54534
|
-
// own clamp off (see .navi_picker_value:has(.navi_badge_list) in picker.jsx).
|
|
54535
|
-
const MaxLinesContext = createContext(undefined);
|
|
54536
|
-
|
|
54537
|
-
// When a component render a prop that can be anything (js value of preact element)
|
|
54538
|
-
// make sure it cannot throw during render by converting it to a string if it's not a valid preact element or a primitive value
|
|
54539
|
-
const renderSafe = (value) => {
|
|
54540
|
-
if (
|
|
54541
|
-
value === null ||
|
|
54542
|
-
value === undefined ||
|
|
54543
|
-
typeof value === "string" ||
|
|
54544
|
-
typeof value === "number" ||
|
|
54545
|
-
typeof value === "boolean"
|
|
54546
|
-
) {
|
|
54547
|
-
return value;
|
|
54548
|
-
}
|
|
54549
|
-
|
|
54550
|
-
if (isValidElement(value)) {
|
|
54551
|
-
return value;
|
|
54552
|
-
}
|
|
54553
|
-
|
|
54554
|
-
return String(value);
|
|
54555
|
-
};
|
|
54556
|
-
|
|
54557
|
-
// How long a popup waits before handing the focus to a field, when giving it
|
|
54558
|
-
// is what raises the on-screen keyboard.
|
|
54559
|
-
//
|
|
54560
|
-
// The focus is normally given as early as possible. But a popup places itself
|
|
54561
|
-
// against the viewport, and on a phone the keyboard takes a third of that
|
|
54562
|
-
// viewport away the moment a field receives focus — so the two landing in the
|
|
54563
|
-
// same tick means the popup is still arriving when the room under it changes,
|
|
54564
|
-
// and it re-places itself mid-entrance. Waiting lets it settle first, and the
|
|
54565
|
-
// keyboard then shrinks a box that has stopped moving.
|
|
54566
|
-
//
|
|
54567
|
-
// Long enough to outlast an entrance transition rather than merely reaching
|
|
54568
|
-
// the next frame: what has to be over is the popup MOVING, not one paint of
|
|
54569
|
-
// it.
|
|
54570
|
-
const FOCUS_DELAY_ON_KEYBOARD_MS = 250;
|
|
54571
|
-
|
|
54572
|
-
/**
|
|
54573
|
-
* Owns open/close decision-making for a popup (Dialog or Popover): guards
|
|
54574
|
-
* against duplicate requests and notifies the popup owner's own reactions.
|
|
54575
|
-
*
|
|
54576
|
-
* `controller.openEffect` is implemented by the controlled element (Dialog or
|
|
54577
|
-
* Popover), reassigned on every render so it always closes over the latest
|
|
54578
|
-
* props (scrollCapture, anchor, etc.). It performs whatever DOM side effects
|
|
54579
|
-
* are needed to make the element actually open (`showModal()`/`showPopover()`,
|
|
54580
|
-
* focus transfer, positioning, traps...) and returns its cleanup —
|
|
54581
|
-
* the matching side effects to sync back to closed (`close()`/
|
|
54582
|
-
* `hidePopover()`, releasing traps...). That cleanup is kept private to the
|
|
54583
|
-
* controller (not exposed as a property) and invoked when the popup actually
|
|
54584
|
-
* closes, however that happens.
|
|
54585
|
-
*
|
|
54586
|
-
* Dialog/Popover also call `openController.requestClose(e, { isCancel })` for
|
|
54587
|
-
* their own internal triggers (backdrop click, Escape).
|
|
54588
|
-
*
|
|
54589
|
-
* `openHandler` is the popup owner's own business logic, passed once to
|
|
54590
|
-
* `createOpenController`. Its return value is `{ onRequestClose, onClose }`,
|
|
54591
|
-
* in the spirit of CloseWatcher
|
|
54592
|
-
* (https://developer.mozilla.org/en-US/docs/Web/API/CloseWatcher) but with
|
|
54593
|
-
* clearer naming than its cancel/close pair:
|
|
54594
|
-
* - `onRequestClose(e)`: about to close — call `e.preventDefault()` to stay
|
|
54595
|
-
* open. Validation lives here.
|
|
54596
|
-
* - `onClose(e)`: actually closing, not preventable — final reactions live here.
|
|
54597
|
-
*
|
|
54598
|
-
* The controller exposes matching action methods:
|
|
54599
|
-
* - `open()`: requests opening — calls the caller's `onOpen` (see below), then
|
|
54600
|
-
* `mountContent`/`openEffect`, then `openHandler`.
|
|
54601
|
-
* - `requestClose()`: requests closing — calls `onRequestClose` then `onClose`,
|
|
54602
|
-
* stopping after the first if denied. The popup may choose to stay open,
|
|
54603
|
-
* which is what a `false` return says (`true`: closed, or closed already).
|
|
54604
|
-
* - `close()`: closes for real — calls only `onClose`, skipping
|
|
54605
|
-
* `onRequestClose` entirely. Used when there really is no choice (e.g. the
|
|
54606
|
-
* popup unmounting).
|
|
54607
|
-
*/
|
|
54608
|
-
const createOpenController = (
|
|
54609
|
-
openHandler,
|
|
54610
|
-
{ debugInteraction } = {},
|
|
54611
|
-
) => {
|
|
54612
|
-
let closeHandlers = null; // { onRequestClose, onClose } returned by openHandler
|
|
54613
|
-
let openEffectCleanup = null; // function returned by openEffect, undoes its DOM side effects
|
|
54614
|
-
let focusedAtClose = null; // what held the focus when the close was decided, see performClose
|
|
54615
|
-
|
|
54616
|
-
// Set true while we're waiting to see whether the click that follows a
|
|
54617
|
-
// mousedown-close will land back on whatever would reopen us — see
|
|
54618
|
-
// armSuppressNextOpenRequest below.
|
|
54619
|
-
let suppressNextOpenRequest = false;
|
|
54620
|
-
let disarmSuppressNextOpenRequest = null;
|
|
54621
|
-
|
|
54622
|
-
// When the popup closes because of a mousedown (e.g. clicking the
|
|
54623
|
-
// backdrop), the browser still dispatches the matching "click" afterward.
|
|
54624
|
-
// If that click lands back on the element that triggers open() (e.g. the
|
|
54625
|
-
// picker button), it would immediately reopen the popup. We cannot
|
|
54626
|
-
// preventDefault/stopPropagation the mousedown to stop that — the browser
|
|
54627
|
-
// dispatches the click regardless.
|
|
54628
|
-
//
|
|
54629
|
-
// Instead: arm a capture-phase "click" listener on document. Capture fires
|
|
54630
|
-
// before the click reaches its target, so by the time any bubble-phase
|
|
54631
|
-
// click handler (e.g. the trigger button's onClick, which calls
|
|
54632
|
-
// controller.open()) runs, `suppressNextOpenRequest` is already true and
|
|
54633
|
-
// open() ignores the request — no need to know *which* element triggers
|
|
54634
|
-
// it. A bubble-phase listener (runs after everything else, once the click
|
|
54635
|
-
// reaches document) clears the flag if nothing consumed it, meaning this
|
|
54636
|
-
// click never resulted in an open() call. A timeout is a last-resort safety
|
|
54637
|
-
// net in case the click never reaches document at all (e.g. some ancestor
|
|
54638
|
-
// called stopPropagation()) — a *task*, never a microtask: a microtask
|
|
54639
|
-
// checkpoint runs between two listeners of the same trusted event dispatch,
|
|
54640
|
-
// so it would clear the flag before the bubble-phase handler this is meant
|
|
54641
|
-
// to block ever runs, which is precisely the case it exists for.
|
|
54642
|
-
const armSuppressNextOpenRequest = () => {
|
|
54643
|
-
disarmSuppressNextOpenRequest?.();
|
|
54644
|
-
let safetyTimeout = null;
|
|
54645
|
-
const onCaptureClick = () => {
|
|
54646
|
-
document.removeEventListener("click", onCaptureClick, {
|
|
54647
|
-
capture: true,
|
|
54648
|
-
});
|
|
54649
|
-
suppressNextOpenRequest = true;
|
|
54650
|
-
document.addEventListener("click", onBubbleClick);
|
|
54651
|
-
safetyTimeout = setTimeout(() => {
|
|
54652
|
-
suppressNextOpenRequest = false;
|
|
54653
|
-
});
|
|
54654
|
-
};
|
|
54655
|
-
const onBubbleClick = () => {
|
|
54656
|
-
document.removeEventListener("click", onBubbleClick);
|
|
54657
|
-
clearTimeout(safetyTimeout);
|
|
54658
|
-
suppressNextOpenRequest = false;
|
|
54659
|
-
};
|
|
54660
|
-
disarmSuppressNextOpenRequest = () => {
|
|
54661
|
-
clearTimeout(safetyTimeout);
|
|
54662
|
-
document.removeEventListener("click", onCaptureClick, {
|
|
54663
|
-
capture: true,
|
|
54664
|
-
});
|
|
54665
|
-
document.removeEventListener("click", onBubbleClick);
|
|
54666
|
-
};
|
|
54667
|
-
document.addEventListener("click", onCaptureClick, { capture: true });
|
|
54668
|
-
};
|
|
54669
|
-
|
|
54670
|
-
const performClose = (closeEvent) => {
|
|
54671
|
-
controller.opened = false;
|
|
54672
|
-
// Read before any close effect touches the DOM: closing a native <dialog>
|
|
54673
|
-
// hands the focus back to whatever held it at showModal() time, so by the
|
|
54674
|
-
// time the close cleanup runs, the popup's content has already lost the
|
|
54675
|
-
// focus and could not be remembered for the next open.
|
|
54676
|
-
focusedAtClose = document.activeElement;
|
|
54677
|
-
|
|
54678
|
-
prevent_reopen: {
|
|
54679
|
-
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
54680
|
-
if (mousedownEvent) {
|
|
54681
|
-
debugInteraction(
|
|
54682
|
-
closeEvent,
|
|
54683
|
-
`closed by mousedown -> ignore next click`,
|
|
54684
|
-
);
|
|
54685
|
-
armSuppressNextOpenRequest();
|
|
54686
|
-
break prevent_reopen;
|
|
54687
|
-
}
|
|
54688
|
-
|
|
54689
|
-
// The keyboard counterpart of the mousedown case above: a key press that
|
|
54690
|
-
// closes the popup and then goes on to activate the trigger, reopening it
|
|
54691
|
-
// on the spot. Space and Enter both get there, but not the same way and
|
|
54692
|
-
// not always — preventing the key unconditionally would eat presses that
|
|
54693
|
-
// were never going to activate anything (a space typed in a field, an
|
|
54694
|
-
// Enter the popup's own handler already consumed), so each is verified
|
|
54695
|
-
// before being prevented.
|
|
54696
|
-
|
|
54697
|
-
// Space: pressed on the trigger itself, which still has focus (closing
|
|
54698
|
-
// does not move it away from an element outside the popup). The browser
|
|
54699
|
-
// turns that press into a click on keyup, and that click lands back on
|
|
54700
|
-
// the trigger. Asked of the browser's own default action rather than
|
|
54701
|
-
// guessed from the tag name: a space that scrolls, or types into a field
|
|
54702
|
-
// inside the popup, has no activation to prevent and preventing it would
|
|
54703
|
-
// swallow the scroll / the character.
|
|
54704
|
-
const spaceKeyEvent = findEvent(
|
|
54705
|
-
closeEvent,
|
|
54706
|
-
(e) => e.type === "keydown" && e.key === " ",
|
|
54707
|
-
);
|
|
54708
|
-
if (
|
|
54709
|
-
spaceKeyEvent &&
|
|
54710
|
-
getKeyboardEventDefaultAction(spaceKeyEvent) === "activate"
|
|
54711
|
-
) {
|
|
54712
|
-
debugInteraction(
|
|
54713
|
-
closeEvent,
|
|
54714
|
-
`closed by space on <${spaceKeyEvent.target.tagName.toLowerCase()}> -> prevent the click it would produce (space.preventDefault())`,
|
|
54715
|
-
);
|
|
54716
|
-
// The browser won't dispatch the click, and our "space_to_open" sees
|
|
54717
|
-
// defaultPrevented too so it won't try to open the picker either.
|
|
54718
|
-
spaceKeyEvent.preventDefault();
|
|
54719
|
-
break prevent_reopen;
|
|
54720
|
-
}
|
|
54721
|
-
|
|
54722
|
-
// Enter: pressed inside the popup (its own submit button, or implicit
|
|
54723
|
-
// submission from a field it contains). The popup closes synchronously
|
|
54724
|
-
// and focus is restored to the trigger, so the activation the browser
|
|
54725
|
-
// still owes this press is delivered to the trigger instead.
|
|
54726
|
-
//
|
|
54727
|
-
// Verified on two counts: the press still owes an activation (the
|
|
54728
|
-
// browser's default action for it is one — "activate" on a submit button,
|
|
54729
|
-
// "form_submit" on a field — and nothing has consumed it yet), and it came
|
|
54730
|
-
// from inside the popup. An Enter from outside is not this case at all.
|
|
54731
|
-
const enterKeyEvent = findEvent(
|
|
54732
|
-
closeEvent,
|
|
54733
|
-
(e) => e.type === "keydown" && e.key === "Enter",
|
|
54734
|
-
);
|
|
54735
|
-
if (
|
|
54736
|
-
enterKeyEvent &&
|
|
54737
|
-
!enterKeyEvent.defaultPrevented &&
|
|
54738
|
-
ENTER_ACTIVATING_DEFAULT_ACTION_SET.has(
|
|
54739
|
-
getKeyboardEventDefaultAction(enterKeyEvent),
|
|
54740
|
-
) &&
|
|
54741
|
-
isInsideOpenPopup(enterKeyEvent.target)
|
|
54742
|
-
) {
|
|
54743
|
-
debugInteraction(
|
|
54744
|
-
closeEvent,
|
|
54745
|
-
`closed by enter from inside the popup -> prevent the activation it would deliver to the trigger (enter.preventDefault())`,
|
|
54746
|
-
);
|
|
54747
|
-
enterKeyEvent.preventDefault();
|
|
54748
|
-
break prevent_reopen;
|
|
54749
|
-
}
|
|
54750
|
-
}
|
|
54751
|
-
|
|
54752
|
-
// Sync the DOM closed first (releasing the focus trap) — only then run
|
|
54753
|
-
// the owner's own reaction (onClose may restore focus to an element
|
|
54754
|
-
// outside the popup, which the focus trap would otherwise fight while
|
|
54755
|
-
// still active).
|
|
54756
|
-
openEffectCleanup?.(closeEvent);
|
|
54757
|
-
openEffectCleanup = null;
|
|
54758
|
-
closeHandlers?.onClose?.(closeEvent);
|
|
54759
|
-
closeHandlers = null;
|
|
54760
|
-
// Last: the close effects above are what starts the exit transition the
|
|
54761
|
-
// content must outlive (see popup_content_mount.js).
|
|
54762
|
-
controller.unmountContent?.();
|
|
54763
|
-
controller.onOpenedChange?.(false, closeEvent);
|
|
54764
|
-
};
|
|
54765
|
-
const controller = {
|
|
54766
|
-
opened: false,
|
|
54767
|
-
openEffect: null,
|
|
54768
|
-
// Set by the controlled element (see popup_content_mount.js) when its
|
|
54769
|
-
// content is still waiting for a first open to be built. Called below,
|
|
54770
|
-
// before openEffect, so the popup measures and positions the real thing.
|
|
54771
|
-
mountContent: null,
|
|
54772
|
-
// The caller's own `onOpen`, set by Dialog/Popover from their props on
|
|
54773
|
-
// every render (like openEffect). Called BEFORE mountContent, so whatever
|
|
54774
|
-
// it decides — which record this dialog is opening on — is already true by
|
|
54775
|
-
// the time the content is built, positioned and shown. That order is the
|
|
54776
|
-
// whole point: learning it afterwards means the content mounted on the
|
|
54777
|
-
// previous subject first.
|
|
54778
|
-
onOpen: null,
|
|
54779
|
-
// The counterpart, set only when the popup was told to throw its content
|
|
54780
|
-
// away on close (`mount="while-opened"`). Called from performClose above.
|
|
54781
|
-
unmountContent: null,
|
|
54782
|
-
// Told whenever `opened` actually changes, whatever asked for it — an
|
|
54783
|
-
// interaction, a command, a prop — with the event that asked. What lets a
|
|
54784
|
-
// `signal` prop reflect the popup's real state, and a `navState` prop write
|
|
54785
|
-
// it into the history entry (see useOpenPropsEffectOnOpenController);
|
|
54786
|
-
// called once the open/close has fully happened rather than mid-sequence.
|
|
54787
|
-
onOpenedChange: null,
|
|
54788
|
-
open: (e, detail) => {
|
|
54789
|
-
if (controller.opened || !controller.openEffect) {
|
|
55302
|
+
const atEnd =
|
|
55303
|
+
allSelected ||
|
|
55304
|
+
(active.selectionStart === active.value.length &&
|
|
55305
|
+
active.selectionEnd === active.value.length);
|
|
55306
|
+
if (!atEnd) {
|
|
55307
|
+
return;
|
|
55308
|
+
}
|
|
55309
|
+
const inputs = getInputs();
|
|
55310
|
+
const idx = inputs.indexOf(active);
|
|
55311
|
+
if (idx === -1) {
|
|
55312
|
+
debugFocus(
|
|
55313
|
+
e,
|
|
55314
|
+
"InputGroup ArrowRight on non group input → do nothing",
|
|
55315
|
+
);
|
|
55316
|
+
return;
|
|
55317
|
+
}
|
|
55318
|
+
if (idx === inputs.length - 1) {
|
|
55319
|
+
debugFocus(
|
|
55320
|
+
e,
|
|
55321
|
+
"InputGroup ArrowRight at end of last input → do nothing",
|
|
55322
|
+
);
|
|
55323
|
+
return;
|
|
55324
|
+
}
|
|
55325
|
+
|
|
55326
|
+
debugFocus(
|
|
55327
|
+
e,
|
|
55328
|
+
"InputGroup ArrowRight at end of input[%d] → focus input[%d]",
|
|
55329
|
+
idx,
|
|
55330
|
+
idx + 1,
|
|
55331
|
+
);
|
|
55332
|
+
e.preventDefault();
|
|
55333
|
+
focusInput(inputs[idx + 1]);
|
|
54790
55334
|
return;
|
|
54791
55335
|
}
|
|
54792
|
-
|
|
54793
|
-
|
|
55336
|
+
const allSelected =
|
|
55337
|
+
active.selectionStart === 0 &&
|
|
55338
|
+
active.selectionEnd === active.value.length;
|
|
55339
|
+
const atStart =
|
|
55340
|
+
allSelected ||
|
|
55341
|
+
(active.selectionStart === 0 && active.selectionEnd === 0);
|
|
55342
|
+
if (!atStart) {
|
|
54794
55343
|
return;
|
|
54795
55344
|
}
|
|
54796
|
-
const
|
|
54797
|
-
|
|
54798
|
-
|
|
54799
|
-
|
|
54800
|
-
|
|
54801
|
-
|
|
54802
|
-
|
|
54803
|
-
|
|
54804
|
-
|
|
54805
|
-
|
|
55345
|
+
const inputs = getInputs();
|
|
55346
|
+
const idx = inputs.indexOf(active);
|
|
55347
|
+
if (idx === 0) {
|
|
55348
|
+
return;
|
|
55349
|
+
}
|
|
55350
|
+
debugFocus(
|
|
55351
|
+
e,
|
|
55352
|
+
"InputGroup ArrowLeft at start of input[%d] → focus input[%d]",
|
|
55353
|
+
idx,
|
|
55354
|
+
idx - 1,
|
|
54806
55355
|
);
|
|
54807
|
-
|
|
54808
|
-
|
|
54809
|
-
|
|
54810
|
-
// element a mousedown/click landed on. `e` itself is usually the raw
|
|
54811
|
-
// native event: its own `.detail` is a number (click count) on a
|
|
54812
|
-
// MouseEvent, so `e.detail.eventChain` is always undefined and the
|
|
54813
|
-
// mousedown/click branches below never matched — silently falling
|
|
54814
|
-
// back to `document.activeElement`, which is often `document.body`
|
|
54815
|
-
// once mousedown.preventDefault() has kept focus from landing
|
|
54816
|
-
// anywhere yet.
|
|
55356
|
+
e.preventDefault();
|
|
55357
|
+
focusInput(inputs[idx - 1]);
|
|
55358
|
+
};
|
|
54817
55359
|
|
|
54818
|
-
|
|
54819
|
-
|
|
54820
|
-
//
|
|
54821
|
-
|
|
54822
|
-
// whichever of its two inputs was just used — the open event still
|
|
54823
|
-
// remembers which one it was. An opening with no pointer in it at
|
|
54824
|
-
// all (a keyboard shortcut, defaultOpen, an app calling open()) is
|
|
54825
|
-
// not one either.
|
|
54826
|
-
// - the target: focusing a button raises nothing, so there is nothing
|
|
54827
|
-
// to wait for and the focus stays immediate. Only a field the
|
|
54828
|
-
// keyboard comes up for is worth delaying — which is why the
|
|
54829
|
-
// decision is taken on the resolved target, inside transferFocus.
|
|
54830
|
-
const openedByTouch = Boolean(
|
|
54831
|
-
findEvent(requestOpenEvent, isTouchDrivenEvent),
|
|
54832
|
-
);
|
|
54833
|
-
const cancelPendingFocus = focusTransfer.transferFocus(e, el, {
|
|
54834
|
-
getDelay: (target) =>
|
|
54835
|
-
openedByTouch && isEditableTarget(target)
|
|
54836
|
-
? FOCUS_DELAY_ON_KEYBOARD_MS
|
|
54837
|
-
: 0,
|
|
54838
|
-
});
|
|
54839
|
-
return (closeEvent) => {
|
|
54840
|
-
// Closed before the delay was up: the focus was never given, so it
|
|
54841
|
-
// must not be given now — to a field inside a popup on its way out,
|
|
54842
|
-
// raising the keyboard as it goes.
|
|
54843
|
-
cancelPendingFocus?.();
|
|
54844
|
-
markAutofocusRestoreOnClose(el, closeEvent, focusedAtClose);
|
|
54845
|
-
const focusoutEvent = findEvent(closeEvent, "focusout");
|
|
54846
|
-
if (focusoutEvent) {
|
|
54847
|
-
debugInteraction(
|
|
54848
|
-
closeEvent,
|
|
54849
|
-
`closed by focusout -> let focus go away`,
|
|
54850
|
-
);
|
|
54851
|
-
} else {
|
|
54852
|
-
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
54853
|
-
if (mousedownEvent) {
|
|
54854
|
-
debugInteraction(
|
|
54855
|
-
closeEvent,
|
|
54856
|
-
"closed by mousedown -> prevent browser focus (mousedown.preventDefault())",
|
|
54857
|
-
);
|
|
54858
|
-
mousedownEvent.preventDefault();
|
|
54859
|
-
}
|
|
54860
|
-
focusTransfer.restoreFocus();
|
|
54861
|
-
}
|
|
54862
|
-
};
|
|
54863
|
-
};
|
|
54864
|
-
// Before mountContent, which builds the content, and before openEffect,
|
|
54865
|
-
// which shows it: what the popup opens ON has to be known before either
|
|
54866
|
-
// (see `onOpen` above).
|
|
54867
|
-
controller.onOpen?.(requestOpenEvent);
|
|
54868
|
-
// After prepareFocusTransfer, which has to record what held the focus
|
|
54869
|
-
// before anything inside the popup can claim it, and before openEffect,
|
|
54870
|
-
// which measures the popup to place it.
|
|
54871
|
-
controller.mountContent?.();
|
|
54872
|
-
// Only now — after the content has been built, before openEffect shows
|
|
54873
|
-
// it. Dialog/Popover recompute aria-expanded and navi-hidden from this
|
|
54874
|
-
// flag on every render, and mountContent above renders synchronously:
|
|
54875
|
-
// flipping it any earlier commits an already-open DOM (aria-expanded
|
|
54876
|
-
// "true", navi-hidden gone) before openEffect has run a single
|
|
54877
|
-
// statement, so the "closed" frame it pins to transition from is in
|
|
54878
|
-
// fact the open one and the entrance animation has nothing to play.
|
|
54879
|
-
// It also gives the content it just built the opening it is documented
|
|
54880
|
-
// to observe — mounted while the popup reads as closed, told it opened
|
|
54881
|
-
// right after (see popup_content_mount.js and
|
|
54882
|
-
// use_displayed_layout_effect.js).
|
|
54883
|
-
controller.opened = true;
|
|
54884
|
-
const openEffectReturnValue =
|
|
54885
|
-
controller.openEffect(requestOpenEvent) || null;
|
|
54886
|
-
openEffectCleanup = (closeEvent) => {
|
|
54887
|
-
openEffectReturnValue?.(closeEvent);
|
|
54888
|
-
};
|
|
54889
|
-
closeHandlers = openHandler(requestOpenEvent) || null;
|
|
54890
|
-
controller.onOpenedChange?.(true, requestOpenEvent);
|
|
54891
|
-
},
|
|
54892
|
-
requestClose: (
|
|
54893
|
-
e = new CustomEvent("programmatic", { detail: {} }),
|
|
54894
|
-
detail,
|
|
54895
|
-
) => {
|
|
54896
|
-
if (!controller.opened) {
|
|
54897
|
-
return true;
|
|
55360
|
+
const handleNaviInputFull = (e) => {
|
|
55361
|
+
if (!e.detail.event?.isTrusted) {
|
|
55362
|
+
// Programmatic value change (e.g. ArrowUp/Down) — don't auto-advance.
|
|
55363
|
+
return;
|
|
54898
55364
|
}
|
|
54899
|
-
const
|
|
54900
|
-
|
|
54901
|
-
|
|
54902
|
-
});
|
|
54903
|
-
chainEvent(requestCloseEvent, e);
|
|
54904
|
-
closeHandlers?.onRequestClose?.(requestCloseEvent);
|
|
54905
|
-
if (requestCloseEvent.defaultPrevented) {
|
|
54906
|
-
// The native <dialog> "cancel" event (Escape key) closes the dialog
|
|
54907
|
-
// by default; prevent that default so denial actually keeps it open.
|
|
54908
|
-
const nativeCancelEvent = findEvent(requestCloseEvent, "cancel");
|
|
54909
|
-
if (nativeCancelEvent) {
|
|
54910
|
-
nativeCancelEvent.preventDefault();
|
|
54911
|
-
}
|
|
54912
|
-
return false;
|
|
55365
|
+
const input = e.detail.event.currentTarget;
|
|
55366
|
+
if (!el.contains(input)) {
|
|
55367
|
+
return;
|
|
54913
55368
|
}
|
|
54914
|
-
|
|
54915
|
-
|
|
54916
|
-
|
|
54917
|
-
close: (e = new CustomEvent("programmatic", { detail: {} }), detail) => {
|
|
54918
|
-
if (!controller.opened) {
|
|
55369
|
+
const inputs = getInputs();
|
|
55370
|
+
const idx = inputs.indexOf(input);
|
|
55371
|
+
if (idx === -1) {
|
|
54919
55372
|
return;
|
|
54920
55373
|
}
|
|
54921
|
-
|
|
54922
|
-
|
|
54923
|
-
}
|
|
54924
|
-
|
|
54925
|
-
|
|
54926
|
-
|
|
54927
|
-
|
|
54928
|
-
|
|
54929
|
-
|
|
54930
|
-
|
|
54931
|
-
|
|
54932
|
-
|
|
54933
|
-
|
|
54934
|
-
const isInsideOpenPopup = (element) => {
|
|
54935
|
-
if (!element || element.nodeType !== 1) {
|
|
54936
|
-
return false;
|
|
54937
|
-
}
|
|
54938
|
-
return Boolean(element.closest("dialog[open], [popover]:popover-open"));
|
|
54939
|
-
};
|
|
55374
|
+
if (idx === inputs.length - 1) {
|
|
55375
|
+
return;
|
|
55376
|
+
}
|
|
55377
|
+
const nextInput = inputs[idx + 1];
|
|
55378
|
+
debugFocus(
|
|
55379
|
+
e,
|
|
55380
|
+
"InputGroup navi_input_full on input -> move to next input",
|
|
55381
|
+
input,
|
|
55382
|
+
nextInput,
|
|
55383
|
+
);
|
|
55384
|
+
e.preventDefault();
|
|
55385
|
+
focusInput(nextInput);
|
|
55386
|
+
};
|
|
54940
55387
|
|
|
54941
|
-
//
|
|
54942
|
-
//
|
|
54943
|
-
// (
|
|
54944
|
-
//
|
|
54945
|
-
|
|
54946
|
-
|
|
54947
|
-
|
|
54948
|
-
|
|
55388
|
+
// const handlePaste = (e) => {
|
|
55389
|
+
// const active = document.activeElement;
|
|
55390
|
+
// if (!isTextInputElement(active) || !el.contains(active)) {
|
|
55391
|
+
// return;
|
|
55392
|
+
// }
|
|
55393
|
+
// const inputs = getInputs();
|
|
55394
|
+
// const startIdx = inputs.indexOf(active);
|
|
55395
|
+
// if (startIdx === -1) {
|
|
55396
|
+
// return;
|
|
55397
|
+
// }
|
|
55398
|
+
// const pastedText = e.clipboardData?.getData("text") ?? "";
|
|
55399
|
+
// if (!pastedText) {
|
|
55400
|
+
// return;
|
|
55401
|
+
// }
|
|
55402
|
+
// // Only intercept when the pasted text contains at least one separator
|
|
55403
|
+
// // from the inputs starting at the focused position.
|
|
55404
|
+
// const remainingInputs = inputs.slice(startIdx);
|
|
55405
|
+
// const hasSeparatorMatch = remainingInputs.some(
|
|
55406
|
+
// (input) =>
|
|
55407
|
+
// input.dataset.separator &&
|
|
55408
|
+
// pastedText.includes(input.dataset.separator),
|
|
55409
|
+
// );
|
|
55410
|
+
// if (!hasSeparatorMatch) {
|
|
55411
|
+
// return;
|
|
55412
|
+
// }
|
|
55413
|
+
// e.preventDefault();
|
|
55414
|
+
// let remaining = pastedText;
|
|
55415
|
+
// let lastFilledIdx = startIdx;
|
|
55416
|
+
// for (let i = 0; i < remainingInputs.length; i++) {
|
|
55417
|
+
// const input = remainingInputs[i];
|
|
55418
|
+
// const separator = input.dataset.separator;
|
|
55419
|
+
// let part;
|
|
55420
|
+
// if (separator && remaining.includes(separator)) {
|
|
55421
|
+
// const sepIdx = remaining.indexOf(separator);
|
|
55422
|
+
// part = remaining.slice(0, sepIdx);
|
|
55423
|
+
// remaining = remaining.slice(sepIdx + separator.length);
|
|
55424
|
+
// } else {
|
|
55425
|
+
// part = remaining;
|
|
55426
|
+
// remaining = "";
|
|
55427
|
+
// }
|
|
55428
|
+
// requestSubPaste(input, part, e);
|
|
55429
|
+
// lastFilledIdx = startIdx + i;
|
|
55430
|
+
// if (remaining === "") {
|
|
55431
|
+
// break;
|
|
55432
|
+
// }
|
|
55433
|
+
// }
|
|
55434
|
+
// focusInput(inputs[lastFilledIdx]);
|
|
55435
|
+
// };
|
|
54949
55436
|
|
|
54950
|
-
|
|
54951
|
-
|
|
54952
|
-
//
|
|
54953
|
-
// openController.openEffect on every render.
|
|
54954
|
-
const useOpenController = (openHandler) => {
|
|
54955
|
-
const debugInteraction = useDebugInteraction();
|
|
54956
|
-
const stableOpenHandler = useStableCallback(openHandler);
|
|
54957
|
-
const controllerRef = useRef(null);
|
|
54958
|
-
if (!controllerRef.current) {
|
|
54959
|
-
controllerRef.current = createOpenController(stableOpenHandler, {
|
|
54960
|
-
debugInteraction,
|
|
54961
|
-
});
|
|
54962
|
-
}
|
|
54963
|
-
// Unmount safety net: if Dialog/Popover unmounts while still open (parent
|
|
54964
|
-
// removes it from the tree without going through requestClose()), there is
|
|
54965
|
-
// no choice to leave open — close it for real.
|
|
54966
|
-
useLayoutEffect(() => {
|
|
55437
|
+
el.addEventListener("keydown", handleKeyDown, { capture: true });
|
|
55438
|
+
el.addEventListener("navi_input_full", handleNaviInputFull);
|
|
55439
|
+
// el.addEventListener("paste", handlePaste, { capture: true });
|
|
54967
55440
|
return () => {
|
|
54968
|
-
|
|
55441
|
+
el.removeEventListener("keydown", handleKeyDown, { capture: true });
|
|
55442
|
+
el.removeEventListener("navi_input_full", handleNaviInputFull);
|
|
55443
|
+
// el.removeEventListener("paste", handlePaste, { capture: true });
|
|
54969
55444
|
};
|
|
54970
|
-
}, []);
|
|
54971
|
-
return controllerRef.current;
|
|
55445
|
+
}, [debugFocus]);
|
|
54972
55446
|
};
|
|
54973
55447
|
|
|
54974
|
-
//
|
|
54975
|
-
//
|
|
54976
|
-
//
|
|
54977
|
-
//
|
|
54978
|
-
//
|
|
54979
|
-
//
|
|
54980
|
-
//
|
|
54981
|
-
//
|
|
54982
|
-
//
|
|
54983
|
-
// then simply running them in *reverse* of their registration order fixes
|
|
54984
|
-
// this — no need to compare DOM positions: since effects already fire
|
|
54985
|
-
// child-first, tree-wide, for *any* ancestor/descendant pair the descendant
|
|
54986
|
-
// is always queued before the ancestor, regardless of what else is in the
|
|
54987
|
-
// tree, so reversing the whole batch always puts every ancestor before its
|
|
54988
|
-
// own descendants. Works for any nesting depth for the same reason. Two
|
|
54989
|
-
// unrelated (sibling) popups both mounting open also get reordered
|
|
54990
|
-
// relative to each other, but there's no meaningful "correct" order between
|
|
54991
|
-
// those anyway.
|
|
54992
|
-
let pendingMountOpens = [];
|
|
54993
|
-
let mountOpenFlushScheduled = false;
|
|
54994
|
-
const scheduleMountOpen = (run) => {
|
|
54995
|
-
pendingMountOpens.push(run);
|
|
54996
|
-
if (mountOpenFlushScheduled) {
|
|
54997
|
-
return;
|
|
54998
|
-
}
|
|
54999
|
-
mountOpenFlushScheduled = true;
|
|
55000
|
-
queueMicrotask(() => {
|
|
55001
|
-
const entries = pendingMountOpens;
|
|
55002
|
-
pendingMountOpens = [];
|
|
55003
|
-
mountOpenFlushScheduled = false;
|
|
55004
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
55005
|
-
entries[i]();
|
|
55006
|
-
}
|
|
55007
|
-
});
|
|
55008
|
-
};
|
|
55448
|
+
// const requestSubPaste = (input, value, event) => {
|
|
55449
|
+
// dispatchRequestInteraction(input, {
|
|
55450
|
+
// event,
|
|
55451
|
+
// name: "subpaste",
|
|
55452
|
+
// allowed: () => {
|
|
55453
|
+
// dispatchRequestSetUIState(input, value, { event });
|
|
55454
|
+
// },
|
|
55455
|
+
// });
|
|
55456
|
+
// };
|
|
55009
55457
|
|
|
55010
|
-
|
|
55011
|
-
|
|
55012
|
-
|
|
55013
|
-
// `true` takes the popup's own id — a popup a `--navi-open` command can name is
|
|
55014
|
-
// a popup that already has a stable one, and that id is what identifies its
|
|
55015
|
-
// open state too.
|
|
55016
|
-
const NO_NAV_STATE = { id: undefined, type: "replace" };
|
|
55017
|
-
const resolveNavStateProp = (navState, popupId) => {
|
|
55018
|
-
if (!navState) {
|
|
55019
|
-
return NO_NAV_STATE;
|
|
55458
|
+
const isTextInputElement = (el) => {
|
|
55459
|
+
if (!el) {
|
|
55460
|
+
return false;
|
|
55020
55461
|
}
|
|
55021
|
-
if (
|
|
55022
|
-
return
|
|
55462
|
+
if (el.tagName === "TEXTAREA") {
|
|
55463
|
+
return true;
|
|
55023
55464
|
}
|
|
55024
|
-
if (
|
|
55025
|
-
return
|
|
55465
|
+
if (el.tagName !== "INPUT") {
|
|
55466
|
+
return false;
|
|
55026
55467
|
}
|
|
55027
|
-
|
|
55468
|
+
const type = el.type || "text";
|
|
55469
|
+
return (
|
|
55470
|
+
type === "text" ||
|
|
55471
|
+
type === "search" ||
|
|
55472
|
+
type === "url" ||
|
|
55473
|
+
type === "tel" ||
|
|
55474
|
+
type === "email" ||
|
|
55475
|
+
type === "password" ||
|
|
55476
|
+
type === "number"
|
|
55477
|
+
);
|
|
55028
55478
|
};
|
|
55029
55479
|
|
|
55030
|
-
|
|
55031
|
-
|
|
55032
|
-
|
|
55033
|
-
|
|
55034
|
-
|
|
55035
|
-
|
|
55036
|
-
|
|
55037
|
-
|
|
55038
|
-
|
|
55039
|
-
*
|
|
55040
|
-
* @param {{ open: (e: Event, detail?: object) => void, requestClose: (e: Event, detail?: object) => void, opened: boolean }} openController
|
|
55041
|
-
* @param {{ id?: string, open?: boolean|"interaction", defaultOpen?: boolean|"interaction", signal?: import("@preact/signals").Signal<boolean>, navState?: boolean|string|{id?: string, type?: "push"|"replace"} }} props
|
|
55042
|
-
*/
|
|
55043
|
-
const useOpenPropsEffectOnOpenController = (openController, props) => {
|
|
55044
|
-
const { signal, defaultOpen, navState } = props;
|
|
55045
|
-
const { id: navStateId, type: navStateType } = resolveNavStateProp(
|
|
55046
|
-
navState,
|
|
55047
|
-
props.id,
|
|
55048
|
-
);
|
|
55049
|
-
// Called unconditionally (it answers with no-ops for an absent id), like
|
|
55050
|
-
// every other hook here.
|
|
55051
|
-
const [navStateValue, enterNavState, leaveNavState] = useNavState(
|
|
55052
|
-
navStateId,
|
|
55053
|
-
{ type: navStateType },
|
|
55054
|
-
);
|
|
55055
|
-
// What the caller holds, however they hold it: the history entry when there
|
|
55056
|
-
// is a `navState`, an `open` they re-render themselves, or a `signal` this
|
|
55057
|
-
// hook also writes (see onOpenedChange below). Reading .value during render
|
|
55058
|
-
// is what subscribes the popup to a signal; reading the document state is
|
|
55059
|
-
// what subscribes it to the history entry, back button included.
|
|
55060
|
-
const open = navStateId
|
|
55061
|
-
? Boolean(navStateValue)
|
|
55062
|
-
: signal
|
|
55063
|
-
? signal.value
|
|
55064
|
-
: props.open;
|
|
55065
|
-
// Assigned on every render, like openEffect, so it always closes over the
|
|
55066
|
-
// latest prop: a popup that opens or closes on its own (Escape, backdrop, a
|
|
55067
|
-
// --navi-close command) writes what happened where the caller keeps it, so
|
|
55068
|
-
// whoever holds it always reads where the popup is.
|
|
55069
|
-
openController.onOpenedChange =
|
|
55070
|
-
navStateId || signal
|
|
55071
|
-
? (opened, event) => {
|
|
55072
|
-
if (navStateId) {
|
|
55073
|
-
if (opened) {
|
|
55074
|
-
enterNavState();
|
|
55075
|
-
} else {
|
|
55076
|
-
// Under type "push" a cancel goes back rather than rewriting the
|
|
55077
|
-
// entry, so everything else written to the url while the popup
|
|
55078
|
-
// was open goes back with it (see useNavState's own leave()).
|
|
55079
|
-
leaveNavState({ isBack: Boolean(event?.detail?.isCancel) });
|
|
55080
|
-
}
|
|
55081
|
-
}
|
|
55082
|
-
if (signal) {
|
|
55083
|
-
signal.value = opened;
|
|
55084
|
-
}
|
|
55085
|
-
}
|
|
55086
|
-
: null;
|
|
55087
|
-
// Tracks whether the effect below has ever run before — only the very
|
|
55088
|
-
// first run gets the "mount already open" treatment (`open` truthy from
|
|
55089
|
-
// the start, or the uncontrolled, mount-only `defaultOpen`); every
|
|
55090
|
-
// subsequent `open` change is a real, later toggle and should animate
|
|
55091
|
-
// normally like any other interactive open/close.
|
|
55092
|
-
const isFirstRunRef = useRef(true);
|
|
55480
|
+
// How many lines the thing around a component gives it, when that thing caps
|
|
55481
|
+
// its own height and cannot cap what it holds.
|
|
55482
|
+
//
|
|
55483
|
+
// A <Picker> is the case it exists for: its value is clamped with maxLines,
|
|
55484
|
+
// which is CSS line-clamp — it counts line boxes of inline text and has no idea
|
|
55485
|
+
// what a wrapped flex row is. So a <BadgeList> rendered as a picker's ui reads
|
|
55486
|
+
// the number from here and caps its own rows to it, and the picker turns its
|
|
55487
|
+
// own clamp off (see .navi_picker_value:has(.navi_badge_list) in picker.jsx).
|
|
55488
|
+
const MaxLinesContext = createContext(undefined);
|
|
55093
55489
|
|
|
55094
|
-
|
|
55095
|
-
|
|
55096
|
-
|
|
55490
|
+
// When a component render a prop that can be anything (js value of preact element)
|
|
55491
|
+
// make sure it cannot throw during render by converting it to a string if it's not a valid preact element or a primitive value
|
|
55492
|
+
const renderSafe = (value) => {
|
|
55493
|
+
if (
|
|
55494
|
+
value === null ||
|
|
55495
|
+
value === undefined ||
|
|
55496
|
+
typeof value === "string" ||
|
|
55497
|
+
typeof value === "number" ||
|
|
55498
|
+
typeof value === "boolean"
|
|
55499
|
+
) {
|
|
55500
|
+
return value;
|
|
55501
|
+
}
|
|
55097
55502
|
|
|
55098
|
-
|
|
55099
|
-
|
|
55100
|
-
|
|
55101
|
-
// Whether this popup being open is something that just happened, or
|
|
55102
|
-
// something that was already true when the page appeared. "interaction"
|
|
55103
|
-
// says the mount IS the opening — the popup exists because the user
|
|
55104
|
-
// just asked for it — so the entrance plays like any other open. Any
|
|
55105
|
-
// other truthy value means it was simply already open: nothing was ever
|
|
55106
|
-
// shown as "closed" for the user to see it transition away from, so the
|
|
55107
|
-
// entrance is skipped (`silent`, see popover.jsx's own openEffect).
|
|
55108
|
-
//
|
|
55109
|
-
// Deferred + batched (see scheduleMountOpen above) rather than called
|
|
55110
|
-
// directly, so nested popups that both mount already-open end up
|
|
55111
|
-
// stacked ancestor-first instead of Preact's own child-first effect
|
|
55112
|
-
// order.
|
|
55113
|
-
scheduleMountOpen(() =>
|
|
55114
|
-
openController.open(new CustomEvent("open_by_prop", { detail: {} }), {
|
|
55115
|
-
silent: mountOpenReason !== "interaction",
|
|
55116
|
-
}),
|
|
55117
|
-
);
|
|
55118
|
-
}
|
|
55119
|
-
return;
|
|
55120
|
-
}
|
|
55503
|
+
if (isValidElement(value)) {
|
|
55504
|
+
return value;
|
|
55505
|
+
}
|
|
55121
55506
|
|
|
55122
|
-
|
|
55123
|
-
return;
|
|
55124
|
-
}
|
|
55125
|
-
// Skip when the controller is already in the desired state.
|
|
55126
|
-
// openController.opened tracks actual open/close (updated by onopen/onclose,
|
|
55127
|
-
// not by renders) so it is the authoritative check against feedback loops.
|
|
55128
|
-
if (open === openController.opened) {
|
|
55129
|
-
return;
|
|
55130
|
-
}
|
|
55131
|
-
if (open) {
|
|
55132
|
-
openController.open(new CustomEvent("open_by_prop", { detail: {} }));
|
|
55133
|
-
} else {
|
|
55134
|
-
openController.requestClose(
|
|
55135
|
-
new CustomEvent("close_by_prop", { detail: {} }),
|
|
55136
|
-
{ isCancel: true },
|
|
55137
|
-
);
|
|
55138
|
-
}
|
|
55139
|
-
// The request can be refused (a busy form denying the close): the popup
|
|
55140
|
-
// then stays where it was, and whoever holds the open state is told so —
|
|
55141
|
-
// otherwise it would keep saying "closed" about a popup still open.
|
|
55142
|
-
if (signal) {
|
|
55143
|
-
signal.value = openController.opened;
|
|
55144
|
-
}
|
|
55145
|
-
if (navStateId && openController.opened) {
|
|
55146
|
-
enterNavState();
|
|
55147
|
-
}
|
|
55148
|
-
}, [open]);
|
|
55507
|
+
return String(value);
|
|
55149
55508
|
};
|
|
55150
55509
|
|
|
55151
55510
|
/**
|
|
@@ -56691,10 +57050,10 @@ const useDialogProps = props => {
|
|
|
56691
57050
|
// the latest prop. Called by openController.open() before the content is
|
|
56692
57051
|
// built: what this popup opens ON is known before anything reads it.
|
|
56693
57052
|
openController.onOpen = onOpen || null;
|
|
56694
|
-
const
|
|
56695
|
-
children: childrenProp,
|
|
57053
|
+
const contentMounted = usePopupContentMount(openController, props.ref, {
|
|
56696
57054
|
mount
|
|
56697
57055
|
});
|
|
57056
|
+
const children = contentMounted ? childrenProp : null;
|
|
56698
57057
|
const isModal = layer === "top";
|
|
56699
57058
|
const ref = props.ref;
|
|
56700
57059
|
const expandY = Boolean(expand) || Boolean(expandYProp);
|
|
@@ -58136,10 +58495,10 @@ const usePopoverProps = props => {
|
|
|
58136
58495
|
// the latest prop. Called by openController.open() before the content is
|
|
58137
58496
|
// built: what this popup opens ON is known before anything reads it.
|
|
58138
58497
|
openController.onOpen = onOpen || null;
|
|
58139
|
-
const
|
|
58140
|
-
children: childrenProp,
|
|
58498
|
+
const contentMounted = usePopupContentMount(openController, props.ref, {
|
|
58141
58499
|
mount
|
|
58142
58500
|
});
|
|
58501
|
+
const children = contentMounted ? childrenProp : null;
|
|
58143
58502
|
const isTopLayer = layer === "top";
|
|
58144
58503
|
const ref = props.ref;
|
|
58145
58504
|
const backdropRef = useRef();
|
|
@@ -80746,5 +81105,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
80746
81105
|
})
|
|
80747
81106
|
});
|
|
80748
81107
|
|
|
80749
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, OfflineError, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isOfflineError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
81108
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Binder, Box, Button, ButtonCopyToClipboard, CalloutStatusIcon, Caption, CardLayout, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, ControlSwap, DaySpin, Details, Dialog, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, Expandable, EyeClosedSvg, EyeSvg, Field, FixedBar, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, InfoSvg, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, ListItems, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, NumberSpin, OfflineError, Paragraph, Picker, Popover, Popup, Quantity, RadioGroup, Route, RouteTransitionArea, RouteTravel, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, Select, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, Slide, SlideContainer, Spin, SpinGroup, SplitButton, StarSvg, Step, StepList, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Textarea, TextareaCharCount, Thead, Time, TimeRange, TimeRangeSpin, TimeRangeWheel, TimeSpin, TimeWheel, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, Wheel, WheelGroup, WheelItem, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, canNavBackSignal, canNavForwardSignal, coarsePointerSignal, compareTwoJsValues, constraintFromValidityRule, createAction, createAvailableConstraint, createI18n, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, createSlot, defineInteractionDetector, defineRouteDefaultTransition, defineRouteTransition, detectHorizontalOverflow, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, errorIsDisplayed, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isOfflineError, isRowSelected, isScrolling, isToday, languagesSignal, localStorageSignal, markErrorAsDisplayedBy, moveArrayItemByIndex, moveFocusTo, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, scrollActivitySignal, setBaseUrl, setNetworkPolicy, setPreferredLanguage, setSupportedLanguages, setUrlTargetOptions, setupRoutes, smallTouchScreenSignal, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, triggerNaviCommand, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutElement, useCalloutRequestClose, useCanNavBack, useCanNavForward, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useInputGroup, useKeyboardShortcuts, useNavState, useNetworkPolicyReason, useOrderedColumns, usePopupMode, useRouteStatus, useSearchText, useSelectableElement, useSelectionController, useSignalSync, useSlideContainer, useSlideValue, useStateArray, useTitleLevel, useTransitionCover, useUrlSearchParam, useUrlTargetId, valueInLocalStorage, windowWidthSignal };
|
|
80750
81109
|
//# sourceMappingURL=jsenv_navi.js.map
|