Skip to content

Commit 8aabe67

Browse files
committed
work
1 parent 6e59002 commit 8aabe67

19 files changed

Lines changed: 384 additions & 325 deletions

File tree

packages/frontend/navi/dist/jsenv_navi.js

Lines changed: 180 additions & 178 deletions
Large diffs are not rendered by default.

packages/frontend/navi/dist/jsenv_navi.js.map

Lines changed: 17 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/navi/docs/create_and_edit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ holds; only its mechanisms change.
479479
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
480480
| two routes, `/new` and `/:id/edit` | one dialog, two modes — hence the value it is opened ON ([popup_open.md](./popup_open.md#opening-it-on-something)) |
481481
| the draft lives in the `searchParams` | it lives in the dialog, and dies with it |
482-
| cancelling = leaving the screen, which is thrown away | `unmountWhenClosed` (+ a `key` on the form), same effect |
482+
| cancelling = leaving the screen, which is thrown away | `mount="while-opened"` (+ a `key` on the form), same effect |
483483
| `RouteTravel` between the two screens | nothing: a dialog has no neighbour |
484484
| `onLoad` + `pristineKey`: the resource arrives after | it is already in hand — the dialog is opened FROM the list |
485485

packages/frontend/navi/docs/popup_open.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ which one — so the press says it, with its own value:
169169

170170
<Dialog
171171
id="radar-dialog"
172-
unmountWhenClosed
172+
mount="while-opened"
173173
onOpen={(e) => {
174174
editedRadarIdSignal.value = e.detail.value; // undefined = création
175175
}}
@@ -200,7 +200,7 @@ The order is the whole point, and it is a guarantee, not a coincidence:
200200

201201
```
202202
onOpen(openEvent) ← the subject is decided here
203-
children mounted ← unmountWhenClosed rebuilds them from scratch, on that subject
203+
children mounted ← mount="while-opened" rebuilds them from scratch, on that subject
204204
positioned, shown
205205
```
206206

@@ -221,7 +221,7 @@ knowing why:
221221
`onnavi_command` receives the whole command string and its value, but it runs
222222
**after** the opening: whatever it writes lands on a popup that is already open.
223223
That works only as long as nothing has read the state yet — which
224-
`unmountWhenClosed` makes a real race rather than a theoretical one.
224+
`mount="while-opened"` makes a real race rather than a theoretical one.
225225

226226
A `navi_request_open` listener added on the element is the request itself, ahead
227227
of the popup acting on it — but it is ordered against the popup's own handler by
@@ -609,23 +609,28 @@ A closed popup builds nothing: `children` are mounted on the first open, and
609609
stay mounted afterwards — a reopened popup finds its scroll position and its
610610
half-typed form where it left them.
611611

612-
Two props move that line:
612+
"Closed" is two states, not one — never opened yet, and closed again after an
613+
opening — so the `mount` prop answers both at once:
613614

614-
| prop | effect |
615-
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
616-
| `mountWhenClosed` | build `children` right away — for content something depends on before any opening (a value read off it, fields a surrounding form submits, a size measured from outside) |
617-
| `unmountWhenClosed` | throw `children` away once the popup has finished closing — for content whose fresh state is its initial state |
615+
| `mount` | before the first open | after a close |
616+
| ----------------------------- | --------------------- | ------------- |
617+
| `"always"` | mounted | mounted |
618+
| `"from-first-open"` (default) | not mounted | mounted |
619+
| `"while-opened"` | not mounted | not mounted |
618620

619-
`unmountWhenClosed` is what an uncontrolled field seeded from a `defaultValue`
621+
`"always"` is for content something depends on before any opening: a value read
622+
off it, fields a surrounding form submits, a size measured from outside.
623+
624+
`"while-opened"` is what an uncontrolled field seeded from a `defaultValue`
620625
needs: without it, a popup reopened after the underlying value changed still
621626
shows what it showed at closing time.
622627

623628
```jsx
624-
<Dialog ref={dialogRef} unmountWhenClosed>
629+
<Dialog ref={dialogRef} mount="while-opened">
625630
<Textarea defaultValue={note.text} />
626631
</Dialog>
627632
```
628633

629634
The content is dropped only once the exit transition is over, so the popup never
630635
plays it on a blank surface; a popup reopened while it was leaving keeps the
631-
content that opening just asked for. `mountWhenClosed` wins if both are set.
636+
content that opening just asked for.

packages/frontend/navi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jsenv/navi",
3-
"version": "0.29.149",
3+
"version": "0.29.150",
44
"type": "module",
55
"description": "Library of components including navigation to create frontend applications",
66
"repository": {

packages/frontend/navi/src/control/demos/21_expandable_demo.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,16 +298,16 @@
298298
</Expandable>
299299
</div>
300300
<div className="demo-box">
301-
<p className="demo-caption">mountWhenClosed</p>
302-
<Expandable ui="Summary" mountWhenClosed animation>
301+
<p className="demo-caption">mount="always"</p>
302+
<Expandable ui="Summary" mount="always" animation>
303303
<div className="demo-content">
304304
<MountClock />
305305
</div>
306306
</Expandable>
307307
</div>
308308
<div className="demo-box">
309-
<p className="demo-caption">unmountWhenClosed</p>
310-
<Expandable ui="Summary" unmountWhenClosed animation>
309+
<p className="demo-caption">mount="while-opened"</p>
310+
<Expandable ui="Summary" mount="while-opened" animation>
311311
<div className="demo-content">
312312
<MountClock />
313313
</div>
@@ -681,10 +681,10 @@
681681
</div>
682682
<div className="demo-box">
683683
<p className="demo-caption">
684-
mountWhenClosed: the closed content sizes the height — last
684+
mount="always": the closed content sizes the height — last
685685
resort, the content may be expensive to render
686686
</p>
687-
<Expandable layout="column" animation mountWhenClosed>
687+
<Expandable layout="column" animation mount="always">
688688
<Expandable.UI style="background: #eceff1; padding: 4px">
689689
More
690690
</Expandable.UI>

packages/frontend/navi/src/control/expandable/expandable.jsx

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,9 @@
3838
* events.
3939
*
4040
* Content is not built until the first expansion and stays built afterwards —
41-
* same policy, same prop names as popups (see popup_content_mount.js):
42-
* `mountWhenClosed` builds it right away, `unmountWhenClosed` throws it away
43-
* once the collapse settles (so a closing animation still plays on real
44-
* content).
41+
* same policy, same `mount` prop as popups (see popup_content_mount.js):
42+
* `"always"` builds it right away, `"while-opened"` throws it away once the
43+
* collapse settles (so a closing animation still plays on real content).
4544
*
4645
* The animation is a REVEAL, not a resize: the expandable's own footprint
4746
* grows/shrinks progressively (the content's grid track interpolates
@@ -73,6 +72,7 @@ import { ActionRenderer } from "../../action/action_renderer.jsx";
7372
import { useAction } from "../../action/use_action.js";
7473
import { useActionStatus } from "../../action/use_action_status.js";
7574
import { Box } from "../../box/box.jsx";
75+
import { MOUNT_DEFAULT } from "../../layout/popup_content_mount.js";
7676
import { whenTransitionSettles } from "../../layout/popup_shared.js";
7777
import { onNaviCommand } from "../commands.js";
7878
import { warnSignalCollision } from "../control_value.js";
@@ -187,7 +187,7 @@ const css = /* css */ `
187187
grid-template-columns: 1fr;
188188
grid-template-rows: 1fr;
189189
}
190-
/* mountWhenClosed: the content is built and width-frozen while closed
190+
/* mount="always": the content is built and width-frozen while closed
191191
(see the component), so it can size the height at all times — the
192192
expandable then keeps one stable height and only the width reveals. */
193193
&[data-closed-content-sized] > .navi_expandable_content_container {
@@ -260,8 +260,7 @@ const useExpandableContext = (partName) => {
260260
* layout?: "row" | "column",
261261
* autoFocus?: boolean,
262262
* maxContentHeight?: string | number,
263-
* mountWhenClosed?: boolean,
264-
* unmountWhenClosed?: boolean,
263+
* mount?: "always" | "from-first-open" | "while-opened",
265264
* arrowKeyShortcuts?: boolean,
266265
* openKeyShortcut?: string,
267266
* closeKeyShortcut?: string,
@@ -298,13 +297,15 @@ const useExpandableContext = (partName) => {
298297
* the UI part (it would otherwise be lost to the closed, inert content).
299298
* @param maxContentHeight - Caps the content height; taller content scrolls
300299
* inside the expandable instead of growing it.
301-
* @param mountWhenClosed - Builds the content right away instead of on first
302-
* expansion. In layout="column" it also gives the closed expandable its
303-
* content's height (the content is kept laid out at its open width), so
304-
* opening only reveals the width instead of changing the height too.
305-
* @param unmountWhenClosed - Throws the content away once the collapse
306-
* settles — after the closing animation, so it still plays on real content —
307-
* and rebuilds it from scratch on every expansion.
300+
* @param mount - When the content is built and thrown away, same three values
301+
* as a popup's (see popup_content_mount.js). `"from-first-open"` (the
302+
* default) builds it on the first expansion and keeps it afterwards.
303+
* `"always"` builds it right away; in layout="column" it also gives the
304+
* closed expandable its content's height (the content is kept laid out at
305+
* its open width), so opening only reveals the width instead of changing the
306+
* height too. `"while-opened"` throws the content away once the collapse
307+
* settles — after the closing animation, so it still plays on real
308+
* content — and rebuilds it from scratch on every expansion.
308309
*/
309310
export const Expandable = (props) => {
310311
import.meta.css = css;
@@ -320,8 +321,7 @@ export const Expandable = (props) => {
320321
layout,
321322
autoFocus,
322323
maxContentHeight,
323-
mountWhenClosed,
324-
unmountWhenClosed,
324+
mount = MOUNT_DEFAULT,
325325
arrowKeyShortcuts = true,
326326
openKeyShortcut = "ArrowRight",
327327
closeKeyShortcut = "ArrowLeft",
@@ -335,7 +335,9 @@ export const Expandable = (props) => {
335335
const contentContainerRef = useRef();
336336
const contentId = useId();
337337
const isColumn = layout === "column";
338-
const closedContentSized = Boolean(isColumn && mountWhenClosed);
338+
const mountedAlways = mount === "always";
339+
const mountedWhileOpened = mount === "while-opened";
340+
const closedContentSized = isColumn && mountedAlways;
339341

340342
if (signal) {
341343
warnSignalCollision(props, "expandable", "open");
@@ -353,14 +355,11 @@ export const Expandable = (props) => {
353355
const { loading: actionLoading } = useActionStatus(effectiveAction);
354356

355357
const [contentMounted, setContentMounted] = useState(
356-
() => Boolean(mountWhenClosed) || opened,
358+
() => mountedAlways || opened,
357359
);
358-
// Same exclusion as popup_content_mount.js: content that must exist while
359-
// closed cannot also be thrown away on close.
360-
const effectiveUnmountWhenClosed = unmountWhenClosed && !mountWhenClosed;
361360

362361
// Fully open and no longer moving — what allows overflow to become visible
363-
// (see the CSS) and what unmountWhenClosed waits for before emptying.
362+
// (see the CSS) and what mount="while-opened" waits for before emptying.
364363
const [settled, setSettled] = useState(true);
365364

366365
// Read before the close touches the DOM: flipping the content to inert can
@@ -435,7 +434,7 @@ export const Expandable = (props) => {
435434
focusedAtPointerDownRef.current = null;
436435
setOpened(nextOpen);
437436
// Flipped here, before the closing/opening commit, so effects of that very
438-
// commit already see the movement as started — unmountWhenClosed must not
437+
// commit already see the movement as started — mount="while-opened" must not
439438
// read a stale "settled" and empty the content under a closing animation.
440439
setSettled(!animation);
441440
if (signal) {
@@ -573,17 +572,17 @@ export const Expandable = (props) => {
573572
}, [opened]);
574573

575574
useLayoutEffect(() => {
576-
if (settled && !opened && effectiveUnmountWhenClosed) {
575+
if (settled && !opened && mountedWhileOpened) {
577576
setContentMounted(false);
578577
}
579-
}, [settled, opened, effectiveUnmountWhenClosed]);
578+
}, [settled, opened, mountedWhileOpened]);
580579
useLayoutEffect(() => {
581-
if (mountWhenClosed) {
580+
if (mountedAlways) {
582581
setContentMounted(true);
583582
}
584-
}, [mountWhenClosed]);
583+
}, [mountedAlways]);
585584

586-
// closedContentSized (column + mountWhenClosed): the closed content sizes
585+
// closedContentSized (column + mount="always"): the closed content sizes
587586
// the height (see the CSS), which is only right if it lies at its OPEN
588587
// width — at its natural closed width (a 0-wide track) it would wrap
589588
// against nothing and stack word by word. So while closed, its width is

packages/frontend/navi/src/control/picker/picker.jsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,8 +1409,7 @@ const PickerFirstResolver = (props) => {
14091409
* popupTestId?: string,
14101410
* confirmTestId?: string,
14111411
* cancelTestId?: string,
1412-
* mountWhenClosed?: boolean,
1413-
* unmountWhenClosed?: boolean,
1412+
* mount?: "always" | "from-first-open" | "while-opened",
14141413
* dialogExpand?: boolean,
14151414
* dialogExpandX?: boolean,
14161415
* dialogExpandY?: boolean,
@@ -1636,13 +1635,12 @@ const PickerFirstResolver = (props) => {
16361635
* Both are elements navi builds, which a test may not name by class (see
16371636
* docs/testid.md).
16381637
* @param {string} [cancelTestId]
1639-
* @param {boolean} [mountWhenClosed] The popup's own (see Popup): build the
1640-
* children before any opening. Left out, a picker told no value builds them
1641-
* right away — it reads its value off the control they hold — and one told
1642-
* a value waits for the first open. A `type="confirm"` picker never builds
1643-
* them early: its popup is a question, there is nothing to read.
1644-
* @param {boolean} [unmountWhenClosed] The popup's own: throw the children
1645-
* away once the popup has closed.
1638+
* @param {"always"|"from-first-open"|"while-opened"} [mount] The popup's own
1639+
* (see Popup): when the children are built and thrown away. Left out, a
1640+
* picker told no value builds them right away (`"always"`) — it reads its
1641+
* value off the control they hold — and one told a value waits for the first
1642+
* open. A `type="confirm"` picker never builds them early: its popup is a
1643+
* question, there is nothing to read.
16461644
*/
16471645
// Every picker type is resolved into one of these before it gets here (see
16481646
// PickerTypeResolver and resolveInputProps): a native input type, or navi_js

packages/frontend/navi/src/control/picker/picker_confirm.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Escape, a click outside) is no.
1616
*/
1717

18+
import { MOUNT_DEFAULT } from "@jsenv/navi/src/layout/popup_content_mount.js";
1819
import { useNextResolver } from "@jsenv/navi/src/resolver/resolver.jsx";
1920
import { naviI18n } from "@jsenv/navi/src/text/navi_i18n.js";
2021
import { triggerNaviCommand } from "../commands.js";
@@ -108,8 +109,8 @@ export const PickerConfirmResolver = (props) => {
108109
onConfirm={onConfirm}
109110
// A question holds no value to read before it is asked: the popup is
110111
// built on the first open, like that of a picker told its value (see
111-
// mountWhenClosed in picker_custom.jsx).
112-
mountWhenClosed={false}
112+
// the `mount` resolution in picker_custom.jsx).
113+
mount={MOUNT_DEFAULT}
113114
message={undefined}
114115
confirmLabel={undefined}
115116
cancelLabel={undefined}

packages/frontend/navi/src/control/picker/picker_custom.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
PopupModeContext,
1616
useResolvedPopupMode,
1717
} from "@jsenv/navi/src/layout/popup_mode.jsx";
18+
import { MOUNT_DEFAULT } from "@jsenv/navi/src/layout/popup_content_mount.js";
1819
import { Popup } from "@jsenv/navi/src/layout/popup.jsx";
1920
import { useNextResolver } from "@jsenv/navi/src/resolver/resolver.jsx";
2021
import { interactionsDisputeThePress } from "../interaction/interactions.js";
@@ -609,9 +610,9 @@ const PickerCustom = (props) => {
609610
// and pushes it down instead, leaving the popup free to build its
610611
// content only when it is first opened (see popup_content_mount.js).
611612
// A caller who knows better says so with the popup's own props.
612-
mountWhenClosed:
613-
props.mountWhenClosed ?? !isControlValueGivenByProps(props),
614-
unmountWhenClosed: props.unmountWhenClosed,
613+
mount:
614+
props.mount ??
615+
(isControlValueGivenByProps(props) ? MOUNT_DEFAULT : "always"),
615616
// Not on pickerProps (the trigger): commands.js's own
616617
// resolveClosestExpandable() does `el.closest("[aria-expanded]")` to
617618
// find where to dispatch navi_request_open/navi_request_close — and
@@ -902,8 +903,7 @@ const PickerContentInsidePopup = (props) => {
902903
{...rest}
903904
// On popupProps already (see the picker's popup assembly); they mean
904905
// nothing to the picker element.
905-
mountWhenClosed={undefined}
906-
unmountWhenClosed={undefined}
906+
mount={undefined}
907907
onFocusOut={(e) => {
908908
if (!isPopover || !closeOnFocusOut) {
909909
return;
@@ -1005,7 +1005,7 @@ const PICKER_CALLOUT_CONTENT_TOKEN = createOpenToken();
10051005
* The content is rendered through a portal into an element this component
10061006
* owns, handed to the callout as its message (a Node, appended as-is). It is
10071007
* rendered whether the callout is open or not, so what the content holds
1008-
* survives a close, the way a popup's `mountWhenClosed` keeps it. The element
1008+
* survives a close, the way a popup's `mount="always"` keeps it. The element
10091009
* carries data-picker-content: the callout is appended inside the picker root,
10101010
* and a press in there must read as inside the popup, not on the trigger.
10111011
*/

0 commit comments

Comments
 (0)