@jsenv/navi 0.27.66 → 0.27.68
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 +1065 -1058
- package/dist/jsenv_navi.js.map +15 -14
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -17418,7 +17418,7 @@ const createCalloutManager = (
|
|
|
17418
17418
|
return;
|
|
17419
17419
|
}
|
|
17420
17420
|
const resolvedAnchorElement =
|
|
17421
|
-
anchorElement || controller.
|
|
17421
|
+
anchorElement || controller.ref.current;
|
|
17422
17422
|
const removeCloseOnCleanup = addTeardown?.(() => {
|
|
17423
17423
|
requestCloseCallout(new CustomEvent("cleanup"), "cleanup");
|
|
17424
17424
|
});
|
|
@@ -17443,7 +17443,7 @@ const createCalloutManager = (
|
|
|
17443
17443
|
tokenData.onClose?.();
|
|
17444
17444
|
}
|
|
17445
17445
|
tokens.clear();
|
|
17446
|
-
const element = controller.
|
|
17446
|
+
const element = controller.ref.current;
|
|
17447
17447
|
if (
|
|
17448
17448
|
shouldTransferFocusFromCallout &&
|
|
17449
17449
|
element &&
|
|
@@ -19491,7 +19491,7 @@ const REQUIRED_CONSTRAINT = {
|
|
|
19491
19491
|
}
|
|
19492
19492
|
return {
|
|
19493
19493
|
message: naviI18n("constraint.required.radio"),
|
|
19494
|
-
target: field.
|
|
19494
|
+
target: field.ref.current,
|
|
19495
19495
|
};
|
|
19496
19496
|
}
|
|
19497
19497
|
if (type === "radio") {
|
|
@@ -19505,7 +19505,7 @@ const REQUIRED_CONSTRAINT = {
|
|
|
19505
19505
|
}
|
|
19506
19506
|
return {
|
|
19507
19507
|
message: naviI18n("constraint.required.radio"),
|
|
19508
|
-
target: field.
|
|
19508
|
+
target: field.ref.current,
|
|
19509
19509
|
};
|
|
19510
19510
|
}
|
|
19511
19511
|
|
|
@@ -19517,7 +19517,7 @@ const REQUIRED_CONSTRAINT = {
|
|
|
19517
19517
|
}
|
|
19518
19518
|
return {
|
|
19519
19519
|
message: naviI18n("constraint.required.checkbox_group"),
|
|
19520
|
-
target: field.
|
|
19520
|
+
target: field.ref.current,
|
|
19521
19521
|
};
|
|
19522
19522
|
}
|
|
19523
19523
|
if (type === "checkbox") {
|
|
@@ -20356,7 +20356,7 @@ const createControlValidation = (
|
|
|
20356
20356
|
|
|
20357
20357
|
const checkValidity = ({
|
|
20358
20358
|
event,
|
|
20359
|
-
requester = controller.
|
|
20359
|
+
requester = controller.ref.current,
|
|
20360
20360
|
fromRequestAction,
|
|
20361
20361
|
} = {}) => {
|
|
20362
20362
|
if (fromRequestAction) {
|
|
@@ -20399,7 +20399,7 @@ const createControlValidation = (
|
|
|
20399
20399
|
...DEFAULT_CONSTRAINT_SET,
|
|
20400
20400
|
...dynamicConstraintSet,
|
|
20401
20401
|
]);
|
|
20402
|
-
const elementSig = getElementSignature(controller.
|
|
20402
|
+
const elementSig = getElementSignature(controller.ref.current);
|
|
20403
20403
|
debugUIState(
|
|
20404
20404
|
event,
|
|
20405
20405
|
`check ${elementSig}: ${constraintSet.size} constraints`,
|
|
@@ -20454,7 +20454,7 @@ const createControlValidation = (
|
|
|
20454
20454
|
if (activeFailedConstraintInfo) {
|
|
20455
20455
|
const titleLess = controller.controlHostProps.title === undefined;
|
|
20456
20456
|
if (titleLess) {
|
|
20457
|
-
const element = controller.
|
|
20457
|
+
const element = controller.ref.current;
|
|
20458
20458
|
if (element) {
|
|
20459
20459
|
element.setAttribute(
|
|
20460
20460
|
"title",
|
|
@@ -20465,7 +20465,7 @@ const createControlValidation = (
|
|
|
20465
20465
|
} else {
|
|
20466
20466
|
const titleLess = controller.controlHostProps.title === undefined;
|
|
20467
20467
|
if (titleLess) {
|
|
20468
|
-
const element = controller.
|
|
20468
|
+
const element = controller.ref.current;
|
|
20469
20469
|
if (element) {
|
|
20470
20470
|
element.removeAttribute("title");
|
|
20471
20471
|
}
|
|
@@ -20479,7 +20479,7 @@ const createControlValidation = (
|
|
|
20479
20479
|
!compareTwoJsValues(constraintValidityState, newConstraintValidityState)
|
|
20480
20480
|
) {
|
|
20481
20481
|
constraintValidityState = newConstraintValidityState;
|
|
20482
|
-
const element = controller.
|
|
20482
|
+
const element = controller.ref.current;
|
|
20483
20483
|
if (element) {
|
|
20484
20484
|
debugUIState(
|
|
20485
20485
|
event,
|
|
@@ -20529,7 +20529,7 @@ const createControlValidation = (
|
|
|
20529
20529
|
event,
|
|
20530
20530
|
{ report = false, fromRequestAction = false } = {},
|
|
20531
20531
|
) => {
|
|
20532
|
-
const elementSig = getElementSignature(controller.
|
|
20532
|
+
const elementSig = getElementSignature(controller.ref.current);
|
|
20533
20533
|
const isValid = checkValidity({ event, fromRequestAction });
|
|
20534
20534
|
if (failingManagedControlValidity) {
|
|
20535
20535
|
// Group/form case: find the actual failing leaf and report on it.
|
|
@@ -21022,7 +21022,7 @@ const createControlInteraction = (
|
|
|
21022
21022
|
// Keep title attribute in sync for accessibility.
|
|
21023
21023
|
const titleLess = !controller.controlHostProps?.title;
|
|
21024
21024
|
if (titleLess) {
|
|
21025
|
-
const element = controller.
|
|
21025
|
+
const element = controller.ref.current;
|
|
21026
21026
|
if (element) {
|
|
21027
21027
|
if (interactionFailedConstraintInfo) {
|
|
21028
21028
|
element.setAttribute(
|
|
@@ -21236,7 +21236,7 @@ const tryActionAfterInteractionAllowed = (
|
|
|
21236
21236
|
if (controller) {
|
|
21237
21237
|
const proxyTargetController = findControlProxyTargetController(controller);
|
|
21238
21238
|
if (proxyTargetController) {
|
|
21239
|
-
elementForAction = proxyTargetController.
|
|
21239
|
+
elementForAction = proxyTargetController.ref.current;
|
|
21240
21240
|
}
|
|
21241
21241
|
const activeController = proxyTargetController ?? controller;
|
|
21242
21242
|
uiState = activeController?.uiState;
|
|
@@ -22344,6 +22344,36 @@ const createControlRules = (
|
|
|
22344
22344
|
};
|
|
22345
22345
|
};
|
|
22346
22346
|
|
|
22347
|
+
/**
|
|
22348
|
+
* Returns a stable object that is mutated across renders.
|
|
22349
|
+
* Closures that capture the returned reference always read current values
|
|
22350
|
+
* because the same object reference is reused — no stale captures.
|
|
22351
|
+
*
|
|
22352
|
+
* - `init(scope)` — called **once** on mount. Receives the (initially empty)
|
|
22353
|
+
* stable scope object and returns properties to assign to it. Use this to
|
|
22354
|
+
* create anything that must live for the component's full lifetime: signals,
|
|
22355
|
+
* pubsub pairs, the controller object itself, etc.
|
|
22356
|
+
*
|
|
22357
|
+
* - `update(scope)` — called on every subsequent render. Receives the current
|
|
22358
|
+
* scope and returns properties to update. Use this to sync mutable values
|
|
22359
|
+
* (current props, parent controller, etc.) and run side-effectful logic like
|
|
22360
|
+
* checking whether a controlled `state` prop changed.
|
|
22361
|
+
*/
|
|
22362
|
+
const useRenderScope = (init, update) => {
|
|
22363
|
+
const scopeRef = useRef();
|
|
22364
|
+
let scope = scopeRef.current;
|
|
22365
|
+
if (!scope) {
|
|
22366
|
+
scope = {};
|
|
22367
|
+
scopeRef.current = scope;
|
|
22368
|
+
const initScope = init(scope);
|
|
22369
|
+
Object.assign(scope, initScope);
|
|
22370
|
+
} else {
|
|
22371
|
+
const updateScope = update(scope);
|
|
22372
|
+
Object.assign(scope, updateScope);
|
|
22373
|
+
}
|
|
22374
|
+
return scope;
|
|
22375
|
+
};
|
|
22376
|
+
|
|
22347
22377
|
/**
|
|
22348
22378
|
* Minimal interface that any object placed in `ParentUIStateControllerContext` must satisfy.
|
|
22349
22379
|
* Implemented by `useUIGroupStateController`, `useUIFacadeStateController`, and
|
|
@@ -22359,7 +22389,7 @@ const createControlRules = (
|
|
|
22359
22389
|
* onChildUIAction(child, e, { stateChanged: boolean }): void; // Called when a child fires a UI action
|
|
22360
22390
|
* unregisterChild(child): void; // Called on child unmount
|
|
22361
22391
|
* props: Object;
|
|
22362
|
-
*
|
|
22392
|
+
* ref: Ref; // Used to dispatch DOM events
|
|
22363
22393
|
* getManagedControls(): UIStateController[]; // Returns controls whose validity is managed by this controller
|
|
22364
22394
|
* }
|
|
22365
22395
|
* ```
|
|
@@ -22395,7 +22425,7 @@ const ParentUIStateControllerContext = createContext();
|
|
|
22395
22425
|
* **internalBehavior events** (e.g. radio_sibling_uncheck, state_prop re-sync):
|
|
22396
22426
|
* skip reactions and parent notification — they are programmatic, not user-initiated.
|
|
22397
22427
|
*
|
|
22398
|
-
* The controller exposes `
|
|
22428
|
+
* The controller exposes `ref` so parent groups can dispatch DOM events on children
|
|
22399
22429
|
* (e.g. `resetUIState` cascading `navi_reset_ui_state`).
|
|
22400
22430
|
*/
|
|
22401
22431
|
const useUIStateController = (
|
|
@@ -22413,522 +22443,512 @@ const useUIStateController = (
|
|
|
22413
22443
|
const debugUIState = useDebugUIState();
|
|
22414
22444
|
const debugFocus = useDebugFocus();
|
|
22415
22445
|
|
|
22416
|
-
const uiStateControllerRef = useRef();
|
|
22417
22446
|
const parentUIStateController = useContext(ParentUIStateControllerContext);
|
|
22418
22447
|
const formContext = useContext(FormContext);
|
|
22419
|
-
const { id, uiAction } = props;
|
|
22420
|
-
const ref = props.ref;
|
|
22421
|
-
const isProxy = Boolean(props["navi-control-proxy-for"]);
|
|
22422
22448
|
if (persists === undefined && formContext) {
|
|
22423
22449
|
persists = true;
|
|
22424
22450
|
}
|
|
22425
22451
|
const controlType = controlInfo.controlType;
|
|
22426
22452
|
const isRadio = controlType === "input" && props.type === "radio";
|
|
22427
|
-
const [
|
|
22428
|
-
notifyParentAboutChildMount,
|
|
22429
|
-
notifyParentAboutChildUIAction,
|
|
22430
|
-
notifyParentAboutChildUnmount,
|
|
22431
|
-
] = useParentControllerNotifiers(
|
|
22432
|
-
parentUIStateController,
|
|
22433
|
-
uiStateControllerRef,
|
|
22434
|
-
controlType,
|
|
22435
|
-
debugUIState,
|
|
22436
|
-
);
|
|
22437
|
-
useLayoutEffect(() => {
|
|
22438
|
-
const controller = uiStateControllerRef.current;
|
|
22439
|
-
const el = ref.current;
|
|
22440
|
-
if (el) {
|
|
22441
|
-
el.__uiStateController__ = controller;
|
|
22442
|
-
}
|
|
22443
|
-
notifyParentAboutChildMount();
|
|
22444
|
-
return () => {
|
|
22445
|
-
if (el && el.__uiStateController__ === controller) {
|
|
22446
|
-
delete el.__uiStateController__;
|
|
22447
|
-
}
|
|
22448
|
-
notifyParentAboutChildUnmount();
|
|
22449
|
-
onUIStateControllerDestroyed(controller);
|
|
22450
|
-
};
|
|
22451
|
-
}, []);
|
|
22453
|
+
const isProxy = Boolean(props["navi-control-proxy-for"]);
|
|
22452
22454
|
|
|
22453
|
-
const
|
|
22454
|
-
|
|
22455
|
-
|
|
22456
|
-
|
|
22457
|
-
|
|
22458
|
-
|
|
22459
|
-
|
|
22460
|
-
|
|
22461
|
-
|
|
22462
|
-
|
|
22463
|
-
|
|
22464
|
-
|
|
22465
|
-
|
|
22466
|
-
|
|
22467
|
-
|
|
22468
|
-
|
|
22469
|
-
|
|
22470
|
-
|
|
22471
|
-
|
|
22472
|
-
|
|
22473
|
-
|
|
22474
|
-
|
|
22475
|
-
|
|
22476
|
-
|
|
22477
|
-
|
|
22478
|
-
|
|
22455
|
+
const scope = useRenderScope(
|
|
22456
|
+
// ── init: runs once on mount ───────────────────────────────────────────
|
|
22457
|
+
// Creates the controller and all long-lived objects. Captures first-render
|
|
22458
|
+
// values for stable config (controlType, isRadio…); live values are read
|
|
22459
|
+
// through `s` which is always updated by `update` before any method call.
|
|
22460
|
+
(s) => {
|
|
22461
|
+
const parentUiStateSignalHolder = signal(
|
|
22462
|
+
parentUIStateController?.uiStateSignal ?? null,
|
|
22463
|
+
);
|
|
22464
|
+
const { stateInitial } = controlInfo;
|
|
22465
|
+
debugUIState(
|
|
22466
|
+
`Creating "${controlType}" ui state controller - initial state:`,
|
|
22467
|
+
JSON.stringify(stateInitial),
|
|
22468
|
+
);
|
|
22469
|
+
const [publishUIState, subscribeUIState] = createPubSub();
|
|
22470
|
+
const ownUIStateSignal = signal(stateInitial);
|
|
22471
|
+
const inherit =
|
|
22472
|
+
controlType === "button" &&
|
|
22473
|
+
!controlInfo.hasStateProp &&
|
|
22474
|
+
parentUIStateController;
|
|
22475
|
+
const uiStateSignal = inherit
|
|
22476
|
+
? computed(() => {
|
|
22477
|
+
const parentSig = parentUiStateSignalHolder.value;
|
|
22478
|
+
const parentUIState = parentSig?.value;
|
|
22479
|
+
const ownUIState = ownUIStateSignal.value;
|
|
22480
|
+
return ownUIState || parentUIState;
|
|
22481
|
+
})
|
|
22482
|
+
: ownUIStateSignal;
|
|
22479
22483
|
|
|
22480
|
-
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
22484
|
-
|
|
22485
|
-
|
|
22486
|
-
uiStateController.id = props.id; // never suppoed to changed, not supported for now
|
|
22487
|
-
uiStateController.name = props.name;
|
|
22484
|
+
const controller = {
|
|
22485
|
+
controlType,
|
|
22486
|
+
parentUIStateController,
|
|
22487
|
+
parentUiStateSignalHolder,
|
|
22488
|
+
isProxy,
|
|
22489
|
+
allowNameless,
|
|
22488
22490
|
|
|
22489
|
-
|
|
22490
|
-
|
|
22491
|
-
|
|
22492
|
-
|
|
22493
|
-
|
|
22494
|
-
|
|
22495
|
-
|
|
22496
|
-
|
|
22497
|
-
|
|
22498
|
-
|
|
22491
|
+
props,
|
|
22492
|
+
ref: props.ref,
|
|
22493
|
+
id: props.id,
|
|
22494
|
+
name: props.name,
|
|
22495
|
+
|
|
22496
|
+
state: stateInitial,
|
|
22497
|
+
uiState: stateInitial,
|
|
22498
|
+
uiStateSignal,
|
|
22499
|
+
value: controlInfo.value,
|
|
22500
|
+
|
|
22501
|
+
facadeChild: null,
|
|
22502
|
+
getManagedControls: () => {
|
|
22503
|
+
if (controller.facadeChild) {
|
|
22504
|
+
const child = controller.facadeChild;
|
|
22505
|
+
const childManaged = child.getManagedControls();
|
|
22506
|
+
if (childManaged.length > 0) return childManaged;
|
|
22507
|
+
return [child];
|
|
22508
|
+
}
|
|
22509
|
+
return [];
|
|
22510
|
+
},
|
|
22511
|
+
onUIAction: (e, { skipCommand } = {}) => {
|
|
22512
|
+
if (controlType === "button" && controller.controlHostProps.name) {
|
|
22513
|
+
const buttonName = controller.controlHostProps.name;
|
|
22514
|
+
const parentController = controller.parentUIStateController;
|
|
22515
|
+
if (parentController && parentController.wantRequesterButtonState) {
|
|
22516
|
+
const currentState = parentController.uiState;
|
|
22517
|
+
const mergedState = {
|
|
22518
|
+
...currentState,
|
|
22519
|
+
[buttonName]: controller.uiState,
|
|
22520
|
+
};
|
|
22521
|
+
parentController.syncInternalState(mergedState);
|
|
22522
|
+
debugUIState(
|
|
22523
|
+
`merging button state into parent control group:`,
|
|
22524
|
+
mergedState,
|
|
22525
|
+
);
|
|
22526
|
+
}
|
|
22527
|
+
}
|
|
22528
|
+
// Trigger uiAction/command side effects without changing UI state.
|
|
22529
|
+
const currentUIState = controller.uiState;
|
|
22530
|
+
s.uiActionInternal?.(currentUIState, e);
|
|
22531
|
+
if (s.uiAction) {
|
|
22532
|
+
debugUIState(`calling uiAction for ${controlType}`, currentUIState);
|
|
22533
|
+
s.uiAction(currentUIState, e);
|
|
22534
|
+
}
|
|
22535
|
+
if (skipCommand) ; else {
|
|
22536
|
+
const command = controller.controlHostProps.command;
|
|
22537
|
+
if (command) {
|
|
22538
|
+
const element = controller.ref.current;
|
|
22539
|
+
if (element) {
|
|
22540
|
+
debugUIState(
|
|
22541
|
+
`triggering command "${command}" for "${controlType}"`,
|
|
22542
|
+
);
|
|
22543
|
+
triggerNaviCommand(element, command, e);
|
|
22544
|
+
}
|
|
22545
|
+
}
|
|
22546
|
+
}
|
|
22547
|
+
},
|
|
22548
|
+
setUIState: (newUIState, e) => {
|
|
22549
|
+
const guardResult = controller.rules.guard.checkUIState(
|
|
22550
|
+
newUIState,
|
|
22551
|
+
e,
|
|
22499
22552
|
);
|
|
22500
|
-
|
|
22501
|
-
|
|
22502
|
-
|
|
22503
|
-
|
|
22504
|
-
|
|
22505
|
-
|
|
22506
|
-
|
|
22507
|
-
|
|
22508
|
-
|
|
22509
|
-
|
|
22510
|
-
allowNameless,
|
|
22511
|
-
elementRef: ref,
|
|
22512
|
-
props,
|
|
22513
|
-
|
|
22514
|
-
id: props.id,
|
|
22515
|
-
name: props.name,
|
|
22516
|
-
state: stateInitial,
|
|
22517
|
-
uiState: stateInitial,
|
|
22518
|
-
uiStateSignal,
|
|
22519
|
-
value: controlInfo.value,
|
|
22520
|
-
|
|
22521
|
-
facadeChild: null,
|
|
22522
|
-
getManagedControls: () => {
|
|
22523
|
-
if (uiStateController.facadeChild) {
|
|
22524
|
-
const child = uiStateController.facadeChild;
|
|
22525
|
-
const childManaged = child.getManagedControls();
|
|
22526
|
-
if (childManaged.length > 0) {
|
|
22527
|
-
return childManaged;
|
|
22528
|
-
}
|
|
22529
|
-
return [child];
|
|
22530
|
-
}
|
|
22531
|
-
return [];
|
|
22532
|
-
},
|
|
22533
|
-
onUIAction: (e, { skipCommand } = {}) => {
|
|
22534
|
-
if (controlType === "button" && uiStateController.controlHostProps.name) {
|
|
22535
|
-
const buttonName = uiStateController.controlHostProps.name;
|
|
22536
|
-
const parentController = uiStateController.parentUIStateController;
|
|
22537
|
-
if (parentController && parentController.wantRequesterButtonState) {
|
|
22538
|
-
const currentState = parentController.uiState;
|
|
22539
|
-
const mergedState = {
|
|
22540
|
-
...currentState,
|
|
22541
|
-
[buttonName]: uiStateController.uiState,
|
|
22542
|
-
};
|
|
22543
|
-
parentController.syncInternalState(mergedState);
|
|
22544
|
-
debugUIState(
|
|
22545
|
-
`merging button state into parent control group:`,
|
|
22546
|
-
mergedState,
|
|
22553
|
+
if (guardResult) {
|
|
22554
|
+
if (Object.hasOwn(guardResult, "fixedValue")) {
|
|
22555
|
+
newUIState = guardResult.fixedValue;
|
|
22556
|
+
// fall through — continue with truncated value (callout already shown by guard)
|
|
22557
|
+
} else {
|
|
22558
|
+
return false;
|
|
22559
|
+
}
|
|
22560
|
+
}
|
|
22561
|
+
const controllerSig = getElementSignature(
|
|
22562
|
+
e.currentTarget || controller.ref.current,
|
|
22547
22563
|
);
|
|
22548
|
-
|
|
22549
|
-
|
|
22550
|
-
|
|
22551
|
-
|
|
22552
|
-
|
|
22553
|
-
|
|
22554
|
-
|
|
22555
|
-
|
|
22556
|
-
|
|
22557
|
-
if (skipCommand) ; else {
|
|
22558
|
-
const command = uiStateController.controlHostProps.command;
|
|
22559
|
-
if (command) {
|
|
22560
|
-
const element = uiStateController.elementRef.current;
|
|
22561
|
-
if (element) {
|
|
22564
|
+
const currentUIState = controller.uiState;
|
|
22565
|
+
const stateIsTheSame = compareTwoJsValues(newUIState, currentUIState);
|
|
22566
|
+
if (stateIsTheSame) {
|
|
22567
|
+
if (controlType === "button" || controlType === "link") {
|
|
22568
|
+
if (!isInternalEvent(e)) {
|
|
22569
|
+
controller.onUIAction(e);
|
|
22570
|
+
}
|
|
22571
|
+
return true;
|
|
22572
|
+
}
|
|
22562
22573
|
debugUIState(
|
|
22563
|
-
|
|
22574
|
+
e,
|
|
22575
|
+
`${controllerSig}.setUIState(${JSON.stringify(newUIState)}, "${e.type}") -> state unchanged, no update needed`,
|
|
22564
22576
|
);
|
|
22565
|
-
|
|
22577
|
+
if (
|
|
22578
|
+
controlType === "input" &&
|
|
22579
|
+
controller.controlHostProps.type === "radio" &&
|
|
22580
|
+
!isInternalEvent(e)
|
|
22581
|
+
) {
|
|
22582
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
22583
|
+
stateChanged: false,
|
|
22584
|
+
});
|
|
22585
|
+
}
|
|
22586
|
+
if (e.currentTarget === null) {
|
|
22587
|
+
// A stale/reused event (currentTarget is null) means this is a debounced
|
|
22588
|
+
// callback firing the original input event after a timeout. The state hasn't
|
|
22589
|
+
// changed and this is not a live user gesture — skip uiAction and command.
|
|
22590
|
+
return false;
|
|
22591
|
+
}
|
|
22592
|
+
if (e.type === "state_prop_change") {
|
|
22593
|
+
// state_prop_change with the same uiState means the state prop was updated
|
|
22594
|
+
// to match what the user already has in the UI (e.g. action completed and
|
|
22595
|
+
// synced state back). No real user gesture — skip uiAction and command.
|
|
22596
|
+
return false;
|
|
22597
|
+
}
|
|
22598
|
+
if (e.type === "change") {
|
|
22599
|
+
// "change" fires after "input" for native inputs (date, color, etc.).
|
|
22600
|
+
// The "input" event already updated the state and fired uiAction.
|
|
22601
|
+
// When state is unchanged here it means "input" already ran — skip to
|
|
22602
|
+
// avoid a duplicate uiAction on the same user gesture.
|
|
22603
|
+
return false;
|
|
22604
|
+
}
|
|
22605
|
+
controller.onUIAction(e);
|
|
22606
|
+
return false;
|
|
22566
22607
|
}
|
|
22567
|
-
|
|
22568
|
-
|
|
22569
|
-
|
|
22570
|
-
|
|
22571
|
-
|
|
22572
|
-
|
|
22573
|
-
|
|
22574
|
-
|
|
22575
|
-
|
|
22576
|
-
|
|
22577
|
-
|
|
22578
|
-
//
|
|
22579
|
-
|
|
22580
|
-
|
|
22581
|
-
|
|
22582
|
-
|
|
22583
|
-
|
|
22584
|
-
|
|
22585
|
-
|
|
22586
|
-
|
|
22587
|
-
|
|
22588
|
-
|
|
22589
|
-
|
|
22590
|
-
|
|
22591
|
-
|
|
22592
|
-
|
|
22608
|
+
// set immediatly (don't wait for preact re-render) so ui is in the right state for:
|
|
22609
|
+
// - side effect
|
|
22610
|
+
// - any "input" event that might be dispatched below
|
|
22611
|
+
syncDomState(newUIState, e);
|
|
22612
|
+
controller.uiState = newUIState;
|
|
22613
|
+
ownUIStateSignal.value = newUIState;
|
|
22614
|
+
const controlProxyFor =
|
|
22615
|
+
controller.controlHostProps["navi-control-proxy-for"];
|
|
22616
|
+
// Radio group: when a radio becomes checked, uncheck all siblings.
|
|
22617
|
+
// We only update their UIState — no parent notification, no synthetic
|
|
22618
|
+
// input event (the browser never fires input on the unchecked radios,
|
|
22619
|
+
// and we don't want to trigger their action flow with a stale DOM value).
|
|
22620
|
+
// Uses the in-memory registry instead of DOM queries so this works even
|
|
22621
|
+
// when sibling items are virtualized (not in the DOM).
|
|
22622
|
+
// Form scoping is preserved by comparing parentUIStateController references.
|
|
22623
|
+
if (isRadio && newUIState && controller.name && !controlProxyFor) {
|
|
22624
|
+
const siblings = getRadioSiblings(controller);
|
|
22625
|
+
if (siblings) {
|
|
22626
|
+
const siblingUncheckEvent = new CustomEvent(
|
|
22627
|
+
"radio_sibling_uncheck",
|
|
22628
|
+
{ detail: {} },
|
|
22629
|
+
);
|
|
22630
|
+
chainEvent(siblingUncheckEvent, e);
|
|
22631
|
+
for (const siblingController of siblings) {
|
|
22632
|
+
if (siblingController === controller) continue;
|
|
22633
|
+
if (
|
|
22634
|
+
siblingController.parentUIStateController !==
|
|
22635
|
+
s.parentUIStateController
|
|
22636
|
+
) {
|
|
22637
|
+
continue;
|
|
22638
|
+
}
|
|
22639
|
+
siblingController.setUIState(undefined, siblingUncheckEvent);
|
|
22640
|
+
}
|
|
22641
|
+
}
|
|
22593
22642
|
}
|
|
22594
|
-
|
|
22595
|
-
|
|
22596
|
-
|
|
22597
|
-
|
|
22598
|
-
|
|
22599
|
-
|
|
22600
|
-
|
|
22601
|
-
|
|
22602
|
-
|
|
22603
|
-
|
|
22604
|
-
|
|
22605
|
-
|
|
22606
|
-
|
|
22607
|
-
|
|
22608
|
-
|
|
22609
|
-
|
|
22610
|
-
|
|
22611
|
-
|
|
22612
|
-
|
|
22613
|
-
|
|
22614
|
-
|
|
22615
|
-
|
|
22616
|
-
|
|
22617
|
-
|
|
22618
|
-
|
|
22619
|
-
|
|
22620
|
-
|
|
22621
|
-
|
|
22622
|
-
|
|
22623
|
-
|
|
22624
|
-
|
|
22625
|
-
|
|
22626
|
-
|
|
22627
|
-
|
|
22628
|
-
|
|
22629
|
-
|
|
22630
|
-
|
|
22631
|
-
|
|
22632
|
-
|
|
22633
|
-
|
|
22634
|
-
|
|
22635
|
-
|
|
22636
|
-
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
22640
|
-
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
|
|
22645
|
-
|
|
22646
|
-
|
|
22647
|
-
|
|
22648
|
-
|
|
22649
|
-
|
|
22650
|
-
|
|
22651
|
-
|
|
22652
|
-
|
|
22653
|
-
|
|
22643
|
+
debugUIState(e, `publishUIState(${JSON.stringify(newUIState)})`);
|
|
22644
|
+
publishUIState(newUIState, e);
|
|
22645
|
+
const el = controller.ref.current;
|
|
22646
|
+
// Always notify the element that its UI state changed.
|
|
22647
|
+
// Listeners use this to stay in sync (e.g. input_effect.js tracks currentState,
|
|
22648
|
+
// useUIState subscribes for reactive updates). Separate from navi_set_ui_state
|
|
22649
|
+
// which is the command; navi_ui_state_change is the notification.
|
|
22650
|
+
if (el) {
|
|
22651
|
+
dispatchInternalCustomEvent(el, "navi_ui_state_change", {
|
|
22652
|
+
event: e,
|
|
22653
|
+
value: newUIState,
|
|
22654
|
+
});
|
|
22655
|
+
}
|
|
22656
|
+
if (!controlProxyFor) {
|
|
22657
|
+
// When this controller is a real input that has a visible proxy
|
|
22658
|
+
// (linked via `navi-control-proxy-for`), mirror the new state to the
|
|
22659
|
+
// proxy DOM synchronously. Otherwise the proxy would only catch up
|
|
22660
|
+
// later through a React re-render — visible as e.g. two radios
|
|
22661
|
+
// appearing checked at once between the real input update and the
|
|
22662
|
+
// next render (radio_sibling_uncheck case).
|
|
22663
|
+
const proxyController = findProxyController(s.id);
|
|
22664
|
+
if (proxyController) {
|
|
22665
|
+
// Find any mounted controller that declared itself as a proxy for this one.
|
|
22666
|
+
// Communicates directly to the proxy controller — no DOM query needed.
|
|
22667
|
+
const mirrorEvent = new CustomEvent("proxy_mirror_state", {
|
|
22668
|
+
detail: {},
|
|
22669
|
+
});
|
|
22670
|
+
chainEvent(mirrorEvent, e);
|
|
22671
|
+
proxyController.setUIState(newUIState, mirrorEvent);
|
|
22672
|
+
}
|
|
22673
|
+
}
|
|
22674
|
+
if (isInternalEvent(e)) {
|
|
22675
|
+
if (e.type === "facade_child_mount_sync") {
|
|
22676
|
+
const wasEmptyString =
|
|
22677
|
+
currentUIState === "" && newUIState === undefined;
|
|
22678
|
+
const wasUndefinedNowEmpty =
|
|
22679
|
+
currentUIState === undefined && newUIState === "";
|
|
22680
|
+
if (wasEmptyString) {
|
|
22681
|
+
console.warn(
|
|
22682
|
+
`[navi] Picker mount sync changed state from "" to undefined. ` +
|
|
22683
|
+
`This will call uiAction on mount, which is likely unintended. ` +
|
|
22684
|
+
`Initialise the signal with undefined instead of "" to avoid this.`,
|
|
22685
|
+
);
|
|
22686
|
+
} else if (wasUndefinedNowEmpty) {
|
|
22687
|
+
console.warn(
|
|
22688
|
+
`[navi] Picker mount sync changed state from undefined to "". ` +
|
|
22689
|
+
`This will call uiAction on mount, which is likely unintended. ` +
|
|
22690
|
+
`The child component is emitting "" for an empty value — it should emit undefined instead.`,
|
|
22691
|
+
);
|
|
22692
|
+
}
|
|
22693
|
+
}
|
|
22694
|
+
// initial_state_push is pure initialization (equivalent to defaultValue on the
|
|
22695
|
+
// child itself): skip uiAction entirely so no side effects fire on mount.
|
|
22696
|
+
if (e.type !== "initial_state_push") {
|
|
22697
|
+
// Still fire uiAction so external listeners (e.g. signals) stay in
|
|
22698
|
+
// sync, but do NOT fire the command and do NOT notify the parent —
|
|
22699
|
+
// both would cause an infinite loop when a parent cascades state
|
|
22700
|
+
// down to its children (child command would re-trigger the cascade).
|
|
22701
|
+
controller.onUIAction(e, { skipCommand: true });
|
|
22702
|
+
}
|
|
22703
|
+
if (e.type === "facade_propagate_up") {
|
|
22704
|
+
// Exception: when the facade propagates a child state change up to the
|
|
22705
|
+
// real picker input, also notify the parent group (e.g. Form) so it
|
|
22706
|
+
// keeps its cached aggregated state in sync and fires its own uiAction.
|
|
22707
|
+
// This is consistent with how a direct Input inside a Form behaves:
|
|
22708
|
+
// the Form's uiAction fires on every value change.
|
|
22709
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
22710
|
+
stateChanged: true,
|
|
22711
|
+
});
|
|
22654
22712
|
}
|
|
22655
22713
|
if (
|
|
22656
|
-
|
|
22657
|
-
parentUIStateController
|
|
22714
|
+
e.type === "state_prop_change" &&
|
|
22715
|
+
s.parentUIStateController &&
|
|
22716
|
+
!s.parentUIStateController.hasStateProp
|
|
22658
22717
|
) {
|
|
22659
|
-
|
|
22718
|
+
// Exception: state_prop_change can only fire on a control with its own
|
|
22719
|
+
// controlled state/value prop (see hasStateProp above) — groups never
|
|
22720
|
+
// cascade state down into such children (they're explicitly skipped,
|
|
22721
|
+
// see shouldPropagateStateToChild/hasStateProp checks), so this change
|
|
22722
|
+
// can never be an echo of the parent's own cascade. The loop risk this
|
|
22723
|
+
// suppression exists for only applies when the parent itself just pushed
|
|
22724
|
+
// this value down, which requires the parent to be controlled (have its
|
|
22725
|
+
// own state/value prop). When the parent is "stateless" (uncontrolled),
|
|
22726
|
+
// notifying it is always safe and necessary — otherwise its aggregated
|
|
22727
|
+
// state silently drifts out of sync with this child.
|
|
22728
|
+
s.parentUIStateController.onChildUIAction(controller, e, {
|
|
22729
|
+
stateChanged: true,
|
|
22730
|
+
});
|
|
22660
22731
|
}
|
|
22661
|
-
|
|
22662
|
-
}
|
|
22663
|
-
}
|
|
22664
|
-
}
|
|
22665
|
-
debugUIState(e, `publishUIState(${JSON.stringify(newUIState)})`);
|
|
22666
|
-
publishUIState(newUIState, e);
|
|
22667
|
-
const el = ref.current;
|
|
22668
|
-
// Always notify the element that its UI state changed.
|
|
22669
|
-
// Listeners use this to stay in sync (e.g. input_effect.js tracks currentState,
|
|
22670
|
-
// useUIState subscribes for reactive updates). Separate from navi_set_ui_state
|
|
22671
|
-
// which is the command; navi_ui_state_change is the notification.
|
|
22672
|
-
if (el) {
|
|
22673
|
-
dispatchInternalCustomEvent(el, "navi_ui_state_change", {
|
|
22674
|
-
event: e,
|
|
22675
|
-
value: newUIState,
|
|
22676
|
-
});
|
|
22677
|
-
}
|
|
22678
|
-
// When this controller is a real input that has a visible proxy
|
|
22679
|
-
// (linked via `navi-control-proxy-for`), mirror the new state to the
|
|
22680
|
-
// proxy DOM synchronously. Otherwise the proxy would only catch up
|
|
22681
|
-
// later through a React re-render — visible as e.g. two radios
|
|
22682
|
-
// appearing checked at once between the real input update and the
|
|
22683
|
-
// next render (radio_sibling_uncheck case).
|
|
22684
|
-
if (!controlProxyFor) {
|
|
22685
|
-
// Find any mounted controller that declared itself as a proxy for this one.
|
|
22686
|
-
// Communicates directly to the proxy controller — no DOM query needed.
|
|
22687
|
-
const proxyController = findProxyController(id);
|
|
22688
|
-
if (proxyController) {
|
|
22689
|
-
const mirrorEvent = new CustomEvent("proxy_mirror_state", {
|
|
22690
|
-
detail: {},
|
|
22691
|
-
});
|
|
22692
|
-
chainEvent(mirrorEvent, e);
|
|
22693
|
-
proxyController.setUIState(newUIState, mirrorEvent);
|
|
22694
|
-
}
|
|
22695
|
-
}
|
|
22696
|
-
if (isInternalEvent(e)) {
|
|
22697
|
-
if (e.type === "facade_child_mount_sync") {
|
|
22698
|
-
// Warn when the picker's initial signal value is "" but the list
|
|
22699
|
-
// resolved to undefined (no selection). These are semantically
|
|
22700
|
-
// equivalent but technically different, so the mount-sync registers
|
|
22701
|
-
// as a state change and fires uiAction unexpectedly.
|
|
22702
|
-
// Fix: initialise the signal with undefined instead of "".
|
|
22703
|
-
const wasEmptyString =
|
|
22704
|
-
currentUIState === "" && newUIState === undefined;
|
|
22705
|
-
const wasUndefinedNowEmpty =
|
|
22706
|
-
currentUIState === undefined && newUIState === "";
|
|
22707
|
-
if (wasEmptyString) {
|
|
22708
|
-
console.warn(
|
|
22709
|
-
`[navi] Picker mount sync changed state from "" to undefined. ` +
|
|
22710
|
-
`This will call uiAction on mount, which is likely unintended. ` +
|
|
22711
|
-
`Initialise the signal with undefined instead of "" to avoid this.`,
|
|
22712
|
-
);
|
|
22713
|
-
} else if (wasUndefinedNowEmpty) {
|
|
22714
|
-
console.warn(
|
|
22715
|
-
`[navi] Picker mount sync changed state from undefined to "". ` +
|
|
22716
|
-
`This will call uiAction on mount, which is likely unintended. ` +
|
|
22717
|
-
`The child component is emitting "" for an empty value — it should emit undefined instead.`,
|
|
22718
|
-
);
|
|
22732
|
+
return true;
|
|
22719
22733
|
}
|
|
22720
|
-
|
|
22721
|
-
|
|
22722
|
-
// child itself): skip uiAction entirely so no side effects fire on mount.
|
|
22723
|
-
if (e.type !== "initial_state_push") {
|
|
22724
|
-
// Still fire uiAction so external listeners (e.g. signals) stay in
|
|
22725
|
-
// sync, but do NOT fire the command and do NOT notify the parent —
|
|
22726
|
-
// both would cause an infinite loop when a parent cascades state
|
|
22727
|
-
// down to its children (child command would re-trigger the cascade).
|
|
22728
|
-
uiStateController.onUIAction(e, {
|
|
22729
|
-
skipCommand: true,
|
|
22730
|
-
});
|
|
22731
|
-
}
|
|
22732
|
-
// Exception: when the facade propagates a child state change up to the
|
|
22733
|
-
// real picker input, also notify the parent group (e.g. Form) so it
|
|
22734
|
-
// keeps its cached aggregated state in sync and fires its own uiAction.
|
|
22735
|
-
// This is consistent with how a direct Input inside a Form behaves:
|
|
22736
|
-
// the Form's uiAction fires on every value change.
|
|
22737
|
-
if (e.type === "facade_propagate_up") {
|
|
22738
|
-
notifyParentAboutChildUIAction(e, { stateChanged: true });
|
|
22739
|
-
}
|
|
22740
|
-
// Exception: state_prop_change can only fire on a control with its own
|
|
22741
|
-
// controlled state/value prop (see hasStateProp above) — groups never
|
|
22742
|
-
// cascade state down into such children (they're explicitly skipped,
|
|
22743
|
-
// see shouldPropagateStateToChild/hasStateProp checks), so this change
|
|
22744
|
-
// can never be an echo of the parent's own cascade. The loop risk this
|
|
22745
|
-
// suppression exists for only applies when the parent itself just pushed
|
|
22746
|
-
// this value down, which requires the parent to be controlled (have its
|
|
22747
|
-
// own state/value prop). When the parent is "stateless" (uncontrolled),
|
|
22748
|
-
// notifying it is always safe and necessary — otherwise its aggregated
|
|
22749
|
-
// state silently drifts out of sync with this child.
|
|
22750
|
-
if (
|
|
22751
|
-
e.type === "state_prop_change" &&
|
|
22752
|
-
parentUIStateController &&
|
|
22753
|
-
!parentUIStateController.hasStateProp
|
|
22754
|
-
) {
|
|
22755
|
-
notifyParentAboutChildUIAction(e, { stateChanged: true });
|
|
22756
|
-
}
|
|
22757
|
-
return true;
|
|
22758
|
-
}
|
|
22759
|
-
notifyParentAboutChildUIAction(e, { stateChanged: true });
|
|
22760
|
-
if (controlProxyFor) {
|
|
22761
|
-
// Proxy: forward the state change to the real input.
|
|
22762
|
-
// Use a dedicated internal event so that when the real input's setUIState
|
|
22763
|
-
// sees stateIsTheSame=true (already updated by the real input's own flow),
|
|
22764
|
-
// it does NOT fire notifyParentAboutChildUIAction(stateChanged=false) back
|
|
22765
|
-
// to the group — which would trigger the group action with a stale value.
|
|
22766
|
-
const targetController = getUIStateControllerById(controlProxyFor);
|
|
22767
|
-
if (targetController) {
|
|
22768
|
-
debugUIState(
|
|
22769
|
-
e,
|
|
22770
|
-
`forwarding set_ui_state "${newUIState}" to ${getElementSignature(targetController.elementRef.current)}`,
|
|
22771
|
-
);
|
|
22772
|
-
const forwardEvent = new CustomEvent("proxy_forward_set_ui_state", {
|
|
22773
|
-
detail: {},
|
|
22734
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
22735
|
+
stateChanged: true,
|
|
22774
22736
|
});
|
|
22775
|
-
|
|
22776
|
-
|
|
22777
|
-
|
|
22778
|
-
|
|
22779
|
-
|
|
22780
|
-
|
|
22781
|
-
|
|
22782
|
-
|
|
22783
|
-
const existingInputEvent = findEvent(e, (eInChain) => {
|
|
22784
|
-
return eInChain.type === "input" && eInChain.target === el;
|
|
22785
|
-
});
|
|
22786
|
-
if (!existingInputEvent) {
|
|
22787
|
-
if (el.tagName === "INPUT") {
|
|
22788
|
-
if (el.type === "radio" || el.type === "checkbox") {
|
|
22737
|
+
if (controlProxyFor) {
|
|
22738
|
+
// Proxy: forward the state change to the real input.
|
|
22739
|
+
// Use a dedicated internal event so that when the real input's setUIState
|
|
22740
|
+
// sees stateIsTheSame=true (already updated by the real input's own flow),
|
|
22741
|
+
// it does NOT fire notifyParentAboutChildUIAction(stateChanged=false) back
|
|
22742
|
+
// to the group — which would trigger the group action with a stale value.
|
|
22743
|
+
const targetController = getUIStateControllerById(controlProxyFor);
|
|
22744
|
+
if (targetController) {
|
|
22789
22745
|
debugUIState(
|
|
22790
22746
|
e,
|
|
22791
|
-
|
|
22747
|
+
`forwarding set_ui_state "${newUIState}" to ${getElementSignature(targetController.ref.current)}`,
|
|
22792
22748
|
);
|
|
22793
|
-
|
|
22794
|
-
|
|
22795
|
-
|
|
22796
|
-
debugUIState(
|
|
22797
|
-
e,
|
|
22798
|
-
`dispatching synthetic input event with data "${newUIState}" for input`,
|
|
22749
|
+
const forwardEvent = new CustomEvent(
|
|
22750
|
+
"proxy_forward_set_ui_state",
|
|
22751
|
+
{ detail: {} },
|
|
22799
22752
|
);
|
|
22800
|
-
|
|
22801
|
-
|
|
22802
|
-
bubbles: true,
|
|
22803
|
-
cancelable: true,
|
|
22804
|
-
inputType: "insertText",
|
|
22805
|
-
data: newUIState,
|
|
22806
|
-
}),
|
|
22807
|
-
);
|
|
22808
|
-
syntheticInputFired = true;
|
|
22753
|
+
chainEvent(forwardEvent, e);
|
|
22754
|
+
targetController.setUIState(newUIState, forwardEvent);
|
|
22809
22755
|
}
|
|
22810
22756
|
}
|
|
22811
|
-
//
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
|
|
22817
|
-
|
|
22818
|
-
|
|
22819
|
-
|
|
22820
|
-
|
|
22821
|
-
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
22825
|
-
|
|
22826
|
-
|
|
22827
|
-
|
|
22828
|
-
|
|
22829
|
-
|
|
22830
|
-
|
|
22831
|
-
|
|
22832
|
-
|
|
22833
|
-
|
|
22834
|
-
|
|
22835
|
-
|
|
22836
|
-
|
|
22837
|
-
|
|
22838
|
-
|
|
22839
|
-
|
|
22840
|
-
|
|
22841
|
-
|
|
22842
|
-
|
|
22843
|
-
|
|
22844
|
-
|
|
22845
|
-
|
|
22846
|
-
|
|
22847
|
-
|
|
22848
|
-
|
|
22849
|
-
|
|
22850
|
-
|
|
22851
|
-
|
|
22852
|
-
|
|
22853
|
-
|
|
22854
|
-
|
|
22855
|
-
|
|
22856
|
-
|
|
22857
|
-
|
|
22858
|
-
|
|
22859
|
-
|
|
22860
|
-
|
|
22861
|
-
|
|
22862
|
-
|
|
22863
|
-
|
|
22757
|
+
// Dispatch a synthetic "input" event so external listeners see the new
|
|
22758
|
+
// value. Skip when an input event on this element already exists in the chain.
|
|
22759
|
+
let syntheticInputFired = false;
|
|
22760
|
+
if (el) {
|
|
22761
|
+
const existingInputEvent = findEvent(e, (eInChain) => {
|
|
22762
|
+
return eInChain.type === "input" && eInChain.target === el;
|
|
22763
|
+
});
|
|
22764
|
+
if (!existingInputEvent) {
|
|
22765
|
+
if (el.tagName === "INPUT") {
|
|
22766
|
+
if (el.type === "radio" || el.type === "checkbox") {
|
|
22767
|
+
debugUIState(
|
|
22768
|
+
e,
|
|
22769
|
+
"dispatching synthetic input event without data for checkbox/radio",
|
|
22770
|
+
);
|
|
22771
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
22772
|
+
syntheticInputFired = true;
|
|
22773
|
+
} else {
|
|
22774
|
+
debugUIState(
|
|
22775
|
+
e,
|
|
22776
|
+
`dispatching synthetic input event with data "${newUIState}" for input`,
|
|
22777
|
+
);
|
|
22778
|
+
el.dispatchEvent(
|
|
22779
|
+
new InputEvent("input", {
|
|
22780
|
+
bubbles: true,
|
|
22781
|
+
cancelable: true,
|
|
22782
|
+
inputType: "insertText",
|
|
22783
|
+
data: newUIState,
|
|
22784
|
+
}),
|
|
22785
|
+
);
|
|
22786
|
+
syntheticInputFired = true;
|
|
22787
|
+
}
|
|
22788
|
+
}
|
|
22789
|
+
// TODO: select, textarea
|
|
22790
|
+
}
|
|
22791
|
+
}
|
|
22792
|
+
if (!syntheticInputFired) {
|
|
22793
|
+
// When a synthetic "input" event was dispatched, the stateIsTheSame path
|
|
22794
|
+
// already called onUIAction via the input event handler — skip here to
|
|
22795
|
+
// avoid a duplicate uiAction on the same user gesture.
|
|
22796
|
+
controller.onUIAction(e);
|
|
22797
|
+
}
|
|
22798
|
+
// Sync validity after state change: re-check constraints against the new value.
|
|
22799
|
+
// Internal events (programmatic) → silent check only.
|
|
22800
|
+
// User events → full sync (may open/close callout).
|
|
22801
|
+
if (isInternalEvent(e)) {
|
|
22802
|
+
controller.rules.validation.checkValidity({ event: e });
|
|
22803
|
+
} else {
|
|
22804
|
+
controller.rules.validation.syncValidity(e);
|
|
22805
|
+
}
|
|
22806
|
+
return true;
|
|
22807
|
+
},
|
|
22808
|
+
clearUIState: (e) => {
|
|
22809
|
+
// Radio and checkbox "unchecked" state is `undefined`, not `""`.
|
|
22810
|
+
// Passing `""` would set checked=true because `"" !== undefined`.
|
|
22811
|
+
const isCheckable =
|
|
22812
|
+
controlType === "input" &&
|
|
22813
|
+
(props.type === "radio" || props.type === "checkbox");
|
|
22814
|
+
controller.setUIState(isCheckable ? undefined : "", e);
|
|
22815
|
+
},
|
|
22816
|
+
resetUIState: (e) => {
|
|
22817
|
+
controller.setUIState(controller.state, e);
|
|
22818
|
+
},
|
|
22819
|
+
onActionEnd: async (e) => {
|
|
22820
|
+
debugUIState(`"${controlType}" actionEnd called`);
|
|
22821
|
+
// wait for preact to re-render to update readonly as action end side effects are runned
|
|
22822
|
+
// await new Promise((r) => requestAnimationFrame(r));
|
|
22823
|
+
controller.rules.validation.syncValidity(e);
|
|
22824
|
+
},
|
|
22825
|
+
onActionError: (e) => {
|
|
22826
|
+
debugUIState(`"${controlType}" actionError called`);
|
|
22827
|
+
controller.rules.validation.syncValidity(e, { report: true });
|
|
22828
|
+
},
|
|
22829
|
+
subscribe: subscribeUIState,
|
|
22830
|
+
// Leaf controls act as a transparent pass-through so that controls
|
|
22831
|
+
// nested inside them (e.g. an Input inside a List.Item) can bubble
|
|
22832
|
+
// up registration to the nearest group ancestor.
|
|
22833
|
+
registerChild: (childUIStateController, options) => {
|
|
22834
|
+
s.parentUIStateController?.registerChild(
|
|
22835
|
+
childUIStateController,
|
|
22836
|
+
options,
|
|
22837
|
+
);
|
|
22838
|
+
},
|
|
22839
|
+
unregisterChild: (childUIStateController) => {
|
|
22840
|
+
s.parentUIStateController?.unregisterChild(childUIStateController);
|
|
22841
|
+
},
|
|
22842
|
+
onChildUIAction: (childUIStateController, e, options) => {
|
|
22843
|
+
s.parentUIStateController?.onChildUIAction(
|
|
22844
|
+
childUIStateController,
|
|
22845
|
+
e,
|
|
22846
|
+
options,
|
|
22847
|
+
);
|
|
22848
|
+
},
|
|
22849
|
+
};
|
|
22850
|
+
const rules = createControlRules(controller, {
|
|
22851
|
+
debugPopup,
|
|
22852
|
+
debugInteraction,
|
|
22853
|
+
debugUIState,
|
|
22854
|
+
debugFocus,
|
|
22855
|
+
});
|
|
22856
|
+
controller.rules = rules;
|
|
22857
|
+
|
|
22858
|
+
// Include all values that controller methods read from the scope so they
|
|
22859
|
+
// are available immediately — even if no re-render happens before the
|
|
22860
|
+
// first user interaction (update only runs on re-renders, not on mount).
|
|
22861
|
+
return {
|
|
22862
|
+
controller,
|
|
22863
|
+
ref: props.ref,
|
|
22864
|
+
id: props.id,
|
|
22865
|
+
name: props.name,
|
|
22866
|
+
props,
|
|
22867
|
+
controlInfo,
|
|
22868
|
+
uiAction: props.uiAction,
|
|
22869
|
+
uiActionInternal,
|
|
22870
|
+
parentUIStateController,
|
|
22871
|
+
parentUiStateSignalHolder,
|
|
22872
|
+
};
|
|
22864
22873
|
},
|
|
22865
|
-
|
|
22866
|
-
|
|
22867
|
-
|
|
22868
|
-
|
|
22869
|
-
|
|
22870
|
-
|
|
22871
|
-
|
|
22874
|
+
// ── update: runs every render after the first ─────────────────────────
|
|
22875
|
+
// Syncs public-facing fields and handles controlled state prop changes.
|
|
22876
|
+
(s) => {
|
|
22877
|
+
const { controller } = s;
|
|
22878
|
+
// Raw Preact props from the current render. These are the component's input props,
|
|
22879
|
+
// not the resolved/curated host props. useInteractiveProps overwrites
|
|
22880
|
+
// uiStateController.controlHostProps with the resolved subset on every render.
|
|
22881
|
+
controller.props = props;
|
|
22882
|
+
// Re-sync to this render's ref object. It's normally stable, but it can
|
|
22883
|
+
// legitimately change identity (e.g. switching from an internal fallback
|
|
22884
|
+
// ref to a forwarded one, or across an interrupted/resumed render such as
|
|
22885
|
+
// a Suspense boundary resolving) — if we kept the original ref forever,
|
|
22886
|
+
// `ref.current` would be stuck at whatever it was at creation time, even
|
|
22887
|
+
// after the controller has moved on to a different, live DOM node.
|
|
22888
|
+
controller.ref = props.ref;
|
|
22889
|
+
controller.id = props.id; // never supposed to change, not supported for now
|
|
22890
|
+
controller.name = props.name;
|
|
22891
|
+
controller.parentUIStateController = parentUIStateController;
|
|
22892
|
+
const { value, hasStateProp, state, stateInitial } = controlInfo;
|
|
22893
|
+
controller.value = value;
|
|
22894
|
+
if (hasStateProp) {
|
|
22895
|
+
controller.hasStateProp = true;
|
|
22896
|
+
const currentState = controller.state;
|
|
22897
|
+
if (!compareTwoJsValues(state, currentState)) {
|
|
22898
|
+
controller.state = state;
|
|
22899
|
+
controller.setUIState(state, new CustomEvent("state_prop_change"));
|
|
22900
|
+
}
|
|
22901
|
+
} else if (controller.hasStateProp) {
|
|
22902
|
+
controller.hasStateProp = false;
|
|
22903
|
+
controller.state = stateInitial;
|
|
22872
22904
|
}
|
|
22905
|
+
return {
|
|
22906
|
+
ref: props.ref,
|
|
22907
|
+
id: props.id,
|
|
22908
|
+
name: props.name,
|
|
22909
|
+
props,
|
|
22910
|
+
controlInfo,
|
|
22911
|
+
uiAction: props.uiAction,
|
|
22912
|
+
uiActionInternal,
|
|
22913
|
+
parentUIStateController,
|
|
22914
|
+
};
|
|
22873
22915
|
},
|
|
22874
|
-
|
|
22875
|
-
|
|
22876
|
-
|
|
22877
|
-
debugPopup,
|
|
22878
|
-
debugInteraction,
|
|
22879
|
-
debugUIState,
|
|
22880
|
-
debugFocus,
|
|
22881
|
-
});
|
|
22882
|
-
uiStateController.rules = rules;
|
|
22883
|
-
return uiStateController;
|
|
22884
|
-
};
|
|
22885
|
-
const NO_PARENT = [() => {}, () => {}, () => {}];
|
|
22886
|
-
const useParentControllerNotifiers = (
|
|
22887
|
-
parentUIStateController,
|
|
22888
|
-
uiStateControllerRef,
|
|
22889
|
-
controlType,
|
|
22890
|
-
debugUIState,
|
|
22891
|
-
) => {
|
|
22892
|
-
return useMemo(() => {
|
|
22893
|
-
if (!parentUIStateController) {
|
|
22894
|
-
return NO_PARENT;
|
|
22895
|
-
}
|
|
22916
|
+
);
|
|
22917
|
+
scope.parentUiStateSignalHolder.value =
|
|
22918
|
+
parentUIStateController?.uiStateSignal ?? null;
|
|
22896
22919
|
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
|
|
22920
|
+
const { controller } = scope;
|
|
22921
|
+
const controllerRef = controller.ref;
|
|
22922
|
+
useLayoutEffect(() => {
|
|
22923
|
+
const el = controllerRef.current;
|
|
22924
|
+
if (el) {
|
|
22925
|
+
el.__uiStateController__ = controller;
|
|
22926
|
+
}
|
|
22927
|
+
return () => {
|
|
22928
|
+
if (el && el.__uiStateController__ === controller) {
|
|
22929
|
+
delete el.__uiStateController__;
|
|
22930
|
+
}
|
|
22931
|
+
onUIStateControllerDestroyed(controller);
|
|
22902
22932
|
};
|
|
22933
|
+
}, [controllerRef]);
|
|
22903
22934
|
|
|
22904
|
-
|
|
22905
|
-
|
|
22906
|
-
|
|
22907
|
-
|
|
22908
|
-
|
|
22909
|
-
debugUIState(
|
|
22910
|
-
`"${controlType}" notifying "${parentControlType}" of child ui action (stateChanged: ${stateChanged})`,
|
|
22911
|
-
);
|
|
22912
|
-
parentUIStateController.onChildUIAction(uiStateController, e, {
|
|
22913
|
-
stateChanged,
|
|
22914
|
-
silent,
|
|
22915
|
-
});
|
|
22916
|
-
};
|
|
22935
|
+
const { parentUIStateController: parentController } = scope;
|
|
22936
|
+
useLayoutEffect(() => {
|
|
22937
|
+
if (!parentController) {
|
|
22938
|
+
return undefined;
|
|
22939
|
+
}
|
|
22917
22940
|
|
|
22918
|
-
|
|
22919
|
-
|
|
22941
|
+
debugUIState(`"${controlType}" registering into "${parent.controlType}"`);
|
|
22942
|
+
parentController.registerChild(controller);
|
|
22943
|
+
return () => {
|
|
22920
22944
|
debugUIState(
|
|
22921
|
-
`"${controlType}" unregistering from "${
|
|
22945
|
+
`"${controlType}" unregistering from "${parent.controlType}"`,
|
|
22922
22946
|
);
|
|
22923
|
-
|
|
22947
|
+
parentController.unregisterChild(controller);
|
|
22924
22948
|
};
|
|
22949
|
+
}, [parentController]);
|
|
22925
22950
|
|
|
22926
|
-
|
|
22927
|
-
notifyParentAboutChildMount,
|
|
22928
|
-
notifyParentAboutChildUIAction,
|
|
22929
|
-
notifyParentAboutChildUnmount,
|
|
22930
|
-
];
|
|
22931
|
-
}, []);
|
|
22951
|
+
return controller;
|
|
22932
22952
|
};
|
|
22933
22953
|
|
|
22934
22954
|
/**
|
|
@@ -23083,9 +23103,8 @@ const useUIGroupStateController = (
|
|
|
23083
23103
|
const parentUIStateController = useContext(ParentUIStateControllerContext);
|
|
23084
23104
|
const hasValueProp = Object.hasOwn(props, "value");
|
|
23085
23105
|
const hasDefaultValueProp = Object.hasOwn(props, "defaultValue");
|
|
23086
|
-
const { id, name, value, defaultValue, uiAction
|
|
23106
|
+
const { id, name, value, defaultValue, uiAction } = props;
|
|
23087
23107
|
const ref = props.ref;
|
|
23088
|
-
const uiActionRef = useRef(uiAction);
|
|
23089
23108
|
const fallbackState =
|
|
23090
23109
|
stateType === "array"
|
|
23091
23110
|
? EMPTY_ARRAY
|
|
@@ -23094,9 +23113,7 @@ const useUIGroupStateController = (
|
|
|
23094
23113
|
: undefined;
|
|
23095
23114
|
const childUIStateControllerArrayRef = useRef([]);
|
|
23096
23115
|
const childUIStateControllerArray = childUIStateControllerArrayRef.current;
|
|
23097
|
-
|
|
23098
|
-
// Tracks children this controller rejected and delegated upward (bubble-up
|
|
23099
|
-
// registration). Used to forward onChildUIAction and unregisterChild.
|
|
23116
|
+
// Tracks children rejected by the filter and delegated upward (bubble-up).
|
|
23100
23117
|
const delegatedChildrenRef = useRef(new Map());
|
|
23101
23118
|
|
|
23102
23119
|
const groupIsRenderingRef = useRef(false);
|
|
@@ -23104,103 +23121,8 @@ const useUIGroupStateController = (
|
|
|
23104
23121
|
groupIsRenderingRef.current = true;
|
|
23105
23122
|
pendingChangeRef.current = false;
|
|
23106
23123
|
|
|
23107
|
-
const [
|
|
23108
|
-
notifyParentAboutChildMount,
|
|
23109
|
-
notifyParentAboutChildUIAction,
|
|
23110
|
-
notifyParentAboutChildUnmount,
|
|
23111
|
-
] = useParentControllerNotifiers(
|
|
23112
|
-
parentUIStateController,
|
|
23113
|
-
controllerRef,
|
|
23114
|
-
controlType,
|
|
23115
|
-
debugUIGroup,
|
|
23116
|
-
);
|
|
23117
|
-
useLayoutEffect(() => {
|
|
23118
|
-
const controller = controllerRef.current;
|
|
23119
|
-
const el = ref.current;
|
|
23120
|
-
if (el) {
|
|
23121
|
-
el.__uiStateController__ = controller;
|
|
23122
|
-
}
|
|
23123
|
-
notifyParentAboutChildMount();
|
|
23124
|
-
return () => {
|
|
23125
|
-
notifyParentAboutChildUnmount();
|
|
23126
|
-
onUIStateControllerDestroyed(controller);
|
|
23127
|
-
};
|
|
23128
|
-
}, []);
|
|
23129
|
-
|
|
23130
|
-
const onChange = (e, { notifyExternal }) => {
|
|
23131
|
-
if (groupIsRenderingRef.current) {
|
|
23132
|
-
pendingChangeRef.current = true;
|
|
23133
|
-
return;
|
|
23134
|
-
}
|
|
23135
|
-
const aggChildState = resolvedAggregateChildStates(
|
|
23136
|
-
childUIStateControllerArray,
|
|
23137
|
-
fallbackState,
|
|
23138
|
-
);
|
|
23139
|
-
const groupUIState =
|
|
23140
|
-
aggChildState === undefined ? fallbackState : aggChildState;
|
|
23141
|
-
debugUIGroup(
|
|
23142
|
-
e,
|
|
23143
|
-
`${controlType}.getUIState -> ${JSON.stringify(groupUIState)}`,
|
|
23144
|
-
);
|
|
23145
|
-
const groupUIStateController = controllerRef.current;
|
|
23146
|
-
if (notifyExternal === true) {
|
|
23147
|
-
applyState(groupUIState, e);
|
|
23148
|
-
} else if (notifyExternal === "silent") {
|
|
23149
|
-
// Silent mount/unmount sync: update state without triggering uiAction/command,
|
|
23150
|
-
// but still notify parent (e.g. facade) so it can track the current child state.
|
|
23151
|
-
groupUIStateController.syncInternalState(groupUIState, e);
|
|
23152
|
-
notifyParentAboutChildUIAction(e, {
|
|
23153
|
-
stateChanged: true,
|
|
23154
|
-
silent: true,
|
|
23155
|
-
});
|
|
23156
|
-
} else {
|
|
23157
|
-
groupUIStateController.syncInternalState(groupUIState, e);
|
|
23158
|
-
}
|
|
23159
|
-
};
|
|
23160
|
-
|
|
23161
|
-
// Applies the aggregated state: updates signal, fires uiAction/command/navi_ui_state_change,
|
|
23162
|
-
// and notifies the parent. Called both from onChange (after child UI action) and from
|
|
23163
|
-
// setUIState (after cascading to children).
|
|
23164
|
-
const applyState = (newUIState, e, { internalBehavior = false } = {}) => {
|
|
23165
|
-
const groupUIStateController = controllerRef.current;
|
|
23166
|
-
const currentUIState = groupUIStateController.uiState;
|
|
23167
|
-
groupUIStateController.uiState = newUIState;
|
|
23168
|
-
uiStateSignal.value = newUIState;
|
|
23169
|
-
debugUIGroup(
|
|
23170
|
-
e,
|
|
23171
|
-
`${controlType}.applyState(${JSON.stringify(newUIState)}, "${e.type}") -> updates from ${JSON.stringify(currentUIState)} to ${JSON.stringify(newUIState)}`,
|
|
23172
|
-
);
|
|
23173
|
-
publishUIState(newUIState);
|
|
23174
|
-
// Notify the parent (facade) BEFORE firing the command so that when a
|
|
23175
|
-
// command like --navi-send closes the picker, the picker input already
|
|
23176
|
-
// holds the new value.
|
|
23177
|
-
notifyParentAboutChildUIAction(e, { stateChanged: true });
|
|
23178
|
-
groupUIStateController.onUIAction(e, {
|
|
23179
|
-
skipCommand: internalBehavior,
|
|
23180
|
-
});
|
|
23181
|
-
const el = ref.current;
|
|
23182
|
-
if (el) {
|
|
23183
|
-
dispatchInternalCustomEvent(el, "navi_ui_state_change", {
|
|
23184
|
-
event: e,
|
|
23185
|
-
value: newUIState,
|
|
23186
|
-
});
|
|
23187
|
-
}
|
|
23188
|
-
};
|
|
23189
|
-
|
|
23190
|
-
useLayoutEffect(() => {
|
|
23191
|
-
groupIsRenderingRef.current = false;
|
|
23192
|
-
if (pendingChangeRef.current) {
|
|
23193
|
-
pendingChangeRef.current = false;
|
|
23194
|
-
onChange(new CustomEvent(`${controlType}_batched_ui_state_update`), {
|
|
23195
|
-
notifyExternal: "silent",
|
|
23196
|
-
});
|
|
23197
|
-
}
|
|
23198
|
-
});
|
|
23199
|
-
|
|
23200
23124
|
const isMonitoringChild = (childUIStateController) => {
|
|
23201
|
-
if (childUIStateController.isProxy)
|
|
23202
|
-
return false;
|
|
23203
|
-
}
|
|
23125
|
+
if (childUIStateController.isProxy) return false;
|
|
23204
23126
|
if (
|
|
23205
23127
|
resolvedChildControlFilter &&
|
|
23206
23128
|
!resolvedChildControlFilter(childUIStateController)
|
|
@@ -23210,350 +23132,411 @@ const useUIGroupStateController = (
|
|
|
23210
23132
|
return true;
|
|
23211
23133
|
};
|
|
23212
23134
|
const shouldPropagateStateToChild = (childUIStateController) => {
|
|
23213
|
-
if (!isMonitoringChild(childUIStateController))
|
|
23214
|
-
|
|
23215
|
-
|
|
23216
|
-
if (childUIStateController.controlType === "button") {
|
|
23217
|
-
return false;
|
|
23218
|
-
}
|
|
23219
|
-
if (childUIStateController.controlType === "link") {
|
|
23220
|
-
return false;
|
|
23221
|
-
}
|
|
23135
|
+
if (!isMonitoringChild(childUIStateController)) return false;
|
|
23136
|
+
if (childUIStateController.controlType === "button") return false;
|
|
23137
|
+
if (childUIStateController.controlType === "link") return false;
|
|
23222
23138
|
return true;
|
|
23223
23139
|
};
|
|
23224
23140
|
|
|
23225
|
-
const
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
|
|
23230
|
-
existingController.id = id;
|
|
23231
|
-
existingController.name = name;
|
|
23232
|
-
existingController.value = value;
|
|
23233
|
-
existingController.defaultValue = defaultValue;
|
|
23234
|
-
existingController.hasValueProp = hasValueProp;
|
|
23235
|
-
existingController.hasDefaultValueProp = hasDefaultValueProp;
|
|
23236
|
-
uiActionRef.current = uiAction;
|
|
23237
|
-
// When the controlled value prop changes (or when becoming controlled for the
|
|
23238
|
-
// first time), silently cascade to children that have no individual state prop.
|
|
23239
|
-
if (
|
|
23240
|
-
hasValueProp &&
|
|
23241
|
-
(!prevHasValueProp || !compareTwoJsValues(value, prevValue))
|
|
23242
|
-
) {
|
|
23243
|
-
const propagateDownEvent = new CustomEvent(
|
|
23244
|
-
"propagate_down_set_ui_state",
|
|
23245
|
-
{ detail: {} },
|
|
23141
|
+
const scope = useRenderScope(
|
|
23142
|
+
// ── init: runs once on mount ───────────────────────────────────────────
|
|
23143
|
+
(s) => {
|
|
23144
|
+
debugUIGroup(
|
|
23145
|
+
`Creating "${controlType}" ui state controller (monitoring some descendants ui state(s))"`,
|
|
23246
23146
|
);
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
|
|
23147
|
+
const [publishUIState, subscribeUIState] = createPubSub();
|
|
23148
|
+
const uiStateSignal = signal(fallbackState);
|
|
23149
|
+
|
|
23150
|
+
// onChange and applyState live inside init so they close over the stable
|
|
23151
|
+
// signals/pubsub without needing external refs.
|
|
23152
|
+
const onChange = (e, { notifyExternal }) => {
|
|
23153
|
+
if (groupIsRenderingRef.current) {
|
|
23154
|
+
pendingChangeRef.current = true;
|
|
23155
|
+
return;
|
|
23253
23156
|
}
|
|
23254
|
-
const
|
|
23255
|
-
|
|
23256
|
-
|
|
23157
|
+
const aggChildState = resolvedAggregateChildStates(
|
|
23158
|
+
childUIStateControllerArray,
|
|
23159
|
+
fallbackState,
|
|
23257
23160
|
);
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
existingController.syncInternalState(value);
|
|
23264
|
-
}
|
|
23265
|
-
return existingController;
|
|
23266
|
-
}
|
|
23267
|
-
debugUIGroup(
|
|
23268
|
-
`Creating "${controlType}" ui state controller (monitoring some descendants ui state(s))"`,
|
|
23269
|
-
);
|
|
23270
|
-
|
|
23271
|
-
const [publishUIState, subscribeUIState] = createPubSub();
|
|
23272
|
-
const uiStateSignal = signal(fallbackState);
|
|
23273
|
-
const groupUIStateController = {
|
|
23274
|
-
controlType,
|
|
23275
|
-
id,
|
|
23276
|
-
name,
|
|
23277
|
-
value,
|
|
23278
|
-
defaultValue,
|
|
23279
|
-
hasValueProp,
|
|
23280
|
-
hasDefaultValueProp,
|
|
23281
|
-
props,
|
|
23282
|
-
uiState: fallbackState,
|
|
23283
|
-
uiStateSignal,
|
|
23284
|
-
wantRequesterButtonState,
|
|
23285
|
-
elementRef: ref,
|
|
23286
|
-
getPropFromState: (uiState) => uiState,
|
|
23287
|
-
distributeChildUIState: resolvedDistributeChildUIState,
|
|
23288
|
-
// Cascades newUIState to each monitored child via resolvedDistributeChildUIState,
|
|
23289
|
-
// then re-aggregates and fires this group's own reactions.
|
|
23290
|
-
setUIState: (newUIState, e) => {
|
|
23291
|
-
if (
|
|
23292
|
-
stateType === "object" &&
|
|
23293
|
-
(newUIState === null || typeof newUIState !== "object")
|
|
23294
|
-
) {
|
|
23295
|
-
console.warn(
|
|
23296
|
-
`[${controlType}] setUIState received a non-object value: ${JSON.stringify(newUIState)} (expected an object). Ignoring.`,
|
|
23297
|
-
newUIState,
|
|
23298
|
-
);
|
|
23299
|
-
return;
|
|
23300
|
-
}
|
|
23301
|
-
if (stateType === "array" && !Array.isArray(newUIState)) {
|
|
23302
|
-
console.warn(
|
|
23303
|
-
`[${controlType}] setUIState received a non-array value: ${JSON.stringify(newUIState)} (expected an array). Ignoring.`,
|
|
23304
|
-
newUIState,
|
|
23161
|
+
const groupUIState =
|
|
23162
|
+
aggChildState === undefined ? fallbackState : aggChildState;
|
|
23163
|
+
debugUIGroup(
|
|
23164
|
+
e,
|
|
23165
|
+
`${controlType}.getUIState -> ${JSON.stringify(groupUIState)}`,
|
|
23305
23166
|
);
|
|
23306
|
-
|
|
23307
|
-
|
|
23308
|
-
|
|
23309
|
-
|
|
23310
|
-
|
|
23311
|
-
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
chainEvent(propagateDownEvent, e);
|
|
23318
|
-
for (const childUIStateController of childUIStateControllerArray) {
|
|
23319
|
-
if (!shouldPropagateStateToChild(childUIStateController)) {
|
|
23320
|
-
continue;
|
|
23167
|
+
const { controller } = s;
|
|
23168
|
+
if (notifyExternal === true) {
|
|
23169
|
+
applyState(groupUIState, e);
|
|
23170
|
+
} else if (notifyExternal === "silent") {
|
|
23171
|
+
controller.syncInternalState(groupUIState, e);
|
|
23172
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
23173
|
+
stateChanged: true,
|
|
23174
|
+
silent: true,
|
|
23175
|
+
});
|
|
23176
|
+
} else {
|
|
23177
|
+
controller.syncInternalState(groupUIState, e);
|
|
23321
23178
|
}
|
|
23322
|
-
|
|
23323
|
-
|
|
23324
|
-
|
|
23179
|
+
};
|
|
23180
|
+
|
|
23181
|
+
const applyState = (newUIState, e, { internalBehavior = false } = {}) => {
|
|
23182
|
+
const { controller } = s;
|
|
23183
|
+
const currentUIState = controller.uiState;
|
|
23184
|
+
controller.uiState = newUIState;
|
|
23185
|
+
uiStateSignal.value = newUIState;
|
|
23186
|
+
debugUIGroup(
|
|
23187
|
+
e,
|
|
23188
|
+
`${controlType}.applyState(${JSON.stringify(newUIState)}, "${e.type}") -> updates from ${JSON.stringify(currentUIState)} to ${JSON.stringify(newUIState)}`,
|
|
23325
23189
|
);
|
|
23326
|
-
|
|
23327
|
-
|
|
23328
|
-
|
|
23329
|
-
|
|
23330
|
-
|
|
23331
|
-
|
|
23332
|
-
// (onChange would call setUIState again, which would cascade again).
|
|
23333
|
-
const aggChildState = resolvedAggregateChildStates(
|
|
23334
|
-
childUIStateControllerArray,
|
|
23335
|
-
fallbackState,
|
|
23336
|
-
);
|
|
23337
|
-
const groupUIState =
|
|
23338
|
-
aggChildState === undefined ? fallbackState : aggChildState;
|
|
23339
|
-
if (e.type === "initial_state_push") {
|
|
23340
|
-
// Silent initialization: update state without firing uiAction or notifying parent.
|
|
23341
|
-
groupUIStateController.syncInternalState(groupUIState);
|
|
23342
|
-
return;
|
|
23343
|
-
}
|
|
23344
|
-
applyState(groupUIState, e, { internalBehavior: true });
|
|
23345
|
-
},
|
|
23346
|
-
// Called on mount/unmount/render-batch: updates state silently with no external reactions.
|
|
23347
|
-
syncInternalState: (newUIState) => {
|
|
23348
|
-
const currentUIState = groupUIStateController.uiState;
|
|
23349
|
-
if (newUIState === currentUIState) {
|
|
23350
|
-
return;
|
|
23351
|
-
}
|
|
23352
|
-
groupUIStateController.uiState = newUIState;
|
|
23353
|
-
uiStateSignal.value = newUIState;
|
|
23354
|
-
publishUIState(newUIState);
|
|
23355
|
-
},
|
|
23356
|
-
// Called when a child UI action does NOT change the aggregated value (e.g. radio re-clicked).
|
|
23357
|
-
// Fires uiAction + command without touching state or the action pipeline.
|
|
23358
|
-
onUIAction: (e, { skipCommand } = {}) => {
|
|
23359
|
-
const currentUIState = groupUIStateController.uiState;
|
|
23360
|
-
const uiAction = uiActionRef.current;
|
|
23361
|
-
uiAction?.(currentUIState, e);
|
|
23362
|
-
uiActionInternal?.(currentUIState, e);
|
|
23363
|
-
if (skipCommand) ; else if (command) {
|
|
23364
|
-
const el = ref.current;
|
|
23190
|
+
publishUIState(newUIState);
|
|
23191
|
+
s.parentUIStateController?.onChildUIAction(controller, e, {
|
|
23192
|
+
stateChanged: true,
|
|
23193
|
+
});
|
|
23194
|
+
controller.onUIAction(e, { skipCommand: internalBehavior });
|
|
23195
|
+
const el = controller.ref.current;
|
|
23365
23196
|
if (el) {
|
|
23366
|
-
|
|
23197
|
+
dispatchInternalCustomEvent(el, "navi_ui_state_change", {
|
|
23198
|
+
event: e,
|
|
23199
|
+
value: newUIState,
|
|
23200
|
+
});
|
|
23367
23201
|
}
|
|
23368
|
-
}
|
|
23369
|
-
|
|
23370
|
-
|
|
23371
|
-
|
|
23372
|
-
|
|
23373
|
-
|
|
23374
|
-
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23202
|
+
};
|
|
23203
|
+
|
|
23204
|
+
const controller = {
|
|
23205
|
+
controlType,
|
|
23206
|
+
id,
|
|
23207
|
+
name,
|
|
23208
|
+
value,
|
|
23209
|
+
defaultValue,
|
|
23210
|
+
hasValueProp,
|
|
23211
|
+
hasDefaultValueProp,
|
|
23212
|
+
props,
|
|
23213
|
+
uiState: fallbackState,
|
|
23214
|
+
uiStateSignal,
|
|
23215
|
+
wantRequesterButtonState,
|
|
23216
|
+
ref,
|
|
23217
|
+
getPropFromState: (uiState) => uiState,
|
|
23218
|
+
distributeChildUIState: resolvedDistributeChildUIState,
|
|
23219
|
+
setUIState: (newUIState, e) => {
|
|
23220
|
+
if (
|
|
23221
|
+
stateType === "object" &&
|
|
23222
|
+
(newUIState === null || typeof newUIState !== "object")
|
|
23223
|
+
) {
|
|
23224
|
+
console.warn(
|
|
23225
|
+
`[${controlType}] setUIState received a non-object value: ${JSON.stringify(newUIState)} (expected an object). Ignoring.`,
|
|
23226
|
+
newUIState,
|
|
23227
|
+
);
|
|
23228
|
+
return;
|
|
23229
|
+
}
|
|
23230
|
+
if (stateType === "array" && !Array.isArray(newUIState)) {
|
|
23231
|
+
console.warn(
|
|
23232
|
+
`[${controlType}] setUIState received a non-array value: ${JSON.stringify(newUIState)} (expected an array). Ignoring.`,
|
|
23233
|
+
newUIState,
|
|
23234
|
+
);
|
|
23235
|
+
return;
|
|
23236
|
+
}
|
|
23237
|
+
const propagateEventType =
|
|
23238
|
+
e.type === "initial_state_push"
|
|
23239
|
+
? "initial_state_push"
|
|
23240
|
+
: "propagate_down_set_ui_state";
|
|
23241
|
+
const propagateDownEvent = new CustomEvent(propagateEventType, {
|
|
23242
|
+
detail: {},
|
|
23243
|
+
});
|
|
23244
|
+
chainEvent(propagateDownEvent, e);
|
|
23245
|
+
for (const childUIStateController of childUIStateControllerArray) {
|
|
23246
|
+
if (!shouldPropagateStateToChild(childUIStateController)) continue;
|
|
23247
|
+
const childNewState = resolvedDistributeChildUIState(
|
|
23248
|
+
newUIState,
|
|
23249
|
+
childUIStateController,
|
|
23250
|
+
);
|
|
23251
|
+
if (childNewState === CANNOT_DERIVE) continue;
|
|
23252
|
+
childUIStateController.setUIState(
|
|
23253
|
+
childNewState,
|
|
23254
|
+
propagateDownEvent,
|
|
23255
|
+
);
|
|
23256
|
+
}
|
|
23257
|
+
const aggChildState = resolvedAggregateChildStates(
|
|
23258
|
+
childUIStateControllerArray,
|
|
23259
|
+
fallbackState,
|
|
23260
|
+
);
|
|
23261
|
+
const groupUIState =
|
|
23262
|
+
aggChildState === undefined ? fallbackState : aggChildState;
|
|
23263
|
+
if (e.type === "initial_state_push") {
|
|
23264
|
+
controller.syncInternalState(groupUIState);
|
|
23265
|
+
return;
|
|
23266
|
+
}
|
|
23267
|
+
applyState(groupUIState, e, { internalBehavior: true });
|
|
23268
|
+
},
|
|
23269
|
+
syncInternalState: (newUIState) => {
|
|
23270
|
+
const currentUIState = controller.uiState;
|
|
23271
|
+
if (newUIState === currentUIState) return;
|
|
23272
|
+
controller.uiState = newUIState;
|
|
23273
|
+
uiStateSignal.value = newUIState;
|
|
23274
|
+
publishUIState(newUIState);
|
|
23275
|
+
},
|
|
23276
|
+
onUIAction: (e, { skipCommand } = {}) => {
|
|
23277
|
+
const currentUIState = controller.uiState;
|
|
23278
|
+
s.uiAction?.(currentUIState, e);
|
|
23279
|
+
s.uiActionInternal?.(currentUIState, e);
|
|
23280
|
+
if (!skipCommand && controller.props.command) {
|
|
23281
|
+
const el = controller.ref.current;
|
|
23282
|
+
if (el) triggerNaviCommand(el, controller.props.command, e);
|
|
23283
|
+
}
|
|
23284
|
+
},
|
|
23285
|
+
registerChild: (childUIStateController) => {
|
|
23286
|
+
if (!isMonitoringChild(childUIStateController)) {
|
|
23287
|
+
const currentParent = s.parentUIStateController;
|
|
23288
|
+
if (!allowCapture && currentParent) {
|
|
23289
|
+
delegatedChildrenRef.current.set(
|
|
23290
|
+
childUIStateController,
|
|
23291
|
+
currentParent,
|
|
23292
|
+
);
|
|
23293
|
+
currentParent.registerChild(childUIStateController);
|
|
23294
|
+
}
|
|
23295
|
+
return;
|
|
23296
|
+
}
|
|
23297
|
+
const childControlType = childUIStateController.controlType;
|
|
23298
|
+
childUIStateControllerArray.push(childUIStateController);
|
|
23299
|
+
debugUIGroup(
|
|
23300
|
+
`${controlType}.registerChild("${childControlType}") -> registered (total: ${childUIStateControllerArray.length})`,
|
|
23301
|
+
);
|
|
23302
|
+
if (!childUIStateController.hasStateProp) {
|
|
23303
|
+
const initialEvent = new CustomEvent("initial_state_push", {
|
|
23304
|
+
detail: {},
|
|
23305
|
+
});
|
|
23306
|
+
if (controller.hasValueProp) {
|
|
23307
|
+
const childNewState = resolvedDistributeChildUIState(
|
|
23308
|
+
controller.value,
|
|
23309
|
+
childUIStateController,
|
|
23310
|
+
);
|
|
23311
|
+
if (childNewState !== CANNOT_DERIVE) {
|
|
23312
|
+
childUIStateController.setUIState(childNewState, initialEvent);
|
|
23313
|
+
}
|
|
23314
|
+
} else if (controller.hasDefaultValueProp) {
|
|
23315
|
+
const childNewState = resolvedDistributeChildUIState(
|
|
23316
|
+
controller.defaultValue,
|
|
23317
|
+
childUIStateController,
|
|
23318
|
+
);
|
|
23319
|
+
if (childNewState !== CANNOT_DERIVE) {
|
|
23320
|
+
childUIStateController.setUIState(childNewState, initialEvent);
|
|
23321
|
+
}
|
|
23322
|
+
}
|
|
23323
|
+
}
|
|
23324
|
+
onChange(new CustomEvent(`${childControlType}_mount`), {
|
|
23325
|
+
notifyExternal: "silent",
|
|
23326
|
+
});
|
|
23327
|
+
},
|
|
23328
|
+
onChildUIAction: (
|
|
23329
|
+
childUIStateController,
|
|
23330
|
+
e,
|
|
23331
|
+
{ stateChanged, silent },
|
|
23332
|
+
) => {
|
|
23333
|
+
const delegatedTo = delegatedChildrenRef.current.get(
|
|
23379
23334
|
childUIStateController,
|
|
23380
|
-
parentUIStateController,
|
|
23381
23335
|
);
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23336
|
+
if (delegatedTo) {
|
|
23337
|
+
delegatedTo.onChildUIAction(childUIStateController, e, {
|
|
23338
|
+
stateChanged,
|
|
23339
|
+
silent,
|
|
23340
|
+
});
|
|
23341
|
+
return;
|
|
23342
|
+
}
|
|
23343
|
+
if (!isMonitoringChild(childUIStateController)) return;
|
|
23344
|
+
const childControlType = childUIStateController.controlType;
|
|
23345
|
+
debugUIGroup(
|
|
23346
|
+
`${controlType}.onChildUIAction("${childControlType}") stateChanged=${stateChanged} -> child state: ${JSON.stringify(
|
|
23347
|
+
childUIStateController.uiState,
|
|
23348
|
+
)}`,
|
|
23349
|
+
);
|
|
23350
|
+
if (stateChanged) {
|
|
23351
|
+
onChange(e, { notifyExternal: silent ? "silent" : true });
|
|
23352
|
+
} else {
|
|
23353
|
+
controller.onUIAction(e);
|
|
23354
|
+
}
|
|
23355
|
+
},
|
|
23356
|
+
unregisterChild: (childUIStateController) => {
|
|
23357
|
+
const delegatedTo = delegatedChildrenRef.current.get(
|
|
23404
23358
|
childUIStateController,
|
|
23405
23359
|
);
|
|
23406
|
-
if (
|
|
23407
|
-
|
|
23360
|
+
if (delegatedTo) {
|
|
23361
|
+
delegatedChildrenRef.current.delete(childUIStateController);
|
|
23362
|
+
delegatedTo.unregisterChild(childUIStateController);
|
|
23363
|
+
return;
|
|
23408
23364
|
}
|
|
23409
|
-
|
|
23410
|
-
|
|
23411
|
-
const
|
|
23412
|
-
groupUIStateController.defaultValue,
|
|
23365
|
+
if (!isMonitoringChild(childUIStateController)) return;
|
|
23366
|
+
const childControlType = childUIStateController.controlType;
|
|
23367
|
+
const index = childUIStateControllerArray.indexOf(
|
|
23413
23368
|
childUIStateController,
|
|
23414
23369
|
);
|
|
23415
|
-
if (
|
|
23416
|
-
|
|
23370
|
+
if (index === -1) {
|
|
23371
|
+
debugUIGroup(
|
|
23372
|
+
`${controlType}.unregisterChild("${childControlType}") -> not found`,
|
|
23373
|
+
);
|
|
23374
|
+
return;
|
|
23417
23375
|
}
|
|
23418
|
-
|
|
23419
|
-
|
|
23420
|
-
|
|
23421
|
-
|
|
23422
|
-
|
|
23376
|
+
childUIStateControllerArray.splice(index, 1);
|
|
23377
|
+
debugUIGroup(
|
|
23378
|
+
`${controlType}.unregisterChild("${childControlType}") -> unregisteed (remaining: ${childUIStateControllerArray.length})`,
|
|
23379
|
+
);
|
|
23380
|
+
onChange(new CustomEvent(`${childControlType}_unmount`), {
|
|
23381
|
+
notifyExternal: "silent",
|
|
23382
|
+
});
|
|
23383
|
+
},
|
|
23384
|
+
resetUIState: (e) => {
|
|
23385
|
+
const ev = new CustomEvent("propagate_down_reset_ui_state", {
|
|
23386
|
+
detail: {},
|
|
23387
|
+
});
|
|
23388
|
+
chainEvent(ev, e);
|
|
23389
|
+
for (const c of childUIStateControllerArray) {
|
|
23390
|
+
if (shouldPropagateStateToChild(c)) c.resetUIState(ev);
|
|
23391
|
+
}
|
|
23392
|
+
onChange(e, { notifyExternal: true });
|
|
23393
|
+
},
|
|
23394
|
+
clearUIState: (e) => {
|
|
23395
|
+
const ev = new CustomEvent("propagate_down_clear_ui_state", {
|
|
23396
|
+
detail: {},
|
|
23397
|
+
});
|
|
23398
|
+
chainEvent(ev, e);
|
|
23399
|
+
for (const c of childUIStateControllerArray) {
|
|
23400
|
+
if (
|
|
23401
|
+
!isMonitoringChild(c) ||
|
|
23402
|
+
c.controlType === "button" ||
|
|
23403
|
+
c.controlType === "link"
|
|
23404
|
+
) {
|
|
23405
|
+
continue;
|
|
23406
|
+
}
|
|
23407
|
+
c.clearUIState(ev);
|
|
23408
|
+
}
|
|
23409
|
+
onChange(e, { notifyExternal: true });
|
|
23410
|
+
},
|
|
23411
|
+
onActionEnd: (e) => {
|
|
23412
|
+
controller.rules.validation.syncValidity(e);
|
|
23413
|
+
},
|
|
23414
|
+
onActionError: (e) => {
|
|
23415
|
+
controller.rules.validation.syncValidity(e, { report: true });
|
|
23416
|
+
},
|
|
23417
|
+
findChildById: (searchId) => {
|
|
23418
|
+
for (const c of childUIStateControllerArray) {
|
|
23419
|
+
if (c.id === searchId) return c;
|
|
23420
|
+
}
|
|
23421
|
+
return null;
|
|
23422
|
+
},
|
|
23423
|
+
getChildControllers: () => childUIStateControllerArray,
|
|
23424
|
+
getManagedControls: () => {
|
|
23425
|
+
if (!cascadeValidationToChildren) return [];
|
|
23426
|
+
return childUIStateControllerArray.slice();
|
|
23427
|
+
},
|
|
23428
|
+
subscribe: subscribeUIState,
|
|
23429
|
+
};
|
|
23430
|
+
const rules = createControlRules(controller, {
|
|
23431
|
+
debugPopup,
|
|
23432
|
+
debugInteraction,
|
|
23433
|
+
debugUIState: debugUIGroup,
|
|
23434
|
+
debugFocus,
|
|
23423
23435
|
});
|
|
23436
|
+
controller.rules = rules;
|
|
23437
|
+
|
|
23438
|
+
// Include all values read by controller methods so they are immediately
|
|
23439
|
+
// available, even if the user interacts before the first re-render.
|
|
23440
|
+
return {
|
|
23441
|
+
controller,
|
|
23442
|
+
_onChange: onChange,
|
|
23443
|
+
ref,
|
|
23444
|
+
parentUIStateController,
|
|
23445
|
+
uiAction,
|
|
23446
|
+
uiActionInternal,
|
|
23447
|
+
};
|
|
23424
23448
|
},
|
|
23425
|
-
|
|
23426
|
-
|
|
23427
|
-
|
|
23428
|
-
|
|
23429
|
-
|
|
23430
|
-
|
|
23431
|
-
|
|
23432
|
-
|
|
23433
|
-
|
|
23434
|
-
|
|
23435
|
-
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
if (
|
|
23439
|
-
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
)
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
onChange(e, { notifyExternal: true });
|
|
23449
|
+
// ── update: runs every render after the first ─────────────────────────
|
|
23450
|
+
(s) => {
|
|
23451
|
+
const { controller } = s;
|
|
23452
|
+
const prevValue = controller.value;
|
|
23453
|
+
const prevHasValueProp = controller.hasValueProp;
|
|
23454
|
+
controller.props = props;
|
|
23455
|
+
controller.ref = ref;
|
|
23456
|
+
controller.id = id;
|
|
23457
|
+
controller.name = name;
|
|
23458
|
+
controller.value = value;
|
|
23459
|
+
controller.defaultValue = defaultValue;
|
|
23460
|
+
controller.hasValueProp = hasValueProp;
|
|
23461
|
+
controller.hasDefaultValueProp = hasDefaultValueProp;
|
|
23462
|
+
if (
|
|
23463
|
+
hasValueProp &&
|
|
23464
|
+
(!prevHasValueProp || !compareTwoJsValues(value, prevValue))
|
|
23465
|
+
) {
|
|
23466
|
+
const propagateDownEvent = new CustomEvent(
|
|
23467
|
+
"propagate_down_set_ui_state",
|
|
23468
|
+
{ detail: {} },
|
|
23469
|
+
);
|
|
23470
|
+
for (const childUIStateController of childUIStateControllerArray) {
|
|
23471
|
+
if (!shouldPropagateStateToChild(childUIStateController)) continue;
|
|
23472
|
+
if (childUIStateController.hasStateProp) continue;
|
|
23473
|
+
const childNewState = controller.distributeChildUIState(
|
|
23474
|
+
value,
|
|
23475
|
+
childUIStateController,
|
|
23476
|
+
);
|
|
23477
|
+
if (childNewState === CANNOT_DERIVE) continue;
|
|
23478
|
+
childUIStateController.setUIState(childNewState, propagateDownEvent);
|
|
23456
23479
|
}
|
|
23457
|
-
|
|
23458
|
-
// Value unchanged (e.g. radio re-clicked): fire uiAction + command only.
|
|
23459
|
-
groupUIStateController.onUIAction(e);
|
|
23480
|
+
controller.syncInternalState(value);
|
|
23460
23481
|
}
|
|
23482
|
+
|
|
23483
|
+
return {
|
|
23484
|
+
ref,
|
|
23485
|
+
parentUIStateController,
|
|
23486
|
+
uiAction,
|
|
23487
|
+
uiActionInternal,
|
|
23488
|
+
id,
|
|
23489
|
+
name,
|
|
23490
|
+
value,
|
|
23491
|
+
defaultValue,
|
|
23492
|
+
hasValueProp,
|
|
23493
|
+
hasDefaultValueProp,
|
|
23494
|
+
props,
|
|
23495
|
+
};
|
|
23461
23496
|
},
|
|
23462
|
-
|
|
23463
|
-
|
|
23464
|
-
|
|
23465
|
-
|
|
23466
|
-
|
|
23467
|
-
|
|
23468
|
-
|
|
23469
|
-
|
|
23470
|
-
|
|
23471
|
-
|
|
23472
|
-
|
|
23473
|
-
|
|
23474
|
-
|
|
23475
|
-
|
|
23476
|
-
|
|
23477
|
-
|
|
23478
|
-
|
|
23479
|
-
|
|
23480
|
-
|
|
23481
|
-
|
|
23482
|
-
|
|
23497
|
+
);
|
|
23498
|
+
|
|
23499
|
+
const { controller } = scope;
|
|
23500
|
+
useLayoutEffect(() => {
|
|
23501
|
+
const el = ref.current;
|
|
23502
|
+
if (el) {
|
|
23503
|
+
el.__uiStateController__ = controller;
|
|
23504
|
+
}
|
|
23505
|
+
return () => {
|
|
23506
|
+
onUIStateControllerDestroyed(controller);
|
|
23507
|
+
};
|
|
23508
|
+
}, []);
|
|
23509
|
+
|
|
23510
|
+
const { parentUIStateController: parentController } = scope;
|
|
23511
|
+
useLayoutEffect(() => {
|
|
23512
|
+
if (!parentController) {
|
|
23513
|
+
return undefined;
|
|
23514
|
+
}
|
|
23515
|
+
|
|
23516
|
+
debugUIGroup(
|
|
23517
|
+
`"${controlType}" registering into "${parentController.controlType}"`,
|
|
23518
|
+
);
|
|
23519
|
+
parentController.registerChild(controller);
|
|
23520
|
+
return () => {
|
|
23483
23521
|
debugUIGroup(
|
|
23484
|
-
|
|
23485
|
-
);
|
|
23486
|
-
onChange(new CustomEvent(`${childControlType}_unmount`), {
|
|
23487
|
-
notifyExternal: "silent",
|
|
23488
|
-
// childUIStateController,
|
|
23489
|
-
});
|
|
23490
|
-
},
|
|
23491
|
-
resetUIState: (e) => {
|
|
23492
|
-
const propagateDownResetEvent = new CustomEvent(
|
|
23493
|
-
"propagate_down_reset_ui_state",
|
|
23494
|
-
{ detail: {} },
|
|
23522
|
+
`"${controlType}" unregistering from "${parentController.controlType}"`,
|
|
23495
23523
|
);
|
|
23496
|
-
|
|
23497
|
-
|
|
23498
|
-
|
|
23499
|
-
|
|
23500
|
-
|
|
23501
|
-
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
|
|
23505
|
-
|
|
23506
|
-
|
|
23507
|
-
"propagate_down_clear_ui_state",
|
|
23508
|
-
{ detail: {} },
|
|
23524
|
+
parentController.unregisterChild(controller);
|
|
23525
|
+
};
|
|
23526
|
+
}, [parentController]);
|
|
23527
|
+
|
|
23528
|
+
useLayoutEffect(() => {
|
|
23529
|
+
groupIsRenderingRef.current = false;
|
|
23530
|
+
if (pendingChangeRef.current) {
|
|
23531
|
+
pendingChangeRef.current = false;
|
|
23532
|
+
scope._onChange(
|
|
23533
|
+
new CustomEvent(`${controlType}_batched_ui_state_update`),
|
|
23534
|
+
{ notifyExternal: "silent" },
|
|
23509
23535
|
);
|
|
23510
|
-
|
|
23511
|
-
for (const childUIStateController of childUIStateControllerArray) {
|
|
23512
|
-
if (!isMonitoringChild(childUIStateController)) {
|
|
23513
|
-
continue;
|
|
23514
|
-
}
|
|
23515
|
-
if (childUIStateController.controlType === "button") {
|
|
23516
|
-
continue;
|
|
23517
|
-
}
|
|
23518
|
-
if (childUIStateController.controlType === "link") {
|
|
23519
|
-
continue;
|
|
23520
|
-
}
|
|
23521
|
-
childUIStateController.clearUIState(propagateDownClearEvent);
|
|
23522
|
-
}
|
|
23523
|
-
onChange(e, { notifyExternal: true });
|
|
23524
|
-
},
|
|
23525
|
-
onActionEnd: (e) => {
|
|
23526
|
-
groupUIStateController.rules.validation.syncValidity(e);
|
|
23527
|
-
},
|
|
23528
|
-
onActionError: (e) => {
|
|
23529
|
-
groupUIStateController.rules.validation.syncValidity(e, { report: true });
|
|
23530
|
-
},
|
|
23531
|
-
findChildById: (id) => {
|
|
23532
|
-
for (const childUIStateController of childUIStateControllerArray) {
|
|
23533
|
-
if (childUIStateController.id === id) {
|
|
23534
|
-
return childUIStateController;
|
|
23535
|
-
}
|
|
23536
|
-
}
|
|
23537
|
-
return null;
|
|
23538
|
-
},
|
|
23539
|
-
getChildControllers: () => childUIStateControllerArray,
|
|
23540
|
-
getManagedControls: () => {
|
|
23541
|
-
if (!cascadeValidationToChildren) {
|
|
23542
|
-
return [];
|
|
23543
|
-
}
|
|
23544
|
-
return childUIStateControllerArray.slice();
|
|
23545
|
-
},
|
|
23546
|
-
subscribe: subscribeUIState,
|
|
23547
|
-
};
|
|
23548
|
-
controllerRef.current = groupUIStateController;
|
|
23549
|
-
const rules = createControlRules(groupUIStateController, {
|
|
23550
|
-
debugPopup,
|
|
23551
|
-
debugInteraction,
|
|
23552
|
-
debugUIState: debugUIGroup,
|
|
23553
|
-
debugFocus,
|
|
23536
|
+
}
|
|
23554
23537
|
});
|
|
23555
|
-
|
|
23556
|
-
return
|
|
23538
|
+
|
|
23539
|
+
return controller;
|
|
23557
23540
|
};
|
|
23558
23541
|
// Stable reference for an empty selection so the action always receives an
|
|
23559
23542
|
// array (never undefined) and callers don't get a new reference each render.
|
|
@@ -23592,6 +23575,141 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
23592
23575
|
const debugUIState = useDebugUIState();
|
|
23593
23576
|
const debugFocus = useDebugFocus();
|
|
23594
23577
|
|
|
23578
|
+
// The facade controller's closures (registerChild/unregisterChild/onChildUIAction)
|
|
23579
|
+
// must not capture `realUIStateController` directly: that parameter can legitimately
|
|
23580
|
+
// point to a different controller instance on a later render (e.g. the picker's own
|
|
23581
|
+
// controller getting recreated). Instead, closures read `s.realUIStateController`
|
|
23582
|
+
// from the stable scope object, which is kept current by `update` on every render.
|
|
23583
|
+
const scope = useRenderScope(
|
|
23584
|
+
// ── init: runs once on mount ───────────────────────────────────────────
|
|
23585
|
+
(s) => {
|
|
23586
|
+
const canRegisterAsFacadeChild = (childController) => {
|
|
23587
|
+
if (childController.controlType === "button") return false;
|
|
23588
|
+
if (childController.controlType === "link") return false;
|
|
23589
|
+
if (childController.controlType === "facade") return false;
|
|
23590
|
+
if (childController.isProxy) return false;
|
|
23591
|
+
if (childController.props["navi-list"]) {
|
|
23592
|
+
// Controls with navi-list act as standalone list navigators and should
|
|
23593
|
+
// not be treated as the picker's synced child.
|
|
23594
|
+
return false;
|
|
23595
|
+
}
|
|
23596
|
+
if (
|
|
23597
|
+
props.type === "controlgroup" &&
|
|
23598
|
+
childController.controlType !== "control_group"
|
|
23599
|
+
) {
|
|
23600
|
+
// ignore non control group registration (input outside the control group for instance)
|
|
23601
|
+
return false;
|
|
23602
|
+
}
|
|
23603
|
+
if (
|
|
23604
|
+
props.type === "array" &&
|
|
23605
|
+
childController.controlType !== "checkbox_group"
|
|
23606
|
+
) {
|
|
23607
|
+
// only selectable list expose array, ignore others
|
|
23608
|
+
return false;
|
|
23609
|
+
}
|
|
23610
|
+
return true;
|
|
23611
|
+
};
|
|
23612
|
+
|
|
23613
|
+
const facadeUIStateController = {
|
|
23614
|
+
controlType: "facade",
|
|
23615
|
+
props,
|
|
23616
|
+
ref: realUIStateController.ref,
|
|
23617
|
+
uiStateSignal: realUIStateController.uiStateSignal,
|
|
23618
|
+
controlHostProps: realUIStateController.controlHostProps,
|
|
23619
|
+
registerChild: (child) => {
|
|
23620
|
+
if (!canRegisterAsFacadeChild(child)) {
|
|
23621
|
+
return;
|
|
23622
|
+
}
|
|
23623
|
+
const childType = child.controlType;
|
|
23624
|
+
if (firstChildControllerRef.current) {
|
|
23625
|
+
console.warn(
|
|
23626
|
+
`[useUIFacadeStateController] A second child ("${childType}"${child.name ? ` name="${child.name}"` : ""}) tried to register in the picker facade. ` +
|
|
23627
|
+
`The facade only syncs with the first child — wrap multiple controls in a single ControlGroup.`,
|
|
23628
|
+
child,
|
|
23629
|
+
);
|
|
23630
|
+
} else {
|
|
23631
|
+
debugUIState(
|
|
23632
|
+
`[useUIFacadeStateController] "${childType}"${child.name ? ` name="${child.name}"` : ""} registered as the first child in the picker facade.`,
|
|
23633
|
+
);
|
|
23634
|
+
firstChildControllerRef.current = child;
|
|
23635
|
+
s.realUIStateController.facadeChild = child;
|
|
23636
|
+
// If the picker already has a meaningful state (from value or defaultValue),
|
|
23637
|
+
// push it to the child on registration so it reflects the pre-set value
|
|
23638
|
+
// without firing uiAction (equivalent to defaultValue on the child itself).
|
|
23639
|
+
const initialState = s.realUIStateController.uiState;
|
|
23640
|
+
if (initialState !== undefined) {
|
|
23641
|
+
updatingRef.current = true;
|
|
23642
|
+
const initialEvent = new CustomEvent("initial_state_push", {
|
|
23643
|
+
detail: {},
|
|
23644
|
+
});
|
|
23645
|
+
child.setUIState(initialState, initialEvent);
|
|
23646
|
+
updatingRef.current = false;
|
|
23647
|
+
}
|
|
23648
|
+
}
|
|
23649
|
+
},
|
|
23650
|
+
unregisterChild: (child) => {
|
|
23651
|
+
if (firstChildControllerRef.current === child) {
|
|
23652
|
+
firstChildControllerRef.current = null;
|
|
23653
|
+
s.realUIStateController.facadeChild = null;
|
|
23654
|
+
}
|
|
23655
|
+
},
|
|
23656
|
+
getManagedControls: () => {
|
|
23657
|
+
const child = firstChildControllerRef.current;
|
|
23658
|
+
if (!child) {
|
|
23659
|
+
return [];
|
|
23660
|
+
}
|
|
23661
|
+
return child.getManagedControls();
|
|
23662
|
+
},
|
|
23663
|
+
onChildUIAction: (child, e, { stateChanged, silent = false }) => {
|
|
23664
|
+
if (!stateChanged) {
|
|
23665
|
+
return;
|
|
23666
|
+
}
|
|
23667
|
+
if (child !== firstChildControllerRef.current) {
|
|
23668
|
+
return;
|
|
23669
|
+
}
|
|
23670
|
+
updatingRef.current = true;
|
|
23671
|
+
// Use a different event type for silent (mount/unmount) syncs so that
|
|
23672
|
+
// the picker's setUIState does not fire navi_change or action pipelines.
|
|
23673
|
+
const eventType = silent
|
|
23674
|
+
? "facade_child_mount_sync"
|
|
23675
|
+
: "facade_propagate_up";
|
|
23676
|
+
const propagateUpEvent = new CustomEvent(eventType, {
|
|
23677
|
+
detail: {},
|
|
23678
|
+
});
|
|
23679
|
+
chainEvent(propagateUpEvent, e);
|
|
23680
|
+
s.realUIStateController.setUIState(child.uiState, propagateUpEvent);
|
|
23681
|
+
updatingRef.current = false;
|
|
23682
|
+
},
|
|
23683
|
+
};
|
|
23684
|
+
const rules = createControlRules(facadeUIStateController, {
|
|
23685
|
+
debugPopup,
|
|
23686
|
+
debugInteraction,
|
|
23687
|
+
debugUIState,
|
|
23688
|
+
debugFocus,
|
|
23689
|
+
});
|
|
23690
|
+
facadeUIStateController.rules = rules;
|
|
23691
|
+
|
|
23692
|
+
// No initial checkValidity() here — the facade has no controlHostProps and no children
|
|
23693
|
+
// have registered yet, so any check would be a no-op. The real validity check happens
|
|
23694
|
+
// when child controllers trigger UI actions through the facade.
|
|
23695
|
+
return {
|
|
23696
|
+
controller: facadeUIStateController,
|
|
23697
|
+
realUIStateController,
|
|
23698
|
+
};
|
|
23699
|
+
},
|
|
23700
|
+
// ── update: runs every render after the first ─────────────────────────
|
|
23701
|
+
(s) => {
|
|
23702
|
+
s.controller.props = props;
|
|
23703
|
+
s.controller.ref = realUIStateController.ref;
|
|
23704
|
+
s.controller.uiStateSignal = realUIStateController.uiStateSignal;
|
|
23705
|
+
s.controller.controlHostProps = realUIStateController.controlHostProps;
|
|
23706
|
+
|
|
23707
|
+
return {
|
|
23708
|
+
realUIStateController,
|
|
23709
|
+
};
|
|
23710
|
+
},
|
|
23711
|
+
);
|
|
23712
|
+
|
|
23595
23713
|
useLayoutEffect(() => {
|
|
23596
23714
|
return realUIStateController.subscribe((newUIState, e) => {
|
|
23597
23715
|
if (updatingRef.current) {
|
|
@@ -23610,132 +23728,9 @@ const useUIFacadeStateController = (props, realUIStateController) => {
|
|
|
23610
23728
|
child.setUIState(newUIState, propagateDownEvent);
|
|
23611
23729
|
updatingRef.current = false;
|
|
23612
23730
|
});
|
|
23613
|
-
}, []);
|
|
23614
|
-
|
|
23615
|
-
const canRegisterAsFacadeChild = (childController) => {
|
|
23616
|
-
if (childController.controlType === "button") {
|
|
23617
|
-
return false;
|
|
23618
|
-
}
|
|
23619
|
-
if (childController.controlType === "link") {
|
|
23620
|
-
return false;
|
|
23621
|
-
}
|
|
23622
|
-
if (childController.controlType === "facade") {
|
|
23623
|
-
return false;
|
|
23624
|
-
}
|
|
23625
|
-
if (childController.isProxy) {
|
|
23626
|
-
return false;
|
|
23627
|
-
}
|
|
23628
|
-
if (childController.props["navi-list"]) {
|
|
23629
|
-
// Controls with navi-list act as standalone list navigators and should
|
|
23630
|
-
// not be treated as the picker's synced child.
|
|
23631
|
-
return false;
|
|
23632
|
-
}
|
|
23633
|
-
if (
|
|
23634
|
-
props.type === "controlgroup" &&
|
|
23635
|
-
childController.controlType !== "control_group"
|
|
23636
|
-
) {
|
|
23637
|
-
// ignore non control group registration (input outside the control group for instance)
|
|
23638
|
-
return false;
|
|
23639
|
-
}
|
|
23640
|
-
if (
|
|
23641
|
-
props.type === "array" &&
|
|
23642
|
-
childController.controlType !== "checkbox_group"
|
|
23643
|
-
) {
|
|
23644
|
-
// only selectable list expose array, ignore others
|
|
23645
|
-
return false;
|
|
23646
|
-
}
|
|
23647
|
-
return true;
|
|
23648
|
-
};
|
|
23649
|
-
|
|
23650
|
-
const controllerRef = useRef();
|
|
23651
|
-
if (controllerRef.current) {
|
|
23652
|
-
return controllerRef.current;
|
|
23653
|
-
}
|
|
23731
|
+
}, [realUIStateController]);
|
|
23654
23732
|
|
|
23655
|
-
|
|
23656
|
-
controlType: "facade",
|
|
23657
|
-
props,
|
|
23658
|
-
elementRef: realUIStateController.elementRef,
|
|
23659
|
-
uiStateSignal: realUIStateController.uiStateSignal,
|
|
23660
|
-
registerChild: (child) => {
|
|
23661
|
-
if (!canRegisterAsFacadeChild(child)) {
|
|
23662
|
-
return;
|
|
23663
|
-
}
|
|
23664
|
-
const childType = child.controlType;
|
|
23665
|
-
if (firstChildControllerRef.current) {
|
|
23666
|
-
console.warn(
|
|
23667
|
-
`[useUIFacadeStateController] A second child ("${childType}"${child.name ? ` name="${child.name}"` : ""}) tried to register in the picker facade. ` +
|
|
23668
|
-
`The facade only syncs with the first child — wrap multiple controls in a single ControlGroup.`,
|
|
23669
|
-
child,
|
|
23670
|
-
);
|
|
23671
|
-
} else {
|
|
23672
|
-
debugUIState(
|
|
23673
|
-
`[useUIFacadeStateController] "${childType}"${child.name ? ` name="${child.name}"` : ""} registered as the first child in the picker facade.`,
|
|
23674
|
-
);
|
|
23675
|
-
firstChildControllerRef.current = child;
|
|
23676
|
-
realUIStateController.facadeChild = child;
|
|
23677
|
-
// If the picker already has a meaningful state (from value or defaultValue),
|
|
23678
|
-
// push it to the child on registration so it reflects the pre-set value
|
|
23679
|
-
// without firing uiAction (equivalent to defaultValue on the child itself).
|
|
23680
|
-
const initialState = realUIStateController.uiState;
|
|
23681
|
-
if (initialState !== undefined) {
|
|
23682
|
-
updatingRef.current = true;
|
|
23683
|
-
const initialEvent = new CustomEvent("initial_state_push", {
|
|
23684
|
-
detail: {},
|
|
23685
|
-
});
|
|
23686
|
-
child.setUIState(initialState, initialEvent);
|
|
23687
|
-
updatingRef.current = false;
|
|
23688
|
-
}
|
|
23689
|
-
}
|
|
23690
|
-
},
|
|
23691
|
-
unregisterChild: (child) => {
|
|
23692
|
-
if (firstChildControllerRef.current === child) {
|
|
23693
|
-
firstChildControllerRef.current = null;
|
|
23694
|
-
realUIStateController.facadeChild = null;
|
|
23695
|
-
}
|
|
23696
|
-
},
|
|
23697
|
-
getManagedControls: () => {
|
|
23698
|
-
const child = firstChildControllerRef.current;
|
|
23699
|
-
if (!child) {
|
|
23700
|
-
return [];
|
|
23701
|
-
}
|
|
23702
|
-
return child.getManagedControls();
|
|
23703
|
-
},
|
|
23704
|
-
onChildUIAction: (child, e, { stateChanged, silent = false }) => {
|
|
23705
|
-
if (!stateChanged) {
|
|
23706
|
-
return;
|
|
23707
|
-
}
|
|
23708
|
-
if (child !== firstChildControllerRef.current) {
|
|
23709
|
-
return;
|
|
23710
|
-
}
|
|
23711
|
-
updatingRef.current = true;
|
|
23712
|
-
// Use a different event type for silent (mount/unmount) syncs so that
|
|
23713
|
-
// the picker's setUIState does not fire navi_change or action pipelines.
|
|
23714
|
-
const eventType = silent
|
|
23715
|
-
? "facade_child_mount_sync"
|
|
23716
|
-
: "facade_propagate_up";
|
|
23717
|
-
const propagateUpEvent = new CustomEvent(eventType, {
|
|
23718
|
-
detail: {},
|
|
23719
|
-
});
|
|
23720
|
-
chainEvent(propagateUpEvent, e);
|
|
23721
|
-
realUIStateController.setUIState(child.uiState, propagateUpEvent);
|
|
23722
|
-
updatingRef.current = false;
|
|
23723
|
-
},
|
|
23724
|
-
};
|
|
23725
|
-
controllerRef.current = facadeUIStateController;
|
|
23726
|
-
const rules = createControlRules(facadeUIStateController, {
|
|
23727
|
-
debugPopup,
|
|
23728
|
-
debugInteraction,
|
|
23729
|
-
debugUIState,
|
|
23730
|
-
debugFocus,
|
|
23731
|
-
});
|
|
23732
|
-
facadeUIStateController.rules = rules;
|
|
23733
|
-
facadeUIStateController.controlHostProps =
|
|
23734
|
-
realUIStateController.controlHostProps;
|
|
23735
|
-
// No initial checkValidity() here — the facade has no controlHostProps and no children
|
|
23736
|
-
// have registered yet, so any check would be a no-op. The real validity check happens
|
|
23737
|
-
// when child controllers trigger UI actions through the facade.
|
|
23738
|
-
return facadeUIStateController;
|
|
23733
|
+
return scope.controller;
|
|
23739
23734
|
};
|
|
23740
23735
|
|
|
23741
23736
|
/**
|
|
@@ -23977,7 +23972,7 @@ const useControlProps = (props, {
|
|
|
23977
23972
|
return true;
|
|
23978
23973
|
};
|
|
23979
23974
|
const syncUIStateWithDOM = e => {
|
|
23980
|
-
const controlEl = e.currentTarget || uiStateController.
|
|
23975
|
+
const controlEl = e.currentTarget || uiStateController.ref.current;
|
|
23981
23976
|
const value = readControlValue(controlEl);
|
|
23982
23977
|
uiStateController.setUIState(value, e);
|
|
23983
23978
|
};
|
|
@@ -23987,7 +23982,6 @@ const useControlProps = (props, {
|
|
|
23987
23982
|
const triggerUIAction = e => {
|
|
23988
23983
|
syncUIStateWithDOM(e);
|
|
23989
23984
|
};
|
|
23990
|
-
const wasCheckedAtMousedownRef = useRef(false);
|
|
23991
23985
|
const getDefaultEventReactionDefinitions = () => {
|
|
23992
23986
|
const keyDownDefault = e => {
|
|
23993
23987
|
const defaultAction = getKeyboardEventDefaultAction(e);
|
|
@@ -24202,16 +24196,20 @@ const useControlProps = (props, {
|
|
|
24202
24196
|
}
|
|
24203
24197
|
return keyDownDefault(e);
|
|
24204
24198
|
},
|
|
24205
|
-
mouseDown: e => {
|
|
24206
|
-
wasCheckedAtMousedownRef.current = e.currentTarget.checked;
|
|
24207
|
-
},
|
|
24208
24199
|
click: e => {
|
|
24209
|
-
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24213
|
-
|
|
24214
|
-
|
|
24200
|
+
// When a radio is already checked and gets clicked, the browser does NOT
|
|
24201
|
+
// fire an input event (state doesn't change), so syncUIStateWithDOM never
|
|
24202
|
+
// runs. We still want uiAction + command to fire. We can tell whether the
|
|
24203
|
+
// click is on an already-checked radio by comparing our own tracked
|
|
24204
|
+
// uiState (value when checked, undefined when not — see toDomProps) to
|
|
24205
|
+
// this radio's value: it's still the PRE-click value here, since it's
|
|
24206
|
+
// only updated later by the "input" handler. Reading the DOM `.checked`
|
|
24207
|
+
// instead would not work: the browser applies the checked toggle before
|
|
24208
|
+
// dispatching "click", so `.checked` is already the POST-click value
|
|
24209
|
+
// whether or not this was already the checked radio — and worse, a click
|
|
24210
|
+
// that lands on the <label> (not the <input>) never fires "mousedown" on
|
|
24211
|
+
// the input at all, so a DOM-snapshot-at-mousedown approach misses it.
|
|
24212
|
+
if (isRadio && uiStateController.uiState !== undefined) {
|
|
24215
24213
|
return {
|
|
24216
24214
|
name: `click on checked radio`,
|
|
24217
24215
|
allowed: () => triggerUIAction(e),
|
|
@@ -24394,7 +24392,7 @@ const useControlProps = (props, {
|
|
|
24394
24392
|
debounce: actionDebounce,
|
|
24395
24393
|
debugInteraction
|
|
24396
24394
|
});
|
|
24397
|
-
}, [actionEvent, actionAfterChange, actionDebounce, hasNaviChangeEventReaction]);
|
|
24395
|
+
}, [actionEvent, actionAfterChange, actionDebounce, hasNaviChangeEventReaction, ref]);
|
|
24398
24396
|
const refComposed = useComposeElementRef(refCallback, ref);
|
|
24399
24397
|
const onPaste = e => {
|
|
24400
24398
|
props.onPaste?.(e);
|
|
@@ -24800,7 +24798,7 @@ const useInteractiveProps = (props, {
|
|
|
24800
24798
|
}
|
|
24801
24799
|
}));
|
|
24802
24800
|
}
|
|
24803
|
-
}, [disabledResolved, readOnlyResolved]);
|
|
24801
|
+
}, [disabledResolved, readOnlyResolved, ref]);
|
|
24804
24802
|
useLayoutEffect(() => {
|
|
24805
24803
|
return () => {
|
|
24806
24804
|
const element = ref.current;
|
|
@@ -25002,7 +25000,7 @@ const useInteractiveProps = (props, {
|
|
|
25002
25000
|
// already aggregated the new state by now, so uiStateSignal is correct.
|
|
25003
25001
|
const parentController = uiStateController.parentUIStateController;
|
|
25004
25002
|
if (parentController && (parentController.controlType === "radio_group" || parentController.controlType === "checkbox_group")) {
|
|
25005
|
-
const parentEl = parentController.
|
|
25003
|
+
const parentEl = parentController.ref.current;
|
|
25006
25004
|
if (parentEl) {
|
|
25007
25005
|
const originalEvent = e.detail.eventChain[0];
|
|
25008
25006
|
dispatchRequestAction(parentEl, {
|
|
@@ -33944,20 +33942,24 @@ const InputTextualFirstResolver = props => {
|
|
|
33944
33942
|
});
|
|
33945
33943
|
};
|
|
33946
33944
|
const InputTextual = createComponentResolver([InputTextualFirstResolver, InputWithListResolver, InputWithSuggestionsResolver, InputTypeResolver, InputModeResolver, InputHeadlessResolver, InputTextualUI]);
|
|
33947
|
-
const RealInput =
|
|
33948
|
-
|
|
33945
|
+
const RealInput = ({
|
|
33946
|
+
maxLength,
|
|
33947
|
+
...domProps
|
|
33948
|
+
}) => {
|
|
33949
|
+
const autoSelectReadOnlyProps = useAutoSelectReadOnly(domProps);
|
|
33949
33950
|
return jsx(Box, {
|
|
33950
|
-
...
|
|
33951
|
+
...domProps,
|
|
33951
33952
|
as: "input",
|
|
33952
33953
|
baseClassName: "navi_control_input",
|
|
33953
33954
|
...autoSelectReadOnlyProps,
|
|
33954
|
-
// Never set native maxLength — our guard handles it.
|
|
33955
|
-
//
|
|
33956
|
-
maxLength
|
|
33957
|
-
//
|
|
33958
|
-
|
|
33959
|
-
|
|
33960
|
-
|
|
33955
|
+
// Never set native maxLength — our guard handles it. Omitting it entirely
|
|
33956
|
+
// avoids a Preact quirk: setting maxLength={undefined} on a fresh DOM element
|
|
33957
|
+
// (e.g. after a Suspense remount) causes Preact to run `el.maxLength = ""`
|
|
33958
|
+
// which coerces to 0 (Number("") = 0), capping the input at 0 characters.
|
|
33959
|
+
// see https://github.com/preactjs/preact/issues/2677
|
|
33960
|
+
// The JS value stays accessible via the navi-max-length attribute and via
|
|
33961
|
+
// inputControlHostProps (which the validation system reads directly).
|
|
33962
|
+
"navi-max-length": maxLength
|
|
33961
33963
|
});
|
|
33962
33964
|
};
|
|
33963
33965
|
const InputStyleCSSVars = {
|
|
@@ -36209,6 +36211,11 @@ const PickerCustom = props => {
|
|
|
36209
36211
|
}
|
|
36210
36212
|
const pickerEl = ref.current;
|
|
36211
36213
|
const inputEl = getPickerInput(pickerEl);
|
|
36214
|
+
const valueAtClose = getUIStateFromElement(inputEl);
|
|
36215
|
+
if (compareTwoJsValues(valueAtClose, valueAtOpen)) {
|
|
36216
|
+
// Value unchanged — no action to run, but still allow the close.
|
|
36217
|
+
return;
|
|
36218
|
+
}
|
|
36212
36219
|
dispatchRequestAction(inputEl, {
|
|
36213
36220
|
event: requestCloseEvent,
|
|
36214
36221
|
name: "picker request close",
|