stimeo-ui 0.15.0 → 0.16.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +178 -0
- data/README.md +120 -0
- data/dist/cable/index.js +63 -15
- data/dist/controllers/accordion_controller.js +98 -9
- data/dist/controllers/announcer_controller.js +8 -1
- data/dist/controllers/auto_submit_controller.js +43 -2
- data/dist/controllers/avatar_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +9 -8
- data/dist/controllers/bulk_select_controller.js +7 -6
- data/dist/controllers/calendar_controller.js +323 -123
- data/dist/controllers/carousel_controller.js +263 -38
- data/dist/controllers/character_counter_controller.js +40 -2
- data/dist/controllers/checkbox_controller.js +54 -10
- data/dist/controllers/clipboard_controller.js +63 -5
- data/dist/controllers/collapsible_controller.js +99 -14
- data/dist/controllers/color_picker_controller.js +80 -34
- data/dist/controllers/combobox_controller.js +106 -16
- data/dist/controllers/command_palette_controller.js +35 -3
- data/dist/controllers/conditional_fields_controller.js +56 -11
- data/dist/controllers/confirm_controller.js +3 -0
- data/dist/controllers/context_menu_controller.js +32 -12
- data/dist/controllers/countdown_controller.js +129 -26
- data/dist/controllers/currency_input_controller.js +205 -58
- data/dist/controllers/data_grid_controller.js +195 -29
- data/dist/controllers/date_range_picker_controller.js +139 -27
- data/dist/controllers/dialog_controller.js +35 -8
- data/dist/controllers/direct_upload_controller.js +22 -4
- data/dist/controllers/dirty_form_controller.js +14 -1
- data/dist/controllers/dismissible_controller.js +1 -0
- data/dist/controllers/drawer_controller.js +54 -19
- data/dist/controllers/dropdown_controller.js +36 -9
- data/dist/controllers/editable_controller.js +34 -0
- data/dist/controllers/file_dropzone_controller.js +144 -51
- data/dist/controllers/filter_controller.js +20 -6
- data/dist/controllers/flash_controller.js +305 -46
- data/dist/controllers/focus_controller.js +1 -0
- data/dist/controllers/form_field_controller.js +7 -5
- data/dist/controllers/form_validation_controller.js +19 -13
- data/dist/controllers/frame_loading_controller.js +16 -2
- data/dist/controllers/highlight_controller.js +19 -2
- data/dist/controllers/hover_card_controller.js +40 -14
- data/dist/controllers/idle_controller.js +90 -5
- data/dist/controllers/input_mask_controller.js +65 -9
- data/dist/controllers/intersection_controller.js +3 -0
- data/dist/controllers/lazy_frame_controller.js +11 -2
- data/dist/controllers/listbox_controller.js +203 -45
- data/dist/controllers/masonry_controller.js +0 -2
- data/dist/controllers/menu_controller.js +45 -16
- data/dist/controllers/menubar_controller.js +58 -24
- data/dist/controllers/meter_controller.js +9 -5
- data/dist/controllers/multi_select_controller.js +278 -104
- data/dist/controllers/navigation_menu_controller.js +48 -15
- data/dist/controllers/nested_form_controller.js +37 -8
- data/dist/controllers/network_status_controller.js +9 -1
- data/dist/controllers/number_input_controller.js +124 -21
- data/dist/controllers/optimistic_controller.js +14 -1
- data/dist/controllers/otp_controller.js +198 -55
- data/dist/controllers/overflow_indicator_controller.js +84 -13
- data/dist/controllers/overflow_menu_controller.js +112 -41
- data/dist/controllers/pagination_controller.js +74 -28
- data/dist/controllers/password_reveal_controller.js +59 -2
- data/dist/controllers/persist_controller.js +30 -8
- data/dist/controllers/pointer_drag_controller.js +131 -52
- data/dist/controllers/popover_controller.js +45 -11
- data/dist/controllers/portal_controller.js +6 -2
- data/dist/controllers/preview_guard_controller.js +16 -1
- data/dist/controllers/progress_controller.js +8 -4
- data/dist/controllers/radio_group_controller.js +42 -17
- data/dist/controllers/range_slider_controller.js +88 -42
- data/dist/controllers/rating_controller.js +39 -15
- data/dist/controllers/read_more_controller.js +69 -2
- data/dist/controllers/resizable_controller.js +82 -22
- data/dist/controllers/scroll_area_controller.js +5 -1
- data/dist/controllers/scroll_visibility_controller.js +0 -1
- data/dist/controllers/scrollspy_controller.js +6 -0
- data/dist/controllers/separator_controller.js +66 -37
- data/dist/controllers/sidebar_controller.js +77 -18
- data/dist/controllers/skeleton_controller.js +6 -1
- data/dist/controllers/slider_controller.js +82 -47
- data/dist/controllers/smart_sticky_header_controller.js +11 -1
- data/dist/controllers/sortable_controller.js +17 -2
- data/dist/controllers/spinner_controller.js +10 -2
- data/dist/controllers/step_indicator_controller.js +18 -17
- data/dist/controllers/stepper_controller.js +101 -19
- data/dist/controllers/stick_to_bottom_controller.js +75 -5
- data/dist/controllers/submit_once_controller.js +16 -3
- data/dist/controllers/switch_controller.js +101 -10
- data/dist/controllers/tabs_controller.js +21 -2
- data/dist/controllers/tags_input_controller.js +209 -59
- data/dist/controllers/theme_controller.js +64 -14
- data/dist/controllers/time_picker_controller.js +23 -8
- data/dist/controllers/toast_controller.js +337 -54
- data/dist/controllers/toggle_group_controller.js +159 -23
- data/dist/controllers/toolbar_controller.js +32 -0
- data/dist/controllers/tooltip_controller.js +39 -13
- data/dist/controllers/transition_controller.js +4 -0
- data/dist/controllers/tree_view_controller.js +169 -16
- data/dist/index.js +4588 -1535
- data/dist/positioning/index.js +2 -0
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -85,68 +85,67 @@ function monthLabelFormatter(locale) {
|
|
|
85
85
|
return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// src/utils/field_mirror.ts
|
|
89
|
+
function writeField(field, value) {
|
|
90
|
+
if (field.value === value) return false;
|
|
91
|
+
field.value = value;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
function commitField(target) {
|
|
95
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
// src/utils/locale.ts
|
|
89
99
|
function resolveLocale(element, declared = "") {
|
|
90
100
|
return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
// src/utils/
|
|
94
|
-
var
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*/
|
|
103
|
-
clear(id) {
|
|
104
|
-
if (this.ids.delete(id)) {
|
|
105
|
-
this.cancel(id);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
110
|
-
* to guarantee no timer outlives the element.
|
|
111
|
-
*/
|
|
112
|
-
clearAll() {
|
|
113
|
-
for (const id of this.ids) {
|
|
114
|
-
this.cancel(id);
|
|
115
|
-
}
|
|
116
|
-
this.ids.clear();
|
|
103
|
+
// src/utils/microtask_coalescer.ts
|
|
104
|
+
var MicrotaskCoalescer = class {
|
|
105
|
+
#run;
|
|
106
|
+
#queued = false;
|
|
107
|
+
#active = false;
|
|
108
|
+
#generation = 0;
|
|
109
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
110
|
+
constructor(run) {
|
|
111
|
+
this.#run = run;
|
|
117
112
|
}
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
114
|
+
activate() {
|
|
115
|
+
this.#active = true;
|
|
121
116
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
128
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
129
|
-
*/
|
|
130
|
-
set(callback, delay) {
|
|
131
|
-
const id = this.schedule(() => {
|
|
132
|
-
this.ids.delete(id);
|
|
133
|
-
callback();
|
|
134
|
-
}, delay);
|
|
135
|
-
this.ids.add(id);
|
|
136
|
-
return id;
|
|
137
|
-
}
|
|
138
|
-
schedule(callback, delay) {
|
|
139
|
-
return window.setTimeout(callback, delay);
|
|
117
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
118
|
+
cancel() {
|
|
119
|
+
this.#active = false;
|
|
120
|
+
this.#queued = false;
|
|
121
|
+
this.#generation += 1;
|
|
140
122
|
}
|
|
141
|
-
|
|
142
|
-
|
|
123
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
124
|
+
schedule() {
|
|
125
|
+
if (!this.#active || this.#queued) return;
|
|
126
|
+
this.#queued = true;
|
|
127
|
+
const generation = this.#generation;
|
|
128
|
+
queueMicrotask(() => {
|
|
129
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
130
|
+
this.#queued = false;
|
|
131
|
+
this.#run();
|
|
132
|
+
});
|
|
143
133
|
}
|
|
144
134
|
};
|
|
145
135
|
|
|
136
|
+
// src/utils/target_selector.ts
|
|
137
|
+
function targetSelector(identifier, name) {
|
|
138
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
139
|
+
}
|
|
140
|
+
|
|
146
141
|
// src/controllers/calendar_controller.ts
|
|
147
|
-
var OWNED_DISABLED = "
|
|
142
|
+
var OWNED_DISABLED = "owns-disabled";
|
|
148
143
|
var CalendarController = class extends Controller {
|
|
149
|
-
|
|
144
|
+
/** The marker above, in the namespace this controller is registered under. */
|
|
145
|
+
get #ownedDisabled() {
|
|
146
|
+
return `data-${this.identifier}-${OWNED_DISABLED}`;
|
|
147
|
+
}
|
|
148
|
+
static targets = ["label", "grid", "day", "field", "monthField"];
|
|
150
149
|
static values = {
|
|
151
150
|
month: { type: String, default: "" },
|
|
152
151
|
selected: { type: String, default: "" },
|
|
@@ -157,68 +156,144 @@ var CalendarController = class extends Controller {
|
|
|
157
156
|
locale: { type: String, default: "" }
|
|
158
157
|
};
|
|
159
158
|
static actions = ["next", "onKeydown", "prev", "selectByClick"];
|
|
160
|
-
static events = ["monthchange", "select"];
|
|
159
|
+
static events = ["monthchange", "reconcile", "select"];
|
|
161
160
|
/** The date currently receiving focus in the grid (local time). */
|
|
162
161
|
focusedDate = /* @__PURE__ */ new Date();
|
|
163
162
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
|
|
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
|
+
#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
|
+
#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
|
+
#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
|
+
#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
|
+
*/
|
|
195
|
+
#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
|
+
#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(() => {
|
|
212
|
+
this.#repaintKeepingFocus();
|
|
213
|
+
});
|
|
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.
|
|
167
217
|
*/
|
|
168
|
-
#
|
|
218
|
+
#focusOrigin = null;
|
|
169
219
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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.
|
|
174
225
|
*/
|
|
175
|
-
#
|
|
226
|
+
#settleFocus = new MicrotaskCoalescer(() => {
|
|
227
|
+
this.#restoreFocus();
|
|
228
|
+
});
|
|
176
229
|
connect() {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
230
|
+
this.#repaint.activate();
|
|
231
|
+
this.#settleFocus.activate();
|
|
232
|
+
this.#focusOrigin = null;
|
|
233
|
+
if (!this.monthValue && !this.#opened) {
|
|
234
|
+
this.#paintedMonth = toISOMonthString(parseISODateString(this.#selection) ?? /* @__PURE__ */ new Date());
|
|
182
235
|
}
|
|
236
|
+
this.#opened = true;
|
|
183
237
|
this.#initializeFocusedDate();
|
|
184
|
-
this
|
|
238
|
+
this.#render();
|
|
239
|
+
this.#connected = true;
|
|
185
240
|
}
|
|
186
|
-
/**
|
|
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
|
+
*/
|
|
187
245
|
disconnect() {
|
|
188
|
-
this.#
|
|
246
|
+
this.#connected = false;
|
|
247
|
+
this.#repaint.cancel();
|
|
248
|
+
this.#settleFocus.cancel();
|
|
189
249
|
}
|
|
190
250
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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.
|
|
193
253
|
*/
|
|
194
254
|
monthValueChanged() {
|
|
195
255
|
if (!this.monthValue) return;
|
|
196
256
|
this.#syncFocusedDateWithMonth();
|
|
197
|
-
this
|
|
257
|
+
this.#repaintKeepingFocus();
|
|
198
258
|
}
|
|
199
259
|
/**
|
|
200
260
|
* Repaints the label when application code changes `locale` at runtime.
|
|
201
261
|
*
|
|
202
262
|
* Stimulus runs this for the initial Value too, before `connect()`. The paint
|
|
203
263
|
* it triggers is idempotent — the month comes from `month` (or, while that is
|
|
204
|
-
* still empty, from `focusedDate`)
|
|
205
|
-
*
|
|
264
|
+
* still empty, from `focusedDate`) — and a grid that has not connected
|
|
265
|
+
* reports nothing, so that paint never reports a month.
|
|
206
266
|
*/
|
|
207
267
|
localeValueChanged() {
|
|
208
|
-
this
|
|
268
|
+
this.#repaintKeepingFocus();
|
|
209
269
|
}
|
|
210
270
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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.
|
|
213
274
|
*/
|
|
214
275
|
selectedValueChanged() {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
this.
|
|
276
|
+
const selection = parseISODateString(this.#selection);
|
|
277
|
+
if (selection) this.focusedDate = selection;
|
|
278
|
+
this.#repaintKeepingFocus();
|
|
279
|
+
}
|
|
280
|
+
/** Repaints when `min` changes after connect, so the disabled days and the selection follow. */
|
|
281
|
+
minValueChanged() {
|
|
282
|
+
this.#repaint.schedule();
|
|
283
|
+
}
|
|
284
|
+
/** Repaints when `max` changes after connect, so the disabled days and the selection follow. */
|
|
285
|
+
maxValueChanged() {
|
|
286
|
+
this.#repaint.schedule();
|
|
287
|
+
}
|
|
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
|
+
weekStartValueChanged() {
|
|
296
|
+
this.#repaint.schedule();
|
|
222
297
|
}
|
|
223
298
|
/** Navigates to the previous month. */
|
|
224
299
|
prev(event) {
|
|
@@ -233,7 +308,7 @@ var CalendarController = class extends Controller {
|
|
|
233
308
|
/** Handles day selection when a gridcell is clicked. */
|
|
234
309
|
selectByClick(event) {
|
|
235
310
|
const dayElement = event.target?.closest(
|
|
236
|
-
"
|
|
311
|
+
targetSelector(this.identifier, "day")
|
|
237
312
|
);
|
|
238
313
|
if (!dayElement) return;
|
|
239
314
|
this.selectDayElement(dayElement);
|
|
@@ -243,7 +318,7 @@ var CalendarController = class extends Controller {
|
|
|
243
318
|
if (event.defaultPrevented) return;
|
|
244
319
|
if (isReservedArrowChord(event)) return;
|
|
245
320
|
const dayElement = event.target?.closest(
|
|
246
|
-
"
|
|
321
|
+
targetSelector(this.identifier, "day")
|
|
247
322
|
);
|
|
248
323
|
if (!dayElement) return;
|
|
249
324
|
const date = parseISODateString(dayElement.getAttribute("data-date") ?? "");
|
|
@@ -308,8 +383,8 @@ var CalendarController = class extends Controller {
|
|
|
308
383
|
}
|
|
309
384
|
}
|
|
310
385
|
/**
|
|
311
|
-
* First day of the month the grid paints: the `month` Value when it parses
|
|
312
|
-
* otherwise the focused date's month.
|
|
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.
|
|
313
388
|
*
|
|
314
389
|
* A malformed `month` falls back instead of stopping the paint: every cell
|
|
315
390
|
* keeps its `aria-selected` and the grid keeps its tab stop, so a Value typo
|
|
@@ -318,12 +393,22 @@ var CalendarController = class extends Controller {
|
|
|
318
393
|
* the paint and the `monthchange` report cannot name different months.
|
|
319
394
|
*/
|
|
320
395
|
#paintedMonthStart() {
|
|
321
|
-
const monthInfo = parseISOMonthString(this
|
|
396
|
+
const monthInfo = parseISOMonthString(this.#monthRequest);
|
|
322
397
|
return monthInfo ? new Date(monthInfo.year, monthInfo.month - 1, 1) : new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1);
|
|
323
398
|
}
|
|
324
399
|
/**
|
|
325
|
-
*
|
|
326
|
-
*
|
|
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
|
+
get #monthRequest() {
|
|
405
|
+
return this.monthValue || this.#paintedMonth;
|
|
406
|
+
}
|
|
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.
|
|
327
412
|
*
|
|
328
413
|
* The report belongs to the paint, not to the `month` Value: the Value is only
|
|
329
414
|
* one of the things that decide the month on screen. While a malformed `month`
|
|
@@ -331,24 +416,36 @@ var CalendarController = class extends Controller {
|
|
|
331
416
|
* move the painted month too, and both repaint from here. Reading the report
|
|
332
417
|
* off the paint keeps the event naming the month the grid shows, in the
|
|
333
418
|
* `YYYY-MM` the detail contract promises, whichever route repainted.
|
|
419
|
+
*
|
|
420
|
+
* @stimeoRenderRoot
|
|
334
421
|
*/
|
|
335
|
-
render() {
|
|
422
|
+
#render() {
|
|
336
423
|
const monthStart = this.#paintedMonthStart();
|
|
337
424
|
const year = monthStart.getFullYear();
|
|
338
425
|
const month = monthStart.getMonth() + 1;
|
|
426
|
+
const selection = this.#selection;
|
|
339
427
|
if (this.hasLabelTarget) {
|
|
340
428
|
const locale = resolveLocale(this.element, this.localeValue);
|
|
341
429
|
this.labelTarget.textContent = monthLabelFormatter(locale).format(monthStart);
|
|
342
430
|
}
|
|
343
431
|
const days = this.#calculateGridDays(year, month);
|
|
344
432
|
const dayElements = this.dayTargets;
|
|
433
|
+
const consumerDisabled = /* @__PURE__ */ new Map();
|
|
434
|
+
for (const el of dayElements) {
|
|
435
|
+
const shown = el.getAttribute("data-date");
|
|
436
|
+
const value = el.getAttribute("aria-disabled");
|
|
437
|
+
if (!shown || value === null || el.hasAttribute(this.#ownedDisabled)) continue;
|
|
438
|
+
consumerDisabled.set(shown, value);
|
|
439
|
+
}
|
|
345
440
|
for (const [index, date] of days.entries()) {
|
|
346
441
|
const el = dayElements[index];
|
|
347
442
|
if (!el) continue;
|
|
348
443
|
const dateStr = toISODateString(date);
|
|
349
444
|
if (el.getAttribute("data-date") !== dateStr) {
|
|
350
445
|
el.removeAttribute("aria-disabled");
|
|
351
|
-
el.removeAttribute(
|
|
446
|
+
el.removeAttribute(this.#ownedDisabled);
|
|
447
|
+
const kept = consumerDisabled.get(dateStr);
|
|
448
|
+
if (kept !== void 0) el.setAttribute("aria-disabled", kept);
|
|
352
449
|
}
|
|
353
450
|
el.setAttribute("data-date", dateStr);
|
|
354
451
|
el.textContent = String(date.getDate());
|
|
@@ -356,17 +453,16 @@ var CalendarController = class extends Controller {
|
|
|
356
453
|
el.setAttribute("data-outside", String(isOutside));
|
|
357
454
|
const todayStr = toISODateString(/* @__PURE__ */ new Date());
|
|
358
455
|
el.setAttribute("data-today", String(dateStr === todayStr));
|
|
359
|
-
|
|
360
|
-
el.setAttribute("aria-selected", String(isSelected));
|
|
456
|
+
el.setAttribute("aria-selected", String(dateStr === selection));
|
|
361
457
|
const isFocused = toISODateString(this.focusedDate) === dateStr;
|
|
362
458
|
el.setAttribute("tabindex", isFocused ? "0" : "-1");
|
|
363
459
|
const isDisabled = this.#isDateOutOfBounds(dateStr);
|
|
364
460
|
if (isDisabled) {
|
|
365
|
-
if (!el.hasAttribute("aria-disabled")) el.setAttribute(
|
|
461
|
+
if (!el.hasAttribute("aria-disabled")) el.setAttribute(this.#ownedDisabled, "");
|
|
366
462
|
el.setAttribute("aria-disabled", "true");
|
|
367
|
-
} else if (el.hasAttribute(
|
|
463
|
+
} else if (el.hasAttribute(this.#ownedDisabled)) {
|
|
368
464
|
el.removeAttribute("aria-disabled");
|
|
369
|
-
el.removeAttribute(
|
|
465
|
+
el.removeAttribute(this.#ownedDisabled);
|
|
370
466
|
}
|
|
371
467
|
}
|
|
372
468
|
if (!dayElements.some((el) => el.getAttribute("tabindex") === "0")) {
|
|
@@ -374,50 +470,151 @@ var CalendarController = class extends Controller {
|
|
|
374
470
|
fallback?.setAttribute("tabindex", "0");
|
|
375
471
|
}
|
|
376
472
|
const painted = toISOMonthString(monthStart);
|
|
377
|
-
const previous = this.#
|
|
378
|
-
this.#
|
|
379
|
-
|
|
473
|
+
const previous = this.#paintedMonth;
|
|
474
|
+
this.#paintedMonth = painted;
|
|
475
|
+
const reconciled = this.#settleSelection(selection);
|
|
476
|
+
const move = this.#selectionMoves;
|
|
477
|
+
for (const [field, value] of this.#mirrorFields(painted, selection, reconciled)) {
|
|
478
|
+
if (field.value === value) commitField(field);
|
|
479
|
+
}
|
|
480
|
+
if (this.#connected && previous !== painted && this.#paintedMonth === painted) {
|
|
380
481
|
this.dispatch("monthchange", { detail: { month: painted } });
|
|
381
482
|
}
|
|
483
|
+
if (reconciled && move === this.#selectionMoves) {
|
|
484
|
+
this.dispatch("reconcile", { detail: { date: selection } });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
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
|
+
#settleSelection(selection) {
|
|
495
|
+
const published = this.#published;
|
|
496
|
+
this.#published = selection;
|
|
497
|
+
if (!this.#connected || selection === published) return false;
|
|
498
|
+
this.#selectionMoves += 1;
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
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
|
+
#repaintKeepingFocus() {
|
|
507
|
+
if (!this.#focusOrigin) {
|
|
508
|
+
const cell = this.dayTargets.find((day) => day === document.activeElement);
|
|
509
|
+
if (cell) {
|
|
510
|
+
this.#focusOrigin = {
|
|
511
|
+
cell,
|
|
512
|
+
date: cell.getAttribute("data-date"),
|
|
513
|
+
month: this.#paintedMonth
|
|
514
|
+
};
|
|
515
|
+
this.#settleFocus.schedule();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
this.#render();
|
|
519
|
+
}
|
|
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
|
+
#restoreFocus() {
|
|
533
|
+
const origin = this.#focusOrigin;
|
|
534
|
+
this.#focusOrigin = null;
|
|
535
|
+
if (!origin || document.activeElement !== origin.cell) return;
|
|
536
|
+
const moved = this.#paintedMonth !== origin.month;
|
|
537
|
+
if (!moved && origin.cell.getAttribute("data-date") === origin.date) return;
|
|
538
|
+
const destination = (moved ? void 0 : this.dayTargets.find((cell) => cell.getAttribute("data-date") === origin.date)) ?? this.dayTargets.find((cell) => cell.getAttribute("tabindex") === "0");
|
|
539
|
+
destination?.focus();
|
|
540
|
+
}
|
|
541
|
+
/** Fills a selected-day field inserted or replaced at runtime with the published selection. */
|
|
542
|
+
fieldTargetConnected(field) {
|
|
543
|
+
writeField(field, this.#selection);
|
|
544
|
+
}
|
|
545
|
+
/** Fills a painted-month field inserted or replaced at runtime. */
|
|
546
|
+
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
|
+
*/
|
|
558
|
+
#mirrorFields(painted, selection, reconciled) {
|
|
559
|
+
const byUser = this.#movedByUser;
|
|
560
|
+
this.#movedByUser = false;
|
|
561
|
+
const moved = [];
|
|
562
|
+
if (this.hasFieldTarget && writeField(this.fieldTarget, selection) && !reconciled) {
|
|
563
|
+
moved.push([this.fieldTarget, selection]);
|
|
564
|
+
}
|
|
565
|
+
if (this.hasMonthFieldTarget && writeField(this.monthFieldTarget, painted)) {
|
|
566
|
+
moved.push([this.monthFieldTarget, painted]);
|
|
567
|
+
}
|
|
568
|
+
return byUser ? moved : [];
|
|
382
569
|
}
|
|
383
570
|
selectDayElement(dayElement) {
|
|
384
571
|
if (dayElement.getAttribute("aria-disabled") === "true") return;
|
|
385
572
|
const dateStr = dayElement.getAttribute("data-date");
|
|
386
|
-
if (!dateStr) return;
|
|
573
|
+
if (!dateStr || this.#isDateOutOfBounds(dateStr)) return;
|
|
387
574
|
this.selectedValue = dateStr;
|
|
388
575
|
const selected = parseISODateString(dateStr);
|
|
389
576
|
if (selected) this.focusedDate = selected;
|
|
390
|
-
this
|
|
577
|
+
this.#movedByUser = true;
|
|
578
|
+
this.#selectionMoves += 1;
|
|
579
|
+
const move = this.#selectionMoves;
|
|
580
|
+
this.#published = dateStr;
|
|
581
|
+
this.#repaintKeepingFocus();
|
|
582
|
+
if (move !== this.#selectionMoves) return;
|
|
391
583
|
this.dispatch("select", { detail: { date: dateStr } });
|
|
392
584
|
}
|
|
393
585
|
#focusAndNavigateToDate(date) {
|
|
394
586
|
this.focusedDate = date;
|
|
395
587
|
const targetMonthStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
396
|
-
|
|
397
|
-
if (
|
|
588
|
+
this.#movedByUser = true;
|
|
589
|
+
if (targetMonthStr !== this.#monthRequest) {
|
|
398
590
|
this.monthValue = targetMonthStr;
|
|
399
|
-
|
|
400
|
-
this.render();
|
|
401
|
-
}
|
|
402
|
-
const focusTarget = () => {
|
|
403
|
-
const dateStr = toISODateString(date);
|
|
404
|
-
const targetEl = this.dayTargets.find((el) => el.getAttribute("data-date") === dateStr);
|
|
405
|
-
targetEl?.focus();
|
|
406
|
-
};
|
|
407
|
-
if (isMonthTransition) {
|
|
408
|
-
this.#focusTimer.set(focusTarget, 0);
|
|
409
|
-
} else {
|
|
410
|
-
focusTarget();
|
|
591
|
+
return;
|
|
411
592
|
}
|
|
593
|
+
this.#render();
|
|
594
|
+
const dateStr = toISODateString(date);
|
|
595
|
+
this.dayTargets.find((el) => el.getAttribute("data-date") === dateStr)?.focus();
|
|
412
596
|
}
|
|
413
597
|
#isDateOutOfBounds(dateStr) {
|
|
414
598
|
if (this.minValue && dateStr < this.minValue) return true;
|
|
415
599
|
if (this.maxValue && dateStr > this.maxValue) return true;
|
|
416
600
|
return false;
|
|
417
601
|
}
|
|
602
|
+
/** The day `selected` requests: the Value when it names a real day, else `""`. */
|
|
603
|
+
get #requestedDay() {
|
|
604
|
+
return parseISODateString(this.selectedValue) ? this.selectedValue : "";
|
|
605
|
+
}
|
|
606
|
+
/** The selection published for `requested`: the day while `min` / `max` allow it, else `""`. */
|
|
607
|
+
#publishable(requested) {
|
|
608
|
+
return requested && !this.#isDateOutOfBounds(requested) ? requested : "";
|
|
609
|
+
}
|
|
610
|
+
/** The selection the grid publishes now. */
|
|
611
|
+
get #selection() {
|
|
612
|
+
return this.#publishable(this.#requestedDay);
|
|
613
|
+
}
|
|
418
614
|
#shiftMonth(delta) {
|
|
419
|
-
const monthInfo = parseISOMonthString(this
|
|
615
|
+
const monthInfo = parseISOMonthString(this.#monthRequest);
|
|
420
616
|
if (!monthInfo) return;
|
|
617
|
+
this.#movedByUser = true;
|
|
421
618
|
const nextMonthDate = new Date(monthInfo.year, monthInfo.month - 1 + delta, 1);
|
|
422
619
|
this.monthValue = `${nextMonthDate.getFullYear()}-${String(nextMonthDate.getMonth() + 1).padStart(2, "0")}`;
|
|
423
620
|
}
|
|
@@ -456,15 +653,18 @@ var CalendarController = class extends Controller {
|
|
|
456
653
|
target.setDate(start.getDate() + 6);
|
|
457
654
|
return target;
|
|
458
655
|
}
|
|
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
|
+
*/
|
|
459
661
|
#initializeFocusedDate() {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
662
|
+
const selection = parseISODateString(this.#selection);
|
|
663
|
+
if (selection) {
|
|
664
|
+
this.focusedDate = selection;
|
|
665
|
+
return;
|
|
466
666
|
}
|
|
467
|
-
const monthInfo = parseISOMonthString(this
|
|
667
|
+
const monthInfo = parseISOMonthString(this.#monthRequest);
|
|
468
668
|
if (monthInfo) {
|
|
469
669
|
const today = /* @__PURE__ */ new Date();
|
|
470
670
|
if (today.getFullYear() === monthInfo.year && today.getMonth() === monthInfo.month - 1) {
|