@jsenv/navi 0.29.236 → 0.29.238

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.
@@ -6984,7 +6984,17 @@ const openCallout = (
6984
6984
  `append callout into ${getElementSignature(calloutContainer)}`,
6985
6985
  );
6986
6986
  }
6987
+ let checkCalloutInsertionImpact;
6988
+ {
6989
+ checkCalloutInsertionImpact = startCalloutInsertionImpactCheck(
6990
+ calloutContainer,
6991
+ anchorElement,
6992
+ );
6993
+ }
6987
6994
  calloutContainer.appendChild(calloutElement);
6995
+ if (checkCalloutInsertionImpact) {
6996
+ checkCalloutInsertionImpact();
6997
+ }
6988
6998
  calloutElement.showPopover();
6989
6999
  addTeardown(() => {
6990
7000
  calloutElement.remove();
@@ -7550,6 +7560,54 @@ const canContainCallout = (element) => {
7550
7560
  return true;
7551
7561
  };
7552
7562
 
7563
+ // A callout is a popover with no layout box of its own, so putting it in the
7564
+ // tree must move nothing. Anything that does move means an app rule counting
7565
+ // element children — :last-child, :nth-child, :only-child, :empty, > * + * —
7566
+ // took the callout for one of them and changed subject. Invisible from the
7567
+ // app, where the rule and the markup both read fine.
7568
+ // Two boxes rather than every child of the container: a callout on a list row
7569
+ // would otherwise measure the whole list on each press.
7570
+ const startCalloutInsertionImpactCheck = (calloutContainer, anchorElement) => {
7571
+ if (!anchorElement) {
7572
+ return null;
7573
+ }
7574
+ const anchorBoxBefore = anchorElement.getBoundingClientRect();
7575
+ const containerBoxBefore = calloutContainer.getBoundingClientRect();
7576
+ return () => {
7577
+ const anchorBoxAfter = anchorElement.getBoundingClientRect();
7578
+ const containerBoxAfter = calloutContainer.getBoundingClientRect();
7579
+ let what;
7580
+ let boxBefore;
7581
+ let boxAfter;
7582
+ if (!isSameBox(anchorBoxBefore, anchorBoxAfter)) {
7583
+ what = "moved it";
7584
+ boxBefore = anchorBoxBefore;
7585
+ boxAfter = anchorBoxAfter;
7586
+ } else if (!isSameBox(containerBoxBefore, containerBoxAfter)) {
7587
+ what = `moved ${getElementSignature(calloutContainer)} around it`;
7588
+ boxBefore = containerBoxBefore;
7589
+ boxAfter = containerBoxAfter;
7590
+ } else {
7591
+ return;
7592
+ }
7593
+ console.warn(
7594
+ `opening a callout on ${getElementSignature(anchorElement)} ${what}: ${formatBox(boxBefore)} -> ${formatBox(boxAfter)}.
7595
+ A callout draws nothing in the flow, but while it is open it is an element child of ${getElementSignature(calloutContainer)}: a rule counting children there (:last-child, :nth-child, :only-child, :empty, > * + *) counts it too and matches something else than what it aims at.
7596
+ Say which children the rule counts: :nth-last-child(1 of S) / :nth-child(An+B of S), where S names yours (a class, or :not([navi-out-of-flow]) — the marker navi's out-of-flow elements carry). A sibling rule takes the marker between its compounds: > *:not([navi-out-of-flow]) + *:not([navi-out-of-flow]).
7597
+ See @jsenv/navi docs/css_architecture.md.`,
7598
+ );
7599
+ };
7600
+ };
7601
+ const isSameBox = (a, b) => {
7602
+ return (
7603
+ a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
7604
+ );
7605
+ };
7606
+ const formatBox = ({ x, y, width, height }) => {
7607
+ const round = (value) => Math.round(value * 100) / 100;
7608
+ return `${round(width)}x${round(height)} at ${round(x)},${round(y)}`;
7609
+ };
7610
+
7553
7611
  const escapeHtml = (string) => {
7554
7612
  return string
7555
7613
  .replace(/&/g, "&")
@@ -18950,6 +19008,20 @@ const resolveSpacingSize = (size, element, property = "padding") => {
18950
19008
  }
18951
19009
  return normalizeStyle(SIZE_MAP[size] || size, property, "js", element);
18952
19010
  };
19011
+ // Whether a spacing is nothing at all, answered without resolving it. A
19012
+ // viewport or container length resolves through a computed-style read, and
19013
+ // asked during a render that read forces the browser to compute the style of
19014
+ // a tree still being built, stylesheets still being adopted. Zero is a fact
19015
+ // about the number written, whatever the unit; a scale keyword is never zero.
19016
+ const isZeroSpacingSize = (size) => {
19017
+ if (typeof size === "number") {
19018
+ return size === 0;
19019
+ }
19020
+ if (typeof size !== "string") {
19021
+ return false;
19022
+ }
19023
+ return parseFloat(size) === 0;
19024
+ };
18953
19025
 
18954
19026
  const COLOR_KEYWORD_MAP = {
18955
19027
  primary: "var(--navi-color-primary)",
@@ -39989,47 +40061,35 @@ const LoadingIndicatorFluid = ({
39989
40061
  const [containerWidth, setContainerWidth] = useState(0);
39990
40062
  const [containerHeight, setContainerHeight] = useState(0);
39991
40063
  const [containerRadius, setContainerRadius] = useState(0);
40064
+
40065
+ // Sized by the observer alone, first frame included: an element gets an
40066
+ // observation for the size it has when first observed, delivered after the
40067
+ // frame's own layout and before its paint. Measuring here instead would
40068
+ // force a layout in the middle of the commit, on a tree the commit is still
40069
+ // writing to, and the frame would lay it out again. Inside the callback style
40070
+ // and layout are clean, so the radius read there is a lookup, not a recalc.
39992
40071
  useLayoutEffect(() => {
39993
40072
  const indicatorEl = ref.current;
39994
40073
  if (!indicatorEl) {
39995
40074
  return null;
39996
40075
  }
39997
- const {
39998
- width,
39999
- height
40000
- } = indicatorEl.getBoundingClientRect();
40001
- setContainerWidth(width);
40002
- setContainerHeight(height);
40003
- if (radius === undefined || radius === "inherit") {
40004
- const parentEl = indicatorEl.parentElement;
40005
- // Prefer the inline style (always available, even before layout is computed).
40006
- // Fall back to computed longhands — getComputedStyle shorthand may return ""
40007
- // if the browser hasn't resolved the layout yet.
40008
- const radius = parentEl.style.borderRadius || parentEl.style.borderTopLeftRadius || getComputedStyle(parentEl).borderTopLeftRadius || "0px";
40009
- setContainerRadius(radius);
40010
- }
40011
- let animationFrameId = null;
40012
- // Create a resize observer to detect changes in the container's dimensions
40013
- const resizeObserver = new ResizeObserver(entries => {
40014
- // Use requestAnimationFrame to debounce updates
40015
- if (animationFrameId) {
40016
- cancelAnimationFrame(animationFrameId);
40017
- }
40018
- animationFrameId = requestAnimationFrame(() => {
40019
- const [containerEntry] = entries;
40020
- const {
40021
- width,
40022
- height
40023
- } = containerEntry.contentRect;
40024
- setContainerWidth(width);
40025
- setContainerHeight(height);
40026
- });
40076
+ const resizeObserver = new ResizeObserver(([containerEntry]) => {
40077
+ const {
40078
+ width,
40079
+ height
40080
+ } = containerEntry.contentRect;
40081
+ setContainerWidth(width);
40082
+ setContainerHeight(height);
40083
+ if (radius === undefined || radius === "inherit") {
40084
+ const parentEl = indicatorEl.parentElement;
40085
+ // The inline style says the radius as the caller wrote it; the
40086
+ // computed longhand otherwise, the shorthand computing to "" when the
40087
+ // four corners differ.
40088
+ setContainerRadius(parentEl.style.borderRadius || parentEl.style.borderTopLeftRadius || getComputedStyle(parentEl).borderTopLeftRadius || "0px");
40089
+ }
40027
40090
  });
40028
40091
  resizeObserver.observe(indicatorEl);
40029
40092
  return () => {
40030
- if (animationFrameId) {
40031
- cancelAnimationFrame(animationFrameId);
40032
- }
40033
40093
  resizeObserver.disconnect();
40034
40094
  };
40035
40095
  }, []);
@@ -44371,6 +44431,13 @@ const measureDrawing = ({
44371
44431
  * Measured rather than declared: a bar's size comes from a prop, a theme
44372
44432
  * variable, its own content or the device's notch, and only the used value
44373
44433
  * knows all four.
44434
+ *
44435
+ * Written once per task, after every bar has measured. The variable resizes
44436
+ * an ancestor of the bars — the scroll container takes its padding from it —
44437
+ * so a write landing between two measurements turns the second one into a
44438
+ * style recalc and a layout of the whole page. Sizes are queued, and the queue
44439
+ * lands in a microtask: still before the browser paints, so the content is
44440
+ * never seen under a bar, and after every read of the commit that queued them.
44374
44441
  */
44375
44442
 
44376
44443
  // Several bars can share an edge — during a page transition the outgoing and
@@ -44384,10 +44451,14 @@ const sizeMapByArea = new Map();
44384
44451
  // size again — the ones for the smaller bars, and the second half of a page
44385
44452
  // transition.
44386
44453
  const writtenValueByArea = new Map();
44454
+ // Sizes said and not yet written. One queue for both callers below: a bar
44455
+ // saying its size twice before the write keeps the last word, whoever said it.
44456
+ const pendingSizeMapByArea = new Map();
44457
+ let flushMicrotaskQueued = false;
44458
+ let flushFrame = null;
44387
44459
 
44388
- // A size that must land before the next paint: a render changed the bar, so
44389
- // the room it takes is given back in that same commit and the content is never
44390
- // painted under it.
44460
+ // A size a render produced: a prop, the children or a theme variable changed
44461
+ // the bar, and the room it takes is given back before that commit is painted.
44391
44462
  /**
44392
44463
  * @param {"top"|"bottom"|"left"|"right"} area
44393
44464
  * @param {Element} barElement - Which bar this size belongs to.
@@ -44395,41 +44466,47 @@ const writtenValueByArea = new Map();
44395
44466
  * content.
44396
44467
  */
44397
44468
  const setFixedBarSpace = (area, barElement, size) => {
44398
- dropPendingSize(area, barElement);
44399
- storeSize(area, barElement, size);
44400
- writeSpace(area);
44401
- };
44402
-
44403
- // A size nothing asked for, coming from a ResizeObserver. Writing the variable
44404
- // resizes an ANCESTOR of the bars — the scroll container takes its padding
44405
- // from it — and mutating layout from inside a resize callback is what makes
44406
- // the browser report "ResizeObserver loop completed with undelivered
44407
- // notifications". So the write waits for the frame that resize produced.
44408
- // Queued here rather than deferred by each bar on its own, so the bars sharing
44409
- // an edge resolve to a single write instead of one per bar.
44469
+ queueSize(area, barElement, size);
44470
+ if (flushMicrotaskQueued) {
44471
+ return;
44472
+ }
44473
+ flushMicrotaskQueued = true;
44474
+ queueMicrotask(() => {
44475
+ flushMicrotaskQueued = false;
44476
+ flushPendingSizes();
44477
+ });
44478
+ };
44479
+
44480
+ // A size nothing asked for, coming from a ResizeObserver. Mutating layout from
44481
+ // inside a resize callback is what makes the browser report "ResizeObserver
44482
+ // loop completed with undelivered notifications", so this write waits for the
44483
+ // frame that resize produced.
44410
44484
  /**
44411
44485
  * @param {"top"|"bottom"|"left"|"right"} area
44412
44486
  * @param {Element} barElement - Which bar this size belongs to.
44413
44487
  * @param {number} size - In px.
44414
44488
  */
44415
44489
  const requestFixedBarSpace = (area, barElement, size) => {
44490
+ queueSize(area, barElement, size);
44491
+ if (flushFrame !== null) {
44492
+ return;
44493
+ }
44494
+ flushFrame = requestAnimationFrame(() => {
44495
+ flushFrame = null;
44496
+ flushPendingSizes();
44497
+ });
44498
+ };
44499
+
44500
+ const queueSize = (area, barElement, size) => {
44416
44501
  let pendingSizeMap = pendingSizeMapByArea.get(area);
44417
44502
  if (!pendingSizeMap) {
44418
44503
  pendingSizeMap = new Map();
44419
44504
  pendingSizeMapByArea.set(area, pendingSizeMap);
44420
44505
  }
44421
44506
  pendingSizeMap.set(barElement, size);
44422
- if (flushFrame !== null) {
44423
- return;
44424
- }
44425
- flushFrame = requestAnimationFrame(flushPendingSizes);
44426
44507
  };
44427
44508
 
44428
- const pendingSizeMapByArea = new Map();
44429
- let flushFrame = null;
44430
-
44431
44509
  const flushPendingSizes = () => {
44432
- flushFrame = null;
44433
44510
  for (const [area, pendingSizeMap] of pendingSizeMapByArea) {
44434
44511
  for (const [barElement, size] of pendingSizeMap) {
44435
44512
  storeSize(area, barElement, size);
@@ -44439,20 +44516,6 @@ const flushPendingSizes = () => {
44439
44516
  pendingSizeMapByArea.clear();
44440
44517
  };
44441
44518
 
44442
- // What the bar itself just said wins over what its observer had queued about
44443
- // it: a bar unmounting gives its room back, and a size queued for it before
44444
- // that must not put it back.
44445
- const dropPendingSize = (area, barElement) => {
44446
- const pendingSizeMap = pendingSizeMapByArea.get(area);
44447
- if (!pendingSizeMap) {
44448
- return;
44449
- }
44450
- pendingSizeMap.delete(barElement);
44451
- if (pendingSizeMap.size === 0) {
44452
- pendingSizeMapByArea.delete(area);
44453
- }
44454
- };
44455
-
44456
44519
  const storeSize = (area, barElement, size) => {
44457
44520
  let sizeMap = sizeMapByArea.get(area);
44458
44521
  if (!sizeMap) {
@@ -59662,7 +59725,7 @@ const useDialogProps = props => {
59662
59725
  bottom: false,
59663
59726
  left: false
59664
59727
  };
59665
- if (resolveSpacingSize(marginWithContainer) === 0) {
59728
+ if (isZeroSpacingSize(marginWithContainer)) {
59666
59729
  const {
59667
59730
  y,
59668
59731
  x