@jsenv/navi 0.29.353 → 0.29.354

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.
@@ -636,6 +636,11 @@ naviI18n.addAll({
636
636
  en: ":",
637
637
  fr: "h",
638
638
  },
639
+ // A time of whole hours has nothing to stand between: what follows the hour.
640
+ "time.hour_suffix": {
641
+ en: "",
642
+ fr: "h",
643
+ },
639
644
  "time.hour_label": {
640
645
  en: "Hours",
641
646
  fr: "Heures",
@@ -4708,14 +4713,12 @@ const minutesFromTime$1 = (time) => {
4708
4713
  return parts.hour * 60 + parts.minute;
4709
4714
  };
4710
4715
 
4716
+ // Not folded back into the day: 1440 is "24:00", the end of the day a span can
4717
+ // run into.
4711
4718
  const timeFromMinutes = (minutes) => {
4712
- const inDay =
4713
- ((minutes % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY;
4714
- return `${padTwo$1(Math.floor(inDay / 60))}:${padTwo$1(inDay % 60)}`;
4719
+ return `${padTwo$1(Math.floor(minutes / 60))}:${padTwo$1(minutes % 60)}`;
4715
4720
  };
4716
4721
 
4717
- const MINUTES_PER_DAY = 24 * 60;
4718
-
4719
4722
  const padTwo$1 = (value) => String(value).padStart(2, "0");
4720
4723
 
4721
4724
  // Maps validity type names → navi input type names.
@@ -5551,6 +5554,14 @@ const constraintAttributeFromProp = (key) => {
5551
5554
  const isConstraintAttributeOn = (value) =>
5552
5555
  value !== undefined && value !== null && value !== false;
5553
5556
 
5557
+ /**
5558
+ * Whether a constraint attribute asks for the value to be CORRECTED rather
5559
+ * than refused: `singleSpace="autoFix"`. Only constraints whose rule knows a
5560
+ * correction offer it, and only the fields that write it get one — a
5561
+ * `maxLength` silently truncating what someone wrote would be a bad default.
5562
+ */
5563
+ const isConstraintAttributeAutoFix = (value) => value === "autoFix";
5564
+
5554
5565
  const CONSTRAINT_NAME_TO_PROP = {
5555
5566
  disabled: "disabledMessage",
5556
5567
  required: "requiredMessage",
@@ -10298,14 +10309,13 @@ const tryActionAfterInteractionAllowed = (
10298
10309
 
10299
10310
  // Resolve proxy so navi_action_* fires on the real control element.
10300
10311
  let elementForAction = controlHost;
10301
- let uiState;
10312
+ let activeController = controller;
10302
10313
  if (controller) {
10303
10314
  const proxyTargetController = findControlProxyTargetController(controller);
10304
10315
  if (proxyTargetController) {
10305
10316
  elementForAction = proxyTargetController.ref.current;
10317
+ activeController = proxyTargetController;
10306
10318
  }
10307
- const activeController = proxyTargetController ?? controller;
10308
- uiState = activeController?.uiState;
10309
10319
  }
10310
10320
 
10311
10321
  // Validity gate: re-check (handles autoResetOnAction side effects), then read
@@ -10332,6 +10342,11 @@ const tryActionAfterInteractionAllowed = (
10332
10342
  }
10333
10343
  }
10334
10344
 
10345
+ // Read after the gate, never before: a constraint allowed to correct the
10346
+ // value rewrites it in there (see applyAutoFix), and what goes out has to be
10347
+ // what the control ends up holding.
10348
+ const uiState = activeController?.uiState;
10349
+
10335
10350
  if (action === "auto" || action?.isAction) {
10336
10351
  // A control that commits gets the last word on whether this particular
10337
10352
  // value is worth acting on — see Form's own `shouldRequestAction`, which
@@ -12642,9 +12657,22 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-same-as");
12642
12657
  * `data-single-space` — no leading or trailing space, never two in a row.
12643
12658
  * The rule itself is @jsenv/validity's SINGLE_SPACE_RULE, so a server checking
12644
12659
  * the value again refuses it for the same reason and in the same words.
12660
+ *
12661
+ * `data-single-space="autoFix"` corrects the value instead of refusing it. A
12662
+ * text field ends with a space more often than anyone means it to — a word
12663
+ * then a pause, a mobile keyboard after a suggestion, a paste — and the person
12664
+ * is then asked to find and delete a character they cannot see. The correction
12665
+ * is the rule's own, so the value a server re-checks with `singleSpace: true`
12666
+ * is one it accepts.
12645
12667
  */
12646
12668
 
12647
12669
 
12670
+ const applyRule = (field) => {
12671
+ const valueAsString =
12672
+ field.uiState === undefined ? "" : String(field.uiState);
12673
+ return SINGLE_SPACE_RULE.applyOn(true, valueAsString);
12674
+ };
12675
+
12648
12676
  const SINGLE_SPACE_CONSTRAINT = {
12649
12677
  name: "single_space",
12650
12678
  messageAttribute: "data-single-space-message",
@@ -12653,14 +12681,29 @@ const SINGLE_SPACE_CONSTRAINT = {
12653
12681
  if (!isConstraintAttributeOn(singleSpace)) {
12654
12682
  return null;
12655
12683
  }
12656
- const valueAsString =
12657
- field.uiState === undefined ? "" : String(field.uiState);
12658
- const result = SINGLE_SPACE_RULE.applyOn(true, valueAsString);
12684
+ if (isConstraintAttributeAutoFix(singleSpace)) {
12685
+ // The correction the rule knows always lands on a value the rule
12686
+ // accepts, and it runs on every commit — so there is nothing left for
12687
+ // the person to do about this, and nothing to say to them.
12688
+ return null;
12689
+ }
12690
+ const result = applyRule(field);
12659
12691
  if (!result) {
12660
12692
  return null;
12661
12693
  }
12662
12694
  return naviI18nFromValidityMessage(result);
12663
12695
  },
12696
+ autoFix: (field) => {
12697
+ const singleSpace = field.controlHostProps["data-single-space"];
12698
+ if (!isConstraintAttributeAutoFix(singleSpace)) {
12699
+ return null;
12700
+ }
12701
+ const result = applyRule(field);
12702
+ if (!result) {
12703
+ return null;
12704
+ }
12705
+ return result.autoFix();
12706
+ },
12664
12707
  };
12665
12708
  CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
12666
12709
 
@@ -12852,6 +12895,74 @@ const createControlValidation = (
12852
12895
  const getConstraintValidityState = () => constraintValidityState;
12853
12896
  controlValidity.getConstraintValidityState = getConstraintValidityState;
12854
12897
 
12898
+ const getConstraintSet = () => {
12899
+ const constraintSet = new Set([
12900
+ ...DEFAULT_CONSTRAINT_SET,
12901
+ ...dynamicConstraintSet,
12902
+ ]);
12903
+ // An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
12904
+ // Read from the raw props on every check so a constraint whose parameters
12905
+ // are closed over is re-created freely, and last so the constraints navi
12906
+ // ships are the ones reported first (see pickConstraintFailureInfo).
12907
+ const constraintsFromProps = controller.props.constraints;
12908
+ if (constraintsFromProps) {
12909
+ for (const constraintFromProps of constraintsFromProps) {
12910
+ constraintSet.add(normalizeConstraint(constraintFromProps));
12911
+ }
12912
+ }
12913
+ return constraintSet;
12914
+ };
12915
+
12916
+ /**
12917
+ * Lets the constraints allowed to correct the value do so, and puts what
12918
+ * they return in the control — ui state, bound signal, the field itself.
12919
+ *
12920
+ * Called at the moments a value is COMMITTED: the field is left, an action
12921
+ * is about to read it. Never while it is being typed into, where removing
12922
+ * the trailing space would eat the one the person is about to follow with a
12923
+ * word.
12924
+ *
12925
+ * Returns whether the value moved, so a caller outside the validity pass can
12926
+ * re-read what the control is now worth.
12927
+ */
12928
+ const applyAutoFix = (event) => {
12929
+ const proxyTargetController = findControlProxyTargetController(controller);
12930
+ if (proxyTargetController) {
12931
+ return proxyTargetController.rules.validation.applyAutoFix(event);
12932
+ }
12933
+ // A value nobody can edit is not navi's to rewrite: it is the app's, and
12934
+ // correcting it would change what gets sent behind the app's back.
12935
+ const controlHostProps = controller.controlHostProps;
12936
+ if (
12937
+ controlHostProps.disabled ||
12938
+ controlHostProps.readOnly ||
12939
+ controlHostProps["aria-readonly"] === "true"
12940
+ ) {
12941
+ return false;
12942
+ }
12943
+ let fixed = false;
12944
+ for (const constraint of getConstraintSet()) {
12945
+ if (!constraint.autoFix) {
12946
+ continue;
12947
+ }
12948
+ const fixedValue = constraint.autoFix(controller);
12949
+ if (fixedValue === null || fixedValue === undefined) {
12950
+ continue;
12951
+ }
12952
+ if (compareTwoJsValues(fixedValue, controller.uiState)) {
12953
+ continue;
12954
+ }
12955
+ const autoFixEvent = new CustomEvent("auto_fix", {
12956
+ detail: { constraint: constraint.name },
12957
+ });
12958
+ chainEvent(autoFixEvent, event);
12959
+ controller.setUIState(fixedValue, autoFixEvent);
12960
+ fixed = true;
12961
+ }
12962
+ return fixed;
12963
+ };
12964
+ controlValidity.applyAutoFix = applyAutoFix;
12965
+
12855
12966
  const checkValidity = ({
12856
12967
  event,
12857
12968
  requester = controller.ref.current,
@@ -12892,21 +13003,15 @@ const createControlValidation = (
12892
13003
  }
12893
13004
  }
12894
13005
 
12895
- let newConstraintValidityState = { valid: true };
12896
- const constraintSet = new Set([
12897
- ...DEFAULT_CONSTRAINT_SET,
12898
- ...dynamicConstraintSet,
12899
- ]);
12900
- // An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
12901
- // Read from the raw props on every check so a constraint whose parameters
12902
- // are closed over is re-created freely, and last so the constraints navi
12903
- // ships are the ones reported first (see pickConstraintFailureInfo).
12904
- const constraintsFromProps = controller.props.constraints;
12905
- if (constraintsFromProps) {
12906
- for (const constraintFromProps of constraintsFromProps) {
12907
- constraintSet.add(normalizeConstraint(constraintFromProps));
12908
- }
13006
+ if (fromRequestAction) {
13007
+ // The value is about to be read and sent: whoever may correct it gets
13008
+ // the last word before the constraints judge it, so a field is never
13009
+ // refused for something navi knows how to put right.
13010
+ applyAutoFix(event);
12909
13011
  }
13012
+
13013
+ let newConstraintValidityState = { valid: true };
13014
+ const constraintSet = getConstraintSet();
12910
13015
  const elementSig = getElementSignature(controller.ref.current);
12911
13016
  // Not logged: every control checks its constraints on every interaction and
12912
13017
  // almost always passes, so this line alone was most of the debug output —
@@ -36907,7 +37012,8 @@ const useUIStateController = (
36907
37012
  }
36908
37013
  if (
36909
37014
  e.type === "facade_propagate_up" ||
36910
- e.type === "cancel_rollback"
37015
+ e.type === "cancel_rollback" ||
37016
+ e.type === "auto_fix"
36911
37017
  ) {
36912
37018
  // Exception: when the facade propagates a child state change up to the
36913
37019
  // real picker input, also notify the parent group (e.g. Form) so it
@@ -36917,6 +37023,9 @@ const useUIStateController = (
36917
37023
  // A cancel takes the same road back: the Form was told what the
36918
37024
  // popup was picking, so it has to be told the picker went back to
36919
37025
  // where it opened, or it sends a value the user said no to.
37026
+ // A correction is the same story once more: the Form sends what
37027
+ // its fields add up to, and a field that just put its own value
37028
+ // right has to be counted for the corrected one.
36920
37029
  s.parentUIStateController?.onChildUIAction(controller, e, {
36921
37030
  stateChanged: true,
36922
37031
  });
@@ -38758,6 +38867,11 @@ const INTERNAL_EVENT_SET = new Set([
38758
38867
  // notification below still happen, exactly as they did on the way in (see
38759
38868
  // picker_custom.jsx's onClose).
38760
38869
  "cancel_rollback",
38870
+ // A constraint allowed to correct the value put it right as the value was
38871
+ // committed (see applyAutoFix). Nobody pressed anything, so no command and
38872
+ // no action of the control's own — but what it holds really did move, so
38873
+ // uiAction, the bound signal and the parent notification below all happen.
38874
+ "auto_fix",
38761
38875
  ]);
38762
38876
  const isInternalEvent = (e) => {
38763
38877
  return INTERNAL_EVENT_SET.has(e.type);
@@ -39781,6 +39895,24 @@ const useControlProps = (props, {
39781
39895
  syncDomState(readControlValue(el), e);
39782
39896
  };
39783
39897
  }
39898
+ // Leaving the field is one of the two moments a value is committed — the
39899
+ // other being an action about to read it (see applyAutoFix). A constraint
39900
+ // allowed to correct the value puts it right here, so what the field
39901
+ // shows, what the counter counts and what a submit would send are one
39902
+ // thing well before the submit.
39903
+ if (controlType === "input") {
39904
+ const onBlurFromProps = controlHostProps.onBlur;
39905
+ controlHostProps.onBlur = e => {
39906
+ onBlurFromProps?.(e);
39907
+ const validation = uiStateController.rules.validation;
39908
+ if (validation.applyAutoFix(e)) {
39909
+ // The value moved without anyone typing: what the constraints had to
39910
+ // say about the old one is out of date, and so is what the controls
39911
+ // above read from this one.
39912
+ validation.syncValidity(e);
39913
+ }
39914
+ };
39915
+ }
39784
39916
  }
39785
39917
  const uiState = uiStateController.uiStateSignal.peek();
39786
39918
  const domProps = toDomProps(uiState);
@@ -79071,8 +79203,10 @@ const css$l = /* css */`.navi_time_range_label {
79071
79203
  }
79072
79204
  `;
79073
79205
  const HOUR_COUNT = 24;
79206
+ const END_OF_DAY_HOUR = 24;
79074
79207
  const MINUTES_PER_HOUR = 60;
79075
79208
  const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
79209
+ const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
79076
79210
 
79077
79211
  /**
79078
79212
  * @type {import("ignore:preact").FunctionComponent<{
@@ -79092,16 +79226,21 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
79092
79226
  * }>}
79093
79227
  * @param {string} [value] The time shown, as "HH:MM".
79094
79228
  * @param {number} [minuteStep=1] How many minutes apart the values on the
79095
- * minute wheel are — 15 for quarters of an hour.
79229
+ * minute wheel are — 15 for quarters of an hour. At 60 the time is a whole
79230
+ * hour: there is no minute wheel at all, the value stays "HH:MM" ("08:00"),
79231
+ * and a value arriving with minutes is shown on its hour.
79096
79232
  * @param {{min?: number, max?: number}|number[]} [hours] Which hours the wheel
79097
79233
  * offers: `{ min: 7, max: 21 }` for a day that starts and ends somewhere, or
79098
- * the list itself. All 24 by default. Rows nobody will ever land on are rows
79099
- * in the way.
79234
+ * the list itself. 0 to 23 by default. Rows nobody will ever land on are rows
79235
+ * in the way. 24 is the end of the day ("24:00", midnight at the end of a
79236
+ * span) and has no minutes: turned onto it, the minute wheel goes back to 0.
79100
79237
  * @param {boolean} [loop=true] The wheels go round: 23h then 0h, 59 minutes
79101
79238
  * then 0. What a clock does. Say `loop={false}` for two ends one cannot turn
79102
79239
  * past.
79103
79240
  * @param {import("ignore:preact").ComponentChildren} [separator] What is written
79104
- * between the hours and the minutes. "h" in French, ":" elsewhere.
79241
+ * between the hours and the minutes. "h" in French, ":" elsewhere. In a time
79242
+ * of whole hours it follows each hour on its row instead ("8h"), and nothing
79243
+ * is written by default outside French.
79105
79244
  * @param {string} [placeholder] What the wheels show while the time holds
79106
79245
  * nothing, as "HH:MM". Wheels have no blank row to land on, so their
79107
79246
  * placeholder is a position rather than a grey word — shown, but not an
@@ -79116,13 +79255,20 @@ const TimeWheel = ({
79116
79255
  hours,
79117
79256
  loop = true,
79118
79257
  placeholder,
79119
- separator = naviI18n("time.hour_separator"),
79258
+ separator,
79120
79259
  hourLabel = naviI18n("time.hour_label"),
79121
79260
  minuteLabel = naviI18n("time.minute_label"),
79122
79261
  size,
79123
79262
  wheelProps,
79263
+ onnavi_wheel_settle,
79124
79264
  ...rest
79125
79265
  }) => {
79266
+ const wholeHours = minuteStep >= MINUTES_PER_HOUR;
79267
+ if (separator === undefined) {
79268
+ separator = naviI18n(wholeHours ? "time.hour_suffix" : "time.hour_separator");
79269
+ }
79270
+ const hourWheelRef = useRef(null);
79271
+ const minuteWheelRef = useRef(null);
79126
79272
  const minutes = useMemo(() => {
79127
79273
  const minuteList = [];
79128
79274
  let minute = 0;
@@ -79136,13 +79282,42 @@ const TimeWheel = ({
79136
79282
  const {
79137
79283
  aggregateChildStates,
79138
79284
  distributeChildUIState
79139
- } = useAnswered(placeholder, rest, aggregateTime, distributeTime);
79285
+ } = useAnswered(placeholder, rest, aggregateTime, (groupState, child) => distributeTime(groupState, child, minuteStep));
79140
79286
  const placeholderParts = parseTimeParts(placeholder);
79287
+
79288
+ // The end of the day has no minutes. The time already reads "24:00" whatever
79289
+ // the minute wheel shows (see aggregateTime); on settle the wheel is brought
79290
+ // back to 0 so what is drawn is what is held.
79291
+ const endOfDayHasNoMinutes = e => {
79292
+ const hourEl = hourWheelRef.current;
79293
+ const minuteEl = minuteWheelRef.current;
79294
+ if (!hourEl || !minuteEl) {
79295
+ return;
79296
+ }
79297
+ if (getUIStateFromElement(hourEl) !== END_OF_DAY_HOUR) {
79298
+ return;
79299
+ }
79300
+ if (getUIStateFromElement(minuteEl) === 0) {
79301
+ return;
79302
+ }
79303
+ dispatchRequestSetUIState(minuteEl, 0, {
79304
+ event: e
79305
+ });
79306
+ };
79141
79307
  return jsxs(WheelGroup, {
79142
79308
  aggregateChildStates: aggregateChildStates,
79143
79309
  distributeChildUIState: distributeChildUIState,
79310
+ onnavi_wheel_settle: e => {
79311
+ if (!wholeHours) {
79312
+ endOfDayHasNoMinutes(e);
79313
+ }
79314
+ if (onnavi_wheel_settle) {
79315
+ onnavi_wheel_settle(e);
79316
+ }
79317
+ },
79144
79318
  ...rest,
79145
79319
  children: [jsx(Wheel, {
79320
+ ref: hourWheelRef,
79146
79321
  name: "hour",
79147
79322
  type: "integer",
79148
79323
  bounded: !loop,
@@ -79153,24 +79328,29 @@ const TimeWheel = ({
79153
79328
  children: hourList.map(hour => jsx(Wheel.Item, {
79154
79329
  value: hour,
79155
79330
  paddingX: "s",
79156
- children: padTwo(hour)
79331
+ children: wholeHours ? jsxs(Fragment, {
79332
+ children: [hour, separator]
79333
+ }) : padTwo(hour)
79157
79334
  }, hour))
79158
- }), jsx(WheelGroup.Separator, {
79159
- size: size,
79160
- children: separator
79161
- }), jsx(Wheel, {
79162
- name: "minute",
79163
- type: "integer",
79164
- bounded: !loop,
79165
- size: size,
79166
- "aria-label": minuteLabel,
79167
- defaultValue: placeholderParts ? placeholderParts.minute : undefined,
79168
- ...wheelProps,
79169
- children: minutes.map(minute => jsx(Wheel.Item, {
79170
- value: minute,
79171
- paddingX: "s",
79172
- children: padTwo(minute)
79173
- }, minute))
79335
+ }), wholeHours ? null : jsxs(Fragment, {
79336
+ children: [jsx(WheelGroup.Separator, {
79337
+ size: size,
79338
+ children: separator
79339
+ }), jsx(Wheel, {
79340
+ ref: minuteWheelRef,
79341
+ name: "minute",
79342
+ type: "integer",
79343
+ bounded: !loop,
79344
+ size: size,
79345
+ "aria-label": minuteLabel,
79346
+ defaultValue: placeholderParts ? floorToStep(placeholderParts.minute, minuteStep) : undefined,
79347
+ ...wheelProps,
79348
+ children: minutes.map(minute => jsx(Wheel.Item, {
79349
+ value: minute,
79350
+ paddingX: "s",
79351
+ children: padTwo(minute)
79352
+ }, minute))
79353
+ })]
79174
79354
  })]
79175
79355
  });
79176
79356
  };
@@ -79204,11 +79384,12 @@ const TimeWheel = ({
79204
79384
  * prepositions belong there; the group is still a row, and takes `flexWrap`
79205
79385
  * for screens too narrow to hold both columns.
79206
79386
  * @param {number} [minuteStep=1] How many minutes apart the values on both
79207
- * minute wheels are.
79387
+ * minute wheels are. 60 for a span of whole hours ("de 8h à 12h").
79208
79388
  * @param {{min?: number, max?: number}|number[]} [hours] Which hours both
79209
- * wheels offer — see `TimeWheel`.
79389
+ * wheels offer — see `TimeWheel`. A 24 is offered to the end alone: a span
79390
+ * can run until midnight ("24:00"), it cannot start there.
79210
79391
  * @param {number} [minDuration=0] How long the span must last at least, in
79211
- * minutes. Zero by default: a span of no length is a span all the same, only
79392
+ * minutes, rounded up to the step — the wheels have nothing in between. Zero by default: a span of no length is a span all the same, only
79212
79393
  * one that goes backwards is not. It is what the bounds keep between them as
79213
79394
  * they turn — turn the start into the end and the end moves along, keeping
79214
79395
  * that much room.
@@ -79241,6 +79422,12 @@ const TimeRangeWheel = ({
79241
79422
  const startId = useId();
79242
79423
  const startRef = useRef(null);
79243
79424
  const endRef = useRef(null);
79425
+ const endHourList = useMemo(() => resolveHourList(hours), [hours ? hours.min : undefined, hours ? hours.max : undefined, hours]);
79426
+ const startHourList = useMemo(() => endHourList.filter(hour => hour !== END_OF_DAY_HOUR), [endHourList]);
79427
+ const step = minuteStep >= MINUTES_PER_HOUR ? MINUTES_PER_HOUR : minuteStep;
79428
+ const minGap = ceilToStep(minDuration, step);
79429
+ // The latest the end can be pushed to: the last time its wheels can show.
79430
+ const lastEnd = endHourList.includes(END_OF_DAY_HOUR) ? END_OF_DAY : floorToStep(LAST_MINUTE_OF_DAY, step);
79244
79431
  // One turn settles the whole span: a start somebody chose makes the end an
79245
79432
  // answer too, left where the placeholder put it.
79246
79433
  const {
@@ -79272,18 +79459,18 @@ const TimeRangeWheel = ({
79272
79459
  return;
79273
79460
  }
79274
79461
  const duration = movedSide === "start" ? otherMinutes - movedMinutes : movedMinutes - otherMinutes;
79275
- if (duration >= minDuration) {
79462
+ if (duration >= minGap) {
79276
79463
  return;
79277
79464
  }
79278
- let pushedMinutes = movedSide === "start" ? movedMinutes + minDuration : movedMinutes - minDuration;
79465
+ let pushedMinutes = movedSide === "start" ? movedMinutes + minGap : movedMinutes - minGap;
79279
79466
  // The day has ends the wheels do not: pushed past midnight, the other bound
79280
79467
  // would come back round on the wrong side of the one that pushed it. It
79281
79468
  // stops at the edge instead, and the span that no longer fits is what the
79282
79469
  // send-time constraint is there to say (see time_range_constraint.js).
79283
79470
  if (pushedMinutes < 0) {
79284
79471
  pushedMinutes = 0;
79285
- } else if (pushedMinutes > LAST_MINUTE_OF_DAY) {
79286
- pushedMinutes = LAST_MINUTE_OF_DAY;
79472
+ } else if (pushedMinutes > lastEnd) {
79473
+ pushedMinutes = lastEnd;
79287
79474
  }
79288
79475
  dispatchRequestSetUIState(otherEl, timeFromMinutes(pushedMinutes), {
79289
79476
  event: e
@@ -79311,7 +79498,7 @@ const TimeRangeWheel = ({
79311
79498
  ref: startRef,
79312
79499
  name: "start",
79313
79500
  minuteStep: minuteStep,
79314
- hours: hours,
79501
+ hours: startHourList,
79315
79502
  loop: loop,
79316
79503
  size: size,
79317
79504
  placeholder: placeholder ? placeholder.start : undefined
@@ -79336,7 +79523,7 @@ const TimeRangeWheel = ({
79336
79523
  ref: endRef,
79337
79524
  name: "end",
79338
79525
  minuteStep: minuteStep,
79339
- hours: hours,
79526
+ hours: endHourList,
79340
79527
  loop: loop,
79341
79528
  size: size,
79342
79529
  placeholder: placeholder ? placeholder.end : undefined,
@@ -79348,7 +79535,7 @@ const TimeRangeWheel = ({
79348
79535
  // the time one would have to move is (see time_range_constraint.js).
79349
79536
  ,
79350
79537
  "data-time-after": startId,
79351
- "data-time-min-duration": minDuration,
79538
+ "data-time-min-duration": minGap,
79352
79539
  ...timeProps,
79353
79540
  ...endTimeProps
79354
79541
  })
@@ -79510,10 +79697,11 @@ const distributeSpan = (groupState, childUIStateController) => {
79510
79697
  return groupState[childUIStateController.name];
79511
79698
  };
79512
79699
 
79513
- // The two wheels as one value, "HH:MM".
79700
+ // The wheels as one value, "HH:MM". A time of whole hours has no minute wheel
79701
+ // and is on the hour; so is the end of the day, whatever the minute wheel says.
79514
79702
  const aggregateTime = childUIStateControllers => {
79515
79703
  let hour = "";
79516
- let minute = "";
79704
+ let minute = 0;
79517
79705
  for (const child of childUIStateControllers) {
79518
79706
  if (child.name === "hour") {
79519
79707
  hour = child.uiState ?? "";
@@ -79522,18 +79710,27 @@ const aggregateTime = childUIStateControllers => {
79522
79710
  minute = child.uiState ?? "";
79523
79711
  }
79524
79712
  }
79713
+ if (hour === END_OF_DAY_HOUR) {
79714
+ return formatTimeParts(hour, 0);
79715
+ }
79525
79716
  return formatTimeParts(hour, minute);
79526
79717
  };
79527
79718
 
79528
79719
  // The way back: what the group is set to (a value given to it, a form being
79529
- // reset, the other bound pushing it) lands on the wheel it belongs to.
79530
- const distributeTime = (groupState, childUIStateController) => {
79720
+ // reset, the other bound pushing it) lands on the wheel it belongs to. Minutes
79721
+ // the wheel does not offer land on the one before.
79722
+ const distributeTime = (groupState, childUIStateController, minuteStep) => {
79531
79723
  const parts = parseTimeParts(groupState);
79532
79724
  if (!parts) {
79533
79725
  return undefined;
79534
79726
  }
79727
+ if (childUIStateController.name === "minute") {
79728
+ return floorToStep(parts.minute, minuteStep);
79729
+ }
79535
79730
  return parts[childUIStateController.name];
79536
79731
  };
79732
+ const floorToStep = (minutes, step) => Math.floor(minutes / step) * step;
79733
+ const ceilToStep = (minutes, step) => Math.ceil(minutes / step) * step;
79537
79734
 
79538
79735
  const TableSelectionContext = createContext();
79539
79736
  const useTableSelectionContextValue = (