@jsenv/navi 0.29.484 → 0.29.486

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.
@@ -1360,7 +1360,7 @@ const CONSTRAINT_MESSAGES = {
1360
1360
  en: "This password must contain at most [max] characters (it currently contains [count] characters).",
1361
1361
  },
1362
1362
  "constraint.max_length.email": {
1363
- fr: "Cette adresse e-mail doit contenir au maximum [max] caractères (il contient actuellement [count] caractères).",
1363
+ fr: "Cette adresse e-mail doit contenir au maximum [max] caractères (elle contient actuellement [count] caractères).",
1364
1364
  en: "This email address must contain at most [max] characters (it currently contains [count] characters).",
1365
1365
  },
1366
1366
  "constraint.max_length.default": {
@@ -12079,6 +12079,31 @@ const resolveEffectiveDocumentState = (
12079
12079
  return { ...state, ...sharedState };
12080
12080
  };
12081
12081
 
12082
+ /**
12083
+ * What a press on a link handed to the navigation it starts (see the
12084
+ * integrations' `carryByLinkNavigation`): each write is an `editState` applied
12085
+ * to the entry that navigation writes, and an `otherwise` that makes the write
12086
+ * where it would have been made, for a press that starts no navigation here.
12087
+ */
12088
+ const applyPressWrites = (state, pressWrites) => {
12089
+ if (!pressWrites) {
12090
+ return state;
12091
+ }
12092
+ let stateWritten = state;
12093
+ for (const { editState } of pressWrites) {
12094
+ stateWritten = editState(stateWritten);
12095
+ }
12096
+ return stateWritten;
12097
+ };
12098
+ const writePressWritesHere = (pressWrites) => {
12099
+ if (!pressWrites) {
12100
+ return;
12101
+ }
12102
+ for (const { otherwise } of pressWrites) {
12103
+ otherwise();
12104
+ }
12105
+ };
12106
+
12082
12107
  // Preact's own useId() (see preact/hooks) returns "P<mask0>-<mask1>", where the
12083
12108
  // mask is derived from render order within the nearest root/async boundary:
12084
12109
  // stable across re-renders of the same mount, but not across two mounts on the
@@ -13734,6 +13759,7 @@ const setupBrowserIntegrationViaHistory = ({
13734
13759
  // Asked to go where we already are: an address that redirects is never
13735
13760
  // the one being displayed, so there is nothing to go to and nothing to
13736
13761
  // stack on the history.
13762
+ writePressWritesHere(options.pressWrites);
13737
13763
  return undefined;
13738
13764
  }
13739
13765
  return handleRoutingTask(redirectionUrl, {
@@ -13745,10 +13771,13 @@ const setupBrowserIntegrationViaHistory = ({
13745
13771
  // Decided before anything is announced: an elided push IS the traversal it
13746
13772
  // becomes, and the traversal will make its own announcements when the
13747
13773
  // browser answers — a before/after cycle here would be about a navigation
13748
- // that never happens.
13774
+ // that never happens. A push with something of its own for its entry — a
13775
+ // state, what the press handed over (see carryByLinkNavigation) — is a
13776
+ // new entry, never one standing there already.
13749
13777
  if (
13750
13778
  options.navigationType === "push" &&
13751
13779
  options.state === undefined &&
13780
+ !options.pressWrites &&
13752
13781
  url !== readAddress()
13753
13782
  ) {
13754
13783
  const delta = adjacentEntryDelta(url);
@@ -13798,6 +13827,7 @@ const setupBrowserIntegrationViaHistory = ({
13798
13827
  redirected,
13799
13828
  landOn,
13800
13829
  debounce,
13830
+ pressWrites,
13801
13831
  } = options;
13802
13832
  let { state } = options;
13803
13833
 
@@ -13808,15 +13838,18 @@ const setupBrowserIntegrationViaHistory = ({
13808
13838
 
13809
13839
  if (navigationType === "push" || navigationType === "replace") {
13810
13840
  markUrlAsVisited(url);
13811
- const effectiveState = resolveEffectiveDocumentState(state, {
13812
- navigationType,
13813
- isArrival: isArrival(url, { from: urlLeft }),
13814
- currentState: getDocumentState(),
13815
- sharedState: {
13816
- jsenv_visited_urls: Array.from(visitedUrlSet),
13817
- [NAV_DEPTH_STATE_KEY]: getNavDepth(),
13818
- },
13819
- });
13841
+ const effectiveState = applyPressWrites(
13842
+ resolveEffectiveDocumentState(state, {
13843
+ navigationType,
13844
+ isArrival: isArrival(url, { from: urlLeft }),
13845
+ currentState: getDocumentState(),
13846
+ sharedState: {
13847
+ jsenv_visited_urls: Array.from(visitedUrlSet),
13848
+ [NAV_DEPTH_STATE_KEY]: getNavDepth(),
13849
+ },
13850
+ }),
13851
+ pressWrites,
13852
+ );
13820
13853
  if (navigationType === "replace" && debounce && state === undefined) {
13821
13854
  // Nothing of its own for the entry: the address moves, the state is
13822
13855
  // carried over. The one kind of write allowed to lag, and only when
@@ -13916,6 +13949,36 @@ const setupBrowserIntegrationViaHistory = ({
13916
13949
  return requestedResult;
13917
13950
  };
13918
13951
 
13952
+ // A press on a link is one navigation, and what the press asks of the
13953
+ // history entry is part of it: a popup the link's own command closes is
13954
+ // closed on the entry the link leads to, while the one it leaves keeps what
13955
+ // the reader was looking at — a back finds the popup open. The link's
13956
+ // listeners run before the routing is asked (below), so a write they make
13957
+ // is handed over, keyed by the click, rather than written onto the entry
13958
+ // being left. Once the link has been answered there is nothing to hand
13959
+ // over to: the navigation, when there was one, has already written its
13960
+ // entry, and a write lands there by itself.
13961
+ const linkPressWritesMap = new WeakMap();
13962
+ const answeredLinkClickSet = new WeakSet();
13963
+ const carryByLinkNavigation = (event, pressWrite) => {
13964
+ const clickEvent = findEvent(event, "click");
13965
+ if (
13966
+ !clickEvent ||
13967
+ clickEvent.eventPhase === Event.NONE ||
13968
+ answeredLinkClickSet.has(clickEvent) ||
13969
+ !clickEvent.target.closest("a")
13970
+ ) {
13971
+ return false;
13972
+ }
13973
+ const pressWrites = linkPressWritesMap.get(clickEvent);
13974
+ if (pressWrites) {
13975
+ pressWrites.push(pressWrite);
13976
+ } else {
13977
+ linkPressWritesMap.set(clickEvent, [pressWrite]);
13978
+ }
13979
+ return true;
13980
+ };
13981
+
13919
13982
  // A click on a link is answered on the link itself, after the link's own
13920
13983
  // listeners: a navi control decides there whether the press is allowed — it
13921
13984
  // refuses with preventDefault from its click reaction — and runs its command
@@ -13923,34 +13986,42 @@ const setupBrowserIntegrationViaHistory = ({
13923
13986
  // is the order the Navigation API integration gets for free, where the
13924
13987
  // navigation starts once the click has been dispatched.
13925
13988
  const onLinkClick = (e) => {
13989
+ answeredLinkClickSet.add(e);
13990
+ const pressWrites = linkPressWritesMap.get(e);
13991
+ linkPressWritesMap.delete(e);
13992
+ if (!routeLinkClick(e, pressWrites)) {
13993
+ writePressWritesHere(pressWrites);
13994
+ }
13995
+ };
13996
+ const routeLinkClick = (e, pressWrites) => {
13926
13997
  if (e.button !== 0) {
13927
13998
  // Ignore non-left clicks
13928
- return;
13999
+ return false;
13929
14000
  }
13930
14001
  if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
13931
14002
  // A modifier asks the browser for something other than this tab: a new
13932
14003
  // tab (meta on macOS, ctrl elsewhere), a new window (shift), a download
13933
14004
  // (alt).
13934
- return;
14005
+ return false;
13935
14006
  }
13936
14007
  if (e.defaultPrevented) {
13937
14008
  // Refused — by the link itself, or by whoever came before it.
13938
- return;
14009
+ return false;
13939
14010
  }
13940
14011
  const linkElement = e.currentTarget;
13941
14012
  if (linkElement.hasAttribute("data-readonly")) {
13942
- return;
14013
+ return false;
13943
14014
  }
13944
14015
  if (linkOpensElsewhere(linkElement)) {
13945
14016
  // Another tab or window, or a download (see link_elsewhere.js).
13946
- return;
14017
+ return false;
13947
14018
  }
13948
14019
  const href = linkElement.href;
13949
14020
  const { isEmpty, isCurrent, isSameOrigin, isAnchor } =
13950
14021
  getHrefTargetInfo(href);
13951
14022
  if (isEmpty || !isSameOrigin) {
13952
14023
  // Let link to other origins be handled by the browser
13953
- return;
14024
+ return false;
13954
14025
  }
13955
14026
  if (isAnchor) {
13956
14027
  // Fragment navigation belongs to the browser: it owns the indicated
@@ -13962,13 +14033,13 @@ const setupBrowserIntegrationViaHistory = ({
13962
14033
  // change reaches whoever is waiting on the designated element.
13963
14034
  rearmUrlTarget();
13964
14035
  }
13965
- return;
14036
+ return false;
13966
14037
  }
13967
14038
  if (linkAsksForDocument(linkElement)) {
13968
14039
  // The link says its address is another document of this origin (see
13969
14040
  // link_document.js). Routing to it would land on the fallback route
13970
14041
  // instead of loading the page.
13971
- return;
14042
+ return false;
13972
14043
  }
13973
14044
  // Nothing here declared a route, so there is nothing to route to: the
13974
14045
  // page is a plain document and a link in it is a plain link. Taking it
@@ -13976,7 +14047,7 @@ const setupBrowserIntegrationViaHistory = ({
13976
14047
  // the address bar moves and the page does not (see applyRouting's own
13977
14048
  // "not called yet" branch, which is where that used to end up).
13978
14049
  if (!isRouting()) {
13979
- return;
14050
+ return false;
13980
14051
  }
13981
14052
  e.preventDefault();
13982
14053
  handleRoutingTask(href, {
@@ -13990,7 +14061,9 @@ const setupBrowserIntegrationViaHistory = ({
13990
14061
  // whoever knows what to do with it, and it is the anchor itself —
13991
14062
  // resolved here, where it already is.
13992
14063
  element: linkElement,
14064
+ pressWrites,
13993
14065
  });
14066
+ return true;
13994
14067
  };
13995
14068
  // Wired from the capture phase on window, the first place a click is seen,
13996
14069
  // so that a link is answered whatever handler stops the click on its way up
@@ -14170,6 +14243,7 @@ const setupBrowserIntegrationViaHistory = ({
14170
14243
  navBack,
14171
14244
  navForward,
14172
14245
  getDocumentState,
14246
+ carryByLinkNavigation,
14173
14247
  isVisited,
14174
14248
  visitedUrlsSignal,
14175
14249
  };
@@ -14707,8 +14781,32 @@ const useNavStateBasic = (
14707
14781
  // that param for gone. Only with nothing of this document behind (the state
14708
14782
  // was entered on a cold-loaded url, navBack has nowhere to go) does it
14709
14783
  // rewrite in place.
14710
- const leave = ({ isBack } = {}) => {
14784
+ //
14785
+ // event: what asked for it. A leave the press of a link asks for (its
14786
+ // --navi-close) is made by the navigation that link starts: the entry the
14787
+ // link leads to does not hold the key, and every entry behind is left as
14788
+ // the reader saw it — a back finds the state still entered.
14789
+ const leave = ({ isBack, event } = {}) => {
14711
14790
  enteredRef.current = false;
14791
+ if (!Object.hasOwn(browserIntegration.getDocumentState() || {}, id)) {
14792
+ return;
14793
+ }
14794
+ if (
14795
+ event &&
14796
+ browserIntegration.carryByLinkNavigation(event, {
14797
+ editState: (state) => {
14798
+ const stateWithoutKey = { ...state };
14799
+ delete stateWithoutKey[id];
14800
+ return stateWithoutKey;
14801
+ },
14802
+ otherwise: () => leaveEntry({ isBack }),
14803
+ })
14804
+ ) {
14805
+ return;
14806
+ }
14807
+ leaveEntry({ isBack });
14808
+ };
14809
+ const leaveEntry = ({ isBack }) => {
14712
14810
  const currentStateCopy = browserIntegration.getDocumentState() || {};
14713
14811
  if (!Object.hasOwn(currentStateCopy, id)) {
14714
14812
  return;
@@ -14766,6 +14864,9 @@ const useNavStateBasic = (
14766
14864
  * the value allows associating extra data when needed.
14767
14865
  * - `leave()`: navigate AWAY FROM this state (removes `id` from document state,
14768
14866
  * or goes back in history when `type` is "push").
14867
+ * `leave({ event })` with the event that asked for it: asked by the press
14868
+ * of a link that navigates, the key is left out of the entry the link
14869
+ * leads to, and the entry it leaves keeps it.
14769
14870
  */
14770
14871
  const useNavState = useNavStateWithWarnings
14771
14872
  ;
@@ -17996,8 +18097,13 @@ const useOpenPropsEffectOnOpenController = (
17996
18097
  } else {
17997
18098
  // Under type "push" a cancel discards everything written while
17998
18099
  // the popup was open — it goes back with the entry; a confirmed
17999
- // close keeps those writes (see useNavState's own leave()).
18000
- leaveNavState({ isBack: Boolean(event?.detail?.isCancel) });
18100
+ // close keeps those writes (see useNavState's own leave()). A
18101
+ // close a link's press asked for is written by the navigation
18102
+ // the link starts.
18103
+ leaveNavState({
18104
+ isBack: Boolean(event?.detail?.isCancel),
18105
+ event,
18106
+ });
18001
18107
  }
18002
18108
  }
18003
18109
  if (signal) {
@@ -54584,7 +54690,9 @@ const LinkPseudoElements = ["::-navi-loader", "::-navi-indicator"];
54584
54690
  * fetching on a hover.
54585
54691
  * @param {string} [props.command] - What the press asks of a control around
54586
54692
  * the link — `"--navi-close"` on a link that leaves the sheet it is in.
54587
- * Triggered on the press, before the navigation.
54693
+ * Triggered on the press, before the navigation. A popup it closes that
54694
+ * keeps its open state in the history entry (`navState`) is closed on the
54695
+ * entry the navigation leads to: Back finds it open.
54588
54696
  * @param {Function} [props.action] - Work the press runs, before the
54589
54697
  * navigation. Nothing waits for it — not the navigation, not `command`: what
54590
54698
  * the next page must find has to be written synchronously (a draft in a
@@ -58039,9 +58147,9 @@ const useExpandableContext = partName => {
58039
58147
  * closeKeyShortcut?: string,
58040
58148
  * }>}
58041
58149
  * @param ui - Shorthand for the common shape: renders `<Expandable.UI>{ui}</Expandable.UI>`
58042
- * above the content (children). Any markup is allowed (buttons, links,
58043
- * fields inside it keep their own behavior and do not toggle the
58044
- * expandable). A function receives `{ open }` to render differently per
58150
+ * above the content (children). Any markup is allowed: a button, a link, a
58151
+ * field, any navi control or `selfInteractions` claimant inside it keeps its
58152
+ * own behavior and does not toggle the expandable (see `isPressOnControl`). A function receives `{ open }` to render differently per
58045
58153
  * state. For other orders/orientations, pass `<Expandable.UI>` and
58046
58154
  * `<Expandable.Content>` as children instead.
58047
58155
  * @param open - Drives the state from outside: the expandable opens/closes to