@jsenv/navi 0.29.473 → 0.29.475

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.
@@ -18934,6 +18934,20 @@ const openCallout = (
18934
18934
  // deduped away on the grounds that the anchor did not move.
18935
18935
  callout.updatePosition = () =>
18936
18936
  positioner.update(new CustomEvent(ELEMENT_SIZE_CHANGE));
18937
+ // Content that sizes itself once on screen (a shrink-wrapped BadgeList) does
18938
+ // so after the callout measured it. The size observer positionCallout puts
18939
+ // on the message sees the change, but answers a frame later (see observeSize
18940
+ // in @jsenv/dom for why); placed again on the spot, the first painted frame
18941
+ // is the right one. Stopped here, since a popup further up holds the callout
18942
+ // out of flow and does not resize with it.
18943
+ const onContentSizeChange = (sizeChangeEvent) => {
18944
+ sizeChangeEvent.stopPropagation();
18945
+ callout.updatePosition();
18946
+ };
18947
+ calloutElement.addEventListener("navi_size_change", onContentSizeChange);
18948
+ addTeardown(() => {
18949
+ calloutElement.removeEventListener("navi_size_change", onContentSizeChange);
18950
+ });
18937
18951
 
18938
18952
  return callout;
18939
18953
  };
@@ -23337,6 +23351,10 @@ const DROP_CONTAINER_ATTRIBUTE = "data-drop-container";
23337
23351
  // every outcome — a list runs one way, and something being put somewhere goes
23338
23352
  // wherever it is put.
23339
23353
  const AXIS_ATTRIBUTE = "data-drag-axis";
23354
+ // How the items of a reordered list are laid out: "column", "row", or "wrap" for
23355
+ // side by side over several lines. Said nothing, it is read from where the
23356
+ // items stand.
23357
+ const FLOW_ATTRIBUTE = "data-reorder-flow";
23340
23358
  const DELAY_ATTRIBUTE = "data-drag-delay";
23341
23359
  const SLOP_ATTRIBUTE = "data-drag-slop";
23342
23360
  const THRESHOLD_ATTRIBUTE$1 = "data-drag-threshold";
@@ -23432,14 +23450,24 @@ defineInteractionDetector({
23432
23450
  }
23433
23451
  // Read at setup rather than at the press: a container can say it, and it is
23434
23452
  // what the gesture is about rather than something it discovers.
23453
+ const itemFlow =
23454
+ element.closest(`[${FLOW_ATTRIBUTE}]`)?.getAttribute(FLOW_ATTRIBUTE) ||
23455
+ undefined;
23435
23456
  const axisHolder = element.closest(`[${AXIS_ATTRIBUTE}]`);
23436
23457
  const axes =
23437
23458
  axisHolder?.getAttribute(AXIS_ATTRIBUTE) ||
23438
- // A list runs one way, and reordering walks it. Anything else goes wherever
23439
- // the hand takes it: a board has places all around, a thing put somewhere has
23440
- // two axes to be put along, a throw goes where it was thrown and a thing
23441
- // that leaves goes out by whichever edge.
23442
- (canReorder && !canLand && !canToss && !canLeave ? "y" : "xy");
23459
+ // A list runs one way, and reordering walks it: down a column, along a
23460
+ // row, and both for a row that wraps — its copy changes line. Anything
23461
+ // else goes wherever the hand takes it: a board has places all around, a
23462
+ // thing put somewhere has two axes to be put along, a throw goes where it
23463
+ // was thrown and a thing that leaves goes out by whichever edge.
23464
+ (canReorder && !canLand && !canToss && !canLeave
23465
+ ? itemFlow === "row"
23466
+ ? "x"
23467
+ : itemFlow === "wrap"
23468
+ ? "xy"
23469
+ : "y"
23470
+ : "xy");
23443
23471
  // Where the places are, read at setup for the same reason as the axes: they
23444
23472
  // are what the gesture is about, and it is what holds the two sides of it
23445
23473
  // that says them.
@@ -23513,6 +23541,7 @@ defineInteractionDetector({
23513
23541
  : undefined,
23514
23542
  containerElement: dropContainer || undefined,
23515
23543
  outsideOf,
23544
+ itemFlow,
23516
23545
  getItemId: (itemElement) => itemElement.id,
23517
23546
  direction: { x: axes.includes("x"), y: axes.includes("y") },
23518
23547
  // Where it may go, said in the DOM. A thing that is put somewhere stays
@@ -48054,8 +48083,8 @@ const useControlgroupProps = (props, {
48054
48083
  const [boundAction] = useActionBoundToOneParam(action, uiGroupStateController.uiStateSignal);
48055
48084
  // Mirror single-input behaviour: a controlled value with nobody listening
48056
48085
  // makes the group read-only so children don't appear interactive when they
48057
- // can't change. A form or a group around it IS someone listening — that is
48058
- // what will send the value and hand a new one back.
48086
+ // can't change. A form or a group collecting it IS someone listening — that
48087
+ // is what will send the value and hand a new one back.
48059
48088
  const listenedTo = useIsControlListenedTo(props, uiGroupStateController);
48060
48089
  const implicitReadOnlyWarnedRef = useRef(false);
48061
48090
  const implicitReadOnly = uiGroupStateController.hasValueProp && !listenedTo;
@@ -71583,6 +71612,15 @@ const useDialogProps = props => {
71583
71612
  freezeSize(dialogEl);
71584
71613
  }
71585
71614
  rectEffect.observeSize(dialogEl);
71615
+ // Content that sizes itself once on screen (a shrink-wrapped BadgeList)
71616
+ // says so, after the placement read it: placed again on the spot, where
71617
+ // observeSize answers a frame late. Stopped here, since a popup further up
71618
+ // holds this one out of flow and does not resize with it.
71619
+ const onContentSizeChange = sizeChangeEvent => {
71620
+ sizeChangeEvent.stopPropagation();
71621
+ rectEffect.check(new CustomEvent(ELEMENT_SIZE_CHANGE));
71622
+ };
71623
+ dialogEl.addEventListener("navi_size_change", onContentSizeChange);
71586
71624
  // Exposed for the placement-props effect below, which needs to re-place an
71587
71625
  // already-open dialog.
71588
71626
  repositionRef.current = repositionEvent => {
@@ -71596,6 +71634,7 @@ const useDialogProps = props => {
71596
71634
  };
71597
71635
  addCleanup(() => {
71598
71636
  repositionRef.current = null;
71637
+ dialogEl.removeEventListener("navi_size_change", onContentSizeChange);
71599
71638
  rectEffect.disconnect();
71600
71639
  });
71601
71640
  // A descendant anchored to something inside this dialog (a Callout, a
@@ -73426,6 +73465,18 @@ const usePopoverProps = props => {
73426
73465
  // while open (e.g. an expand/collapse toggle inside it) — not just when
73427
73466
  // the anchor itself moves/resizes/re-anchors.
73428
73467
  rectEffect.observeSize(popoverEl);
73468
+ // Content that sizes itself once on screen (a shrink-wrapped BadgeList)
73469
+ // says so, after the placement read it: placed again on the spot, where
73470
+ // observeSize answers a frame late. Stopped here, since a popup further up
73471
+ // holds this one out of flow and does not resize with it.
73472
+ const onContentSizeChange = sizeChangeEvent => {
73473
+ sizeChangeEvent.stopPropagation();
73474
+ rectEffect.check(new CustomEvent(ELEMENT_SIZE_CHANGE));
73475
+ };
73476
+ popoverEl.addEventListener("navi_size_change", onContentSizeChange);
73477
+ addCleanup(() => {
73478
+ popoverEl.removeEventListener("navi_size_change", onContentSizeChange);
73479
+ });
73429
73480
  // Exposed for the placement-props effect above, which needs to re-place an
73430
73481
  // already-open popover.
73431
73482
  repositionRef.current = repositionEvent => {
@@ -78950,6 +79001,7 @@ const useListScrollSync = ({
78950
79001
  useDuplicateHeaderWarning(ref);
78951
79002
  useStuckWindowWarning({
78952
79003
  ref,
79004
+ getListEl,
78953
79005
  scrollerElResolved,
78954
79006
  renderWindowRef,
78955
79007
  windowLeavesItemsOut,
@@ -80497,6 +80549,7 @@ const readItemsPerLine = listEl => {
80497
80549
  // instead of scrolling — and there is no event missing to notice it by.
80498
80550
  const useStuckWindowWarning = ({
80499
80551
  ref,
80552
+ getListEl,
80500
80553
  scrollerElResolved,
80501
80554
  renderWindowRef,
80502
80555
  windowLeavesItemsOut,
@@ -80525,6 +80578,22 @@ const useStuckWindowWarning = ({
80525
80578
  if (canScrollerScroll(scrollerElResolved, horizontal ? "x" : "y")) {
80526
80579
  return;
80527
80580
  }
80581
+ // Scrolling nothing, the box keeps out of reach only the items past what
80582
+ // it shows. A list standing whole on the window's screen is one the window
80583
+ // covers at its next evaluation: an item added in this commit is drawn on
80584
+ // the resize it causes (see onGeometryChangeRef), after this effect.
80585
+ const listEl = getListEl();
80586
+ if (!listEl) {
80587
+ return;
80588
+ }
80589
+ const screen = readWindowScreen(scrollerElResolved, horizontal);
80590
+ const listRect = listEl.getBoundingClientRect();
80591
+ const listFrom = horizontal ? listRect.left : listRect.top;
80592
+ const listTo = horizontal ? listRect.right : listRect.bottom;
80593
+ // A pixel of slack, like canScroll.
80594
+ if (listFrom > screen.from - 1 && listTo < screen.to + 1) {
80595
+ return;
80596
+ }
80528
80597
  doneRef.current = true;
80529
80598
  const {
80530
80599
  start,
@@ -81143,28 +81212,40 @@ const findItemElementAt = (listEl, index) => {
81143
81212
  }
81144
81213
  return null;
81145
81214
  };
81146
- // What the render window is placed and sized on (see evaluateWindow), along
81147
- // the scroll axis and in viewport coordinates: the size of the screen, the part
81148
- // of the list on it (the band), and the items drawn, in the order they stand.
81149
- // The screen is the viewport of the box that scrolls the list — what "150%"
81150
- // is a percentage of. A box that does not scroll — a list given no height, as
81151
- // tall as its items — shows no more of them than the browser does: its
81152
- // viewport is cut by the browser's, or a budget in % would draw items to fill
81153
- // a screen they make grow. A list off screen is met at its edge nearest to the
81154
- // screen, where it comes in.
81155
- const readWindowGeometry = (scrollerEl, listEl, horizontal) => {
81215
+ // The screen the render window is framed on, along the scroll axis and in
81216
+ // viewport coordinates: the viewport of the box that scrolls the list — what
81217
+ // "150%" is a percentage of. A box that does not scroll — a list given no
81218
+ // height, as tall as its items — shows no more of them than the browser does:
81219
+ // its viewport is cut by the browser's, or a budget in % would draw items to
81220
+ // fill a screen they make grow.
81221
+ const readWindowScreen = (scrollerEl, horizontal) => {
81156
81222
  const scrollerRect = getScrollerViewportRect(scrollerEl);
81157
- let screenFrom = horizontal ? scrollerRect.left : scrollerRect.top;
81158
- let screenTo = horizontal ? scrollerRect.right : scrollerRect.bottom;
81223
+ let from = horizontal ? scrollerRect.left : scrollerRect.top;
81224
+ let to = horizontal ? scrollerRect.right : scrollerRect.bottom;
81159
81225
  if (!canScrollerScroll(scrollerEl, horizontal ? "x" : "y")) {
81160
81226
  const browserSize = horizontal ? document.documentElement.clientWidth : document.documentElement.clientHeight;
81161
- if (screenFrom < 0) {
81162
- screenFrom = 0;
81227
+ if (from < 0) {
81228
+ from = 0;
81163
81229
  }
81164
- if (screenTo > browserSize) {
81165
- screenTo = browserSize;
81230
+ if (to > browserSize) {
81231
+ to = browserSize;
81166
81232
  }
81167
81233
  }
81234
+ return {
81235
+ from,
81236
+ to
81237
+ };
81238
+ };
81239
+ // What the render window is placed and sized on (see evaluateWindow), along
81240
+ // the scroll axis and in viewport coordinates: the size of the screen (see
81241
+ // readWindowScreen), the part of the list on it (the band), and the items
81242
+ // drawn, in the order they stand. A list off screen is met at its edge nearest
81243
+ // to the screen, where it comes in.
81244
+ const readWindowGeometry = (scrollerEl, listEl, horizontal) => {
81245
+ const {
81246
+ from: screenFrom,
81247
+ to: screenTo
81248
+ } = readWindowScreen(scrollerEl, horizontal);
81168
81249
  const screenSize = screenTo - screenFrom;
81169
81250
  if (screenSize < 1) {
81170
81251
  return null;
@@ -85079,39 +85160,29 @@ const BadgeListShrinkWrap = ({
85079
85160
  const registry = useBadgeRegistry(children, max !== undefined || fallback !== undefined);
85080
85161
  const measureRef = useRef();
85081
85162
  const visibleRef = useRef();
85163
+
85164
+ // In a closed popup the rows have no size to read: the list is measured when
85165
+ // it comes on screen, before the frame is painted.
85166
+ useDisplayedLayoutEffect(visibleRef, () => {
85167
+ shrinkWrapToWidestRow(visibleRef.current, measureRef.current);
85168
+ }, [children]);
85082
85169
  useLayoutEffect(() => {
85083
85170
  const measureEl = measureRef.current;
85084
85171
  const visibleEl = visibleRef.current;
85085
85172
  if (!measureEl || !visibleEl) {
85086
85173
  return undefined;
85087
85174
  }
85088
- let observer;
85089
- let rafId;
85090
- const measure = () => {
85091
- visibleEl.style.width = "";
85092
- // Clone the already-rendered DOM nodes instead of letting React/Preact
85093
- // render the children a second time into the ghost: re-rendering would
85094
- // instantiate Badge/Badge.Button a second time, double-registering their
85095
- // controllers (and any other mount side effect) under the same id.
85096
- measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
85097
- const optimalWidth = measureWidestChildRow(measureEl);
85098
- if (optimalWidth !== null) {
85099
- visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
85100
- }
85101
- };
85102
-
85103
- // A single badge is a single row, and a single row is already the widest
85104
- // one: there is nothing to narrow, and no width the list could be given
85105
- // that would change that.
85175
+ // A single row whatever room it is given: nothing a resize could narrow.
85106
85176
  if (visibleEl.children.length < 2) {
85107
- visibleEl.style.width = "";
85108
- measureEl.replaceChildren();
85109
85177
  return undefined;
85110
85178
  }
85111
- measure();
85179
+ let observer;
85180
+ let rafId;
85112
85181
  const onResize = () => {
85113
85182
  cancelAnimationFrame(rafId);
85114
- rafId = requestAnimationFrame(measure);
85183
+ rafId = requestAnimationFrame(() => {
85184
+ shrinkWrapToWidestRow(visibleEl, measureEl);
85185
+ });
85115
85186
  };
85116
85187
  const outerParent = measureEl.parentElement?.parentElement;
85117
85188
  if (outerParent) {
@@ -85157,6 +85228,40 @@ const BadgeListShrinkWrap = ({
85157
85228
  })
85158
85229
  );
85159
85230
  };
85231
+
85232
+ // Narrows the list to its widest row, read from clones of its badges in the
85233
+ // ghost.
85234
+ //
85235
+ // What holds the list may have sized itself around the width it had before —
85236
+ // a callout pins its body to the width it measured — so a change is announced
85237
+ // with a bubbling navi_size_change for it to measure again, in the same frame.
85238
+ const shrinkWrapToWidestRow = (visibleEl, measureEl) => {
85239
+ // Not drawn (back in a closed popup): no row to read. The width is kept for
85240
+ // the next opening, which measures again before painting.
85241
+ if (visibleEl.getClientRects().length === 0) {
85242
+ return;
85243
+ }
85244
+ const widthBefore = visibleEl.style.width;
85245
+ visibleEl.style.width = "";
85246
+ if (visibleEl.children.length < 2) {
85247
+ // A single badge is a single row, and a single row is already the widest
85248
+ // one: there is nothing to narrow.
85249
+ measureEl.replaceChildren();
85250
+ } else {
85251
+ // Clone the already-rendered DOM nodes instead of letting React/Preact
85252
+ // render the children a second time into the ghost: re-rendering would
85253
+ // instantiate Badge/Badge.Button a second time, double-registering their
85254
+ // controllers (and any other mount side effect) under the same id.
85255
+ measureEl.replaceChildren(...Array.from(visibleEl.children, child => child.cloneNode(true)));
85256
+ const optimalWidth = measureWidestChildRow(measureEl);
85257
+ if (optimalWidth !== null) {
85258
+ visibleEl.style.width = `${Math.ceil(optimalWidth)}px`;
85259
+ }
85260
+ }
85261
+ if (visibleEl.style.width !== widthBefore) {
85262
+ dispatchPublicCustomEvent(visibleEl, "navi_size_change");
85263
+ }
85264
+ };
85160
85265
  const BadgeListMaxLines = ({
85161
85266
  fallback,
85162
85267
  children,