@jsenv/navi 0.30.1 → 0.30.3

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