@jsenv/navi 0.27.62 → 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 +286 -278
- package/dist/jsenv_navi.js.map +36 -32
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -33658,13 +33658,16 @@ const css$x = /* css */`
|
|
|
33658
33658
|
&::placeholder {
|
|
33659
33659
|
color: var(--x-placeholder-color);
|
|
33660
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 */
|
|
33661
33664
|
&:-webkit-autofill {
|
|
33662
33665
|
-webkit-box-shadow: 0 0 0 1000px var(--x-background-color) inset;
|
|
33663
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 */
|
|
33664
33670
|
&:-internal-autofill-selected {
|
|
33665
|
-
/* Webkit is putting some nasty styles after automplete that look as follow */
|
|
33666
|
-
/* input:-internal-autofill-selected { color: FieldText !important; } */
|
|
33667
|
-
/* Fortunately we can override it as follow */
|
|
33668
33671
|
-webkit-text-fill-color: var(--x-color) !important;
|
|
33669
33672
|
}
|
|
33670
33673
|
|
|
@@ -35123,7 +35126,10 @@ const useClipboardProps = groupRef => {
|
|
|
35123
35126
|
if (!target.matches) {
|
|
35124
35127
|
return false;
|
|
35125
35128
|
}
|
|
35126
|
-
if (!target.matches("
|
|
35129
|
+
if (!target.matches(`[navi-control-host="input"`)) {
|
|
35130
|
+
return false;
|
|
35131
|
+
}
|
|
35132
|
+
if (!target.closest(".navi_duration_part")) {
|
|
35127
35133
|
return false;
|
|
35128
35134
|
}
|
|
35129
35135
|
return true;
|
|
@@ -35541,29 +35547,6 @@ const transferFocus = (containerEl, debugFocus, e, fallback) => {
|
|
|
35541
35547
|
}
|
|
35542
35548
|
};
|
|
35543
35549
|
|
|
35544
|
-
const useCleanup = () => {
|
|
35545
|
-
const cleanupMethodsRef = useRef(null);
|
|
35546
|
-
let cleanupMethods = cleanupMethodsRef.current;
|
|
35547
|
-
if (!cleanupMethods) {
|
|
35548
|
-
const [publish, subscribe, clear] = createPubSub();
|
|
35549
|
-
const cleanup = () => {
|
|
35550
|
-
publish();
|
|
35551
|
-
clear();
|
|
35552
|
-
};
|
|
35553
|
-
const registerCleanup = (cb) => {
|
|
35554
|
-
subscribe(cb);
|
|
35555
|
-
};
|
|
35556
|
-
cleanupMethodsRef.current = cleanupMethods = [registerCleanup, cleanup];
|
|
35557
|
-
}
|
|
35558
|
-
useLayoutEffect(() => {
|
|
35559
|
-
return () => {
|
|
35560
|
-
const [, cleanup] = cleanupMethods;
|
|
35561
|
-
cleanup();
|
|
35562
|
-
};
|
|
35563
|
-
}, []);
|
|
35564
|
-
return cleanupMethods;
|
|
35565
|
-
};
|
|
35566
|
-
|
|
35567
35550
|
installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
35568
35551
|
.navi_dialog {
|
|
35569
35552
|
&[open] {
|
|
@@ -35585,14 +35568,12 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
|
35585
35568
|
const Dialog = props => {
|
|
35586
35569
|
import.meta.css = [css$r, "@jsenv/navi/src/popup/dialog.jsx"];
|
|
35587
35570
|
const {
|
|
35588
|
-
|
|
35589
|
-
requestClose,
|
|
35571
|
+
openController,
|
|
35590
35572
|
anchorRef,
|
|
35591
35573
|
children,
|
|
35592
35574
|
scrollTrap,
|
|
35593
35575
|
pointerTrap,
|
|
35594
35576
|
centerInVisualViewport: centerInVisualViewportProp,
|
|
35595
|
-
closeRequestHandler,
|
|
35596
35577
|
...rest
|
|
35597
35578
|
} = props;
|
|
35598
35579
|
const defaultRef = useRef();
|
|
@@ -35601,16 +35582,20 @@ const Dialog = props => {
|
|
|
35601
35582
|
const debugFocus = useDebugFocus();
|
|
35602
35583
|
const autoFocusProps = useAutoFocus(ref, props.autoFocus);
|
|
35603
35584
|
|
|
35604
|
-
//
|
|
35605
|
-
//
|
|
35606
|
-
//
|
|
35607
|
-
|
|
35608
|
-
|
|
35609
|
-
|
|
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);
|
|
35610
35596
|
const anchor = anchorRef?.current ?? null;
|
|
35611
35597
|
const effectiveAnchor = anchor || document.documentElement;
|
|
35612
35598
|
debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
|
|
35613
|
-
const dialogEl = ref.current;
|
|
35614
35599
|
const {
|
|
35615
35600
|
width,
|
|
35616
35601
|
height
|
|
@@ -35658,22 +35643,15 @@ const Dialog = props => {
|
|
|
35658
35643
|
dialogEl.style.removeProperty("--dialog-top-inset");
|
|
35659
35644
|
});
|
|
35660
35645
|
}
|
|
35661
|
-
|
|
35662
|
-
|
|
35663
|
-
|
|
35664
|
-
|
|
35665
|
-
|
|
35666
|
-
|
|
35667
|
-
|
|
35668
|
-
|
|
35669
|
-
|
|
35670
|
-
markAutofocusRestoreOnClose(dialogEl);
|
|
35671
|
-
dialogEl.close();
|
|
35672
|
-
cleanup();
|
|
35673
|
-
openedRef.current = false;
|
|
35674
|
-
dispatchCustomEvent(dialogEl, "navi_close", {
|
|
35675
|
-
event: e
|
|
35676
|
-
});
|
|
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
|
+
};
|
|
35677
35655
|
};
|
|
35678
35656
|
return jsx(Box, {
|
|
35679
35657
|
...rest,
|
|
@@ -35691,58 +35669,17 @@ const Dialog = props => {
|
|
|
35691
35669
|
const rect = ref.current.getBoundingClientRect();
|
|
35692
35670
|
const isBackdrop = e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom;
|
|
35693
35671
|
if (isBackdrop) {
|
|
35694
|
-
requestClose(e, {
|
|
35672
|
+
openController.requestClose(e, {
|
|
35695
35673
|
isCancel: true
|
|
35696
35674
|
});
|
|
35697
35675
|
}
|
|
35698
35676
|
}
|
|
35699
35677
|
},
|
|
35700
35678
|
onCancel: e => {
|
|
35701
|
-
requestClose(e, {
|
|
35679
|
+
openController.requestClose(e, {
|
|
35702
35680
|
isCancel: true
|
|
35703
35681
|
});
|
|
35704
35682
|
},
|
|
35705
|
-
onnavi_request_open: e => {
|
|
35706
|
-
const dialogEl = ref.current;
|
|
35707
|
-
if (!dialogEl) {
|
|
35708
|
-
return;
|
|
35709
|
-
}
|
|
35710
|
-
if (openedRef.current) {
|
|
35711
|
-
return;
|
|
35712
|
-
}
|
|
35713
|
-
open(e);
|
|
35714
|
-
},
|
|
35715
|
-
onnavi_request_close: e => {
|
|
35716
|
-
const dialogEl = ref.current;
|
|
35717
|
-
if (!dialogEl) {
|
|
35718
|
-
return;
|
|
35719
|
-
}
|
|
35720
|
-
if (!openedRef.current) {
|
|
35721
|
-
return;
|
|
35722
|
-
}
|
|
35723
|
-
if (closeRequestHandler) {
|
|
35724
|
-
let denied = false;
|
|
35725
|
-
const closePermission = {
|
|
35726
|
-
deny: () => {
|
|
35727
|
-
denied = true;
|
|
35728
|
-
},
|
|
35729
|
-
allow: () => {
|
|
35730
|
-
denied = false;
|
|
35731
|
-
}
|
|
35732
|
-
};
|
|
35733
|
-
closeRequestHandler(e, closePermission);
|
|
35734
|
-
if (denied) {
|
|
35735
|
-
if (e.type === "cancel") {
|
|
35736
|
-
e.preventDefault();
|
|
35737
|
-
}
|
|
35738
|
-
closePermission.allow = () => {
|
|
35739
|
-
close(e);
|
|
35740
|
-
};
|
|
35741
|
-
return;
|
|
35742
|
-
}
|
|
35743
|
-
}
|
|
35744
|
-
close(e);
|
|
35745
|
-
},
|
|
35746
35683
|
children: children
|
|
35747
35684
|
});
|
|
35748
35685
|
};
|
|
@@ -35773,7 +35710,7 @@ installImportMetaCssBuild(import.meta);const css$q = /* css */`
|
|
|
35773
35710
|
const Popover = props => {
|
|
35774
35711
|
import.meta.css = [css$q, "@jsenv/navi/src/popup/popover.jsx"];
|
|
35775
35712
|
const {
|
|
35776
|
-
|
|
35713
|
+
openController,
|
|
35777
35714
|
anchorRef,
|
|
35778
35715
|
scrollTrap,
|
|
35779
35716
|
pointerTrap,
|
|
@@ -35785,7 +35722,6 @@ const Popover = props => {
|
|
|
35785
35722
|
positionYFixed,
|
|
35786
35723
|
spacing = 0,
|
|
35787
35724
|
viewportSpacing = 0,
|
|
35788
|
-
closeRequestHandler,
|
|
35789
35725
|
...rest
|
|
35790
35726
|
} = props;
|
|
35791
35727
|
const defaultRef = useRef();
|
|
@@ -35796,14 +35732,18 @@ const Popover = props => {
|
|
|
35796
35732
|
const debugFocus = useDebugFocus();
|
|
35797
35733
|
const autoFocusProps = useAutoFocus(ref, props.autoFocus);
|
|
35798
35734
|
|
|
35799
|
-
//
|
|
35800
|
-
//
|
|
35801
|
-
//
|
|
35802
|
-
|
|
35803
|
-
|
|
35804
|
-
|
|
35805
|
-
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 => {
|
|
35806
35741
|
const popoverEl = ref.current;
|
|
35742
|
+
if (!popoverEl) {
|
|
35743
|
+
return undefined;
|
|
35744
|
+
}
|
|
35745
|
+
const [cleanup, addCleanup] = createPubSub(true);
|
|
35746
|
+
debugPopup(e, `openPopover()`);
|
|
35807
35747
|
const focusedBeforeOpen = getFocusedBeforeTransfer(e);
|
|
35808
35748
|
popoverEl.showPopover();
|
|
35809
35749
|
transferFocus(popoverEl, debugFocus, e, focusedBeforeOpen);
|
|
@@ -35879,22 +35819,15 @@ const Popover = props => {
|
|
|
35879
35819
|
addCleanup(() => {
|
|
35880
35820
|
rectEffect.disconnect();
|
|
35881
35821
|
});
|
|
35882
|
-
|
|
35883
|
-
|
|
35884
|
-
|
|
35885
|
-
|
|
35886
|
-
|
|
35887
|
-
|
|
35888
|
-
|
|
35889
|
-
|
|
35890
|
-
|
|
35891
|
-
markAutofocusRestoreOnClose(popoverEl);
|
|
35892
|
-
popoverEl.hidePopover();
|
|
35893
|
-
cleanup();
|
|
35894
|
-
openedRef.current = false;
|
|
35895
|
-
dispatchCustomEvent(popoverEl, "navi_close", {
|
|
35896
|
-
event: e
|
|
35897
|
-
});
|
|
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
|
+
};
|
|
35898
35831
|
};
|
|
35899
35832
|
return jsxs(Box, {
|
|
35900
35833
|
id: id,
|
|
@@ -35904,44 +35837,6 @@ const Popover = props => {
|
|
|
35904
35837
|
ref: ref,
|
|
35905
35838
|
baseClassName: "navi_popover",
|
|
35906
35839
|
pseudoClasses: POPOVER_PSEUDO_CLASSES,
|
|
35907
|
-
onnavi_request_open: e => {
|
|
35908
|
-
const popoverEl = ref.current;
|
|
35909
|
-
if (!popoverEl) {
|
|
35910
|
-
return;
|
|
35911
|
-
}
|
|
35912
|
-
if (openedRef.current) {
|
|
35913
|
-
return;
|
|
35914
|
-
}
|
|
35915
|
-
open(e);
|
|
35916
|
-
},
|
|
35917
|
-
onnavi_request_close: e => {
|
|
35918
|
-
const popoverEl = ref.current;
|
|
35919
|
-
if (!popoverEl) {
|
|
35920
|
-
return;
|
|
35921
|
-
}
|
|
35922
|
-
if (!openedRef.current) {
|
|
35923
|
-
return;
|
|
35924
|
-
}
|
|
35925
|
-
if (closeRequestHandler) {
|
|
35926
|
-
let denied = false;
|
|
35927
|
-
const closePermission = {
|
|
35928
|
-
deny: () => {
|
|
35929
|
-
denied = true;
|
|
35930
|
-
},
|
|
35931
|
-
allow: () => {
|
|
35932
|
-
denied = false;
|
|
35933
|
-
}
|
|
35934
|
-
};
|
|
35935
|
-
closeRequestHandler(e, closePermission);
|
|
35936
|
-
if (denied) {
|
|
35937
|
-
closePermission.allow = () => {
|
|
35938
|
-
close(e);
|
|
35939
|
-
};
|
|
35940
|
-
return;
|
|
35941
|
-
}
|
|
35942
|
-
}
|
|
35943
|
-
close(e);
|
|
35944
|
-
},
|
|
35945
35840
|
children: [jsx("div", {
|
|
35946
35841
|
className: "navi_popover_backdrop",
|
|
35947
35842
|
"aria-hidden": "true",
|
|
@@ -35953,7 +35848,7 @@ const Popover = props => {
|
|
|
35953
35848
|
e.preventDefault();
|
|
35954
35849
|
return;
|
|
35955
35850
|
}
|
|
35956
|
-
requestClose(e, {
|
|
35851
|
+
openController.requestClose(e, {
|
|
35957
35852
|
isCancel: true
|
|
35958
35853
|
});
|
|
35959
35854
|
}
|
|
@@ -36259,7 +36154,6 @@ const PickerCustom = props => {
|
|
|
36259
36154
|
// In "dialog" mode, enterExpanded() pushes a history entry so the back button closes.
|
|
36260
36155
|
// In "popover" mode, it replaces the current history state (no history entry added).
|
|
36261
36156
|
const pickerNavType = mode === "dialog" ? "push" : "replace";
|
|
36262
|
-
const expandedRef = useRef(false);
|
|
36263
36157
|
const [expanded, enterExpanded, leaveExpanded] = useNavState(popupId, {
|
|
36264
36158
|
type: pickerNavType,
|
|
36265
36159
|
// onLeave fires only when the state key disappears externally (back button/gesture most of the time).
|
|
@@ -36271,95 +36165,108 @@ const PickerCustom = props => {
|
|
|
36271
36165
|
});
|
|
36272
36166
|
}
|
|
36273
36167
|
});
|
|
36274
|
-
// expandedRef tracks actual open/close state, updated only by onOpen/onClose.
|
|
36275
|
-
// NOT synced to expanded on every render so the useLayoutEffect guard below works.
|
|
36276
|
-
const valueAtOpenRef = useRef(null);
|
|
36277
|
-
const activeElementAtOpenRef = useRef(null);
|
|
36278
|
-
const onOpen = e => {
|
|
36279
|
-
expandedRef.current = true;
|
|
36280
|
-
enterExpanded();
|
|
36281
|
-
const focusedBeforeOpen = e.detail.focusedBeforeOpen;
|
|
36282
|
-
activeElementAtOpenRef.current = focusedBeforeOpen;
|
|
36283
|
-
debugFocus(e, "picked opened, store element focused", focusedBeforeOpen);
|
|
36284
|
-
const valueAtOpen = getPickerInputUIState(ref.current);
|
|
36285
|
-
valueAtOpenRef.current = valueAtOpen;
|
|
36286
|
-
debugPopup(e, `picker opened, store value at open`, valueAtOpen);
|
|
36287
|
-
};
|
|
36288
|
-
const restoreFocus = e => {
|
|
36289
|
-
const activeElementAtOpen = activeElementAtOpenRef.current;
|
|
36290
|
-
activeElementAtOpenRef.current = null;
|
|
36291
|
-
const focusoutEvent = findEvent(e, "focusout");
|
|
36292
|
-
if (focusoutEvent) {
|
|
36293
|
-
debugFocus(e, `closed by focusout -> let focus go away`);
|
|
36294
|
-
return;
|
|
36295
|
-
}
|
|
36296
|
-
const mousedownEvent = findEvent(e, "mousedown");
|
|
36297
|
-
if (mousedownEvent) {
|
|
36298
|
-
debugFocus(e, "closed by mousedown -> prevent browser focus (mousedown.preventDefault())");
|
|
36299
|
-
mousedownEvent.preventDefault();
|
|
36300
|
-
}
|
|
36301
|
-
debugFocus(e, `restore focus to previously focused element`, activeElementAtOpen);
|
|
36302
|
-
activeElementAtOpen.focus({
|
|
36303
|
-
preventScroll: true
|
|
36304
|
-
});
|
|
36305
|
-
return;
|
|
36306
|
-
};
|
|
36307
|
-
const onClose = e => {
|
|
36308
|
-
const mousedownEvent = findEvent(e, "mousedown");
|
|
36309
|
-
if (mousedownEvent) {
|
|
36310
|
-
debugPopup(e, `closed by mousedown -> disable next click`);
|
|
36311
|
-
disableClickFor();
|
|
36312
|
-
} else {
|
|
36313
|
-
const spaceEvent = findEvent(e, e => e.type === "keydown" && e.key === " ");
|
|
36314
|
-
if (spaceEvent) {
|
|
36315
|
-
// space would trigger a click on the picker button causing it to re-open immediatly after closing
|
|
36316
|
-
debugPopup(e, `closed by space key -> prevent browser click`);
|
|
36317
|
-
// browser won't try to dispatch click
|
|
36318
|
-
// and our "space_to_open" will see e.defaultPrevented too and won't try to open picker
|
|
36319
|
-
spaceEvent.preventDefault();
|
|
36320
|
-
}
|
|
36321
|
-
}
|
|
36322
|
-
expandedRef.current = false;
|
|
36323
|
-
leaveExpanded({
|
|
36324
|
-
isBack: e.detail.isCancel
|
|
36325
|
-
});
|
|
36326
|
-
// Reset so the next opening re-evaluates screen size
|
|
36327
|
-
defaultModeRef.current = null;
|
|
36328
|
-
restoreFocus(e);
|
|
36329
|
-
};
|
|
36330
36168
|
const disableClickFor = useIgnoreClickForMousedown(ref, e => {
|
|
36331
36169
|
debugPopup(e, `click ignored`);
|
|
36332
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
|
+
});
|
|
36333
36252
|
const requestOpen = (e, detail) => {
|
|
36334
36253
|
// scroll <button> of the picker into view when opening it
|
|
36335
36254
|
const pickerEl = ref.current;
|
|
36336
36255
|
pickerEl.scrollIntoView({
|
|
36337
36256
|
block: "nearest"
|
|
36338
36257
|
});
|
|
36339
|
-
|
|
36340
|
-
return dispatchCustomEvent(popupEl, "navi_request_open", {
|
|
36341
|
-
event: e,
|
|
36342
|
-
...detail
|
|
36343
|
-
});
|
|
36344
|
-
};
|
|
36345
|
-
const requestClose = (e = new CustomEvent("programmatic", {
|
|
36346
|
-
detail: {}
|
|
36347
|
-
}), detail) => {
|
|
36348
|
-
const popupEl = popupRef.current;
|
|
36349
|
-
return dispatchCustomEvent(popupEl, "navi_request_close", {
|
|
36350
|
-
event: e,
|
|
36351
|
-
...detail
|
|
36352
|
-
});
|
|
36258
|
+
openController.open(e, detail);
|
|
36353
36259
|
};
|
|
36260
|
+
const requestClose = openController.requestClose;
|
|
36354
36261
|
const open = Boolean(expanded);
|
|
36355
36262
|
useLayoutEffect(() => {
|
|
36356
36263
|
if (open === undefined) {
|
|
36357
36264
|
return;
|
|
36358
36265
|
}
|
|
36359
36266
|
// Skip when the popup is already in the desired state.
|
|
36360
|
-
//
|
|
36361
|
-
// so it is the authoritative check against feedback loops.
|
|
36362
|
-
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) {
|
|
36363
36270
|
return;
|
|
36364
36271
|
}
|
|
36365
36272
|
// open_prop_change means the parent is driving the open state directly
|
|
@@ -36394,7 +36301,7 @@ const PickerCustom = props => {
|
|
|
36394
36301
|
// requestClose(e);
|
|
36395
36302
|
},
|
|
36396
36303
|
"onnavi_request_open": e => {
|
|
36397
|
-
if (
|
|
36304
|
+
if (openController.opened) {
|
|
36398
36305
|
return;
|
|
36399
36306
|
}
|
|
36400
36307
|
requestInteraction({
|
|
@@ -36409,7 +36316,9 @@ const PickerCustom = props => {
|
|
|
36409
36316
|
requestInteraction({
|
|
36410
36317
|
event: e,
|
|
36411
36318
|
allowed: () => {
|
|
36412
|
-
requestClose(e
|
|
36319
|
+
requestClose(e, {
|
|
36320
|
+
isCancel: e.detail.isCancel
|
|
36321
|
+
});
|
|
36413
36322
|
}
|
|
36414
36323
|
});
|
|
36415
36324
|
},
|
|
@@ -36420,41 +36329,7 @@ const PickerCustom = props => {
|
|
|
36420
36329
|
});
|
|
36421
36330
|
Object.assign(popupProps, {
|
|
36422
36331
|
anchorRef: props.ref,
|
|
36423
|
-
|
|
36424
|
-
closeRequestHandler: (requestCloseEvent, closePermission) => {
|
|
36425
|
-
const isCancel = requestCloseEvent.detail.isCancel;
|
|
36426
|
-
if (isCancel) {
|
|
36427
|
-
const pickerEl = ref.current;
|
|
36428
|
-
const inputEl = getPickerInput(pickerEl);
|
|
36429
|
-
const valueAtOpen = valueAtOpenRef.current;
|
|
36430
|
-
debugPopup(requestCloseEvent, `picker cancel, restoring value at open ${JSON.stringify(valueAtOpen)}`);
|
|
36431
|
-
dispatchRequestSetUIState(inputEl, valueAtOpen, {
|
|
36432
|
-
event: requestCloseEvent
|
|
36433
|
-
});
|
|
36434
|
-
return;
|
|
36435
|
-
}
|
|
36436
|
-
const pickerEl = ref.current;
|
|
36437
|
-
const inputEl = getPickerInput(pickerEl);
|
|
36438
|
-
dispatchRequestAction(inputEl, {
|
|
36439
|
-
event: requestCloseEvent,
|
|
36440
|
-
name: "picker close",
|
|
36441
|
-
prevented: () => {
|
|
36442
|
-
closePermission.deny();
|
|
36443
|
-
},
|
|
36444
|
-
// Always report validation when the picker tries to close so the
|
|
36445
|
-
// user sees what is wrong, even if the picker has no action prop.
|
|
36446
|
-
reportOnInvalid: true,
|
|
36447
|
-
onInvalid: () => {
|
|
36448
|
-
closePermission.deny();
|
|
36449
|
-
}
|
|
36450
|
-
});
|
|
36451
|
-
},
|
|
36452
|
-
onnavi_open: e => {
|
|
36453
|
-
onOpen(e);
|
|
36454
|
-
},
|
|
36455
|
-
onnavi_close: e => {
|
|
36456
|
-
onClose(e);
|
|
36457
|
-
}
|
|
36332
|
+
openController
|
|
36458
36333
|
});
|
|
36459
36334
|
{
|
|
36460
36335
|
const onKeyDownShortcuts = createOnKeyDownForShortcuts({
|
|
@@ -36511,7 +36386,7 @@ const PickerCustom = props => {
|
|
|
36511
36386
|
};
|
|
36512
36387
|
},
|
|
36513
36388
|
"escape": e => {
|
|
36514
|
-
if (!
|
|
36389
|
+
if (!openController.opened) {
|
|
36515
36390
|
return null;
|
|
36516
36391
|
}
|
|
36517
36392
|
return {
|
|
@@ -36535,7 +36410,7 @@ const PickerCustom = props => {
|
|
|
36535
36410
|
if (isWithinPopup(e.target)) {
|
|
36536
36411
|
return null;
|
|
36537
36412
|
}
|
|
36538
|
-
if (
|
|
36413
|
+
if (openController.opened) {
|
|
36539
36414
|
return {
|
|
36540
36415
|
name: "mousedown to close picker",
|
|
36541
36416
|
allowed: () => requestClose(e, {
|
|
@@ -36631,8 +36506,7 @@ const PickerContentInsidePopover = props => {
|
|
|
36631
36506
|
name: "blur",
|
|
36632
36507
|
category: "interaction",
|
|
36633
36508
|
allowed: () => {
|
|
36634
|
-
|
|
36635
|
-
event: e,
|
|
36509
|
+
popupProps.openController.requestClose(e, {
|
|
36636
36510
|
isCancel: true
|
|
36637
36511
|
});
|
|
36638
36512
|
}
|
|
@@ -36657,9 +36531,7 @@ const PickerContentInsidePopover = props => {
|
|
|
36657
36531
|
if (e.button !== 0) {
|
|
36658
36532
|
return;
|
|
36659
36533
|
}
|
|
36660
|
-
|
|
36661
|
-
dispatchCustomEvent(popupEl, "navi_request_close", {
|
|
36662
|
-
event: e,
|
|
36534
|
+
popupProps.openController.requestClose(e, {
|
|
36663
36535
|
isCancel: true
|
|
36664
36536
|
});
|
|
36665
36537
|
},
|
|
@@ -36744,6 +36616,142 @@ const useIgnoreClickForMousedown = (elementRef, onIgnore) => {
|
|
|
36744
36616
|
return disableClickFor;
|
|
36745
36617
|
};
|
|
36746
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
|
+
|
|
36747
36755
|
const PickerNaviMinute = props => {
|
|
36748
36756
|
const Next = useNextResolver();
|
|
36749
36757
|
const {
|