@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.
@@ -663,6 +663,11 @@ naviI18n.addAll({
663
663
  en: ":",
664
664
  fr: "h",
665
665
  },
666
+ // A time of whole hours has nothing to stand between: what follows the hour.
667
+ "time.hour_suffix": {
668
+ en: "",
669
+ fr: "h",
670
+ },
666
671
  "time.hour_label": {
667
672
  en: "Hours",
668
673
  fr: "Heures",
@@ -4869,14 +4874,12 @@ const minutesFromTime$1 = (time) => {
4869
4874
  return parts.hour * 60 + parts.minute;
4870
4875
  };
4871
4876
 
4877
+ // Not folded back into the day: 1440 is "24:00", the end of the day a span can
4878
+ // run into.
4872
4879
  const timeFromMinutes = (minutes) => {
4873
- const inDay =
4874
- ((minutes % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY;
4875
- return `${padTwo$1(Math.floor(inDay / 60))}:${padTwo$1(inDay % 60)}`;
4880
+ return `${padTwo$1(Math.floor(minutes / 60))}:${padTwo$1(minutes % 60)}`;
4876
4881
  };
4877
4882
 
4878
- const MINUTES_PER_DAY = 24 * 60;
4879
-
4880
4883
  const padTwo$1 = (value) => String(value).padStart(2, "0");
4881
4884
 
4882
4885
  // Maps validity type names → navi input type names.
@@ -5772,6 +5775,14 @@ const constraintAttributeFromProp = (key) => {
5772
5775
  const isConstraintAttributeOn = (value) =>
5773
5776
  value !== undefined && value !== null && value !== false;
5774
5777
 
5778
+ /**
5779
+ * Whether a constraint attribute asks for the value to be CORRECTED rather
5780
+ * than refused: `singleSpace="autoFix"`. Only constraints whose rule knows a
5781
+ * correction offer it, and only the fields that write it get one — a
5782
+ * `maxLength` silently truncating what someone wrote would be a bad default.
5783
+ */
5784
+ const isConstraintAttributeAutoFix = (value) => value === "autoFix";
5785
+
5775
5786
  const CONSTRAINT_NAME_TO_PROP = {
5776
5787
  disabled: "disabledMessage",
5777
5788
  required: "requiredMessage",
@@ -10639,14 +10650,13 @@ const tryActionAfterInteractionAllowed = (
10639
10650
 
10640
10651
  // Resolve proxy so navi_action_* fires on the real control element.
10641
10652
  let elementForAction = controlHost;
10642
- let uiState;
10653
+ let activeController = controller;
10643
10654
  if (controller) {
10644
10655
  const proxyTargetController = findControlProxyTargetController(controller);
10645
10656
  if (proxyTargetController) {
10646
10657
  elementForAction = proxyTargetController.ref.current;
10658
+ activeController = proxyTargetController;
10647
10659
  }
10648
- const activeController = proxyTargetController ?? controller;
10649
- uiState = activeController?.uiState;
10650
10660
  }
10651
10661
 
10652
10662
  // Validity gate: re-check (handles autoResetOnAction side effects), then read
@@ -10673,6 +10683,11 @@ const tryActionAfterInteractionAllowed = (
10673
10683
  }
10674
10684
  }
10675
10685
 
10686
+ // Read after the gate, never before: a constraint allowed to correct the
10687
+ // value rewrites it in there (see applyAutoFix), and what goes out has to be
10688
+ // what the control ends up holding.
10689
+ const uiState = activeController?.uiState;
10690
+
10676
10691
  if (action === "auto" || action?.isAction) {
10677
10692
  // A control that commits gets the last word on whether this particular
10678
10693
  // value is worth acting on — see Form's own `shouldRequestAction`, which
@@ -13125,9 +13140,22 @@ CONSTRAINT_ATTRIBUTE_SET.add("data-same-as");
13125
13140
  * `data-single-space` — no leading or trailing space, never two in a row.
13126
13141
  * The rule itself is @jsenv/validity's SINGLE_SPACE_RULE, so a server checking
13127
13142
  * the value again refuses it for the same reason and in the same words.
13143
+ *
13144
+ * `data-single-space="autoFix"` corrects the value instead of refusing it. A
13145
+ * text field ends with a space more often than anyone means it to — a word
13146
+ * then a pause, a mobile keyboard after a suggestion, a paste — and the person
13147
+ * is then asked to find and delete a character they cannot see. The correction
13148
+ * is the rule's own, so the value a server re-checks with `singleSpace: true`
13149
+ * is one it accepts.
13128
13150
  */
13129
13151
 
13130
13152
 
13153
+ const applyRule = (field) => {
13154
+ const valueAsString =
13155
+ field.uiState === undefined ? "" : String(field.uiState);
13156
+ return SINGLE_SPACE_RULE.applyOn(true, valueAsString);
13157
+ };
13158
+
13131
13159
  const SINGLE_SPACE_CONSTRAINT = {
13132
13160
  name: "single_space",
13133
13161
  messageAttribute: "data-single-space-message",
@@ -13136,14 +13164,29 @@ const SINGLE_SPACE_CONSTRAINT = {
13136
13164
  if (!isConstraintAttributeOn(singleSpace)) {
13137
13165
  return null;
13138
13166
  }
13139
- const valueAsString =
13140
- field.uiState === undefined ? "" : String(field.uiState);
13141
- const result = SINGLE_SPACE_RULE.applyOn(true, valueAsString);
13167
+ if (isConstraintAttributeAutoFix(singleSpace)) {
13168
+ // The correction the rule knows always lands on a value the rule
13169
+ // accepts, and it runs on every commit — so there is nothing left for
13170
+ // the person to do about this, and nothing to say to them.
13171
+ return null;
13172
+ }
13173
+ const result = applyRule(field);
13142
13174
  if (!result) {
13143
13175
  return null;
13144
13176
  }
13145
13177
  return naviI18nFromValidityMessage(result);
13146
13178
  },
13179
+ autoFix: (field) => {
13180
+ const singleSpace = field.controlHostProps["data-single-space"];
13181
+ if (!isConstraintAttributeAutoFix(singleSpace)) {
13182
+ return null;
13183
+ }
13184
+ const result = applyRule(field);
13185
+ if (!result) {
13186
+ return null;
13187
+ }
13188
+ return result.autoFix();
13189
+ },
13147
13190
  };
13148
13191
  CONSTRAINT_ATTRIBUTE_SET.add("data-single-space");
13149
13192
 
@@ -13335,6 +13378,74 @@ const createControlValidation = (
13335
13378
  const getConstraintValidityState = () => constraintValidityState;
13336
13379
  controlValidity.getConstraintValidityState = getConstraintValidityState;
13337
13380
 
13381
+ const getConstraintSet = () => {
13382
+ const constraintSet = new Set([
13383
+ ...DEFAULT_CONSTRAINT_SET,
13384
+ ...dynamicConstraintSet,
13385
+ ]);
13386
+ // An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
13387
+ // Read from the raw props on every check so a constraint whose parameters
13388
+ // are closed over is re-created freely, and last so the constraints navi
13389
+ // ships are the ones reported first (see pickConstraintFailureInfo).
13390
+ const constraintsFromProps = controller.props.constraints;
13391
+ if (constraintsFromProps) {
13392
+ for (const constraintFromProps of constraintsFromProps) {
13393
+ constraintSet.add(normalizeConstraint(constraintFromProps));
13394
+ }
13395
+ }
13396
+ return constraintSet;
13397
+ };
13398
+
13399
+ /**
13400
+ * Lets the constraints allowed to correct the value do so, and puts what
13401
+ * they return in the control — ui state, bound signal, the field itself.
13402
+ *
13403
+ * Called at the moments a value is COMMITTED: the field is left, an action
13404
+ * is about to read it. Never while it is being typed into, where removing
13405
+ * the trailing space would eat the one the person is about to follow with a
13406
+ * word.
13407
+ *
13408
+ * Returns whether the value moved, so a caller outside the validity pass can
13409
+ * re-read what the control is now worth.
13410
+ */
13411
+ const applyAutoFix = (event) => {
13412
+ const proxyTargetController = findControlProxyTargetController(controller);
13413
+ if (proxyTargetController) {
13414
+ return proxyTargetController.rules.validation.applyAutoFix(event);
13415
+ }
13416
+ // A value nobody can edit is not navi's to rewrite: it is the app's, and
13417
+ // correcting it would change what gets sent behind the app's back.
13418
+ const controlHostProps = controller.controlHostProps;
13419
+ if (
13420
+ controlHostProps.disabled ||
13421
+ controlHostProps.readOnly ||
13422
+ controlHostProps["aria-readonly"] === "true"
13423
+ ) {
13424
+ return false;
13425
+ }
13426
+ let fixed = false;
13427
+ for (const constraint of getConstraintSet()) {
13428
+ if (!constraint.autoFix) {
13429
+ continue;
13430
+ }
13431
+ const fixedValue = constraint.autoFix(controller);
13432
+ if (fixedValue === null || fixedValue === undefined) {
13433
+ continue;
13434
+ }
13435
+ if (compareTwoJsValues(fixedValue, controller.uiState)) {
13436
+ continue;
13437
+ }
13438
+ const autoFixEvent = new CustomEvent("auto_fix", {
13439
+ detail: { constraint: constraint.name },
13440
+ });
13441
+ chainEvent(autoFixEvent, event);
13442
+ controller.setUIState(fixedValue, autoFixEvent);
13443
+ fixed = true;
13444
+ }
13445
+ return fixed;
13446
+ };
13447
+ controlValidity.applyAutoFix = applyAutoFix;
13448
+
13338
13449
  const checkValidity = ({
13339
13450
  event,
13340
13451
  requester = controller.ref.current,
@@ -13375,21 +13486,15 @@ const createControlValidation = (
13375
13486
  }
13376
13487
  }
13377
13488
 
13378
- let newConstraintValidityState = { valid: true };
13379
- const constraintSet = new Set([
13380
- ...DEFAULT_CONSTRAINT_SET,
13381
- ...dynamicConstraintSet,
13382
- ]);
13383
- // An app constraint declared at the call site: `constraints={[MY_CONSTRAINT]}`.
13384
- // Read from the raw props on every check so a constraint whose parameters
13385
- // are closed over is re-created freely, and last so the constraints navi
13386
- // ships are the ones reported first (see pickConstraintFailureInfo).
13387
- const constraintsFromProps = controller.props.constraints;
13388
- if (constraintsFromProps) {
13389
- for (const constraintFromProps of constraintsFromProps) {
13390
- constraintSet.add(normalizeConstraint(constraintFromProps));
13391
- }
13489
+ if (fromRequestAction) {
13490
+ // The value is about to be read and sent: whoever may correct it gets
13491
+ // the last word before the constraints judge it, so a field is never
13492
+ // refused for something navi knows how to put right.
13493
+ applyAutoFix(event);
13392
13494
  }
13495
+
13496
+ let newConstraintValidityState = { valid: true };
13497
+ const constraintSet = getConstraintSet();
13393
13498
  const elementSig = getElementSignature(controller.ref.current);
13394
13499
  // Not logged: every control checks its constraints on every interaction and
13395
13500
  // almost always passes, so this line alone was most of the debug output —
@@ -37859,7 +37964,8 @@ const useUIStateController = (
37859
37964
  }
37860
37965
  if (
37861
37966
  e.type === "facade_propagate_up" ||
37862
- e.type === "cancel_rollback"
37967
+ e.type === "cancel_rollback" ||
37968
+ e.type === "auto_fix"
37863
37969
  ) {
37864
37970
  // Exception: when the facade propagates a child state change up to the
37865
37971
  // real picker input, also notify the parent group (e.g. Form) so it
@@ -37869,6 +37975,9 @@ const useUIStateController = (
37869
37975
  // A cancel takes the same road back: the Form was told what the
37870
37976
  // popup was picking, so it has to be told the picker went back to
37871
37977
  // where it opened, or it sends a value the user said no to.
37978
+ // A correction is the same story once more: the Form sends what
37979
+ // its fields add up to, and a field that just put its own value
37980
+ // right has to be counted for the corrected one.
37872
37981
  s.parentUIStateController?.onChildUIAction(controller, e, {
37873
37982
  stateChanged: true,
37874
37983
  });
@@ -39763,6 +39872,11 @@ const INTERNAL_EVENT_SET = new Set([
39763
39872
  // notification below still happen, exactly as they did on the way in (see
39764
39873
  // picker_custom.jsx's onClose).
39765
39874
  "cancel_rollback",
39875
+ // A constraint allowed to correct the value put it right as the value was
39876
+ // committed (see applyAutoFix). Nobody pressed anything, so no command and
39877
+ // no action of the control's own — but what it holds really did move, so
39878
+ // uiAction, the bound signal and the parent notification below all happen.
39879
+ "auto_fix",
39766
39880
  ]);
39767
39881
  const isInternalEvent = (e) => {
39768
39882
  return INTERNAL_EVENT_SET.has(e.type);
@@ -40843,6 +40957,24 @@ const useControlProps = (props, {
40843
40957
  syncDomState(readControlValue(el), e);
40844
40958
  };
40845
40959
  }
40960
+ // Leaving the field is one of the two moments a value is committed — the
40961
+ // other being an action about to read it (see applyAutoFix). A constraint
40962
+ // allowed to correct the value puts it right here, so what the field
40963
+ // shows, what the counter counts and what a submit would send are one
40964
+ // thing well before the submit.
40965
+ if (controlType === "input") {
40966
+ const onBlurFromProps = controlHostProps.onBlur;
40967
+ controlHostProps.onBlur = e => {
40968
+ onBlurFromProps?.(e);
40969
+ const validation = uiStateController.rules.validation;
40970
+ if (validation.applyAutoFix(e)) {
40971
+ // The value moved without anyone typing: what the constraints had to
40972
+ // say about the old one is out of date, and so is what the controls
40973
+ // above read from this one.
40974
+ validation.syncValidity(e);
40975
+ }
40976
+ };
40977
+ }
40846
40978
  }
40847
40979
  const uiState = uiStateController.uiStateSignal.peek();
40848
40980
  const domProps = toDomProps(uiState);
@@ -81054,8 +81186,10 @@ const css$l = /* css */`.navi_time_range_label {
81054
81186
  }
81055
81187
  `;
81056
81188
  const HOUR_COUNT = 24;
81189
+ const END_OF_DAY_HOUR = 24;
81057
81190
  const MINUTES_PER_HOUR = 60;
81058
81191
  const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
81192
+ const END_OF_DAY = END_OF_DAY_HOUR * MINUTES_PER_HOUR;
81059
81193
 
81060
81194
  /**
81061
81195
  * @type {import("ignore:preact").FunctionComponent<{
@@ -81075,16 +81209,21 @@ const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
81075
81209
  * }>}
81076
81210
  * @param {string} [value] The time shown, as "HH:MM".
81077
81211
  * @param {number} [minuteStep=1] How many minutes apart the values on the
81078
- * minute wheel are — 15 for quarters of an hour.
81212
+ * minute wheel are — 15 for quarters of an hour. At 60 the time is a whole
81213
+ * hour: there is no minute wheel at all, the value stays "HH:MM" ("08:00"),
81214
+ * and a value arriving with minutes is shown on its hour.
81079
81215
  * @param {{min?: number, max?: number}|number[]} [hours] Which hours the wheel
81080
81216
  * offers: `{ min: 7, max: 21 }` for a day that starts and ends somewhere, or
81081
- * the list itself. All 24 by default. Rows nobody will ever land on are rows
81082
- * in the way.
81217
+ * the list itself. 0 to 23 by default. Rows nobody will ever land on are rows
81218
+ * in the way. 24 is the end of the day ("24:00", midnight at the end of a
81219
+ * span) and has no minutes: turned onto it, the minute wheel goes back to 0.
81083
81220
  * @param {boolean} [loop=true] The wheels go round: 23h then 0h, 59 minutes
81084
81221
  * then 0. What a clock does. Say `loop={false}` for two ends one cannot turn
81085
81222
  * past.
81086
81223
  * @param {import("ignore:preact").ComponentChildren} [separator] What is written
81087
- * between the hours and the minutes. "h" in French, ":" elsewhere.
81224
+ * between the hours and the minutes. "h" in French, ":" elsewhere. In a time
81225
+ * of whole hours it follows each hour on its row instead ("8h"), and nothing
81226
+ * is written by default outside French.
81088
81227
  * @param {string} [placeholder] What the wheels show while the time holds
81089
81228
  * nothing, as "HH:MM". Wheels have no blank row to land on, so their
81090
81229
  * placeholder is a position rather than a grey word — shown, but not an
@@ -81099,13 +81238,20 @@ const TimeWheel = ({
81099
81238
  hours,
81100
81239
  loop = true,
81101
81240
  placeholder,
81102
- separator = naviI18n("time.hour_separator"),
81241
+ separator,
81103
81242
  hourLabel = naviI18n("time.hour_label"),
81104
81243
  minuteLabel = naviI18n("time.minute_label"),
81105
81244
  size,
81106
81245
  wheelProps,
81246
+ onnavi_wheel_settle,
81107
81247
  ...rest
81108
81248
  }) => {
81249
+ const wholeHours = minuteStep >= MINUTES_PER_HOUR;
81250
+ if (separator === undefined) {
81251
+ separator = naviI18n(wholeHours ? "time.hour_suffix" : "time.hour_separator");
81252
+ }
81253
+ const hourWheelRef = useRef(null);
81254
+ const minuteWheelRef = useRef(null);
81109
81255
  const minutes = useMemo(() => {
81110
81256
  const minuteList = [];
81111
81257
  let minute = 0;
@@ -81119,13 +81265,42 @@ const TimeWheel = ({
81119
81265
  const {
81120
81266
  aggregateChildStates,
81121
81267
  distributeChildUIState
81122
- } = useAnswered(placeholder, rest, aggregateTime, distributeTime);
81268
+ } = useAnswered(placeholder, rest, aggregateTime, (groupState, child) => distributeTime(groupState, child, minuteStep));
81123
81269
  const placeholderParts = parseTimeParts(placeholder);
81270
+
81271
+ // The end of the day has no minutes. The time already reads "24:00" whatever
81272
+ // the minute wheel shows (see aggregateTime); on settle the wheel is brought
81273
+ // back to 0 so what is drawn is what is held.
81274
+ const endOfDayHasNoMinutes = e => {
81275
+ const hourEl = hourWheelRef.current;
81276
+ const minuteEl = minuteWheelRef.current;
81277
+ if (!hourEl || !minuteEl) {
81278
+ return;
81279
+ }
81280
+ if (getUIStateFromElement(hourEl) !== END_OF_DAY_HOUR) {
81281
+ return;
81282
+ }
81283
+ if (getUIStateFromElement(minuteEl) === 0) {
81284
+ return;
81285
+ }
81286
+ dispatchRequestSetUIState(minuteEl, 0, {
81287
+ event: e
81288
+ });
81289
+ };
81124
81290
  return jsxs(WheelGroup, {
81125
81291
  aggregateChildStates: aggregateChildStates,
81126
81292
  distributeChildUIState: distributeChildUIState,
81293
+ onnavi_wheel_settle: e => {
81294
+ if (!wholeHours) {
81295
+ endOfDayHasNoMinutes(e);
81296
+ }
81297
+ if (onnavi_wheel_settle) {
81298
+ onnavi_wheel_settle(e);
81299
+ }
81300
+ },
81127
81301
  ...rest,
81128
81302
  children: [jsx(Wheel, {
81303
+ ref: hourWheelRef,
81129
81304
  name: "hour",
81130
81305
  type: "integer",
81131
81306
  bounded: !loop,
@@ -81136,24 +81311,29 @@ const TimeWheel = ({
81136
81311
  children: hourList.map(hour => jsx(Wheel.Item, {
81137
81312
  value: hour,
81138
81313
  paddingX: "s",
81139
- children: padTwo(hour)
81314
+ children: wholeHours ? jsxs(Fragment, {
81315
+ children: [hour, separator]
81316
+ }) : padTwo(hour)
81140
81317
  }, hour))
81141
- }), jsx(WheelGroup.Separator, {
81142
- size: size,
81143
- children: separator
81144
- }), jsx(Wheel, {
81145
- name: "minute",
81146
- type: "integer",
81147
- bounded: !loop,
81148
- size: size,
81149
- "aria-label": minuteLabel,
81150
- defaultValue: placeholderParts ? placeholderParts.minute : undefined,
81151
- ...wheelProps,
81152
- children: minutes.map(minute => jsx(Wheel.Item, {
81153
- value: minute,
81154
- paddingX: "s",
81155
- children: padTwo(minute)
81156
- }, minute))
81318
+ }), wholeHours ? null : jsxs(Fragment, {
81319
+ children: [jsx(WheelGroup.Separator, {
81320
+ size: size,
81321
+ children: separator
81322
+ }), jsx(Wheel, {
81323
+ ref: minuteWheelRef,
81324
+ name: "minute",
81325
+ type: "integer",
81326
+ bounded: !loop,
81327
+ size: size,
81328
+ "aria-label": minuteLabel,
81329
+ defaultValue: placeholderParts ? floorToStep(placeholderParts.minute, minuteStep) : undefined,
81330
+ ...wheelProps,
81331
+ children: minutes.map(minute => jsx(Wheel.Item, {
81332
+ value: minute,
81333
+ paddingX: "s",
81334
+ children: padTwo(minute)
81335
+ }, minute))
81336
+ })]
81157
81337
  })]
81158
81338
  });
81159
81339
  };
@@ -81187,11 +81367,12 @@ const TimeWheel = ({
81187
81367
  * prepositions belong there; the group is still a row, and takes `flexWrap`
81188
81368
  * for screens too narrow to hold both columns.
81189
81369
  * @param {number} [minuteStep=1] How many minutes apart the values on both
81190
- * minute wheels are.
81370
+ * minute wheels are. 60 for a span of whole hours ("de 8h à 12h").
81191
81371
  * @param {{min?: number, max?: number}|number[]} [hours] Which hours both
81192
- * wheels offer — see `TimeWheel`.
81372
+ * wheels offer — see `TimeWheel`. A 24 is offered to the end alone: a span
81373
+ * can run until midnight ("24:00"), it cannot start there.
81193
81374
  * @param {number} [minDuration=0] How long the span must last at least, in
81194
- * minutes. Zero by default: a span of no length is a span all the same, only
81375
+ * 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
81195
81376
  * one that goes backwards is not. It is what the bounds keep between them as
81196
81377
  * they turn — turn the start into the end and the end moves along, keeping
81197
81378
  * that much room.
@@ -81224,6 +81405,12 @@ const TimeRangeWheel = ({
81224
81405
  const startId = useId();
81225
81406
  const startRef = useRef(null);
81226
81407
  const endRef = useRef(null);
81408
+ const endHourList = useMemo(() => resolveHourList(hours), [hours ? hours.min : undefined, hours ? hours.max : undefined, hours]);
81409
+ const startHourList = useMemo(() => endHourList.filter(hour => hour !== END_OF_DAY_HOUR), [endHourList]);
81410
+ const step = minuteStep >= MINUTES_PER_HOUR ? MINUTES_PER_HOUR : minuteStep;
81411
+ const minGap = ceilToStep(minDuration, step);
81412
+ // The latest the end can be pushed to: the last time its wheels can show.
81413
+ const lastEnd = endHourList.includes(END_OF_DAY_HOUR) ? END_OF_DAY : floorToStep(LAST_MINUTE_OF_DAY, step);
81227
81414
  // One turn settles the whole span: a start somebody chose makes the end an
81228
81415
  // answer too, left where the placeholder put it.
81229
81416
  const {
@@ -81255,18 +81442,18 @@ const TimeRangeWheel = ({
81255
81442
  return;
81256
81443
  }
81257
81444
  const duration = movedSide === "start" ? otherMinutes - movedMinutes : movedMinutes - otherMinutes;
81258
- if (duration >= minDuration) {
81445
+ if (duration >= minGap) {
81259
81446
  return;
81260
81447
  }
81261
- let pushedMinutes = movedSide === "start" ? movedMinutes + minDuration : movedMinutes - minDuration;
81448
+ let pushedMinutes = movedSide === "start" ? movedMinutes + minGap : movedMinutes - minGap;
81262
81449
  // The day has ends the wheels do not: pushed past midnight, the other bound
81263
81450
  // would come back round on the wrong side of the one that pushed it. It
81264
81451
  // stops at the edge instead, and the span that no longer fits is what the
81265
81452
  // send-time constraint is there to say (see time_range_constraint.js).
81266
81453
  if (pushedMinutes < 0) {
81267
81454
  pushedMinutes = 0;
81268
- } else if (pushedMinutes > LAST_MINUTE_OF_DAY) {
81269
- pushedMinutes = LAST_MINUTE_OF_DAY;
81455
+ } else if (pushedMinutes > lastEnd) {
81456
+ pushedMinutes = lastEnd;
81270
81457
  }
81271
81458
  dispatchRequestSetUIState(otherEl, timeFromMinutes(pushedMinutes), {
81272
81459
  event: e
@@ -81294,7 +81481,7 @@ const TimeRangeWheel = ({
81294
81481
  ref: startRef,
81295
81482
  name: "start",
81296
81483
  minuteStep: minuteStep,
81297
- hours: hours,
81484
+ hours: startHourList,
81298
81485
  loop: loop,
81299
81486
  size: size,
81300
81487
  placeholder: placeholder ? placeholder.start : undefined
@@ -81319,7 +81506,7 @@ const TimeRangeWheel = ({
81319
81506
  ref: endRef,
81320
81507
  name: "end",
81321
81508
  minuteStep: minuteStep,
81322
- hours: hours,
81509
+ hours: endHourList,
81323
81510
  loop: loop,
81324
81511
  size: size,
81325
81512
  placeholder: placeholder ? placeholder.end : undefined,
@@ -81331,7 +81518,7 @@ const TimeRangeWheel = ({
81331
81518
  // the time one would have to move is (see time_range_constraint.js).
81332
81519
  ,
81333
81520
  "data-time-after": startId,
81334
- "data-time-min-duration": minDuration,
81521
+ "data-time-min-duration": minGap,
81335
81522
  ...timeProps,
81336
81523
  ...endTimeProps
81337
81524
  })
@@ -81493,10 +81680,11 @@ const distributeSpan = (groupState, childUIStateController) => {
81493
81680
  return groupState[childUIStateController.name];
81494
81681
  };
81495
81682
 
81496
- // The two wheels as one value, "HH:MM".
81683
+ // The wheels as one value, "HH:MM". A time of whole hours has no minute wheel
81684
+ // and is on the hour; so is the end of the day, whatever the minute wheel says.
81497
81685
  const aggregateTime = childUIStateControllers => {
81498
81686
  let hour = "";
81499
- let minute = "";
81687
+ let minute = 0;
81500
81688
  for (const child of childUIStateControllers) {
81501
81689
  if (child.name === "hour") {
81502
81690
  hour = child.uiState ?? "";
@@ -81505,18 +81693,27 @@ const aggregateTime = childUIStateControllers => {
81505
81693
  minute = child.uiState ?? "";
81506
81694
  }
81507
81695
  }
81696
+ if (hour === END_OF_DAY_HOUR) {
81697
+ return formatTimeParts(hour, 0);
81698
+ }
81508
81699
  return formatTimeParts(hour, minute);
81509
81700
  };
81510
81701
 
81511
81702
  // The way back: what the group is set to (a value given to it, a form being
81512
- // reset, the other bound pushing it) lands on the wheel it belongs to.
81513
- const distributeTime = (groupState, childUIStateController) => {
81703
+ // reset, the other bound pushing it) lands on the wheel it belongs to. Minutes
81704
+ // the wheel does not offer land on the one before.
81705
+ const distributeTime = (groupState, childUIStateController, minuteStep) => {
81514
81706
  const parts = parseTimeParts(groupState);
81515
81707
  if (!parts) {
81516
81708
  return undefined;
81517
81709
  }
81710
+ if (childUIStateController.name === "minute") {
81711
+ return floorToStep(parts.minute, minuteStep);
81712
+ }
81518
81713
  return parts[childUIStateController.name];
81519
81714
  };
81715
+ const floorToStep = (minutes, step) => Math.floor(minutes / step) * step;
81716
+ const ceilToStep = (minutes, step) => Math.ceil(minutes / step) * step;
81520
81717
 
81521
81718
  const TableSelectionContext = createContext();
81522
81719
  const useTableSelectionContextValue = (