@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/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, 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, 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, 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;
|
|
@@ -298,6 +321,112 @@ const urlToScheme = (url) => {
|
|
|
298
321
|
return scheme;
|
|
299
322
|
};
|
|
300
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The document's rendering, held for the one frame a view transition needs.
|
|
326
|
+
*
|
|
327
|
+
* The browser does not take the picture of the page being left when a
|
|
328
|
+
* transition is ASKED for — it takes it at the next frame, just before running
|
|
329
|
+
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
330
|
+
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
331
|
+
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
332
|
+
* Both sides of the animation then show it, and one watches a page slide onto
|
|
333
|
+
* itself.
|
|
334
|
+
*
|
|
335
|
+
* So what Preact has queued waits until the update callback, which is the
|
|
336
|
+
* moment the API is built around — the change belongs inside it. The whole
|
|
337
|
+
* document is held: it is about to be frozen under a picture anyway.
|
|
338
|
+
*
|
|
339
|
+
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
340
|
+
* around Preact's `options.debounceRendering`, and two of them installed
|
|
341
|
+
* independently restore each other in the wrong order when they let go — every
|
|
342
|
+
* render queued in between is then handed to a wrapper nobody will ever
|
|
343
|
+
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
344
|
+
* route transition) must therefore hold through this module, never through a
|
|
345
|
+
* wrapper of its own.
|
|
346
|
+
*/
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
let renderingHold = null;
|
|
350
|
+
const holdRendering = () => {
|
|
351
|
+
if (renderingHold) {
|
|
352
|
+
return renderingHold.release;
|
|
353
|
+
}
|
|
354
|
+
const debounceRenderingBefore = options.debounceRendering;
|
|
355
|
+
const hold = {
|
|
356
|
+
render: null,
|
|
357
|
+
waiting: [],
|
|
358
|
+
release: () => {
|
|
359
|
+
// Only the hold that is still standing may be given back: a holder
|
|
360
|
+
// releasing after another has taken over must not let go of what it
|
|
361
|
+
// does not hold.
|
|
362
|
+
if (renderingHold !== hold) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
renderingHold = null;
|
|
366
|
+
options.debounceRendering = debounceRenderingBefore;
|
|
367
|
+
const { render, waiting } = hold;
|
|
368
|
+
hold.render = null;
|
|
369
|
+
hold.waiting = [];
|
|
370
|
+
if (render) {
|
|
371
|
+
render();
|
|
372
|
+
}
|
|
373
|
+
for (const wait of waiting) {
|
|
374
|
+
wait();
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
renderingHold = hold;
|
|
379
|
+
options.debounceRendering = (render) => {
|
|
380
|
+
hold.render = render;
|
|
381
|
+
};
|
|
382
|
+
return hold.release;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Anything else that must not happen before the picture is taken, and the
|
|
386
|
+
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
387
|
+
// document put back to its top while the page being left is still on screen is
|
|
388
|
+
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
389
|
+
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
390
|
+
// the band it had already painted — the page being left is then seen in
|
|
391
|
+
// fragments, whatever the movement does afterwards.
|
|
392
|
+
//
|
|
393
|
+
// Run at once when nobody is photographing anything, which is the common case
|
|
394
|
+
// and must stay free.
|
|
395
|
+
const whenRenderingResumes = (callback) => {
|
|
396
|
+
if (!renderingHold) {
|
|
397
|
+
callback();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
renderingHold.waiting.push(callback);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// The hold a navigation takes on its way in — from before its first write,
|
|
404
|
+
// because by the time a route announces that it matches, Preact has already
|
|
405
|
+
// been told and the render is queued; a hold taken then is a hold taken too
|
|
406
|
+
// late. Kept here until whoever animates the change takes it over, or the
|
|
407
|
+
// navigation turns out to be one nobody animates.
|
|
408
|
+
let routingRenderingHold = null;
|
|
409
|
+
const holdRenderingForRouting = () => {
|
|
410
|
+
routingRenderingHold = holdRendering();
|
|
411
|
+
};
|
|
412
|
+
// Nobody had a picture to take: a page held for a change it does not animate
|
|
413
|
+
// is a page that stutters for nothing.
|
|
414
|
+
const releaseRoutingRenderingHold = () => {
|
|
415
|
+
const release = routingRenderingHold;
|
|
416
|
+
routingRenderingHold = null;
|
|
417
|
+
if (release) {
|
|
418
|
+
release();
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
// An animator takes the navigation's hold as its own — taking another would be
|
|
422
|
+
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
423
|
+
// the change it animates is not a navigation.
|
|
424
|
+
const takeoverRoutingRenderingHold = () => {
|
|
425
|
+
const release = routingRenderingHold || holdRendering();
|
|
426
|
+
routingRenderingHold = null;
|
|
427
|
+
return release;
|
|
428
|
+
};
|
|
429
|
+
|
|
301
430
|
installImportMetaCssBuild(import.meta);
|
|
302
431
|
|
|
303
432
|
const URL_TARGET_ATTRIBUTE = "data-url-target";
|
|
@@ -347,7 +476,8 @@ let urlTargetOptions = {
|
|
|
347
476
|
* How long, in ms, to keep waiting for a target that has not arrived, counted
|
|
348
477
|
* from the moment the document stops working.
|
|
349
478
|
* @param {number} [options.maxWait=10000]
|
|
350
|
-
* Longest wait, in ms, for a document that never stops working.
|
|
479
|
+
* Longest wait, in ms, for a document that never stops working. The element
|
|
480
|
+
* is then answered as it stands, when it is there.
|
|
351
481
|
*/
|
|
352
482
|
const setUrlTargetOptions = (options) => {
|
|
353
483
|
urlTargetOptions = { ...urlTargetOptions, ...options };
|
|
@@ -374,6 +504,23 @@ const useUrlTargetId = () => {
|
|
|
374
504
|
let stopWaitingForCurrentTarget = null;
|
|
375
505
|
let currentTargetKey;
|
|
376
506
|
|
|
507
|
+
const documentLoadIsReturn = () => {
|
|
508
|
+
const [navigationEntry] = performance.getEntriesByType("navigation");
|
|
509
|
+
if (!navigationEntry) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
return (
|
|
513
|
+
navigationEntry.type === "reload" || navigationEntry.type === "back_forward"
|
|
514
|
+
);
|
|
515
|
+
};
|
|
516
|
+
// The url the document is on its way back to — a back, a forward, a reload —
|
|
517
|
+
// where the reader was, as opposed to a place just sent to. Said by the
|
|
518
|
+
// navigation before it writes anything, and by the load for the first url.
|
|
519
|
+
let returningToUrl = documentLoadIsReturn() ? window.location.href : null;
|
|
520
|
+
observeBeforeRouting(({ url, navigationType }) => {
|
|
521
|
+
returningToUrl = navigationType === "traverse" ? url : null;
|
|
522
|
+
});
|
|
523
|
+
|
|
377
524
|
/**
|
|
378
525
|
* Answers the URL's target again, as if it had just been designated.
|
|
379
526
|
*
|
|
@@ -383,10 +530,10 @@ let currentTargetKey;
|
|
|
383
530
|
*/
|
|
384
531
|
const rearmUrlTarget = () => {
|
|
385
532
|
currentTargetKey = undefined;
|
|
386
|
-
armUrlTarget(documentUrlSignal.peek());
|
|
533
|
+
armUrlTarget(documentUrlSignal.peek(), { isReturn: false });
|
|
387
534
|
};
|
|
388
535
|
|
|
389
|
-
const armUrlTarget = (documentUrl) => {
|
|
536
|
+
const armUrlTarget = (documentUrl, { isReturn }) => {
|
|
390
537
|
const targetKey = urlToTargetKey(documentUrl);
|
|
391
538
|
if (targetKey === currentTargetKey) {
|
|
392
539
|
return;
|
|
@@ -397,11 +544,25 @@ const armUrlTarget = (documentUrl) => {
|
|
|
397
544
|
stopWaitingForCurrentTarget = null;
|
|
398
545
|
}
|
|
399
546
|
const targetId = urlToTargetId(documentUrl);
|
|
547
|
+
// The browser leaves `:target` on the element a URL named once the URL has
|
|
548
|
+
// stopped naming it — a pushState never touches it.
|
|
549
|
+
const staleTarget = document.querySelector(":target");
|
|
550
|
+
if (staleTarget && staleTarget.id !== targetId) {
|
|
551
|
+
writeTarget(null);
|
|
552
|
+
}
|
|
400
553
|
if (!targetId) {
|
|
401
554
|
return;
|
|
402
555
|
}
|
|
403
|
-
stopWaitingForCurrentTarget =
|
|
556
|
+
stopWaitingForCurrentTarget = waitForUrlTarget(targetId, (element) => {
|
|
404
557
|
stopWaitingForCurrentTarget = null;
|
|
558
|
+
if (!element.matches(":target")) {
|
|
559
|
+
writeTarget(element);
|
|
560
|
+
}
|
|
561
|
+
if (isReturn) {
|
|
562
|
+
// The offset put back by scroll_restoration.js is the reader's own: the
|
|
563
|
+
// element is named by the URL, not arrived at.
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
405
566
|
revealUrlTarget(element);
|
|
406
567
|
});
|
|
407
568
|
};
|
|
@@ -418,14 +579,27 @@ const urlToTargetId = (url) => {
|
|
|
418
579
|
return hash ? decodeURIComponent(hash.slice(1)) : "";
|
|
419
580
|
};
|
|
420
581
|
|
|
421
|
-
|
|
582
|
+
// Calls `onSettled` with the element once it can be answered: it exists, it
|
|
583
|
+
// shows something, and the document is done working. Found is not enough while
|
|
584
|
+
// the document works — what sits above it may still be drawn and push it down,
|
|
585
|
+
// and a page reusing a node of the page being left holds it before its own
|
|
586
|
+
// data has come.
|
|
587
|
+
//
|
|
588
|
+
// Confirmed at the next frame, once rendering has resumed: the render the last
|
|
589
|
+
// answer asked for has run by then, and a component starting its own run from
|
|
590
|
+
// that render has made the document busy again.
|
|
591
|
+
const waitForUrlTarget = (id, onSettled) => {
|
|
592
|
+
const { graceAfterIdle, maxWait } = urlTargetOptions;
|
|
593
|
+
let stopped = false;
|
|
594
|
+
let confirmationPending = false;
|
|
595
|
+
let frame = null;
|
|
422
596
|
let mutationObserver = null;
|
|
423
597
|
let stopWatchingBusy = null;
|
|
424
598
|
let idleTimeout = null;
|
|
425
599
|
let maxWaitTimeout = null;
|
|
426
|
-
let found = false;
|
|
427
600
|
|
|
428
601
|
const stopWaiting = () => {
|
|
602
|
+
stopped = true;
|
|
429
603
|
if (mutationObserver) {
|
|
430
604
|
mutationObserver.disconnect();
|
|
431
605
|
mutationObserver = null;
|
|
@@ -436,76 +610,214 @@ const waitForElementWithId = (id, onFound) => {
|
|
|
436
610
|
}
|
|
437
611
|
clearTimeout(idleTimeout);
|
|
438
612
|
clearTimeout(maxWaitTimeout);
|
|
613
|
+
if (frame !== null) {
|
|
614
|
+
cancelAnimationFrame(frame);
|
|
615
|
+
frame = null;
|
|
616
|
+
}
|
|
439
617
|
};
|
|
440
618
|
|
|
441
|
-
const
|
|
619
|
+
const findElement = () => {
|
|
442
620
|
const element = document.getElementById(id);
|
|
443
621
|
if (!element) {
|
|
444
|
-
return;
|
|
622
|
+
return null;
|
|
445
623
|
}
|
|
446
624
|
// Rendered inside a closed tab, a folded details, a view that is not the
|
|
447
|
-
// one on screen: the element exists but would show nothing.
|
|
448
|
-
// it is the same wait, for the same reason.
|
|
625
|
+
// one on screen: the element exists but would show nothing.
|
|
449
626
|
if (element.checkVisibility && !element.checkVisibility()) {
|
|
450
|
-
return;
|
|
627
|
+
return null;
|
|
451
628
|
}
|
|
452
|
-
|
|
629
|
+
return element;
|
|
630
|
+
};
|
|
631
|
+
const settle = (element) => {
|
|
453
632
|
stopWaiting();
|
|
454
|
-
|
|
633
|
+
onSettled(element);
|
|
634
|
+
};
|
|
635
|
+
const check = () => {
|
|
636
|
+
if (confirmationPending || documentIsBusySignal.peek() || !findElement()) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
confirmationPending = true;
|
|
640
|
+
whenRenderingResumes(() => {
|
|
641
|
+
if (stopped) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
frame = requestAnimationFrame(() => {
|
|
645
|
+
frame = null;
|
|
646
|
+
confirmationPending = false;
|
|
647
|
+
if (documentIsBusySignal.peek()) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const element = findElement();
|
|
651
|
+
if (element) {
|
|
652
|
+
settle(element);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
});
|
|
455
656
|
};
|
|
456
657
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
mutationObserver = new MutationObserver(checkForElement);
|
|
658
|
+
mutationObserver = new MutationObserver(check);
|
|
659
|
+
// Every attribute, not only `id`: what shows an element that is already
|
|
660
|
+
// there — a <details> opening, a tab panel losing its display: none — is an
|
|
661
|
+
// attribute written somewhere above it, and nothing else says so.
|
|
463
662
|
mutationObserver.observe(document.documentElement, {
|
|
464
663
|
childList: true,
|
|
465
664
|
subtree: true,
|
|
466
665
|
attributes: true,
|
|
467
|
-
attributeFilter: ["id"],
|
|
468
666
|
});
|
|
469
|
-
const { graceAfterIdle, maxWait } = urlTargetOptions;
|
|
470
667
|
stopWatchingBusy = effect(() => {
|
|
471
668
|
const documentIsBusy = documentIsBusySignal.value;
|
|
472
669
|
clearTimeout(idleTimeout);
|
|
473
|
-
if (
|
|
474
|
-
|
|
670
|
+
if (documentIsBusy) {
|
|
671
|
+
return;
|
|
475
672
|
}
|
|
476
|
-
|
|
477
|
-
|
|
673
|
+
check();
|
|
674
|
+
// The grace is for an element that has not come; one already there and
|
|
675
|
+
// waiting for rendering to resume is not given up on.
|
|
676
|
+
idleTimeout = setTimeout(() => {
|
|
677
|
+
if (!confirmationPending) {
|
|
678
|
+
stopWaiting();
|
|
679
|
+
}
|
|
680
|
+
}, graceAfterIdle);
|
|
681
|
+
});
|
|
682
|
+
// A document that never stops working: the element as it stands, when it is
|
|
683
|
+
// there at all.
|
|
684
|
+
maxWaitTimeout = setTimeout(() => {
|
|
685
|
+
const element = findElement();
|
|
686
|
+
if (element) {
|
|
687
|
+
settle(element);
|
|
688
|
+
} else {
|
|
689
|
+
stopWaiting();
|
|
690
|
+
}
|
|
691
|
+
}, maxWait);
|
|
478
692
|
|
|
479
693
|
return stopWaiting;
|
|
480
694
|
};
|
|
481
695
|
|
|
482
696
|
const revealUrlTarget = (element) => {
|
|
483
697
|
const { block, behavior, markDuration } = urlTargetOptions;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
698
|
+
const prefersReducedMotion = window.matchMedia(
|
|
699
|
+
"(prefers-reduced-motion: reduce)",
|
|
700
|
+
).matches;
|
|
701
|
+
element.scrollIntoView({
|
|
702
|
+
block,
|
|
703
|
+
behavior: prefersReducedMotion ? "instant" : behavior,
|
|
704
|
+
});
|
|
705
|
+
// What the browser does when it handles a fragment itself: keyboard
|
|
706
|
+
// navigation resumes from the target, not from the top of the document.
|
|
707
|
+
if (elementIsFocusable(element)) {
|
|
708
|
+
element.focus({ preventScroll: true });
|
|
709
|
+
}
|
|
710
|
+
element.setAttribute(URL_TARGET_ATTRIBUTE, "");
|
|
711
|
+
setTimeout(() => {
|
|
712
|
+
element.removeAttribute(URL_TARGET_ATTRIBUTE);
|
|
713
|
+
}, markDuration);
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
let targetWriteInProgress = false;
|
|
717
|
+
/**
|
|
718
|
+
* True while navi writes the browser's `:target`. The fragment navigation that
|
|
719
|
+
* takes is announced like any other — a popstate, a navigate event, an entry
|
|
720
|
+
* change — and names the address and the state the document is already at, so
|
|
721
|
+
* whoever routes on those events leaves it alone.
|
|
722
|
+
*/
|
|
723
|
+
const isTargetWriteInProgress = () => {
|
|
724
|
+
return targetWriteInProgress;
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// `:target` onto `element`, or off whatever holds it when `element` is null.
|
|
728
|
+
//
|
|
729
|
+
// A replace towards the address the browser is at: the browser answers it as a
|
|
730
|
+
// fragment navigation, and that is the only moment it sets `:target`. It also
|
|
731
|
+
// scrolls to what the fragment designates, resets the entry's states
|
|
732
|
+
// (history.state in Firefox, the Navigation API state everywhere) and fires a
|
|
733
|
+
// popstate (Chrome, Safari) and a navigate event. The address, both states and
|
|
734
|
+
// the scroll are put back in the same task, so nothing of it is painted; a
|
|
735
|
+
// running view transition is not skipped by it (measured in Chrome, Firefox
|
|
736
|
+
// and Safari).
|
|
737
|
+
const writeTarget = (element) => {
|
|
738
|
+
const address = window.location.href;
|
|
739
|
+
const historyState = window.history.state;
|
|
740
|
+
const { navigation } = window;
|
|
741
|
+
const entry = navigation ? navigation.currentEntry : null;
|
|
742
|
+
const entryState = entry ? entry.getState() : undefined;
|
|
743
|
+
const putScrollBack = captureScroll(element);
|
|
744
|
+
const addressFragment = new URL(address).hash.slice(1);
|
|
745
|
+
const addressTargetId = decodeURIComponent(addressFragment);
|
|
746
|
+
|
|
747
|
+
let fragmentUrl;
|
|
748
|
+
let silencedElement = null;
|
|
749
|
+
if (element) {
|
|
750
|
+
fragmentUrl =
|
|
751
|
+
element.id === addressTargetId
|
|
752
|
+
? address
|
|
753
|
+
: `${urlWithoutFragment(address)}#${encodeURIComponent(element.id)}`;
|
|
754
|
+
} else if (addressFragment) {
|
|
755
|
+
// The address itself, so the fragment does not change and no hashchange
|
|
756
|
+
// follows; the element it names briefly without its id, so it designates
|
|
757
|
+
// nothing.
|
|
758
|
+
fragmentUrl = address;
|
|
759
|
+
silencedElement = document.getElementById(addressTargetId);
|
|
760
|
+
} else {
|
|
761
|
+
// An empty fragment designates the top of the document; the scroll it
|
|
762
|
+
// makes is put back with the others.
|
|
763
|
+
fragmentUrl = `${address}#`;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
targetWriteInProgress = true;
|
|
767
|
+
try {
|
|
768
|
+
if (silencedElement) {
|
|
769
|
+
silencedElement.removeAttribute("id");
|
|
498
770
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
771
|
+
window.location.replace(fragmentUrl);
|
|
772
|
+
} finally {
|
|
773
|
+
if (silencedElement) {
|
|
774
|
+
silencedElement.id = addressTargetId;
|
|
775
|
+
}
|
|
776
|
+
window.history.replaceState(historyState, null, address);
|
|
777
|
+
if (entry && navigation.currentEntry) {
|
|
778
|
+
navigation.updateCurrentEntry({ state: entryState });
|
|
779
|
+
}
|
|
780
|
+
targetWriteInProgress = false;
|
|
781
|
+
}
|
|
782
|
+
putScrollBack();
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// Every box a fragment navigation towards `element` may scroll: its ancestors,
|
|
786
|
+
// and the document.
|
|
787
|
+
const captureScroll = (element) => {
|
|
788
|
+
const boxes = [];
|
|
789
|
+
let box = element ? element.parentElement : null;
|
|
790
|
+
while (box) {
|
|
791
|
+
boxes.push(box);
|
|
792
|
+
box = box.parentElement;
|
|
793
|
+
}
|
|
794
|
+
const { scrollingElement } = document;
|
|
795
|
+
if (!boxes.includes(scrollingElement)) {
|
|
796
|
+
boxes.push(scrollingElement);
|
|
797
|
+
}
|
|
798
|
+
const positions = boxes.map((box) => {
|
|
799
|
+
return { box, left: box.scrollLeft, top: box.scrollTop };
|
|
503
800
|
});
|
|
801
|
+
return () => {
|
|
802
|
+
for (const { box, left, top } of positions) {
|
|
803
|
+
if (box.scrollLeft !== left || box.scrollTop !== top) {
|
|
804
|
+
box.scrollTo({ left, top, behavior: "instant" });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
const urlWithoutFragment = (url) => {
|
|
811
|
+
const urlObject = new URL(url);
|
|
812
|
+
urlObject.hash = "";
|
|
813
|
+
return urlObject.href;
|
|
504
814
|
};
|
|
505
815
|
|
|
506
816
|
effect(() => {
|
|
507
817
|
const documentUrl = documentUrlSignal.value;
|
|
508
|
-
|
|
818
|
+
const isReturn = returningToUrl === documentUrl;
|
|
819
|
+
returningToUrl = null;
|
|
820
|
+
armUrlTarget(documentUrl, { isReturn });
|
|
509
821
|
});
|
|
510
822
|
|
|
511
823
|
/**
|
|
@@ -10103,6 +10415,26 @@ const dispatchRequestInteraction = (
|
|
|
10103
10415
|
});
|
|
10104
10416
|
};
|
|
10105
10417
|
|
|
10418
|
+
/**
|
|
10419
|
+
* Whether the control `element` belongs to lets an interaction of this intent
|
|
10420
|
+
* through, asked ahead of the event the gate will judge, and silently: nothing
|
|
10421
|
+
* is reported, nothing runs. For what a press is about to lead to (see
|
|
10422
|
+
* announceOpeningPress in commands.js) — the press itself is not that event: a
|
|
10423
|
+
* drag source cancels a `pointerdown` whose click still comes and opens.
|
|
10424
|
+
*
|
|
10425
|
+
* @param {Element} element
|
|
10426
|
+
* @param {{ intent: "read" | "write" }} options
|
|
10427
|
+
* @returns {boolean}
|
|
10428
|
+
*/
|
|
10429
|
+
const allowsInteraction = (element, { intent }) => {
|
|
10430
|
+
const controlHost = findControlHost(element) || element;
|
|
10431
|
+
const ci = controlHost.__uiStateController__?.rules.interaction;
|
|
10432
|
+
if (!ci) {
|
|
10433
|
+
return true;
|
|
10434
|
+
}
|
|
10435
|
+
return ci.checkInteractivity({ intent });
|
|
10436
|
+
};
|
|
10437
|
+
|
|
10106
10438
|
const onRequestInteraction = (
|
|
10107
10439
|
requestInteractionCustomEvent,
|
|
10108
10440
|
{ debugInteraction },
|
|
@@ -27538,112 +27870,6 @@ const dropGeneratedIdKeys = (state) => {
|
|
|
27538
27870
|
return stateWithoutGeneratedIdKeys;
|
|
27539
27871
|
};
|
|
27540
27872
|
|
|
27541
|
-
/**
|
|
27542
|
-
* The document's rendering, held for the one frame a view transition needs.
|
|
27543
|
-
*
|
|
27544
|
-
* The browser does not take the picture of the page being left when a
|
|
27545
|
-
* transition is ASKED for — it takes it at the next frame, just before running
|
|
27546
|
-
* the update callback. Preact renders sooner than that, in a microtask: so a
|
|
27547
|
-
* change nobody asked for (a tab pressed, the back button) has already reached
|
|
27548
|
-
* the DOM when the picture is taken, and the picture is of the page ARRIVING.
|
|
27549
|
-
* Both sides of the animation then show it, and one watches a page slide onto
|
|
27550
|
-
* itself.
|
|
27551
|
-
*
|
|
27552
|
-
* So what Preact has queued waits until the update callback, which is the
|
|
27553
|
-
* moment the API is built around — the change belongs inside it. The whole
|
|
27554
|
-
* document is held: it is about to be frozen under a picture anyway.
|
|
27555
|
-
*
|
|
27556
|
-
* ONE hold for the whole document, whoever animates. The hold is a wrapper
|
|
27557
|
-
* around Preact's `options.debounceRendering`, and two of them installed
|
|
27558
|
-
* independently restore each other in the wrong order when they let go — every
|
|
27559
|
-
* render queued in between is then handed to a wrapper nobody will ever
|
|
27560
|
-
* release. Everything that photographs a navigation (RouteTravel's box, a
|
|
27561
|
-
* route transition) must therefore hold through this module, never through a
|
|
27562
|
-
* wrapper of its own.
|
|
27563
|
-
*/
|
|
27564
|
-
|
|
27565
|
-
|
|
27566
|
-
let renderingHold = null;
|
|
27567
|
-
const holdRendering = () => {
|
|
27568
|
-
if (renderingHold) {
|
|
27569
|
-
return renderingHold.release;
|
|
27570
|
-
}
|
|
27571
|
-
const debounceRenderingBefore = options.debounceRendering;
|
|
27572
|
-
const hold = {
|
|
27573
|
-
render: null,
|
|
27574
|
-
waiting: [],
|
|
27575
|
-
release: () => {
|
|
27576
|
-
// Only the hold that is still standing may be given back: a holder
|
|
27577
|
-
// releasing after another has taken over must not let go of what it
|
|
27578
|
-
// does not hold.
|
|
27579
|
-
if (renderingHold !== hold) {
|
|
27580
|
-
return;
|
|
27581
|
-
}
|
|
27582
|
-
renderingHold = null;
|
|
27583
|
-
options.debounceRendering = debounceRenderingBefore;
|
|
27584
|
-
const { render, waiting } = hold;
|
|
27585
|
-
hold.render = null;
|
|
27586
|
-
hold.waiting = [];
|
|
27587
|
-
if (render) {
|
|
27588
|
-
render();
|
|
27589
|
-
}
|
|
27590
|
-
for (const wait of waiting) {
|
|
27591
|
-
wait();
|
|
27592
|
-
}
|
|
27593
|
-
},
|
|
27594
|
-
};
|
|
27595
|
-
renderingHold = hold;
|
|
27596
|
-
options.debounceRendering = (render) => {
|
|
27597
|
-
hold.render = render;
|
|
27598
|
-
};
|
|
27599
|
-
return hold.release;
|
|
27600
|
-
};
|
|
27601
|
-
|
|
27602
|
-
// Anything else that must not happen before the picture is taken, and the
|
|
27603
|
-
// scroll is the other one: a page one arrives at starts at its top, and the
|
|
27604
|
-
// document put back to its top while the page being left is still on screen is
|
|
27605
|
-
// a page that has ALREADY jumped when the picture is taken. Worse, the browser
|
|
27606
|
-
// paints what the new offset shows and nothing else, so the picture keeps only
|
|
27607
|
-
// the band it had already painted — the page being left is then seen in
|
|
27608
|
-
// fragments, whatever the movement does afterwards.
|
|
27609
|
-
//
|
|
27610
|
-
// Run at once when nobody is photographing anything, which is the common case
|
|
27611
|
-
// and must stay free.
|
|
27612
|
-
const whenRenderingResumes = (callback) => {
|
|
27613
|
-
if (!renderingHold) {
|
|
27614
|
-
callback();
|
|
27615
|
-
return;
|
|
27616
|
-
}
|
|
27617
|
-
renderingHold.waiting.push(callback);
|
|
27618
|
-
};
|
|
27619
|
-
|
|
27620
|
-
// The hold a navigation takes on its way in — from before its first write,
|
|
27621
|
-
// because by the time a route announces that it matches, Preact has already
|
|
27622
|
-
// been told and the render is queued; a hold taken then is a hold taken too
|
|
27623
|
-
// late. Kept here until whoever animates the change takes it over, or the
|
|
27624
|
-
// navigation turns out to be one nobody animates.
|
|
27625
|
-
let routingRenderingHold = null;
|
|
27626
|
-
const holdRenderingForRouting = () => {
|
|
27627
|
-
routingRenderingHold = holdRendering();
|
|
27628
|
-
};
|
|
27629
|
-
// Nobody had a picture to take: a page held for a change it does not animate
|
|
27630
|
-
// is a page that stutters for nothing.
|
|
27631
|
-
const releaseRoutingRenderingHold = () => {
|
|
27632
|
-
const release = routingRenderingHold;
|
|
27633
|
-
routingRenderingHold = null;
|
|
27634
|
-
if (release) {
|
|
27635
|
-
release();
|
|
27636
|
-
}
|
|
27637
|
-
};
|
|
27638
|
-
// An animator takes the navigation's hold as its own — taking another would be
|
|
27639
|
-
// taking a hold on a page that is holding still — or takes a fresh one when
|
|
27640
|
-
// the change it animates is not a navigation.
|
|
27641
|
-
const takeoverRoutingRenderingHold = () => {
|
|
27642
|
-
const release = routingRenderingHold || holdRendering();
|
|
27643
|
-
routingRenderingHold = null;
|
|
27644
|
-
return release;
|
|
27645
|
-
};
|
|
27646
|
-
|
|
27647
27873
|
/**
|
|
27648
27874
|
* Where a page was left, given back when one comes back to it.
|
|
27649
27875
|
*
|
|
@@ -27844,6 +28070,11 @@ const restoreScrollPosition = (url) => {
|
|
|
27844
28070
|
// So `from`, the url being left, tells an arrival from the same place said
|
|
27845
28071
|
// differently, and only the first is moved.
|
|
27846
28072
|
//
|
|
28073
|
+
// A fragment does not make it less of an arrival. The browser finds nothing to
|
|
28074
|
+
// scroll to after a pushState; url_target.js brings the element in once it
|
|
28075
|
+
// renders, from this top — and when it never comes, the page is read from its
|
|
28076
|
+
// top like any other.
|
|
28077
|
+
//
|
|
27847
28078
|
// The document, because the document is the scrollport in the common case. An
|
|
27848
28079
|
// app that scrolls an element of its own scrolls it itself.
|
|
27849
28080
|
const startAtTop = (url, { from } = {}) => {
|
|
@@ -27853,14 +28084,10 @@ const startAtTop = (url, { from } = {}) => {
|
|
|
27853
28084
|
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
|
|
27854
28085
|
};
|
|
27855
28086
|
const isArrival = (url, { from }) => {
|
|
27856
|
-
const urlObject = new URL(url, window.location.href);
|
|
27857
|
-
// A fragment names where to land, and the browser is the one that finds it.
|
|
27858
|
-
if (urlObject.hash) {
|
|
27859
|
-
return false;
|
|
27860
|
-
}
|
|
27861
28087
|
if (
|
|
27862
28088
|
from !== undefined &&
|
|
27863
|
-
new URL(from, window.location.href).pathname ===
|
|
28089
|
+
new URL(from, window.location.href).pathname ===
|
|
28090
|
+
new URL(url, window.location.href).pathname
|
|
27864
28091
|
) {
|
|
27865
28092
|
return false;
|
|
27866
28093
|
}
|
|
@@ -27937,29 +28164,6 @@ const forgetScrollerUnlessPageLeft = (name) => {
|
|
|
27937
28164
|
}
|
|
27938
28165
|
};
|
|
27939
28166
|
|
|
27940
|
-
/**
|
|
27941
|
-
* A navigation is ABOUT to be applied — said before its very first write.
|
|
27942
|
-
*
|
|
27943
|
-
* Everything else a router says arrives once the change is made: a route
|
|
27944
|
-
* announces that it matches, an action that it is running. That is too late for
|
|
27945
|
-
* anyone who needs the page as it stands BEFORE, and the browser's view
|
|
27946
|
-
* transitions are exactly that kind of reader — the picture they keep of the
|
|
27947
|
-
* page being left is taken at the next frame, and a render answering a signal
|
|
27948
|
-
* written a moment ago is already in the DOM by then (see route_travel.jsx).
|
|
27949
|
-
*
|
|
27950
|
-
* So this is the one moment where nothing has moved yet. It is published
|
|
27951
|
-
* synchronously, from the top of the navigation, and whoever listens runs
|
|
27952
|
-
* before the URL, the visited set, or any route has changed.
|
|
27953
|
-
*
|
|
27954
|
-
* The other end is published too, and for the same kind of reader: whoever
|
|
27955
|
-
* held something across the change and has nobody to hand it to gets a moment
|
|
27956
|
-
* to let go of it that does not depend on guessing how long the change takes.
|
|
27957
|
-
*/
|
|
27958
|
-
|
|
27959
|
-
|
|
27960
|
-
const [publishBeforeRouting, observeBeforeRouting] = createPubSub();
|
|
27961
|
-
const [publishAfterRouting, observeAfterRouting] = createPubSub();
|
|
27962
|
-
|
|
27963
28167
|
const getHrefTargetInfo = (href) => {
|
|
27964
28168
|
href = String(href);
|
|
27965
28169
|
|
|
@@ -28467,6 +28671,11 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28467
28671
|
});
|
|
28468
28672
|
|
|
28469
28673
|
window.addEventListener("popstate", (popstateEvent) => {
|
|
28674
|
+
if (isTargetWriteInProgress()) {
|
|
28675
|
+
// The fragment navigation navi makes to set `:target` (see
|
|
28676
|
+
// url_target.js): the entry keeps its address and its state.
|
|
28677
|
+
return;
|
|
28678
|
+
}
|
|
28470
28679
|
// The entry a pending replace was for is no longer the current one, and
|
|
28471
28680
|
// the History API has no way to write it (see replaceAddressWhenSettled).
|
|
28472
28681
|
dropPendingReplace();
|
|
@@ -28542,9 +28751,14 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
28542
28751
|
// one), so whoever awaits this reads a document url and state that
|
|
28543
28752
|
// already say where it landed.
|
|
28544
28753
|
const landedPromise = new Promise((resolve) => {
|
|
28545
|
-
|
|
28546
|
-
|
|
28547
|
-
|
|
28754
|
+
const onPopstate = () => {
|
|
28755
|
+
if (isTargetWriteInProgress()) {
|
|
28756
|
+
return;
|
|
28757
|
+
}
|
|
28758
|
+
window.removeEventListener("popstate", onPopstate);
|
|
28759
|
+
resolve(true);
|
|
28760
|
+
};
|
|
28761
|
+
window.addEventListener("popstate", onPopstate);
|
|
28548
28762
|
});
|
|
28549
28763
|
landOnPending = landOn || null;
|
|
28550
28764
|
window.history.back();
|
|
@@ -33365,10 +33579,15 @@ const RouteTravel = ({
|
|
|
33365
33579
|
// stopping. Which end decides how it is answered, and the two are not the
|
|
33366
33580
|
// same amount of work.
|
|
33367
33581
|
onEdge: ({
|
|
33368
|
-
sign
|
|
33582
|
+
sign,
|
|
33583
|
+
thrown
|
|
33369
33584
|
}) => {
|
|
33370
33585
|
const travel = travelRef.current;
|
|
33371
|
-
if (
|
|
33586
|
+
if (
|
|
33587
|
+
// Thrown on before its end: handing over from there would mean jumping
|
|
33588
|
+
// the pictures the rest of the way first (see scrubTravel below), a
|
|
33589
|
+
// page seen leaping. The travel caught simply arrives.
|
|
33590
|
+
thrown || !travel || travel.noPicture || travel.ended || travel.reverting || !CAN_KEEP_PICTURE) {
|
|
33372
33591
|
return false;
|
|
33373
33592
|
}
|
|
33374
33593
|
// Where the page in hand is coming from, said the way the gesture says
|
|
@@ -37329,22 +37548,28 @@ const armOutsidePressClose = (
|
|
|
37329
37548
|
* frames, never a timeout: browsers hold timers back while a finger is down,
|
|
37330
37549
|
* and a timeout started by the press fires at its release, on top of the click.
|
|
37331
37550
|
*
|
|
37332
|
-
* "while-opened" content is warmed by
|
|
37333
|
-
*
|
|
37334
|
-
*
|
|
37335
|
-
*
|
|
37336
|
-
* content exists at a time). A pointer crossing four
|
|
37337
|
-
* contents; a press is the start of one opening.
|
|
37338
|
-
*
|
|
37339
|
-
*
|
|
37340
|
-
*
|
|
37341
|
-
*
|
|
37342
|
-
*
|
|
37343
|
-
*
|
|
37344
|
-
*
|
|
37345
|
-
*
|
|
37346
|
-
*
|
|
37347
|
-
*
|
|
37551
|
+
* "while-opened" content is warmed by an opening press, never by a hover or a
|
|
37552
|
+
* focus. That mode promises content built fresh for the gesture that opens
|
|
37553
|
+
* it, and mounted only while that gesture and its opening last: callers lean
|
|
37554
|
+
* on it (several pickers sharing one set of content ids, because only one
|
|
37555
|
+
* content exists at a time). A pointer crossing four triggers would build four
|
|
37556
|
+
* contents; a press is the start of one opening.
|
|
37557
|
+
*
|
|
37558
|
+
* Only what opens the popup can say its press is one, and it says so as the
|
|
37559
|
+
* press starts, with a `navi_open_press` on the popup (see
|
|
37560
|
+
* announceOpeningPress in commands.js): a `--navi-open` button, a picker's
|
|
37561
|
+
* trigger when a press is what opens it, an expandable's UI part. Not the
|
|
37562
|
+
* anchor: it says where the popup is placed, not what opens it — a card
|
|
37563
|
+
* opened by a hold is pressed all day by taps meant for what it holds.
|
|
37564
|
+
*
|
|
37565
|
+
* The press builds the content, and the press ending without an open throws
|
|
37566
|
+
* it away — its click reaching the document with the popup still closed, no
|
|
37567
|
+
* click coming after the release, the browser taking the gesture
|
|
37568
|
+
* (`pointercancel`), or the next press starting, before any of its own
|
|
37569
|
+
* handlers run: one press at a time holds a content. A keyboard opening builds
|
|
37570
|
+
* at open time. A popup with an `onOpen` is never warmed: that callback runs
|
|
37571
|
+
* before the content is built (see open_controller.js), and a content seeding
|
|
37572
|
+
* itself from what it writes must be built after it.
|
|
37348
37573
|
*/
|
|
37349
37574
|
|
|
37350
37575
|
|
|
@@ -37478,21 +37703,21 @@ const usePopupContentMount = (
|
|
|
37478
37703
|
anchorElement.removeEventListener("focusin", warm);
|
|
37479
37704
|
};
|
|
37480
37705
|
}, [contentMounted, anchor, mount]);
|
|
37481
|
-
// Warm on
|
|
37482
|
-
// as long as the
|
|
37483
|
-
// the press that throws a warmed content away can be a new
|
|
37484
|
-
// same
|
|
37706
|
+
// Warm on an opening press, for "while-opened" (see the top comment).
|
|
37707
|
+
// Listening for as long as the popup is there, not only while the content is
|
|
37708
|
+
// unmounted: the press that throws a warmed content away can be a new
|
|
37709
|
+
// opening press of this same popup, and it must warm again.
|
|
37485
37710
|
useEffect(() => {
|
|
37486
|
-
if (
|
|
37711
|
+
if (mount !== "while-opened") {
|
|
37487
37712
|
return undefined;
|
|
37488
37713
|
}
|
|
37489
|
-
const
|
|
37490
|
-
if (!
|
|
37714
|
+
const element = openController.getElement?.();
|
|
37715
|
+
if (!element) {
|
|
37491
37716
|
return undefined;
|
|
37492
37717
|
}
|
|
37493
37718
|
let stopPressWarm = null;
|
|
37494
|
-
const
|
|
37495
|
-
if (
|
|
37719
|
+
const onOpeningPress = () => {
|
|
37720
|
+
if (contentMountedRef.current || openController.onOpen) {
|
|
37496
37721
|
return;
|
|
37497
37722
|
}
|
|
37498
37723
|
const cancelBuild = requestFrameAfterNext(() => {
|
|
@@ -37510,12 +37735,12 @@ const usePopupContentMount = (
|
|
|
37510
37735
|
stopWatching();
|
|
37511
37736
|
};
|
|
37512
37737
|
};
|
|
37513
|
-
|
|
37738
|
+
element.addEventListener("navi_open_press", onOpeningPress);
|
|
37514
37739
|
return () => {
|
|
37515
37740
|
stopPressWarm?.();
|
|
37516
|
-
|
|
37741
|
+
element.removeEventListener("navi_open_press", onOpeningPress);
|
|
37517
37742
|
};
|
|
37518
|
-
}, [
|
|
37743
|
+
}, [mount]);
|
|
37519
37744
|
|
|
37520
37745
|
return contentMounted;
|
|
37521
37746
|
};
|
|
@@ -38641,6 +38866,9 @@ const useUIStateController = (
|
|
|
38641
38866
|
// The suggestion this control started on — what tells a field showing
|
|
38642
38867
|
// its default from one carrying an answer (see isUIStateHeld).
|
|
38643
38868
|
defaultValue: controlInfo.defaultValue,
|
|
38869
|
+
// The default last put on screen, which lags `defaultValue` while an
|
|
38870
|
+
// edit holds a newer one back (see followDefaultValue).
|
|
38871
|
+
defaultValueTaken: controlInfo.defaultValue,
|
|
38644
38872
|
|
|
38645
38873
|
facadeChild: null,
|
|
38646
38874
|
// Set for the duration of one interaction by whatever wants the
|
|
@@ -38742,6 +38970,18 @@ const useUIStateController = (
|
|
|
38742
38970
|
e.currentTarget || controller.ref.current,
|
|
38743
38971
|
);
|
|
38744
38972
|
const currentUIState = controller.uiState;
|
|
38973
|
+
// What a control is STARTED on is accepted, as its own defaultValue
|
|
38974
|
+
// would be: a value placed on it as it registers, or what a picker
|
|
38975
|
+
// takes up from its popup while it holds nothing of its own. A
|
|
38976
|
+
// rollback goes back there, and a control left on it holds no edit.
|
|
38977
|
+
if (!controller.hasStateProp) {
|
|
38978
|
+
if (
|
|
38979
|
+
e.type === "initial_state_push" ||
|
|
38980
|
+
(e.type === "facade_child_mount_sync" && !controller.holdsEdit())
|
|
38981
|
+
) {
|
|
38982
|
+
controller.state = newUIState;
|
|
38983
|
+
}
|
|
38984
|
+
}
|
|
38745
38985
|
const stateIsTheSame = compareTwoJsValues(newUIState, currentUIState);
|
|
38746
38986
|
if (stateIsTheSame) {
|
|
38747
38987
|
if (controlType === "button" || controlType === "link") {
|
|
@@ -38907,7 +39147,12 @@ const useUIStateController = (
|
|
|
38907
39147
|
}
|
|
38908
39148
|
// initial_state_push is pure initialization (equivalent to defaultValue on the
|
|
38909
39149
|
// child itself): skip uiAction entirely so no side effects fire on mount.
|
|
38910
|
-
|
|
39150
|
+
// A value the outside moved since (see outside_value_follow) is the
|
|
39151
|
+
// same initialization, later.
|
|
39152
|
+
if (
|
|
39153
|
+
e.type !== "initial_state_push" &&
|
|
39154
|
+
e.type !== "outside_value_follow"
|
|
39155
|
+
) {
|
|
38911
39156
|
// Still fire uiAction so external listeners (e.g. signals) stay in
|
|
38912
39157
|
// sync, but do NOT fire the command and do NOT notify the parent —
|
|
38913
39158
|
// both would cause an infinite loop when a parent cascades state
|
|
@@ -38961,6 +39206,19 @@ const useUIStateController = (
|
|
|
38961
39206
|
stateChanged: true,
|
|
38962
39207
|
});
|
|
38963
39208
|
}
|
|
39209
|
+
if (
|
|
39210
|
+
e.type === "outside_value_follow" ||
|
|
39211
|
+
(e.type === "facade_child_mount_sync" && isOutsideValueFollow(e))
|
|
39212
|
+
) {
|
|
39213
|
+
// Heard the way a control mounting on that value would be:
|
|
39214
|
+
// silently, nobody acted. But heard, all the way up — a picker
|
|
39215
|
+
// whose popup followed tells its own group in turn — or what
|
|
39216
|
+
// the groups send is not what is on screen.
|
|
39217
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
39218
|
+
stateChanged: true,
|
|
39219
|
+
silent: true,
|
|
39220
|
+
});
|
|
39221
|
+
}
|
|
38964
39222
|
return true;
|
|
38965
39223
|
}
|
|
38966
39224
|
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
@@ -39056,6 +39314,41 @@ const useUIStateController = (
|
|
|
39056
39314
|
// pick — the signal would hand it straight back on the next render,
|
|
39057
39315
|
// and the refused value would win over the rollback.
|
|
39058
39316
|
writeBoundSignal(controller.state);
|
|
39317
|
+
controller.followDefaultValue();
|
|
39318
|
+
},
|
|
39319
|
+
// Something typed, picked or toggled since the value the outside last
|
|
39320
|
+
// gave or accepted.
|
|
39321
|
+
holdsEdit: () => {
|
|
39322
|
+
return !compareTwoJsValues(controller.uiState, controller.state);
|
|
39323
|
+
},
|
|
39324
|
+
// An uncontrolled control starts on its defaultValue, and a new one is
|
|
39325
|
+
// what it would start on if it were mounted again — a record saved
|
|
39326
|
+
// and handed back normalized, refreshed from elsewhere. It takes it
|
|
39327
|
+
// while it holds no edit. An edit is never undone; the default waits
|
|
39328
|
+
// for it to be accepted or rolled back.
|
|
39329
|
+
followDefaultValue: () => {
|
|
39330
|
+
// A bound signal carries its own default, followed through
|
|
39331
|
+
// stateFromSignal; a proxy shows the control it stands for.
|
|
39332
|
+
if (
|
|
39333
|
+
controller.hasStateProp ||
|
|
39334
|
+
controller.isProxy ||
|
|
39335
|
+
controller.props.signal
|
|
39336
|
+
) {
|
|
39337
|
+
return;
|
|
39338
|
+
}
|
|
39339
|
+
const { defaultValue } = controller;
|
|
39340
|
+
if (compareTwoJsValues(defaultValue, controller.defaultValueTaken)) {
|
|
39341
|
+
return;
|
|
39342
|
+
}
|
|
39343
|
+
if (controller.holdsEdit()) {
|
|
39344
|
+
return;
|
|
39345
|
+
}
|
|
39346
|
+
controller.defaultValueTaken = defaultValue;
|
|
39347
|
+
controller.state = defaultValue;
|
|
39348
|
+
controller.setUIState(
|
|
39349
|
+
defaultValue,
|
|
39350
|
+
new CustomEvent("outside_value_follow", { detail: {} }),
|
|
39351
|
+
);
|
|
39059
39352
|
},
|
|
39060
39353
|
// Read by the callout manager when it has nowhere else to point.
|
|
39061
39354
|
getCalloutAnchorElement: (event) =>
|
|
@@ -39069,7 +39362,12 @@ const useUIStateController = (
|
|
|
39069
39362
|
if (controller.hasStateProp || controller.queuedActionAllowedEvent) {
|
|
39070
39363
|
return;
|
|
39071
39364
|
}
|
|
39365
|
+
// A picker told yes is told for what its popup shows too, the way a
|
|
39366
|
+
// group tells its children. First, so that a default one of them
|
|
39367
|
+
// takes on the way is part of what this control accepts below.
|
|
39368
|
+
controller.facadeChild?.acknowledgeUIState();
|
|
39072
39369
|
controller.state = controller.uiState;
|
|
39370
|
+
controller.followDefaultValue();
|
|
39073
39371
|
},
|
|
39074
39372
|
onActionEnd: (e) => {
|
|
39075
39373
|
acknowledgeOwnAction(controller);
|
|
@@ -39224,6 +39522,7 @@ const useUIStateController = (
|
|
|
39224
39522
|
}
|
|
39225
39523
|
}
|
|
39226
39524
|
}
|
|
39525
|
+
controller.followDefaultValue();
|
|
39227
39526
|
}
|
|
39228
39527
|
return liveValues();
|
|
39229
39528
|
},
|
|
@@ -39804,7 +40103,12 @@ const useUIGroupStateController = (
|
|
|
39804
40103
|
// deferred alongside a mount sync stays a real change.
|
|
39805
40104
|
const pendingChange = pendingChangeRef.current;
|
|
39806
40105
|
pendingChangeRef.current = {
|
|
39807
|
-
|
|
40106
|
+
// A default followed is not replaced by a mount deferred beside
|
|
40107
|
+
// it: the event is what lets the sync below read the children.
|
|
40108
|
+
e:
|
|
40109
|
+
pendingChange && isOutsideValueFollow(pendingChange.e)
|
|
40110
|
+
? pendingChange.e
|
|
40111
|
+
: e,
|
|
39808
40112
|
notifyExternal:
|
|
39809
40113
|
pendingChange?.notifyExternal === true ? true : notifyExternal,
|
|
39810
40114
|
actingChild,
|
|
@@ -39812,6 +40116,13 @@ const useUIGroupStateController = (
|
|
|
39812
40116
|
return;
|
|
39813
40117
|
}
|
|
39814
40118
|
const { controller } = s;
|
|
40119
|
+
if (isOutsideValueFollow(e)) {
|
|
40120
|
+
// Not a partial reading: the child was there already and its value
|
|
40121
|
+
// moved, told by the outside. The group is worth what its children
|
|
40122
|
+
// show, whatever it was handed before — kept, the value it sends
|
|
40123
|
+
// would be one the screen no longer shows.
|
|
40124
|
+
controller.stateGivenFromAbove = false;
|
|
40125
|
+
}
|
|
39815
40126
|
// A child mounting or unmounting is not somebody answering: while the
|
|
39816
40127
|
// children of a group are still arriving, their aggregate is a partial
|
|
39817
40128
|
// reading, and taking it for the truth is how the value the group was
|
|
@@ -39990,6 +40301,9 @@ const useUIGroupStateController = (
|
|
|
39990
40301
|
name,
|
|
39991
40302
|
value,
|
|
39992
40303
|
defaultValue,
|
|
40304
|
+
// Same as the leaf's (see useUIStateController): the default last
|
|
40305
|
+
// placed on the children, behind `defaultValue` while an edit waits.
|
|
40306
|
+
defaultValueTaken: defaultValue,
|
|
39993
40307
|
hasValueProp,
|
|
39994
40308
|
hasDefaultValueProp,
|
|
39995
40309
|
props,
|
|
@@ -40270,6 +40584,7 @@ const useUIGroupStateController = (
|
|
|
40270
40584
|
}
|
|
40271
40585
|
}
|
|
40272
40586
|
onChange(e, { notifyExternal: true });
|
|
40587
|
+
controller.followDefaultValue();
|
|
40273
40588
|
},
|
|
40274
40589
|
clearUIState: (e) => {
|
|
40275
40590
|
const ev = new CustomEvent("propagate_down_clear_ui_state", {
|
|
@@ -40298,6 +40613,46 @@ const useUIGroupStateController = (
|
|
|
40298
40613
|
c.acknowledgeUIState();
|
|
40299
40614
|
}
|
|
40300
40615
|
}
|
|
40616
|
+
controller.followDefaultValue();
|
|
40617
|
+
},
|
|
40618
|
+
// A group holds no value of its own to have edited: it holds an edit
|
|
40619
|
+
// when one of its children does.
|
|
40620
|
+
holdsEdit: () => {
|
|
40621
|
+
return childUIStateControllerArray.some(
|
|
40622
|
+
(c) => shouldPropagateStateToChild(c) && c.holdsEdit(),
|
|
40623
|
+
);
|
|
40624
|
+
},
|
|
40625
|
+
// The leaf's rule (see useUIStateController), for a group given a
|
|
40626
|
+
// plain `defaultValue`: a new one is placed on the children the way
|
|
40627
|
+
// the first one was as they registered, while none of them holds an
|
|
40628
|
+
// edit. A signal's own default is followed by the update below.
|
|
40629
|
+
followDefaultValue: () => {
|
|
40630
|
+
if (controller.hasValueProp || controller.props.signal) {
|
|
40631
|
+
return;
|
|
40632
|
+
}
|
|
40633
|
+
const { defaultValue } = controller;
|
|
40634
|
+
if (compareTwoJsValues(defaultValue, controller.defaultValueTaken)) {
|
|
40635
|
+
return;
|
|
40636
|
+
}
|
|
40637
|
+
if (controller.holdsEdit()) {
|
|
40638
|
+
return;
|
|
40639
|
+
}
|
|
40640
|
+
controller.defaultValueTaken = defaultValue;
|
|
40641
|
+
const followEvent = new CustomEvent("outside_value_follow", {
|
|
40642
|
+
detail: {},
|
|
40643
|
+
});
|
|
40644
|
+
const placeEvent = new CustomEvent("initial_state_push", {
|
|
40645
|
+
detail: {},
|
|
40646
|
+
});
|
|
40647
|
+
chainEvent(placeEvent, followEvent);
|
|
40648
|
+
controller.setUIState(
|
|
40649
|
+
defaultValue === undefined ? fallbackState : defaultValue,
|
|
40650
|
+
placeEvent,
|
|
40651
|
+
);
|
|
40652
|
+
s.parentUIStateController?.onChildUIAction(controller, followEvent, {
|
|
40653
|
+
stateChanged: true,
|
|
40654
|
+
silent: true,
|
|
40655
|
+
});
|
|
40301
40656
|
},
|
|
40302
40657
|
onActionEnd: (e) => {
|
|
40303
40658
|
acknowledgeOwnAction(controller);
|
|
@@ -40420,6 +40775,7 @@ const useUIGroupStateController = (
|
|
|
40420
40775
|
// former value back over it on the next child interaction.
|
|
40421
40776
|
placeChildrenFrom(defaultValue);
|
|
40422
40777
|
}
|
|
40778
|
+
controller.followDefaultValue();
|
|
40423
40779
|
|
|
40424
40780
|
return liveValues();
|
|
40425
40781
|
},
|
|
@@ -40644,7 +41000,11 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
40644
41000
|
if (child !== firstChildControllerRef.current) {
|
|
40645
41001
|
return;
|
|
40646
41002
|
}
|
|
40647
|
-
if (
|
|
41003
|
+
if (
|
|
41004
|
+
silent &&
|
|
41005
|
+
!isOutsideValueFollow(e) &&
|
|
41006
|
+
uiStateHoldsNothing(child.uiState)
|
|
41007
|
+
) {
|
|
40648
41008
|
// A silent sync means the child's own structure changed (children
|
|
40649
41009
|
// mounted/unmounted), not that the user acted. A child that ends up
|
|
40650
41010
|
// with no value there is one that currently *cannot* express one —
|
|
@@ -40655,7 +41015,8 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
40655
41015
|
// still nothing to adopt: the sync would only respell one nothing
|
|
40656
41016
|
// as another (an array picker's [] becoming undefined, say) and
|
|
40657
41017
|
// hand that to uiAction — an empty array picker opened its popup
|
|
40658
|
-
// and told its owner the value changed.
|
|
41018
|
+
// and told its owner the value changed. A default followed down to
|
|
41019
|
+
// nothing is not that: the outside emptied a value the child held.
|
|
40659
41020
|
return;
|
|
40660
41021
|
}
|
|
40661
41022
|
updatingRef.current = true;
|
|
@@ -40719,6 +41080,69 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
40719
41080
|
return scope.controller;
|
|
40720
41081
|
};
|
|
40721
41082
|
|
|
41083
|
+
/**
|
|
41084
|
+
* What every control of a picker — the picker and the controls in its popup —
|
|
41085
|
+
* has been accepted on as the popup opens. Read back by followOutsideSinceOpen
|
|
41086
|
+
* when the popup is cancelled.
|
|
41087
|
+
*/
|
|
41088
|
+
const readStatesAtOpen = (pickerController) => {
|
|
41089
|
+
const statesAtOpen = new Map();
|
|
41090
|
+
visitControllers(pickerController, (controller) => {
|
|
41091
|
+
if (Object.hasOwn(controller, "state")) {
|
|
41092
|
+
statesAtOpen.set(controller, controller.state);
|
|
41093
|
+
}
|
|
41094
|
+
});
|
|
41095
|
+
return statesAtOpen;
|
|
41096
|
+
};
|
|
41097
|
+
|
|
41098
|
+
/**
|
|
41099
|
+
* A cancel puts back the value the picker held at open, which takes back
|
|
41100
|
+
* everything that moved since — and not only what the user moved: a record
|
|
41101
|
+
* refreshed while the popup was open, a default that arrived under a field
|
|
41102
|
+
* holding an edit. Those were not the user's to take back. Once the value at
|
|
41103
|
+
* open is back, each control the outside moved since shows what the outside
|
|
41104
|
+
* holds again, and a default left waiting on an edit is taken there.
|
|
41105
|
+
*
|
|
41106
|
+
* Children before the control around them: a picker given its value by its
|
|
41107
|
+
* owner has the last word over the controls in its popup.
|
|
41108
|
+
*/
|
|
41109
|
+
const followOutsideSinceOpen = (pickerController, statesAtOpen) => {
|
|
41110
|
+
visitControllers(
|
|
41111
|
+
pickerController,
|
|
41112
|
+
(controller) => {
|
|
41113
|
+
if (
|
|
41114
|
+
statesAtOpen.has(controller) &&
|
|
41115
|
+
!compareTwoJsValues(controller.state, statesAtOpen.get(controller)) &&
|
|
41116
|
+
controller.holdsEdit()
|
|
41117
|
+
) {
|
|
41118
|
+
controller.setUIState(
|
|
41119
|
+
controller.state,
|
|
41120
|
+
new CustomEvent("outside_value_follow", { detail: {} }),
|
|
41121
|
+
);
|
|
41122
|
+
}
|
|
41123
|
+
controller.followDefaultValue();
|
|
41124
|
+
},
|
|
41125
|
+
{ childrenFirst: true },
|
|
41126
|
+
);
|
|
41127
|
+
};
|
|
41128
|
+
|
|
41129
|
+
// A picker reaches the controls of its popup through its facade child, a group
|
|
41130
|
+
// through the children it registered.
|
|
41131
|
+
const visitControllers = (controller, visit, { childrenFirst } = {}) => {
|
|
41132
|
+
if (!childrenFirst) {
|
|
41133
|
+
visit(controller);
|
|
41134
|
+
}
|
|
41135
|
+
const children = controller.facadeChild
|
|
41136
|
+
? [controller.facadeChild]
|
|
41137
|
+
: controller.getChildControllers?.() || [];
|
|
41138
|
+
for (const child of children) {
|
|
41139
|
+
visitControllers(child, visit, { childrenFirst });
|
|
41140
|
+
}
|
|
41141
|
+
if (childrenFirst) {
|
|
41142
|
+
visit(controller);
|
|
41143
|
+
}
|
|
41144
|
+
};
|
|
41145
|
+
|
|
40722
41146
|
const describePicker = (props) =>
|
|
40723
41147
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
40724
41148
|
|
|
@@ -40783,10 +41207,18 @@ const INTERNAL_EVENT_SET = new Set([
|
|
|
40783
41207
|
// no action of the control's own — but what it holds really did move, so
|
|
40784
41208
|
// uiAction, the bound signal and the parent notification below all happen.
|
|
40785
41209
|
"auto_fix",
|
|
41210
|
+
// A control put on a value the outside moved while nobody was editing it: a
|
|
41211
|
+
// new defaultValue (see followDefaultValue), or what a cancel puts back
|
|
41212
|
+
// (see followOutsideSinceOpen). Initialization arriving after mount, so
|
|
41213
|
+
// nothing fires, and the groups above hear it silently.
|
|
41214
|
+
"outside_value_follow",
|
|
40786
41215
|
]);
|
|
40787
41216
|
const isInternalEvent = (e) => {
|
|
40788
41217
|
return INTERNAL_EVENT_SET.has(e.type);
|
|
40789
41218
|
};
|
|
41219
|
+
const isOutsideValueFollow = (e) => {
|
|
41220
|
+
return Boolean(findEvent(e, "outside_value_follow"));
|
|
41221
|
+
};
|
|
40790
41222
|
|
|
40791
41223
|
/**
|
|
40792
41224
|
* A value handed DOWN to a control by whoever owns it: a picker filling its
|
|
@@ -46240,7 +46672,11 @@ const usePreloadOpeningOnPress = (ref, command, prefetch = true) => {
|
|
|
46240
46672
|
if (!element) {
|
|
46241
46673
|
return undefined;
|
|
46242
46674
|
}
|
|
46243
|
-
const onPointerDown = () => {
|
|
46675
|
+
const onPointerDown = (pointerdownEvent) => {
|
|
46676
|
+
// A right click opens a menu, never the popup.
|
|
46677
|
+
if (pointerdownEvent.button !== 0) {
|
|
46678
|
+
return;
|
|
46679
|
+
}
|
|
46244
46680
|
announceOpeningPress(element, command);
|
|
46245
46681
|
};
|
|
46246
46682
|
element.addEventListener("pointerdown", onPointerDown);
|
|
@@ -50330,8 +50766,14 @@ const Expandable = props => {
|
|
|
50330
50766
|
// before the click ever fires — so what held the focus has to be remembered
|
|
50331
50767
|
// at pointerdown time.
|
|
50332
50768
|
const focusedAtPointerDownRef = useRef(null);
|
|
50333
|
-
const onUIPointerDown =
|
|
50769
|
+
const onUIPointerDown = pointerdownEvent => {
|
|
50334
50770
|
focusedAtPointerDownRef.current = document.activeElement;
|
|
50771
|
+
// The start of an expansion, announced as a --navi-open button announces
|
|
50772
|
+
// its own (see announceOpeningPress in commands.js) — unless aimed at a
|
|
50773
|
+
// control inside the UI part, whose click does not toggle (see onUIClick).
|
|
50774
|
+
if (pointerdownEvent.button === 0 && !openController.opened && !isAimedAtControlInsideUI(pointerdownEvent.target)) {
|
|
50775
|
+
dispatchCustomEvent(rootRef.current, "navi_open_press");
|
|
50776
|
+
}
|
|
50335
50777
|
};
|
|
50336
50778
|
|
|
50337
50779
|
// The content keeps its final size while the track animates (see the top
|
|
@@ -50629,17 +51071,18 @@ const Expandable = props => {
|
|
|
50629
51071
|
if (clickEvent.defaultPrevented) {
|
|
50630
51072
|
return;
|
|
50631
51073
|
}
|
|
50632
|
-
|
|
50633
|
-
|
|
50634
|
-
} = clickEvent;
|
|
50635
|
-
if (target.nodeType === 1) {
|
|
50636
|
-
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
50637
|
-
if (interactiveElement && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement)) {
|
|
50638
|
-
return;
|
|
50639
|
-
}
|
|
51074
|
+
if (isAimedAtControlInsideUI(clickEvent.target)) {
|
|
51075
|
+
return;
|
|
50640
51076
|
}
|
|
50641
51077
|
toggle(clickEvent);
|
|
50642
51078
|
};
|
|
51079
|
+
const isAimedAtControlInsideUI = target => {
|
|
51080
|
+
if (target.nodeType !== 1) {
|
|
51081
|
+
return false;
|
|
51082
|
+
}
|
|
51083
|
+
const interactiveElement = target.closest(UI_INTERACTIVE_SELECTOR);
|
|
51084
|
+
return Boolean(interactiveElement) && interactiveElement !== uiRef.current && uiRef.current.contains(interactiveElement);
|
|
51085
|
+
};
|
|
50643
51086
|
|
|
50644
51087
|
// Space/Enter on the UI part itself (role button) — a key pressed on a
|
|
50645
51088
|
// control inside it belongs to that control.
|
|
@@ -58941,6 +59384,52 @@ const SlideContainer = ({
|
|
|
58941
59384
|
drag.areaPulled = pulled > 0 ? drag.areaBack : drag.areaOn;
|
|
58942
59385
|
paintDrag();
|
|
58943
59386
|
},
|
|
59387
|
+
// The slide being brought in walked whole and the hand still going, or a
|
|
59388
|
+
// travel caught on its way thrown on: either way the hand asks for the
|
|
59389
|
+
// slide past it, and the gesture moves on to it. Nothing on screen moves
|
|
59390
|
+
// for that — the slide reached becomes the one dragged, the one the hand
|
|
59391
|
+
// came from stays next to it for a hand that turns around, and the slide
|
|
59392
|
+
// past it is put on the other side.
|
|
59393
|
+
onEdge: ({
|
|
59394
|
+
axis,
|
|
59395
|
+
sign
|
|
59396
|
+
}) => {
|
|
59397
|
+
const reached = sign > 0 ? drag.areaBack : drag.areaOn;
|
|
59398
|
+
const {
|
|
59399
|
+
slideElements,
|
|
59400
|
+
placeOf
|
|
59401
|
+
} = readMap();
|
|
59402
|
+
const reachedElement = reached && slideElements.find(slideElement => readArea(slideElement) === reached);
|
|
59403
|
+
if (!reachedElement) {
|
|
59404
|
+
return false;
|
|
59405
|
+
}
|
|
59406
|
+
// The gate goToArea reads at the release, asked of the slide the hand
|
|
59407
|
+
// is now in: a slide holding on to the user is not walked through.
|
|
59408
|
+
const holds = reachedElement.hasAttribute(sign > 0 ? "data-prevent-nav-previous" : "data-prevent-nav-next");
|
|
59409
|
+
const past = holds ? undefined : axis === "x" ? areaTowards(-sign, 0, reached) : areaTowards(0, -sign, reached);
|
|
59410
|
+
if (!past) {
|
|
59411
|
+
return false;
|
|
59412
|
+
}
|
|
59413
|
+
const currentElement = slideElements.find(slideElement => slideElement.hasAttribute("data-current")) || slideElements[0];
|
|
59414
|
+
const basePlace = stageRef.current?.placeByArea.get(reached) || placeOf.get(reached) || {
|
|
59415
|
+
x: 0,
|
|
59416
|
+
y: 0
|
|
59417
|
+
};
|
|
59418
|
+
drag.areaBack = sign > 0 ? past : drag.area;
|
|
59419
|
+
drag.areaOn = sign > 0 ? drag.area : past;
|
|
59420
|
+
drag.area = reached;
|
|
59421
|
+
drag.basePlace = basePlace;
|
|
59422
|
+
drag.baseOffset = {
|
|
59423
|
+
x: -basePlace.x * drag.box.width,
|
|
59424
|
+
y: -basePlace.y * drag.box.height
|
|
59425
|
+
};
|
|
59426
|
+
stageDrag(drag, readArea(currentElement));
|
|
59427
|
+
return {
|
|
59428
|
+
size: axis === "x" ? drag.box.width : drag.box.height,
|
|
59429
|
+
travelBack: Boolean(drag.areaBack),
|
|
59430
|
+
travelOn: Boolean(drag.areaOn)
|
|
59431
|
+
};
|
|
59432
|
+
},
|
|
58944
59433
|
onEnd: ({
|
|
58945
59434
|
axis,
|
|
58946
59435
|
sign,
|
|
@@ -62831,8 +63320,9 @@ const css$E = /* css */`
|
|
|
62831
63320
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
62832
63321
|
* changed while the popup was closed. Intent on the anchor (pointer
|
|
62833
63322
|
* entering it, focus landing in it) builds the content ahead of the click;
|
|
62834
|
-
* under `"while-opened"`, only a press on
|
|
62835
|
-
* open
|
|
63323
|
+
* under `"while-opened"`, only a press on what opens the popup does (a
|
|
63324
|
+
* `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
|
|
63325
|
+
* press that does not open the popup throws it away.
|
|
62836
63326
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
62837
63327
|
*/
|
|
62838
63328
|
const Dialog = props => {
|
|
@@ -64512,8 +65002,9 @@ const css$D = /* css */`
|
|
|
64512
65002
|
* initial state: an uncontrolled field seeded from a `defaultValue` that
|
|
64513
65003
|
* changed while the popup was closed. Intent on the anchor (pointer
|
|
64514
65004
|
* entering it, focus landing in it) builds the content ahead of the click;
|
|
64515
|
-
* under `"while-opened"`, only a press on
|
|
64516
|
-
* open
|
|
65005
|
+
* under `"while-opened"`, only a press on what opens the popup does (a
|
|
65006
|
+
* `--navi-open` button, a picker's trigger), never with an `onOpen`, and a
|
|
65007
|
+
* press that does not open the popup throws it away.
|
|
64517
65008
|
* @param {import("ignore:preact").ComponentChildren} props.children
|
|
64518
65009
|
*/
|
|
64519
65010
|
const Popover = props => {
|
|
@@ -66176,7 +66667,10 @@ const PickerCustom = props => {
|
|
|
66176
66667
|
// a defaultValue holds nothing, so closing on it untouched IS the answer
|
|
66177
66668
|
// ("yes, 2h15") — the same rule Form applies to an untouched field (see
|
|
66178
66669
|
// isUIStateHeld). Read at open, before anything inside can change it.
|
|
66179
|
-
const
|
|
66670
|
+
const pickerController = getPickerInput(ref.current)?.__uiStateController__;
|
|
66671
|
+
const heldAtOpen = isUIStateHeld(pickerController);
|
|
66672
|
+
// What a cancel must not take back: see followOutsideSinceOpen.
|
|
66673
|
+
const statesAtOpen = pickerController ? readStatesAtOpen(pickerController) : null;
|
|
66180
66674
|
onOpen?.(openEvent);
|
|
66181
66675
|
return {
|
|
66182
66676
|
onRequestClose: requestCloseEvent => {
|
|
@@ -66244,6 +66738,9 @@ const PickerCustom = props => {
|
|
|
66244
66738
|
});
|
|
66245
66739
|
chainEvent(rollbackEvent, closeEvent);
|
|
66246
66740
|
inputEl.__uiStateController__.setUIState(valueAtOpen, rollbackEvent);
|
|
66741
|
+
if (statesAtOpen) {
|
|
66742
|
+
followOutsideSinceOpen(inputEl.__uiStateController__, statesAtOpen);
|
|
66743
|
+
}
|
|
66247
66744
|
} else if (!heldAtOpen) {
|
|
66248
66745
|
// Confirmed a suggestion: nothing changed, so nothing has told the
|
|
66249
66746
|
// control's own bound signal / uiAction that this is now the
|
|
@@ -66324,6 +66821,11 @@ const PickerCustom = props => {
|
|
|
66324
66821
|
"onnavi_request_open": e => {
|
|
66325
66822
|
dispatchCustomEvent(popupRef.current, "navi_request_open", e.detail);
|
|
66326
66823
|
},
|
|
66824
|
+
// And the press announcing that request (see announceOpeningPress in
|
|
66825
|
+
// commands.js), for the popup's content to be built on it.
|
|
66826
|
+
"onnavi_open_press": () => {
|
|
66827
|
+
dispatchCustomEvent(popupRef.current, "navi_open_press");
|
|
66828
|
+
},
|
|
66327
66829
|
"onnavi_request_close": e => {
|
|
66328
66830
|
const closing = dispatchCustomEvent(popupRef.current, "navi_request_close", e.detail);
|
|
66329
66831
|
if (!closing) {
|
|
@@ -66518,6 +67020,25 @@ const PickerCustom = props => {
|
|
|
66518
67020
|
// below), and the keyboard keeps its own ways in (the shortcuts above).
|
|
66519
67021
|
const openOnList = Array.isArray(openOn) ? openOn : [openOn];
|
|
66520
67022
|
const opensOnPress = openOnList.includes("press");
|
|
67023
|
+
// A press on the trigger of a picker a press opens is announced as it
|
|
67024
|
+
// starts, as a --navi-open button announces its own (see
|
|
67025
|
+
// announceOpeningPress in commands.js). Only a press that can open it: a
|
|
67026
|
+
// picker opened by a hold says nothing on a tap, nor does one refusing
|
|
67027
|
+
// to open (read-only with openWhileReadOnly={false}, busy). The
|
|
67028
|
+
// interactivity is asked alone, not the whole gate: a drag source
|
|
67029
|
+
// cancels the pointerdown, and the click that opens still comes.
|
|
67030
|
+
if (opensOnPress) {
|
|
67031
|
+
pickerProps.onPointerDown = pointerdownEvent => {
|
|
67032
|
+
props.onPointerDown?.(pointerdownEvent);
|
|
67033
|
+
const pickerEl = ref.current;
|
|
67034
|
+
if (pointerdownEvent.button !== 0 || openController.opened || isWithinPickerContent(pointerdownEvent.target) || !allowsInteraction(pickerEl, {
|
|
67035
|
+
intent: "read"
|
|
67036
|
+
})) {
|
|
67037
|
+
return;
|
|
67038
|
+
}
|
|
67039
|
+
dispatchCustomEvent(pickerEl, "navi_open_press");
|
|
67040
|
+
};
|
|
67041
|
+
}
|
|
66521
67042
|
let interactions = props.interactions;
|
|
66522
67043
|
if (!opensOnPress) {
|
|
66523
67044
|
interactions = {
|