@jsenv/navi 0.27.54 → 0.27.56
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 +282 -176
- package/dist/jsenv_navi.js.map +23 -19
- package/dist/jsenv_navi_side_effects.js +40 -2
- package/dist/jsenv_navi_side_effects.js.map +10 -7
- package/package.json +2 -2
package/dist/jsenv_navi.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { installImportMetaCssBuild } from "./jsenv_navi_side_effects.js";
|
|
1
|
+
import { installImportMetaCssBuild, windowWidthSignal } from "./jsenv_navi_side_effects.js";
|
|
2
2
|
import { isValidElement, createContext, h, toChildArray, render, Fragment, cloneElement } from "preact";
|
|
3
3
|
import { useErrorBoundary, useLayoutEffect, useEffect, useContext, useMemo, useRef, useState, useCallback, useId } from "preact/hooks";
|
|
4
4
|
import { jsxs, jsx, Fragment as Fragment$1 } from "preact/jsx-runtime";
|
|
@@ -14530,6 +14530,14 @@ computed(() => {
|
|
|
14530
14530
|
return reasonArray;
|
|
14531
14531
|
});
|
|
14532
14532
|
|
|
14533
|
+
const documentStateSignal = signal(null);
|
|
14534
|
+
const useDocumentState = () => {
|
|
14535
|
+
return documentStateSignal.value;
|
|
14536
|
+
};
|
|
14537
|
+
const updateDocumentState = (value) => {
|
|
14538
|
+
documentStateSignal.value = value;
|
|
14539
|
+
};
|
|
14540
|
+
|
|
14533
14541
|
const documentUrlSignal = signal(
|
|
14534
14542
|
typeof window === "undefined" ? "http://localhost" : window.location.href,
|
|
14535
14543
|
);
|
|
@@ -14574,14 +14582,6 @@ const urlToScheme = (url) => {
|
|
|
14574
14582
|
return scheme;
|
|
14575
14583
|
};
|
|
14576
14584
|
|
|
14577
|
-
const documentStateSignal = signal(null);
|
|
14578
|
-
const useDocumentState = () => {
|
|
14579
|
-
return documentStateSignal.value;
|
|
14580
|
-
};
|
|
14581
|
-
const updateDocumentState = (value) => {
|
|
14582
|
-
documentStateSignal.value = value;
|
|
14583
|
-
};
|
|
14584
|
-
|
|
14585
14585
|
const getHrefTargetInfo = (href) => {
|
|
14586
14586
|
href = String(href);
|
|
14587
14587
|
|
|
@@ -14664,18 +14664,6 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14664
14664
|
return window.history.state ? { ...window.history.state } : null;
|
|
14665
14665
|
};
|
|
14666
14666
|
|
|
14667
|
-
const replaceDocumentState = (
|
|
14668
|
-
newState,
|
|
14669
|
-
{ reason = "replaceDocumentState called" } = {},
|
|
14670
|
-
) => {
|
|
14671
|
-
const url = window.location.href;
|
|
14672
|
-
handleRoutingTask(url, {
|
|
14673
|
-
reason,
|
|
14674
|
-
navigationType: "replace",
|
|
14675
|
-
state: newState,
|
|
14676
|
-
});
|
|
14677
|
-
};
|
|
14678
|
-
|
|
14679
14667
|
const historyStartAtStart = getDocumentState();
|
|
14680
14668
|
const visitedUrlSet = historyStartAtStart
|
|
14681
14669
|
? new Set(historyStartAtStart.jsenv_visited_urls || [])
|
|
@@ -14695,19 +14683,7 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14695
14683
|
return;
|
|
14696
14684
|
}
|
|
14697
14685
|
visitedUrlSet.add(url);
|
|
14698
|
-
visitedUrlsSignal.value++;
|
|
14699
|
-
|
|
14700
|
-
const historyState = getDocumentState() || {};
|
|
14701
|
-
const historyStateWithVisitedUrls = {
|
|
14702
|
-
...historyState,
|
|
14703
|
-
jsenv_visited_urls: Array.from(visitedUrlSet),
|
|
14704
|
-
};
|
|
14705
|
-
window.history.replaceState(
|
|
14706
|
-
historyStateWithVisitedUrls,
|
|
14707
|
-
null,
|
|
14708
|
-
window.location.href,
|
|
14709
|
-
);
|
|
14710
|
-
updateDocumentState(historyStateWithVisitedUrls);
|
|
14686
|
+
visitedUrlsSignal.value++;
|
|
14711
14687
|
};
|
|
14712
14688
|
|
|
14713
14689
|
let abortController = null;
|
|
@@ -14719,15 +14695,34 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14719
14695
|
state,
|
|
14720
14696
|
},
|
|
14721
14697
|
) => {
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14698
|
+
const isSameUrl = url === window.location.href;
|
|
14699
|
+
|
|
14700
|
+
if (navigationType === "push" || navigationType === "replace") {
|
|
14701
|
+
// Pre-merge visited URLs so push/replaceState is called only once.
|
|
14702
|
+
markUrlAsVisited(url);
|
|
14703
|
+
const effectiveState = {
|
|
14704
|
+
...(state || {}),
|
|
14705
|
+
jsenv_visited_urls: Array.from(visitedUrlSet),
|
|
14706
|
+
};
|
|
14707
|
+
if (navigationType === "push") {
|
|
14708
|
+
window.history.pushState(effectiveState, null, url);
|
|
14709
|
+
} else {
|
|
14710
|
+
window.history.replaceState(effectiveState, null, url);
|
|
14711
|
+
}
|
|
14712
|
+
updateDocumentUrl(url);
|
|
14713
|
+
updateDocumentState(effectiveState);
|
|
14714
|
+
} else {
|
|
14715
|
+
// traverse / reload: state comes from the history entry, no push/replace needed.
|
|
14716
|
+
markUrlAsVisited(url);
|
|
14717
|
+
updateDocumentUrl(url);
|
|
14718
|
+
updateDocumentState(state);
|
|
14719
|
+
}
|
|
14720
|
+
|
|
14721
|
+
// Routes only match on URL — skip route matching for state-only changes.
|
|
14722
|
+
if (isSameUrl && navigationType !== "reload") {
|
|
14723
|
+
return undefined;
|
|
14726
14724
|
}
|
|
14727
14725
|
|
|
14728
|
-
updateDocumentUrl(url);
|
|
14729
|
-
updateDocumentState(state);
|
|
14730
|
-
markUrlAsVisited(url);
|
|
14731
14726
|
if (abortController) {
|
|
14732
14727
|
abortController.abort(`navigating to ${url}`);
|
|
14733
14728
|
}
|
|
@@ -14861,7 +14856,6 @@ const setupBrowserIntegrationViaHistory = ({
|
|
|
14861
14856
|
navBack,
|
|
14862
14857
|
navForward,
|
|
14863
14858
|
getDocumentState,
|
|
14864
|
-
replaceDocumentState,
|
|
14865
14859
|
isVisited,
|
|
14866
14860
|
visitedUrlsSignal,
|
|
14867
14861
|
};
|
|
@@ -14947,12 +14941,19 @@ setOnAllRouteReady((v) => {
|
|
|
14947
14941
|
});
|
|
14948
14942
|
setRouteIntegration(browserIntegration);
|
|
14949
14943
|
|
|
14950
|
-
const
|
|
14944
|
+
const navIntegratedVia = browserIntegration.integration;
|
|
14951
14945
|
const navTo = (target, options) => {
|
|
14952
14946
|
const url = new URL(target, window.location.href).href;
|
|
14953
14947
|
const currentUrl = documentUrlSignal.peek();
|
|
14954
14948
|
if (url === currentUrl) {
|
|
14955
|
-
|
|
14949
|
+
if (options?.state === undefined) {
|
|
14950
|
+
return null;
|
|
14951
|
+
}
|
|
14952
|
+
// State-only update on same URL: skip if state is identical to current.
|
|
14953
|
+
const currentState = browserIntegration.getDocumentState();
|
|
14954
|
+
if (compareTwoJsValues(options.state, currentState)) {
|
|
14955
|
+
return null;
|
|
14956
|
+
}
|
|
14956
14957
|
}
|
|
14957
14958
|
return browserIntegration.navTo(url, options);
|
|
14958
14959
|
};
|
|
@@ -14973,78 +14974,113 @@ const isVisited = browserIntegration.isVisited;
|
|
|
14973
14974
|
const visitedUrlsSignal = browserIntegration.visitedUrlsSignal;
|
|
14974
14975
|
browserIntegration.handleActionTask;
|
|
14975
14976
|
|
|
14976
|
-
const NOT_SET = {};
|
|
14977
14977
|
const NO_OP = () => {};
|
|
14978
14978
|
const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
|
|
14979
|
-
const useNavStateBasic = (
|
|
14980
|
-
|
|
14979
|
+
const useNavStateBasic = (
|
|
14980
|
+
id,
|
|
14981
|
+
initialValue,
|
|
14982
|
+
{ debug, type = "replace", onLeave } = {},
|
|
14983
|
+
) => {
|
|
14984
|
+
// Hooks must be called unconditionally — before the !id early return.
|
|
14985
|
+
const state = documentStateSignal.value;
|
|
14986
|
+
const valueInState = id
|
|
14987
|
+
? state !== null
|
|
14988
|
+
? state[id]
|
|
14989
|
+
: undefined
|
|
14990
|
+
: undefined;
|
|
14991
|
+
const onLeaveRef = useRef(onLeave);
|
|
14992
|
+
onLeaveRef.current = onLeave;
|
|
14993
|
+
const prevValueInStateRef = useRef(valueInState);
|
|
14994
|
+
// enteredRef tracks whether enter() was called without a matching leave() yet.
|
|
14995
|
+
// It lets the effect distinguish an external disappearance (back button → fire onLeave)
|
|
14996
|
+
// from a programmatic one (leave() already set it to false before the state updates).
|
|
14997
|
+
const enteredRef = useRef(false);
|
|
14998
|
+
useEffect(() => {
|
|
14999
|
+
const prevValue = prevValueInStateRef.current;
|
|
15000
|
+
prevValueInStateRef.current = valueInState;
|
|
15001
|
+
if (
|
|
15002
|
+
prevValue !== undefined &&
|
|
15003
|
+
valueInState === undefined &&
|
|
15004
|
+
enteredRef.current
|
|
15005
|
+
) {
|
|
15006
|
+
enteredRef.current = false;
|
|
15007
|
+
onLeaveRef.current?.();
|
|
15008
|
+
}
|
|
15009
|
+
}, [valueInState]);
|
|
15010
|
+
|
|
14981
15011
|
if (!id) {
|
|
14982
15012
|
return NO_ID_GIVEN;
|
|
14983
15013
|
}
|
|
14984
15014
|
|
|
14985
|
-
|
|
14986
|
-
const documentState = browserIntegration.getDocumentState();
|
|
14987
|
-
const valueInDocumentState = documentState ? documentState[id] : undefined;
|
|
14988
|
-
if (valueInDocumentState === undefined) {
|
|
14989
|
-
navStateRef.current = initialValue;
|
|
14990
|
-
if (initialValue !== undefined) {
|
|
14991
|
-
console.debug(
|
|
14992
|
-
`useNavState(${id}) initial value is ${initialValue} (from initialValue passed in as argument)`,
|
|
14993
|
-
);
|
|
14994
|
-
}
|
|
14995
|
-
} else {
|
|
14996
|
-
navStateRef.current = valueInDocumentState;
|
|
14997
|
-
if (debug) {
|
|
14998
|
-
console.debug(
|
|
14999
|
-
`useNavState(${id}) initial value is ${initialValue} (from nav state)`,
|
|
15000
|
-
);
|
|
15001
|
-
}
|
|
15002
|
-
}
|
|
15003
|
-
}
|
|
15015
|
+
const currentValue = valueInState !== undefined ? valueInState : initialValue;
|
|
15004
15016
|
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
value = value(currentValue);
|
|
15009
|
-
}
|
|
15010
|
-
if (debug) {
|
|
15011
|
-
console.debug(
|
|
15012
|
-
`useNavState(${id}) set ${value} (previous was ${currentValue})`,
|
|
15013
|
-
);
|
|
15014
|
-
}
|
|
15017
|
+
if (debug) {
|
|
15018
|
+
console.debug(`useNavState(${id}) current value is ${currentValue}`);
|
|
15019
|
+
}
|
|
15015
15020
|
|
|
15021
|
+
// enter(value): navigate TO this state (push or replace depending on type).
|
|
15022
|
+
// Calling enter() without a value stores "on" — the mere presence of the key
|
|
15023
|
+
// in the document state is enough to match; the value just allows associating
|
|
15024
|
+
// extra data with the entry when needed.
|
|
15025
|
+
const enter = (value = "on") => {
|
|
15026
|
+
enteredRef.current = true;
|
|
15016
15027
|
const currentState = browserIntegration.getDocumentState() || {};
|
|
15017
|
-
|
|
15018
|
-
if (value === undefined) {
|
|
15019
|
-
if (!Object.hasOwn(currentState, id)) {
|
|
15020
|
-
return;
|
|
15021
|
-
}
|
|
15022
|
-
delete currentState[id];
|
|
15023
|
-
browserIntegration.replaceDocumentState(currentState, {
|
|
15024
|
-
reason: `delete "${id}" from browser state`,
|
|
15025
|
-
});
|
|
15028
|
+
if (currentState[id] === value) {
|
|
15026
15029
|
return;
|
|
15027
15030
|
}
|
|
15031
|
+
const newState = { ...currentState, [id]: value };
|
|
15032
|
+
navTo(window.location.href, {
|
|
15033
|
+
replace: type !== "push",
|
|
15034
|
+
state: newState,
|
|
15035
|
+
});
|
|
15036
|
+
};
|
|
15028
15037
|
|
|
15029
|
-
|
|
15030
|
-
|
|
15038
|
+
// leave(): navigate AWAY FROM this state (navBack in push mode, replace in replace mode).
|
|
15039
|
+
const leave = () => {
|
|
15040
|
+
enteredRef.current = false;
|
|
15041
|
+
const currentState = browserIntegration.getDocumentState() || {};
|
|
15042
|
+
if (!Object.hasOwn(currentState, id)) {
|
|
15031
15043
|
return;
|
|
15032
15044
|
}
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15045
|
+
if (type === "push") {
|
|
15046
|
+
browserIntegration.navBack();
|
|
15047
|
+
} else {
|
|
15048
|
+
const newState = { ...currentState };
|
|
15049
|
+
delete newState[id];
|
|
15050
|
+
navTo(window.location.href, { replace: true, state: newState });
|
|
15051
|
+
}
|
|
15037
15052
|
};
|
|
15038
15053
|
|
|
15039
|
-
return [
|
|
15040
|
-
navStateRef.current,
|
|
15041
|
-
set,
|
|
15042
|
-
() => {
|
|
15043
|
-
set(undefined);
|
|
15044
|
-
},
|
|
15045
|
-
];
|
|
15054
|
+
return [currentValue, enter, leave];
|
|
15046
15055
|
};
|
|
15047
15056
|
|
|
15057
|
+
/**
|
|
15058
|
+
* Stores a named value in the browser's document state and returns it reactively.
|
|
15059
|
+
* The component re-renders whenever the value changes (navigation, back/forward button).
|
|
15060
|
+
*
|
|
15061
|
+
* @param {string} id
|
|
15062
|
+
* Unique key used to store the value in document state. Must be stable across renders.
|
|
15063
|
+
*
|
|
15064
|
+
* @param {*} initialValue
|
|
15065
|
+
* Value returned when `id` is absent from document state (e.g. before enter() is called).
|
|
15066
|
+
*
|
|
15067
|
+
* @param {object} [options]
|
|
15068
|
+
* @param {"push"|"replace"} [options.type="replace"]
|
|
15069
|
+
* Controls how enter() adds the state to browser history.
|
|
15070
|
+
* - "push": creates a new history entry — pressing the back button removes it and calls onLeave.
|
|
15071
|
+
* - "replace": updates the current history entry — no extra history entry is created.
|
|
15072
|
+
* @param {() => void} [options.onLeave]
|
|
15073
|
+
* Called when the state key disappears **externally** — e.g. the user presses the browser
|
|
15074
|
+
* back button. Not called when leave() is invoked programmatically.
|
|
15075
|
+
*
|
|
15076
|
+
* @returns {[value, enter, leave]}
|
|
15077
|
+
* - `value`: current value from document state, or `initialValue` when the key is absent.
|
|
15078
|
+
* - `enter(value = "on")`: navigate TO this state (stores `value` under `id`).
|
|
15079
|
+
* Calling without an argument stores `"on"` — the presence of the key is enough to match;
|
|
15080
|
+
* the value allows associating extra data when needed.
|
|
15081
|
+
* - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
|
|
15082
|
+
* or goes back in history when `type` is "push").
|
|
15083
|
+
*/
|
|
15048
15084
|
const useNavState = useNavStateBasic;
|
|
15049
15085
|
|
|
15050
15086
|
const NEVER_SET = {};
|
|
@@ -35356,12 +35392,6 @@ const renderSafe = (value) => {
|
|
|
35356
35392
|
|
|
35357
35393
|
const PickerContext = createContext();
|
|
35358
35394
|
|
|
35359
|
-
const windowWidthSignal = signal(window.innerWidth);
|
|
35360
|
-
|
|
35361
|
-
window.addEventListener("resize", () => {
|
|
35362
|
-
windowWidthSignal.value = window.innerWidth;
|
|
35363
|
-
});
|
|
35364
|
-
|
|
35365
35395
|
/**
|
|
35366
35396
|
* Mirrors what browsers do when navigating to a page:
|
|
35367
35397
|
* 1. Focus the first element with [navi-autofocus] (but not [navi-autofocus="fallback"]) inside the container
|
|
@@ -35388,14 +35418,17 @@ const markAutofocusRestoreOnClose = (containerEl) => {
|
|
|
35388
35418
|
// meaning the focus did not yet reach the element receiving the mousedown
|
|
35389
35419
|
// as a result document.activeElement is not up-to-date (can be document.body for instance)
|
|
35390
35420
|
const getFocusedBeforeTransfer = (e) => {
|
|
35391
|
-
const initiator =
|
|
35392
|
-
|
|
35393
|
-
|
|
35394
|
-
|
|
35395
|
-
|
|
35396
|
-
|
|
35397
|
-
|
|
35398
|
-
|
|
35421
|
+
const initiator =
|
|
35422
|
+
e.detail && e.detail.eventChain ? e.detail.eventChain[0] : null;
|
|
35423
|
+
if (initiator) {
|
|
35424
|
+
if (initiator.type === "mousedown") {
|
|
35425
|
+
// if we we had let browser give focus, the element would be the one that would be focused
|
|
35426
|
+
return initiator.currentTarget;
|
|
35427
|
+
}
|
|
35428
|
+
if (initiator.type === "click") {
|
|
35429
|
+
// label use case
|
|
35430
|
+
return initiator.currentTarget;
|
|
35431
|
+
}
|
|
35399
35432
|
}
|
|
35400
35433
|
return document.activeElement;
|
|
35401
35434
|
};
|
|
@@ -35492,6 +35525,7 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
|
35492
35525
|
margin-top: var(--dialog-top-inset, auto);
|
|
35493
35526
|
margin-bottom: auto;
|
|
35494
35527
|
flex-direction: column;
|
|
35528
|
+
transition: margin-top 0.1s ease-in-out;
|
|
35495
35529
|
}
|
|
35496
35530
|
|
|
35497
35531
|
&::backdrop {
|
|
@@ -35502,6 +35536,8 @@ installImportMetaCssBuild(import.meta);const css$r = /* css */`
|
|
|
35502
35536
|
const Dialog = props => {
|
|
35503
35537
|
import.meta.css = [css$r, "@jsenv/navi/src/popup/dialog.jsx"];
|
|
35504
35538
|
const {
|
|
35539
|
+
open: openProp = false,
|
|
35540
|
+
anchorRef,
|
|
35505
35541
|
children,
|
|
35506
35542
|
scrollTrap,
|
|
35507
35543
|
pointerTrap,
|
|
@@ -35514,11 +35550,14 @@ const Dialog = props => {
|
|
|
35514
35550
|
const debugPopup = useDebugPopup();
|
|
35515
35551
|
const debugFocus = useDebugFocus();
|
|
35516
35552
|
const autoFocusProps = useAutoFocus(ref, props.autoFocus);
|
|
35553
|
+
|
|
35554
|
+
// Tracks actual DOM state (open/closed), updated only by open() and close().
|
|
35555
|
+
// Intentionally NOT synced to openProp on every render so the useEffect guard
|
|
35556
|
+
// below can detect whether a prop change requires action.
|
|
35517
35557
|
const openedRef = useRef(false);
|
|
35518
35558
|
const [addCleanup, cleanup] = useCleanup();
|
|
35519
|
-
const open =
|
|
35520
|
-
anchor
|
|
35521
|
-
}) => {
|
|
35559
|
+
const open = e => {
|
|
35560
|
+
const anchor = anchorRef?.current ?? null;
|
|
35522
35561
|
const effectiveAnchor = anchor || document.documentElement;
|
|
35523
35562
|
debugPopup(`"${e.type}" on ${getElementSignature(e.target)} -> openDialog`);
|
|
35524
35563
|
const dialogEl = ref.current;
|
|
@@ -35587,9 +35626,7 @@ const Dialog = props => {
|
|
|
35587
35626
|
...detail
|
|
35588
35627
|
});
|
|
35589
35628
|
};
|
|
35590
|
-
const onRequestOpen =
|
|
35591
|
-
anchor
|
|
35592
|
-
}) => {
|
|
35629
|
+
const onRequestOpen = e => {
|
|
35593
35630
|
const dialogEl = ref.current;
|
|
35594
35631
|
if (!dialogEl) {
|
|
35595
35632
|
return;
|
|
@@ -35597,9 +35634,7 @@ const Dialog = props => {
|
|
|
35597
35634
|
if (openedRef.current) {
|
|
35598
35635
|
return;
|
|
35599
35636
|
}
|
|
35600
|
-
open(e
|
|
35601
|
-
anchor
|
|
35602
|
-
});
|
|
35637
|
+
open(e);
|
|
35603
35638
|
};
|
|
35604
35639
|
const onRequestClose = (e, detail = {}) => {
|
|
35605
35640
|
const dialogEl = ref.current;
|
|
@@ -35629,6 +35664,25 @@ const Dialog = props => {
|
|
|
35629
35664
|
}
|
|
35630
35665
|
close(e, detail);
|
|
35631
35666
|
};
|
|
35667
|
+
useLayoutEffect(() => {
|
|
35668
|
+
if (openProp === undefined) {
|
|
35669
|
+
return;
|
|
35670
|
+
}
|
|
35671
|
+
// Skip when the popover is already in the desired state.
|
|
35672
|
+
// This avoids a feedback loop: our own close/open dispatches navi_close/navi_open,
|
|
35673
|
+
// the parent updates the prop, and the effect would fire again for a change we
|
|
35674
|
+
// already handled. Comparing against openedRef is the authoritative check because
|
|
35675
|
+
// it reflects the actual DOM state, not the React render cycle.
|
|
35676
|
+
if (openProp === openedRef.current) {
|
|
35677
|
+
return;
|
|
35678
|
+
}
|
|
35679
|
+
const e = new CustomEvent("open_prop_change");
|
|
35680
|
+
if (openProp) {
|
|
35681
|
+
onRequestOpen(e);
|
|
35682
|
+
} else {
|
|
35683
|
+
onRequestClose(e);
|
|
35684
|
+
}
|
|
35685
|
+
}, [openProp]);
|
|
35632
35686
|
return jsx(Box, {
|
|
35633
35687
|
...rest,
|
|
35634
35688
|
...autoFocusProps,
|
|
@@ -35638,12 +35692,17 @@ const Dialog = props => {
|
|
|
35638
35692
|
pseudoClasses: DIALOG_PSEUDO_CLASSES,
|
|
35639
35693
|
onMouseDown: e => {
|
|
35640
35694
|
rest.onMouseDown?.(e);
|
|
35641
|
-
//
|
|
35642
|
-
//
|
|
35695
|
+
// Detect backdrop click: the click must land outside the dialog's
|
|
35696
|
+
// bounding rect. Checking coordinates is necessary because clicking
|
|
35697
|
+
// on the dialog's own padding also sets e.target === ref.current.
|
|
35643
35698
|
if (!pointerTrap && e.button === 0 && e.target === ref.current) {
|
|
35644
|
-
|
|
35645
|
-
|
|
35646
|
-
|
|
35699
|
+
const rect = ref.current.getBoundingClientRect();
|
|
35700
|
+
const isBackdrop = e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom;
|
|
35701
|
+
if (isBackdrop) {
|
|
35702
|
+
onRequestClose(e, {
|
|
35703
|
+
isClickOutside: true
|
|
35704
|
+
});
|
|
35705
|
+
}
|
|
35647
35706
|
}
|
|
35648
35707
|
},
|
|
35649
35708
|
onCancel: e => {
|
|
@@ -35653,12 +35712,7 @@ const Dialog = props => {
|
|
|
35653
35712
|
onRequestClose(e);
|
|
35654
35713
|
},
|
|
35655
35714
|
onnavi_request_open: e => {
|
|
35656
|
-
|
|
35657
|
-
anchor
|
|
35658
|
-
} = e.detail;
|
|
35659
|
-
onRequestOpen(e, {
|
|
35660
|
-
anchor
|
|
35661
|
-
});
|
|
35715
|
+
onRequestOpen(e);
|
|
35662
35716
|
},
|
|
35663
35717
|
onnavi_request_close: e => {
|
|
35664
35718
|
onRequestClose(e);
|
|
@@ -35693,6 +35747,8 @@ installImportMetaCssBuild(import.meta);const css$q = /* css */`
|
|
|
35693
35747
|
const Popover = props => {
|
|
35694
35748
|
import.meta.css = [css$q, "@jsenv/navi/src/popup/popover.jsx"];
|
|
35695
35749
|
const {
|
|
35750
|
+
open: openProp = false,
|
|
35751
|
+
anchorRef,
|
|
35696
35752
|
scrollTrap,
|
|
35697
35753
|
pointerTrap,
|
|
35698
35754
|
focusTrap,
|
|
@@ -35713,18 +35769,19 @@ const Popover = props => {
|
|
|
35713
35769
|
const debugPopup = useDebugPopup();
|
|
35714
35770
|
const debugFocus = useDebugFocus();
|
|
35715
35771
|
const autoFocusProps = useAutoFocus(ref, props.autoFocus);
|
|
35716
|
-
|
|
35717
|
-
|
|
35718
|
-
|
|
35772
|
+
|
|
35773
|
+
// Tracks actual DOM state (open/closed), updated only by open() and close().
|
|
35774
|
+
// Intentionally NOT synced to openProp on every render so the useEffect guard
|
|
35775
|
+
// below can detect whether a prop change requires action.
|
|
35776
|
+
const openedRef = useRef(false);
|
|
35719
35777
|
const [addCleanup, cleanup] = useCleanup();
|
|
35720
|
-
const open =
|
|
35721
|
-
anchor
|
|
35722
|
-
}) => {
|
|
35778
|
+
const open = e => {
|
|
35723
35779
|
debugPopup(e, `openPopover()`);
|
|
35724
35780
|
const popoverEl = ref.current;
|
|
35725
35781
|
const focusedBeforeOpen = getFocusedBeforeTransfer(e);
|
|
35726
35782
|
popoverEl.showPopover();
|
|
35727
35783
|
transferFocus(popoverEl, debugFocus, e, focusedBeforeOpen);
|
|
35784
|
+
const anchor = anchorRef?.current ?? null;
|
|
35728
35785
|
const effectiveAnchor = anchor || document.documentElement;
|
|
35729
35786
|
const positionPopover = positionEvent => {
|
|
35730
35787
|
const {
|
|
@@ -35797,7 +35854,6 @@ const Popover = props => {
|
|
|
35797
35854
|
rectEffect.disconnect();
|
|
35798
35855
|
});
|
|
35799
35856
|
openedRef.current = true;
|
|
35800
|
-
setOpened(true);
|
|
35801
35857
|
dispatchCustomEvent(popoverEl, "navi_open", {
|
|
35802
35858
|
event: e,
|
|
35803
35859
|
focusedBeforeOpen
|
|
@@ -35810,15 +35866,12 @@ const Popover = props => {
|
|
|
35810
35866
|
popoverEl.hidePopover();
|
|
35811
35867
|
cleanup();
|
|
35812
35868
|
openedRef.current = false;
|
|
35813
|
-
setOpened(false);
|
|
35814
35869
|
dispatchCustomEvent(popoverEl, "navi_close", {
|
|
35815
35870
|
event: e,
|
|
35816
35871
|
...detail
|
|
35817
35872
|
});
|
|
35818
35873
|
};
|
|
35819
|
-
const onRequestOpen =
|
|
35820
|
-
anchor
|
|
35821
|
-
}) => {
|
|
35874
|
+
const onRequestOpen = e => {
|
|
35822
35875
|
const popoverEl = ref.current;
|
|
35823
35876
|
if (!popoverEl) {
|
|
35824
35877
|
return;
|
|
@@ -35826,9 +35879,7 @@ const Popover = props => {
|
|
|
35826
35879
|
if (openedRef.current) {
|
|
35827
35880
|
return;
|
|
35828
35881
|
}
|
|
35829
|
-
open(e
|
|
35830
|
-
anchor
|
|
35831
|
-
});
|
|
35882
|
+
open(e);
|
|
35832
35883
|
};
|
|
35833
35884
|
const onRequestClose = (e, detail = {}) => {
|
|
35834
35885
|
const popoverEl = ref.current;
|
|
@@ -35858,6 +35909,27 @@ const Popover = props => {
|
|
|
35858
35909
|
}
|
|
35859
35910
|
close(e, detail);
|
|
35860
35911
|
};
|
|
35912
|
+
useLayoutEffect(() => {
|
|
35913
|
+
if (openProp === undefined) {
|
|
35914
|
+
return;
|
|
35915
|
+
}
|
|
35916
|
+
// Skip when the popover is already in the desired state.
|
|
35917
|
+
// This avoids a feedback loop: our own close/open dispatches navi_close/navi_open,
|
|
35918
|
+
// the parent updates the prop, and the effect would fire again for a change we
|
|
35919
|
+
// already handled. Comparing against openedRef is the authoritative check because
|
|
35920
|
+
// it reflects the actual DOM state, not the React render cycle.
|
|
35921
|
+
if (openProp === openedRef.current) {
|
|
35922
|
+
return;
|
|
35923
|
+
}
|
|
35924
|
+
const e = new CustomEvent("open_prop_change", {
|
|
35925
|
+
detail: {}
|
|
35926
|
+
});
|
|
35927
|
+
if (openProp) {
|
|
35928
|
+
onRequestOpen(e);
|
|
35929
|
+
} else {
|
|
35930
|
+
onRequestClose(e);
|
|
35931
|
+
}
|
|
35932
|
+
}, [openProp]);
|
|
35861
35933
|
return jsxs(Box, {
|
|
35862
35934
|
id: id,
|
|
35863
35935
|
popover: "manual",
|
|
@@ -35867,12 +35939,7 @@ const Popover = props => {
|
|
|
35867
35939
|
baseClassName: "navi_popover",
|
|
35868
35940
|
pseudoClasses: POPOVER_PSEUDO_CLASSES,
|
|
35869
35941
|
onnavi_request_open: e => {
|
|
35870
|
-
|
|
35871
|
-
anchor
|
|
35872
|
-
} = e.detail;
|
|
35873
|
-
onRequestOpen(e, {
|
|
35874
|
-
anchor
|
|
35875
|
-
});
|
|
35942
|
+
onRequestOpen(e);
|
|
35876
35943
|
},
|
|
35877
35944
|
onnavi_request_close: e => {
|
|
35878
35945
|
onRequestClose(e);
|
|
@@ -35902,11 +35969,21 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
|
|
|
35902
35969
|
/* Shared by popover and dialog */
|
|
35903
35970
|
--picker-popup-background-color: var(--picker-background-color);
|
|
35904
35971
|
--picker-popup-border-radius: var(--picker-border-radius);
|
|
35972
|
+
--picker-popup-border-width: var(--picker-border-width);
|
|
35905
35973
|
/* Popover */
|
|
35906
|
-
--picker-popover-max-height: 300px;
|
|
35974
|
+
--picker-popover-max-height: 300px; /* soft: user-configurable preferred max-height */
|
|
35975
|
+
--picker-popover-maxmax-height: calc(0.95 * var(--navi-vvh));
|
|
35976
|
+
--picker-popover-maxmax-width: calc(0.95 * var(--navi-vvw));
|
|
35977
|
+
/* --picker-popover-max-width: soft, leave unset to rely on maxmax */
|
|
35907
35978
|
/* Dialog */
|
|
35908
|
-
--picker-dialog-
|
|
35909
|
-
--picker-dialog-
|
|
35979
|
+
--picker-dialog-margin: 3dvw; /* min gap between dialog edges and viewport */
|
|
35980
|
+
--picker-dialog-maxmax-width: calc(
|
|
35981
|
+
var(--navi-vvw) - 2 * var(--picker-dialog-margin)
|
|
35982
|
+
);
|
|
35983
|
+
--picker-dialog-maxmax-height: calc(
|
|
35984
|
+
var(--navi-vvh) - 2 * var(--picker-dialog-margin)
|
|
35985
|
+
);
|
|
35986
|
+
--picker-dialog-border-width: 0px; /* Dialog do not need border like popover (they stand out more) */
|
|
35910
35987
|
|
|
35911
35988
|
/* popover */
|
|
35912
35989
|
&[aria-haspopup="listbox"] {
|
|
@@ -35914,11 +35991,15 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
|
|
|
35914
35991
|
position: absolute;
|
|
35915
35992
|
inset: unset;
|
|
35916
35993
|
min-width: var(--anchor-width, 0px);
|
|
35917
|
-
max-width:
|
|
35994
|
+
max-width: min(
|
|
35995
|
+
var(--picker-popover-max-width, var(--picker-popover-maxmax-width)),
|
|
35996
|
+
var(--picker-popover-maxmax-width)
|
|
35997
|
+
);
|
|
35918
35998
|
/* max-height covers the placeholder + list; the list scrolls internally */
|
|
35919
35999
|
max-height: min(
|
|
35920
36000
|
var(--picker-popover-max-height),
|
|
35921
|
-
var(--space-available,
|
|
36001
|
+
var(--space-available, var(--picker-popover-maxmax-height)),
|
|
36002
|
+
var(--picker-popover-maxmax-height)
|
|
35922
36003
|
);
|
|
35923
36004
|
margin: 0;
|
|
35924
36005
|
padding: 0;
|
|
@@ -36012,11 +36093,18 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
|
|
|
36012
36093
|
&[aria-haspopup="dialog"] {
|
|
36013
36094
|
.navi_picker_dialog {
|
|
36014
36095
|
min-width: var(--anchor-width, 0px);
|
|
36015
|
-
max-width:
|
|
36016
|
-
|
|
36096
|
+
max-width: min(
|
|
36097
|
+
var(--picker-dialog-max-width, var(--picker-dialog-maxmax-width)),
|
|
36098
|
+
var(--picker-dialog-maxmax-width)
|
|
36099
|
+
);
|
|
36100
|
+
max-height: min(
|
|
36101
|
+
var(--picker-dialog-max-height, var(--picker-dialog-maxmax-height)),
|
|
36102
|
+
var(--picker-dialog-maxmax-height)
|
|
36103
|
+
);
|
|
36017
36104
|
padding: 0;
|
|
36018
36105
|
background: var(--picker-popup-background-color);
|
|
36019
|
-
border: var(--picker-border-width) solid
|
|
36106
|
+
border: var(--picker-dialog-border-width) solid
|
|
36107
|
+
var(--x-picker-border-color);
|
|
36020
36108
|
border-radius: var(--picker-popup-border-radius);
|
|
36021
36109
|
outline-width: var(--picker-outline-width);
|
|
36022
36110
|
outline-color: var(--picker-outline-color);
|
|
@@ -36028,10 +36116,10 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
|
|
|
36028
36116
|
/* overscroll-behavior: contain; */
|
|
36029
36117
|
|
|
36030
36118
|
&[data-expand-x] {
|
|
36031
|
-
width: var(--picker-dialog-
|
|
36119
|
+
width: var(--picker-dialog-maxmax-width);
|
|
36032
36120
|
}
|
|
36033
36121
|
&[data-expand-y] {
|
|
36034
|
-
height: var(--picker-dialog-
|
|
36122
|
+
height: var(--picker-dialog-maxmax-height);
|
|
36035
36123
|
}
|
|
36036
36124
|
|
|
36037
36125
|
&[open] {
|
|
@@ -36157,8 +36245,9 @@ const PickerCustom = props => {
|
|
|
36157
36245
|
const popupRef = useRef(null);
|
|
36158
36246
|
popupProps.ref = popupRef;
|
|
36159
36247
|
// aria-controls + id
|
|
36248
|
+
const autoId = useId();
|
|
36249
|
+
const popupId = `picker_popup_${autoId}`;
|
|
36160
36250
|
{
|
|
36161
|
-
const popupId = useId();
|
|
36162
36251
|
Object.assign(pickerProps, {
|
|
36163
36252
|
"aria-controls": popupId
|
|
36164
36253
|
});
|
|
@@ -36170,14 +36259,27 @@ const PickerCustom = props => {
|
|
|
36170
36259
|
{
|
|
36171
36260
|
const debugFocus = useDebugFocus();
|
|
36172
36261
|
const debugPopup = useDebugPopup();
|
|
36173
|
-
|
|
36174
|
-
|
|
36175
|
-
|
|
36262
|
+
// In "dialog" mode, enterExpanded() pushes a history entry so the back button closes.
|
|
36263
|
+
// In "popover" mode, it replaces the current history state (no history entry added).
|
|
36264
|
+
const pickerNavType = mode === "dialog" ? "push" : "replace";
|
|
36265
|
+
const expandedRef = useRef(false);
|
|
36266
|
+
const [expanded, enterExpanded, leaveExpanded] = useNavState(popupId, false, {
|
|
36267
|
+
type: pickerNavType,
|
|
36268
|
+
// onLeave fires only when the state key disappears externally (back button/gesture most of the time).
|
|
36269
|
+
onLeave: () => {
|
|
36270
|
+
requestClose(new CustomEvent("navi_nav_away", {
|
|
36271
|
+
detail: {}
|
|
36272
|
+
}), {
|
|
36273
|
+
isCancel: true
|
|
36274
|
+
});
|
|
36275
|
+
}
|
|
36276
|
+
});
|
|
36277
|
+
expandedRef.current = Boolean(expanded);
|
|
36176
36278
|
const valueAtOpenRef = useRef(null);
|
|
36177
36279
|
const activeElementAtOpenRef = useRef(null);
|
|
36178
36280
|
const onOpen = e => {
|
|
36179
36281
|
expandedRef.current = true;
|
|
36180
|
-
|
|
36282
|
+
enterExpanded();
|
|
36181
36283
|
const focusedBeforeOpen = e.detail.focusedBeforeOpen;
|
|
36182
36284
|
activeElementAtOpenRef.current = focusedBeforeOpen;
|
|
36183
36285
|
debugFocus(e, "picked opened, store element focused", focusedBeforeOpen);
|
|
@@ -36220,7 +36322,7 @@ const PickerCustom = props => {
|
|
|
36220
36322
|
}
|
|
36221
36323
|
}
|
|
36222
36324
|
expandedRef.current = false;
|
|
36223
|
-
|
|
36325
|
+
leaveExpanded();
|
|
36224
36326
|
// Reset so the next opening re-evaluates screen size
|
|
36225
36327
|
defaultModeRef.current = null;
|
|
36226
36328
|
restoreFocus(e);
|
|
@@ -36236,8 +36338,7 @@ const PickerCustom = props => {
|
|
|
36236
36338
|
});
|
|
36237
36339
|
const popupEl = popupRef.current;
|
|
36238
36340
|
return dispatchCustomEvent(popupEl, "navi_request_open", {
|
|
36239
|
-
event: e
|
|
36240
|
-
anchor: pickerEl
|
|
36341
|
+
event: e
|
|
36241
36342
|
});
|
|
36242
36343
|
};
|
|
36243
36344
|
const requestClose = (e = new CustomEvent("programmatic"), {
|
|
@@ -36258,7 +36359,7 @@ const PickerCustom = props => {
|
|
|
36258
36359
|
uiAction: uiActionProp
|
|
36259
36360
|
} = props;
|
|
36260
36361
|
Object.assign(pickerProps, {
|
|
36261
|
-
"aria-expanded": expanded,
|
|
36362
|
+
"aria-expanded": Boolean(expanded),
|
|
36262
36363
|
"onActionStart": e => {
|
|
36263
36364
|
onActionStart?.(e);
|
|
36264
36365
|
// requestClose(e);
|
|
@@ -36289,6 +36390,8 @@ const PickerCustom = props => {
|
|
|
36289
36390
|
children
|
|
36290
36391
|
});
|
|
36291
36392
|
Object.assign(popupProps, {
|
|
36393
|
+
anchorRef: props.ref,
|
|
36394
|
+
open: Boolean(expanded),
|
|
36292
36395
|
closeRequestHandler: (requestCloseEvent, closePermission, {
|
|
36293
36396
|
isClickOutside
|
|
36294
36397
|
} = {}) => {
|
|
@@ -36396,11 +36499,14 @@ const PickerCustom = props => {
|
|
|
36396
36499
|
};
|
|
36397
36500
|
}
|
|
36398
36501
|
});
|
|
36502
|
+
const isWithinPopup = el => {
|
|
36503
|
+
const popupEl = popupRef.current;
|
|
36504
|
+
return el === popupEl || popupEl.contains(el);
|
|
36505
|
+
};
|
|
36399
36506
|
Object.assign(pickerProps, {
|
|
36400
36507
|
eventReactionDefinitions: {
|
|
36401
36508
|
mouseDown: e => {
|
|
36402
|
-
|
|
36403
|
-
if (popupEl && popupEl.contains(e.target)) {
|
|
36509
|
+
if (isWithinPopup(e.target)) {
|
|
36404
36510
|
return null;
|
|
36405
36511
|
}
|
|
36406
36512
|
if (expandedRef.current) {
|
|
@@ -36421,8 +36527,7 @@ const PickerCustom = props => {
|
|
|
36421
36527
|
};
|
|
36422
36528
|
},
|
|
36423
36529
|
click: e => {
|
|
36424
|
-
|
|
36425
|
-
if (popupEl && popupEl.contains(e.target)) {
|
|
36530
|
+
if (isWithinPopup(e.target)) {
|
|
36426
36531
|
return null;
|
|
36427
36532
|
}
|
|
36428
36533
|
// When a label is clicked it transfers focus to the select
|
|
@@ -40979,6 +41084,7 @@ const PickerStyleCSSVars = {
|
|
|
40979
41084
|
"popoverMaxHeight": "--picker-popover-max-height",
|
|
40980
41085
|
"popupBackgroundColor": "--picker-popup-background-color",
|
|
40981
41086
|
"popupBorderRadius": "--picker-popup-border-radius",
|
|
41087
|
+
"dialogBorderWidth": "--picker-dialog-border-width",
|
|
40982
41088
|
"padding": "--picker-padding",
|
|
40983
41089
|
"paddingX": "--picker-padding-x",
|
|
40984
41090
|
"paddingY": "--picker-padding-y",
|
|
@@ -47066,5 +47172,5 @@ const UserSvg = () => jsx("svg", {
|
|
|
47066
47172
|
})
|
|
47067
47173
|
});
|
|
47068
47174
|
|
|
47069
|
-
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout,
|
|
47175
|
+
export { ActionRenderer, ActiveKeyboardShortcuts, Address, Badge, BadgeCount, BadgeList, Box, Button, ButtonCopyToClipboard, Caption, CheckSvg, CheckboxGroup, CloseSvg, Code, Col, Colgroup, Color, ConstructionSvg, ControlGroup, Details, Dialog, DialogLayout, Editable, ErrorBoundary, ErrorBoundaryContext, ExclamationSvg, EyeClosedSvg, EyeSvg, Field, Form, Group, Head, HeartSvg, HomeSvg, Icon, Image, Input, InputDuration, Interpolate, Label, Link, LinkAnchorSvg, LinkBlankTargetSvg, LinkCurrentSvg, List, ListItem, ListItemGroup, Loading, LoadingDotsSvg, LoadingIndicator, LoadingIndicatorFluid, LoadingOutline, MessageBox, Meter, Nav, NaviDebug, Paragraph, Picker, Popover, Quantity, RadioGroup, Route, RowNumberCol, RowNumberTableCell, SVGMaskOverlay, SearchSvg, SelectableInput, SelectionContext, Separator, SettingsSvg, SidePanel, StarSvg, SummaryMarker, Svg, Table, TableCell, Tbody, Text, TextBox, Thead, Time, Title, Tr, UITransition, Unit, UserSvg, ViewportLayout, actionRunEffect, anyMatchingRouteSignal, applySearch, arraySignalMembership, compareTwoJsValues, createAction, createAvailableConstraint, createRequestCanceller, createSearch, createSelectionKeyboardShortcuts, enableDebugActions, enableDebugOnDocumentLoading, ensureDocumentStartViewTransition, filterTableSelection, formatDatetime, formatDay, formatDayRelative, formatMonth, formatNumber, formatTime, formatTimeRelative, getNowHours, getNowHoursRoundedToStep, interpolateText, isCellSelected, isColumnSelected, isRowSelected, isToday, langSignal, localStorageSignal, moveArrayItemByIndex, navBack, navForward, navIntegratedVia, navTo, naviI18n, openCallout, rawUrlPart, registerGlobalConstraint, reload, rerunActions, resource, route, routeAction, setBaseUrl, setupRoutes, stateSignal, stopLoad, stringifyTableSelectionValue, swapArrayItemByIndex, syncOwnedResourceToSignals, syncResourceToSignals, updateActions, useActionStatus, useArraySignalMembership, useAsyncData, useCalloutRequestClose, useCancelPrevious, useCellGridFromRows, useConstraintValidityState, useDependenciesDiff, useDisplayedLayoutEffect, useDocumentResource, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useOrderedColumns, useRouteStatus, useRunOnMount, useSearchText, useSelectableElement, useSelectionController, useSidePanelClose, useSignalSync, useStateArray, useTitleLevel, useUrlSearchParam, valueInLocalStorage, windowWidthSignal };
|
|
47070
47176
|
//# sourceMappingURL=jsenv_navi.js.map
|