@jsenv/navi 0.29.88 → 0.29.89
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/jsenv_navi.js +231 -52
- package/dist/jsenv_navi.js.map +16 -3
- package/docs/control_object.md +26 -1
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -24381,6 +24381,13 @@ const GROUP_DEFAULTS = {
|
|
|
24381
24381
|
? emptyUIState
|
|
24382
24382
|
: child.uiState;
|
|
24383
24383
|
if (!name) {
|
|
24384
|
+
if (allowNameless) {
|
|
24385
|
+
// A control that says it is not a field is not one, whatever it
|
|
24386
|
+
// holds: a picker used as a door holds the shape its popup draws,
|
|
24387
|
+
// and merging that in would put the popup's keys in the object as
|
|
24388
|
+
// if the door had been a group.
|
|
24389
|
+
continue;
|
|
24390
|
+
}
|
|
24384
24391
|
// A nameless GROUP is a grouping, not a value: it exists to hold its
|
|
24385
24392
|
// children together (a WheelGroup sharing navigation, a fieldset-ish
|
|
24386
24393
|
// cluster) without claiming a key of its own, so what it holds is
|
|
@@ -24390,12 +24397,10 @@ const GROUP_DEFAULTS = {
|
|
|
24390
24397
|
Object.assign(groupValues, uiState);
|
|
24391
24398
|
continue;
|
|
24392
24399
|
}
|
|
24393
|
-
|
|
24394
|
-
|
|
24395
|
-
|
|
24396
|
-
|
|
24397
|
-
);
|
|
24398
|
-
}
|
|
24400
|
+
console.warn(
|
|
24401
|
+
"A group child is missing a name property, its state won't be included in the group state",
|
|
24402
|
+
child,
|
|
24403
|
+
);
|
|
24399
24404
|
continue;
|
|
24400
24405
|
}
|
|
24401
24406
|
groupValues[name] = uiState;
|
|
@@ -24520,6 +24525,14 @@ const useUIGroupStateController = (
|
|
|
24520
24525
|
: stateType === "object"
|
|
24521
24526
|
? EMPTY_OBJECT
|
|
24522
24527
|
: undefined;
|
|
24528
|
+
// A group told what it holds holds it from the start, before any child has
|
|
24529
|
+
// registered to show it: what it was given is the answer, and the children
|
|
24530
|
+
// are where that answer is shown (see stateGivenFromAbove).
|
|
24531
|
+
const stateInitial = hasValueProp
|
|
24532
|
+
? value
|
|
24533
|
+
: hasDefaultValueProp && defaultValue !== undefined
|
|
24534
|
+
? defaultValue
|
|
24535
|
+
: fallbackState;
|
|
24523
24536
|
const childUIStateControllerArrayRef = useRef([]);
|
|
24524
24537
|
const childUIStateControllerArray = childUIStateControllerArrayRef.current;
|
|
24525
24538
|
// Tracks children rejected by the filter and delegated upward (bubble-up).
|
|
@@ -24583,14 +24596,35 @@ const useUIGroupStateController = (
|
|
|
24583
24596
|
`Creating "${controlType}" ui state controller (monitoring some descendants ui state(s))"`,
|
|
24584
24597
|
);
|
|
24585
24598
|
const [publishUIState, subscribeUIState] = createPubSub();
|
|
24586
|
-
const uiStateSignal = signal(
|
|
24587
|
-
|
|
24588
|
-
|
|
24599
|
+
const uiStateSignal = signal(stateInitial);
|
|
24600
|
+
|
|
24601
|
+
// What the group is worth right now, and what it keeps when there is
|
|
24602
|
+
// nobody to ask: a list whose items have not arrived yet, a popup built
|
|
24603
|
+
// at open, a group whose children are still mounting. Such a group has
|
|
24604
|
+
// no opinion — its aggregate falls back to the empty of its type, and
|
|
24605
|
+
// taking that for an answer is how a value handed to it evaporates on
|
|
24606
|
+
// the way in, and how that emptiness then travels back up to whoever
|
|
24607
|
+
// handed it (a picker showing its row as unanswered).
|
|
24608
|
+
const aggregateGroupUIState = (whenNobodyCanAnswer) => {
|
|
24609
|
+
const someChildCanAnswer = childUIStateControllerArray.some(
|
|
24610
|
+
shouldPropagateStateToChild,
|
|
24611
|
+
);
|
|
24612
|
+
if (!someChildCanAnswer) {
|
|
24613
|
+
return whenNobodyCanAnswer;
|
|
24614
|
+
}
|
|
24589
24615
|
const aggChildState = resolvedAggregateChildStates(
|
|
24590
24616
|
childUIStateControllerArray,
|
|
24591
24617
|
fallbackState,
|
|
24592
24618
|
);
|
|
24593
|
-
|
|
24619
|
+
if (aggChildState !== undefined) {
|
|
24620
|
+
return aggChildState;
|
|
24621
|
+
}
|
|
24622
|
+
// A group with an aggregate of its own is the one who knows what its
|
|
24623
|
+
// children add up to, `undefined` included — half a time is not a time,
|
|
24624
|
+
// wheels nobody turned have settled nothing. Only the default shapes
|
|
24625
|
+
// fall back to the empty of their type, where "no child says anything"
|
|
24626
|
+
// and "the value is empty" are the same sentence.
|
|
24627
|
+
return stateShapeIsTheDefaultOne ? fallbackState : undefined;
|
|
24594
24628
|
};
|
|
24595
24629
|
|
|
24596
24630
|
// onChange and applyState live inside init so they close over the stable
|
|
@@ -24611,13 +24645,26 @@ const useUIGroupStateController = (
|
|
|
24611
24645
|
};
|
|
24612
24646
|
return;
|
|
24613
24647
|
}
|
|
24614
|
-
const
|
|
24648
|
+
const { controller } = s;
|
|
24649
|
+
// A child mounting or unmounting is not somebody answering: while the
|
|
24650
|
+
// children of a group are still arriving, their aggregate is a partial
|
|
24651
|
+
// reading, and taking it for the truth is how the value the group was
|
|
24652
|
+
// given gets destroyed one row at a time — the first row to register
|
|
24653
|
+
// aggregates alone, the group drops to that, and every row after it is
|
|
24654
|
+
// placed from what is left. A group that derived its own value has
|
|
24655
|
+
// nothing to protect and aggregates as usual.
|
|
24656
|
+
const groupUIState =
|
|
24657
|
+
notifyExternal === "silent" && controller.stateGivenFromAbove
|
|
24658
|
+
? controller.uiState
|
|
24659
|
+
: aggregateGroupUIState(controller.uiState);
|
|
24615
24660
|
debugUIGroup(
|
|
24616
24661
|
e,
|
|
24617
24662
|
`${controlType}.getUIState -> ${JSON.stringify(groupUIState)}`,
|
|
24618
24663
|
);
|
|
24619
|
-
const { controller } = s;
|
|
24620
24664
|
if (notifyExternal === true) {
|
|
24665
|
+
// Somebody answered: what the group is worth is what its children say
|
|
24666
|
+
// between them, from here on.
|
|
24667
|
+
controller.stateGivenFromAbove = false;
|
|
24621
24668
|
applyState(groupUIState, e);
|
|
24622
24669
|
} else if (notifyExternal === "silent") {
|
|
24623
24670
|
controller.syncInternalState(groupUIState);
|
|
@@ -24683,7 +24730,11 @@ const useUIGroupStateController = (
|
|
|
24683
24730
|
hasValueProp,
|
|
24684
24731
|
hasDefaultValueProp,
|
|
24685
24732
|
props,
|
|
24686
|
-
uiState:
|
|
24733
|
+
uiState: stateInitial,
|
|
24734
|
+
// Whether what the group holds was HANDED to it (a parent distributing,
|
|
24735
|
+
// a picker filling its popup, a value prop) rather than worked out from
|
|
24736
|
+
// its children. What it protects is read in onChange.
|
|
24737
|
+
stateGivenFromAbove: hasValueProp || hasDefaultValueProp,
|
|
24687
24738
|
uiStateSignal,
|
|
24688
24739
|
wantRequesterButtonState,
|
|
24689
24740
|
ref,
|
|
@@ -24731,6 +24782,7 @@ const useUIGroupStateController = (
|
|
|
24731
24782
|
);
|
|
24732
24783
|
return;
|
|
24733
24784
|
}
|
|
24785
|
+
controller.stateGivenFromAbove = true;
|
|
24734
24786
|
const propagateEventType =
|
|
24735
24787
|
e.type === "initial_state_push"
|
|
24736
24788
|
? "initial_state_push"
|
|
@@ -24746,9 +24798,10 @@ const useUIGroupStateController = (
|
|
|
24746
24798
|
propagateDownEvent,
|
|
24747
24799
|
);
|
|
24748
24800
|
}
|
|
24749
|
-
const groupUIState = aggregateGroupUIState();
|
|
24801
|
+
const groupUIState = aggregateGroupUIState(newUIState);
|
|
24750
24802
|
if (e.type === "initial_state_push") {
|
|
24751
24803
|
controller.syncInternalState(groupUIState);
|
|
24804
|
+
writeBoundSignal(groupUIState);
|
|
24752
24805
|
return;
|
|
24753
24806
|
}
|
|
24754
24807
|
applyState(groupUIState, e, { internalBehavior: true });
|
|
@@ -24800,15 +24853,28 @@ const useUIGroupStateController = (
|
|
|
24800
24853
|
debugUIGroup(
|
|
24801
24854
|
`${controlType}.registerChild("${childControlType}") -> registered (total: ${childUIStateControllerArray.length})`,
|
|
24802
24855
|
);
|
|
24803
|
-
|
|
24856
|
+
const stateToPlaceChildFrom = controller.hasValueProp
|
|
24857
|
+
? controller.value
|
|
24858
|
+
: controller.hasDefaultValueProp
|
|
24859
|
+
? controller.defaultValue
|
|
24860
|
+
: // What the group HOLDS, for a child arriving after the value
|
|
24861
|
+
// did: a list item loaded later, a row scrolled back into a
|
|
24862
|
+
// virtualized list, a popup built at open. Two conditions, and
|
|
24863
|
+
// both are about not overwriting an answer with a silence — the
|
|
24864
|
+
// group must actually hold something, and the child must have
|
|
24865
|
+
// nothing of its own to show (one arriving with its own default
|
|
24866
|
+
// is answering, and the group is what its answers add up to).
|
|
24867
|
+
uiStateHoldsNothing(controller.uiState) ||
|
|
24868
|
+
!uiStateHoldsNothing(childUIStateController.uiState)
|
|
24869
|
+
? undefined
|
|
24870
|
+
: controller.uiState;
|
|
24871
|
+
if (stateToPlaceChildFrom !== undefined) {
|
|
24804
24872
|
const initialEvent = new CustomEvent("initial_state_push", {
|
|
24805
24873
|
detail: {},
|
|
24806
24874
|
});
|
|
24807
24875
|
controller.placeChildUIState(
|
|
24808
24876
|
childUIStateController,
|
|
24809
|
-
|
|
24810
|
-
? controller.value
|
|
24811
|
-
: controller.defaultValue,
|
|
24877
|
+
stateToPlaceChildFrom,
|
|
24812
24878
|
initialEvent,
|
|
24813
24879
|
);
|
|
24814
24880
|
}
|
|
@@ -25097,6 +25163,7 @@ const EMPTY_OBJECT = {};
|
|
|
25097
25163
|
*/
|
|
25098
25164
|
const useUIFacadeStateController = (props, realUIStateController) => {
|
|
25099
25165
|
const firstChildControllerRef = useRef(null);
|
|
25166
|
+
const namelessChildSetRef = useRef(new Set());
|
|
25100
25167
|
const updatingRef = useRef(false);
|
|
25101
25168
|
const debugPopup = useDebugPopup();
|
|
25102
25169
|
const debugInteraction = useDebugInteraction();
|
|
@@ -25128,6 +25195,7 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25128
25195
|
// A control saying it is not a field is not the one the picker talks
|
|
25129
25196
|
// to: the search box above the list, the "select all" switch beside
|
|
25130
25197
|
// it. It is there to help find the answer, not to be it.
|
|
25198
|
+
namelessChildSetRef.current.add(childController);
|
|
25131
25199
|
return false;
|
|
25132
25200
|
}
|
|
25133
25201
|
if (childController.props["navi-list"]) {
|
|
@@ -25150,6 +25218,10 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25150
25218
|
}
|
|
25151
25219
|
const child = firstChildControllerRef.current;
|
|
25152
25220
|
if (!child) {
|
|
25221
|
+
warnPopupHasNothingButNamelessControls(
|
|
25222
|
+
props,
|
|
25223
|
+
namelessChildSetRef.current,
|
|
25224
|
+
);
|
|
25153
25225
|
return;
|
|
25154
25226
|
}
|
|
25155
25227
|
updatingRef.current = true;
|
|
@@ -25298,6 +25370,11 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
25298
25370
|
|
|
25299
25371
|
const describePicker = (props) =>
|
|
25300
25372
|
`<Picker${props.name ? ` name="${props.name}"` : ""}${props.type ? ` type="${props.type}"` : ""}>`;
|
|
25373
|
+
const warnPopupHasNothingButNamelessControls = (props, namelessChildSet) => {
|
|
25374
|
+
{
|
|
25375
|
+
return;
|
|
25376
|
+
}
|
|
25377
|
+
};
|
|
25301
25378
|
|
|
25302
25379
|
/**
|
|
25303
25380
|
* Returns true when `e` should trigger parent notification (child → parent bubbling).
|
|
@@ -68158,12 +68235,19 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
|
|
|
68158
68235
|
* past.
|
|
68159
68236
|
* @param {import("ignore:preact").ComponentChildren} [separator] What is written
|
|
68160
68237
|
* between the hours and the minutes. "h" in French, ":" elsewhere.
|
|
68238
|
+
* @param {string} [placeholder] What the wheels show while the time holds
|
|
68239
|
+
* nothing, as "HH:MM". Wheels have no blank row to land on, so their
|
|
68240
|
+
* placeholder is a position rather than a grey word — shown, but not an
|
|
68241
|
+
* answer: the value stays `undefined` until a wheel is turned or a real value
|
|
68242
|
+
* arrives. `defaultValue` is the other half of the pair — a time that IS the
|
|
68243
|
+
* answer, and where a reset goes back to.
|
|
68161
68244
|
* @param {object} [wheelProps] Anything a `Wheel` takes, said once for both of
|
|
68162
68245
|
* them — `visibleCount`, `itemWidth`, `glideSpeed`.
|
|
68163
68246
|
*/
|
|
68164
68247
|
const TimeWheel = ({
|
|
68165
68248
|
minuteStep = 1,
|
|
68166
68249
|
loop = true,
|
|
68250
|
+
placeholder,
|
|
68167
68251
|
separator = naviI18n("time.hour_separator"),
|
|
68168
68252
|
hourLabel = naviI18n("time.hour_label"),
|
|
68169
68253
|
minuteLabel = naviI18n("time.minute_label"),
|
|
@@ -68180,8 +68264,12 @@ const TimeWheel = ({
|
|
|
68180
68264
|
}
|
|
68181
68265
|
return minuteList;
|
|
68182
68266
|
}, [minuteStep]);
|
|
68267
|
+
const {
|
|
68268
|
+
aggregateChildStates
|
|
68269
|
+
} = useAnswered(placeholder, rest, aggregateTime);
|
|
68270
|
+
const placeholderParts = parseTimeParts(placeholder);
|
|
68183
68271
|
return jsxs(WheelGroup, {
|
|
68184
|
-
aggregateChildStates:
|
|
68272
|
+
aggregateChildStates: aggregateChildStates,
|
|
68185
68273
|
distributeChildUIState: distributeTime,
|
|
68186
68274
|
...rest,
|
|
68187
68275
|
children: [jsx(Wheel, {
|
|
@@ -68190,6 +68278,7 @@ const TimeWheel = ({
|
|
|
68190
68278
|
bounded: !loop,
|
|
68191
68279
|
size: size,
|
|
68192
68280
|
"aria-label": hourLabel,
|
|
68281
|
+
defaultValue: placeholderParts ? placeholderParts.hour : undefined,
|
|
68193
68282
|
...wheelProps,
|
|
68194
68283
|
children: HOURS.map(hour => jsx(Wheel.Item, {
|
|
68195
68284
|
value: hour,
|
|
@@ -68205,6 +68294,7 @@ const TimeWheel = ({
|
|
|
68205
68294
|
bounded: !loop,
|
|
68206
68295
|
size: size,
|
|
68207
68296
|
"aria-label": minuteLabel,
|
|
68297
|
+
defaultValue: placeholderParts ? placeholderParts.minute : undefined,
|
|
68208
68298
|
...wheelProps,
|
|
68209
68299
|
children: minutes.map(minute => jsx(Wheel.Item, {
|
|
68210
68300
|
value: minute,
|
|
@@ -68242,6 +68332,12 @@ const TimeWheel = ({
|
|
|
68242
68332
|
* one that goes backwards is not. It is what the bounds keep between them as
|
|
68243
68333
|
* they turn — turn the start into the end and the end moves along, keeping
|
|
68244
68334
|
* that much room.
|
|
68335
|
+
* @param {{ start?: string, end?: string }} [placeholder] What the two wheels
|
|
68336
|
+
* show while the span holds nothing — a position, since wheels have no blank
|
|
68337
|
+
* row to land on, and not an answer: the value stays `undefined` until one of
|
|
68338
|
+
* them is turned. One turn settles both, the untouched bound included, left
|
|
68339
|
+
* where the placeholder put it. For a span that is optional ("any time of
|
|
68340
|
+
* day") and still has to show hours.
|
|
68245
68341
|
* @param {object} [timeProps] Anything a `TimeWheel` takes, said once for both
|
|
68246
68342
|
* of them. `startTimeProps`/`endTimeProps` say it to one of the two, and win
|
|
68247
68343
|
* over this one.
|
|
@@ -68250,6 +68346,7 @@ const TimeRangeWheel = ({
|
|
|
68250
68346
|
minuteStep = 1,
|
|
68251
68347
|
minDuration = 0,
|
|
68252
68348
|
loop = true,
|
|
68349
|
+
placeholder,
|
|
68253
68350
|
size,
|
|
68254
68351
|
startLabel = naviI18n("time_range.from"),
|
|
68255
68352
|
endLabel = naviI18n("time_range.to"),
|
|
@@ -68261,6 +68358,12 @@ const TimeRangeWheel = ({
|
|
|
68261
68358
|
const startId = useId();
|
|
68262
68359
|
const startRef = useRef(null);
|
|
68263
68360
|
const endRef = useRef(null);
|
|
68361
|
+
// One turn settles the whole span: a start somebody chose makes the end an
|
|
68362
|
+
// answer too, left where the placeholder put it.
|
|
68363
|
+
const {
|
|
68364
|
+
answeredRef,
|
|
68365
|
+
aggregateChildStates
|
|
68366
|
+
} = useAnswered(placeholder, rest, aggregateSpan);
|
|
68264
68367
|
|
|
68265
68368
|
// What the pair does while it is being turned: the bound that just moved is
|
|
68266
68369
|
// the one the user is holding, so it stays where it was put and the OTHER one
|
|
@@ -68297,51 +68400,127 @@ const TimeRangeWheel = ({
|
|
|
68297
68400
|
event: e
|
|
68298
68401
|
});
|
|
68299
68402
|
};
|
|
68300
|
-
return
|
|
68403
|
+
return jsx(ControlGroup, {
|
|
68301
68404
|
flex: true,
|
|
68302
68405
|
alignY: "center",
|
|
68303
68406
|
spacing: "s",
|
|
68304
68407
|
size: size,
|
|
68408
|
+
aggregateChildStates: aggregateChildStates,
|
|
68305
68409
|
...rest,
|
|
68306
|
-
children:
|
|
68307
|
-
|
|
68308
|
-
children: startLabel
|
|
68309
|
-
|
|
68310
|
-
|
|
68311
|
-
|
|
68312
|
-
|
|
68313
|
-
|
|
68314
|
-
|
|
68315
|
-
|
|
68316
|
-
|
|
68317
|
-
|
|
68318
|
-
|
|
68319
|
-
|
|
68320
|
-
|
|
68321
|
-
|
|
68322
|
-
|
|
68323
|
-
|
|
68324
|
-
|
|
68325
|
-
|
|
68326
|
-
|
|
68327
|
-
|
|
68328
|
-
|
|
68329
|
-
|
|
68330
|
-
|
|
68331
|
-
|
|
68332
|
-
|
|
68333
|
-
|
|
68334
|
-
|
|
68335
|
-
|
|
68336
|
-
|
|
68337
|
-
|
|
68410
|
+
children: jsxs(AnsweredContext.Provider, {
|
|
68411
|
+
value: answeredRef,
|
|
68412
|
+
children: [startLabel === null ? null : jsx(Text, {
|
|
68413
|
+
size: size,
|
|
68414
|
+
children: startLabel
|
|
68415
|
+
}), jsx(TimeWheel, {
|
|
68416
|
+
id: startId,
|
|
68417
|
+
ref: startRef,
|
|
68418
|
+
name: "start",
|
|
68419
|
+
minuteStep: minuteStep,
|
|
68420
|
+
loop: loop,
|
|
68421
|
+
size: size,
|
|
68422
|
+
placeholder: placeholder ? placeholder.start : undefined,
|
|
68423
|
+
uiAction: (value, e) => keepBoundsApart("start", value, e),
|
|
68424
|
+
...timeProps,
|
|
68425
|
+
...startTimeProps
|
|
68426
|
+
}), endLabel === null ? null : jsx(Text, {
|
|
68427
|
+
size: size,
|
|
68428
|
+
children: endLabel
|
|
68429
|
+
}), jsx(TimeWheel, {
|
|
68430
|
+
ref: endRef,
|
|
68431
|
+
name: "end",
|
|
68432
|
+
minuteStep: minuteStep,
|
|
68433
|
+
loop: loop,
|
|
68434
|
+
size: size,
|
|
68435
|
+
placeholder: placeholder ? placeholder.end : undefined,
|
|
68436
|
+
uiAction: (value, e) => keepBoundsApart("end", value, e)
|
|
68437
|
+
// Which time it comes after, and how much room there must be between
|
|
68438
|
+
// the two: said on the LATER of the two, so the answer is given where
|
|
68439
|
+
// the time one would have to move is (see time_range_constraint.js).
|
|
68440
|
+
,
|
|
68441
|
+
"data-time-after": startId,
|
|
68442
|
+
"data-time-min-duration": minDuration,
|
|
68443
|
+
...timeProps,
|
|
68444
|
+
...endTimeProps
|
|
68445
|
+
})]
|
|
68446
|
+
})
|
|
68338
68447
|
});
|
|
68339
68448
|
};
|
|
68449
|
+
|
|
68450
|
+
/**
|
|
68451
|
+
* Wheels always show something — there is no blank row to land on — so a pair of
|
|
68452
|
+
* them cannot say "nothing set" by looking empty. Their `placeholder` is
|
|
68453
|
+
* therefore a position rather than a grey word: shown like a value, and not one.
|
|
68454
|
+
* The value stays `undefined` until a wheel is turned, which is what tells "any
|
|
68455
|
+
* time of day" from a span somebody chose. `defaultValue` remains what it is
|
|
68456
|
+
* everywhere else — a time that IS the answer.
|
|
68457
|
+
*
|
|
68458
|
+
* What counts as turned is read from the value itself rather than from a
|
|
68459
|
+
* gesture: while nothing has moved off the placeholder, nothing is set; the
|
|
68460
|
+
* moment it differs, it is an answer and stays one, even turned back onto the
|
|
68461
|
+
* placeholder. A wheel's own `uiAction` runs after its group has aggregated, so
|
|
68462
|
+
* a flag set from there would always be one turn late.
|
|
68463
|
+
*
|
|
68464
|
+
* The flag is a ref rather than state because the aggregate a group is created
|
|
68465
|
+
* with is the one it keeps: swapping the function on a later render changes
|
|
68466
|
+
* nothing (see useUIGroupStateController). One stable function reading one ref.
|
|
68467
|
+
*
|
|
68468
|
+
* A pair shares ONE flag through `AnsweredContext`: turning the start settles
|
|
68469
|
+
* the end too, left where the placeholder put it. Each of the two times gating
|
|
68470
|
+
* on its own would answer half a span — and would leave the pair nothing to
|
|
68471
|
+
* compare its own placeholder against.
|
|
68472
|
+
*/
|
|
68473
|
+
const AnsweredContext = createContext(null);
|
|
68474
|
+
const useAnswered = (placeholder, props, aggregateWhenAnswered) => {
|
|
68475
|
+
const answeredFromPair = useContext(AnsweredContext);
|
|
68476
|
+
const ownAnsweredRef = useRef(false);
|
|
68477
|
+
const answeredRef = answeredFromPair || ownAnsweredRef;
|
|
68478
|
+
if (!placeholder || isAnswerGivenByProps(props)) {
|
|
68479
|
+
answeredRef.current = true;
|
|
68480
|
+
}
|
|
68481
|
+
// Inside a pair, only the pair decides: a time that gated on its own would
|
|
68482
|
+
// hand the pair nothing to compare, and half a span cannot be read.
|
|
68483
|
+
const gates = !answeredFromPair;
|
|
68484
|
+
const placeholderRef = useRef(placeholder);
|
|
68485
|
+
placeholderRef.current = placeholder;
|
|
68486
|
+
const aggregateRef = useRef(null);
|
|
68487
|
+
if (!aggregateRef.current) {
|
|
68488
|
+
aggregateRef.current = children => {
|
|
68489
|
+
const aggregated = aggregateWhenAnswered(children);
|
|
68490
|
+
if (answeredRef.current) {
|
|
68491
|
+
return aggregated;
|
|
68492
|
+
}
|
|
68493
|
+
if (compareTwoJsValues(aggregated, placeholderRef.current)) {
|
|
68494
|
+
return undefined;
|
|
68495
|
+
}
|
|
68496
|
+
// It moved: from here on this is an answer, and stays one even when it is
|
|
68497
|
+
// turned back onto the placeholder — somebody chose that time.
|
|
68498
|
+
answeredRef.current = true;
|
|
68499
|
+
return aggregated;
|
|
68500
|
+
};
|
|
68501
|
+
}
|
|
68502
|
+
return {
|
|
68503
|
+
answeredRef,
|
|
68504
|
+
aggregateChildStates: gates ? aggregateRef.current : aggregateWhenAnswered
|
|
68505
|
+
};
|
|
68506
|
+
};
|
|
68507
|
+
const isAnswerGivenByProps = props => props.value !== undefined || props.defaultValue !== undefined || props.signal && props.signal.value !== undefined;
|
|
68340
68508
|
const HOURS = Array.from({
|
|
68341
68509
|
length: HOUR_COUNT
|
|
68342
68510
|
}, (_, hour) => hour);
|
|
68343
68511
|
const padTwo = value => String(value).padStart(2, "0");
|
|
68344
68512
|
|
|
68513
|
+
// The two times as one span, { start, end } — the shape a pair carries.
|
|
68514
|
+
const aggregateSpan = childUIStateControllers => {
|
|
68515
|
+
const span = {};
|
|
68516
|
+
for (const child of childUIStateControllers) {
|
|
68517
|
+
if (child.name === "start" || child.name === "end") {
|
|
68518
|
+
span[child.name] = child.uiState;
|
|
68519
|
+
}
|
|
68520
|
+
}
|
|
68521
|
+
return span;
|
|
68522
|
+
};
|
|
68523
|
+
|
|
68345
68524
|
// The two wheels as one value, "HH:MM".
|
|
68346
68525
|
const aggregateTime = childUIStateControllers => {
|
|
68347
68526
|
let hour = "";
|