@jsenv/navi 0.27.61 → 0.27.63
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 +300 -286
- package/dist/jsenv_navi.js.map +38 -34
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -32244,6 +32244,7 @@ const getNowHoursRoundedToStep = (stepMinutes, offsetMinutes = 0) => {
|
|
|
32244
32244
|
// Numeric signal types must not fall through to the native type="number"
|
|
32245
32245
|
// (which adds spinner buttons and has poor UX) — they map to navi_number instead.
|
|
32246
32246
|
const VALIDITY_TYPE_TO_INPUT_TYPE = {
|
|
32247
|
+
boolean: "checkbox",
|
|
32247
32248
|
number: "navi_number",
|
|
32248
32249
|
integer: "navi_number",
|
|
32249
32250
|
percentage: "navi_percentage",
|
|
@@ -33102,7 +33103,9 @@ const InputSearchUI = ({
|
|
|
33102
33103
|
const InputEmail = props => {
|
|
33103
33104
|
const Next = useNextResolver();
|
|
33104
33105
|
return jsx(Next, {
|
|
33105
|
-
ui: jsx(InputEmailUI, {
|
|
33106
|
+
ui: jsx(InputEmailUI, {
|
|
33107
|
+
icon: props.icon
|
|
33108
|
+
}),
|
|
33106
33109
|
...props
|
|
33107
33110
|
});
|
|
33108
33111
|
};
|
|
@@ -33652,6 +33655,22 @@ const css$x = /* css */`
|
|
|
33652
33655
|
outline: none;
|
|
33653
33656
|
-webkit-tap-highlight-color: var(--navi-control-tap-highlight-color);
|
|
33654
33657
|
|
|
33658
|
+
&::placeholder {
|
|
33659
|
+
color: var(--x-placeholder-color);
|
|
33660
|
+
}
|
|
33661
|
+
/* Webkit is putting a slight blue bckground on autofilled input */
|
|
33662
|
+
/* For now we override with out custom background color */
|
|
33663
|
+
/* Ideally we'll later provide a custom data attribute with ability to see styles when autofilled */
|
|
33664
|
+
&:-webkit-autofill {
|
|
33665
|
+
-webkit-box-shadow: 0 0 0 1000px var(--x-background-color) inset;
|
|
33666
|
+
}
|
|
33667
|
+
/* Webkit is putting some nasty styles after automplete that look as follow */
|
|
33668
|
+
/* input:-internal-autofill-selected { color: FieldText !important; } */
|
|
33669
|
+
/* Fortunately we can override it as follow */
|
|
33670
|
+
&:-internal-autofill-selected {
|
|
33671
|
+
-webkit-text-fill-color: var(--x-color) !important;
|
|
33672
|
+
}
|
|
33673
|
+
|
|
33655
33674
|
&[type="search"] {
|
|
33656
33675
|
-webkit-appearance: textfield;
|
|
33657
33676
|
|
|
@@ -33769,16 +33788,6 @@ const css$x = /* css */`
|
|
|
33769
33788
|
}
|
|
33770
33789
|
}
|
|
33771
33790
|
}
|
|
33772
|
-
|
|
33773
|
-
.navi_input .navi_control_input::placeholder {
|
|
33774
|
-
color: var(--x-placeholder-color);
|
|
33775
|
-
}
|
|
33776
|
-
.navi_input .navi_control_input:-internal-autofill-selected {
|
|
33777
|
-
/* Webkit is putting some nasty styles after automplete that look as follow */
|
|
33778
|
-
/* input:-internal-autofill-selected { color: FieldText !important; } */
|
|
33779
|
-
/* Fortunately we can override it as follow */
|
|
33780
|
-
-webkit-text-fill-color: var(--x-color) !important;
|
|
33781
|
-
}
|
|
33782
33791
|
`;
|
|
33783
33792
|
const InputHeadlessResolver = props => {
|
|
33784
33793
|
const Next = useNextResolver();
|
|
@@ -35117,7 +35126,10 @@ const useClipboardProps = groupRef => {
|
|
|
35117
35126
|
if (!target.matches) {
|
|
35118
35127
|
return false;
|
|
35119
35128
|
}
|
|
35120
|
-
if (!target.matches("
|
|
35129
|
+
if (!target.matches(`[navi-control-host="input"`)) {
|
|
35130
|
+
return false;
|
|
35131
|
+
}
|
|
35132
|
+
if (!target.closest(".navi_duration_part")) {
|
|
35121
35133
|
return false;
|
|
35122
35134
|
}
|
|
35123
35135
|
return true;
|
|
@@ -35535,29 +35547,6 @@ const transferFocus = (containerEl, debugFocus, e, fallback) => {
|
|
|
35535
35547
|
}
|
|
35536
35548
|
};
|
|
35537
35549
|
|
|
35538
|
-
const useCleanup = () => {
|
|
35539
|
-
const cleanupMethodsRef = useRef(null);
|
|
35540
|
-
let cleanupMethods = cleanupMethodsRef.current;
|
|
35541
|
-
if (!cleanupMethods) {
|
|
35542
|
-
const [publish, subscribe, clear] = createPubSub();
|
|
35543
|
-
const cleanup = () => {
|
|
35544
|
-
publish();
|
|
35545
|
-
clear();
|
|
35546
|
-
};
|
|
35547
|
-
const registerCleanup = (cb) => {
|
|
35548
|
-
subscribe(cb);
|
|
35549
|
-
};
|
|
35550
|
-
cleanupMethodsRef.current = cleanupMethods = [registerCleanup, cleanup];
|
|
35551
|
-
}
|
|
35552
|
-
useLayoutEffect(() => {
|
|
35553
|
-
return () => {
|
|
35554
|
-
const [, cleanup] = cleanupMethods;
|
|
35555
|
-
cleanup();
|
|
35556
|
-
};
|
|
35557
|
-
}, []);
|
|
35558
|
-
return cleanupMethods;
|
|
35559
|
-
};
|
|
35560
|
-
|
|
35561
35550
|
installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
35562
35551
|
.navi_dialog {
|
|
35563
35552
|
&[open] {
|
|
@@ -35579,14 +35568,12 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
|
35579
35568
|
const Dialog = props => {
|
|
35580
35569
|
import.meta.css = [css$r, "@jsenv/navi/src/popup/dialog.jsx"];
|
|
35581
35570
|
const {
|
|
35582
|
-
|
|
35583
|
-
requestClose,
|
|
35571
|
+
openController,
|
|
35584
35572
|
anchorRef,
|
|
35585
35573
|
children,
|
|
35586
35574
|
scrollTrap,
|
|
35587
35575
|
pointerTrap,
|
|
35588
35576
|
centerInVisualViewport: centerInVisualViewportProp,
|
|
35589
|
-
closeRequestHandler,
|
|
35590
35577
|
...rest
|
|
35591
35578
|
} = props;
|
|
35592
35579
|
const defaultRef = useRef();
|
|
@@ -35595,16 +35582,20 @@ const Dialog = props => {
|
|
|
35595
35582
|
const debugFocus = useDebugFocus();
|
|
35596
35583
|
const autoFocusProps = useAutoFocus(ref, props.autoFocus);
|
|
35597
35584
|
|
|
35598
|
-
//
|
|
35599
|
-
//
|
|
35600
|
-
//
|
|
35601
|
-
|
|
35602
|
-
|
|
35603
|
-
|
|
35585
|
+
// Sync the DOM open and return how to sync it back closed, fresh on every
|
|
35586
|
+
// render so it closes over the latest props (scrollTrap, etc.). The
|
|
35587
|
+
// controller (owned by picker_custom.jsx) decides *when* this runs.
|
|
35588
|
+
// openEffect runs outside of render (triggered by openController.open()), so
|
|
35589
|
+
// it cannot call hooks — cleanup is a plain pub/sub.
|
|
35590
|
+
openController.openEffect = e => {
|
|
35591
|
+
const dialogEl = ref.current;
|
|
35592
|
+
if (!dialogEl) {
|
|
35593
|
+
return undefined;
|
|
35594
|
+
}
|
|
35595
|
+
const [cleanup, addCleanup] = createPubSub(true);
|
|
35604
35596
|
const anchor = anchorRef?.current ?? null;
|
|
35605
35597
|
const effectiveAnchor = anchor || document.documentElement;
|
|
35606
35598
|
debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
|
|
35607
|
-
const dialogEl = ref.current;
|
|
35608
35599
|
const {
|
|
35609
35600
|
width,
|
|
35610
35601
|
height
|
|
@@ -35652,22 +35643,15 @@ const Dialog = props => {
|
|
|
35652
35643
|
dialogEl.style.removeProperty("--dialog-top-inset");
|
|
35653
35644
|
});
|
|
35654
35645
|
}
|
|
35655
|
-
|
|
35656
|
-
|
|
35657
|
-
|
|
35658
|
-
|
|
35659
|
-
|
|
35660
|
-
|
|
35661
|
-
|
|
35662
|
-
|
|
35663
|
-
|
|
35664
|
-
markAutofocusRestoreOnClose(dialogEl);
|
|
35665
|
-
dialogEl.close();
|
|
35666
|
-
cleanup();
|
|
35667
|
-
openedRef.current = false;
|
|
35668
|
-
dispatchCustomEvent(dialogEl, "navi_close", {
|
|
35669
|
-
event: e
|
|
35670
|
-
});
|
|
35646
|
+
// Picker's openController.open() reads this back synchronously right
|
|
35647
|
+
// after openEffect() returns (see picker_custom.jsx useOpenController).
|
|
35648
|
+
e.detail.focusedBeforeOpen = focusedBeforeOpen;
|
|
35649
|
+
return () => {
|
|
35650
|
+
debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> closeDialog`);
|
|
35651
|
+
markAutofocusRestoreOnClose(dialogEl);
|
|
35652
|
+
dialogEl.close();
|
|
35653
|
+
cleanup();
|
|
35654
|
+
};
|
|
35671
35655
|
};
|
|
35672
35656
|
return jsx(Box, {
|
|
35673
35657
|
...rest,
|
|
@@ -35685,58 +35669,17 @@ const Dialog = props => {
|
|
|
35685
35669
|
const rect = ref.current.getBoundingClientRect();
|
|
35686
35670
|
const isBackdrop = e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom;
|
|
35687
35671
|
if (isBackdrop) {
|
|
35688
|
-
requestClose(e, {
|
|
35672
|
+
openController.requestClose(e, {
|
|
35689
35673
|
isCancel: true
|
|
35690
35674
|
});
|
|
35691
35675
|
}
|
|
35692
35676
|
}
|
|
35693
35677
|
},
|
|
35694
35678
|
onCancel: e => {
|
|
35695
|
-
requestClose(e, {
|
|
35679
|
+
openController.requestClose(e, {
|
|
35696
35680
|
isCancel: true
|
|
35697
35681
|
});
|
|
35698
35682
|
},
|
|
35699
|
-
onnavi_request_open: e => {
|
|
35700
|
-
const dialogEl = ref.current;
|
|
35701
|
-
if (!dialogEl) {
|
|
35702
|
-
return;
|
|
35703
|
-
}
|
|
35704
|
-
if (openedRef.current) {
|
|
35705
|
-
return;
|
|
35706
|
-
}
|
|
35707
|
-
open(e);
|
|
35708
|
-
},
|
|
35709
|
-
onnavi_request_close: e => {
|
|
35710
|
-
const dialogEl = ref.current;
|
|
35711
|
-
if (!dialogEl) {
|
|
35712
|
-
return;
|
|
35713
|
-
}
|
|
35714
|
-
if (!openedRef.current) {
|
|
35715
|
-
return;
|
|
35716
|
-
}
|
|
35717
|
-
if (closeRequestHandler) {
|
|
35718
|
-
let denied = false;
|
|
35719
|
-
const closePermission = {
|
|
35720
|
-
deny: () => {
|
|
35721
|
-
denied = true;
|
|
35722
|
-
},
|
|
35723
|
-
allow: () => {
|
|
35724
|
-
denied = false;
|
|
35725
|
-
}
|
|
35726
|
-
};
|
|
35727
|
-
closeRequestHandler(e, closePermission);
|
|
35728
|
-
if (denied) {
|
|
35729
|
-
if (e.type === "cancel") {
|
|
35730
|
-
e.preventDefault();
|
|
35731
|
-
}
|
|
35732
|
-
closePermission.allow = () => {
|
|
35733
|
-
close(e);
|
|
35734
|
-
};
|
|
35735
|
-
return;
|
|
35736
|
-
}
|
|
35737
|
-
}
|
|
35738
|
-
close(e);
|
|
35739
|
-
},
|
|
35740
35683
|
children: children
|
|
35741
35684
|
});
|
|
35742
35685
|
};
|
|
@@ -35767,7 +35710,7 @@ installImportMetaCssBuild(import.meta);const css$q = /* css */`
|
|
|
35767
35710
|
const Popover = props => {
|
|
35768
35711
|
import.meta.css = [css$q, "@jsenv/navi/src/popup/popover.jsx"];
|
|
35769
35712
|
const {
|
|
35770
|
-
|
|
35713
|
+
openController,
|
|
35771
35714
|
anchorRef,
|
|
35772
35715
|
scrollTrap,
|
|
35773
35716
|
pointerTrap,
|
|
@@ -35779,7 +35722,6 @@ const Popover = props => {
|
|
|
35779
35722
|
positionYFixed,
|
|
35780
35723
|
spacing = 0,
|
|
35781
35724
|
viewportSpacing = 0,
|
|
35782
|
-
closeRequestHandler,
|
|
35783
35725
|
...rest
|
|
35784
35726
|
} = props;
|
|
35785
35727
|
const defaultRef = useRef();
|
|
@@ -35790,14 +35732,18 @@ const Popover = props => {
|
|
|
35790
35732
|
const debugFocus = useDebugFocus();
|
|
35791
35733
|
const autoFocusProps = useAutoFocus(ref, props.autoFocus);
|
|
35792
35734
|
|
|
35793
|
-
//
|
|
35794
|
-
//
|
|
35795
|
-
//
|
|
35796
|
-
|
|
35797
|
-
|
|
35798
|
-
|
|
35799
|
-
debugPopup(e, `openPopover()`);
|
|
35735
|
+
// Sync the DOM open and return how to sync it back closed, fresh on every
|
|
35736
|
+
// render so it closes over the latest props (scrollTrap, etc.). The
|
|
35737
|
+
// controller (owned by picker_custom.jsx) decides *when* this runs.
|
|
35738
|
+
// openEffect runs outside of render (triggered by openController.open()), so
|
|
35739
|
+
// it cannot call hooks — cleanup is a plain pub/sub.
|
|
35740
|
+
openController.openEffect = e => {
|
|
35800
35741
|
const popoverEl = ref.current;
|
|
35742
|
+
if (!popoverEl) {
|
|
35743
|
+
return undefined;
|
|
35744
|
+
}
|
|
35745
|
+
const [cleanup, addCleanup] = createPubSub(true);
|
|
35746
|
+
debugPopup(e, `openPopover()`);
|
|
35801
35747
|
const focusedBeforeOpen = getFocusedBeforeTransfer(e);
|
|
35802
35748
|
popoverEl.showPopover();
|
|
35803
35749
|
transferFocus(popoverEl, debugFocus, e, focusedBeforeOpen);
|
|
@@ -35873,22 +35819,15 @@ const Popover = props => {
|
|
|
35873
35819
|
addCleanup(() => {
|
|
35874
35820
|
rectEffect.disconnect();
|
|
35875
35821
|
});
|
|
35876
|
-
|
|
35877
|
-
|
|
35878
|
-
|
|
35879
|
-
|
|
35880
|
-
|
|
35881
|
-
|
|
35882
|
-
|
|
35883
|
-
|
|
35884
|
-
|
|
35885
|
-
markAutofocusRestoreOnClose(popoverEl);
|
|
35886
|
-
popoverEl.hidePopover();
|
|
35887
|
-
cleanup();
|
|
35888
|
-
openedRef.current = false;
|
|
35889
|
-
dispatchCustomEvent(popoverEl, "navi_close", {
|
|
35890
|
-
event: e
|
|
35891
|
-
});
|
|
35822
|
+
// Picker's openController.open() reads this back synchronously right
|
|
35823
|
+
// after openEffect() returns (see picker_custom.jsx useOpenController).
|
|
35824
|
+
e.detail.focusedBeforeOpen = focusedBeforeOpen;
|
|
35825
|
+
return () => {
|
|
35826
|
+
debugPopup(e, `closePopover()`);
|
|
35827
|
+
markAutofocusRestoreOnClose(popoverEl);
|
|
35828
|
+
popoverEl.hidePopover();
|
|
35829
|
+
cleanup();
|
|
35830
|
+
};
|
|
35892
35831
|
};
|
|
35893
35832
|
return jsxs(Box, {
|
|
35894
35833
|
id: id,
|
|
@@ -35898,44 +35837,6 @@ const Popover = props => {
|
|
|
35898
35837
|
ref: ref,
|
|
35899
35838
|
baseClassName: "navi_popover",
|
|
35900
35839
|
pseudoClasses: POPOVER_PSEUDO_CLASSES,
|
|
35901
|
-
onnavi_request_open: e => {
|
|
35902
|
-
const popoverEl = ref.current;
|
|
35903
|
-
if (!popoverEl) {
|
|
35904
|
-
return;
|
|
35905
|
-
}
|
|
35906
|
-
if (openedRef.current) {
|
|
35907
|
-
return;
|
|
35908
|
-
}
|
|
35909
|
-
open(e);
|
|
35910
|
-
},
|
|
35911
|
-
onnavi_request_close: e => {
|
|
35912
|
-
const popoverEl = ref.current;
|
|
35913
|
-
if (!popoverEl) {
|
|
35914
|
-
return;
|
|
35915
|
-
}
|
|
35916
|
-
if (!openedRef.current) {
|
|
35917
|
-
return;
|
|
35918
|
-
}
|
|
35919
|
-
if (closeRequestHandler) {
|
|
35920
|
-
let denied = false;
|
|
35921
|
-
const closePermission = {
|
|
35922
|
-
deny: () => {
|
|
35923
|
-
denied = true;
|
|
35924
|
-
},
|
|
35925
|
-
allow: () => {
|
|
35926
|
-
denied = false;
|
|
35927
|
-
}
|
|
35928
|
-
};
|
|
35929
|
-
closeRequestHandler(e, closePermission);
|
|
35930
|
-
if (denied) {
|
|
35931
|
-
closePermission.allow = () => {
|
|
35932
|
-
close(e);
|
|
35933
|
-
};
|
|
35934
|
-
return;
|
|
35935
|
-
}
|
|
35936
|
-
}
|
|
35937
|
-
close(e);
|
|
35938
|
-
},
|
|
35939
35840
|
children: [jsx("div", {
|
|
35940
35841
|
className: "navi_popover_backdrop",
|
|
35941
35842
|
"aria-hidden": "true",
|
|
@@ -35947,7 +35848,7 @@ const Popover = props => {
|
|
|
35947
35848
|
e.preventDefault();
|
|
35948
35849
|
return;
|
|
35949
35850
|
}
|
|
35950
|
-
requestClose(e, {
|
|
35851
|
+
openController.requestClose(e, {
|
|
35951
35852
|
isCancel: true
|
|
35952
35853
|
});
|
|
35953
35854
|
}
|
|
@@ -36253,7 +36154,6 @@ const PickerCustom = props => {
|
|
|
36253
36154
|
// In "dialog" mode, enterExpanded() pushes a history entry so the back button closes.
|
|
36254
36155
|
// In "popover" mode, it replaces the current history state (no history entry added).
|
|
36255
36156
|
const pickerNavType = mode === "dialog" ? "push" : "replace";
|
|
36256
|
-
const expandedRef = useRef(false);
|
|
36257
36157
|
const [expanded, enterExpanded, leaveExpanded] = useNavState(popupId, {
|
|
36258
36158
|
type: pickerNavType,
|
|
36259
36159
|
// onLeave fires only when the state key disappears externally (back button/gesture most of the time).
|
|
@@ -36265,95 +36165,108 @@ const PickerCustom = props => {
|
|
|
36265
36165
|
});
|
|
36266
36166
|
}
|
|
36267
36167
|
});
|
|
36268
|
-
// expandedRef tracks actual open/close state, updated only by onOpen/onClose.
|
|
36269
|
-
// NOT synced to expanded on every render so the useLayoutEffect guard below works.
|
|
36270
|
-
const valueAtOpenRef = useRef(null);
|
|
36271
|
-
const activeElementAtOpenRef = useRef(null);
|
|
36272
|
-
const onOpen = e => {
|
|
36273
|
-
expandedRef.current = true;
|
|
36274
|
-
enterExpanded();
|
|
36275
|
-
const focusedBeforeOpen = e.detail.focusedBeforeOpen;
|
|
36276
|
-
activeElementAtOpenRef.current = focusedBeforeOpen;
|
|
36277
|
-
debugFocus(e, "picked opened, store element focused", focusedBeforeOpen);
|
|
36278
|
-
const valueAtOpen = getPickerInputUIState(ref.current);
|
|
36279
|
-
valueAtOpenRef.current = valueAtOpen;
|
|
36280
|
-
debugPopup(e, `picker opened, store value at open`, valueAtOpen);
|
|
36281
|
-
};
|
|
36282
|
-
const restoreFocus = e => {
|
|
36283
|
-
const activeElementAtOpen = activeElementAtOpenRef.current;
|
|
36284
|
-
activeElementAtOpenRef.current = null;
|
|
36285
|
-
const focusoutEvent = findEvent(e, "focusout");
|
|
36286
|
-
if (focusoutEvent) {
|
|
36287
|
-
debugFocus(e, `closed by focusout -> let focus go away`);
|
|
36288
|
-
return;
|
|
36289
|
-
}
|
|
36290
|
-
const mousedownEvent = findEvent(e, "mousedown");
|
|
36291
|
-
if (mousedownEvent) {
|
|
36292
|
-
debugFocus(e, "closed by mousedown -> prevent browser focus (mousedown.preventDefault())");
|
|
36293
|
-
mousedownEvent.preventDefault();
|
|
36294
|
-
}
|
|
36295
|
-
debugFocus(e, `restore focus to previously focused element`, activeElementAtOpen);
|
|
36296
|
-
activeElementAtOpen.focus({
|
|
36297
|
-
preventScroll: true
|
|
36298
|
-
});
|
|
36299
|
-
return;
|
|
36300
|
-
};
|
|
36301
|
-
const onClose = e => {
|
|
36302
|
-
const mousedownEvent = findEvent(e, "mousedown");
|
|
36303
|
-
if (mousedownEvent) {
|
|
36304
|
-
debugPopup(e, `closed by mousedown -> disable next click`);
|
|
36305
|
-
disableClickFor();
|
|
36306
|
-
} else {
|
|
36307
|
-
const spaceEvent = findEvent(e, e => e.type === "keydown" && e.key === " ");
|
|
36308
|
-
if (spaceEvent) {
|
|
36309
|
-
// space would trigger a click on the picker button causing it to re-open immediatly after closing
|
|
36310
|
-
debugPopup(e, `closed by space key -> prevent browser click`);
|
|
36311
|
-
// browser won't try to dispatch click
|
|
36312
|
-
// and our "space_to_open" will see e.defaultPrevented too and won't try to open picker
|
|
36313
|
-
spaceEvent.preventDefault();
|
|
36314
|
-
}
|
|
36315
|
-
}
|
|
36316
|
-
expandedRef.current = false;
|
|
36317
|
-
leaveExpanded({
|
|
36318
|
-
isBack: e.detail.isCancel
|
|
36319
|
-
});
|
|
36320
|
-
// Reset so the next opening re-evaluates screen size
|
|
36321
|
-
defaultModeRef.current = null;
|
|
36322
|
-
restoreFocus(e);
|
|
36323
|
-
};
|
|
36324
36168
|
const disableClickFor = useIgnoreClickForMousedown(ref, e => {
|
|
36325
36169
|
debugPopup(e, `click ignored`);
|
|
36326
36170
|
});
|
|
36171
|
+
// openController centralizes open/close decision-making (validation,
|
|
36172
|
+
// focus and value bookkeeping) for the picker. The returned
|
|
36173
|
+
// { onRequestClose, onClose } pair is the picker's reaction to close
|
|
36174
|
+
// requests — see createOpenController below for the full contract.
|
|
36175
|
+
const openController = useOpenController(openEvent => {
|
|
36176
|
+
enterExpanded();
|
|
36177
|
+
const focusedBeforeOpen = openEvent.detail.focusedBeforeOpen;
|
|
36178
|
+
debugFocus(openEvent, "picked opened, store element focused", focusedBeforeOpen);
|
|
36179
|
+
const valueAtOpen = getPickerInputUIState(ref.current);
|
|
36180
|
+
debugPopup(openEvent, `picker opened, store value at open`, valueAtOpen);
|
|
36181
|
+
return {
|
|
36182
|
+
onRequestClose: requestCloseEvent => {
|
|
36183
|
+
if (requestCloseEvent.detail.isCancel) {
|
|
36184
|
+
// Cancelling always succeeds — nothing to validate.
|
|
36185
|
+
return;
|
|
36186
|
+
}
|
|
36187
|
+
const pickerEl = ref.current;
|
|
36188
|
+
const inputEl = getPickerInput(pickerEl);
|
|
36189
|
+
dispatchRequestAction(inputEl, {
|
|
36190
|
+
event: requestCloseEvent,
|
|
36191
|
+
name: "picker request close",
|
|
36192
|
+
prevented: () => {
|
|
36193
|
+
requestCloseEvent.preventDefault();
|
|
36194
|
+
},
|
|
36195
|
+
// Always report validation when the picker tries to close so the
|
|
36196
|
+
// user sees what is wrong, even if the picker has no action prop.
|
|
36197
|
+
reportOnInvalid: true,
|
|
36198
|
+
onInvalid: () => {
|
|
36199
|
+
requestCloseEvent.preventDefault();
|
|
36200
|
+
}
|
|
36201
|
+
});
|
|
36202
|
+
},
|
|
36203
|
+
onClose: closeEvent => {
|
|
36204
|
+
if (closeEvent.detail.isCancel) {
|
|
36205
|
+
const pickerEl = ref.current;
|
|
36206
|
+
const inputEl = getPickerInput(pickerEl);
|
|
36207
|
+
debugPopup(closeEvent, `picker cancel, restoring value at open ${JSON.stringify(valueAtOpen)}`);
|
|
36208
|
+
dispatchRequestSetUIState(inputEl, valueAtOpen, {
|
|
36209
|
+
event: closeEvent
|
|
36210
|
+
});
|
|
36211
|
+
}
|
|
36212
|
+
{
|
|
36213
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
36214
|
+
if (mousedownEvent) {
|
|
36215
|
+
debugPopup(closeEvent, `closed by mousedown -> disable next click`);
|
|
36216
|
+
disableClickFor();
|
|
36217
|
+
} else {
|
|
36218
|
+
const spaceEvent = findEvent(closeEvent, e => e.type === "keydown" && e.key === " ");
|
|
36219
|
+
if (spaceEvent) {
|
|
36220
|
+
// space would trigger a click on the picker button causing it to re-open immediatly after closing
|
|
36221
|
+
debugPopup(closeEvent, `closed by space key -> prevent browser click`);
|
|
36222
|
+
// browser won't try to dispatch click
|
|
36223
|
+
// and our "space_to_open" will see e.defaultPrevented too and won't try to open picker
|
|
36224
|
+
spaceEvent.preventDefault();
|
|
36225
|
+
}
|
|
36226
|
+
}
|
|
36227
|
+
}
|
|
36228
|
+
{
|
|
36229
|
+
const focusoutEvent = findEvent(closeEvent, "focusout");
|
|
36230
|
+
if (focusoutEvent) {
|
|
36231
|
+
debugFocus(closeEvent, `closed by focusout -> let focus go away`);
|
|
36232
|
+
} else {
|
|
36233
|
+
const mousedownEvent = findEvent(closeEvent, "mousedown");
|
|
36234
|
+
if (mousedownEvent) {
|
|
36235
|
+
debugFocus(closeEvent, "closed by mousedown -> prevent browser focus (mousedown.preventDefault())");
|
|
36236
|
+
mousedownEvent.preventDefault();
|
|
36237
|
+
}
|
|
36238
|
+
debugFocus(closeEvent, `restore focus to previously focused element`, focusedBeforeOpen);
|
|
36239
|
+
focusedBeforeOpen.focus({
|
|
36240
|
+
preventScroll: true
|
|
36241
|
+
});
|
|
36242
|
+
}
|
|
36243
|
+
}
|
|
36244
|
+
leaveExpanded({
|
|
36245
|
+
isBack: closeEvent.detail.isCancel
|
|
36246
|
+
});
|
|
36247
|
+
// Reset so the next opening re-evaluates screen size
|
|
36248
|
+
defaultModeRef.current = null;
|
|
36249
|
+
}
|
|
36250
|
+
};
|
|
36251
|
+
});
|
|
36327
36252
|
const requestOpen = (e, detail) => {
|
|
36328
36253
|
// scroll <button> of the picker into view when opening it
|
|
36329
36254
|
const pickerEl = ref.current;
|
|
36330
36255
|
pickerEl.scrollIntoView({
|
|
36331
36256
|
block: "nearest"
|
|
36332
36257
|
});
|
|
36333
|
-
|
|
36334
|
-
return dispatchCustomEvent(popupEl, "navi_request_open", {
|
|
36335
|
-
event: e,
|
|
36336
|
-
...detail
|
|
36337
|
-
});
|
|
36338
|
-
};
|
|
36339
|
-
const requestClose = (e = new CustomEvent("programmatic", {
|
|
36340
|
-
detail: {}
|
|
36341
|
-
}), detail) => {
|
|
36342
|
-
const popupEl = popupRef.current;
|
|
36343
|
-
return dispatchCustomEvent(popupEl, "navi_request_close", {
|
|
36344
|
-
event: e,
|
|
36345
|
-
...detail
|
|
36346
|
-
});
|
|
36258
|
+
openController.open(e, detail);
|
|
36347
36259
|
};
|
|
36260
|
+
const requestClose = openController.requestClose;
|
|
36348
36261
|
const open = Boolean(expanded);
|
|
36349
36262
|
useLayoutEffect(() => {
|
|
36350
36263
|
if (open === undefined) {
|
|
36351
36264
|
return;
|
|
36352
36265
|
}
|
|
36353
36266
|
// Skip when the popup is already in the desired state.
|
|
36354
|
-
//
|
|
36355
|
-
// so it is the authoritative check against feedback loops.
|
|
36356
|
-
if (open ===
|
|
36267
|
+
// openController.opened tracks actual open/close (updated by onopen/onclose,
|
|
36268
|
+
// not by renders) so it is the authoritative check against feedback loops.
|
|
36269
|
+
if (open === openController.opened) {
|
|
36357
36270
|
return;
|
|
36358
36271
|
}
|
|
36359
36272
|
// open_prop_change means the parent is driving the open state directly
|
|
@@ -36388,7 +36301,7 @@ const PickerCustom = props => {
|
|
|
36388
36301
|
// requestClose(e);
|
|
36389
36302
|
},
|
|
36390
36303
|
"onnavi_request_open": e => {
|
|
36391
|
-
if (
|
|
36304
|
+
if (openController.opened) {
|
|
36392
36305
|
return;
|
|
36393
36306
|
}
|
|
36394
36307
|
requestInteraction({
|
|
@@ -36403,7 +36316,9 @@ const PickerCustom = props => {
|
|
|
36403
36316
|
requestInteraction({
|
|
36404
36317
|
event: e,
|
|
36405
36318
|
allowed: () => {
|
|
36406
|
-
requestClose(e
|
|
36319
|
+
requestClose(e, {
|
|
36320
|
+
isCancel: e.detail.isCancel
|
|
36321
|
+
});
|
|
36407
36322
|
}
|
|
36408
36323
|
});
|
|
36409
36324
|
},
|
|
@@ -36414,41 +36329,7 @@ const PickerCustom = props => {
|
|
|
36414
36329
|
});
|
|
36415
36330
|
Object.assign(popupProps, {
|
|
36416
36331
|
anchorRef: props.ref,
|
|
36417
|
-
|
|
36418
|
-
closeRequestHandler: (requestCloseEvent, closePermission) => {
|
|
36419
|
-
const isCancel = requestCloseEvent.detail.isCancel;
|
|
36420
|
-
if (isCancel) {
|
|
36421
|
-
const pickerEl = ref.current;
|
|
36422
|
-
const inputEl = getPickerInput(pickerEl);
|
|
36423
|
-
const valueAtOpen = valueAtOpenRef.current;
|
|
36424
|
-
debugPopup(requestCloseEvent, `picker cancel, restoring value at open ${JSON.stringify(valueAtOpen)}`);
|
|
36425
|
-
dispatchRequestSetUIState(inputEl, valueAtOpen, {
|
|
36426
|
-
event: requestCloseEvent
|
|
36427
|
-
});
|
|
36428
|
-
return;
|
|
36429
|
-
}
|
|
36430
|
-
const pickerEl = ref.current;
|
|
36431
|
-
const inputEl = getPickerInput(pickerEl);
|
|
36432
|
-
dispatchRequestAction(inputEl, {
|
|
36433
|
-
event: requestCloseEvent,
|
|
36434
|
-
name: "picker close",
|
|
36435
|
-
prevented: () => {
|
|
36436
|
-
closePermission.deny();
|
|
36437
|
-
},
|
|
36438
|
-
// Always report validation when the picker tries to close so the
|
|
36439
|
-
// user sees what is wrong, even if the picker has no action prop.
|
|
36440
|
-
reportOnInvalid: true,
|
|
36441
|
-
onInvalid: () => {
|
|
36442
|
-
closePermission.deny();
|
|
36443
|
-
}
|
|
36444
|
-
});
|
|
36445
|
-
},
|
|
36446
|
-
onnavi_open: e => {
|
|
36447
|
-
onOpen(e);
|
|
36448
|
-
},
|
|
36449
|
-
onnavi_close: e => {
|
|
36450
|
-
onClose(e);
|
|
36451
|
-
}
|
|
36332
|
+
openController
|
|
36452
36333
|
});
|
|
36453
36334
|
{
|
|
36454
36335
|
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
@@ -36505,7 +36386,7 @@ const PickerCustom = props => {
|
|
|
36505
36386
|
};
|
|
36506
36387
|
},
|
|
36507
36388
|
"escape": e => {
|
|
36508
|
-
if (!
|
|
36389
|
+
if (!openController.opened) {
|
|
36509
36390
|
return null;
|
|
36510
36391
|
}
|
|
36511
36392
|
return {
|
|
@@ -36529,7 +36410,7 @@ const PickerCustom = props => {
|
|
|
36529
36410
|
if (isWithinPopup(e.target)) {
|
|
36530
36411
|
return null;
|
|
36531
36412
|
}
|
|
36532
|
-
if (
|
|
36413
|
+
if (openController.opened) {
|
|
36533
36414
|
return {
|
|
36534
36415
|
name: "mousedown to close picker",
|
|
36535
36416
|
allowed: () => requestClose(e, {
|
|
@@ -36625,8 +36506,7 @@ const PickerContentInsidePopover = props => {
|
|
|
36625
36506
|
name: "blur",
|
|
36626
36507
|
category: "interaction",
|
|
36627
36508
|
allowed: () => {
|
|
36628
|
-
|
|
36629
|
-
event: e,
|
|
36509
|
+
popupProps.openController.requestClose(e, {
|
|
36630
36510
|
isCancel: true
|
|
36631
36511
|
});
|
|
36632
36512
|
}
|
|
@@ -36651,9 +36531,7 @@ const PickerContentInsidePopover = props => {
|
|
|
36651
36531
|
if (e.button !== 0) {
|
|
36652
36532
|
return;
|
|
36653
36533
|
}
|
|
36654
|
-
|
|
36655
|
-
dispatchCustomEvent(popupEl, "navi_request_close", {
|
|
36656
|
-
event: e,
|
|
36534
|
+
popupProps.openController.requestClose(e, {
|
|
36657
36535
|
isCancel: true
|
|
36658
36536
|
});
|
|
36659
36537
|
},
|
|
@@ -36738,6 +36616,142 @@ const useIgnoreClickForMousedown = (elementRef, onIgnore) => {
|
|
|
36738
36616
|
return disableClickFor;
|
|
36739
36617
|
};
|
|
36740
36618
|
|
|
36619
|
+
// Created once per picker instance: openHandler is wrapped in a stable callback
|
|
36620
|
+
// so the controller identity never changes across renders, even though
|
|
36621
|
+
// Dialog/Popover read fresh closures (scrollTrap, etc.) via
|
|
36622
|
+
// openController.openEffect on every render.
|
|
36623
|
+
const useOpenController = openHandler => {
|
|
36624
|
+
const stableOpenHandler = useStableCallback(openHandler);
|
|
36625
|
+
const controllerRef = useRef(null);
|
|
36626
|
+
if (!controllerRef.current) {
|
|
36627
|
+
controllerRef.current = createOpenController(stableOpenHandler);
|
|
36628
|
+
}
|
|
36629
|
+
// Unmount safety net: if Dialog/Popover unmounts while still open (parent
|
|
36630
|
+
// removes it from the tree without going through requestClose()), there is
|
|
36631
|
+
// no choice to leave open — close it for real.
|
|
36632
|
+
useLayoutEffect(() => {
|
|
36633
|
+
return () => {
|
|
36634
|
+
controllerRef.current.close();
|
|
36635
|
+
};
|
|
36636
|
+
}, []);
|
|
36637
|
+
return controllerRef.current;
|
|
36638
|
+
};
|
|
36639
|
+
|
|
36640
|
+
/**
|
|
36641
|
+
* Owns open/close decision-making for a picker popup (Dialog or Popover):
|
|
36642
|
+
* guards against duplicate requests and notifies the picker's own reactions.
|
|
36643
|
+
*
|
|
36644
|
+
* `controller.openEffect` is implemented by the controlled element (Dialog or
|
|
36645
|
+
* Popover), reassigned on every render so it always closes over the latest
|
|
36646
|
+
* props (scrollTrap, anchorRef, etc.). It performs whatever DOM side effects
|
|
36647
|
+
* are needed to make the element actually open (`showModal()`/`showPopover()`,
|
|
36648
|
+
* focus transfer, positioning, traps...) and returns its cleanup —
|
|
36649
|
+
* the matching side effects to sync back to closed (`close()`/
|
|
36650
|
+
* `hidePopover()`, releasing traps...). That cleanup is kept private to the
|
|
36651
|
+
* controller (not exposed as a property) and invoked when the popup actually
|
|
36652
|
+
* closes, however that happens.
|
|
36653
|
+
*
|
|
36654
|
+
* Dialog/Popover also call `openController.requestClose(e, { isCancel })` for
|
|
36655
|
+
* their own internal triggers (backdrop click, Escape).
|
|
36656
|
+
*
|
|
36657
|
+
* `openHandler` is the picker's own business logic, passed once to
|
|
36658
|
+
* `createOpenController`. Its return value is `{ onRequestClose, onClose }`,
|
|
36659
|
+
* in the spirit of CloseWatcher
|
|
36660
|
+
* (https://developer.mozilla.org/en-US/docs/Web/API/CloseWatcher) but with
|
|
36661
|
+
* clearer naming than its cancel/close pair:
|
|
36662
|
+
* - `onRequestClose(e)`: about to close — call `e.preventDefault()` to stay
|
|
36663
|
+
* open. Validation lives here.
|
|
36664
|
+
* - `onClose(e)`: actually closing, not preventable — final reactions live here.
|
|
36665
|
+
*
|
|
36666
|
+
* The controller exposes matching action methods:
|
|
36667
|
+
* - `open()`: requests opening — runs `openEffect`, then `openHandler`.
|
|
36668
|
+
* - `requestClose()`: requests closing — calls `onRequestClose` then `onClose`,
|
|
36669
|
+
* stopping after the first if denied. The popup may choose to stay open.
|
|
36670
|
+
* - `close()`: closes for real — calls only `onClose`, skipping
|
|
36671
|
+
* `onRequestClose` entirely. Used when there really is no choice (e.g. the
|
|
36672
|
+
* popup unmounting).
|
|
36673
|
+
*/
|
|
36674
|
+
const createOpenController = openHandler => {
|
|
36675
|
+
let closeHandlers = null; // { onRequestClose, onClose } returned by openHandler
|
|
36676
|
+
let openEffectCleanup = null; // function returned by openEffect, undoes its DOM side effects
|
|
36677
|
+
const performClose = closeEvent => {
|
|
36678
|
+
controller.opened = false;
|
|
36679
|
+
// Sync the DOM closed first (releasing the focus trap) — only then run
|
|
36680
|
+
// the picker's own reaction (onClose may restore focus to an element
|
|
36681
|
+
// outside the popup, which the focus trap would otherwise fight while
|
|
36682
|
+
// still active).
|
|
36683
|
+
openEffectCleanup?.(closeEvent);
|
|
36684
|
+
openEffectCleanup = null;
|
|
36685
|
+
closeHandlers?.onClose?.(closeEvent);
|
|
36686
|
+
closeHandlers = null;
|
|
36687
|
+
};
|
|
36688
|
+
const controller = {
|
|
36689
|
+
opened: false,
|
|
36690
|
+
openEffect: null,
|
|
36691
|
+
open: (e, detail) => {
|
|
36692
|
+
if (controller.opened || !controller.openEffect) {
|
|
36693
|
+
return;
|
|
36694
|
+
}
|
|
36695
|
+
const requestOpenEvent = new CustomEvent("navi_request_open", {
|
|
36696
|
+
detail: {
|
|
36697
|
+
event: e,
|
|
36698
|
+
...detail
|
|
36699
|
+
},
|
|
36700
|
+
cancelable: true
|
|
36701
|
+
});
|
|
36702
|
+
chainEvent(requestOpenEvent, e);
|
|
36703
|
+
controller.opened = true;
|
|
36704
|
+
// openEffect may populate requestOpenEvent.detail (e.g. focusedBeforeOpen)
|
|
36705
|
+
// by mutating it — openHandler reads it right after, synchronously.
|
|
36706
|
+
openEffectCleanup = controller.openEffect(requestOpenEvent) || null;
|
|
36707
|
+
closeHandlers = openHandler(requestOpenEvent) || null;
|
|
36708
|
+
},
|
|
36709
|
+
requestClose: (e = new CustomEvent("programmatic", {
|
|
36710
|
+
detail: {}
|
|
36711
|
+
}), detail) => {
|
|
36712
|
+
if (!controller.opened) {
|
|
36713
|
+
return;
|
|
36714
|
+
}
|
|
36715
|
+
const requestCloseEvent = new CustomEvent("navi_request_close", {
|
|
36716
|
+
detail: {
|
|
36717
|
+
event: e,
|
|
36718
|
+
...detail
|
|
36719
|
+
},
|
|
36720
|
+
cancelable: true
|
|
36721
|
+
});
|
|
36722
|
+
chainEvent(requestCloseEvent, e);
|
|
36723
|
+
closeHandlers?.onRequestClose?.(requestCloseEvent);
|
|
36724
|
+
if (requestCloseEvent.defaultPrevented) {
|
|
36725
|
+
// The native <dialog> "cancel" event (Escape key) closes the dialog
|
|
36726
|
+
// by default; prevent that default so denial actually keeps it open.
|
|
36727
|
+
const nativeCancelEvent = findEvent(requestCloseEvent, "cancel");
|
|
36728
|
+
if (nativeCancelEvent) {
|
|
36729
|
+
nativeCancelEvent.preventDefault();
|
|
36730
|
+
}
|
|
36731
|
+
return;
|
|
36732
|
+
}
|
|
36733
|
+
performClose(requestCloseEvent);
|
|
36734
|
+
},
|
|
36735
|
+
close: (e = new CustomEvent("programmatic", {
|
|
36736
|
+
detail: {}
|
|
36737
|
+
}), detail) => {
|
|
36738
|
+
if (!controller.opened) {
|
|
36739
|
+
return;
|
|
36740
|
+
}
|
|
36741
|
+
const closeEvent = new CustomEvent("navi_close", {
|
|
36742
|
+
detail: {
|
|
36743
|
+
event: e,
|
|
36744
|
+
...detail
|
|
36745
|
+
}
|
|
36746
|
+
});
|
|
36747
|
+
chainEvent(closeEvent, e);
|
|
36748
|
+
// Skips onRequestClose entirely — there is no choice here.
|
|
36749
|
+
performClose(closeEvent);
|
|
36750
|
+
}
|
|
36751
|
+
};
|
|
36752
|
+
return controller;
|
|
36753
|
+
};
|
|
36754
|
+
|
|
36741
36755
|
const PickerNaviMinute = props => {
|
|
36742
36756
|
const Next = useNextResolver();
|
|
36743
36757
|
const {
|