Skip to content

Commit 20b73a8

Browse files
committed
work
1 parent 8133536 commit 20b73a8

6 files changed

Lines changed: 205 additions & 12 deletions

File tree

packages/frontend/navi/dist/jsenv_navi.js

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34483,7 +34483,12 @@ const GROUP_DEFAULTS = {
3448334483
}
3448434484
// Merged in on the way up (see above), so on the way down it takes the
3448534485
// whole object and picks out its own keys — the same value it produced.
34486-
if (isNamelessGrouping(child, child.uiState)) {
34486+
// Only for a child with no name of its own: the same condition the
34487+
// aggregate applies before merging. A NAMED child holding an object (a
34488+
// `type="object"` picker, say) answers for one key, and the object it is
34489+
// not mentioned in says nothing about it — handing it the whole value
34490+
// would make it hold the group.
34491+
if (!childName && isNamelessGrouping(child, child.uiState)) {
3448734492
return newUIState;
3448834493
}
3448934494
if (
@@ -34771,7 +34776,17 @@ const useUIGroupStateController = (
3477134776
e,
3477234777
`${controlType}.getUIState -> ${JSON.stringify(groupUIState)}`,
3477334778
);
34774-
if (notifyExternal === true) {
34779+
if (notifyExternal === true || notifyExternal === "if-it-moves") {
34780+
if (
34781+
notifyExternal === "if-it-moves" &&
34782+
compareTwoJsValues(groupUIState, controller.uiState)
34783+
) {
34784+
// A child confirming what it already shows, to a group already
34785+
// worth it: the value arrived while the popup was open and the
34786+
// group reacted then. One trip through a picker is one answer, so
34787+
// there is nothing left to say here.
34788+
return;
34789+
}
3477534790
// Somebody answered: what the group is worth is what its children say
3477634791
// between them, from here on.
3477734792
controller.stateGivenFromAbove = false;
@@ -35085,7 +35100,7 @@ const useUIGroupStateController = (
3508535100
onChildUIAction: (
3508635101
childUIStateController,
3508735102
e,
35088-
{ stateChanged, silent },
35103+
{ stateChanged, silent, onlyIfGroupValueMoves },
3508935104
) => {
3509035105
const delegatedTo = delegatedChildrenRef.current.get(
3509135106
childUIStateController,
@@ -35094,6 +35109,7 @@ const useUIGroupStateController = (
3509435109
delegatedTo.onChildUIAction(childUIStateController, e, {
3509535110
stateChanged,
3509635111
silent,
35112+
onlyIfGroupValueMoves,
3509735113
});
3509835114
return;
3509935115
}
@@ -35108,7 +35124,11 @@ const useUIGroupStateController = (
3510835124
);
3510935125
if (stateChanged) {
3511035126
onChange(e, {
35111-
notifyExternal: silent ? "silent" : true,
35127+
notifyExternal: silent
35128+
? "silent"
35129+
: onlyIfGroupValueMoves
35130+
? "if-it-moves"
35131+
: true,
3511235132
actingChild: childUIStateController,
3511335133
});
3511435134
} else {
@@ -51839,13 +51859,28 @@ const isUIStateHeld = (controller) => {
5183951859
* `command` on a control is its reaction to being used, and this is a
5184051860
* confirmation happening elsewhere, whose own command (the picker's) is already
5184151861
* running.
51862+
*
51863+
* And up one, to the group the control answers to: a picker inside a form is
51864+
* one of its fields, and a form is worth what its fields say. What the popup
51865+
* put there arrived through a mount sync, which is deliberately silent — a
51866+
* popup opening is nobody answering (see onChange in ui_state_controller.js) —
51867+
* so this is the first moment the form can be told.
51868+
*
51869+
* That group only hears about it when it moves: a value the user picked in the
51870+
* popup reached it already, and one trip through a picker is one answer. Down
51871+
* the subtree the reaction re-runs either way — a control saying again what it
51872+
* holds says the same thing, and it is where a signal is written.
5184251873
*/
5184351874
const commitUIStateAsAnswer = (controller, e) => {
5184451875
if (!controller) {
5184551876
return;
5184651877
}
5184751878
const answering = controller.facadeChild || controller;
5184851879
commitSubtree(answering, e);
51880+
controller.parentUIStateController?.onChildUIAction(controller, e, {
51881+
stateChanged: true,
51882+
onlyIfGroupValueMoves: true,
51883+
});
5184951884
};
5185051885
const commitSubtree = (controller, e) => {
5185151886
controller.onUIAction?.(e, { skipCommand: true });

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

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

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.168",
3+
"version": "0.29.169",
44
"type": "module",
55
"description": "Library of components including navigation to create frontend applications",
66
"repository": {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" href="data:," />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Picker on a defaultValue, inside a Form</title>
8+
<style>
9+
body {
10+
max-width: 720px;
11+
margin: 0 auto;
12+
padding: 24px;
13+
color: #222;
14+
font-family: system-ui, sans-serif;
15+
}
16+
.repro {
17+
margin: 0 0 20px;
18+
padding: 12px 14px;
19+
font-size: 0.9rem;
20+
line-height: 1.5;
21+
background: #fff4e0;
22+
border: 1px solid #f0c46a;
23+
border-left: 4px solid #e8940c;
24+
border-radius: 4px;
25+
}
26+
pre {
27+
padding: 8px 10px;
28+
font-family: monospace;
29+
background: #f4f4f5;
30+
border: 1px solid #e0e0e0;
31+
border-radius: 4px;
32+
}
33+
</style>
34+
</head>
35+
<body>
36+
<h1>Picker on a defaultValue, inside a Form</h1>
37+
<p class="repro">
38+
A tie-break cell: a <code>type="object"</code> picker named
39+
<code>set1_tie_break</code>, sitting on the most likely score (7-4), in a
40+
form the score sheet drives with a signal.
41+
<br />
42+
<strong>Open the popup and press Noter without turning a wheel</strong>
43+
the form value below must gain
44+
<code>set1_tie_break: {"{"} winner: 7, loser: 4 {"}"}</code>. Same for a
45+
click outside. <strong>Escape</strong> cancels and must leave the form
46+
alone, and so must never opening the popup at all: a suggestion is not an
47+
answer until it is confirmed.
48+
</p>
49+
<div id="app"></div>
50+
<script type="module" jsenv-type="module/jsx">
51+
import { render } from "preact";
52+
import { signal } from "@preact/signals";
53+
import {
54+
Box,
55+
Button,
56+
Form,
57+
Input,
58+
Picker,
59+
Wheel,
60+
WheelGroup,
61+
} from "@jsenv/navi";
62+
63+
const scoreSignal = signal({ set1_winner: 6 });
64+
const WINNERS = [7, 8, 9, 10];
65+
const LOSERS = [0, 1, 2, 3, 4, 5, 6];
66+
67+
const App = () => {
68+
const score = scoreSignal.value;
69+
const tieBreak = score.set1_tie_break;
70+
return (
71+
<Box flex="y" spacing="m" alignX="start">
72+
<Form signal={scoreSignal}>
73+
<Box flex="x" spacing="m" alignY="center">
74+
<Input id="winner_field" name="set1_winner" type="number" />
75+
<Picker
76+
id="tie_break"
77+
name="set1_tie_break"
78+
type="object"
79+
defaultValue={{ winner: 7, loser: 4 }}
80+
ui={
81+
<span>
82+
{tieBreak
83+
? `${tieBreak.winner}-${tieBreak.loser}`
84+
: "tie?"}
85+
</span>
86+
}
87+
>
88+
<Form command="--navi-close" canSendWhileUnchanged>
89+
<Box flex="y" spacing="s" padding="s">
90+
<WheelGroup>
91+
<Wheel name="winner" type="integer" defaultValue={7}>
92+
{WINNERS.map((w) => (
93+
<Wheel.Item key={w} value={w} paddingX="s">
94+
{w}
95+
</Wheel.Item>
96+
))}
97+
</Wheel>
98+
<WheelGroup.Separator>-</WheelGroup.Separator>
99+
<Wheel name="loser" type="integer" defaultValue={4}>
100+
{LOSERS.map((l) => (
101+
<Wheel.Item key={l} value={l} paddingX="s">
102+
{l}
103+
</Wheel.Item>
104+
))}
105+
</Wheel>
106+
</WheelGroup>
107+
<Button id="noter" command="--navi-send">
108+
Noter
109+
</Button>
110+
</Box>
111+
</Form>
112+
</Picker>
113+
</Box>
114+
</Form>
115+
<pre id="score">{JSON.stringify(score)}</pre>
116+
</Box>
117+
);
118+
};
119+
120+
render(<App />, document.getElementById("app"));
121+
</script>
122+
</body>
123+
</html>

packages/frontend/navi/src/control/held_ui_state.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,28 @@ export const isUIStateHeld = (controller) => {
6464
* `command` on a control is its reaction to being used, and this is a
6565
* confirmation happening elsewhere, whose own command (the picker's) is already
6666
* running.
67+
*
68+
* And up one, to the group the control answers to: a picker inside a form is
69+
* one of its fields, and a form is worth what its fields say. What the popup
70+
* put there arrived through a mount sync, which is deliberately silent — a
71+
* popup opening is nobody answering (see onChange in ui_state_controller.js) —
72+
* so this is the first moment the form can be told.
73+
*
74+
* That group only hears about it when it moves: a value the user picked in the
75+
* popup reached it already, and one trip through a picker is one answer. Down
76+
* the subtree the reaction re-runs either way — a control saying again what it
77+
* holds says the same thing, and it is where a signal is written.
6778
*/
6879
export const commitUIStateAsAnswer = (controller, e) => {
6980
if (!controller) {
7081
return;
7182
}
7283
const answering = controller.facadeChild || controller;
7384
commitSubtree(answering, e);
85+
controller.parentUIStateController?.onChildUIAction(controller, e, {
86+
stateChanged: true,
87+
onlyIfGroupValueMoves: true,
88+
});
7489
};
7590
const commitSubtree = (controller, e) => {
7691
controller.onUIAction?.(e, { skipCommand: true });

packages/frontend/navi/src/control/ui_state_controller.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,12 @@ const GROUP_DEFAULTS = {
961961
}
962962
// Merged in on the way up (see above), so on the way down it takes the
963963
// whole object and picks out its own keys — the same value it produced.
964-
if (isNamelessGrouping(child, child.uiState)) {
964+
// Only for a child with no name of its own: the same condition the
965+
// aggregate applies before merging. A NAMED child holding an object (a
966+
// `type="object"` picker, say) answers for one key, and the object it is
967+
// not mentioned in says nothing about it — handing it the whole value
968+
// would make it hold the group.
969+
if (!childName && isNamelessGrouping(child, child.uiState)) {
965970
return newUIState;
966971
}
967972
if (
@@ -1256,7 +1261,17 @@ export const useUIGroupStateController = (
12561261
e,
12571262
`${controlType}.getUIState -> ${JSON.stringify(groupUIState)}`,
12581263
);
1259-
if (notifyExternal === true) {
1264+
if (notifyExternal === true || notifyExternal === "if-it-moves") {
1265+
if (
1266+
notifyExternal === "if-it-moves" &&
1267+
compareTwoJsValues(groupUIState, controller.uiState)
1268+
) {
1269+
// A child confirming what it already shows, to a group already
1270+
// worth it: the value arrived while the popup was open and the
1271+
// group reacted then. One trip through a picker is one answer, so
1272+
// there is nothing left to say here.
1273+
return;
1274+
}
12601275
// Somebody answered: what the group is worth is what its children say
12611276
// between them, from here on.
12621277
controller.stateGivenFromAbove = false;
@@ -1570,7 +1585,7 @@ export const useUIGroupStateController = (
15701585
onChildUIAction: (
15711586
childUIStateController,
15721587
e,
1573-
{ stateChanged, silent },
1588+
{ stateChanged, silent, onlyIfGroupValueMoves },
15741589
) => {
15751590
const delegatedTo = delegatedChildrenRef.current.get(
15761591
childUIStateController,
@@ -1579,6 +1594,7 @@ export const useUIGroupStateController = (
15791594
delegatedTo.onChildUIAction(childUIStateController, e, {
15801595
stateChanged,
15811596
silent,
1597+
onlyIfGroupValueMoves,
15821598
});
15831599
return;
15841600
}
@@ -1593,7 +1609,11 @@ export const useUIGroupStateController = (
15931609
);
15941610
if (stateChanged) {
15951611
onChange(e, {
1596-
notifyExternal: silent ? "silent" : true,
1612+
notifyExternal: silent
1613+
? "silent"
1614+
: onlyIfGroupValueMoves
1615+
? "if-it-moves"
1616+
: true,
15971617
actingChild: childUIStateController,
15981618
});
15991619
} else {

0 commit comments

Comments
 (0)