stimeo-ui 0.16.0 → 0.17.0

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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,13 +1,31 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/calendar_controller.ts
3
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
4
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
5
+ function stateReasonFor(event) {
6
+ if (!event) return "api";
7
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
8
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
9
+ return "user";
10
+ }
11
+
12
+ function actionSource(source) {
13
+ if (source instanceof HTMLElement) {
14
+ return { event: null, host: source, origin: source, reason: "api" };
15
+ }
16
+ const event = source ?? null;
17
+ return {
18
+ event,
19
+ host: event?.currentTarget instanceof Element ? event.currentTarget : null,
20
+ origin: event?.target instanceof Element ? event.target : null,
21
+ reason: stateReasonFor(event)
22
+ };
23
+ }
4
24
 
5
- // src/utils/logical_scroll.ts
6
25
  function isRtl(element) {
7
26
  return window.getComputedStyle(element).direction === "rtl";
8
27
  }
9
28
 
10
- // src/utils/arrow_step.ts
11
29
  function logicalArrowKey(key, element) {
12
30
  if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
13
31
  if (!isRtl(element)) return key;
@@ -18,7 +36,6 @@ function isReservedArrowChord(event, allow = []) {
18
36
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
37
  }
20
38
 
21
- // src/utils/intl_format.ts
22
39
  var formatters = /* @__PURE__ */ new WeakMap();
23
40
  function intlFormatter(factory, locale, options, fallbackLocale) {
24
41
  const cache = cacheFor(factory);
@@ -47,7 +64,6 @@ function construct(factory, locale, options, fallbackLocale) {
47
64
  }
48
65
  }
49
66
 
50
- // src/utils/dates.ts
51
67
  function toISODateString(date) {
52
68
  const year = date.getFullYear();
53
69
  const month = String(date.getMonth() + 1).padStart(2, "0");
@@ -85,7 +101,6 @@ function monthLabelFormatter(locale) {
85
101
  return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
86
102
  }
87
103
 
88
- // src/utils/field_mirror.ts
89
104
  function writeField(field, value) {
90
105
  if (field.value === value) return false;
91
106
  field.value = value;
@@ -95,32 +110,26 @@ function commitField(target) {
95
110
  target.dispatchEvent(new Event("change", { bubbles: true }));
96
111
  }
97
112
 
98
- // src/utils/locale.ts
99
113
  function resolveLocale(element, declared = "") {
100
114
  return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
101
115
  }
102
116
 
103
- // src/utils/microtask_coalescer.ts
104
117
  var MicrotaskCoalescer = class {
105
118
  #run;
106
119
  #queued = false;
107
120
  #active = false;
108
121
  #generation = 0;
109
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
110
122
  constructor(run) {
111
123
  this.#run = run;
112
124
  }
113
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
114
125
  activate() {
115
126
  this.#active = true;
116
127
  }
117
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
118
128
  cancel() {
119
129
  this.#active = false;
120
130
  this.#queued = false;
121
131
  this.#generation += 1;
122
132
  }
123
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
124
133
  schedule() {
125
134
  if (!this.#active || this.#queued) return;
126
135
  this.#queued = true;
@@ -133,15 +142,92 @@ var MicrotaskCoalescer = class {
133
142
  }
134
143
  };
135
144
 
136
- // src/utils/target_selector.ts
145
+ var ListenerSet = class {
146
+ #abort = new AbortController();
147
+ add(target, type, handler, options) {
148
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
149
+ }
150
+ dispose() {
151
+ this.#abort.abort();
152
+ this.#abort = new AbortController();
153
+ }
154
+ };
155
+
156
+ var MorphRenderWatcher = class {
157
+ #listeners = new ListenerSet();
158
+ #pass;
159
+ #element = null;
160
+ #onMorph = () => this.#pass.schedule();
161
+ constructor(run) {
162
+ this.#pass = new MicrotaskCoalescer(run);
163
+ }
164
+ observe(element) {
165
+ if (this.#element === element) return;
166
+ this.disconnect();
167
+ this.#element = element;
168
+ this.#pass.activate();
169
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
170
+ }
171
+ schedule() {
172
+ this.#pass.schedule();
173
+ }
174
+ disconnect() {
175
+ this.#listeners.dispose();
176
+ this.#pass.cancel();
177
+ this.#element = null;
178
+ }
179
+ };
180
+
181
+ function matchesNumberBounds(value, bounds) {
182
+ if (!Number.isFinite(value)) {
183
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
184
+ if (direction === null) return false;
185
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
186
+ }
187
+ if (bounds.min !== void 0 && value < bounds.min) return false;
188
+ if (bounds.max !== void 0 && value > bounds.max) return false;
189
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
190
+ if (bounds.integer && !Number.isInteger(value)) return false;
191
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
192
+ return true;
193
+ }
194
+
195
+ function readNumber(raw, fallback, bounds) {
196
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
197
+ }
198
+
199
+ var NumberValueReader = class {
200
+ #lastRejected = /* @__PURE__ */ new Map();
201
+ read(owner, name, raw, fallback, bounds) {
202
+ const resolved = readNumber(raw, fallback, bounds);
203
+ if (matchesNumberBounds(raw, bounds)) {
204
+ this.#lastRejected.delete(name);
205
+ return resolved;
206
+ }
207
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
208
+ const literal = owner.element.getAttribute(attribute);
209
+ if (literal === null) {
210
+ this.#lastRejected.delete(name);
211
+ return resolved;
212
+ }
213
+ if (this.#lastRejected.get(name) !== literal) {
214
+ this.#lastRejected.set(name, literal);
215
+ console.warn(
216
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
217
+ );
218
+ }
219
+ return resolved;
220
+ }
221
+ };
222
+
137
223
  function targetSelector(identifier, name) {
138
224
  return `[data-${identifier}-target~="${name}"]`;
139
225
  }
140
226
 
141
- // src/controllers/calendar_controller.ts
142
227
  var OWNED_DISABLED = "owns-disabled";
143
- var CalendarController = class extends Controller {
144
- /** The marker above, in the namespace this controller is registered under. */
228
+ var CalendarController = class _CalendarController extends Controller {
229
+ #numbers = new NumberValueReader();
230
+ #fieldWrites = /* @__PURE__ */ new WeakMap();
145
231
  get #ownedDisabled() {
146
232
  return `data-${this.identifier}-${OWNED_DISABLED}`;
147
233
  }
@@ -152,82 +238,31 @@ var CalendarController = class extends Controller {
152
238
  min: { type: String, default: "" },
153
239
  max: { type: String, default: "" },
154
240
  weekStart: { type: Number, default: 0 },
155
- // 0 = Sunday, 1 = Monday, etc.
156
241
  locale: { type: String, default: "" }
157
242
  };
158
- static actions = ["next", "onKeydown", "prev", "selectByClick"];
243
+ static valueConstraints = {
244
+ weekStart: { finite: true, integer: true, min: 0, max: 6 }
245
+ };
246
+ static actions = ["next", "onKeydown", "prev", "select"];
159
247
  static events = ["monthchange", "reconcile", "select"];
160
- /** The date currently receiving focus in the grid (local time). */
161
248
  focusedDate = /* @__PURE__ */ new Date();
162
- /**
163
- * Whether `connect()` has run and `disconnect()` has not since. The paints
164
- * before and during `connect()` describe the grid, so they report nothing.
165
- */
166
249
  #connected = false;
167
- /**
168
- * The selection the last paint published — a `YYYY-MM-DD` day, or `""` — which
169
- * the next move of the selection is measured from. A pick records its own day
170
- * here before painting it.
171
- */
172
250
  #published = "";
173
- /**
174
- * How many times a pick or a change the page made has moved the selection, so
175
- * a report still pending for a selection a listener has since replaced can tell.
176
- */
177
251
  #selectionMoves = 0;
178
- /**
179
- * The month the last paint showed (`YYYY-MM`), or `""` before the first paint.
180
- * Only a connected grid reports a paint that moves it: settling on the initial
181
- * month — before `connect()` or in it, from the attribute or derived here — is
182
- * the grid describing itself, not a navigation, so it must not reach a
183
- * listener that refetches inventory or pushes history.
184
- *
185
- * While `month` is empty this is also the month the next paint shows, so the
186
- * grid stays where it is; the first `connect()` of this instance sets it to the
187
- * month the grid opens on.
188
- */
189
252
  #paintedMonth = "";
190
- /**
191
- * Whether this instance has connected before. Only its first connection picks
192
- * the month a grid with no `month` opens on. A later one keeps the month on
193
- * screen, and so do the paints Stimulus runs before it.
194
- */
253
+ #monthMoves = 0;
254
+ #paintedDates = /* @__PURE__ */ new WeakMap();
195
255
  #opened = false;
196
- /**
197
- * Whether the paint about to run was asked for by this grid's own controls.
198
- * The `month` and `selected` Values are shared with the page — application
199
- * code and a Turbo morph write them too — so the field mirrors take their
200
- * "did the user commit this" answer from the route, not from the Value.
201
- */
202
256
  #movedByUser = false;
203
- /**
204
- * Repaints the month on screen after `min`, `max` or `weekStart` changed.
205
- *
206
- * A morph that swaps several of them repaints once, and the delivery Stimulus
207
- * makes before `connect()` is refused, because the grid is painted on
208
- * connect. None of these Values decides the month on screen; `min` and `max`
209
- * decide whether the requested day is published.
210
- */
211
- #repaint = new MicrotaskCoalescer(() => {
257
+ #repaint = new MorphRenderWatcher(() => {
212
258
  this.#repaintKeepingFocus();
213
259
  });
214
- /**
215
- * The cell DOM focus was on, its day and the month on screen, recorded by the
216
- * first repaint of a batch for the pass that settles focus after it.
217
- */
218
260
  #focusOrigin = null;
219
- /**
220
- * Settles DOM focus once, after the last repaint of a batch. The callbacks of
221
- * one mutation repaint one by one and each reads every Value as it stands, so
222
- * a `selected` or `locale` callback can paint a month before that month's own
223
- * callback has moved the tab stop into it. Outside a connection no pass is
224
- * scheduled, so a grid that is not connected moves no focus.
225
- */
226
261
  #settleFocus = new MicrotaskCoalescer(() => {
227
262
  this.#restoreFocus();
228
263
  });
229
264
  connect() {
230
- this.#repaint.activate();
265
+ this.#repaint.observe(this.element);
231
266
  this.#settleFocus.activate();
232
267
  this.#focusOrigin = null;
233
268
  if (!this.monthValue && !this.#opened) {
@@ -238,82 +273,49 @@ var CalendarController = class extends Controller {
238
273
  this.#render();
239
274
  this.#connected = true;
240
275
  }
241
- /**
242
- * Drops a pending repaint and a pending focus move, and stops reporting, so
243
- * nothing reaches for a grid that is not connected.
244
- */
245
276
  disconnect() {
246
277
  this.#connected = false;
247
- this.#repaint.cancel();
278
+ this.#repaint.disconnect();
248
279
  this.#settleFocus.cancel();
249
280
  }
250
- /**
251
- * Paints the month `month` names, whether application code, a morph or the
252
- * grid's own navigation wrote it; focus in the grid follows to the tab stop.
253
- */
254
281
  monthValueChanged() {
255
282
  if (!this.monthValue) return;
256
283
  this.#syncFocusedDateWithMonth();
257
284
  this.#repaintKeepingFocus();
258
285
  }
259
- /**
260
- * Repaints the label when application code changes `locale` at runtime.
261
- *
262
- * Stimulus runs this for the initial Value too, before `connect()`. The paint
263
- * it triggers is idempotent — the month comes from `month` (or, while that is
264
- * still empty, from `focusedDate`) — and a grid that has not connected
265
- * reports nothing, so that paint never reports a month.
266
- */
267
286
  localeValueChanged() {
268
287
  this.#repaintKeepingFocus();
269
288
  }
270
- /**
271
- * Repaints `aria-selected` and the field when the requested day changes, and
272
- * moves the tab stop to the day the grid now publishes. A request `min` /
273
- * `max` withhold is not published, so the tab stop stays where it was.
274
- */
275
289
  selectedValueChanged() {
276
290
  const selection = parseISODateString(this.#selection);
277
291
  if (selection) this.focusedDate = selection;
278
292
  this.#repaintKeepingFocus();
279
293
  }
280
- /** Repaints when `min` changes after connect, so the disabled days and the selection follow. */
281
294
  minValueChanged() {
282
295
  this.#repaint.schedule();
283
296
  }
284
- /** Repaints when `max` changes after connect, so the disabled days and the selection follow. */
285
297
  maxValueChanged() {
286
298
  this.#repaint.schedule();
287
299
  }
288
- /**
289
- * Lays the grid out again when `weekStart` changes after connect, so every
290
- * date moves to the column of its weekday.
291
- *
292
- * Only the day cells are painted: column headers are author markup, so the
293
- * author updates them together with the Value.
294
- */
295
300
  weekStartValueChanged() {
296
301
  this.#repaint.schedule();
297
302
  }
298
- /** Navigates to the previous month. */
299
303
  prev(event) {
300
304
  if (event) event.preventDefault();
301
305
  this.#shiftMonth(-1);
302
306
  }
303
- /** Navigates to the next month. */
304
307
  next(event) {
305
308
  if (event) event.preventDefault();
306
309
  this.#shiftMonth(1);
307
310
  }
308
- /** Handles day selection when a gridcell is clicked. */
309
- selectByClick(event) {
310
- const dayElement = event.target?.closest(
311
- targetSelector(this.identifier, "day")
312
- );
313
- if (!dayElement) return;
314
- this.selectDayElement(dayElement);
311
+ select(source) {
312
+ const { origin, reason } = actionSource(source);
313
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
314
+ return;
315
+ const day = origin?.closest(targetSelector(this.identifier, "day"));
316
+ if (!day || !this.dayTargets.includes(day)) return;
317
+ this.#selectDay(day, reason);
315
318
  }
316
- /** Handles grid cell keyboard navigation and triggers selection. */
317
319
  onKeydown(event) {
318
320
  if (event.defaultPrevented) return;
319
321
  if (isReservedArrowChord(event)) return;
@@ -371,7 +373,7 @@ var CalendarController = class extends Controller {
371
373
  case "Enter":
372
374
  case " ":
373
375
  event.preventDefault();
374
- this.selectDayElement(dayElement);
376
+ this.#selectDay(dayElement, stateReasonFor(event));
375
377
  return;
376
378
  default:
377
379
  handled = false;
@@ -382,43 +384,13 @@ var CalendarController = class extends Controller {
382
384
  this.#focusAndNavigateToDate(nextDate);
383
385
  }
384
386
  }
385
- /**
386
- * First day of the month the grid paints: the `month` Value when it parses —
387
- * while it is empty, the month on screen — otherwise the focused date's month.
388
- *
389
- * A malformed `month` falls back instead of stopping the paint: every cell
390
- * keeps its `aria-selected` and the grid keeps its tab stop, so a Value typo
391
- * still leaves the grid reachable by Tab and the author can see what they
392
- * typed. Every consumer of "which month is on screen" reads it from here, so
393
- * the paint and the `monthchange` report cannot name different months.
394
- */
395
387
  #paintedMonthStart() {
396
388
  const monthInfo = parseISOMonthString(this.#monthRequest);
397
389
  return monthInfo ? new Date(monthInfo.year, monthInfo.month - 1, 1) : new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1);
398
390
  }
399
- /**
400
- * The month the grid is asked to paint: `month`, or while that is empty the
401
- * month on screen, which `connect()` opens on the month of the published
402
- * selection, else on the current one.
403
- */
404
391
  get #monthRequest() {
405
392
  return this.monthValue || this.#paintedMonth;
406
393
  }
407
- /**
408
- * Renders the grid days and updates the month/year label, mirrors both
409
- * fields, then reports each field the user's move moved and — once connected
410
- * — a painted month that differs from the last one painted, and a published
411
- * selection that a change the page made moved.
412
- *
413
- * The report belongs to the paint, not to the `month` Value: the Value is only
414
- * one of the things that decide the month on screen. While a malformed `month`
415
- * falls back, changing `selected` and selecting a neighbouring month's cell
416
- * move the painted month too, and both repaint from here. Reading the report
417
- * off the paint keeps the event naming the month the grid shows, in the
418
- * `YYYY-MM` the detail contract promises, whichever route repainted.
419
- *
420
- * @stimeoRenderRoot
421
- */
422
394
  #render() {
423
395
  const monthStart = this.#paintedMonthStart();
424
396
  const year = monthStart.getFullYear();
@@ -447,6 +419,7 @@ var CalendarController = class extends Controller {
447
419
  const kept = consumerDisabled.get(dateStr);
448
420
  if (kept !== void 0) el.setAttribute("aria-disabled", kept);
449
421
  }
422
+ this.#paintedDates.set(el, dateStr);
450
423
  el.setAttribute("data-date", dateStr);
451
424
  el.textContent = String(date.getDate());
452
425
  const isOutside = date.getFullYear() !== year || date.getMonth() !== month - 1;
@@ -472,25 +445,20 @@ var CalendarController = class extends Controller {
472
445
  const painted = toISOMonthString(monthStart);
473
446
  const previous = this.#paintedMonth;
474
447
  this.#paintedMonth = painted;
448
+ if (previous !== painted) this.#monthMoves += 1;
449
+ const monthMove = this.#monthMoves;
475
450
  const reconciled = this.#settleSelection(selection);
476
451
  const move = this.#selectionMoves;
477
- for (const [field, value] of this.#mirrorFields(painted, selection, reconciled)) {
478
- if (field.value === value) commitField(field);
452
+ for (const [field, value, write] of this.#mirrorFields(painted, selection, reconciled)) {
453
+ if (field.value === value && this.#fieldWrites.get(field) === write) commitField(field);
479
454
  }
480
- if (this.#connected && previous !== painted && this.#paintedMonth === painted) {
455
+ if (this.#connected && previous !== painted && monthMove === this.#monthMoves) {
481
456
  this.dispatch("monthchange", { detail: { month: painted } });
482
457
  }
483
458
  if (reconciled && move === this.#selectionMoves) {
484
459
  this.dispatch("reconcile", { detail: { date: selection } });
485
460
  }
486
461
  }
487
- /**
488
- * Records the selection a paint publishes and says whether it moved from the
489
- * one published before, which a connected grid reports as `reconcile`. A pick
490
- * records its own day before its paint, so that paint finds no move, and
491
- * nothing counts before the grid connects. A move counts as a move of the
492
- * selection.
493
- */
494
462
  #settleSelection(selection) {
495
463
  const published = this.#published;
496
464
  this.#published = selection;
@@ -498,18 +466,13 @@ var CalendarController = class extends Controller {
498
466
  this.#selectionMoves += 1;
499
467
  return true;
500
468
  }
501
- /**
502
- * Repaints the grid for a Value callback or a pick. The first repaint of a
503
- * batch records where DOM focus stood on a day, for the settle pass; focus
504
- * outside the grid is left alone.
505
- */
506
469
  #repaintKeepingFocus() {
507
470
  if (!this.#focusOrigin) {
508
471
  const cell = this.dayTargets.find((day) => day === document.activeElement);
509
472
  if (cell) {
510
473
  this.#focusOrigin = {
511
474
  cell,
512
- date: cell.getAttribute("data-date"),
475
+ date: cell.getAttribute("data-date") ?? this.#paintedDates.get(cell) ?? null,
513
476
  month: this.#paintedMonth
514
477
  };
515
478
  this.#settleFocus.schedule();
@@ -517,18 +480,6 @@ var CalendarController = class extends Controller {
517
480
  }
518
481
  this.#render();
519
482
  }
520
- /**
521
- * Puts DOM focus back on a day it can stay on once a batch of repaints has
522
- * landed.
523
- *
524
- * While the painted month stays, a bound only toggles `aria-disabled` and a
525
- * disabled day stays focusable, so nothing is called (`focus()` would scroll
526
- * the day into view); `weekStart` moves every date to another cell, so focus
527
- * follows its date, or goes to the tab stop once the date has left the grid.
528
- * When the painted month moves, every cell shows another day, and focus goes
529
- * to the tab stop of the new month. Focus a listener of the paint's events
530
- * moved off the recorded cell stays where it went.
531
- */
532
483
  #restoreFocus() {
533
484
  const origin = this.#focusOrigin;
534
485
  this.#focusOrigin = null;
@@ -538,36 +489,46 @@ var CalendarController = class extends Controller {
538
489
  const destination = (moved ? void 0 : this.dayTargets.find((cell) => cell.getAttribute("data-date") === origin.date)) ?? this.dayTargets.find((cell) => cell.getAttribute("tabindex") === "0");
539
490
  destination?.focus();
540
491
  }
541
- /** Fills a selected-day field inserted or replaced at runtime with the published selection. */
492
+ labelTargetConnected() {
493
+ this.#repaint.schedule();
494
+ }
495
+ labelTargetDisconnected() {
496
+ this.#repaint.schedule();
497
+ }
542
498
  fieldTargetConnected(field) {
543
- writeField(field, this.#selection);
499
+ this.#writeField(field, this.#selection);
500
+ }
501
+ fieldTargetDisconnected() {
502
+ this.#repaint.schedule();
544
503
  }
545
- /** Fills a painted-month field inserted or replaced at runtime. */
546
504
  monthFieldTargetConnected(field) {
547
- writeField(field, toISOMonthString(this.#paintedMonthStart()));
548
- }
549
- /**
550
- * Mirrors the painted month and the published selection into their optional
551
- * fields.
552
- *
553
- * @param reconciled - Whether a change the page made moved the selection in
554
- * this paint, which `reconcile` reports rather than the field.
555
- * @returns Each field this grid's own controls moved, with the value written
556
- * to it, for the paint to report.
557
- */
505
+ this.#writeField(field, toISOMonthString(this.#paintedMonthStart()));
506
+ }
507
+ monthFieldTargetDisconnected() {
508
+ this.#repaint.schedule();
509
+ }
510
+ #writeField(field, value) {
511
+ if (!writeField(field, value)) return false;
512
+ this.#fieldWrites.set(field, (this.#fieldWrites.get(field) ?? 0) + 1);
513
+ return true;
514
+ }
558
515
  #mirrorFields(painted, selection, reconciled) {
559
516
  const byUser = this.#movedByUser;
560
517
  this.#movedByUser = false;
561
518
  const moved = [];
562
- if (this.hasFieldTarget && writeField(this.fieldTarget, selection) && !reconciled) {
563
- moved.push([this.fieldTarget, selection]);
519
+ if (this.hasFieldTarget && this.#writeField(this.fieldTarget, selection) && !reconciled) {
520
+ moved.push([this.fieldTarget, selection, this.#fieldWrites.get(this.fieldTarget) ?? 0]);
564
521
  }
565
- if (this.hasMonthFieldTarget && writeField(this.monthFieldTarget, painted)) {
566
- moved.push([this.monthFieldTarget, painted]);
522
+ if (this.hasMonthFieldTarget && this.#writeField(this.monthFieldTarget, painted)) {
523
+ moved.push([
524
+ this.monthFieldTarget,
525
+ painted,
526
+ this.#fieldWrites.get(this.monthFieldTarget) ?? 0
527
+ ]);
567
528
  }
568
529
  return byUser ? moved : [];
569
530
  }
570
- selectDayElement(dayElement) {
531
+ #selectDay(dayElement, reason) {
571
532
  if (dayElement.getAttribute("aria-disabled") === "true") return;
572
533
  const dateStr = dayElement.getAttribute("data-date");
573
534
  if (!dateStr || this.#isDateOutOfBounds(dateStr)) return;
@@ -575,12 +536,13 @@ var CalendarController = class extends Controller {
575
536
  const selected = parseISODateString(dateStr);
576
537
  if (selected) this.focusedDate = selected;
577
538
  this.#movedByUser = true;
578
- this.#selectionMoves += 1;
539
+ const changed = dateStr !== this.#published;
540
+ if (changed) this.#selectionMoves += 1;
579
541
  const move = this.#selectionMoves;
580
542
  this.#published = dateStr;
581
543
  this.#repaintKeepingFocus();
582
- if (move !== this.#selectionMoves) return;
583
- this.dispatch("select", { detail: { date: dateStr } });
544
+ if (!changed || move !== this.#selectionMoves) return;
545
+ this.dispatch("select", { detail: { date: dateStr, reason } });
584
546
  }
585
547
  #focusAndNavigateToDate(date) {
586
548
  this.focusedDate = date;
@@ -588,7 +550,7 @@ var CalendarController = class extends Controller {
588
550
  this.#movedByUser = true;
589
551
  if (targetMonthStr !== this.#monthRequest) {
590
552
  this.monthValue = targetMonthStr;
591
- return;
553
+ if (targetMonthStr !== this.#paintedMonth) return;
592
554
  }
593
555
  this.#render();
594
556
  const dateStr = toISODateString(date);
@@ -599,15 +561,12 @@ var CalendarController = class extends Controller {
599
561
  if (this.maxValue && dateStr > this.maxValue) return true;
600
562
  return false;
601
563
  }
602
- /** The day `selected` requests: the Value when it names a real day, else `""`. */
603
564
  get #requestedDay() {
604
565
  return parseISODateString(this.selectedValue) ? this.selectedValue : "";
605
566
  }
606
- /** The selection published for `requested`: the day while `min` / `max` allow it, else `""`. */
607
567
  #publishable(requested) {
608
568
  return requested && !this.#isDateOutOfBounds(requested) ? requested : "";
609
569
  }
610
- /** The selection the grid publishes now. */
611
570
  get #selection() {
612
571
  return this.#publishable(this.#requestedDay);
613
572
  }
@@ -617,6 +576,10 @@ var CalendarController = class extends Controller {
617
576
  this.#movedByUser = true;
618
577
  const nextMonthDate = new Date(monthInfo.year, monthInfo.month - 1 + delta, 1);
619
578
  this.monthValue = `${nextMonthDate.getFullYear()}-${String(nextMonthDate.getMonth() + 1).padStart(2, "0")}`;
579
+ if (this.monthValue === this.#paintedMonth) {
580
+ this.#syncFocusedDateWithMonth();
581
+ this.#repaintKeepingFocus();
582
+ }
620
583
  }
621
584
  #calculateShiftedMonthDate(baseDate, delta) {
622
585
  const targetDate = new Date(baseDate.getFullYear(), baseDate.getMonth() + delta, 1);
@@ -642,7 +605,7 @@ var CalendarController = class extends Controller {
642
605
  }
643
606
  #getStartOfWeekDate(date) {
644
607
  const currentDay = date.getDay();
645
- const shift = (currentDay - this.weekStartValue + 7) % 7;
608
+ const shift = (currentDay - this.#safeWeekStart + 7) % 7;
646
609
  const target = new Date(date);
647
610
  target.setDate(date.getDate() - shift);
648
611
  return target;
@@ -653,11 +616,6 @@ var CalendarController = class extends Controller {
653
616
  target.setDate(start.getDate() + 6);
654
617
  return target;
655
618
  }
656
- /**
657
- * Opens the tab stop on the published selection, else on today when the shown
658
- * month holds it, else on the 1st of the shown month. A selection the grid
659
- * does not show falls back to the 1st in the paint.
660
- */
661
619
  #initializeFocusedDate() {
662
620
  const selection = parseISODateString(this.#selection);
663
621
  if (selection) {
@@ -693,7 +651,7 @@ var CalendarController = class extends Controller {
693
651
  #calculateGridDays(year, month) {
694
652
  const firstDay = new Date(year, month - 1, 1);
695
653
  const dayOfWeek = firstDay.getDay();
696
- const offset = (dayOfWeek - this.weekStartValue + 7) % 7;
654
+ const offset = (dayOfWeek - this.#safeWeekStart + 7) % 7;
697
655
  const days = [];
698
656
  const current = new Date(firstDay);
699
657
  current.setDate(firstDay.getDate() - offset);
@@ -703,8 +661,16 @@ var CalendarController = class extends Controller {
703
661
  }
704
662
  return days;
705
663
  }
664
+ get #safeWeekStart() {
665
+ return this.#numbers.read(
666
+ this,
667
+ "weekStart",
668
+ this.weekStartValue,
669
+ _CalendarController.values.weekStart.default,
670
+ _CalendarController.valueConstraints.weekStart
671
+ );
672
+ }
706
673
  };
707
674
 
708
675
  export { CalendarController };
709
- //# sourceMappingURL=calendar_controller.js.map
710
676
  //# sourceMappingURL=calendar_controller.js.map