@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.
@@ -30237,9 +30237,13 @@ installImportMetaCssBuild(import.meta);
30237
30237
  * - a bar only one state has never meets a counterpart. It belongs to the page
30238
30238
  * that has it, so it travels with that page — leaving by the keyframes the
30239
30239
  * page being left leaves by, arriving by the ones the page arriving arrives
30240
- * by — instead of going out with the render. Under the pages, so a page
30241
- * coming over it covers it — a popup over them instead, standing where it
30242
- * stands in the document (see the z-order in route_transition.jsx).
30240
+ * by — instead of going out with the render. Over that page, as it is at
30241
+ * rest: the page runs under it, and what it paints outside its box (a button
30242
+ * standing up out of a tab bar) is not cut by its own page. Under the pages
30243
+ * only when its page is covered by the other one — a cover-* movement — so
30244
+ * the sheet comes over it; a popup over everything, standing where it
30245
+ * stands in the document (see the z-order in route_transition.jsx). Which
30246
+ * side a bar is on is said by a class on its group, written here.
30243
30247
  *
30244
30248
  * A popup shown in the top layer is furniture of the same kind, and its case
30245
30249
  * is the sharper one: it is a DOM descendant of the area, yet painted outside
@@ -30317,11 +30321,17 @@ const FURNITURE_NAME_PREFIX = "navi-transition-furniture-";
30317
30321
  const TOP_LAYER_WALL_SELECTOR =
30318
30322
  ".navi_dialog:modal, .navi_popover_backdrop:popover-open";
30319
30323
  const WALL_ATTRIBUTE = "data-navi-transition-wall";
30320
- // Worn by a bar both states have, from the hold on: its two pictures are one
30321
- // group, and the group is ordered over the pages (route_transition.jsx). Only
30322
- // a bar — a popup's class is written by layout/popup_css.js, and a second rule
30323
- // on the same property would replace it.
30324
- const SHARED_ATTRIBUTE = "data-navi-transition-furniture-shared";
30324
+ // Which side of the movement a bar is on, worn by the bar and read by the
30325
+ // browser as the class of its group (the CSS below): "leaving" from the first
30326
+ // picture, "arriving" for a bar the state arriving brought, "shared" for one
30327
+ // that stands in both states, written over "leaving" at the hold. The group's
30328
+ // class is the one captured with the LAST state the element stands in, so a
30329
+ // bar only the state being left has keeps "leaving" from the first capture,
30330
+ // and a shared one takes "shared" from the second. The z-order in
30331
+ // route_transition.jsx is written on these three. Only a bar — a popup's
30332
+ // class is written by layout/popup_css.js, and a second rule on the same
30333
+ // property would replace it.
30334
+ const FURNITURE_ATTRIBUTE = "data-navi-transition-furniture";
30325
30335
  // What the wall paints, resolved on the matched element in both cases (the
30326
30336
  // dialog for its ::backdrop, the popover's wall for itself): the same two
30327
30337
  // properties, copied onto the stand-in.
@@ -30330,7 +30340,15 @@ const WALL_PROPERTIES = ["--backdrop-background", "--backdrop-filter"];
30330
30340
  // The pictures the wall is painted into: the pages', and every bar's own.
30331
30341
  const FIXED_BAR_SELECTOR$1 = ".navi_fixed_bar";
30332
30342
 
30333
- const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition-furniture-shared] {
30343
+ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition-furniture="leaving"] {
30344
+ view-transition-class: navi_furniture_leaving;
30345
+ }
30346
+
30347
+ .navi_fixed_bar[data-navi-transition-furniture="arriving"] {
30348
+ view-transition-class: navi_furniture_arriving;
30349
+ }
30350
+
30351
+ .navi_fixed_bar[data-navi-transition-furniture="shared"] {
30334
30352
  view-transition-class: navi_furniture_shared;
30335
30353
  }
30336
30354
 
@@ -30403,7 +30421,7 @@ const nameTransitionFurniture = (owner, areaElement) => {
30403
30421
  furnitureOwner = owner;
30404
30422
  namedElements = new Set();
30405
30423
  }
30406
- nameFurnitureAround(areaElement);
30424
+ nameFurnitureAround(areaElement, "leaving");
30407
30425
  const sources = paintTransitionWalls(areaElement);
30408
30426
  // The state being left has walls: what no picture covers starts dimmed.
30409
30427
  // The frame the first picture is taken on shows the live document, so the
@@ -30430,10 +30448,10 @@ const holdTransitionFurniture = (owner, areaElement) => {
30430
30448
  if (!element.isConnected) {
30431
30449
  namesLeaving.push(nameByElement.get(element));
30432
30450
  } else if (element.matches(FIXED_BAR_SELECTOR$1)) {
30433
- element.setAttribute(SHARED_ATTRIBUTE, "");
30451
+ element.setAttribute(FURNITURE_ATTRIBUTE, "shared");
30434
30452
  }
30435
30453
  }
30436
- const namesArriving = nameFurnitureAround(areaElement);
30454
+ const namesArriving = nameFurnitureAround(areaElement, "arriving");
30437
30455
  const sources = paintTransitionWalls(areaElement);
30438
30456
  // The state arriving has walls: what no picture covers ends dimmed. Painted
30439
30457
  // from the state being left when that one had walls (they stay up if both
@@ -30483,7 +30501,8 @@ const holdTransitionFurniture = (owner, areaElement) => {
30483
30501
 
30484
30502
  // Returns the elements it had to name, which are the ones the state arriving
30485
30503
  // brought: everything else was already wearing its name from the first pass.
30486
- const nameFurnitureAround = (areaElement) => {
30504
+ // `side` is what a bar named here is told it is on (see FURNITURE_ATTRIBUTE).
30505
+ const nameFurnitureAround = (areaElement, side) => {
30487
30506
  const namesAdded = [];
30488
30507
  for (const element of document.querySelectorAll(FURNITURE_SELECTOR)) {
30489
30508
  if (namedElements.has(element)) {
@@ -30509,6 +30528,9 @@ const nameFurnitureAround = (areaElement) => {
30509
30528
  nameByElement.set(element, name);
30510
30529
  }
30511
30530
  element.style.setProperty(NAME_PROPERTY$1, name);
30531
+ if (element.matches(FIXED_BAR_SELECTOR$1)) {
30532
+ element.setAttribute(FURNITURE_ATTRIBUTE, side);
30533
+ }
30512
30534
  namedElements.add(element);
30513
30535
  namesAdded.push(name);
30514
30536
  }
@@ -30558,7 +30580,7 @@ const releaseTransitionFurniture = (owner) => {
30558
30580
  furnitureOwner = null;
30559
30581
  for (const element of namedElements) {
30560
30582
  element.style.removeProperty(NAME_PROPERTY$1);
30561
- element.removeAttribute(SHARED_ATTRIBUTE);
30583
+ element.removeAttribute(FURNITURE_ATTRIBUTE);
30562
30584
  }
30563
30585
  namedElements = new Set();
30564
30586
  if (travelStyleElement) {
@@ -30881,7 +30903,20 @@ const measureTransitionWindowState = (element) => {
30881
30903
  };
30882
30904
  };
30883
30905
 
30884
- const holdTransitionWindow = (owner, element, stateBefore) => {
30906
+ /**
30907
+ * @param {object} options
30908
+ * @param {boolean} options.furnitureLive - Whether the bars stay live under
30909
+ * the pictures for the movement (a travel), or are pictures of their own
30910
+ * ordered over the page they belong to (a route transition, see
30911
+ * transition_furniture.js). Decides how far the cut may be opened for the
30912
+ * picture being left — see bandTheCutMayBeRelaxedTo.
30913
+ */
30914
+ const holdTransitionWindow = (
30915
+ owner,
30916
+ element,
30917
+ stateBefore,
30918
+ { furnitureLive },
30919
+ ) => {
30885
30920
  const bandBefore = stateBefore.band;
30886
30921
  const rectAfter = element.getBoundingClientRect();
30887
30922
  const bandAfter = readBand();
@@ -30915,7 +30950,9 @@ const holdTransitionWindow = (owner, element, stateBefore) => {
30915
30950
  style.setProperty(WINDOW_OLD_LEFT_PROPERTY, `${rectBefore.left}px`);
30916
30951
  style.setProperty(WINDOW_NEW_TOP_PROPERTY, `${rectAfter.top}px`);
30917
30952
  style.setProperty(WINDOW_NEW_LEFT_PROPERTY, `${rectAfter.left}px`);
30918
- const oldBand = bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter);
30953
+ const oldBand = furnitureLive
30954
+ ? bandTheCutMayBeRelaxedTo(bandBefore, bandAfter, rectAfter)
30955
+ : bandBefore;
30919
30956
  style.setProperty(OLD_BAND_TOP_PROPERTY, `${oldBand.top}px`);
30920
30957
  style.setProperty(OLD_BAND_RIGHT_PROPERTY, `${oldBand.right}px`);
30921
30958
  style.setProperty(OLD_BAND_BOTTOM_PROPERTY, `${oldBand.bottom}px`);
@@ -31078,18 +31115,25 @@ const readBand = () => {
31078
31115
  };
31079
31116
  };
31080
31117
 
31081
- // How far the cut may be opened for the picture being left — the band its own
31082
- // state kept free, which is the whole point of photographing it: furniture
31083
- // present in only one of the two states is not the frame, it is part of what
31084
- // changes, and cutting the page being left at a bar it never had shows its
31085
- // header being sliced instead of leaving.
31118
+ // How far the cut may be opened for the picture being left when the furniture
31119
+ // is LIVE under the pictures — the band its own state kept free, which is the
31120
+ // whole point of photographing it: furniture present in only one of the two
31121
+ // states is not the frame, it is part of what changes, and cutting the page
31122
+ // being left at a bar it never had shows its header being sliced instead of
31123
+ // leaving.
31086
31124
  //
31087
31125
  // Never past what the picture ARRIVING needs, though: the cut is one line for
31088
31126
  // both pictures, so opening it for one opens it for the other. A box that runs
31089
- // under the furniture it arrives beside — a page scrolled below a top bar —
31090
- // would then be watched painting over that bar for the length of the movement,
31091
- // which is what the cut exists to prevent. On such an edge the arriving band
31092
- // stands, and the page being left is cut as it always was.
31127
+ // under the live furniture it arrives beside — a page scrolled below a top bar
31128
+ // — would then be watched painting over that bar for the length of the
31129
+ // movement, which is what the cut exists to prevent. On such an edge the
31130
+ // arriving band stands, and the page being left is cut as it always was.
31131
+ //
31132
+ // With the furniture photographed (a route transition) none of this holds: a
31133
+ // bar only the arriving state has is a picture ordered over the page it
31134
+ // belongs to, and a page arriving under it paints nothing over it. The band
31135
+ // of the state being left is then published as it was, so the page being
31136
+ // left is never cut at a bar it never had.
31093
31137
  const bandTheCutMayBeRelaxedTo = (bandBefore, bandAfter, rectAfter) => {
31094
31138
  return {
31095
31139
  top: rectAfter.top < bandAfter.top ? bandAfter.top : bandBefore.top,
@@ -31282,6 +31326,16 @@ const startViewTransition$1 = ensureDocumentStartViewTransition();
31282
31326
  const TRANSITION_ATTRIBUTE = "data-navi-route-transition";
31283
31327
  const TRANSITION_TYPE_ATTRIBUTE = "data-navi-route-transition-type";
31284
31328
  const TRANSITION_DURATION_PROPERTY = "--navi-route-transition-duration";
31329
+ // Which page the other one COVERS, when the movement is one page over the
31330
+ // other: "old" (the page arriving comes over the page being left) or "new"
31331
+ // (the page being left slides off the page arriving). Published by the type
31332
+ // as a value on the root, like its keyframes (see the css), and mirrored as an
31333
+ // attribute for the length of the transition so the rules that depend on it
31334
+ // — which page is on top, what is cut, where the furniture goes — can be
31335
+ // written statically. Absent for a movement where neither page covers the
31336
+ // other.
31337
+ const TRANSITION_COVERED_PROPERTY = "--navi-route-transition-covered";
31338
+ const TRANSITION_COVERED_ATTRIBUTE = "data-navi-route-transition-covered";
31285
31339
  // What the movement is played on. The root snapshot spans the viewport, and
31286
31340
  // the regions of elements captured on their own (a named bar) are BLANK in it
31287
31341
  // — a page sliding vertically then drags a blank band across the screen where
@@ -31364,7 +31418,7 @@ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
31364
31418
  z-index: 1;
31365
31419
  }
31366
31420
 
31367
- &::view-transition-group(.navi_furniture_shared) {
31421
+ &::view-transition-group(.navi_furniture_leaving), &::view-transition-group(.navi_furniture_arriving), &::view-transition-group(.navi_furniture_shared) {
31368
31422
  z-index: 2;
31369
31423
  }
31370
31424
 
@@ -31372,6 +31426,18 @@ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
31372
31426
  z-index: 3;
31373
31427
  }
31374
31428
 
31429
+ &[data-navi-route-transition-covered="old"] {
31430
+ &::view-transition-group(.navi_furniture_leaving) {
31431
+ z-index: 0;
31432
+ }
31433
+ }
31434
+
31435
+ &[data-navi-route-transition-covered="new"] {
31436
+ &::view-transition-group(.navi_furniture_arriving) {
31437
+ z-index: 0;
31438
+ }
31439
+ }
31440
+
31375
31441
  &::view-transition-group(*), &::view-transition-old(*), &::view-transition-new(*) {
31376
31442
  animation-duration: var(--navi-route-transition-duration, .3s);
31377
31443
  }
@@ -31411,6 +31477,38 @@ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
31411
31477
  left: calc(var(--navi-transition-window-new-left) - var(--navi-transition-window-left));
31412
31478
  }
31413
31479
 
31480
+ &[data-navi-route-transition-target="area"] {
31481
+ &[data-navi-route-transition-covered="old"] {
31482
+ &::view-transition-old(navi-route-transition) {
31483
+ clip-path: polygon(calc(var(--navi-transition-old-band-left) - var(--navi-transition-window-old-left))
31484
+ calc(var(--navi-transition-old-band-top) - var(--navi-transition-window-old-top)),
31485
+ calc(100dvw - var(--navi-transition-old-band-right) - var(--navi-transition-window-old-left))
31486
+ calc(var(--navi-transition-old-band-top) - var(--navi-transition-window-old-top)),
31487
+ calc(100dvw - var(--navi-transition-old-band-right) - var(--navi-transition-window-old-left))
31488
+ calc(100dvh - var(--navi-transition-old-band-bottom) - var(--navi-transition-window-old-top)),
31489
+ calc(var(--navi-transition-old-band-left) - var(--navi-transition-window-old-left))
31490
+ calc(100dvh - var(--navi-transition-old-band-bottom) - var(--navi-transition-window-old-top)));
31491
+ }
31492
+ }
31493
+
31494
+ &[data-navi-route-transition-covered="new"] {
31495
+ &::view-transition-new(navi-route-transition) {
31496
+ clip-path: polygon(calc(var(--navi-safe-area-inset-left) +
31497
+ var(--navi-transition-cover-left) - var(--navi-transition-window-new-left))
31498
+ calc(var(--navi-safe-area-inset-top) +
31499
+ var(--navi-transition-cover-top) - var(--navi-transition-window-new-top)),
31500
+ calc(100dvw - var(--navi-safe-area-inset-right) - var(--navi-transition-cover-right) - var(--navi-transition-window-new-left))
31501
+ calc(var(--navi-safe-area-inset-top) +
31502
+ var(--navi-transition-cover-top) - var(--navi-transition-window-new-top)),
31503
+ calc(100dvw - var(--navi-safe-area-inset-right) - var(--navi-transition-cover-right) - var(--navi-transition-window-new-left))
31504
+ calc(100dvh - var(--navi-safe-area-inset-bottom) - var(--navi-transition-cover-bottom) - var(--navi-transition-window-new-top)),
31505
+ calc(var(--navi-safe-area-inset-left) +
31506
+ var(--navi-transition-cover-left) - var(--navi-transition-window-new-left))
31507
+ calc(100dvh - var(--navi-safe-area-inset-bottom) - var(--navi-transition-cover-bottom) - var(--navi-transition-window-new-top)));
31508
+ }
31509
+ }
31510
+ }
31511
+
31414
31512
  &[data-navi-route-transition-type] {
31415
31513
  &::view-transition-old(root), &::view-transition-new(root), &::view-transition-old(navi-route-transition), &::view-transition-new(navi-route-transition) {
31416
31514
  object-fit: none;
@@ -31495,10 +31593,18 @@ const css$12 = /* css */`:root[data-navi-route-transition] [data-navi-route-tran
31495
31593
  }
31496
31594
 
31497
31595
  &[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"] {
31596
+ &[data-navi-route-transition="forward"] {
31597
+ --navi-route-transition-covered: old;
31598
+ }
31599
+
31498
31600
  &[data-navi-route-transition="back"] {
31499
- &::view-transition-old(root), &::view-transition-old(navi-route-transition) {
31500
- z-index: 1;
31501
- }
31601
+ --navi-route-transition-covered: new;
31602
+ }
31603
+ }
31604
+
31605
+ &[data-navi-route-transition-covered="new"] {
31606
+ &::view-transition-old(root), &::view-transition-old(navi-route-transition) {
31607
+ z-index: 1;
31502
31608
  }
31503
31609
  }
31504
31610
 
@@ -31701,6 +31807,11 @@ const RouteTransitionArea = ({
31701
31807
  * }
31702
31808
  * }
31703
31809
  *
31810
+ * A type of the application's own that plays one page OVER the other says
31811
+ * which one is covered, next to its keyframes:
31812
+ * `--navi-route-transition-covered: old` (the page arriving comes over the
31813
+ * page being left) or `new` (the page being left slides off the page
31814
+ * arriving) — see docs/route_transitions.md, "Custom movements".
31704
31815
  * Whatever is written here is what EVERY crossing of the pair plays. One
31705
31816
  * crossing can ask for something else — `<Link routeTransition>`, or
31706
31817
  * navTo(url, { routeTransition }) — which overrides this field by field, for
@@ -32344,6 +32455,12 @@ const beginTransition = ({
32344
32455
  documentElement.setAttribute(TRANSITION_ATTRIBUTE, direction);
32345
32456
  if (type) {
32346
32457
  documentElement.setAttribute(TRANSITION_TYPE_ATTRIBUTE, type);
32458
+ // Read once the type is worn, which is what makes the type's value
32459
+ // resolve; a value a type does not publish leaves the attribute off.
32460
+ const covered = getComputedStyle(documentElement).getPropertyValue(TRANSITION_COVERED_PROPERTY).trim();
32461
+ if (covered === "old" || covered === "new") {
32462
+ documentElement.setAttribute(TRANSITION_COVERED_ATTRIBUTE, covered);
32463
+ }
32347
32464
  }
32348
32465
  // Looked up per transition, not once: the area is the application's own
32349
32466
  // element and follows its lifecycle — a page layout without bars has none,
@@ -32446,7 +32563,9 @@ const beginTransition = ({
32446
32563
  // replaced holds nothing: what it wore was taken off at the takeover.
32447
32564
  if (areaElement && currentTransition === transition) {
32448
32565
  holdTransitionFurniture(transition, areaElement);
32449
- holdTransitionWindow(transition, areaElement, areaStateBefore);
32566
+ holdTransitionWindow(transition, areaElement, areaStateBefore, {
32567
+ furnitureLive: false
32568
+ });
32450
32569
  }
32451
32570
  });
32452
32571
  const end = () => {
@@ -32481,6 +32600,7 @@ const releaseTransitionRoot = transition => {
32481
32600
  const documentElement = document.documentElement;
32482
32601
  documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
32483
32602
  documentElement.removeAttribute(TRANSITION_TYPE_ATTRIBUTE);
32603
+ documentElement.removeAttribute(TRANSITION_COVERED_ATTRIBUTE);
32484
32604
  documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
32485
32605
  releaseTransitionWindow(transition);
32486
32606
  releaseTransitionDestination(transition);
@@ -33235,7 +33355,11 @@ const RouteTravel = ({
33235
33355
  // which is what the box is measured through: the two states are at the
33236
33356
  // same place in the layout without being at the same place in the window
33237
33357
  // (see transition_window.js).
33238
- holdTransitionWindow(travel, elementRef.current, stateBefore);
33358
+ // The bars stay live under the pictures for a travel: only the box is
33359
+ // photographed (see the TRAVEL_ATTRIBUTE css).
33360
+ holdTransitionWindow(travel, elementRef.current, stateBefore, {
33361
+ furnitureLive: true
33362
+ });
33239
33363
  });
33240
33364
  travel.viewTransition = viewTransition;
33241
33365
  if (scrub) {
@@ -50338,10 +50462,10 @@ const css$R = /* css */`.navi_expandable {
50338
50462
  }
50339
50463
 
50340
50464
  & > .navi_expandable_content_container {
50341
- clip-path: inset(-9999px -9999px 0);
50342
50465
  grid-template-rows: 0fr;
50343
50466
  display: grid;
50344
50467
  position: relative;
50468
+ overflow-y: clip;
50345
50469
 
50346
50470
  & > .navi_expandable_content_sizer {
50347
50471
  min-height: 0;
@@ -50354,7 +50478,6 @@ const css$R = /* css */`.navi_expandable {
50354
50478
  }
50355
50479
 
50356
50480
  &[data-open-direction="up"] > .navi_expandable_content_container {
50357
- clip-path: inset(0 -9999px -9999px);
50358
50481
  align-content: end;
50359
50482
 
50360
50483
  & > .navi_expandable_content_sizer {
@@ -50376,9 +50499,9 @@ const css$R = /* css */`.navi_expandable {
50376
50499
  }
50377
50500
 
50378
50501
  & > .navi_expandable_content_container {
50379
- clip-path: inset(-9999px 0 0 -9999px);
50380
50502
  grid-template-rows: 0fr;
50381
50503
  grid-template-columns: 0fr;
50504
+ overflow: clip;
50382
50505
 
50383
50506
  & > .navi_expandable_content_sizer {
50384
50507
  min-width: 0;
@@ -50396,7 +50519,6 @@ const css$R = /* css */`.navi_expandable {
50396
50519
  }
50397
50520
 
50398
50521
  &[data-open-direction="left"] > .navi_expandable_content_container {
50399
- clip-path: inset(-9999px -9999px 0 0);
50400
50522
  justify-content: end;
50401
50523
 
50402
50524
  & > .navi_expandable_content_sizer {
@@ -50418,11 +50540,10 @@ const css$R = /* css */`.navi_expandable {
50418
50540
  }
50419
50541
 
50420
50542
  &[aria-expanded="true"][data-settled] > .navi_expandable_content_container {
50421
- clip-path: none;
50543
+ overflow: visible;
50422
50544
  }
50423
50545
 
50424
50546
  &:not([aria-expanded="true"])[data-settled] > .navi_expandable_content_container {
50425
- clip-path: none;
50426
50547
  overflow: clip;
50427
50548
  }
50428
50549
 
@@ -50519,11 +50640,12 @@ const useExpandableContext = partName => {
50519
50640
  * that can scroll so the content revealed past its edge is on screen: an
50520
50641
  * expandable opening at the bottom of a list otherwise grows below the
50521
50642
  * fold, and the user sees the row grow but not what it grew with. Measured
50522
- * on the open layout and done once per opening, alongside the reveal
50523
- * (smoothly when `animation` plays, at once otherwise). An expandable
50524
- * taller than the scroller keeps its UI part on screen and the content
50525
- * follows as far as it fits. Only the nearest scroller moves — a popup or
50526
- * a page around it stays where it is.
50643
+ * on the open layout, once per opening; when `animation` plays the
50644
+ * scroller rides along with the reveal, at the track's own progress, and
50645
+ * otherwise it goes there at once. A `scroll-margin` on the expandable is
50646
+ * honored. An expandable taller than the scroller keeps its UI part on
50647
+ * screen and the content follows as far as it fits. Only the nearest
50648
+ * scroller moves — a popup or a page around it stays where it is.
50527
50649
  * @param maxContentHeight - Caps the content height; taller content scrolls
50528
50650
  * inside the expandable instead of growing it.
50529
50651
  * @param mount - When the content is built and thrown away, a popup's own
@@ -50646,15 +50768,27 @@ const Expandable = props => {
50646
50768
  cancelSettleWatchRef.current?.();
50647
50769
  cancelSettleWatchRef.current = null;
50648
50770
  };
50649
- const watchSettle = () => {
50771
+ // `stopRide`, when the scroller rides along with the reveal (keepInView):
50772
+ // the ride is part of the movement, so it ends with it — settled or
50773
+ // cancelled — and never on its own clock.
50774
+ const watchSettle = stopRide => {
50650
50775
  const contentContainer = contentContainerRef.current;
50651
50776
  const contentElement = contentContainer.firstElementChild.firstElementChild;
50652
- cancelSettleWatchRef.current = whenTransitionSettles(contentContainer, () => {
50777
+ const cancelWatch = whenTransitionSettles(contentContainer, () => {
50653
50778
  cancelSettleWatchRef.current = null;
50779
+ if (stopRide) {
50780
+ stopRide();
50781
+ }
50654
50782
  contentElement.style.width = "";
50655
50783
  contentElement.style.height = "";
50656
50784
  setSettled(true);
50657
50785
  });
50786
+ cancelSettleWatchRef.current = () => {
50787
+ if (stopRide) {
50788
+ stopRide();
50789
+ }
50790
+ cancelWatch();
50791
+ };
50658
50792
  };
50659
50793
 
50660
50794
  // The reveal needs the content at its final size before the track starts
@@ -50673,17 +50807,22 @@ const Expandable = props => {
50673
50807
  } else {
50674
50808
  contentElement.style.height = `${finalRect.height}px`;
50675
50809
  }
50810
+ let stopRide = null;
50676
50811
  if (keepInView) {
50677
- // Here and not once the reveal settles: the layout is the open one at
50678
- // this point, so where the content ends is known before the movement
50679
- // starts, and the scroll can travel along with it. The frozen content
50680
- // overflowing the collapsed track is still scrollable overflow (the
50681
- // one-sided clip-path, see the CSS), so the scroller can reach that
50682
- // position from the first frame.
50683
- scrollExpandableIntoView(rootRef.current, {
50684
- revealDirection,
50685
- behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "instant" : "smooth"
50686
- });
50812
+ // Measured here and not once the reveal settles: the layout is the open
50813
+ // one at this point, so where the content ends is known before the
50814
+ // movement starts. The scroll then rides the movement rather than
50815
+ // running ahead of it (see rideScrollerAlongReveal).
50816
+ const revealScroll = measureRevealScroll(rootRef.current, revealDirection);
50817
+ if (revealScroll) {
50818
+ const finalContainerRect = contentContainer.getBoundingClientRect();
50819
+ stopRide = rideScrollerAlongReveal(revealScroll, {
50820
+ contentContainer,
50821
+ axis: isColumn ? "x" : "y",
50822
+ startSize: isColumn ? startRect.width : startRect.height,
50823
+ finalSize: isColumn ? finalContainerRect.width : finalContainerRect.height
50824
+ });
50825
+ }
50687
50826
  }
50688
50827
  // Put the tracks back where the last paint left them and let the
50689
50828
  // transition play from there. In fr — px does not interpolate with fr.
@@ -50706,6 +50845,7 @@ const Expandable = props => {
50706
50845
  contentContainer.style.transitionProperty = "";
50707
50846
  contentContainer.style.gridTemplateColumns = "";
50708
50847
  contentContainer.style.gridTemplateRows = "";
50848
+ return stopRide;
50709
50849
  };
50710
50850
 
50711
50851
  // What opening LOOKS like here, and how to undo it — the one thing an
@@ -50737,12 +50877,17 @@ const Expandable = props => {
50737
50877
  // transfer there is nothing to restore on close either.
50738
50878
  const restoreFocus = autoFocus && !silent && contentContainer ? openController.transferFocusOnOpen(contentContainer) : null;
50739
50879
  if (revealing) {
50740
- armReveal(startRect);
50741
- watchSettle();
50880
+ const stopRide = armReveal(startRect);
50881
+ watchSettle(stopRide);
50742
50882
  } else if (keepInView && !silent && contentContainer) {
50743
- scrollExpandableIntoView(rootRef.current, {
50744
- revealDirection
50745
- });
50883
+ const revealScroll = measureRevealScroll(rootRef.current, revealDirection);
50884
+ if (revealScroll) {
50885
+ // No behavior: the scroller's own scroll-behavior decides.
50886
+ revealScroll.scrollerEl.scrollTo({
50887
+ left: revealScroll.left,
50888
+ top: revealScroll.top
50889
+ });
50890
+ }
50746
50891
  }
50747
50892
  if (!silent) {
50748
50893
  rootRef.current.dispatchEvent(createToggleEvent(true));
@@ -50784,7 +50929,7 @@ const Expandable = props => {
50784
50929
  }
50785
50930
  }
50786
50931
  if (collapsing) {
50787
- watchSettle();
50932
+ watchSettle(null);
50788
50933
  }
50789
50934
  rootRef.current.dispatchEvent(createToggleEvent(false));
50790
50935
  };
@@ -51124,16 +51269,14 @@ Expandable.Content = ExpandableContent;
51124
51269
  // the one exception, excluded at the call site.
51125
51270
  const UI_INTERACTIVE_SELECTOR = ["a[href]", "button", "input", "select", "textarea", "label", "[role='button']", "[contenteditable='']", "[contenteditable='true']", "audio[controls]", "video[controls]"].join(", ");
51126
51271
 
51127
- // Scrolls the nearest scroller that can scroll so the whole expandable — UI
51128
- // part and revealed content — is in view. Measured on the layout as it stands,
51129
- // so the caller calls it while the open layout is the one laid out (see
51130
- // openEffect). A root taller than the view is aligned on its UI side instead:
51131
- // what the user pressed stays on screen, and the content follows as far as it
51132
- // fits. One scroller only — a popup or a page around it is left where it is.
51133
- const scrollExpandableIntoView = (rootEl, {
51134
- revealDirection,
51135
- behavior
51136
- }) => {
51272
+ // Where the nearest scroller that can scroll would have to be for the whole
51273
+ // expandable — UI part and revealed content — to be in view. Measured on the
51274
+ // layout as it stands, so the caller measures while the open layout is the
51275
+ // one laid out (see openEffect). A root taller than the view is aligned on
51276
+ // its UI side instead: what the user pressed stays on screen, and the content
51277
+ // follows as far as it fits. One scroller only — a popup or a page around it
51278
+ // is left where it is. Null when there is no scroller, or nothing to scroll.
51279
+ const measureRevealScroll = (rootEl, revealDirection) => {
51137
51280
  let scrollerEl = null;
51138
51281
  for (const scrollContainer of getScrollContainerSet(rootEl)) {
51139
51282
  if (isScrollable(scrollContainer)) {
@@ -51142,7 +51285,7 @@ const scrollExpandableIntoView = (rootEl, {
51142
51285
  }
51143
51286
  }
51144
51287
  if (!scrollerEl) {
51145
- return;
51288
+ return null;
51146
51289
  }
51147
51290
  const rootRect = rootEl.getBoundingClientRect();
51148
51291
  let block = "nearest";
@@ -51154,12 +51297,74 @@ const scrollExpandableIntoView = (rootEl, {
51154
51297
  } else if (rootRect.width > scrollerEl.clientWidth) {
51155
51298
  inline = revealDirection === "right" ? "start" : "end";
51156
51299
  }
51157
- scrollIntoViewScoped(rootEl, {
51300
+ const {
51301
+ left,
51302
+ top
51303
+ } = getScrollIntoViewScopedOffsets(rootEl, {
51158
51304
  container: scrollerEl,
51159
51305
  block,
51160
- inline,
51161
- behavior
51306
+ inline
51162
51307
  });
51308
+ if (left === scrollerEl.scrollLeft && top === scrollerEl.scrollTop) {
51309
+ return null;
51310
+ }
51311
+ return {
51312
+ scrollerEl,
51313
+ left,
51314
+ top
51315
+ };
51316
+ };
51317
+
51318
+ // Moves the scroller toward where the reveal ends at the pace of the reveal
51319
+ // itself: every frame the track's progress is read from the container's size
51320
+ // and the scroll stands the same fraction of its own way. The scroll cannot
51321
+ // run ahead of the track: only the box's own growth extends the scroller's
51322
+ // range, and what the target adds around the box (a parent's padding, a
51323
+ // scroll-margin) becomes reachable as the track grows — a scroll started at
51324
+ // once would be clamped short of it. One clock, the transition's, so the
51325
+ // content never waits below the fold either. Returns what stops the ride.
51326
+ const rideScrollerAlongReveal = ({
51327
+ scrollerEl,
51328
+ left,
51329
+ top
51330
+ }, {
51331
+ contentContainer,
51332
+ axis,
51333
+ startSize,
51334
+ finalSize
51335
+ }) => {
51336
+ const startLeft = scrollerEl.scrollLeft;
51337
+ const startTop = scrollerEl.scrollTop;
51338
+ const leftDelta = left - startLeft;
51339
+ const topDelta = top - startTop;
51340
+ let frameId;
51341
+ const tick = () => {
51342
+ const rect = contentContainer.getBoundingClientRect();
51343
+ const size = axis === "x" ? rect.width : rect.height;
51344
+ let progress = 1;
51345
+ if (finalSize !== startSize) {
51346
+ progress = (size - startSize) / (finalSize - startSize);
51347
+ if (progress < 0) {
51348
+ progress = 0;
51349
+ } else if (progress > 1) {
51350
+ progress = 1;
51351
+ }
51352
+ }
51353
+ scrollerEl.scrollTo({
51354
+ left: startLeft + leftDelta * progress,
51355
+ top: startTop + topDelta * progress,
51356
+ // Driven frame by frame: a smooth scroll-behavior on the scroller is
51357
+ // for a scroll that lands, not for one that is held.
51358
+ behavior: "instant"
51359
+ });
51360
+ if (progress < 1) {
51361
+ frameId = requestAnimationFrame(tick);
51362
+ }
51363
+ };
51364
+ frameId = requestAnimationFrame(tick);
51365
+ return () => {
51366
+ cancelAnimationFrame(frameId);
51367
+ };
51163
51368
  };
51164
51369
  const createToggleEvent = open => {
51165
51370
  const newState = open ? "open" : "closed";