@jsenv/navi 0.29.399 → 0.29.400
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev/jsenv_navi.js +754 -233
- package/dist/dev/jsenv_navi.js.map +19 -18
- package/dist/jsenv_navi.js +754 -233
- package/dist/jsenv_navi.js.map +19 -18
- package/docs/AI_INSTRUCTIONS.md +10 -4
- package/docs/control_value.md +52 -6
- package/docs/drag_to_travel.md +32 -0
- package/docs/navigation.md +81 -1
- package/docs/popup_open.md +7 -9
- package/docs/scroll.md +3 -3
- package/package.json +2 -2
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
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,
|
|
7
|
+
import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchInternalCustomEvent, dispatchCustomEvent, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, getElementSignature, findEvent, createValueEffect, findFocusDelegateTarget, findFocusable, scrollIntoViewThroughScrollables, allowWheelThrough, dispatchPublicCustomEvent, resolveCSSColor, getScrollContainerSet, ELEMENT_SIZE_CHANGE, findSelfOrAncestorFixedPosition, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, applyNewPosition, measureLongestVisualLineWidth, chainEvent, keepTouchRefusable, isPressDrivenClick, waitForTap, waitForPressHeld, suppressClickAfterGesture, startDragToTravel, dragSourceThatStoodDown, markDragSource, refuseDragTo, startDragTo, installPanZoom, createEventGroupLogger, createInternalCustomEvent, getKeyboardEventDefaultAction, activeElementSignal, normalizeStyle, mergeOneStyle, getPositionedParent, normalizeStyles, createGroupTransitionController, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, getScrollContainer, watchWheelTravel, scrollRoomTowards, isTouchDrivenEvent, scrollIntoViewScoped, closestOpenableAncestor, isAncestorOpen, isDisplayedDespiteClosedAncestor, observeAncestorOpenState, getAncestorOpenType, findBefore, findAfter, resolveCSSSize, hasCSSSizeUnit, releaseWheelGesture, getScrollIntoViewScopedOffsets, wheelGestureIsTakenFrom, claimWheelGesture, initFocusGroup, stringifyStyle as stringifyStyle$1, isScrollable, resolveOklchLightness, contrastColor, trapScrollInside, parsePositionArea, snapToPixel, trapFocusInside, getVirtualKeyboardOverlayHeight, onAncestorReopen, isPressDisputedByDrag, canScroll, measureWidestChildRow, performTabNavigation, dragAfterIntent, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement } from "@jsenv/dom";
|
|
8
8
|
export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
|
|
9
9
|
import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
|
|
10
|
-
import { isValidElement, createContext, render, h, toChildArray,
|
|
10
|
+
import { options, isValidElement, createContext, render, h, toChildArray, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
|
|
11
11
|
import { useErrorBoundary, useLayoutEffect, useContext, useCallback, useRef, useState, useEffect, useMemo, useId } from "preact/hooks";
|
|
12
12
|
import { humanizeI18n, prefixFirstAndIndentRemainingLines, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
|
|
13
13
|
export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
|
|
@@ -95,6 +95,29 @@ const css$17 = /* css */ `@layer navi {
|
|
|
95
95
|
`;
|
|
96
96
|
import.meta.css = [css$17, "@jsenv/navi/src/navi_z_indexes.js"];
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A navigation is ABOUT to be applied — said before its very first write.
|
|
100
|
+
*
|
|
101
|
+
* Everything else a router says arrives once the change is made: a route
|
|
102
|
+
* announces that it matches, an action that it is running. That is too late for
|
|
103
|
+
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
104
|
+
* transitions are exactly that kind of reader — the picture they keep of the
|
|
105
|
+
* page being left is taken at the next frame, and a render answering a signal
|
|
106
|
+
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
107
|
+
*
|
|
108
|
+
* So this is the one moment where nothing has moved yet. It is published
|
|
109
|
+
* synchronously, from the top of the navigation, and whoever listens runs
|
|
110
|
+
* before the URL, the visited set, or any route has changed.
|
|
111
|
+
*
|
|
112
|
+
* The other end is published too, and for the same kind of reader: whoever
|
|
113
|
+
* held something across the change and has nobody to hand it to gets a moment
|
|
114
|
+
* to let go of it that does not depend on guessing how long the change takes.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
119
|
+
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
120
|
+
|
|
98
121
|
const addIntoArray = (array, ...valuesToAdd) => {
|
|
99
122
|
if (valuesToAdd.length === 1) {
|
|
100
123
|
const [valueToAdd] = valuesToAdd;
|
|
@@ -325,6 +348,112 @@ const urlToScheme = (url) => {
|
|
|
325
348
|
return scheme;
|
|
326
349
|
};
|
|
327
350
|
|
|
351
|
+
/**
|
|
352
|
+
* The document's rendering, held for the one frame a view transition needs.
|
|
353
|
+
*
|
|
354
|
+
* The browser does not take the picture of the page being left when a
|
|
355
|
+
* transition is ASKED for — it takes it at the next frame, just before running
|
|
356
|
+
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
357
|
+
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
358
|
+
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
359
|
+
* Both sides of the animation then show it, and one watches a page slide onto
|
|
360
|
+
* itself.
|
|
361
|
+
*
|
|
362
|
+
* So what Preact has queued waits until the update callback, which is the
|
|
363
|
+
* moment the API is built around — the change belongs inside it. The whole
|
|
364
|
+
* document is held: it is about to be frozen under a picture anyway.
|
|
365
|
+
*
|
|
366
|
+
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
367
|
+
* around Preact's `options.debounceRendering`, and two of them installed
|
|
368
|
+
* independently restore each other in the wrong order when they let go — every
|
|
369
|
+
* render queued in between is then handed to a wrapper nobody will ever
|
|
370
|
+
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
371
|
+
* route transition) must therefore hold through this module, never through a
|
|
372
|
+
* wrapper of its own.
|
|
373
|
+
*/
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
let renderingHold = null;
|
|
377
|
+
const holdRendering = () => {
|
|
378
|
+
if (renderingHold) {
|
|
379
|
+
return renderingHold.release;
|
|
380
|
+
}
|
|
381
|
+
const debounceRenderingBefore = options.debounceRendering;
|
|
382
|
+
const hold = {
|
|
383
|
+
render: null,
|
|
384
|
+
waiting: [],
|
|
385
|
+
release: () => {
|
|
386
|
+
// Only the hold that is still standing may be given back: a holder
|
|
387
|
+
// releasing after another has taken over must not let go of what it
|
|
388
|
+
// does not hold.
|
|
389
|
+
if (renderingHold !== hold) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
renderingHold = null;
|
|
393
|
+
options.debounceRendering = debounceRenderingBefore;
|
|
394
|
+
const { render, waiting } = hold;
|
|
395
|
+
hold.render = null;
|
|
396
|
+
hold.waiting = [];
|
|
397
|
+
if (render) {
|
|
398
|
+
render();
|
|
399
|
+
}
|
|
400
|
+
for (const wait of waiting) {
|
|
401
|
+
wait();
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
renderingHold = hold;
|
|
406
|
+
options.debounceRendering = (render) => {
|
|
407
|
+
hold.render = render;
|
|
408
|
+
};
|
|
409
|
+
return hold.release;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Anything else that must not happen before the picture is taken, and the
|
|
413
|
+
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
414
|
+
// document put back to its top while the page being left is still on screen is
|
|
415
|
+
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
416
|
+
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
417
|
+
// the band it had already painted — the page being left is then seen in
|
|
418
|
+
// fragments, whatever the movement does afterwards.
|
|
419
|
+
//
|
|
420
|
+
// Run at once when nobody is photographing anything, which is the common case
|
|
421
|
+
// and must stay free.
|
|
422
|
+
const whenRenderingResumes = (callback) => {
|
|
423
|
+
if (!renderingHold) {
|
|
424
|
+
callback();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
renderingHold.waiting.push(callback);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// The hold a navigation takes on its way in — from before its first write,
|
|
431
|
+
// because by the time a route announces that it matches, Preact has already
|
|
432
|
+
// been told and the render is queued; a hold taken then is a hold taken too
|
|
433
|
+
// late. Kept here until whoever animates the change takes it over, or the
|
|
434
|
+
// navigation turns out to be one nobody animates.
|
|
435
|
+
let routingRenderingHold = null;
|
|
436
|
+
const holdRenderingForRouting = () => {
|
|
437
|
+
routingRenderingHold = holdRendering();
|
|
438
|
+
};
|
|
439
|
+
// Nobody had a picture to take: a page held for a change it does not animate
|
|
440
|
+
// is a page that stutters for nothing.
|
|
441
|
+
const releaseRoutingRenderingHold = () => {
|
|
442
|
+
const release = routingRenderingHold;
|
|
443
|
+
routingRenderingHold = null;
|
|
444
|
+
if (release) {
|
|
445
|
+
release();
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
// An animator takes the navigation's hold as its own — taking another would be
|
|
449
|
+
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
450
|
+
// the change it animates is not a navigation.
|
|
451
|
+
const takeoverRoutingRenderingHold = () => {
|
|
452
|
+
const release = routingRenderingHold || holdRendering();
|
|
453
|
+
routingRenderingHold = null;
|
|
454
|
+
return release;
|
|
455
|
+
};
|
|
456
|
+
|
|
328
457
|
installImportMetaCssBuild(import.meta);
|
|
329
458
|
|
|
330
459
|
const URL_TARGET_ATTRIBUTE = "data-url-target";
|
|
@@ -374,7 +503,8 @@ let urlTargetOptions = {
|
|
|
374
503
|
* How long, in ms, to keep waiting for a target that has not arrived, counted
|
|
375
504
|
* from the moment the document stops working.
|
|
376
505
|
* @param {number} [options.maxWait=10000]
|
|
377
|
-
* Longest wait, in ms, for a document that never stops working.
|
|
506
|
+
* Longest wait, in ms, for a document that never stops working. The element
|
|
507
|
+
* is then answered as it stands, when it is there.
|
|
378
508
|
*/
|
|
379
509
|
const setUrlTargetOptions = (options) => {
|
|
380
510
|
urlTargetOptions = { ...urlTargetOptions, ...options };
|
|
@@ -401,6 +531,23 @@ const useUrlTargetId = () => {
|
|
|
401
531
|
let stopWaitingForCurrentTarget = null;
|
|
402
532
|
let currentTargetKey;
|
|
403
533
|
|
|
534
|
+
const documentLoadIsReturn = () => {
|
|
535
|
+
const [navigationEntry] = performance.getEntriesByType("navigation");
|
|
536
|
+
if (!navigationEntry) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
return (
|
|
540
|
+
navigationEntry.type === "reload" || navigationEntry.type === "back_forward"
|
|
541
|
+
);
|
|
542
|
+
};
|
|
543
|
+
// The url the document is on its way back to — a back, a forward, a reload —
|
|
544
|
+
// where the reader was, as opposed to a place just sent to. Said by the
|
|
545
|
+
// navigation before it writes anything, and by the load for the first url.
|
|
546
|
+
let returningToUrl = documentLoadIsReturn() ? window.location.href : null;
|
|
547
|
+
observeBeforeRouting(({ url, navigationType }) => {
|
|
548
|
+
returningToUrl = navigationType === "traverse" ? url : null;
|
|
549
|
+
});
|
|
550
|
+
|
|
404
551
|
/**
|
|
405
552
|
* Answers the URL's target again, as if it had just been designated.
|
|
406
553
|
*
|
|
@@ -410,10 +557,10 @@ let currentTargetKey;
|
|
|
410
557
|
*/
|
|
411
558
|
const rearmUrlTarget = () => {
|
|
412
559
|
currentTargetKey = undefined;
|
|
413
|
-
armUrlTarget(documentUrlSignal.peek());
|
|
560
|
+
armUrlTarget(documentUrlSignal.peek(), { isReturn: false });
|
|
414
561
|
};
|
|
415
562
|
|
|
416
|
-
const armUrlTarget = (documentUrl) => {
|
|
563
|
+
const armUrlTarget = (documentUrl, { isReturn }) => {
|
|
417
564
|
const targetKey = urlToTargetKey(documentUrl);
|
|
418
565
|
if (targetKey === currentTargetKey) {
|
|
419
566
|
return;
|
|
@@ -424,11 +571,25 @@ const armUrlTarget = (documentUrl) => {
|
|
|
424
571
|
stopWaitingForCurrentTarget = null;
|
|
425
572
|
}
|
|
426
573
|
const targetId = urlToTargetId(documentUrl);
|
|
574
|
+
// The browser leaves `:target` on the element a URL named once the URL has
|
|
575
|
+
// stopped naming it — a pushState never touches it.
|
|
576
|
+
const staleTarget = document.querySelector(":target");
|
|
577
|
+
if (staleTarget && staleTarget.id !== targetId) {
|
|
578
|
+
writeTarget(null);
|
|
579
|
+
}
|
|
427
580
|
if (!targetId) {
|
|
428
581
|
return;
|
|
429
582
|
}
|
|
430
|
-
stopWaitingForCurrentTarget =
|
|
583
|
+
stopWaitingForCurrentTarget = waitForUrlTarget(targetId, (element) => {
|
|
431
584
|
stopWaitingForCurrentTarget = null;
|
|
585
|
+
if (!element.matches(":target")) {
|
|
586
|
+
writeTarget(element);
|
|
587
|
+
}
|
|
588
|
+
if (isReturn) {
|
|
589
|
+
// The offset put back by scroll_restoration.js is the reader's own: the
|
|
590
|
+
// element is named by the URL, not arrived at.
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
432
593
|
revealUrlTarget(element);
|
|
433
594
|
});
|
|
434
595
|
};
|
|
@@ -445,14 +606,27 @@ const urlToTargetId = (url) => {
|
|
|
445
606
|
return hash ? decodeURIComponent(hash.slice(1)) : "";
|
|
446
607
|
};
|
|
447
608
|
|
|
448
|
-
|
|
609
|
+
// Calls `onSettled` with the element once it can be answered: it exists, it
|
|
610
|
+
// shows something, and the document is done working. Found is not enough while
|
|
611
|
+
// the document works — what sits above it may still be drawn and push it down,
|
|
612
|
+
// and a page reusing a node of the page being left holds it before its own
|
|
613
|
+
// data has come.
|
|
614
|
+
//
|
|
615
|
+
// Confirmed at the next frame, once rendering has resumed: the render the last
|
|
616
|
+
// answer asked for has run by then, and a component starting its own run from
|
|
617
|
+
// that render has made the document busy again.
|
|
618
|
+
const waitForUrlTarget = (id, onSettled) => {
|
|
619
|
+
const { graceAfterIdle, maxWait } = urlTargetOptions;
|
|
620
|
+
let stopped = false;
|
|
621
|
+
let confirmationPending = false;
|
|
622
|
+
let frame = null;
|
|
449
623
|
let mutationObserver = null;
|
|
450
624
|
let stopWatchingBusy = null;
|
|
451
625
|
let idleTimeout = null;
|
|
452
626
|
let maxWaitTimeout = null;
|
|
453
|
-
let found = false;
|
|
454
627
|
|
|
455
628
|
const stopWaiting = () => {
|
|
629
|
+
stopped = true;
|
|
456
630
|
if (mutationObserver) {
|
|
457
631
|
mutationObserver.disconnect();
|
|
458
632
|
mutationObserver = null;
|
|
@@ -463,76 +637,214 @@ const waitForElementWithId = (id, onFound) => {
|
|
|
463
637
|
}
|
|
464
638
|
clearTimeout(idleTimeout);
|
|
465
639
|
clearTimeout(maxWaitTimeout);
|
|
640
|
+
if (frame !== null) {
|
|
641
|
+
cancelAnimationFrame(frame);
|
|
642
|
+
frame = null;
|
|
643
|
+
}
|
|
466
644
|
};
|
|
467
645
|
|
|
468
|
-
const
|
|
646
|
+
const findElement = () => {
|
|
469
647
|
const element = document.getElementById(id);
|
|
470
648
|
if (!element) {
|
|
471
|
-
return;
|
|
649
|
+
return null;
|
|
472
650
|
}
|
|
473
651
|
// Rendered inside a closed tab, a folded details, a view that is not the
|
|
474
|
-
// one on screen: the element exists but would show nothing.
|
|
475
|
-
// it is the same wait, for the same reason.
|
|
652
|
+
// one on screen: the element exists but would show nothing.
|
|
476
653
|
if (element.checkVisibility && !element.checkVisibility()) {
|
|
477
|
-
return;
|
|
654
|
+
return null;
|
|
478
655
|
}
|
|
479
|
-
|
|
656
|
+
return element;
|
|
657
|
+
};
|
|
658
|
+
const settle = (element) => {
|
|
480
659
|
stopWaiting();
|
|
481
|
-
|
|
660
|
+
onSettled(element);
|
|
661
|
+
};
|
|
662
|
+
const check = () => {
|
|
663
|
+
if (confirmationPending || documentIsBusySignal.peek() || !findElement()) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
confirmationPending = true;
|
|
667
|
+
whenRenderingResumes(() => {
|
|
668
|
+
if (stopped) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
frame = requestAnimationFrame(() => {
|
|
672
|
+
frame = null;
|
|
673
|
+
confirmationPending = false;
|
|
674
|
+
if (documentIsBusySignal.peek()) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const element = findElement();
|
|
678
|
+
if (element) {
|
|
679
|
+
settle(element);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
});
|
|
482
683
|
};
|
|
483
684
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
mutationObserver = new MutationObserver(checkForElement);
|
|
685
|
+
mutationObserver = new MutationObserver(check);
|
|
686
|
+
// Every attribute, not only `id`: what shows an element that is already
|
|
687
|
+
// there — a <details> opening, a tab panel losing its display: none — is an
|
|
688
|
+
// attribute written somewhere above it, and nothing else says so.
|
|
490
689
|
mutationObserver.observe(document.documentElement, {
|
|
491
690
|
childList: true,
|
|
492
691
|
subtree: true,
|
|
493
692
|
attributes: true,
|
|
494
|
-
attributeFilter: ["id"],
|
|
495
693
|
});
|
|
496
|
-
const { graceAfterIdle, maxWait } = urlTargetOptions;
|
|
497
694
|
stopWatchingBusy = effect(() => {
|
|
498
695
|
const documentIsBusy = documentIsBusySignal.value;
|
|
499
696
|
clearTimeout(idleTimeout);
|
|
500
|
-
if (
|
|
501
|
-
|
|
697
|
+
if (documentIsBusy) {
|
|
698
|
+
return;
|
|
502
699
|
}
|
|
503
|
-
|
|
504
|
-
|
|
700
|
+
check();
|
|
701
|
+
// The grace is for an element that has not come; one already there and
|
|
702
|
+
// waiting for rendering to resume is not given up on.
|
|
703
|
+
idleTimeout = setTimeout(() => {
|
|
704
|
+
if (!confirmationPending) {
|
|
705
|
+
stopWaiting();
|
|
706
|
+
}
|
|
707
|
+
}, graceAfterIdle);
|
|
708
|
+
});
|
|
709
|
+
// A document that never stops working: the element as it stands, when it is
|
|
710
|
+
// there at all.
|
|
711
|
+
maxWaitTimeout = setTimeout(() => {
|
|
712
|
+
const element = findElement();
|
|
713
|
+
if (element) {
|
|
714
|
+
settle(element);
|
|
715
|
+
} else {
|
|
716
|
+
stopWaiting();
|
|
717
|
+
}
|
|
718
|
+
}, maxWait);
|
|
505
719
|
|
|
506
720
|
return stopWaiting;
|
|
507
721
|
};
|
|
508
722
|
|
|
509
723
|
const revealUrlTarget = (element) => {
|
|
510
724
|
const { block, behavior, markDuration } = urlTargetOptions;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
725
|
+
const prefersReducedMotion = window.matchMedia(
|
|
726
|
+
"(prefers-reduced-motion: reduce)",
|
|
727
|
+
).matches;
|
|
728
|
+
element.scrollIntoView({
|
|
729
|
+
block,
|
|
730
|
+
behavior: prefersReducedMotion ? "instant" : behavior,
|
|
731
|
+
});
|
|
732
|
+
// What the browser does when it handles a fragment itself: keyboard
|
|
733
|
+
// navigation resumes from the target, not from the top of the document.
|
|
734
|
+
if (elementIsFocusable(element)) {
|
|
735
|
+
element.focus({ preventScroll: true });
|
|
736
|
+
}
|
|
737
|
+
element.setAttribute(URL_TARGET_ATTRIBUTE, "");
|
|
738
|
+
setTimeout(() => {
|
|
739
|
+
element.removeAttribute(URL_TARGET_ATTRIBUTE);
|
|
740
|
+
}, markDuration);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
let targetWriteInProgress = false;
|
|
744
|
+
/**
|
|
745
|
+
* True while navi writes the browser's `:target`. The fragment navigation that
|
|
746
|
+
* takes is announced like any other — a popstate, a navigate event, an entry
|
|
747
|
+
* change — and names the address and the state the document is already at, so
|
|
748
|
+
* whoever routes on those events leaves it alone.
|
|
749
|
+
*/
|
|
750
|
+
const isTargetWriteInProgress = () => {
|
|
751
|
+
return targetWriteInProgress;
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// `:target` onto `element`, or off whatever holds it when `element` is null.
|
|
755
|
+
//
|
|
756
|
+
// A replace towards the address the browser is at: the browser answers it as a
|
|
757
|
+
// fragment navigation, and that is the only moment it sets `:target`. It also
|
|
758
|
+
// scrolls to what the fragment designates, resets the entry's states
|
|
759
|
+
// (history.state in Firefox, the Navigation API state everywhere) and fires a
|
|
760
|
+
// popstate (Chrome, Safari) and a navigate event. The address, both states and
|
|
761
|
+
// the scroll are put back in the same task, so nothing of it is painted; a
|
|
762
|
+
// running view transition is not skipped by it (measured in Chrome, Firefox
|
|
763
|
+
// and Safari).
|
|
764
|
+
const writeTarget = (element) => {
|
|
765
|
+
const address = window.location.href;
|
|
766
|
+
const historyState = window.history.state;
|
|
767
|
+
const { navigation } = window;
|
|
768
|
+
const entry = navigation ? navigation.currentEntry : null;
|
|
769
|
+
const entryState = entry ? entry.getState() : undefined;
|
|
770
|
+
const putScrollBack = captureScroll(element);
|
|
771
|
+
const addressFragment = new URL(address).hash.slice(1);
|
|
772
|
+
const addressTargetId = decodeURIComponent(addressFragment);
|
|
773
|
+
|
|
774
|
+
let fragmentUrl;
|
|
775
|
+
let silencedElement = null;
|
|
776
|
+
if (element) {
|
|
777
|
+
fragmentUrl =
|
|
778
|
+
element.id === addressTargetId
|
|
779
|
+
? address
|
|
780
|
+
: `${urlWithoutFragment(address)}#${encodeURIComponent(element.id)}`;
|
|
781
|
+
} else if (addressFragment) {
|
|
782
|
+
// The address itself, so the fragment does not change and no hashchange
|
|
783
|
+
// follows; the element it names briefly without its id, so it designates
|
|
784
|
+
// nothing.
|
|
785
|
+
fragmentUrl = address;
|
|
786
|
+
silencedElement = document.getElementById(addressTargetId);
|
|
787
|
+
} else {
|
|
788
|
+
// An empty fragment designates the top of the document; the scroll it
|
|
789
|
+
// makes is put back with the others.
|
|
790
|
+
fragmentUrl = `${address}#`;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
targetWriteInProgress = true;
|
|
794
|
+
try {
|
|
795
|
+
if (silencedElement) {
|
|
796
|
+
silencedElement.removeAttribute("id");
|
|
525
797
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
798
|
+
window.location.replace(fragmentUrl);
|
|
799
|
+
} finally {
|
|
800
|
+
if (silencedElement) {
|
|
801
|
+
silencedElement.id = addressTargetId;
|
|
802
|
+
}
|
|
803
|
+
window.history.replaceState(historyState, null, address);
|
|
804
|
+
if (entry && navigation.currentEntry) {
|
|
805
|
+
navigation.updateCurrentEntry({ state: entryState });
|
|
806
|
+
}
|
|
807
|
+
targetWriteInProgress = false;
|
|
808
|
+
}
|
|
809
|
+
putScrollBack();
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// Every box a fragment navigation towards `element` may scroll: its ancestors,
|
|
813
|
+
// and the document.
|
|
814
|
+
const captureScroll = (element) => {
|
|
815
|
+
const boxes = [];
|
|
816
|
+
let box = element ? element.parentElement : null;
|
|
817
|
+
while (box) {
|
|
818
|
+
boxes.push(box);
|
|
819
|
+
box = box.parentElement;
|
|
820
|
+
}
|
|
821
|
+
const { scrollingElement } = document;
|
|
822
|
+
if (!boxes.includes(scrollingElement)) {
|
|
823
|
+
boxes.push(scrollingElement);
|
|
824
|
+
}
|
|
825
|
+
const positions = boxes.map((box) => {
|
|
826
|
+
return { box, left: box.scrollLeft, top: box.scrollTop };
|
|
530
827
|
});
|
|
828
|
+
return () => {
|
|
829
|
+
for (const { box, left, top } of positions) {
|
|
830
|
+
if (box.scrollLeft !== left || box.scrollTop !== top) {
|
|
831
|
+
box.scrollTo({ left, top, behavior: "instant" });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const urlWithoutFragment = (url) => {
|
|
838
|
+
const urlObject = new URL(url);
|
|
839
|
+
urlObject.hash = "";
|
|
840
|
+
return urlObject.href;
|
|
531
841
|
};
|
|
532
842
|
|
|
533
843
|
effect(() => {
|
|
534
844
|
const documentUrl = documentUrlSignal.value;
|
|
535
|
-
|
|
845
|
+
const isReturn = returningToUrl === documentUrl;
|
|
846
|
+
returningToUrl = null;
|
|
847
|
+
armUrlTarget(documentUrl, { isReturn });
|
|
536
848
|
});
|
|
537
849
|
|
|
538
850
|
/**
|
|
@@ -10437,6 +10749,26 @@ const dispatchRequestInteraction = (
|
|
|
10437
10749
|
});
|
|
10438
10750
|
};
|
|
10439
10751
|
|
|
10752
|
+
/**
|
|
10753
|
+
* Whether the control `element` belongs to lets an interaction of this intent
|
|
10754
|
+
* through, asked ahead of the event the gate will judge, and silently: nothing
|
|
10755
|
+
* is reported, nothing runs. For what a press is about to lead to (see
|
|
10756
|
+
* announceOpeningPress in commands.js) — the press itself is not that event: a
|
|
10757
|
+
* drag source cancels a `pointerdown` whose click still comes and opens.
|
|
10758
|
+
*
|
|
10759
|
+
* @param {Element} element
|
|
10760
|
+
* @param {{ intent: "read" | "write" }} options
|
|
10761
|
+
* @returns {boolean}
|
|
10762
|
+
*/
|
|
10763
|
+
const allowsInteraction = (element, { intent }) => {
|
|
10764
|
+
const controlHost = findControlHost(element) || element;
|
|
10765
|
+
const ci = controlHost.__uiStateController__?.rules.interaction;
|
|
10766
|
+
if (!ci) {
|
|
10767
|
+
return true;
|
|
10768
|
+
}
|
|
10769
|
+
return ci.checkInteractivity({ intent });
|
|
10770
|
+
};
|
|
10771
|
+
|
|
10440
10772
|
const onRequestInteraction = (
|
|
10441
10773
|
requestInteractionCustomEvent,
|
|
10442
10774
|
{ debugInteraction },
|
|
@@ -28293,112 +28625,6 @@ const dropGeneratedIdKeys = (state) => {
|
|
|
28293
28625
|
return stateWithoutGeneratedIdKeys;
|
|
28294
28626
|
};
|
|
28295
28627
|
|
|
28296
|
-
/**
|
|
28297
|
-
* The document's rendering, held for the one frame a view transition needs.
|
|
28298
|
-
*
|
|
28299
|
-
* The browser does not take the picture of the page being left when a
|
|
28300
|
-
* transition is ASKED for — it takes it at the next frame, just before running
|
|
28301
|
-
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
28302
|
-
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
28303
|
-
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
28304
|
-
* Both sides of the animation then show it, and one watches a page slide onto
|
|
28305
|
-
* itself.
|
|
28306
|
-
*
|
|
28307
|
-
* So what Preact has queued waits until the update callback, which is the
|
|
28308
|
-
* moment the API is built around — the change belongs inside it. The whole
|
|
28309
|
-
* document is held: it is about to be frozen under a picture anyway.
|
|
28310
|
-
*
|
|
28311
|
-
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
28312
|
-
* around Preact's `options.debounceRendering`, and two of them installed
|
|
28313
|
-
* independently restore each other in the wrong order when they let go — every
|
|
28314
|
-
* render queued in between is then handed to a wrapper nobody will ever
|
|
28315
|
-
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
28316
|
-
* route transition) must therefore hold through this module, never through a
|
|
28317
|
-
* wrapper of its own.
|
|
28318
|
-
*/
|
|
28319
|
-
|
|
28320
|
-
|
|
28321
|
-
let renderingHold = null;
|
|
28322
|
-
const holdRendering = () => {
|
|
28323
|
-
if (renderingHold) {
|
|
28324
|
-
return renderingHold.release;
|
|
28325
|
-
}
|
|
28326
|
-
const debounceRenderingBefore = options.debounceRendering;
|
|
28327
|
-
const hold = {
|
|
28328
|
-
render: null,
|
|
28329
|
-
waiting: [],
|
|
28330
|
-
release: () => {
|
|
28331
|
-
// Only the hold that is still standing may be given back: a holder
|
|
28332
|
-
// releasing after another has taken over must not let go of what it
|
|
28333
|
-
// does not hold.
|
|
28334
|
-
if (renderingHold !== hold) {
|
|
28335
|
-
return;
|
|
28336
|
-
}
|
|
28337
|
-
renderingHold = null;
|
|
28338
|
-
options.debounceRendering = debounceRenderingBefore;
|
|
28339
|
-
const { render, waiting } = hold;
|
|
28340
|
-
hold.render = null;
|
|
28341
|
-
hold.waiting = [];
|
|
28342
|
-
if (render) {
|
|
28343
|
-
render();
|
|
28344
|
-
}
|
|
28345
|
-
for (const wait of waiting) {
|
|
28346
|
-
wait();
|
|
28347
|
-
}
|
|
28348
|
-
},
|
|
28349
|
-
};
|
|
28350
|
-
renderingHold = hold;
|
|
28351
|
-
options.debounceRendering = (render) => {
|
|
28352
|
-
hold.render = render;
|
|
28353
|
-
};
|
|
28354
|
-
return hold.release;
|
|
28355
|
-
};
|
|
28356
|
-
|
|
28357
|
-
// Anything else that must not happen before the picture is taken, and the
|
|
28358
|
-
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
28359
|
-
// document put back to its top while the page being left is still on screen is
|
|
28360
|
-
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
28361
|
-
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
28362
|
-
// the band it had already painted — the page being left is then seen in
|
|
28363
|
-
// fragments, whatever the movement does afterwards.
|
|
28364
|
-
//
|
|
28365
|
-
// Run at once when nobody is photographing anything, which is the common case
|
|
28366
|
-
// and must stay free.
|
|
28367
|
-
const whenRenderingResumes = (callback) => {
|
|
28368
|
-
if (!renderingHold) {
|
|
28369
|
-
callback();
|
|
28370
|
-
return;
|
|
28371
|
-
}
|
|
28372
|
-
renderingHold.waiting.push(callback);
|
|
28373
|
-
};
|
|
28374
|
-
|
|
28375
|
-
// The hold a navigation takes on its way in — from before its first write,
|
|
28376
|
-
// because by the time a route announces that it matches, Preact has already
|
|
28377
|
-
// been told and the render is queued; a hold taken then is a hold taken too
|
|
28378
|
-
// late. Kept here until whoever animates the change takes it over, or the
|
|
28379
|
-
// navigation turns out to be one nobody animates.
|
|
28380
|
-
let routingRenderingHold = null;
|
|
28381
|
-
const holdRenderingForRouting = () => {
|
|
28382
|
-
routingRenderingHold = holdRendering();
|
|
28383
|
-
};
|
|
28384
|
-
// Nobody had a picture to take: a page held for a change it does not animate
|
|
28385
|
-
// is a page that stutters for nothing.
|
|
28386
|
-
const releaseRoutingRenderingHold = () => {
|
|
28387
|
-
const release = routingRenderingHold;
|
|
28388
|
-
routingRenderingHold = null;
|
|
28389
|
-
if (release) {
|
|
28390
|
-
release();
|
|
28391
|
-
}
|
|
28392
|
-
};
|
|
28393
|
-
// An animator takes the navigation's hold as its own — taking another would be
|
|
28394
|
-
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
28395
|
-
// the change it animates is not a navigation.
|
|
28396
|
-
const takeoverRoutingRenderingHold = () => {
|
|
28397
|
-
const release = routingRenderingHold || holdRendering();
|
|
28398
|
-
routingRenderingHold = null;
|
|
28399
|
-
return release;
|
|
28400
|
-
};
|
|
28401
|
-
|
|
28402
28628
|
/**
|
|
28403
28629
|
* Where a page was left, given back when one comes back to it.
|
|
28404
28630
|
*
|
|
@@ -28599,6 +28825,11 @@ const restoreScrollPosition = (url) => {
|
|
|
28599
28825
|
// So `from`, the url being left, tells an arrival from the same place said
|
|
28600
28826
|
// differently, and only the first is moved.
|
|
28601
28827
|
//
|
|
28828
|
+
// A fragment does not make it less of an arrival. The browser finds nothing to
|
|
28829
|
+
// scroll to after a pushState; url_target.js brings the element in once it
|
|
28830
|
+
// renders, from this top — and when it never comes, the page is read from its
|
|
28831
|
+
// top like any other.
|
|
28832
|
+
//
|
|
28602
28833
|
// The document, because the document is the scrollport in the common case. An
|
|
28603
28834
|
// app that scrolls an element of its own scrolls it itself.
|
|
28604
28835
|
const startAtTop = (url, { from } = {}) => {
|
|
@@ -28608,14 +28839,10 @@ const startAtTop = (url, { from } = {}) => {
|
|
|
28608
28839
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
28609
28840
|
};
|
|
28610
28841
|
const isArrival = (url, { from }) => {
|
|
28611
|
-
const urlObject = new URL(url, window.location.href);
|
|
28612
|
-
// A fragment names where to land, and the browser is the one that finds it.
|
|
28613
|
-
if (urlObject.hash) {
|
|
28614
|
-
return false;
|
|
28615
|
-
}
|
|
28616
28842
|
if (
|
|
28617
28843
|
from !== undefined &&
|
|
28618
|
-
new URL(from, window.location.href).pathname ===
|
|
28844
|
+
new URL(from, window.location.href).pathname ===
|
|
28845
|
+
new URL(url, window.location.href).pathname
|
|
28619
28846
|
) {
|
|
28620
28847
|
return false;
|
|
28621
28848
|
}
|
|
@@ -28692,29 +28919,6 @@ const forgetScrollerUnlessPageLeft = (name) => {
|
|
|
28692
28919
|
}
|
|
28693
28920
|
};
|
|
28694
28921
|
|
|
28695
|
-
/**
|
|
28696
|
-
* A navigation is ABOUT to be applied — said before its very first write.
|
|
28697
|
-
*
|
|
28698
|
-
* Everything else a router says arrives once the change is made: a route
|
|
28699
|
-
* announces that it matches, an action that it is running. That is too late for
|
|
28700
|
-
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
28701
|
-
* transitions are exactly that kind of reader — the picture they keep of the
|
|
28702
|
-
* page being left is taken at the next frame, and a render answering a signal
|
|
28703
|
-
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
28704
|
-
*
|
|
28705
|
-
* So this is the one moment where nothing has moved yet. It is published
|
|
28706
|
-
* synchronously, from the top of the navigation, and whoever listens runs
|
|
28707
|
-
* before the URL, the visited set, or any route has changed.
|
|
28708
|
-
*
|
|
28709
|
-
* The other end is published too, and for the same kind of reader: whoever
|
|
28710
|
-
* held something across the change and has nobody to hand it to gets a moment
|
|
28711
|
-
* to let go of it that does not depend on guessing how long the change takes.
|
|
28712
|
-
*/
|
|
28713
|
-
|
|
28714
|
-
|
|
28715
|
-
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
28716
|
-
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
28717
|
-
|
|
28718
28922
|
const getHrefTargetInfo = (href) => {
|
|
28719
28923
|
href = String(href);
|
|
28720
28924
|
|
|
@@ -29222,6 +29426,11 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
29222
29426
|
});
|
|
29223
29427
|
|
|
29224
29428
|
window.addEventListener("popstate", (popstateEvent) => {
|
|
29429
|
+
if (isTargetWriteInProgress()) {
|
|
29430
|
+
// The fragment navigation navi makes to set `:target` (see
|
|
29431
|
+
// url_target.js): the entry keeps its address and its state.
|
|
29432
|
+
return;
|
|
29433
|
+
}
|
|
29225
29434
|
// The entry a pending replace was for is no longer the current one, and
|
|
29226
29435
|
// the History API has no way to write it (see replaceAddressWhenSettled).
|
|
29227
29436
|
dropPendingReplace();
|
|
@@ -29297,9 +29506,14 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
29297
29506
|
// one), so whoever awaits this reads a document url and state that
|
|
29298
29507
|
// already say where it landed.
|
|
29299
29508
|
const landedPromise = new Promise((resolve) => {
|
|
29300
|
-
|
|
29301
|
-
|
|
29302
|
-
|
|
29509
|
+
const onPopstate = () => {
|
|
29510
|
+
if (isTargetWriteInProgress()) {
|
|
29511
|
+
return;
|
|
29512
|
+
}
|
|
29513
|
+
window.removeEventListener("popstate", onPopstate);
|
|
29514
|
+
resolve(true);
|
|
29515
|
+
};
|
|
29516
|
+
window.addEventListener("popstate", onPopstate);
|
|
29303
29517
|
});
|
|
29304
29518
|
landOnPending = landOn || null;
|
|
29305
29519
|
window.history.back();
|
|
@@ -34291,10 +34505,15 @@ const RouteTravel = ({
|
|
|
34291
34505
|
// stopping. Which end decides how it is answered, and the two are not the
|
|
34292
34506
|
// same amount of work.
|
|
34293
34507
|
onEdge: ({
|
|
34294
|
-
sign
|
|
34508
|
+
sign,
|
|
34509
|
+
thrown
|
|
34295
34510
|
}) => {
|
|
34296
34511
|
const travel = travelRef.current;
|
|
34297
|
-
if (
|
|
34512
|
+
if (
|
|
34513
|
+
// Thrown on before its end: handing over from there would mean jumping
|
|
34514
|
+
// the pictures the rest of the way first (see scrubTravel below), a
|
|
34515
|
+
// page seen leaping. The travel caught simply arrives.
|
|
34516
|
+
thrown || !travel || travel.noPicture || travel.ended || travel.reverting || !CAN_KEEP_PICTURE) {
|
|
34298
34517
|
return false;
|
|
34299
34518
|
}
|
|
34300
34519
|
// Where the page in hand is coming from, said the way the gesture says
|
|
@@ -38327,22 +38546,28 @@ const armOutsidePressClose = (
|
|
|
38327
38546
|
* frames, never a timeout: browsers hold timers back while a finger is down,
|
|
38328
38547
|
* and a timeout started by the press fires at its release, on top of the click.
|
|
38329
38548
|
*
|
|
38330
|
-
* "while-opened" content is warmed by
|
|
38331
|
-
*
|
|
38332
|
-
*
|
|
38333
|
-
*
|
|
38334
|
-
* content exists at a time). A pointer crossing four
|
|
38335
|
-
* contents; a press is the start of one opening.
|
|
38336
|
-
*
|
|
38337
|
-
*
|
|
38338
|
-
*
|
|
38339
|
-
*
|
|
38340
|
-
*
|
|
38341
|
-
*
|
|
38342
|
-
*
|
|
38343
|
-
*
|
|
38344
|
-
*
|
|
38345
|
-
*
|
|
38549
|
+
* "while-opened" content is warmed by an opening press, never by a hover or a
|
|
38550
|
+
* focus. That mode promises content built fresh for the gesture that opens
|
|
38551
|
+
* it, and mounted only while that gesture and its opening last: callers lean
|
|
38552
|
+
* on it (several pickers sharing one set of content ids, because only one
|
|
38553
|
+
* content exists at a time). A pointer crossing four triggers would build four
|
|
38554
|
+
* contents; a press is the start of one opening.
|
|
38555
|
+
*
|
|
38556
|
+
* Only what opens the popup can say its press is one, and it says so as the
|
|
38557
|
+
* press starts, with a `navi_open_press` on the popup (see
|
|
38558
|
+
* announceOpeningPress in commands.js): a `--navi-open` button, a picker's
|
|
38559
|
+
* trigger when a press is what opens it, an expandable's UI part. Not the
|
|
38560
|
+
* anchor: it says where the popup is placed, not what opens it — a card
|
|
38561
|
+
* opened by a hold is pressed all day by taps meant for what it holds.
|
|
38562
|
+
*
|
|
38563
|
+
* The press builds the content, and the press ending without an open throws
|
|
38564
|
+
* it away — its click reaching the document with the popup still closed, no
|
|
38565
|
+
* click coming after the release, the browser taking the gesture
|
|
38566
|
+
* (`pointercancel`), or the next press starting, before any of its own
|
|
38567
|
+
* handlers run: one press at a time holds a content. A keyboard opening builds
|
|
38568
|
+
* at open time. A popup with an `onOpen` is never warmed: that callback runs
|
|
38569
|
+
* before the content is built (see open_controller.js), and a content seeding
|
|
38570
|
+
* itself from what it writes must be built after it.
|
|
38346
38571
|
*/
|
|
38347
38572
|
|
|
38348
38573
|
|
|
@@ -38476,21 +38701,21 @@ const usePopupContentMount = (
|
|
|
38476
38701
|
anchorElement.removeEventListener("focusin", warm);
|
|
38477
38702
|
};
|
|
38478
38703
|
}, [contentMounted, anchor, mount]);
|
|
38479
|
-
// Warm on
|
|
38480
|
-
// as long as the
|
|
38481
|
-
// the press that throws a warmed content away can be a new
|
|
38482
|
-
// same
|
|
38704
|
+
// Warm on an opening press, for "while-opened" (see the top comment).
|
|
38705
|
+
// Listening for as long as the popup is there, not only while the content is
|
|
38706
|
+
// unmounted: the press that throws a warmed content away can be a new
|
|
38707
|
+
// opening press of this same popup, and it must warm again.
|
|
38483
38708
|
useEffect(() => {
|
|
38484
|
-
if (
|
|
38709
|
+
if (mount !== "while-opened") {
|
|
38485
38710
|
return undefined;
|
|
38486
38711
|
}
|
|
38487
|
-
const
|
|
38488
|
-
if (!
|
|
38712
|
+
const element = openController.getElement?.();
|
|
38713
|
+
if (!element) {
|
|
38489
38714
|
return undefined;
|
|
38490
38715
|
}
|
|
38491
38716
|
let stopPressWarm = null;
|
|
38492
|
-
const
|
|
38493
|
-
if (
|
|
38717
|
+
const onOpeningPress = () => {
|
|
38718
|
+
if (contentMountedRef.current || openController.onOpen) {
|
|
38494
38719
|
return;
|
|
38495
38720
|
}
|
|
38496
38721
|
const cancelBuild = requestFrameAfterNext(() => {
|
|
@@ -38508,12 +38733,12 @@ const usePopupContentMount = (
|
|
|
38508
38733
|
stopWatching();
|
|
38509
38734
|
};
|
|
38510
38735
|
};
|
|
38511
|
-
|
|
38736
|
+
element.addEventListener("navi_open_press", onOpeningPress);
|
|
38512
38737
|
return () => {
|
|
38513
38738
|
stopPressWarm?.();
|
|
38514
|
-
|
|
38739
|
+
element.removeEventListener("navi_open_press", onOpeningPress);
|
|
38515
38740
|
};
|
|
38516
|
-
}, [
|
|
38741
|
+
}, [mount]);
|
|
38517
38742
|
|
|
38518
38743
|
return contentMounted;
|
|
38519
38744
|
};
|
|
@@ -39641,6 +39866,9 @@ const useUIStateController = (
|
|
|
39641
39866
|
// The suggestion this control started on — what tells a field showing
|
|
39642
39867
|
// its default from one carrying an answer (see isUIStateHeld).
|
|
39643
39868
|
defaultValue: controlInfo.defaultValue,
|
|
39869
|
+
// The default last put on screen, which lags `defaultValue` while an
|
|
39870
|
+
// edit holds a newer one back (see followDefaultValue).
|
|
39871
|
+
defaultValueTaken: controlInfo.defaultValue,
|
|
39644
39872
|
|
|
39645
39873
|
facadeChild: null,
|
|
39646
39874
|
// Set for the duration of one interaction by whatever wants the
|
|
@@ -39750,6 +39978,18 @@ const useUIStateController = (
|
|
|
39750
39978
|
e.currentTarget || controller.ref.current,
|
|
39751
39979
|
);
|
|
39752
39980
|
const currentUIState = controller.uiState;
|
|
39981
|
+
// What a control is STARTED on is accepted, as its own defaultValue
|
|
39982
|
+
// would be: a value placed on it as it registers, or what a picker
|
|
39983
|
+
// takes up from its popup while it holds nothing of its own. A
|
|
39984
|
+
// rollback goes back there, and a control left on it holds no edit.
|
|
39985
|
+
if (!controller.hasStateProp) {
|
|
39986
|
+
if (
|
|
39987
|
+
e.type === "initial_state_push" ||
|
|
39988
|
+
(e.type === "facade_child_mount_sync" && !controller.holdsEdit())
|
|
39989
|
+
) {
|
|
39990
|
+
controller.state = newUIState;
|
|
39991
|
+
}
|
|
39992
|
+
}
|
|
39753
39993
|
const stateIsTheSame = compareTwoJsValues(newUIState, currentUIState);
|
|
39754
39994
|
if (stateIsTheSame) {
|
|
39755
39995
|
if (controlType === "button" || controlType === "link") {
|
|
@@ -39915,7 +40155,12 @@ const useUIStateController = (
|
|
|
39915
40155
|
}
|
|
39916
40156
|
// initial_state_push is pure initialization (equivalent to defaultValue on the
|
|
39917
40157
|
// child itself): skip uiAction entirely so no side effects fire on mount.
|
|
39918
|
-
|
|
40158
|
+
// A value the outside moved since (see outside_value_follow) is the
|
|
40159
|
+
// same initialization, later.
|
|
40160
|
+
if (
|
|
40161
|
+
e.type !== "initial_state_push" &&
|
|
40162
|
+
e.type !== "outside_value_follow"
|
|
40163
|
+
) {
|
|
39919
40164
|
// Still fire uiAction so external listeners (e.g. signals) stay in
|
|
39920
40165
|
// sync, but do NOT fire the command and do NOT notify the parent —
|
|
39921
40166
|
// both would cause an infinite loop when a parent cascades state
|
|
@@ -39969,6 +40214,19 @@ const useUIStateController = (
|
|
|
39969
40214
|
stateChanged: true,
|
|
39970
40215
|
});
|
|
39971
40216
|
}
|
|
40217
|
+
if (
|
|
40218
|
+
e.type === "outside_value_follow" ||
|
|
40219
|
+
(e.type === "facade_child_mount_sync" && isOutsideValueFollow(e))
|
|
40220
|
+
) {
|
|
40221
|
+
// Heard the way a control mounting on that value would be:
|
|
40222
|
+
// silently, nobody acted. But heard, all the way up — a picker
|
|
40223
|
+
// whose popup followed tells its own group in turn — or what
|
|
40224
|
+
// the groups send is not what is on screen.
|
|
40225
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
40226
|
+
stateChanged: true,
|
|
40227
|
+
silent: true,
|
|
40228
|
+
});
|
|
40229
|
+
}
|
|
39972
40230
|
return true;
|
|
39973
40231
|
}
|
|
39974
40232
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
@@ -40076,6 +40334,41 @@ const useUIStateController = (
|
|
|
40076
40334
|
// pick — the signal would hand it straight back on the next render,
|
|
40077
40335
|
// and the refused value would win over the rollback.
|
|
40078
40336
|
writeBoundSignal(controller.state);
|
|
40337
|
+
controller.followDefaultValue();
|
|
40338
|
+
},
|
|
40339
|
+
// Something typed, picked or toggled since the value the outside last
|
|
40340
|
+
// gave or accepted.
|
|
40341
|
+
holdsEdit: () => {
|
|
40342
|
+
return !compareTwoJsValues(controller.uiState, controller.state);
|
|
40343
|
+
},
|
|
40344
|
+
// An uncontrolled control starts on its defaultValue, and a new one is
|
|
40345
|
+
// what it would start on if it were mounted again — a record saved
|
|
40346
|
+
// and handed back normalized, refreshed from elsewhere. It takes it
|
|
40347
|
+
// while it holds no edit. An edit is never undone; the default waits
|
|
40348
|
+
// for it to be accepted or rolled back.
|
|
40349
|
+
followDefaultValue: () => {
|
|
40350
|
+
// A bound signal carries its own default, followed through
|
|
40351
|
+
// stateFromSignal; a proxy shows the control it stands for.
|
|
40352
|
+
if (
|
|
40353
|
+
controller.hasStateProp ||
|
|
40354
|
+
controller.isProxy ||
|
|
40355
|
+
controller.props.signal
|
|
40356
|
+
) {
|
|
40357
|
+
return;
|
|
40358
|
+
}
|
|
40359
|
+
const { defaultValue } = controller;
|
|
40360
|
+
if (compareTwoJsValues(defaultValue, controller.defaultValueTaken)) {
|
|
40361
|
+
return;
|
|
40362
|
+
}
|
|
40363
|
+
if (controller.holdsEdit()) {
|
|
40364
|
+
return;
|
|
40365
|
+
}
|
|
40366
|
+
controller.defaultValueTaken = defaultValue;
|
|
40367
|
+
controller.state = defaultValue;
|
|
40368
|
+
controller.setUIState(
|
|
40369
|
+
defaultValue,
|
|
40370
|
+
new CustomEvent("outside_value_follow", { detail: {} }),
|
|
40371
|
+
);
|
|
40079
40372
|
},
|
|
40080
40373
|
// Read by the callout manager when it has nowhere else to point.
|
|
40081
40374
|
getCalloutAnchorElement: (event) =>
|
|
@@ -40089,7 +40382,12 @@ const useUIStateController = (
|
|
|
40089
40382
|
if (controller.hasStateProp || controller.queuedActionAllowedEvent) {
|
|
40090
40383
|
return;
|
|
40091
40384
|
}
|
|
40385
|
+
// A picker told yes is told for what its popup shows too, the way a
|
|
40386
|
+
// group tells its children. First, so that a default one of them
|
|
40387
|
+
// takes on the way is part of what this control accepts below.
|
|
40388
|
+
controller.facadeChild?.acknowledgeUIState();
|
|
40092
40389
|
controller.state = controller.uiState;
|
|
40390
|
+
controller.followDefaultValue();
|
|
40093
40391
|
},
|
|
40094
40392
|
onActionEnd: (e) => {
|
|
40095
40393
|
debugUIState(`"${controlType}" actionEnd called`);
|
|
@@ -40246,6 +40544,7 @@ const useUIStateController = (
|
|
|
40246
40544
|
}
|
|
40247
40545
|
}
|
|
40248
40546
|
}
|
|
40547
|
+
controller.followDefaultValue();
|
|
40249
40548
|
}
|
|
40250
40549
|
return liveValues();
|
|
40251
40550
|
},
|
|
@@ -40836,7 +41135,12 @@ const useUIGroupStateController = (
|
|
|
40836
41135
|
// deferred alongside a mount sync stays a real change.
|
|
40837
41136
|
const pendingChange = pendingChangeRef.current;
|
|
40838
41137
|
pendingChangeRef.current = {
|
|
40839
|
-
|
|
41138
|
+
// A default followed is not replaced by a mount deferred beside
|
|
41139
|
+
// it: the event is what lets the sync below read the children.
|
|
41140
|
+
e:
|
|
41141
|
+
pendingChange && isOutsideValueFollow(pendingChange.e)
|
|
41142
|
+
? pendingChange.e
|
|
41143
|
+
: e,
|
|
40840
41144
|
notifyExternal:
|
|
40841
41145
|
pendingChange?.notifyExternal === true ? true : notifyExternal,
|
|
40842
41146
|
actingChild,
|
|
@@ -40844,6 +41148,13 @@ const useUIGroupStateController = (
|
|
|
40844
41148
|
return;
|
|
40845
41149
|
}
|
|
40846
41150
|
const { controller } = s;
|
|
41151
|
+
if (isOutsideValueFollow(e)) {
|
|
41152
|
+
// Not a partial reading: the child was there already and its value
|
|
41153
|
+
// moved, told by the outside. The group is worth what its children
|
|
41154
|
+
// show, whatever it was handed before — kept, the value it sends
|
|
41155
|
+
// would be one the screen no longer shows.
|
|
41156
|
+
controller.stateGivenFromAbove = false;
|
|
41157
|
+
}
|
|
40847
41158
|
// A child mounting or unmounting is not somebody answering: while the
|
|
40848
41159
|
// children of a group are still arriving, their aggregate is a partial
|
|
40849
41160
|
// reading, and taking it for the truth is how the value the group was
|
|
@@ -41022,6 +41333,9 @@ const useUIGroupStateController = (
|
|
|
41022
41333
|
name,
|
|
41023
41334
|
value,
|
|
41024
41335
|
defaultValue,
|
|
41336
|
+
// Same as the leaf's (see useUIStateController): the default last
|
|
41337
|
+
// placed on the children, behind `defaultValue` while an edit waits.
|
|
41338
|
+
defaultValueTaken: defaultValue,
|
|
41025
41339
|
hasValueProp,
|
|
41026
41340
|
hasDefaultValueProp,
|
|
41027
41341
|
props,
|
|
@@ -41305,6 +41619,7 @@ const useUIGroupStateController = (
|
|
|
41305
41619
|
}
|
|
41306
41620
|
}
|
|
41307
41621
|
onChange(e, { notifyExternal: true });
|
|
41622
|
+
controller.followDefaultValue();
|
|
41308
41623
|
},
|
|
41309
41624
|
clearUIState: (e) => {
|
|
41310
41625
|
const ev = new CustomEvent("propagate_down_clear_ui_state", {
|
|
@@ -41333,6 +41648,46 @@ const useUIGroupStateController = (
|
|
|
41333
41648
|
c.acknowledgeUIState();
|
|
41334
41649
|
}
|
|
41335
41650
|
}
|
|
41651
|
+
controller.followDefaultValue();
|
|
41652
|
+
},
|
|
41653
|
+
// A group holds no value of its own to have edited: it holds an edit
|
|
41654
|
+
// when one of its children does.
|
|
41655
|
+
holdsEdit: () => {
|
|
41656
|
+
return childUIStateControllerArray.some(
|
|
41657
|
+
(c) => shouldPropagateStateToChild(c) && c.holdsEdit(),
|
|
41658
|
+
);
|
|
41659
|
+
},
|
|
41660
|
+
// The leaf's rule (see useUIStateController), for a group given a
|
|
41661
|
+
// plain `defaultValue`: a new one is placed on the children the way
|
|
41662
|
+
// the first one was as they registered, while none of them holds an
|
|
41663
|
+
// edit. A signal's own default is followed by the update below.
|
|
41664
|
+
followDefaultValue: () => {
|
|
41665
|
+
if (controller.hasValueProp || controller.props.signal) {
|
|
41666
|
+
return;
|
|
41667
|
+
}
|
|
41668
|
+
const { defaultValue } = controller;
|
|
41669
|
+
if (compareTwoJsValues(defaultValue, controller.defaultValueTaken)) {
|
|
41670
|
+
return;
|
|
41671
|
+
}
|
|
41672
|
+
if (controller.holdsEdit()) {
|
|
41673
|
+
return;
|
|
41674
|
+
}
|
|
41675
|
+
controller.defaultValueTaken = defaultValue;
|
|
41676
|
+
const followEvent = new CustomEvent("outside_value_follow", {
|
|
41677
|
+
detail: {},
|
|
41678
|
+
});
|
|
41679
|
+
const placeEvent = new CustomEvent("initial_state_push", {
|
|
41680
|
+
detail: {},
|
|
41681
|
+
});
|
|
41682
|
+
chainEvent(placeEvent, followEvent);
|
|
41683
|
+
controller.setUIState(
|
|
41684
|
+
defaultValue === undefined ? fallbackState : defaultValue,
|
|
41685
|
+
placeEvent,
|
|
41686
|
+
);
|
|
41687
|
+
s.parentUIStateController?.onChildUIAction(controller, followEvent, {
|
|
41688
|
+
stateChanged: true,
|
|
41689
|
+
silent: true,
|
|
41690
|
+
});
|
|
41336
41691
|
},
|
|
41337
41692
|
onActionEnd: (e) => {
|
|
41338
41693
|
acknowledgeOwnAction(controller);
|
|
@@ -41455,6 +41810,7 @@ const useUIGroupStateController = (
|
|
|
41455
41810
|
// former value back over it on the next child interaction.
|
|
41456
41811
|
placeChildrenFrom(defaultValue);
|
|
41457
41812
|
}
|
|
41813
|
+
controller.followDefaultValue();
|
|
41458
41814
|
|
|
41459
41815
|
return liveValues();
|
|
41460
41816
|
},
|
|
@@ -41681,7 +42037,11 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
41681
42037
|
if (child !== firstChildControllerRef.current) {
|
|
41682
42038
|
return;
|
|
41683
42039
|
}
|
|
41684
|
-
if (
|
|
42040
|
+
if (
|
|
42041
|
+
silent &&
|
|
42042
|
+
!isOutsideValueFollow(e) &&
|
|
42043
|
+
uiStateHoldsNothing(child.uiState)
|
|
42044
|
+
) {
|
|
41685
42045
|
// A silent sync means the child's own structure changed (children
|
|
41686
42046
|
// mounted/unmounted), not that the user acted. A child that ends up
|
|
41687
42047
|
// with no value there is one that currently *cannot* express one —
|
|
@@ -41692,7 +42052,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
41692
42052
|
// still nothing to adopt: the sync would only respell one nothing
|
|
41693
42053
|
// as another (an array picker's [] becoming undefined, say) and
|
|
41694
42054
|
// hand that to uiAction — an empty array picker opened its popup
|
|
41695
|
-
// and told its owner the value changed.
|
|
42055
|
+
// and told its owner the value changed. A default followed down to
|
|
42056
|
+
// nothing is not that: the outside emptied a value the child held.
|
|
41696
42057
|
return;
|
|
41697
42058
|
}
|
|
41698
42059
|
updatingRef.current = true;
|
|
@@ -41756,6 +42117,69 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
41756
42117
|
return scope.controller;
|
|
41757
42118
|
};
|
|
41758
42119
|
|
|
42120
|
+
/**
|
|
42121
|
+
* What every control of a picker — the picker and the controls in its popup —
|
|
42122
|
+
* has been accepted on as the popup opens. Read back by followOutsideSinceOpen
|
|
42123
|
+
* when the popup is cancelled.
|
|
42124
|
+
*/
|
|
42125
|
+
const readStatesAtOpen = (pickerController) => {
|
|
42126
|
+
const statesAtOpen = new Map();
|
|
42127
|
+
visitControllers(pickerController, (controller) => {
|
|
42128
|
+
if (Object.hasOwn(controller, "state")) {
|
|
42129
|
+
statesAtOpen.set(controller, controller.state);
|
|
42130
|
+
}
|
|
42131
|
+
});
|
|
42132
|
+
return statesAtOpen;
|
|
42133
|
+
};
|
|
42134
|
+
|
|
42135
|
+
/**
|
|
42136
|
+
* A cancel puts back the value the picker held at open, which takes back
|
|
42137
|
+
* everything that moved since — and not only what the user moved: a record
|
|
42138
|
+
* refreshed while the popup was open, a default that arrived under a field
|
|
42139
|
+
* holding an edit. Those were not the user's to take back. Once the value at
|
|
42140
|
+
* open is back, each control the outside moved since shows what the outside
|
|
42141
|
+
* holds again, and a default left waiting on an edit is taken there.
|
|
42142
|
+
*
|
|
42143
|
+
* Children before the control around them: a picker given its value by its
|
|
42144
|
+
* owner has the last word over the controls in its popup.
|
|
42145
|
+
*/
|
|
42146
|
+
const followOutsideSinceOpen = (pickerController, statesAtOpen) => {
|
|
42147
|
+
visitControllers(
|
|
42148
|
+
pickerController,
|
|
42149
|
+
(controller) => {
|
|
42150
|
+
if (
|
|
42151
|
+
statesAtOpen.has(controller) &&
|
|
42152
|
+
!compareTwoJsValues(controller.state, statesAtOpen.get(controller)) &&
|
|
42153
|
+
controller.holdsEdit()
|
|
42154
|
+
) {
|
|
42155
|
+
controller.setUIState(
|
|
42156
|
+
controller.state,
|
|
42157
|
+
new CustomEvent("outside_value_follow", { detail: {} }),
|
|
42158
|
+
);
|
|
42159
|
+
}
|
|
42160
|
+
controller.followDefaultValue();
|
|
42161
|
+
},
|
|
42162
|
+
{ childrenFirst: true },
|
|
42163
|
+
);
|
|
42164
|
+
};
|
|
42165
|
+
|
|
42166
|
+
// A picker reaches the controls of its popup through its facade child, a group
|
|
42167
|
+
// through the children it registered.
|
|
42168
|
+
const visitControllers = (controller, visit, { childrenFirst } = {}) => {
|
|
42169
|
+
if (!childrenFirst) {
|
|
42170
|
+
visit(controller);
|
|
42171
|
+
}
|
|
42172
|
+
const children = controller.facadeChild
|
|
42173
|
+
? [controller.facadeChild]
|
|
42174
|
+
: controller.getChildControllers?.() || [];
|
|
42175
|
+
for (const child of children) {
|
|
42176
|
+
visitControllers(child, visit, { childrenFirst });
|
|
42177
|
+
}
|
|
42178
|
+
if (childrenFirst) {
|
|
42179
|
+
visit(controller);
|
|
42180
|
+
}
|
|
42181
|
+
};
|
|
42182
|
+
|
|
41759
42183
|
const describePicker = (props) =>
|
|
41760
42184
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
41761
42185
|
|
|
@@ -41844,10 +42268,18 @@ const INTERNAL_EVENT_SET = new Set([
|
|
|
41844
42268
|
// no action of the control's own — but what it holds really did move, so
|
|
41845
42269
|
// uiAction, the bound signal and the parent notification below all happen.
|
|
41846
42270
|
"auto_fix",
|
|
42271
|
+
// A control put on a value the outside moved while nobody was editing it: a
|
|
42272
|
+
// new defaultValue (see followDefaultValue), or what a cancel puts back
|
|
42273
|
+
// (see followOutsideSinceOpen). Initialization arriving after mount, so
|
|
42274
|
+
// nothing fires, and the groups above hear it silently.
|
|
42275
|
+
"outside_value_follow",
|
|
41847
42276
|
]);
|
|
41848
42277
|
const isInternalEvent = (e) => {
|
|
41849
42278
|
return INTERNAL_EVENT_SET.has(e.type);
|
|
41850
42279
|
};
|
|
42280
|
+
const isOutsideValueFollow = (e) => {
|
|
42281
|
+
return Boolean(findEvent(e, "outside_value_follow"));
|
|
42282
|
+
};
|
|
41851
42283
|
|
|
41852
42284
|
/**
|
|
41853
42285
|
* A value handed DOWN to a control by whoever owns it: a picker filling its
|
|
@@ -47381,7 +47813,11 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
|
47381
47813
|
if (!element) {
|
|
47382
47814
|
return undefined;
|
|
47383
47815
|
}
|
|
47384
|
-
const onPointerDown = () => {
|
|
47816
|
+
const onPointerDown = (pointerdownEvent) => {
|
|
47817
|
+
// A right click opens a menu, never the popup.
|
|
47818
|
+
if (pointerdownEvent.button !== 0) {
|
|
47819
|
+
return;
|
|
47820
|
+
}
|
|
47385
47821
|
announceOpeningPress(element, command);
|
|
47386
47822
|
};
|
|
47387
47823
|
element.addEventListener("pointerdown", onPointerDown);
|
|
@@ -51480,8 +51916,14 @@ const Expandable = props => {
|
|
|
51480
51916
|
// before the click ever fires — so what held the focus has to be remembered
|
|
51481
51917
|
// at pointerdown time.
|
|
51482
51918
|
const focusedAtPointerDownRef = useRef(null);
|
|
51483
|
-
const onUIPointerDown =
|
|
51919
|
+
const onUIPointerDown = pointerdownEvent => {
|
|
51484
51920
|
focusedAtPointerDownRef.current = document.activeElement;
|
|
51921
|
+
// The start of an expansion, announced as a --navi-open button announces
|
|
51922
|
+
// its own (see announceOpeningPress in commands.js) — unless aimed at a
|
|
51923
|
+
// control inside the UI part, whose click does not toggle (see onUIClick).
|
|
51924
|
+
if (pointerdownEvent.button === 0 && !openController.opened && !isAimedAtControlInsideUI(pointerdownEvent.target)) {
|
|
51925
|
+
dispatchCustomEvent(rootRef.current, "navi_open_press");
|
|
51926
|
+
}
|
|
51485
51927
|
};
|
|
51486
51928
|
|
|
51487
51929
|
// The content keeps its final size while the track animates (see the top
|
|
@@ -51779,17 +52221,18 @@ const Expandable = props => {
|
|
|
51779
52221
|
if (clickEvent.defaultPrevented) {
|
|
51780
52222
|
return;
|
|
51781
52223
|
}
|
|
51782
|
-
|
|
51783
|
-
|
|
51784
|
-
} = clickEvent;
|
|
51785
|
-
if (target.nodeType === 1) {
|
|
51786
|
-
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
51787
|
-
if (interactiveElement && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement)) {
|
|
51788
|
-
return;
|
|
51789
|
-
}
|
|
52224
|
+
if (isAimedAtControlInsideUI(clickEvent.target)) {
|
|
52225
|
+
return;
|
|
51790
52226
|
}
|
|
51791
52227
|
toggle(clickEvent);
|
|
51792
52228
|
};
|
|
52229
|
+
const isAimedAtControlInsideUI = target => {
|
|
52230
|
+
if (target.nodeType !== 1) {
|
|
52231
|
+
return false;
|
|
52232
|
+
}
|
|
52233
|
+
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
52234
|
+
return Boolean(interactiveElement) && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement);
|
|
52235
|
+
};
|
|
51793
52236
|
|
|
51794
52237
|
// Space/Enter on the UI part itself (role button) — a key pressed on a
|
|
51795
52238
|
// control inside it belongs to that control.
|
|
@@ -60127,6 +60570,52 @@ const SlideContainer = ({
|
|
|
60127
60570
|
drag.areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
60128
60571
|
paintDrag();
|
|
60129
60572
|
},
|
|
60573
|
+
// The slide being brought in walked whole and the hand still going, or a
|
|
60574
|
+
// travel caught on its way thrown on: either way the hand asks for the
|
|
60575
|
+
// slide past it, and the gesture moves on to it. Nothing on screen moves
|
|
60576
|
+
// for that — the slide reached becomes the one dragged, the one the hand
|
|
60577
|
+
// came from stays next to it for a hand that turns around, and the slide
|
|
60578
|
+
// past it is put on the other side.
|
|
60579
|
+
onEdge: ({
|
|
60580
|
+
axis,
|
|
60581
|
+
sign
|
|
60582
|
+
}) => {
|
|
60583
|
+
const reached = sign > 0 ? drag.areaBack : drag.areaOn;
|
|
60584
|
+
const {
|
|
60585
|
+
slideElements,
|
|
60586
|
+
placeOf
|
|
60587
|
+
} = readMap();
|
|
60588
|
+
const reachedElement = reached && slideElements.find(slideElement => readArea(slideElement) === reached);
|
|
60589
|
+
if (!reachedElement) {
|
|
60590
|
+
return false;
|
|
60591
|
+
}
|
|
60592
|
+
// The gate goToArea reads at the release, asked of the slide the hand
|
|
60593
|
+
// is now in: a slide holding on to the user is not walked through.
|
|
60594
|
+
const holds = reachedElement.hasAttribute(sign > 0 ? "data-prevent-nav-previous" : "data-prevent-nav-next");
|
|
60595
|
+
const past = holds ? undefined : axis === "x" ? areaTowards(-sign, 0, reached) : areaTowards(0, -sign, reached);
|
|
60596
|
+
if (!past) {
|
|
60597
|
+
return false;
|
|
60598
|
+
}
|
|
60599
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
60600
|
+
const basePlace = stageRef.current?.placeByArea.get(reached) || placeOf.get(reached) || {
|
|
60601
|
+
x: 0,
|
|
60602
|
+
y: 0
|
|
60603
|
+
};
|
|
60604
|
+
drag.areaBack = sign > 0 ? past : drag.area;
|
|
60605
|
+
drag.areaOn = sign > 0 ? drag.area : past;
|
|
60606
|
+
drag.area = reached;
|
|
60607
|
+
drag.basePlace = basePlace;
|
|
60608
|
+
drag.baseOffset = {
|
|
60609
|
+
x: -basePlace.x * drag.box.width,
|
|
60610
|
+
y: -basePlace.y * drag.box.height
|
|
60611
|
+
};
|
|
60612
|
+
stageDrag(drag, readArea(currentElement));
|
|
60613
|
+
return {
|
|
60614
|
+
size: axis === "x" ? drag.box.width : drag.box.height,
|
|
60615
|
+
travelBack: Boolean(drag.areaBack),
|
|
60616
|
+
travelOn: Boolean(drag.areaOn)
|
|
60617
|
+
};
|
|
60618
|
+
},
|
|
60130
60619
|
onEnd: ({
|
|
60131
60620
|
axis,
|
|
60132
60621
|
sign,
|
|
@@ -64099,8 +64588,9 @@ const css$E = /* css */`
|
|
|
64099
64588
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
64100
64589
|
* changed while the popup was closed. Intent on the anchor (pointer
|
|
64101
64590
|
* entering it, focus landing in it) builds the content ahead of the click;
|
|
64102
|
-
* under `"while-opened"`, only a press on
|
|
64103
|
-
* open
|
|
64591
|
+
* under `"while-opened"`, only a press on what opens the popup does (a
|
|
64592
|
+
* `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
|
|
64593
|
+
* press that does not open the popup throws it away.
|
|
64104
64594
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
64105
64595
|
*/
|
|
64106
64596
|
const Dialog = props => {
|
|
@@ -65862,8 +66352,9 @@ const css$D = /* css */`
|
|
|
65862
66352
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
65863
66353
|
* changed while the popup was closed. Intent on the anchor (pointer
|
|
65864
66354
|
* entering it, focus landing in it) builds the content ahead of the click;
|
|
65865
|
-
* under `"while-opened"`, only a press on
|
|
65866
|
-
* open
|
|
66355
|
+
* under `"while-opened"`, only a press on what opens the popup does (a
|
|
66356
|
+
* `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
|
|
66357
|
+
* press that does not open the popup throws it away.
|
|
65867
66358
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
65868
66359
|
*/
|
|
65869
66360
|
const Popover = props => {
|
|
@@ -67536,7 +68027,10 @@ const PickerCustom = props => {
|
|
|
67536
68027
|
// a defaultValue holds nothing, so closing on it untouched IS the answer
|
|
67537
68028
|
// ("yes, 2h15") — the same rule Form applies to an untouched field (see
|
|
67538
68029
|
// isUIStateHeld). Read at open, before anything inside can change it.
|
|
67539
|
-
const
|
|
68030
|
+
const pickerController = getPickerInput(ref.current)?.__uiStateController__;
|
|
68031
|
+
const heldAtOpen = isUIStateHeld(pickerController);
|
|
68032
|
+
// What a cancel must not take back: see followOutsideSinceOpen.
|
|
68033
|
+
const statesAtOpen = pickerController ? readStatesAtOpen(pickerController) : null;
|
|
67540
68034
|
debugPopup(openEvent, `picker opened, store value at open`, valueAtOpen, heldAtOpen ? `(held)` : `(a suggestion, not an answer yet)`);
|
|
67541
68035
|
onOpen?.(openEvent);
|
|
67542
68036
|
return {
|
|
@@ -67605,6 +68099,9 @@ const PickerCustom = props => {
|
|
|
67605
68099
|
});
|
|
67606
68100
|
chainEvent(rollbackEvent, closeEvent);
|
|
67607
68101
|
inputEl.__uiStateController__.setUIState(valueAtOpen, rollbackEvent);
|
|
68102
|
+
if (statesAtOpen) {
|
|
68103
|
+
followOutsideSinceOpen(inputEl.__uiStateController__, statesAtOpen);
|
|
68104
|
+
}
|
|
67608
68105
|
} else if (!heldAtOpen) {
|
|
67609
68106
|
// Confirmed a suggestion: nothing changed, so nothing has told the
|
|
67610
68107
|
// control's own bound signal / uiAction that this is now the
|
|
@@ -67696,6 +68193,11 @@ const PickerCustom = props => {
|
|
|
67696
68193
|
"onnavi_request_open": e => {
|
|
67697
68194
|
dispatchCustomEvent(popupRef.current, "navi_request_open", e.detail);
|
|
67698
68195
|
},
|
|
68196
|
+
// And the press announcing that request (see announceOpeningPress in
|
|
68197
|
+
// commands.js), for the popup's content to be built on it.
|
|
68198
|
+
"onnavi_open_press": () => {
|
|
68199
|
+
dispatchCustomEvent(popupRef.current, "navi_open_press");
|
|
68200
|
+
},
|
|
67699
68201
|
"onnavi_request_close": e => {
|
|
67700
68202
|
const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
|
|
67701
68203
|
if (!closing) {
|
|
@@ -67890,6 +68392,25 @@ const PickerCustom = props => {
|
|
|
67890
68392
|
// below), and the keyboard keeps its own ways in (the shortcuts above).
|
|
67891
68393
|
const openOnList = Array.isArray(openOn) ? openOn : [openOn];
|
|
67892
68394
|
const opensOnPress = openOnList.includes("press");
|
|
68395
|
+
// A press on the trigger of a picker a press opens is announced as it
|
|
68396
|
+
// starts, as a --navi-open button announces its own (see
|
|
68397
|
+
// announceOpeningPress in commands.js). Only a press that can open it: a
|
|
68398
|
+
// picker opened by a hold says nothing on a tap, nor does one refusing
|
|
68399
|
+
// to open (read-only with openWhileReadOnly={false}, busy). The
|
|
68400
|
+
// interactivity is asked alone, not the whole gate: a drag source
|
|
68401
|
+
// cancels the pointerdown, and the click that opens still comes.
|
|
68402
|
+
if (opensOnPress) {
|
|
68403
|
+
pickerProps.onPointerDown = pointerdownEvent => {
|
|
68404
|
+
props.onPointerDown?.(pointerdownEvent);
|
|
68405
|
+
const pickerEl = ref.current;
|
|
68406
|
+
if (pointerdownEvent.button !== 0 || openController.opened || isWithinPickerContent(pointerdownEvent.target) || !allowsInteraction(pickerEl, {
|
|
68407
|
+
intent: "read"
|
|
68408
|
+
})) {
|
|
68409
|
+
return;
|
|
68410
|
+
}
|
|
68411
|
+
dispatchCustomEvent(pickerEl, "navi_open_press");
|
|
68412
|
+
};
|
|
68413
|
+
}
|
|
67893
68414
|
let interactions = props.interactions;
|
|
67894
68415
|
if (!opensOnPress) {
|
|
67895
68416
|
interactions = {
|