@jsenv/navi 0.29.390 → 0.29.392

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.
@@ -29331,9 +29331,13 @@ installImportMetaCssBuild(import.meta);
29331
29331
  * - a bar only one state has never meets a counterpart. It belongs to the page
29332
29332
  * that has it, so it travels with that page — leaving by the keyframes the
29333
29333
  * page being left leaves by, arriving by the ones the page arriving arrives
29334
- * by — instead of going out with the render. Under the pages, so a page
29335
- * coming over it covers it — a popup over them instead, standing where it
29336
- * stands in the document (see the z-order in route_transition.jsx).
29334
+ * by — instead of going out with the render. Over that page, as it is at
29335
+ * rest: the page runs under it, and what it paints outside its box (a button
29336
+ * standing up out of a tab bar) is not cut by its own page. Under the pages
29337
+ * only when its page is covered by the other one — a cover-* movement — so
29338
+ * the sheet comes over it; a popup over everything, standing where it
29339
+ * stands in the document (see the z-order in route_transition.jsx). Which
29340
+ * side a bar is on is said by a class on its group, written here.
29337
29341
  *
29338
29342
  * A popup shown in the top layer is furniture of the same kind, and its case
29339
29343
  * is the sharper one: it is a DOM descendant of the area, yet painted outside
@@ -29411,11 +29415,17 @@ const FURNITURE_NAME_PREFIX = "navi-transition-furniture-";
29411
29415
  const TOP_LAYER_WALL_SELECTOR =
29412
29416
  ".navi_dialog:modal, .navi_popover_backdrop:popover-open";
29413
29417
  const WALL_ATTRIBUTE = "data-navi-transition-wall";
29414
- // Worn by a bar both states have, from the hold on: its two pictures are one
29415
- // group, and the group is ordered over the pages (route_transition.jsx). Only
29416
- // a bar — a popup's class is written by layout/popup_css.js, and a second rule
29417
- // on the same property would replace it.
29418
- const SHARED_ATTRIBUTE = "data-navi-transition-furniture-shared";
29418
+ // Which side of the movement a bar is on, worn by the bar and read by the
29419
+ // browser as the class of its group (the CSS below): "leaving" from the first
29420
+ // picture, "arriving" for a bar the state arriving brought, "shared" for one
29421
+ // that stands in both states, written over "leaving" at the hold. The group's
29422
+ // class is the one captured with the LAST state the element stands in, so a
29423
+ // bar only the state being left has keeps "leaving" from the first capture,
29424
+ // and a shared one takes "shared" from the second. The z-order in
29425
+ // route_transition.jsx is written on these three. Only a bar — a popup's
29426
+ // class is written by layout/popup_css.js, and a second rule on the same
29427
+ // property would replace it.
29428
+ const FURNITURE_ATTRIBUTE = "data-navi-transition-furniture";
29419
29429
  // What the wall paints, resolved on the matched element in both cases (the
29420
29430
  // dialog for its ::backdrop, the popover's wall for itself): the same two
29421
29431
  // properties, copied onto the stand-in.
@@ -29424,7 +29434,15 @@ const WALL_PROPERTIES = ["--backdrop-background", "--backdrop-filter"];
29424
29434
  // The pictures the wall is painted into: the pages', and every bar's own.
29425
29435
  const FIXED_BAR_SELECTOR$1 = ".navi_fixed_bar";
29426
29436
 
29427
- const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition-furniture-shared] {
29437
+ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition-furniture="leaving"] {
29438
+ view-transition-class: navi_furniture_leaving;
29439
+ }
29440
+
29441
+ .navi_fixed_bar[data-navi-transition-furniture="arriving"] {
29442
+ view-transition-class: navi_furniture_arriving;
29443
+ }
29444
+
29445
+ .navi_fixed_bar[data-navi-transition-furniture="shared"] {
29428
29446
  view-transition-class: navi_furniture_shared;
29429
29447
  }
29430
29448
 
@@ -29497,7 +29515,7 @@ const nameTransitionFurniture = (owner, areaElement) => {
29497
29515
  furnitureOwner = owner;
29498
29516
  namedElements = new Set();
29499
29517
  }
29500
- nameFurnitureAround(areaElement);
29518
+ nameFurnitureAround(areaElement, "leaving");
29501
29519
  const sources = paintTransitionWalls(areaElement);
29502
29520
  // The state being left has walls: what no picture covers starts dimmed.
29503
29521
  // The frame the first picture is taken on shows the live document, so the
@@ -29524,10 +29542,10 @@ const holdTransitionFurniture = (owner, areaElement) => {
29524
29542
  if (!element.isConnected) {
29525
29543
  namesLeaving.push(nameByElement.get(element));
29526
29544
  } else if (element.matches(FIXED_BAR_SELECTOR$1)) {
29527
- element.setAttribute(SHARED_ATTRIBUTE, "");
29545
+ element.setAttribute(FURNITURE_ATTRIBUTE, "shared");
29528
29546
  }
29529
29547
  }
29530
- const namesArriving = nameFurnitureAround(areaElement);
29548
+ const namesArriving = nameFurnitureAround(areaElement, "arriving");
29531
29549
  const sources = paintTransitionWalls(areaElement);
29532
29550
  // The state arriving has walls: what no picture covers ends dimmed. Painted
29533
29551
  // from the state being left when that one had walls (they stay up if both
@@ -29577,7 +29595,8 @@ const holdTransitionFurniture = (owner, areaElement) => {
29577
29595
 
29578
29596
  // Returns the elements it had to name, which are the ones the state arriving
29579
29597
  // brought: everything else was already wearing its name from the first pass.
29580
- const nameFurnitureAround = (areaElement) => {
29598
+ // `side` is what a bar named here is told it is on (see FURNITURE_ATTRIBUTE).
29599
+ const nameFurnitureAround = (areaElement, side) => {
29581
29600
  const namesAdded = [];
29582
29601
  for (const element of document.querySelectorAll(FURNITURE_SELECTOR)) {
29583
29602
  if (namedElements.has(element)) {
@@ -29603,6 +29622,9 @@ const nameFurnitureAround = (areaElement) => {
29603
29622
  nameByElement.set(element, name);
29604
29623
  }
29605
29624
  element.style.setProperty(NAME_PROPERTY$1, name);
29625
+ if (element.matches(FIXED_BAR_SELECTOR$1)) {
29626
+ element.setAttribute(FURNITURE_ATTRIBUTE, side);
29627
+ }
29606
29628
  namedElements.add(element);
29607
29629
  namesAdded.push(name);
29608
29630
  }
@@ -29652,7 +29674,7 @@ const releaseTransitionFurniture = (owner) => {
29652
29674
  furnitureOwner = null;
29653
29675
  for (const element of namedElements) {
29654
29676
  element.style.removeProperty(NAME_PROPERTY$1);
29655
- element.removeAttribute(SHARED_ATTRIBUTE);
29677
+ element.removeAttribute(FURNITURE_ATTRIBUTE);
29656
29678
  }
29657
29679
  namedElements = new Set();
29658
29680
  if (travelStyleElement) {
@@ -29975,7 +29997,20 @@ const measureTransitionWindowState = (element) => {
29975
29997
  };
29976
29998
  };
29977
29999
 
29978
- const holdTransitionWindow = (owner, element, stateBefore) => {
30000
+ /**
30001
+ * @param {object} options
30002
+ * @param {boolean} options.furnitureLive - Whether the bars stay live under
30003
+ * the pictures for the movement (a travel), or are pictures of their own
30004
+ * ordered over the page they belong to (a route transition, see
30005
+ * transition_furniture.js). Decides how far the cut may be opened for the
30006
+ * picture being left — see bandTheCutMayBeRelaxedTo.
30007
+ */
30008
+ const holdTransitionWindow = (
30009
+ owner,
30010
+ element,
30011
+ stateBefore,
30012
+ { furnitureLive },
30013
+ ) => {
29979
30014
  const bandBefore = stateBefore.band;
29980
30015
  const rectAfter = element.getBoundingClientRect();
29981
30016
  const bandAfter = readBand();
@@ -30009,7 +30044,9 @@ const holdTransitionWindow = (owner, element, stateBefore) => {
30009
30044
  style.setProperty(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
30010
30045
  style.setProperty(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
30011
30046
  style.setProperty(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
30012
- const oldBand = bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter);
30047
+ const oldBand = furnitureLive
30048
+ ? bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter)
30049
+ : bandBefore;
30013
30050
  style.setProperty(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
30014
30051
  style.setProperty(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
30015
30052
  style.setProperty(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
@@ -30172,18 +30209,25 @@ const readBand = () => {
30172
30209
  };
30173
30210
  };
30174
30211
 
30175
- // How far the cut may be opened for the picture being left — the band its own
30176
- // state kept free, which is the whole point of photographing it: furniture
30177
- // present in only one of the two states is not the frame, it is part of what
30178
- // changes, and cutting the page being left at a bar it never had shows its
30179
- // header being sliced instead of leaving.
30212
+ // How far the cut may be opened for the picture being left when the furniture
30213
+ // is LIVE under the pictures — the band its own state kept free, which is the
30214
+ // whole point of photographing it: furniture present in only one of the two
30215
+ // states is not the frame, it is part of what changes, and cutting the page
30216
+ // being left at a bar it never had shows its header being sliced instead of
30217
+ // leaving.
30180
30218
  //
30181
30219
  // Never past what the picture ARRIVING needs, though: the cut is one line for
30182
30220
  // both pictures, so opening it for one opens it for the other. A box that runs
30183
- // under the furniture it arrives beside — a page scrolled below a top bar —
30184
- // would then be watched painting over that bar for the length of the movement,
30185
- // which is what the cut exists to prevent. On such an edge the arriving band
30186
- // stands, and the page being left is cut as it always was.
30221
+ // under the live furniture it arrives beside — a page scrolled below a top bar
30222
+ // — would then be watched painting over that bar for the length of the
30223
+ // movement, which is what the cut exists to prevent. On such an edge the
30224
+ // arriving band stands, and the page being left is cut as it always was.
30225
+ //
30226
+ // With the furniture photographed (a route transition) none of this holds: a
30227
+ // bar only the arriving state has is a picture ordered over the page it
30228
+ // belongs to, and a page arriving under it paints nothing over it. The band
30229
+ // of the state being left is then published as it was, so the page being
30230
+ // left is never cut at a bar it never had.
30187
30231
  const bandTheCutMayBeRelaxedTo = (bandBefore, bandAfter, rectAfter) => {
30188
30232
  return {
30189
30233
  top: rectAfter.top < bandAfter.top ? bandAfter.top : bandBefore.top,
@@ -30376,6 +30420,16 @@ const startViewTransition$1 = ensureDocumentStartViewTransition();
30376
30420
  const TRANSITION_ATTRIBUTE = "data-navi-route-transition";
30377
30421
  const TRANSITION_TYPE_ATTRIBUTE = "data-navi-route-transition-type";
30378
30422
  const TRANSITION_DURATION_PROPERTY = "--navi-route-transition-duration";
30423
+ // Which page the other one COVERS, when the movement is one page over the
30424
+ // other: "old" (the page arriving comes over the page being left) or "new"
30425
+ // (the page being left slides off the page arriving). Published by the type
30426
+ // as a value on the root, like its keyframes (see the css), and mirrored as an
30427
+ // attribute for the length of the transition so the rules that depend on it
30428
+ // — which page is on top, what is cut, where the furniture goes — can be
30429
+ // written statically. Absent for a movement where neither page covers the
30430
+ // other.
30431
+ const TRANSITION_COVERED_PROPERTY = "--navi-route-transition-covered";
30432
+ const TRANSITION_COVERED_ATTRIBUTE = "data-navi-route-transition-covered";
30379
30433
  // What the movement is played on. The root snapshot spans the viewport, and
30380
30434
  // the regions of elements captured on their own (a named bar) are BLANK in it
30381
30435
  // — a page sliding vertically then drags a blank band across the screen where
@@ -30458,7 +30512,7 @@ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
30458
30512
  z-index: 1;
30459
30513
  }
30460
30514
 
30461
- &::view-transition-group(.navi_furniture_shared) {
30515
+ &::view-transition-group(.navi_furniture_leaving), &::view-transition-group(.navi_furniture_arriving), &::view-transition-group(.navi_furniture_shared) {
30462
30516
  z-index: 2;
30463
30517
  }
30464
30518
 
@@ -30466,6 +30520,18 @@ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
30466
30520
  z-index: 3;
30467
30521
  }
30468
30522
 
30523
+ &[data-navi-route-transition-covered="old"] {
30524
+ &::view-transition-group(.navi_furniture_leaving) {
30525
+ z-index: 0;
30526
+ }
30527
+ }
30528
+
30529
+ &[data-navi-route-transition-covered="new"] {
30530
+ &::view-transition-group(.navi_furniture_arriving) {
30531
+ z-index: 0;
30532
+ }
30533
+ }
30534
+
30469
30535
  &::view-transition-group(*), &::view-transition-old(*), &::view-transition-new(*) {
30470
30536
  animation-duration: var(--navi-route-transition-duration, .3s);
30471
30537
  }
@@ -30505,6 +30571,38 @@ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
30505
30571
  left: calc(var(--navi-transition-window-new-left) - var(--navi-transition-window-left));
30506
30572
  }
30507
30573
 
30574
+ &[data-navi-route-transition-target="area"] {
30575
+ &[data-navi-route-transition-covered="old"] {
30576
+ &::view-transition-old(navi-route-transition) {
30577
+ clip-path: polygon(calc(var(--navi-transition-old-band-left) - var(--navi-transition-window-old-left))
30578
+ calc(var(--navi-transition-old-band-top) - var(--navi-transition-window-old-top)),
30579
+ calc(100dvw - var(--navi-transition-old-band-right) - var(--navi-transition-window-old-left))
30580
+ calc(var(--navi-transition-old-band-top) - var(--navi-transition-window-old-top)),
30581
+ calc(100dvw - var(--navi-transition-old-band-right) - var(--navi-transition-window-old-left))
30582
+ calc(100dvh - var(--navi-transition-old-band-bottom) - var(--navi-transition-window-old-top)),
30583
+ calc(var(--navi-transition-old-band-left) - var(--navi-transition-window-old-left))
30584
+ calc(100dvh - var(--navi-transition-old-band-bottom) - var(--navi-transition-window-old-top)));
30585
+ }
30586
+ }
30587
+
30588
+ &[data-navi-route-transition-covered="new"] {
30589
+ &::view-transition-new(navi-route-transition) {
30590
+ clip-path: polygon(calc(var(--navi-safe-area-inset-left) +
30591
+ var(--navi-transition-cover-left) - var(--navi-transition-window-new-left))
30592
+ calc(var(--navi-safe-area-inset-top) +
30593
+ var(--navi-transition-cover-top) - var(--navi-transition-window-new-top)),
30594
+ calc(100dvw - var(--navi-safe-area-inset-right) - var(--navi-transition-cover-right) - var(--navi-transition-window-new-left))
30595
+ calc(var(--navi-safe-area-inset-top) +
30596
+ var(--navi-transition-cover-top) - var(--navi-transition-window-new-top)),
30597
+ calc(100dvw - var(--navi-safe-area-inset-right) - var(--navi-transition-cover-right) - var(--navi-transition-window-new-left))
30598
+ calc(100dvh - var(--navi-safe-area-inset-bottom) - var(--navi-transition-cover-bottom) - var(--navi-transition-window-new-top)),
30599
+ calc(var(--navi-safe-area-inset-left) +
30600
+ var(--navi-transition-cover-left) - var(--navi-transition-window-new-left))
30601
+ calc(100dvh - var(--navi-safe-area-inset-bottom) - var(--navi-transition-cover-bottom) - var(--navi-transition-window-new-top)));
30602
+ }
30603
+ }
30604
+ }
30605
+
30508
30606
  &[data-navi-route-transition-type] {
30509
30607
  &::view-transition-old(root), &::view-transition-new(root), &::view-transition-old(navi-route-transition), &::view-transition-new(navi-route-transition) {
30510
30608
  object-fit: none;
@@ -30589,10 +30687,18 @@ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
30589
30687
  }
30590
30688
 
30591
30689
  &[data-navi-route-transition-type="cover-right"], &[data-navi-route-transition-type="cover-left"], &[data-navi-route-transition-type="cover-bottom"], &[data-navi-route-transition-type="cover-top"] {
30690
+ &[data-navi-route-transition="forward"] {
30691
+ --navi-route-transition-covered: old;
30692
+ }
30693
+
30592
30694
  &[data-navi-route-transition="back"] {
30593
- &::view-transition-old(root), &::view-transition-old(navi-route-transition) {
30594
- z-index: 1;
30595
- }
30695
+ --navi-route-transition-covered: new;
30696
+ }
30697
+ }
30698
+
30699
+ &[data-navi-route-transition-covered="new"] {
30700
+ &::view-transition-old(root), &::view-transition-old(navi-route-transition) {
30701
+ z-index: 1;
30596
30702
  }
30597
30703
  }
30598
30704
 
@@ -30795,6 +30901,11 @@ const RouteTransitionArea = ({
30795
30901
  * }
30796
30902
  * }
30797
30903
  *
30904
+ * A type of the application's own that plays one page OVER the other says
30905
+ * which one is covered, next to its keyframes:
30906
+ * `--navi-route-transition-covered: old` (the page arriving comes over the
30907
+ * page being left) or `new` (the page being left slides off the page
30908
+ * arriving) — see docs/route_transitions.md, "Custom movements".
30798
30909
  * Whatever is written here is what EVERY crossing of the pair plays. One
30799
30910
  * crossing can ask for something else — `<Link routeTransition>`, or
30800
30911
  * navTo(url, { routeTransition }) — which overrides this field by field, for
@@ -31438,6 +31549,12 @@ const beginTransition = ({
31438
31549
  documentElement.setAttribute(TRANSITION_ATTRIBUTE, direction);
31439
31550
  if (type) {
31440
31551
  documentElement.setAttribute(TRANSITION_TYPE_ATTRIBUTE, type);
31552
+ // Read once the type is worn, which is what makes the type's value
31553
+ // resolve; a value a type does not publish leaves the attribute off.
31554
+ const covered = getComputedStyle(documentElement).getPropertyValue(TRANSITION_COVERED_PROPERTY).trim();
31555
+ if (covered === "old" || covered === "new") {
31556
+ documentElement.setAttribute(TRANSITION_COVERED_ATTRIBUTE, covered);
31557
+ }
31441
31558
  }
31442
31559
  // Looked up per transition, not once: the area is the application's own
31443
31560
  // element and follows its lifecycle — a page layout without bars has none,
@@ -31540,7 +31657,9 @@ const beginTransition = ({
31540
31657
  // replaced holds nothing: what it wore was taken off at the takeover.
31541
31658
  if (areaElement && currentTransition === transition) {
31542
31659
  holdTransitionFurniture(transition, areaElement);
31543
- holdTransitionWindow(transition, areaElement, areaStateBefore);
31660
+ holdTransitionWindow(transition, areaElement, areaStateBefore, {
31661
+ furnitureLive: false
31662
+ });
31544
31663
  }
31545
31664
  });
31546
31665
  const end = () => {
@@ -31575,6 +31694,7 @@ const releaseTransitionRoot = transition => {
31575
31694
  const documentElement = document.documentElement;
31576
31695
  documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
31577
31696
  documentElement.removeAttribute(TRANSITION_TYPE_ATTRIBUTE);
31697
+ documentElement.removeAttribute(TRANSITION_COVERED_ATTRIBUTE);
31578
31698
  documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
31579
31699
  releaseTransitionWindow(transition);
31580
31700
  releaseTransitionDestination(transition);
@@ -32329,7 +32449,11 @@ const RouteTravel = ({
32329
32449
  // which is what the box is measured through: the two states are at the
32330
32450
  // same place in the layout without being at the same place in the window
32331
32451
  // (see transition_window.js).
32332
- holdTransitionWindow(travel, elementRef.current, stateBefore);
32452
+ // The bars stay live under the pictures for a travel: only the box is
32453
+ // photographed (see the TRAVEL_ATTRIBUTE css).
32454
+ holdTransitionWindow(travel, elementRef.current, stateBefore, {
32455
+ furnitureLive: true
32456
+ });
32333
32457
  });
32334
32458
  travel.viewTransition = viewTransition;
32335
32459
  if (scrub) {
@@ -49208,10 +49332,10 @@ const css$R = /* css */`.navi_expandable {
49208
49332
  }
49209
49333
 
49210
49334
  & > .navi_expandable_content_container {
49211
- clip-path: inset(-9999px -9999px 0);
49212
49335
  grid-template-rows: 0fr;
49213
49336
  display: grid;
49214
49337
  position: relative;
49338
+ overflow-y: clip;
49215
49339
 
49216
49340
  & > .navi_expandable_content_sizer {
49217
49341
  min-height: 0;
@@ -49224,7 +49348,6 @@ const css$R = /* css */`.navi_expandable {
49224
49348
  }
49225
49349
 
49226
49350
  &[data-open-direction="up"] > .navi_expandable_content_container {
49227
- clip-path: inset(0 -9999px -9999px);
49228
49351
  align-content: end;
49229
49352
 
49230
49353
  & > .navi_expandable_content_sizer {
@@ -49246,9 +49369,9 @@ const css$R = /* css */`.navi_expandable {
49246
49369
  }
49247
49370
 
49248
49371
  & > .navi_expandable_content_container {
49249
- clip-path: inset(-9999px 0 0 -9999px);
49250
49372
  grid-template-rows: 0fr;
49251
49373
  grid-template-columns: 0fr;
49374
+ overflow: clip;
49252
49375
 
49253
49376
  & > .navi_expandable_content_sizer {
49254
49377
  min-width: 0;
@@ -49266,7 +49389,6 @@ const css$R = /* css */`.navi_expandable {
49266
49389
  }
49267
49390
 
49268
49391
  &[data-open-direction="left"] > .navi_expandable_content_container {
49269
- clip-path: inset(-9999px -9999px 0 0);
49270
49392
  justify-content: end;
49271
49393
 
49272
49394
  & > .navi_expandable_content_sizer {
@@ -49288,11 +49410,10 @@ const css$R = /* css */`.navi_expandable {
49288
49410
  }
49289
49411
 
49290
49412
  &[aria-expanded="true"][data-settled] > .navi_expandable_content_container {
49291
- clip-path: none;
49413
+ overflow: visible;
49292
49414
  }
49293
49415
 
49294
49416
  &:not([aria-expanded="true"])[data-settled] > .navi_expandable_content_container {
49295
- clip-path: none;
49296
49417
  overflow: clip;
49297
49418
  }
49298
49419
 
@@ -49389,11 +49510,12 @@ const useExpandableContext = partName => {
49389
49510
  * that can scroll so the content revealed past its edge is on screen: an
49390
49511
  * expandable opening at the bottom of a list otherwise grows below the
49391
49512
  * fold, and the user sees the row grow but not what it grew with. Measured
49392
- * on the open layout and done once per opening, alongside the reveal
49393
- * (smoothly when `animation` plays, at once otherwise). An expandable
49394
- * taller than the scroller keeps its UI part on screen and the content
49395
- * follows as far as it fits. Only the nearest scroller moves — a popup or
49396
- * a page around it stays where it is.
49513
+ * on the open layout, once per opening; when `animation` plays the
49514
+ * scroller rides along with the reveal, at the track's own progress, and
49515
+ * otherwise it goes there at once. A `scroll-margin` on the expandable is
49516
+ * honored. An expandable taller than the scroller keeps its UI part on
49517
+ * screen and the content follows as far as it fits. Only the nearest
49518
+ * scroller moves — a popup or a page around it stays where it is.
49397
49519
  * @param maxContentHeight - Caps the content height; taller content scrolls
49398
49520
  * inside the expandable instead of growing it.
49399
49521
  * @param mount - When the content is built and thrown away, a popup's own
@@ -49516,15 +49638,27 @@ const Expandable = props => {
49516
49638
  cancelSettleWatchRef.current?.();
49517
49639
  cancelSettleWatchRef.current = null;
49518
49640
  };
49519
- const watchSettle = () => {
49641
+ // `stopRide`, when the scroller rides along with the reveal (keepInView):
49642
+ // the ride is part of the movement, so it ends with it — settled or
49643
+ // cancelled — and never on its own clock.
49644
+ const watchSettle = stopRide => {
49520
49645
  const contentContainer = contentContainerRef.current;
49521
49646
  const contentElement = contentContainer.firstElementChild.firstElementChild;
49522
- cancelSettleWatchRef.current = whenTransitionSettles(contentContainer, () => {
49647
+ const cancelWatch = whenTransitionSettles(contentContainer, () => {
49523
49648
  cancelSettleWatchRef.current = null;
49649
+ if (stopRide) {
49650
+ stopRide();
49651
+ }
49524
49652
  contentElement.style.width = "";
49525
49653
  contentElement.style.height = "";
49526
49654
  setSettled(true);
49527
49655
  });
49656
+ cancelSettleWatchRef.current = () => {
49657
+ if (stopRide) {
49658
+ stopRide();
49659
+ }
49660
+ cancelWatch();
49661
+ };
49528
49662
  };
49529
49663
 
49530
49664
  // The reveal needs the content at its final size before the track starts
@@ -49543,17 +49677,22 @@ const Expandable = props => {
49543
49677
  } else {
49544
49678
  contentElement.style.height = `${finalRect.height}px`;
49545
49679
  }
49680
+ let stopRide = null;
49546
49681
  if (keepInView) {
49547
- // Here and not once the reveal settles: the layout is the open one at
49548
- // this point, so where the content ends is known before the movement
49549
- // starts, and the scroll can travel along with it. The frozen content
49550
- // overflowing the collapsed track is still scrollable overflow (the
49551
- // one-sided clip-path, see the CSS), so the scroller can reach that
49552
- // position from the first frame.
49553
- scrollExpandableIntoView(rootRef.current, {
49554
- revealDirection,
49555
- behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth"
49556
- });
49682
+ // Measured here and not once the reveal settles: the layout is the open
49683
+ // one at this point, so where the content ends is known before the
49684
+ // movement starts. The scroll then rides the movement rather than
49685
+ // running ahead of it (see rideScrollerAlongReveal).
49686
+ const revealScroll = measureRevealScroll(rootRef.current, revealDirection);
49687
+ if (revealScroll) {
49688
+ const finalContainerRect = contentContainer.getBoundingClientRect();
49689
+ stopRide = rideScrollerAlongReveal(revealScroll, {
49690
+ contentContainer,
49691
+ axis: isColumn ? "x" : "y",
49692
+ startSize: isColumn ? startRect.width : startRect.height,
49693
+ finalSize: isColumn ? finalContainerRect.width : finalContainerRect.height
49694
+ });
49695
+ }
49557
49696
  }
49558
49697
  // Put the tracks back where the last paint left them and let the
49559
49698
  // transition play from there. In fr — px does not interpolate with fr.
@@ -49576,6 +49715,7 @@ const Expandable = props => {
49576
49715
  contentContainer.style.transitionProperty = "";
49577
49716
  contentContainer.style.gridTemplateColumns = "";
49578
49717
  contentContainer.style.gridTemplateRows = "";
49718
+ return stopRide;
49579
49719
  };
49580
49720
 
49581
49721
  // What opening LOOKS like here, and how to undo it — the one thing an
@@ -49607,12 +49747,17 @@ const Expandable = props => {
49607
49747
  // transfer there is nothing to restore on close either.
49608
49748
  const restoreFocus = autoFocus && !silent && contentContainer ? openController.transferFocusOnOpen(contentContainer) : null;
49609
49749
  if (revealing) {
49610
- armReveal(startRect);
49611
- watchSettle();
49750
+ const stopRide = armReveal(startRect);
49751
+ watchSettle(stopRide);
49612
49752
  } else if (keepInView && !silent && contentContainer) {
49613
- scrollExpandableIntoView(rootRef.current, {
49614
- revealDirection
49615
- });
49753
+ const revealScroll = measureRevealScroll(rootRef.current, revealDirection);
49754
+ if (revealScroll) {
49755
+ // No behavior: the scroller's own scroll-behavior decides.
49756
+ revealScroll.scrollerEl.scrollTo({
49757
+ left: revealScroll.left,
49758
+ top: revealScroll.top
49759
+ });
49760
+ }
49616
49761
  }
49617
49762
  if (!silent) {
49618
49763
  rootRef.current.dispatchEvent(createToggleEvent(true));
@@ -49654,7 +49799,7 @@ const Expandable = props => {
49654
49799
  }
49655
49800
  }
49656
49801
  if (collapsing) {
49657
- watchSettle();
49802
+ watchSettle(null);
49658
49803
  }
49659
49804
  rootRef.current.dispatchEvent(createToggleEvent(false));
49660
49805
  };
@@ -49994,16 +50139,14 @@ Expandable.Content = ExpandableContent;
49994
50139
  // the one exception, excluded at the call site.
49995
50140
  const UI_INTERACTIVE_SELECTOR = ["a[href]", "button", "input", "select", "textarea", "label", "[role='button']", "[contenteditable='']", "[contenteditable='true']", "audio[controls]", "video[controls]"].join(", ");
49996
50141
 
49997
- // Scrolls the nearest scroller that can scroll so the whole expandable — UI
49998
- // part and revealed content — is in view. Measured on the layout as it stands,
49999
- // so the caller calls it while the open layout is the one laid out (see
50000
- // openEffect). A root taller than the view is aligned on its UI side instead:
50001
- // what the user pressed stays on screen, and the content follows as far as it
50002
- // fits. One scroller only — a popup or a page around it is left where it is.
50003
- const scrollExpandableIntoView = (rootEl, {
50004
- revealDirection,
50005
- behavior
50006
- }) => {
50142
+ // Where the nearest scroller that can scroll would have to be for the whole
50143
+ // expandable — UI part and revealed content — to be in view. Measured on the
50144
+ // layout as it stands, so the caller measures while the open layout is the
50145
+ // one laid out (see openEffect). A root taller than the view is aligned on
50146
+ // its UI side instead: what the user pressed stays on screen, and the content
50147
+ // follows as far as it fits. One scroller only — a popup or a page around it
50148
+ // is left where it is. Null when there is no scroller, or nothing to scroll.
50149
+ const measureRevealScroll = (rootEl, revealDirection) => {
50007
50150
  let scrollerEl = null;
50008
50151
  for (const scrollContainer of getScrollContainerSet(rootEl)) {
50009
50152
  if (isScrollable(scrollContainer)) {
@@ -50012,7 +50155,7 @@ const scrollExpandableIntoView = (rootEl, {
50012
50155
  }
50013
50156
  }
50014
50157
  if (!scrollerEl) {
50015
- return;
50158
+ return null;
50016
50159
  }
50017
50160
  const rootRect = rootEl.getBoundingClientRect();
50018
50161
  let block = "nearest";
@@ -50024,12 +50167,74 @@ const scrollExpandableIntoView = (rootEl, {
50024
50167
  } else if (rootRect.width > scrollerEl.clientWidth) {
50025
50168
  inline = revealDirection === "right" ? "start" : "end";
50026
50169
  }
50027
- scrollIntoViewScoped(rootEl, {
50170
+ const {
50171
+ left,
50172
+ top
50173
+ } = getScrollIntoViewScopedOffsets(rootEl, {
50028
50174
  container: scrollerEl,
50029
50175
  block,
50030
- inline,
50031
- behavior
50176
+ inline
50032
50177
  });
50178
+ if (left === scrollerEl.scrollLeft && top === scrollerEl.scrollTop) {
50179
+ return null;
50180
+ }
50181
+ return {
50182
+ scrollerEl,
50183
+ left,
50184
+ top
50185
+ };
50186
+ };
50187
+
50188
+ // Moves the scroller toward where the reveal ends at the pace of the reveal
50189
+ // itself: every frame the track's progress is read from the container's size
50190
+ // and the scroll stands the same fraction of its own way. The scroll cannot
50191
+ // run ahead of the track: only the box's own growth extends the scroller's
50192
+ // range, and what the target adds around the box (a parent's padding, a
50193
+ // scroll-margin) becomes reachable as the track grows — a scroll started at
50194
+ // once would be clamped short of it. One clock, the transition's, so the
50195
+ // content never waits below the fold either. Returns what stops the ride.
50196
+ const rideScrollerAlongReveal = ({
50197
+ scrollerEl,
50198
+ left,
50199
+ top
50200
+ }, {
50201
+ contentContainer,
50202
+ axis,
50203
+ startSize,
50204
+ finalSize
50205
+ }) => {
50206
+ const startLeft = scrollerEl.scrollLeft;
50207
+ const startTop = scrollerEl.scrollTop;
50208
+ const leftDelta = left - startLeft;
50209
+ const topDelta = top - startTop;
50210
+ let frameId;
50211
+ const tick = () => {
50212
+ const rect = contentContainer.getBoundingClientRect();
50213
+ const size = axis === "x" ? rect.width : rect.height;
50214
+ let progress = 1;
50215
+ if (finalSize !== startSize) {
50216
+ progress = (size - startSize) / (finalSize - startSize);
50217
+ if (progress < 0) {
50218
+ progress = 0;
50219
+ } else if (progress > 1) {
50220
+ progress = 1;
50221
+ }
50222
+ }
50223
+ scrollerEl.scrollTo({
50224
+ left: startLeft + leftDelta * progress,
50225
+ top: startTop + topDelta * progress,
50226
+ // Driven frame by frame: a smooth scroll-behavior on the scroller is
50227
+ // for a scroll that lands, not for one that is held.
50228
+ behavior: "instant"
50229
+ });
50230
+ if (progress < 1) {
50231
+ frameId = requestAnimationFrame(tick);
50232
+ }
50233
+ };
50234
+ frameId = requestAnimationFrame(tick);
50235
+ return () => {
50236
+ cancelAnimationFrame(frameId);
50237
+ };
50033
50238
  };
50034
50239
  const createToggleEvent = open => {
50035
50240
  const newState = open ? "open" : "closed";