@jsenv/navi 0.29.435 → 0.29.436

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.
@@ -7,11 +7,11 @@ import { createPubSub, elementIsFocusable, createIterableWeakSet, dispatchIntern
7
7
  export { chainEvent, clickIsSuppressed, contrastColor, createDragGestureController, dragAfterIntent, findEvent, markDragSource, startDragTo } from "@jsenv/dom";
8
8
  import { signal, computed, effect, untracked, batch, useComputed, useSignal } from "@preact/signals";
9
9
  import { options, isValidElement, createContext, render as render$1, h, toChildArray, createRef, cloneElement, createElement, Fragment as Fragment$1 } from "preact";
10
- import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useEffect, useCallback, useReducer, useId } from "preact/hooks";
10
+ import { useErrorBoundary, useLayoutEffect, useContext, useState, useMemo, useRef, useReducer, useEffect, useCallback, useId } from "preact/hooks";
11
11
  import { humanizeI18n, setRuntimeLangSource, formatDuration, formatMonth, formatDay, resolveTimeRangePrecision, formatDatePlaceholder, toDate, getRelativeDay, formatDayRelative, formatMonthPlaceholder, formatWeekPlaceholder, formatDatetimePlaceholder, formatDatetime, toTimeOfDay, formatTimeOfDay, formatTime, formatMinuteDuration, formatSecondDuration, formatHourDuration, formatTimeRelative, formatNumber, interpolateText, installInterpolateJsx } from "@jsenv/humanize";
12
12
  export { createI18n, formatDatetime, formatDay, formatDayRelative, formatDuration, formatHourDuration, formatMinuteDuration, formatMonth, formatNumber, formatSecondDuration, formatTime, formatTimeOfDay, formatTimeRange, formatTimeRelative, interpolateText } from "@jsenv/humanize";
13
13
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
14
- import { CHAR_CLASS_PRESETS, createValidity, TYPE_RULE, durationContainsNaN, compareTwoDurations, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, parseDuration, durationToISOString } from "@jsenv/validity";
14
+ import { CHAR_CLASS_PRESETS, createValidity, durationContainsNaN, compareTwoDurations, TYPE_RULE, durationToSeconds, DISPLAYABLE_RULE, MAX_LINE_BREAKS_RULE, NO_EMOJI_RULE, SINGLE_SPACE_RULE, resolveCharClass, getCharClassMessageKey, compileCharClassAnchored, compileCharClass, parseDuration, durationToISOString } from "@jsenv/validity";
15
15
  export { compareTwoDurations, durationContainsNaN, durationToHours, durationToISOString, durationToMinutes, durationToNumber, durationToSeconds, durationToString, parseDuration } from "@jsenv/validity";
16
16
  import { Suspense, createPortal, forwardRef } from "preact/compat";
17
17
 
@@ -944,8 +944,10 @@ naviI18n.addAll({
944
944
  },
945
945
  });
946
946
 
947
- // Spin messages — the ends of what one steps through, said without naming
948
- // what it is made of: the same words fit days, months, pages or sizes.
947
+ // Spin messages — the ends of what one steps through when the control holding
948
+ // it has no sentence of its own for a bound (a day or a number says its
949
+ // constraint.min/max one, see picker_spin.jsx): said without naming what it is
950
+ // made of, so they fit pages, sizes or any list of items.
949
951
  naviI18n.addAll({
950
952
  "spin.previous": {
951
953
  en: "Previous",
@@ -10779,6 +10781,42 @@ const urlWithoutSearchAndHash = (url) => {
10779
10781
  return `${urlObject.origin}${urlObject.pathname}`;
10780
10782
  };
10781
10783
 
10784
+ /**
10785
+ * What a component can learn about the page it is rendered in, from the route
10786
+ * container rendering that page (see useContentKeepingLeavingPages in
10787
+ * route.jsx). `null` outside of any route container.
10788
+ *
10789
+ * - `isShown()` — whether the container would still show the page. False as
10790
+ * soon as the address leads elsewhere: the page is about to be taken down, or
10791
+ * hidden until the route transition leaving it is over, and nothing it reads
10792
+ * is worth a render any more. False for good once it is kept while leaving: a
10793
+ * return to its route mounts the page anew. Asked outside any render — by a
10794
+ * subscription deciding whether to re-render — and it reads the routes
10795
+ * untracked, so asking subscribes nobody to them.
10796
+ * - `leavingSignal` — true once the page is hidden and kept while its movement
10797
+ * plays: still mounted, so what it fills outside its own nodes (a slot in a
10798
+ * bar) steps out rather than showing with the page arriving.
10799
+ */
10800
+ const RoutePageContext = createContext(null);
10801
+
10802
+ /**
10803
+ * A container has put its page on screen — or as much of it as it can.
10804
+ *
10805
+ * A route matching is a signal changing, and the page it selects reaches the
10806
+ * DOM only once Preact has rendered — an unknown number of passes later, in an
10807
+ * unknown number of microtasks. Anyone who needs the page as it IS rather than
10808
+ * as it has been decided (a travel about to have its picture taken by the
10809
+ * browser, see route_travel.jsx) waits for this instead of counting.
10810
+ *
10811
+ * A page waiting on data is announced too, by the boundary showing its loading
10812
+ * state (see Loading in use_async_data.jsx): what the container could put on
10813
+ * screen is what the browser is about to take a picture of, and a page that
10814
+ * cannot render yet would otherwise be waited on until the transition dies of
10815
+ * it. It lives in a module of its own for that: the async layer says it as much
10816
+ * as the router does, and neither can import the other.
10817
+ */
10818
+ const [publishRouteRender, observeRouteRender] = createPubSub();
10819
+
10782
10820
  const documentStateSignal = signal(null);
10783
10821
  const useDocumentState = () => {
10784
10822
  return documentStateSignal.value;
@@ -11038,24 +11076,6 @@ const readTopInWindow = element => {
11038
11076
  return element.getBoundingClientRect().top;
11039
11077
  };
11040
11078
 
11041
- /**
11042
- * A container has put its page on screen — or as much of it as it can.
11043
- *
11044
- * A route matching is a signal changing, and the page it selects reaches the
11045
- * DOM only once Preact has rendered — an unknown number of passes later, in an
11046
- * unknown number of microtasks. Anyone who needs the page as it IS rather than
11047
- * as it has been decided (a travel about to have its picture taken by the
11048
- * browser, see route_travel.jsx) waits for this instead of counting.
11049
- *
11050
- * A page waiting on data is announced too, by the boundary showing its loading
11051
- * state (see Loading in use_async_data.jsx): what the container could put on
11052
- * screen is what the browser is about to take a picture of, and a page that
11053
- * cannot render yet would otherwise be waited on until the transition dies of
11054
- * it. It lives in a module of its own for that: the async layer says it as much
11055
- * as the router does, and neither can import the other.
11056
- */
11057
- const [publishRouteRender, observeRouteRender] = createPubSub();
11058
-
11059
11079
  /**
11060
11080
  * Where a page was left, given back when one comes back to it.
11061
11081
  *
@@ -12450,6 +12470,7 @@ browserIntegration.handleActionTask;
12450
12470
 
12451
12471
  const NO_OP = () => {};
12452
12472
  const NO_ID_GIVEN = [undefined, NO_OP, NO_OP];
12473
+ const countRenders$1 = (count) => count + 1;
12453
12474
  // What the computed below answers for a key the document state does not hold:
12454
12475
  // key presence is the flag, and the value under a present key may be anything,
12455
12476
  // undefined included.
@@ -12478,7 +12499,36 @@ const useNavStateBasic = (
12478
12499
  [id],
12479
12500
  );
12480
12501
  const keyValue = keyValueComputed.value;
12481
- const keyInState = keyValue !== KEY_NOT_IN_STATE;
12502
+ // A page the address has left keeps reading the entry it was shown on until
12503
+ // it is taken down (kept hidden while a route transition plays): the new
12504
+ // entry would close what was open there — a Picker as a cancel — where the
12505
+ // same page taken down at once is only unmounted. Its render still happens:
12506
+ // the state is written before the routes say the page is left.
12507
+ const routePage = useContext(RoutePageContext);
12508
+ const keyValueReadRef = useRef(keyValue);
12509
+ const keyValueWithheld =
12510
+ keyValue !== keyValueReadRef.current &&
12511
+ routePage !== null &&
12512
+ !routePage.isShown();
12513
+ if (!keyValueWithheld) {
12514
+ keyValueReadRef.current = keyValue;
12515
+ }
12516
+ const keyValueRead = keyValueReadRef.current;
12517
+ // Owed rather than dropped, as in useAsyncData: the address can come back
12518
+ // before the container renders (a redirect straight back), and the page then
12519
+ // stays on screen.
12520
+ const [, rerender] = useReducer(countRenders$1, 0);
12521
+ useEffect(() => {
12522
+ if (!keyValueWithheld) {
12523
+ return undefined;
12524
+ }
12525
+ return observeRouteRender(() => {
12526
+ if (routePage.isShown()) {
12527
+ rerender();
12528
+ }
12529
+ });
12530
+ }, [keyValueWithheld]);
12531
+ const keyInState = keyValueRead !== KEY_NOT_IN_STATE;
12482
12532
  const onLeaveRef = useRef(onLeave);
12483
12533
  onLeaveRef.current = onLeave;
12484
12534
  const prevKeyInStateRef = useRef(keyInState);
@@ -12508,7 +12558,7 @@ const useNavStateBasic = (
12508
12558
  effectiveType = "replace";
12509
12559
  }
12510
12560
 
12511
- const currentValue = keyInState ? keyValue : defaultValue;
12561
+ const currentValue = keyInState ? keyValueRead : defaultValue;
12512
12562
 
12513
12563
  if (debug) {
12514
12564
  console.debug(`useNavState(${id}) current value is ${currentValue}`);
@@ -12576,6 +12626,8 @@ const useNavStateBasic = (
12576
12626
  /**
12577
12627
  * Stores a named value in the browser's document state and returns it reactively.
12578
12628
  * The component re-renders whenever the value changes (navigation, back/forward button).
12629
+ * In a page the address has left — kept hidden while a route transition plays —
12630
+ * the value stays the one of the entry that page was shown on.
12579
12631
  *
12580
12632
  * @param {string} id
12581
12633
  * Unique key used to store the value in document state. Must be stable across renders.
@@ -19553,18 +19605,17 @@ const MIN_CONSTRAINT = {
19553
19605
  if (cmp === null || cmp >= 0) {
19554
19606
  return null;
19555
19607
  }
19556
-
19557
- return naviI18n("constraint.min.duration.default", {
19558
- min: formatDuration(min),
19559
- });
19608
+ return boundMessage(field, "min");
19560
19609
  }
19561
19610
  if (field.controlType !== "input" && field.controlType !== "picker") {
19562
19611
  return null;
19563
19612
  }
19564
- const minString = field.controlHostProps.min;
19565
- if (!minString) {
19613
+ const min = field.controlHostProps.min;
19614
+ // 0 is a bound like any other: only a bound nobody gave is skipped.
19615
+ if (min === undefined || min === null || min === "") {
19566
19616
  return null;
19567
19617
  }
19618
+ const minString = String(min);
19568
19619
  const type = field.controlHostProps.type;
19569
19620
  const naviInputType = field.controlHostProps["navi-input-type"];
19570
19621
  const valueAsString = uiStateAsText(field.uiState);
@@ -19580,40 +19631,16 @@ const MIN_CONSTRAINT = {
19580
19631
  if (isNaN(numericValue)) {
19581
19632
  return null;
19582
19633
  }
19583
- if (numericValue < minNumber) {
19584
- if (naviInputType === "hour") {
19585
- return naviI18n(`constraint.min.hour.default`, {
19586
- min: minString,
19587
- });
19588
- }
19589
- if (naviInputType === "minute") {
19590
- return naviI18n(`constraint.min.minute.default`, {
19591
- min: minString,
19592
- });
19593
- }
19594
- if (naviInputType === "second") {
19595
- return naviI18n(`constraint.min.second.default`, {
19596
- min: minString,
19597
- });
19598
- }
19599
- if (naviInputType === "percentage") {
19600
- return naviI18n(`constraint.min.percentage.default`, {
19601
- min: minString,
19602
- });
19603
- }
19604
- return naviI18n(`constraint.min.number.default`, {
19605
- min: minString,
19606
- });
19634
+ if (numericValue >= minNumber) {
19635
+ return null;
19607
19636
  }
19608
- return null;
19637
+ return boundMessage(field, "min");
19609
19638
  }
19610
19639
  if (type === "time") {
19611
19640
  const [minHours, minMinutes] = minString.split(":").map(Number);
19612
19641
  const [hours, minutes] = valueAsString.split(":").map(Number);
19613
19642
  if (hours < minHours || (hours === minHours && minutes < minMinutes)) {
19614
- return naviI18n("constraint.min.time.default", {
19615
- min: minString,
19616
- });
19643
+ return boundMessage(field, "min");
19617
19644
  }
19618
19645
  return null;
19619
19646
  }
@@ -19621,18 +19648,13 @@ const MIN_CONSTRAINT = {
19621
19648
  // so they never need a min/max validation message.
19622
19649
  if (DATE_INPUT_TYPE_SET.has(type)) {
19623
19650
  if (valueAsString < minString) {
19624
- const todayIso = getTodayIso(type);
19625
- if (minString === todayIso) {
19626
- return naviI18n("constraint.min.date.today.default");
19627
- }
19628
- return naviI18n("constraint.min.date.default", {
19629
- min: formatDateIso(minString, type),
19630
- });
19651
+ return boundMessage(field, "min");
19631
19652
  }
19632
19653
  return null;
19633
19654
  }
19634
19655
  return null;
19635
19656
  },
19657
+ messageAtBound: (field) => boundMessage(field, "min"),
19636
19658
  };
19637
19659
  CONSTRAINT_ATTRIBUTE_SET.add("min");
19638
19660
 
@@ -19652,18 +19674,17 @@ const MAX_CONSTRAINT = {
19652
19674
  if (cmp === null || cmp <= 0) {
19653
19675
  return null;
19654
19676
  }
19655
-
19656
- return naviI18n("constraint.max.duration.default", {
19657
- max: formatDuration(max),
19658
- });
19677
+ return boundMessage(field, "max");
19659
19678
  }
19660
19679
  if (field.controlType !== "input" && field.controlType !== "picker") {
19661
19680
  return null;
19662
19681
  }
19663
- const maxString = field.controlHostProps.max;
19664
- if (!maxString) {
19682
+ const max = field.controlHostProps.max;
19683
+ // 0 is a bound like any other: only a bound nobody gave is skipped.
19684
+ if (max === undefined || max === null || max === "") {
19665
19685
  return null;
19666
19686
  }
19687
+ const maxString = String(max);
19667
19688
  const type = field.controlHostProps.type;
19668
19689
  const naviInputType = field.controlHostProps["navi-input-type"];
19669
19690
  const valueAsString = uiStateAsText(field.uiState);
@@ -19682,38 +19703,13 @@ const MAX_CONSTRAINT = {
19682
19703
  if (numericValue <= maxNumber) {
19683
19704
  return null;
19684
19705
  }
19685
-
19686
- if (naviInputType === "hour") {
19687
- return naviI18n(`constraint.max.hour.default`, {
19688
- max: maxString,
19689
- });
19690
- }
19691
- if (naviInputType === "minute") {
19692
- return naviI18n(`constraint.max.minute.default`, {
19693
- max: maxString,
19694
- });
19695
- }
19696
- if (naviInputType === "second") {
19697
- return naviI18n(`constraint.max.second.default`, {
19698
- max: maxString,
19699
- });
19700
- }
19701
- if (naviInputType === "percentage") {
19702
- return naviI18n(`constraint.max.percentage.default`, {
19703
- max: maxString,
19704
- });
19705
- }
19706
- return naviI18n(`constraint.max.number.default`, {
19707
- max: maxString,
19708
- });
19706
+ return boundMessage(field, "max");
19709
19707
  }
19710
19708
  if (type === "time") {
19711
19709
  const [maxHours, maxMinutes] = maxString.split(":").map(Number);
19712
19710
  const [hours, minutes] = valueAsString.split(":").map(Number);
19713
19711
  if (hours > maxHours || (hours === maxHours && minutes > maxMinutes)) {
19714
- return naviI18n("constraint.max.time.default", {
19715
- max: maxString,
19716
- });
19712
+ return boundMessage(field, "max");
19717
19713
  }
19718
19714
  return null;
19719
19715
  }
@@ -19721,17 +19717,11 @@ const MAX_CONSTRAINT = {
19721
19717
  if (valueAsString <= maxString) {
19722
19718
  return null;
19723
19719
  }
19724
-
19725
- const todayIso = getTodayIso(type);
19726
- if (maxString === todayIso) {
19727
- return naviI18n("constraint.max.date.today.default");
19728
- }
19729
- return naviI18n("constraint.max.date.default", {
19730
- max: formatDateIso(maxString, type),
19731
- });
19720
+ return boundMessage(field, "max");
19732
19721
  }
19733
19722
  return null;
19734
19723
  },
19724
+ messageAtBound: (field) => boundMessage(field, "max"),
19735
19725
  };
19736
19726
  CONSTRAINT_ATTRIBUTE_SET.add("max");
19737
19727
 
@@ -19926,6 +19916,51 @@ const formatMsToTime = (ms, showSeconds) => {
19926
19916
  return `${hh}:${mm}:${ss}`;
19927
19917
  };
19928
19918
 
19919
+ // What a bound says, whatever the value: the sentence `check` gives a value
19920
+ // past it, and the one a spin gives when its chevron is refused there
19921
+ // (picker_spin.jsx) — one sentence, so the two refuse in the same words. null
19922
+ // for a type that has nothing to say about a bound.
19923
+ const boundMessage = (field, bound) => {
19924
+ const boundValue = field.controlHostProps[bound];
19925
+ if (field.controlType === "duration_group") {
19926
+ return naviI18n(`constraint.${bound}.duration.default`, {
19927
+ [bound]: formatDuration(boundValue),
19928
+ });
19929
+ }
19930
+ const type = field.controlHostProps.type;
19931
+ const naviInputType = field.controlHostProps["navi-input-type"];
19932
+ if (isNumberInput(type, naviInputType)) {
19933
+ const numberKind = NUMBER_BOUND_KIND_SET.has(naviInputType)
19934
+ ? naviInputType
19935
+ : "number";
19936
+ return naviI18n(`constraint.${bound}.${numberKind}.default`, {
19937
+ [bound]: String(boundValue),
19938
+ });
19939
+ }
19940
+ if (type === "time") {
19941
+ return naviI18n(`constraint.${bound}.time.default`, {
19942
+ [bound]: boundValue,
19943
+ });
19944
+ }
19945
+ if (DATE_INPUT_TYPE_SET.has(type)) {
19946
+ if (boundValue === getTodayIso(type)) {
19947
+ return naviI18n(`constraint.${bound}.date.today.default`);
19948
+ }
19949
+ return naviI18n(`constraint.${bound}.date.default`, {
19950
+ [bound]: formatDateIso(boundValue, type),
19951
+ });
19952
+ }
19953
+ return null;
19954
+ };
19955
+ // The numbers whose bound is said with what they count ("Max 23 heures");
19956
+ // any other is "Ce nombre".
19957
+ const NUMBER_BOUND_KIND_SET = new Set([
19958
+ "hour",
19959
+ "minute",
19960
+ "second",
19961
+ "percentage",
19962
+ ]);
19963
+
19929
19964
  const getTodayIso = (inputType) => {
19930
19965
  const now = new Date();
19931
19966
  const yyyy = now.getFullYear();
@@ -28261,24 +28296,6 @@ const TYPE_CONVERTERS = {
28261
28296
  },
28262
28297
  };
28263
28298
 
28264
- /**
28265
- * What a component can learn about the page it is rendered in, from the route
28266
- * container rendering that page (see useContentKeepingLeavingPages in
28267
- * route.jsx). `null` outside of any route container.
28268
- *
28269
- * - `isShown()` — whether the container would still show the page. False as
28270
- * soon as the address leads elsewhere: the page is about to be taken down, or
28271
- * hidden until the route transition leaving it is over, and nothing it reads
28272
- * is worth a render any more. False for good once it is kept while leaving: a
28273
- * return to its route mounts the page anew. Asked outside any render — by a
28274
- * subscription deciding whether to re-render — and it reads the routes
28275
- * untracked, so asking subscribes nobody to them.
28276
- * - `leavingSignal` — true once the page is hidden and kept while its movement
28277
- * plays: still mounted, so what it fills outside its own nodes (a slot in a
28278
- * bar) steps out rather than showing with the page arriving.
28279
- */
28280
- const RoutePageContext = createContext(null);
28281
-
28282
28299
  const promiseStateWeakMap = new WeakMap();
28283
28300
  const usePromiseAsyncData = (
28284
28301
  promise,
@@ -33120,11 +33137,17 @@ const useContentKeepingLeavingPages = (branch, readShownBranch, parentPage) => {
33120
33137
  }
33121
33138
  }
33122
33139
  });
33140
+ // Also run when a Suspense boundary above parks this container — a page
33141
+ // arriving that suspends to a `<Loading>` around both pages: every hook of
33142
+ // the pages kept while leaving has then been cleaned up, which is all that
33143
+ // keeping them saved. Dropped, so the render resuming the container takes
33144
+ // them down, rather than re-running every effect of a hidden page.
33123
33145
  useLayoutEffect(() => {
33124
33146
  return () => {
33125
33147
  for (const entry of leavingRef.current) {
33126
33148
  entry.hold.containers.delete(rerender);
33127
33149
  }
33150
+ leavingRef.current = [];
33128
33151
  };
33129
33152
  }, []);
33130
33153
  if (leaving.length === 0) {
@@ -80965,6 +80988,8 @@ const TRAVELS_NONE = [];
80965
80988
  * name?: string,
80966
80989
  * min?: any,
80967
80990
  * max?: any,
80991
+ * minMessage?: any,
80992
+ * maxMessage?: any,
80968
80993
  * step?: number,
80969
80994
  * type?: string,
80970
80995
  * editable?: boolean,
@@ -81003,7 +81028,15 @@ const TRAVELS_NONE = [];
81003
81028
  * @param {number} [step=1] How many steps a press covers.
81004
81029
  * @param {number} [duration=250] How long a travel takes, in milliseconds.
81005
81030
  * @param {any} [min] The first value one can reach; `max` is the last. Beyond
81006
- * them the travel simply does not happen and the chevron that way says so.
81031
+ * them the travel simply does not happen and the chevron that way says so,
81032
+ * in the words the control in the middle uses for a value past that end ("La
81033
+ * date doit être au plus tard le 2 avril 2027", "Max 5"). A value the control
81034
+ * has no such sentence for — what a plain `Spin` steps through — is "nothing
81035
+ * after this one".
81036
+ * @param {any} [maxMessage] What is said at `max` instead: by the chevron
81037
+ * refused there, by a travel asked past it, and by the control in the middle
81038
+ * for a value past it (picked in the calendar, typed). `minMessage` is its
81039
+ * twin at `min`.
81007
81040
  * @param {string} [padding] The room around the value, `paddingX`/`paddingY`
81008
81041
  * and the four sides included. It goes on what is inside the box rather than
81009
81042
  * on the box: above and below it is taken by the value AND by the two
@@ -81033,6 +81066,8 @@ const Spin = ({
81033
81066
  name,
81034
81067
  min,
81035
81068
  max,
81069
+ minMessage,
81070
+ maxMessage,
81036
81071
  step = 1,
81037
81072
  duration = 250,
81038
81073
  type = "text",
@@ -81158,15 +81193,31 @@ const Spin = ({
81158
81193
  const readOnlyResolved = Boolean(readOnly || readOnlyFromAbove || loadingResolved);
81159
81194
  const disabledResolved = Boolean(disabled || disabledFromAbove);
81160
81195
 
81196
+ // The end of what one may reach, said the way the control in the middle
81197
+ // says it about a value past that end — its `minMessage`/`maxMessage`, or the
81198
+ // sentence its type has for the bound — so a chevron, the calendar and a
81199
+ // typed value refuse in the same words. Asked of the control at the press:
81200
+ // it is not there yet on the first render. `goingNext` is towards the bigger
81201
+ // values, whichever end of the box that is.
81202
+ const boundMessage = goingNext => {
81203
+ const constraint = goingNext ? MAX_CONSTRAINT : MIN_CONSTRAINT;
81204
+ const controlEl = controlRef.current;
81205
+ const controller = (findControlHost(controlEl) || controlEl).__uiStateController__;
81206
+ const {
81207
+ message
81208
+ } = getConstraintMessage(controller, constraint, constraint.messageAtBound(controller), {});
81209
+ return message ?? naviI18n(goingNext ? "spin.nothing_after" : "spin.nothing_before");
81210
+ };
81211
+
81161
81212
  // Why a chevron refuses, in its own words — and only when the reason is
81162
81213
  // ours: the end of what one may reach is something this control knows and
81163
81214
  // navi cannot guess. For everything else the reason belongs to the state the
81164
81215
  // whole control is in, and it is said about the control ("read-only",
81165
81216
  // "busy") rather than about the button, which is not what the user was
81166
81217
  // pressing.
81167
- const wayOutMessage = (allowed, endKey) => {
81218
+ const wayOutMessage = (allowed, isNext) => {
81168
81219
  if (!allowed) {
81169
- return naviI18n(endKey);
81220
+ return boundMessage(isNext);
81170
81221
  }
81171
81222
  if (loadingResolved) {
81172
81223
  return naviI18n("constraint.busy.default");
@@ -81278,7 +81329,7 @@ const Spin = ({
81278
81329
  // answer belongs.
81279
81330
  e.preventDefault();
81280
81331
  e.stopPropagation();
81281
- openCallout(naviI18n(goingNext ? "spin.nothing_after" : "spin.nothing_before"), {
81332
+ openCallout(boundMessage(goingNext), {
81282
81333
  anchorElement: middleRef.current,
81283
81334
  status: "info",
81284
81335
  openingEvent: e.detail.event ?? e
@@ -81337,12 +81388,14 @@ const Spin = ({
81337
81388
  const valueDrawnAtEnd = travelPending?.area === "end" ? travelPending.value : valueAtEnd;
81338
81389
  const wayOut = atStart => {
81339
81390
  const isNext = atStart ? startIsNext : !startIsNext;
81391
+ const allowed = atStart ? startAllowed : endAllowed;
81340
81392
  return jsx(WayOut, {
81341
81393
  atStart: atStart,
81342
81394
  onPointerDown: e => {
81343
81395
  wayOutPointerTypeRef.current = e.pointerType;
81344
81396
  },
81345
- unavailableMessage: wayOutMessage(atStart ? startAllowed : endAllowed, isNext ? "spin.nothing_after" : "spin.nothing_before"),
81397
+ unavailable: !allowed || loadingResolved || readOnlyResolved,
81398
+ unavailableMessage: () => wayOutMessage(allowed, isNext),
81346
81399
  label: isNext ? nextLabel ?? naviI18n("spin.next") : previousLabel ?? naviI18n("spin.previous"),
81347
81400
  onPress: e => {
81348
81401
  focusMiddle();
@@ -81389,6 +81442,8 @@ const Spin = ({
81389
81442
  ...valueProps,
81390
81443
  min: min,
81391
81444
  max: max,
81445
+ minMessage: minMessage,
81446
+ maxMessage: maxMessage,
81392
81447
  step: step,
81393
81448
  readOnly: readOnly,
81394
81449
  disabled: disabled,
@@ -81422,6 +81477,8 @@ const Spin = ({
81422
81477
  ...valueProps,
81423
81478
  min: min,
81424
81479
  max: max,
81480
+ minMessage: minMessage,
81481
+ maxMessage: maxMessage,
81425
81482
  readOnly: readOnly,
81426
81483
  disabled: disabled,
81427
81484
  loading: loading,
@@ -81532,6 +81589,7 @@ const Spin = ({
81532
81589
  const WayOut = ({
81533
81590
  atStart,
81534
81591
  commandFor,
81592
+ unavailable,
81535
81593
  unavailableMessage,
81536
81594
  label,
81537
81595
  onPress,
@@ -81542,10 +81600,10 @@ const WayOut = ({
81542
81600
  // onPointerUp below).
81543
81601
  const pointerDownRef = useRef(null);
81544
81602
  const press = e => {
81545
- if (unavailableMessage) {
81603
+ if (unavailable) {
81546
81604
  // Why it does nothing, said where one pressed: a control would have
81547
81605
  // done this through its own interaction gate, and this one has none.
81548
- openCallout(unavailableMessage, {
81606
+ openCallout(unavailableMessage(), {
81549
81607
  anchorElement: e.currentTarget,
81550
81608
  status: "info",
81551
81609
  openingEvent: e
@@ -81572,8 +81630,8 @@ const WayOut = ({
81572
81630
  ,
81573
81631
  role: "button",
81574
81632
  "aria-label": label,
81575
- "aria-disabled": unavailableMessage ? "true" : undefined,
81576
- "data-unavailable": unavailableMessage ? "" : undefined
81633
+ "aria-disabled": unavailable ? "true" : undefined,
81634
+ "data-unavailable": unavailable ? "" : undefined
81577
81635
  // At the chevron, not beside it: a callout aims its arrow at where the
81578
81636
  // anchor's text starts, and there is no text here — only a glyph in the
81579
81637
  // middle of the box, which is what one pressed and what the answer is
@@ -81806,6 +81864,8 @@ SpinGroup.Separator = SpinGroupSeparator;
81806
81864
  * defaultValue?: number|string,
81807
81865
  * min?: number,
81808
81866
  * max?: number,
81867
+ * minMessage?: any,
81868
+ * maxMessage?: any,
81809
81869
  * step?: number,
81810
81870
  * pad?: number,
81811
81871
  * loop?: boolean,
@@ -81911,6 +81971,8 @@ const numberAtStep = (value, count, {
81911
81971
  * name?: string,
81912
81972
  * min?: string,
81913
81973
  * max?: string,
81974
+ * minMessage?: any,
81975
+ * maxMessage?: any,
81914
81976
  * step?: number,
81915
81977
  * lang?: string,
81916
81978
  * renderDay?: (day: string) => import("ignore:preact").ComponentChildren,