@jsenv/navi 0.29.417 → 0.29.419
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/README.md +1 -1
- package/dist/dev/jsenv_navi.js +347 -83
- package/dist/dev/jsenv_navi.js.map +12 -12
- package/dist/jsenv_navi.js +345 -81
- package/dist/jsenv_navi.js.map +12 -12
- package/docs/AI_INSTRUCTIONS.md +177 -258
- package/docs/actions.md +164 -251
- package/docs/autofocus.md +66 -83
- package/docs/control_object.md +117 -214
- package/docs/control_value.md +121 -185
- package/docs/create_and_edit.md +159 -254
- package/docs/css_architecture.md +196 -260
- package/docs/data_states.md +41 -44
- package/docs/dialog_shape.md +91 -134
- package/docs/drag_interactions.md +291 -454
- package/docs/drag_to_travel.md +152 -748
- package/docs/dynamic_import.md +119 -136
- package/docs/error_handling.md +75 -127
- package/docs/field_validation.md +49 -89
- package/docs/form_changed.md +61 -78
- package/docs/group.md +141 -0
- package/docs/i18n.md +31 -76
- package/docs/interactions.md +170 -326
- package/docs/list_action.md +36 -51
- package/docs/list_refresh.md +73 -115
- package/docs/mobile_touch.md +56 -62
- package/docs/navigation.md +377 -723
- package/docs/network_policy.md +61 -75
- package/docs/pan_zoom.md +62 -103
- package/docs/popup_backdrop.md +128 -168
- package/docs/popup_lift.md +179 -268
- package/docs/popup_open.md +380 -651
- package/docs/resource.md +228 -408
- package/docs/route_transitions.md +333 -473
- package/docs/safe_area.md +102 -89
- package/docs/scroll.md +175 -317
- package/docs/state_binding.md +40 -84
- package/docs/testid.md +18 -23
- package/docs/typography.md +111 -141
- package/docs/view_transitions.md +106 -104
- package/docs/z_index.md +61 -109
- package/package.json +2 -2
- package/docs/badge_list.md +0 -83
- package/docs/control_group.md +0 -168
- package/docs/mobile_layout_pitfalls.md +0 -145
package/dist/dev/jsenv_navi.js
CHANGED
|
@@ -11254,6 +11254,28 @@ const suspendScrollRecording = () => {
|
|
|
11254
11254
|
};
|
|
11255
11255
|
};
|
|
11256
11256
|
|
|
11257
|
+
// A list scrolling the document that opens on a row puts the document where
|
|
11258
|
+
// that row is, by measuring it (see placeWhereHeld in list.jsx). The offset kept
|
|
11259
|
+
// for the url is pixels of the page as it was drawn — and a list draws a window
|
|
11260
|
+
// of its rows and holds the room of the others with fillers of an estimated
|
|
11261
|
+
// height, so the same pixels now fall on other rows. Put back after the list
|
|
11262
|
+
// has placed itself, they would undo it, in the very picture a route transition
|
|
11263
|
+
// takes. So the list holds the document while it is placing itself, and the
|
|
11264
|
+
// url's offset is not put back meanwhile. Counted: two lists can share one
|
|
11265
|
+
// document.
|
|
11266
|
+
let documentHoldCount = 0;
|
|
11267
|
+
const holdDocumentScroll = () => {
|
|
11268
|
+
documentHoldCount++;
|
|
11269
|
+
let released = false;
|
|
11270
|
+
return () => {
|
|
11271
|
+
if (released) {
|
|
11272
|
+
return;
|
|
11273
|
+
}
|
|
11274
|
+
released = true;
|
|
11275
|
+
documentHoldCount--;
|
|
11276
|
+
};
|
|
11277
|
+
};
|
|
11278
|
+
|
|
11257
11279
|
let installed = false;
|
|
11258
11280
|
const installScrollRestoration = () => {
|
|
11259
11281
|
if (installed) {
|
|
@@ -11288,6 +11310,9 @@ const installScrollRestoration = () => {
|
|
|
11288
11310
|
if (positionOnLoad && (positionOnLoad.x || positionOnLoad.y)) {
|
|
11289
11311
|
const stopListening = observeRouteRender(() => {
|
|
11290
11312
|
stopListening();
|
|
11313
|
+
if (documentHoldCount) {
|
|
11314
|
+
return;
|
|
11315
|
+
}
|
|
11291
11316
|
scrollTo(positionOnLoad);
|
|
11292
11317
|
});
|
|
11293
11318
|
}
|
|
@@ -11302,6 +11327,11 @@ const restoreScrollPosition = (url) => {
|
|
|
11302
11327
|
if (!position) {
|
|
11303
11328
|
return false;
|
|
11304
11329
|
}
|
|
11330
|
+
if (documentHoldCount) {
|
|
11331
|
+
// There is a place to go back to, and the list holding the document is
|
|
11332
|
+
// putting it back (see holdDocumentScroll).
|
|
11333
|
+
return true;
|
|
11334
|
+
}
|
|
11305
11335
|
scrollTo(position);
|
|
11306
11336
|
return true;
|
|
11307
11337
|
};
|
|
@@ -15033,7 +15063,11 @@ const createOpenController = (
|
|
|
15033
15063
|
const applyChange = () => {
|
|
15034
15064
|
// Recorded with the change itself: what the DOM shows is what a render
|
|
15035
15065
|
// landing between the ask and the picture must draw (see `openedInDom`).
|
|
15036
|
-
|
|
15066
|
+
// An opening records it inside the change, once its content is built
|
|
15067
|
+
// (see open() below).
|
|
15068
|
+
if (!opened) {
|
|
15069
|
+
controller.openedInDom = false;
|
|
15070
|
+
}
|
|
15037
15071
|
change();
|
|
15038
15072
|
};
|
|
15039
15073
|
const { transitionChange } = controller;
|
|
@@ -15313,15 +15347,17 @@ const createOpenController = (
|
|
|
15313
15347
|
controller.mountContent?.();
|
|
15314
15348
|
// Only now — after the content has been built, before openEffect
|
|
15315
15349
|
// shows it. Dialog/Popover recompute aria-expanded and navi-hidden
|
|
15316
|
-
// from
|
|
15350
|
+
// from `openedInDom` on every render, and mountContent above renders
|
|
15317
15351
|
// synchronously: flipping it any earlier commits an already-open DOM
|
|
15318
15352
|
// (aria-expanded "true", navi-hidden gone) before openEffect has run
|
|
15319
15353
|
// a single statement, so the "closed" frame it pins to transition
|
|
15320
15354
|
// from is in fact the open one and the entrance animation has
|
|
15321
|
-
// nothing to play
|
|
15322
|
-
//
|
|
15323
|
-
//
|
|
15324
|
-
// popup_content_mount.js and
|
|
15355
|
+
// nothing to play — on every first opening that builds its content.
|
|
15356
|
+
// It also gives the content it just built the opening it is
|
|
15357
|
+
// documented to observe — mounted while the popup reads as closed,
|
|
15358
|
+
// told it opened right after (see popup_content_mount.js and
|
|
15359
|
+
// use_displayed_layout_effect.js).
|
|
15360
|
+
controller.openedInDom = true;
|
|
15325
15361
|
controller.opened = true;
|
|
15326
15362
|
// Which press it opened during, so the release of that press is not
|
|
15327
15363
|
// read as somebody dismissing it (see openedDuringThisPress).
|
|
@@ -15730,7 +15766,14 @@ const useOpenPropsEffectOnOpenController = (
|
|
|
15730
15766
|
lastRunOpenRef.current = open;
|
|
15731
15767
|
|
|
15732
15768
|
if (isFirstRun) {
|
|
15733
|
-
|
|
15769
|
+
// "interaction" is said even over what the caller holds reading open: a
|
|
15770
|
+
// popup built by the request that opens it — a dialog standing in until
|
|
15771
|
+
// its signal said open, see DialogStandIn in dialog.jsx — mounts BECAUSE
|
|
15772
|
+
// of that opening.
|
|
15773
|
+
const mountOpenReason =
|
|
15774
|
+
open && defaultOpen === "interaction"
|
|
15775
|
+
? "interaction"
|
|
15776
|
+
: open || defaultOpen;
|
|
15734
15777
|
if (mountOpenReason) {
|
|
15735
15778
|
// Whether this popup being open is something that just happened, or
|
|
15736
15779
|
// something that was already true when the page appeared. "interaction"
|
|
@@ -28663,6 +28706,18 @@ const useAsyncData = (promiseOrAction, {
|
|
|
28663
28706
|
|
|
28664
28707
|
// ─── useAction ────────────────────────────────────────────────────────────────
|
|
28665
28708
|
|
|
28709
|
+
// subscribe() calls back at once with the value the signal holds; what these
|
|
28710
|
+
// subscriptions are for is what changes after.
|
|
28711
|
+
const subscribeToChanges = (signal, onChange) => {
|
|
28712
|
+
let initial = true;
|
|
28713
|
+
return signal.subscribe(value => {
|
|
28714
|
+
if (initial) {
|
|
28715
|
+
initial = false;
|
|
28716
|
+
return;
|
|
28717
|
+
}
|
|
28718
|
+
onChange(value);
|
|
28719
|
+
});
|
|
28720
|
+
};
|
|
28666
28721
|
const LoadingContext = createContext(null);
|
|
28667
28722
|
const actionPendingPromiseWeakMap = new WeakMap();
|
|
28668
28723
|
const dismissedActionWeakSet = new WeakSet();
|
|
@@ -28707,6 +28762,15 @@ const useActionAsyncData = (action, {
|
|
|
28707
28762
|
const runningState = action.runningStateSignal.peek();
|
|
28708
28763
|
const [, setTick] = useState(0);
|
|
28709
28764
|
const routePage = useContext(RoutePageContext);
|
|
28765
|
+
// What this render read of the action, for the subscriptions below to tell
|
|
28766
|
+
// a change that came between the render and them from none at all.
|
|
28767
|
+
const readRef = useRef(null);
|
|
28768
|
+
readRef.current = {
|
|
28769
|
+
runningState,
|
|
28770
|
+
data: action.dataSignal.peek(),
|
|
28771
|
+
params: action.paramsSignal.peek(),
|
|
28772
|
+
settling: action.paramsSettlingSignal.peek()
|
|
28773
|
+
};
|
|
28710
28774
|
useEffect(() => {
|
|
28711
28775
|
let stopOwingRender = null;
|
|
28712
28776
|
const rerender = () => {
|
|
@@ -28734,7 +28798,12 @@ const useActionAsyncData = (action, {
|
|
|
28734
28798
|
}
|
|
28735
28799
|
setTick(n => n + 1);
|
|
28736
28800
|
};
|
|
28737
|
-
|
|
28801
|
+
// A run under way is a new try: an error dismissed before it is not the
|
|
28802
|
+
// one it may end on.
|
|
28803
|
+
if (action.runningStateSignal.peek() === RUNNING) {
|
|
28804
|
+
dismissedActionWeakSet.delete(action);
|
|
28805
|
+
}
|
|
28806
|
+
const unsubscribeFromRunningState = subscribeToChanges(action.runningStateSignal, state => {
|
|
28738
28807
|
if (state === RUNNING) {
|
|
28739
28808
|
dismissedActionWeakSet.delete(action);
|
|
28740
28809
|
}
|
|
@@ -28745,38 +28814,24 @@ const useActionAsyncData = (action, {
|
|
|
28745
28814
|
// (a PUT upserting an item that a GET_MANY list already holds) changes the
|
|
28746
28815
|
// data while this action stays COMPLETED. Subscribing here re-renders
|
|
28747
28816
|
// through the same controlled path as the run state, instead of `.value`.
|
|
28748
|
-
|
|
28749
|
-
const unsubscribeFromData = action.dataSignal.subscribe(() => {
|
|
28750
|
-
if (dataNotificationIsInitial) {
|
|
28751
|
-
// subscribe() calls back synchronously with the current value
|
|
28752
|
-
dataNotificationIsInitial = false;
|
|
28753
|
-
return;
|
|
28754
|
-
}
|
|
28755
|
-
rerender();
|
|
28756
|
-
});
|
|
28817
|
+
const unsubscribeFromData = subscribeToChanges(action.dataSignal, rerender);
|
|
28757
28818
|
// The params say WHICH question this is, and this hook reads them: to know
|
|
28758
28819
|
// there is nothing to ask for, and to start the run it owns. A binding
|
|
28759
28820
|
// retargeting from no question to a question — a filter chosen, a first
|
|
28760
28821
|
// character typed — is announced by nothing above: a fresh target is IDLE
|
|
28761
28822
|
// holding no data, the very state the hook already sees.
|
|
28762
|
-
|
|
28763
|
-
const unsubscribeFromParams = action.paramsSignal.subscribe(() => {
|
|
28764
|
-
if (paramsNotificationIsInitial) {
|
|
28765
|
-
paramsNotificationIsInitial = false;
|
|
28766
|
-
return;
|
|
28767
|
-
}
|
|
28768
|
-
rerender();
|
|
28769
|
-
});
|
|
28823
|
+
const unsubscribeFromParams = subscribeToChanges(action.paramsSignal, rerender);
|
|
28770
28824
|
// A debounced binding waits before it retargets, so nothing above changes
|
|
28771
28825
|
// while the delay runs — but what is on screen is already out of date.
|
|
28772
|
-
|
|
28773
|
-
|
|
28774
|
-
|
|
28775
|
-
|
|
28776
|
-
|
|
28777
|
-
|
|
28826
|
+
const unsubscribeFromParamsSettling = subscribeToChanges(action.paramsSettlingSignal, rerender);
|
|
28827
|
+
// Subscribed once the browser has painted, so the action may have moved
|
|
28828
|
+
// since the render: a run that very render started may be over already,
|
|
28829
|
+
// and nothing would say so again. What the render read, against what is
|
|
28830
|
+
// there now — and nothing moved, nothing to draw again.
|
|
28831
|
+
const read = readRef.current;
|
|
28832
|
+
if (action.runningStateSignal.peek() !== read.runningState || action.dataSignal.peek() !== read.data || action.paramsSignal.peek() !== read.params || action.paramsSettlingSignal.peek() !== read.settling) {
|
|
28778
28833
|
rerender();
|
|
28779
|
-
}
|
|
28834
|
+
}
|
|
28780
28835
|
return () => {
|
|
28781
28836
|
unsubscribeFromRunningState();
|
|
28782
28837
|
unsubscribeFromData();
|
|
@@ -40354,6 +40409,17 @@ const useDisplayedLayoutEffect = (ref, callback, deps) => {
|
|
|
40354
40409
|
decisionDeferredToRef.current = popupMounting;
|
|
40355
40410
|
return;
|
|
40356
40411
|
}
|
|
40412
|
+
if (
|
|
40413
|
+
(el.tagName === "DIALOG" || el.hasAttribute("popover")) &&
|
|
40414
|
+
!isAncestorOpen(el)
|
|
40415
|
+
) {
|
|
40416
|
+
// Itself a closed <dialog> or [popover]: display:none by the UA's own
|
|
40417
|
+
// rules, which the markup says without asking the layout — asked, every
|
|
40418
|
+
// closed popup mounting would force a style recalculation, and a list
|
|
40419
|
+
// whose rows each hold a few mounts hundreds of them. The observer
|
|
40420
|
+
// below fires as for any element the ancestor hides.
|
|
40421
|
+
return;
|
|
40422
|
+
}
|
|
40357
40423
|
if (!isDisplayedDespiteClosedAncestor(el, ancestor)) {
|
|
40358
40424
|
// Ancestor is closed and took this element off screen with it — skip
|
|
40359
40425
|
// now; the observeAncestorOpenState call below will fire once it
|
|
@@ -52612,9 +52678,9 @@ const FixedBar = ({
|
|
|
52612
52678
|
* A page that overflows horizontally is a bug wherever it happens, and on
|
|
52613
52679
|
* Chrome Android it is a catastrophic one: the layout viewport inflates to the
|
|
52614
52680
|
* content and `position: fixed` centering goes with it (see
|
|
52615
|
-
* docs/
|
|
52616
|
-
* `overflow-x: clip`, and it works — at the price of making the
|
|
52617
|
-
* invisible: nothing sticks out anymore, so nothing says a fixed width, a
|
|
52681
|
+
* docs/safe_area.md#a-document-wider-than-the-screen). The remedy there is a
|
|
52682
|
+
* wrapper in `overflow-x: clip`, and it works — at the price of making the
|
|
52683
|
+
* cause invisible: nothing sticks out anymore, so nothing says a fixed width, a
|
|
52618
52684
|
* `min-width` or an unbreakable string is still oversized. This puts the
|
|
52619
52685
|
* signal back, without giving up the net.
|
|
52620
52686
|
*
|
|
@@ -65627,7 +65693,9 @@ const css$E = /* css */`
|
|
|
65627
65693
|
* @param {"always"|"idle"|"from-first-open"|"while-opened"} [props.mount] - When
|
|
65628
65694
|
* `children` are built and thrown away (see popup_content_mount.js).
|
|
65629
65695
|
* `"from-first-open"` (the default) builds them on the first open and keeps
|
|
65630
|
-
* them afterwards
|
|
65696
|
+
* them afterwards — and the dialog itself is built then too: until its first
|
|
65697
|
+
* request (an open, a press on what opens it, its signal saying open) it is
|
|
65698
|
+
* a bare `<dialog>` holding its id (see docs/popup_open.md). `"always"` builds them right away, for content something
|
|
65631
65699
|
* depends on while the popup is still closed: a value read off it, fields a
|
|
65632
65700
|
* surrounding form collects on submit, a size measured from outside.
|
|
65633
65701
|
* `"idle"` builds them in a browser idle moment after load — "always" minus
|
|
@@ -65648,11 +65716,122 @@ const Dialog = props => {
|
|
|
65648
65716
|
...props
|
|
65649
65717
|
});
|
|
65650
65718
|
}
|
|
65651
|
-
return jsx(
|
|
65719
|
+
return jsx(DialogStandIn, {
|
|
65652
65720
|
...props
|
|
65653
65721
|
});
|
|
65654
65722
|
};
|
|
65655
65723
|
|
|
65724
|
+
// A dialog nobody has asked for yet stands in as its bare <dialog>. That
|
|
65725
|
+
// element is all that reaches a closed dialog before its first opening: a
|
|
65726
|
+
// command finds it by id (`commandfor`) and asks it to open, a press on what
|
|
65727
|
+
// opens it announces itself on it (navi_open_press), --navi-toggle reads its
|
|
65728
|
+
// aria-expanded. The dialog proper — its open controller, its box, its focus
|
|
65729
|
+
// and pseudo-class wiring — is built at the first request: an open, a press
|
|
65730
|
+
// about to open it, its signal or its `open` saying open. A list whose rows
|
|
65731
|
+
// each hold a few dialogs mounts hundreds of them, and would pay for every one
|
|
65732
|
+
// at every mount, for openings that mostly never come. Built once, the dialog
|
|
65733
|
+
// stays: the next opening finds it there.
|
|
65734
|
+
const DialogStandIn = props => {
|
|
65735
|
+
const {
|
|
65736
|
+
ref,
|
|
65737
|
+
id,
|
|
65738
|
+
signal,
|
|
65739
|
+
value,
|
|
65740
|
+
open
|
|
65741
|
+
} = props;
|
|
65742
|
+
const ownRef = useRef();
|
|
65743
|
+
const dialogRef = ref || ownRef;
|
|
65744
|
+
// Why the dialog proper exists, once it does; never undone.
|
|
65745
|
+
const builtRef = useRef(null);
|
|
65746
|
+
const mountedRef = useRef(false);
|
|
65747
|
+
const isMount = !mountedRef.current;
|
|
65748
|
+
mountedRef.current = true;
|
|
65749
|
+
// Its own reading of the signal: the dialog of one card among many, told
|
|
65750
|
+
// apart by `value`, follows whether IT is the one open rather than every
|
|
65751
|
+
// write the others cause.
|
|
65752
|
+
const openedSignal = useMemo(() => signal ? computed(() => readOpened(signal.value, value)) : null, [signal, value]);
|
|
65753
|
+
const openedBySignal = openedSignal ? openedSignal.value : false;
|
|
65754
|
+
if (builtRef.current === null) {
|
|
65755
|
+
if (isMount && (needsDialogFromMount(props) || open || openedBySignal)) {
|
|
65756
|
+
builtRef.current = "mount";
|
|
65757
|
+
} else if (open || openedBySignal) {
|
|
65758
|
+
// Asked open while this stood in: the opening is happening now, and it
|
|
65759
|
+
// plays like any other (see defaultOpen "interaction").
|
|
65760
|
+
builtRef.current = "interaction";
|
|
65761
|
+
}
|
|
65762
|
+
}
|
|
65763
|
+
const [, setBuildCount] = useState(0);
|
|
65764
|
+
const warmRef = useRef(null);
|
|
65765
|
+
const build = reason => {
|
|
65766
|
+
if (builtRef.current !== null) {
|
|
65767
|
+
return;
|
|
65768
|
+
}
|
|
65769
|
+
builtRef.current = reason;
|
|
65770
|
+
flushSyncRendering(() => {
|
|
65771
|
+
setBuildCount(count => count + 1);
|
|
65772
|
+
});
|
|
65773
|
+
};
|
|
65774
|
+
const builtReason = builtRef.current;
|
|
65775
|
+
if (builtReason !== null) {
|
|
65776
|
+
return jsx(UncontrolledDialog, {
|
|
65777
|
+
...props,
|
|
65778
|
+
ref: dialogRef,
|
|
65779
|
+
defaultOpen: builtReason === "interaction" ? "interaction" : props.defaultOpen
|
|
65780
|
+
});
|
|
65781
|
+
}
|
|
65782
|
+
return jsx("dialog", {
|
|
65783
|
+
ref: dialogRef,
|
|
65784
|
+
id: id,
|
|
65785
|
+
className: "navi_dialog"
|
|
65786
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
65787
|
+
,
|
|
65788
|
+
"navi-hidden": ""
|
|
65789
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
65790
|
+
,
|
|
65791
|
+
"navi-out-of-flow": "",
|
|
65792
|
+
"aria-expanded": "false",
|
|
65793
|
+
"data-testid": props["data-testid"]
|
|
65794
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
65795
|
+
,
|
|
65796
|
+
onnavi_request_open: e => {
|
|
65797
|
+
// Built in this same event, and asked again: the opening is the one
|
|
65798
|
+
// the dialog proper would have had — its source, its anchor, its value.
|
|
65799
|
+
build("request");
|
|
65800
|
+
const opened = dispatchCustomEvent(dialogRef.current, "navi_request_open", e.detail);
|
|
65801
|
+
if (!opened) {
|
|
65802
|
+
e.preventDefault();
|
|
65803
|
+
}
|
|
65804
|
+
}
|
|
65805
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
65806
|
+
,
|
|
65807
|
+
onnavi_open_press: () => {
|
|
65808
|
+
// What the opening will read is asked for at once, as the dialog
|
|
65809
|
+
// proper does (see open_controller.js); the dialog itself two frames
|
|
65810
|
+
// later, so the pressed trigger is painted first — the click that
|
|
65811
|
+
// follows opens it built, or builds it itself when it comes sooner.
|
|
65812
|
+
if (signal) {
|
|
65813
|
+
preloadState(signal, readOpenValue(value));
|
|
65814
|
+
}
|
|
65815
|
+
if (warmRef.current) {
|
|
65816
|
+
return;
|
|
65817
|
+
}
|
|
65818
|
+
warmRef.current = requestFrameAfterNext(() => {
|
|
65819
|
+
warmRef.current = null;
|
|
65820
|
+
build("press");
|
|
65821
|
+
});
|
|
65822
|
+
}
|
|
65823
|
+
});
|
|
65824
|
+
};
|
|
65825
|
+
// What needs the dialog proper from the start: an opening at mount, content
|
|
65826
|
+
// built ahead of any opening, a history entry to read, an anchor whose hover
|
|
65827
|
+
// or focus builds the content ahead of the click.
|
|
65828
|
+
const needsDialogFromMount = props => {
|
|
65829
|
+
if (props.defaultOpen || props.navState || props.anchor !== undefined) {
|
|
65830
|
+
return true;
|
|
65831
|
+
}
|
|
65832
|
+
return props.mount !== undefined && props.mount !== "from-first-open";
|
|
65833
|
+
};
|
|
65834
|
+
|
|
65656
65835
|
// No openController passed: this Dialog is used declaratively (e.g. driven
|
|
65657
65836
|
// by --navi-toggle/--navi-open/--navi-close commands, the `open` prop, or
|
|
65658
65837
|
// `defaultOpen`) rather than owned by a parent component.
|
|
@@ -72626,9 +72805,17 @@ const ListUI = props => {
|
|
|
72626
72805
|
// why not an effect), and the floor of 30 is its: it protects the scrolling
|
|
72627
72806
|
// window, not a picture nobody has seen yet.
|
|
72628
72807
|
const {
|
|
72629
|
-
initial:
|
|
72808
|
+
initial: initialRenderBudgetProp,
|
|
72630
72809
|
after: renderBudgetAfterPaint
|
|
72631
72810
|
} = resolveRenderBudget(renderBudgetProp);
|
|
72811
|
+
// Opening on a position that says how many rows were on screen from its row
|
|
72812
|
+
// on (the way back to where the list was, see reportPosition): that is what
|
|
72813
|
+
// the first picture shows, whatever `initial` guessed — with the row above
|
|
72814
|
+
// when it stood partly in view. The rest waits for the paint like any
|
|
72815
|
+
// `initial`: a page coming back in a route transition builds it inside the
|
|
72816
|
+
// update callback, and every row below the fold delays the movement.
|
|
72817
|
+
const openingPosition = scrolled ?? defaultScrolled;
|
|
72818
|
+
const initialRenderBudget = openingPosition && typeof openingPosition === "object" && typeof openingPosition.visibleCount === "number" ? openingPosition.visibleCount + (openingPosition.offset > 0 ? 1 : 0) : initialRenderBudgetProp;
|
|
72632
72819
|
let renderBudget = renderBudgetAfterPaint;
|
|
72633
72820
|
if (renderBudget < 30 && !renderBudgetSkipCheck) {
|
|
72634
72821
|
console.warn(`List: renderBudget=${renderBudget} is too low. A renderBudget below 30 is not supported: on large screens or when the list grows, items outside the window would appear as blank space instead of rendered content. Use a value of at least 30, or omit the prop to use the default (${RENDER_BUDGET_DEFAULT}).`);
|
|
@@ -73091,11 +73278,23 @@ const useListScrollSync = ({
|
|
|
73091
73278
|
horizontal
|
|
73092
73279
|
});
|
|
73093
73280
|
};
|
|
73281
|
+
|
|
73282
|
+
// How many rows the window keeps above the row the list is held at. Around it
|
|
73283
|
+
// for the scrolling to come, from the first paint on; before, the window is
|
|
73284
|
+
// the first picture's, and a picture of the list opening on a row shows that
|
|
73285
|
+
// row and what is below it — with the one above when a remembered position
|
|
73286
|
+
// says it stood partly in view.
|
|
73287
|
+
const rowsAboveOpening = (openAt, windowSize) => {
|
|
73288
|
+
if (renderBudget !== renderBudgetSteady) {
|
|
73289
|
+
return openAt && typeof openAt === "object" && openAt.offset > 0 ? 1 : 0;
|
|
73290
|
+
}
|
|
73291
|
+
return Math.floor(windowSize / 2);
|
|
73292
|
+
};
|
|
73094
73293
|
const [renderWindow, setRenderWindow] = useState(() => {
|
|
73095
73294
|
// Opening somewhere else than the beginning starts by framing there: the
|
|
73096
73295
|
// rows the list will draw are the rows it will ask for.
|
|
73097
73296
|
const openAt = scrolled ?? defaultScrolled;
|
|
73098
|
-
const start = typeof openAt === "number" ? openAt -
|
|
73297
|
+
const start = typeof openAt === "number" ? openAt - rowsAboveOpening(openAt, renderBudget) : 0;
|
|
73099
73298
|
const startClamped = start < 0 ? 0 : start;
|
|
73100
73299
|
return {
|
|
73101
73300
|
start: startClamped,
|
|
@@ -73169,8 +73368,12 @@ const useListScrollSync = ({
|
|
|
73169
73368
|
// Held somewhere it has not reached yet: what the window frames right now
|
|
73170
73369
|
// is not what it will frame, so nothing should be fetched for it.
|
|
73171
73370
|
listRows.holdPending = scrolledWanted !== "start" && scrolledWanted !== undefined;
|
|
73371
|
+
// Zero until the runs have rendered — they count their rows as they do,
|
|
73372
|
+
// after the list — which is not a collection fitting in the window: it is
|
|
73373
|
+
// one whose end is not known yet, and the window then frames the row the
|
|
73374
|
+
// list opens on, already in the first commit.
|
|
73172
73375
|
const total = listRows.totalSignal.peek();
|
|
73173
|
-
if (total <= renderBudget) {
|
|
73376
|
+
if (total > 0 && total <= renderBudget) {
|
|
73174
73377
|
// The whole collection is what the list draws: wherever in it the list is
|
|
73175
73378
|
// held, the window is already its place. Nowhere to move to means nothing
|
|
73176
73379
|
// to wait for — a hold left standing here is a list that never asks for
|
|
@@ -73178,21 +73381,21 @@ const useListScrollSync = ({
|
|
|
73178
73381
|
listRows.holdPending = false;
|
|
73179
73382
|
return;
|
|
73180
73383
|
}
|
|
73181
|
-
const
|
|
73384
|
+
const above = rowsAboveOpening(scrolledWanted, renderBudget);
|
|
73182
73385
|
let wantedStart = null;
|
|
73183
73386
|
if (scrolledWanted === "end") {
|
|
73184
73387
|
wantedStart = total - renderBudget;
|
|
73185
73388
|
} else if (typeof scrolledWanted === "number") {
|
|
73186
|
-
wantedStart = scrolledWanted -
|
|
73389
|
+
wantedStart = scrolledWanted - above;
|
|
73187
73390
|
} else if (scrolledWanted && scrolledWanted.id !== undefined) {
|
|
73188
73391
|
const rowIndex = listRows.locateRow(scrolledWanted.id);
|
|
73189
73392
|
if (rowIndex !== null) {
|
|
73190
|
-
wantedStart = rowIndex -
|
|
73393
|
+
wantedStart = rowIndex - above;
|
|
73191
73394
|
} else if (typeof scrolledWanted.index === "number") {
|
|
73192
73395
|
// The row has not come back yet, but where it stood is known: near
|
|
73193
73396
|
// enough to frame, and to put the scrollbar roughly where it will end
|
|
73194
73397
|
// up rather than at the top.
|
|
73195
|
-
wantedStart = scrolledWanted.index -
|
|
73398
|
+
wantedStart = scrolledWanted.index - above;
|
|
73196
73399
|
}
|
|
73197
73400
|
}
|
|
73198
73401
|
if (wantedStart === null) {
|
|
@@ -73201,12 +73404,12 @@ const useListScrollSync = ({
|
|
|
73201
73404
|
// back — the run asks for it by name (see useRequestMissing).
|
|
73202
73405
|
return;
|
|
73203
73406
|
}
|
|
73407
|
+
if (total > 0 && wantedStart + renderBudget > total) {
|
|
73408
|
+
wantedStart = total - renderBudget;
|
|
73409
|
+
}
|
|
73204
73410
|
if (wantedStart < 0) {
|
|
73205
73411
|
wantedStart = 0;
|
|
73206
73412
|
}
|
|
73207
|
-
if (wantedStart + renderBudget > total) {
|
|
73208
|
-
wantedStart = total - renderBudget;
|
|
73209
|
-
}
|
|
73210
73413
|
const {
|
|
73211
73414
|
start,
|
|
73212
73415
|
end
|
|
@@ -73543,8 +73746,8 @@ const useListScrollSync = ({
|
|
|
73543
73746
|
end
|
|
73544
73747
|
} = renderWindowRef.current;
|
|
73545
73748
|
if (rowIndex < start || rowIndex >= end) {
|
|
73546
|
-
const
|
|
73547
|
-
const wantedStart = rowIndex -
|
|
73749
|
+
const above = rowsAboveOpening(scrolledWanted, end - start);
|
|
73750
|
+
const wantedStart = rowIndex - above < 0 ? 0 : rowIndex - above;
|
|
73548
73751
|
updateRenderWindow(wantedStart, wantedStart + (end - start), `opening on row ${scrolledWanted.id}`);
|
|
73549
73752
|
return;
|
|
73550
73753
|
}
|
|
@@ -73590,6 +73793,15 @@ const useListScrollSync = ({
|
|
|
73590
73793
|
}
|
|
73591
73794
|
};
|
|
73592
73795
|
useLayoutEffect(placeWhereHeld);
|
|
73796
|
+
// Held on a row of a list scrolling the document: where the document goes is
|
|
73797
|
+
// this list's to say, measured on the row, not the url's offset in pixels
|
|
73798
|
+
// (see holdDocumentScroll).
|
|
73799
|
+
useLayoutEffect(() => {
|
|
73800
|
+
if (!heldSomewhere || getScroller() !== document.scrollingElement) {
|
|
73801
|
+
return undefined;
|
|
73802
|
+
}
|
|
73803
|
+
return holdDocumentScroll();
|
|
73804
|
+
}, [heldSomewhere, scrollerElResolved]);
|
|
73593
73805
|
// What to do when the list's own geometry moves under it, kept fresh for the
|
|
73594
73806
|
// observer below (which is installed once).
|
|
73595
73807
|
const onGeometryChangeRef = useRef(null);
|
|
@@ -73632,31 +73844,40 @@ const useListScrollSync = ({
|
|
|
73632
73844
|
// Where the list was at the last thing that moved it. Kept whether anyone
|
|
73633
73845
|
// asked for it or not: it is what a resize needs to put things back.
|
|
73634
73846
|
const positionRef = useRef(null);
|
|
73847
|
+
const captureListAnchor = countVisible => captureScrollAnchor({
|
|
73848
|
+
scrollerEl: getScroller(),
|
|
73849
|
+
listEl: getListEl(),
|
|
73850
|
+
items: listRows.visibleItemsSignal.peek(),
|
|
73851
|
+
horizontal,
|
|
73852
|
+
countVisible
|
|
73853
|
+
});
|
|
73854
|
+
// Said from where the row lands on its own, and with `visibleCount`: the
|
|
73855
|
+
// rows on screen from that one on, which is what a list given this position
|
|
73856
|
+
// back draws first (see ListUI).
|
|
73857
|
+
const toScrolledPosition = position => {
|
|
73858
|
+
const rowEl = findRowElement(getListEl(), position.id);
|
|
73859
|
+
return {
|
|
73860
|
+
id: position.id,
|
|
73861
|
+
index: position.index,
|
|
73862
|
+
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal),
|
|
73863
|
+
visibleCount: position.visibleCount
|
|
73864
|
+
};
|
|
73865
|
+
};
|
|
73635
73866
|
const reportPosition = () => {
|
|
73636
73867
|
if (!ref.current) {
|
|
73637
73868
|
return;
|
|
73638
73869
|
}
|
|
73639
|
-
const
|
|
73640
|
-
|
|
73641
|
-
|
|
73642
|
-
items: listRows.visibleItemsSignal.peek(),
|
|
73643
|
-
horizontal
|
|
73644
|
-
});
|
|
73870
|
+
const remember = rememberScrollRef.current;
|
|
73871
|
+
const onScrolledChange = onScrolledChangeRef.current;
|
|
73872
|
+
const position = captureListAnchor(remember || Boolean(onScrolledChange));
|
|
73645
73873
|
if (!position) {
|
|
73646
73874
|
return;
|
|
73647
73875
|
}
|
|
73648
73876
|
positionRef.current = position;
|
|
73649
|
-
const remember = rememberScrollRef.current;
|
|
73650
|
-
const onScrolledChange = onScrolledChangeRef.current;
|
|
73651
73877
|
if (!remember && !onScrolledChange) {
|
|
73652
73878
|
return;
|
|
73653
73879
|
}
|
|
73654
|
-
const
|
|
73655
|
-
const scrolledNow = {
|
|
73656
|
-
id: position.id,
|
|
73657
|
-
index: position.index,
|
|
73658
|
-
offset: position.offset - getRowScrollInset(getScroller(), rowEl, horizontal)
|
|
73659
|
-
};
|
|
73880
|
+
const scrolledNow = toScrolledPosition(position);
|
|
73660
73881
|
if (remember) {
|
|
73661
73882
|
rememberScrollerPosition(listId, scrolledNow);
|
|
73662
73883
|
}
|
|
@@ -73664,6 +73885,20 @@ const useListScrollSync = ({
|
|
|
73664
73885
|
onScrolledChange(scrolledNow);
|
|
73665
73886
|
}
|
|
73666
73887
|
};
|
|
73888
|
+
// Nothing scrolled, yet what is on screen changed: the list has just been
|
|
73889
|
+
// laid out, its rows have arrived, one of them grew. A list read where it
|
|
73890
|
+
// opened would otherwise come back with nothing kept — opening again as a
|
|
73891
|
+
// fresh arrival does, all of its window drawn in the first commit instead of
|
|
73892
|
+
// the rows that were on screen. Kept, and told to nobody: nobody scrolled.
|
|
73893
|
+
const rememberPosition = () => {
|
|
73894
|
+
if (!rememberScrollRef.current || !ref.current) {
|
|
73895
|
+
return;
|
|
73896
|
+
}
|
|
73897
|
+
const position = captureListAnchor(true);
|
|
73898
|
+
if (position) {
|
|
73899
|
+
rememberScrollerPosition(listId, toScrolledPosition(position));
|
|
73900
|
+
}
|
|
73901
|
+
};
|
|
73667
73902
|
// Leaving: with its page, or alone (see forgetScrollerUnlessPageLeft).
|
|
73668
73903
|
useLayoutEffect(() => {
|
|
73669
73904
|
return () => {
|
|
@@ -73689,6 +73924,7 @@ const useListScrollSync = ({
|
|
|
73689
73924
|
// list which has outgrown a bounded ancestor stops holding on to the
|
|
73690
73925
|
// page.
|
|
73691
73926
|
resolveScroller();
|
|
73927
|
+
rememberPosition();
|
|
73692
73928
|
// Two things resize here, and they call for opposite answers. The LIST
|
|
73693
73929
|
// growing is its own content settling: only a list holding itself
|
|
73694
73930
|
// somewhere cares (the end it aims at has moved), and a list the user is
|
|
@@ -74477,12 +74713,15 @@ const getRowName = rowEl => rowEl.getAttribute("navi-list-item-real");
|
|
|
74477
74713
|
// the list is rebuilt around it. Read off the rows' own boxes, not by
|
|
74478
74714
|
// hit-testing the screen: a scrolling list takes its rows out of hit-testing
|
|
74479
74715
|
// (see the navi-scrolling rule in the css above), and the scroll event is
|
|
74480
|
-
// precisely when this is asked.
|
|
74716
|
+
// precisely when this is asked. `countVisible` adds how many rows are on
|
|
74717
|
+
// screen from that one on: what a list coming back there has to draw before
|
|
74718
|
+
// anything else (see the first paint in ListUI).
|
|
74481
74719
|
const captureScrollAnchor = ({
|
|
74482
74720
|
scrollerEl,
|
|
74483
74721
|
listEl,
|
|
74484
74722
|
items,
|
|
74485
|
-
horizontal
|
|
74723
|
+
horizontal,
|
|
74724
|
+
countVisible
|
|
74486
74725
|
}) => {
|
|
74487
74726
|
if (!scrollerEl || !listEl) {
|
|
74488
74727
|
return null;
|
|
@@ -74518,6 +74757,9 @@ const captureScrollAnchor = ({
|
|
|
74518
74757
|
offset
|
|
74519
74758
|
};
|
|
74520
74759
|
if (offset >= 0) {
|
|
74760
|
+
if (countVisible) {
|
|
74761
|
+
anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
|
|
74762
|
+
}
|
|
74521
74763
|
return anchor;
|
|
74522
74764
|
}
|
|
74523
74765
|
// The row under the top of the view starts above it. Good enough to hold
|
|
@@ -74526,10 +74768,26 @@ const captureScrollAnchor = ({
|
|
|
74526
74768
|
// top" reads, and can be drawn. Keep looking; this one is the fallback.
|
|
74527
74769
|
if (!fallbackAnchor) {
|
|
74528
74770
|
fallbackAnchor = anchor;
|
|
74771
|
+
if (countVisible) {
|
|
74772
|
+
anchor.visibleCount = countRowsStartingBefore(rowEls, i, range.to, horizontal);
|
|
74773
|
+
}
|
|
74529
74774
|
}
|
|
74530
74775
|
}
|
|
74531
74776
|
return fallbackAnchor;
|
|
74532
74777
|
};
|
|
74778
|
+
// How many of the rows from `fromIndex` on start before `to` along the scroll
|
|
74779
|
+
// axis — the ones on screen, when `to` is where the view ends.
|
|
74780
|
+
const countRowsStartingBefore = (rowEls, fromIndex, to, horizontal) => {
|
|
74781
|
+
let index = fromIndex;
|
|
74782
|
+
while (index < rowEls.length) {
|
|
74783
|
+
const rect = rowEls[index].getBoundingClientRect();
|
|
74784
|
+
if ((horizontal ? rect.left : rect.top) >= to) {
|
|
74785
|
+
break;
|
|
74786
|
+
}
|
|
74787
|
+
index++;
|
|
74788
|
+
}
|
|
74789
|
+
return index - fromIndex;
|
|
74790
|
+
};
|
|
74533
74791
|
// The part of the list that is on screen, along the scrolling axis. Both edges
|
|
74534
74792
|
// matter: the scroller may be larger than the list (scroller="parent") as well
|
|
74535
74793
|
// as smaller (the list scrolls inside its own box).
|
|
@@ -76897,7 +77155,7 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76897
77155
|
* are skeletons, the others hold their room. `0` says the list is already
|
|
76898
77156
|
* known to be empty: the empty `fallback` shows right away rather than an
|
|
76899
77157
|
* empty frame.
|
|
76900
|
-
* @param {"start"|"end"|number|{id: string, offset?: number}} [props.defaultScrolled="start"]
|
|
77158
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.defaultScrolled="start"]
|
|
76901
77159
|
* Where the list opens, after which the user owns the scroll — unless it is
|
|
76902
77160
|
* being come back to (see `scrollResetOnNavigation`). `"end"` is a
|
|
76903
77161
|
* thread read backwards — the last rows are the ones to show, and the ones
|
|
@@ -76909,8 +77167,10 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76909
77167
|
* the screen it was saved on. `offset: 0` is where a `scrollIntoView()` puts
|
|
76910
77168
|
* it — in front of the fixed bar the scroller gives room for, below the
|
|
76911
77169
|
* sticky header and the group label the row lives under — so nothing of that
|
|
76912
|
-
* room has to be restated as a number by whoever asks.
|
|
76913
|
-
*
|
|
77170
|
+
* room has to be restated as a number by whoever asks. The `index` and
|
|
77171
|
+
* `visibleCount` it also hands out say where to aim before the row is found,
|
|
77172
|
+
* and how many rows to draw before the first paint (see `renderBudget`).
|
|
77173
|
+
* @param {"start"|"end"|number|{id: string, index?: number, offset?: number, visibleCount?: number}} [props.scrolled]
|
|
76914
77174
|
* The same, but held: the list goes back there every time this changes, even
|
|
76915
77175
|
* after the user has scrolled — the caller owns where the list is (see
|
|
76916
77176
|
* `defaultScrolled` for the uncontrolled form, and `open`/`defaultOpen`
|
|
@@ -76921,12 +77181,12 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76921
77181
|
* In every form the list holds itself there while it is still finding out
|
|
76922
77182
|
* how many rows there are and how tall one is, and lets go the moment the
|
|
76923
77183
|
* user reaches for the list.
|
|
76924
|
-
* @param {(scrolled: {id: string, index: number, offset: number}) => void} [props.onScrolledChange]
|
|
77184
|
+
* @param {(scrolled: {id: string, index: number, offset: number, visibleCount: number}) => void} [props.onScrolledChange]
|
|
76925
77185
|
* Where the list is, as the user scrolls: the row at the top of the view and
|
|
76926
77186
|
* how far below the place a row lands on its own (see `defaultScrolled`) it
|
|
76927
|
-
* starts. Keep it
|
|
76928
|
-
* later through `scrolled`/`defaultScrolled` — an index
|
|
76929
|
-
* rows get inserted while a list is being read.
|
|
77187
|
+
* starts, and how many rows are on screen from that one on. Keep it whole to
|
|
77188
|
+
* come back to it later through `scrolled`/`defaultScrolled` — an index alone
|
|
77189
|
+
* would not do, since rows get inserted while a list is being read.
|
|
76930
77190
|
* @param {number|{initial: number, after: number}} [props.renderBudget=100]
|
|
76931
77191
|
* How many rows of a `<List.Items>` run are in the DOM at once: the render
|
|
76932
77192
|
* window, which slides as the user scrolls while fillers hold the room of
|
|
@@ -76938,11 +77198,15 @@ const ListResolved = /*#__PURE__*/createComponentResolver([ListFirstResolver, Li
|
|
|
76938
77198
|
* once, with room for that lookahead — the list warns below 30, and when a
|
|
76939
77199
|
* budget leaves fewer than two rows beyond the screen.
|
|
76940
77200
|
*
|
|
76941
|
-
* `{ initial, after }` for a list
|
|
76942
|
-
*
|
|
76943
|
-
*
|
|
76944
|
-
*
|
|
76945
|
-
*
|
|
77201
|
+
* `{ initial, after }` for a list whose first picture is taken as it is
|
|
77202
|
+
* built — drawn in the click that opens a popup, or in the update callback
|
|
77203
|
+
* of a route transition bringing its page back: `initial` rows in the commit
|
|
77204
|
+
* the browser paints first — about what a phone screen shows — counted from
|
|
77205
|
+
* the row the list opens on, and `after` from the paint on, around it, where
|
|
77206
|
+
* the floor of 30 applies. Opening on a position that says how many rows were
|
|
77207
|
+
* on screen (`visibleCount`, see `onScrolledChange`) draws those first,
|
|
77208
|
+
* whatever `initial` says. The runs ask their source for `after` rows from
|
|
77209
|
+
* the start, so the first picture costs no second request.
|
|
76946
77210
|
* @param {number} [props.virtualItemSize]
|
|
76947
77211
|
* The size of one row along the scroll axis, in px, when every row has the
|
|
76948
77212
|
* same: what the fillers are sized with and what a scroll position is
|
|
@@ -77412,10 +77676,10 @@ const useBadgeRegistry = (children, enabled) => {
|
|
|
77412
77676
|
* list renders nothing at all. In a <Picker> this is the only placeholder
|
|
77413
77677
|
* the user gets — a picker given a `ui` does not draw its own — so pass the
|
|
77414
77678
|
* placeholder text: `fallback="Select skills…"`. Plain text is the right
|
|
77415
|
-
* choice there: it reads at the picker's size, in the
|
|
77416
|
-
*
|
|
77417
|
-
* transparent <Badge> matches the slot to the pixel instead, at the
|
|
77418
|
-
* badge-sized text. See docs/
|
|
77679
|
+
* choice there: it reads at the picker's size, in the normal text color (a
|
|
77680
|
+
* caller's `ui` is never greyed as a placeholder), and the box stays the same
|
|
77681
|
+
* height. A transparent <Badge> matches the slot to the pixel instead, at the
|
|
77682
|
+
* price of badge-sized text. See docs/typography.md#badgelist.
|
|
77419
77683
|
* @param {boolean} [shrinkWrap]
|
|
77420
77684
|
* Narrows the list down to its widest row so the last row isn't ragged.
|
|
77421
77685
|
* Defaults to true inside a <Picker> — the trigger draws a border around the
|
|
@@ -79616,8 +79880,8 @@ const PickerFirstResolver = props => {
|
|
|
79616
79880
|
* filter", "anywhere"), so its empty look is the caller's. A `ui` must
|
|
79617
79881
|
* therefore say on its own that the picker is empty, and take the same room
|
|
79618
79882
|
* empty as it does filled. A <BadgeList> gets both from its `fallback` — the
|
|
79619
|
-
* placeholder text, as plain text, see docs/
|
|
79620
|
-
* nothing in the slot at all.
|
|
79883
|
+
* placeholder text, as plain text, see docs/typography.md#badgelist.
|
|
79884
|
+
* `"default"` draws nothing in the slot at all.
|
|
79621
79885
|
*
|
|
79622
79886
|
* Under `variant="icon"` there is no value to draw, so this IS the trigger:
|
|
79623
79887
|
* left out it is the icon the right slot would have shown (`rightSlotIcon`,
|