@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.
- package/dist/dev/jsenv_navi.js +150 -45
- package/dist/dev/jsenv_navi.js.map +8 -8
- package/dist/jsenv_navi.js +134 -45
- package/dist/jsenv_navi.js.map +8 -8
- package/docs/control_value.md +3 -2
- package/docs/drag_interactions.md +21 -4
- package/docs/popup_open.md +10 -9
- package/package.json +2 -2
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -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
|
|
23439
|
-
//
|
|
23440
|
-
//
|
|
23441
|
-
//
|
|
23442
|
-
|
|
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
|
|
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
|
-
//
|
|
81147
|
-
//
|
|
81148
|
-
//
|
|
81149
|
-
//
|
|
81150
|
-
//
|
|
81151
|
-
//
|
|
81152
|
-
|
|
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
|
|
81158
|
-
let
|
|
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 (
|
|
81162
|
-
|
|
81227
|
+
if (from < 0) {
|
|
81228
|
+
from = 0;
|
|
81163
81229
|
}
|
|
81164
|
-
if (
|
|
81165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85179
|
+
let observer;
|
|
85180
|
+
let rafId;
|
|
85112
85181
|
const onResize = () => {
|
|
85113
85182
|
cancelAnimationFrame(rafId);
|
|
85114
|
-
rafId = requestAnimationFrame(
|
|
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,
|