stimeo-ui 0.4.0 → 0.6.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.
- package/CHANGELOG.md +184 -0
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +195 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +5 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +90 -10
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +31 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js +8 -3
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +4 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +9 -3
- package/dist/controllers/color_picker_controller.js +41 -11
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js +2 -2
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +5 -3
- package/dist/controllers/countdown_controller.js +5 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +162 -31
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +1 -1
- package/dist/controllers/direct_upload_controller.js +3 -3
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +27 -7
- package/dist/controllers/empty_state_controller.js +107 -16
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +7 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +25 -5
- package/dist/controllers/flash_controller.js +161 -21
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -3
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +8 -2
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +19 -2
- package/dist/controllers/frame_loading_controller.js +94 -22
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +8 -4
- package/dist/controllers/highlight_controller.js +38 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -4
- package/dist/controllers/idle_controller.js +39 -6
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +3 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +46 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +15 -13
- package/dist/controllers/network_status_controller.js +1 -3
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +29 -3
- package/dist/controllers/number_input_controller.js +455 -64
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +5 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js +38 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +4 -4
- package/dist/controllers/password_strength_controller.js +21 -3
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +4 -4
- package/dist/controllers/persist_controller.js +24 -5
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +10 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -2
- package/dist/controllers/portal_controller.js +10 -0
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +1 -1
- package/dist/controllers/progress_controller.js +7 -3
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +41 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +23 -0
- package/dist/controllers/range_slider_controller.js +385 -94
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +36 -31
- package/dist/controllers/rating_controller.js +274 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +2 -0
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +4 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -1
- package/dist/controllers/scroll_area_controller.js +155 -23
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js +13 -17
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js +71 -3
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +17 -1
- package/dist/controllers/slider_controller.js +325 -48
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +15 -10
- package/dist/controllers/spinner_controller.js +18 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +1 -1
- package/dist/controllers/step_indicator_controller.js +3 -1
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.js +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -8
- package/dist/controllers/switch_controller.js +162 -18
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +41 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +42 -10
- package/dist/controllers/time_picker_controller.js +296 -104
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +39 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +2 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +1 -2
- package/dist/controllers/tree_view_controller.js +24 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +3811 -1048
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +92 -4
- package/dist/inspector/cli.js +174 -6
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +234 -8
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +24 -24
- package/dist/inspector/manifest.json +573 -59
- package/dist/positioning/index.d.ts +2 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -18,9 +18,46 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
18
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// src/utils/microtask_coalescer.ts
|
|
22
|
+
var MicrotaskCoalescer = class {
|
|
23
|
+
#run;
|
|
24
|
+
#queued = false;
|
|
25
|
+
#active = false;
|
|
26
|
+
#generation = 0;
|
|
27
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
28
|
+
constructor(run) {
|
|
29
|
+
this.#run = run;
|
|
30
|
+
}
|
|
31
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
32
|
+
activate() {
|
|
33
|
+
this.#active = true;
|
|
34
|
+
}
|
|
35
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
36
|
+
cancel() {
|
|
37
|
+
this.#active = false;
|
|
38
|
+
this.#queued = false;
|
|
39
|
+
this.#generation += 1;
|
|
40
|
+
}
|
|
41
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
42
|
+
schedule() {
|
|
43
|
+
if (!this.#active || this.#queued) return;
|
|
44
|
+
this.#queued = true;
|
|
45
|
+
const generation = this.#generation;
|
|
46
|
+
queueMicrotask(() => {
|
|
47
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
48
|
+
this.#queued = false;
|
|
49
|
+
this.#run();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
21
54
|
// src/controllers/time_picker_controller.ts
|
|
22
55
|
var AM = 0;
|
|
23
56
|
var PM = 1;
|
|
57
|
+
var SECONDS_PER_MINUTE = 60;
|
|
58
|
+
var SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE;
|
|
59
|
+
var SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
|
|
60
|
+
var hasModifier = (event) => event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
24
61
|
var TimePickerController = class extends Controller {
|
|
25
62
|
static targets = ["segment", "field"];
|
|
26
63
|
static values = {
|
|
@@ -30,35 +67,80 @@ var TimePickerController = class extends Controller {
|
|
|
30
67
|
wrap: { type: Boolean, default: true }
|
|
31
68
|
};
|
|
32
69
|
static actions = ["onKeydown"];
|
|
33
|
-
static events = ["change"];
|
|
34
|
-
/**
|
|
35
|
-
#
|
|
70
|
+
static events = ["change", "reconcile"];
|
|
71
|
+
/** Collapses target, Value, and retained-attribute morphs into one silent render. */
|
|
72
|
+
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
73
|
+
/** Canonical state; the displayed hour and meridiem are derived from this value. */
|
|
74
|
+
#state = { hour: 0, minute: 0, second: 0 };
|
|
36
75
|
/** Direct-entry digit buffer and the segment it belongs to. */
|
|
37
76
|
#typeBuffer = "";
|
|
38
77
|
#typeSegment = null;
|
|
39
|
-
/** Last composed
|
|
78
|
+
/** Last composed value, used to suppress duplicate user notifications. */
|
|
40
79
|
#lastValue = "";
|
|
41
|
-
|
|
80
|
+
#observer = null;
|
|
81
|
+
#connected = false;
|
|
82
|
+
#domDirty = false;
|
|
83
|
+
/** Seeds state from the DOM, renders canonical ARIA, and starts morph observation. */
|
|
42
84
|
connect() {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
this.#connected = true;
|
|
86
|
+
this.#reconcile.activate();
|
|
87
|
+
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
88
|
+
this.#domDirty = false;
|
|
89
|
+
this.#adoptDomState();
|
|
90
|
+
this.#render();
|
|
91
|
+
this.#observeMutations();
|
|
92
|
+
}
|
|
93
|
+
/** Releases the observer, listener, queued reconciliation, and transient input state. */
|
|
94
|
+
disconnect() {
|
|
95
|
+
this.#connected = false;
|
|
96
|
+
this.#reconcile.cancel();
|
|
97
|
+
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
98
|
+
this.#observer?.disconnect();
|
|
99
|
+
this.#observer = null;
|
|
100
|
+
this.#domDirty = false;
|
|
101
|
+
this.#clearTypeBuffer();
|
|
102
|
+
this.#lastValue = "";
|
|
103
|
+
}
|
|
104
|
+
/** Re-renders the canonical instant when 12/24-hour presentation changes at runtime. */
|
|
105
|
+
hourCycleValueChanged() {
|
|
106
|
+
this.#reconcile.schedule();
|
|
107
|
+
}
|
|
108
|
+
/** Recomposes the optional seconds portion after a runtime Value change. */
|
|
109
|
+
secondsValueChanged() {
|
|
110
|
+
this.#reconcile.schedule();
|
|
111
|
+
}
|
|
112
|
+
/** Adopts a segment inserted or replaced by a Turbo morph. */
|
|
113
|
+
segmentTargetConnected() {
|
|
114
|
+
this.#domDirty = true;
|
|
115
|
+
this.#clearTypeBuffer();
|
|
116
|
+
this.#reconcile.schedule();
|
|
117
|
+
}
|
|
118
|
+
/** Rebuilds state after a segment leaves the retained controller element. */
|
|
119
|
+
segmentTargetDisconnected() {
|
|
120
|
+
this.#domDirty = true;
|
|
121
|
+
this.#clearTypeBuffer();
|
|
122
|
+
this.#reconcile.schedule();
|
|
123
|
+
}
|
|
124
|
+
/** Reflects the current composed value into a field added or replaced at runtime. */
|
|
125
|
+
fieldTargetConnected() {
|
|
126
|
+
this.#reconcile.schedule();
|
|
127
|
+
}
|
|
128
|
+
/** Reconciles silently when the optional field leaves at runtime. */
|
|
129
|
+
fieldTargetDisconnected() {
|
|
130
|
+
this.#reconcile.schedule();
|
|
52
131
|
}
|
|
53
|
-
/** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
|
|
132
|
+
/** Handles APG stepping, inter-segment focus moves, jumps, and direct entry. */
|
|
54
133
|
onKeydown(event) {
|
|
55
134
|
if (isReservedArrowChord(event)) return;
|
|
56
135
|
const segment = event.target?.closest(
|
|
57
|
-
"[data-stimeo--time-picker-target
|
|
136
|
+
"[data-stimeo--time-picker-target~='segment']"
|
|
58
137
|
);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
138
|
+
if (!segment || !this.segmentTargets.includes(segment)) return;
|
|
139
|
+
const kind = this.#kindOf(segment);
|
|
140
|
+
if (!kind) return;
|
|
141
|
+
const key = logicalArrowKey(event.key, this.element);
|
|
142
|
+
if ((key === "Home" || key === "End") && hasModifier(event)) return;
|
|
143
|
+
switch (key) {
|
|
62
144
|
case "ArrowUp":
|
|
63
145
|
event.preventDefault();
|
|
64
146
|
this.#step(kind, this.#delta(kind));
|
|
@@ -77,131 +159,202 @@ var TimePickerController = class extends Controller {
|
|
|
77
159
|
break;
|
|
78
160
|
case "Home":
|
|
79
161
|
event.preventDefault();
|
|
80
|
-
this.#
|
|
162
|
+
this.#setDisplayed(kind, this.#bounds(kind).min);
|
|
163
|
+
this.#commitRender();
|
|
81
164
|
break;
|
|
82
165
|
case "End":
|
|
83
166
|
event.preventDefault();
|
|
84
|
-
this.#
|
|
167
|
+
this.#setDisplayed(kind, this.#bounds(kind).max);
|
|
168
|
+
this.#commitRender();
|
|
85
169
|
break;
|
|
86
170
|
default:
|
|
87
|
-
if (/^[0-9]$/.test(event.key)) {
|
|
171
|
+
if (kind !== "meridiem" && /^[0-9]$/.test(event.key) && !hasModifier(event)) {
|
|
88
172
|
event.preventDefault();
|
|
89
173
|
this.#typeDigit(segment, kind, event.key);
|
|
174
|
+
return;
|
|
90
175
|
}
|
|
91
176
|
return;
|
|
92
177
|
}
|
|
93
|
-
this.#
|
|
94
|
-
this.#typeSegment = null;
|
|
178
|
+
this.#clearTypeBuffer();
|
|
95
179
|
}
|
|
96
|
-
/**
|
|
180
|
+
/** Clears direct-entry state when Tab, Shift+Tab, or pointer focus leaves a segment. */
|
|
181
|
+
#onFocusOut = (event) => {
|
|
182
|
+
const segment = event.target?.closest(
|
|
183
|
+
"[data-stimeo--time-picker-target~='segment']"
|
|
184
|
+
);
|
|
185
|
+
if (!segment || !this.segmentTargets.includes(segment)) return;
|
|
186
|
+
this.#clearTypeBuffer();
|
|
187
|
+
};
|
|
188
|
+
/** Uses the normalized positive-integer minute step; other segments step by one. */
|
|
97
189
|
#delta(kind) {
|
|
98
|
-
|
|
190
|
+
if (kind !== "minute") return 1;
|
|
191
|
+
const step = Math.trunc(this.stepValue);
|
|
192
|
+
return Number.isFinite(step) && step > 0 ? step : 1;
|
|
99
193
|
}
|
|
100
|
-
/**
|
|
194
|
+
/** Mutates one complete action and then renders/notifies its final value once. */
|
|
101
195
|
#step(kind, delta) {
|
|
102
|
-
if (kind === "meridiem") {
|
|
103
|
-
this.#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
const wrapped = ((raw - min) % span + span) % span + min;
|
|
115
|
-
const carry = Math.floor((raw - min) / span);
|
|
116
|
-
this.#set(kind, wrapped);
|
|
117
|
-
this.#carry(kind, carry);
|
|
118
|
-
return;
|
|
196
|
+
if (kind === "meridiem" && this.wrapValue) {
|
|
197
|
+
this.#state.hour = modulo(this.#state.hour + 12, 24);
|
|
198
|
+
} else if (this.wrapValue) {
|
|
199
|
+
const scale = kind === "hour" ? SECONDS_PER_HOUR : kind === "minute" ? SECONDS_PER_MINUTE : 1;
|
|
200
|
+
this.#setFromSeconds(this.#secondsOfDay + delta * scale);
|
|
201
|
+
} else if (kind === "hour") {
|
|
202
|
+
this.#state.hour = Math.min(23, Math.max(0, this.#state.hour + delta));
|
|
203
|
+
} else {
|
|
204
|
+
const { min, max } = this.#bounds(kind);
|
|
205
|
+
const displayed = this.#displayValue(kind);
|
|
206
|
+
this.#setDisplayed(kind, Math.min(max, Math.max(min, displayed + delta)));
|
|
119
207
|
}
|
|
120
|
-
this.#
|
|
121
|
-
}
|
|
122
|
-
/** Propagates a wrap carry from `kind` into the next larger segment. */
|
|
123
|
-
#carry(kind, amount) {
|
|
124
|
-
if (amount === 0) return;
|
|
125
|
-
if (kind === "second") this.#step("minute", amount);
|
|
126
|
-
else if (kind === "minute") this.#step("hour", amount);
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,
|
|
130
|
-
* and resyncs the composed field. Clamping here guards the direct-entry path:
|
|
131
|
-
* typing `0` into a 12-hour hour (min 1) must not commit an out-of-range
|
|
132
|
-
* `aria-valuenow="0"`. The stepping path already passes in-bounds values, so
|
|
133
|
-
* the clamp is a no-op there.
|
|
134
|
-
*/
|
|
135
|
-
#set(kind, value) {
|
|
136
|
-
const { min, max } = this.#bounds(kind);
|
|
137
|
-
this.#state[kind] = Math.min(max, Math.max(min, value));
|
|
138
|
-
const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);
|
|
139
|
-
if (segment) this.#renderSegment(segment);
|
|
140
|
-
this.#syncField(true);
|
|
208
|
+
this.#commitRender();
|
|
141
209
|
}
|
|
142
|
-
/** Accumulates a typed digit,
|
|
210
|
+
/** Accumulates a typed digit, rendering once and advancing after completion. */
|
|
143
211
|
#typeDigit(segment, kind, digit) {
|
|
144
|
-
if (kind === "meridiem") return;
|
|
145
212
|
if (this.#typeSegment !== kind) this.#typeBuffer = "";
|
|
146
213
|
this.#typeSegment = kind;
|
|
147
214
|
const { max } = this.#bounds(kind);
|
|
148
215
|
const candidate = Number(`${this.#typeBuffer}${digit}`);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.#
|
|
216
|
+
this.#typeBuffer = candidate <= max ? `${this.#typeBuffer}${digit}` : digit;
|
|
217
|
+
this.#setDisplayed(kind, Number(this.#typeBuffer));
|
|
218
|
+
this.#commitRender();
|
|
152
219
|
if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {
|
|
153
|
-
this.#
|
|
154
|
-
this.#typeSegment = null;
|
|
220
|
+
this.#clearTypeBuffer();
|
|
155
221
|
this.#focusSibling(segment, 1);
|
|
156
222
|
}
|
|
157
223
|
}
|
|
158
|
-
/** Moves focus to the previous/next segment, if one exists. */
|
|
224
|
+
/** Moves focus to the previous/next valid segment, if one exists. */
|
|
159
225
|
#focusSibling(segment, direction) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
226
|
+
const segments = this.segmentTargets.filter((candidate) => this.#kindOf(candidate) !== null);
|
|
227
|
+
const index = segments.indexOf(segment);
|
|
228
|
+
segments[index + direction]?.focus();
|
|
163
229
|
}
|
|
164
|
-
/**
|
|
230
|
+
/** Clears the direct-entry buffer and its owning segment together. */
|
|
231
|
+
#clearTypeBuffer() {
|
|
232
|
+
this.#typeBuffer = "";
|
|
233
|
+
this.#typeSegment = null;
|
|
234
|
+
}
|
|
235
|
+
/** Reconciles one coalesced target, Value, or retained-attribute mutation batch. */
|
|
236
|
+
#reconcileDom() {
|
|
237
|
+
if (this.#domDirty) {
|
|
238
|
+
this.#domDirty = false;
|
|
239
|
+
this.#clearTypeBuffer();
|
|
240
|
+
this.#adoptDomState();
|
|
241
|
+
}
|
|
242
|
+
this.#render();
|
|
243
|
+
}
|
|
244
|
+
/** Rebuilds canonical state from the targets; an absent hour represents midnight. */
|
|
245
|
+
#adoptDomState() {
|
|
246
|
+
let displayedHour = 0;
|
|
247
|
+
let meridiem = AM;
|
|
248
|
+
let minute = 0;
|
|
249
|
+
let second = 0;
|
|
250
|
+
for (const segment of this.segmentTargets) {
|
|
251
|
+
const kind = this.#kindOf(segment);
|
|
252
|
+
if (!kind) continue;
|
|
253
|
+
const value = this.#authoredValue(segment, kind);
|
|
254
|
+
if (kind === "hour") displayedHour = value;
|
|
255
|
+
else if (kind === "minute") minute = value;
|
|
256
|
+
else if (kind === "second") second = value;
|
|
257
|
+
else meridiem = value;
|
|
258
|
+
}
|
|
259
|
+
this.#state = {
|
|
260
|
+
hour: this.hourCycleValue === 12 ? displayedHour % 12 + (meridiem === PM ? 12 : 0) : displayedHour,
|
|
261
|
+
minute,
|
|
262
|
+
second
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Reads and integer-clamps one authored `aria-valuenow`. */
|
|
266
|
+
#authoredValue(segment, kind) {
|
|
267
|
+
const raw = Number(segment.getAttribute("aria-valuenow"));
|
|
268
|
+
const { min, max } = this.#bounds(kind);
|
|
269
|
+
if (!Number.isFinite(raw)) return min;
|
|
270
|
+
return Math.min(max, Math.max(min, Math.trunc(raw)));
|
|
271
|
+
}
|
|
272
|
+
/** Writes a displayed segment value back into canonical state without rendering. */
|
|
273
|
+
#setDisplayed(kind, raw) {
|
|
274
|
+
const { min, max } = this.#bounds(kind);
|
|
275
|
+
const value = Math.min(max, Math.max(min, Math.trunc(raw)));
|
|
276
|
+
if (kind === "hour") {
|
|
277
|
+
this.#state.hour = this.hourCycleValue === 12 ? value % 12 + (this.#state.hour >= 12 ? 12 : 0) : value;
|
|
278
|
+
} else if (kind === "minute" || kind === "second") {
|
|
279
|
+
this.#state[kind] = value;
|
|
280
|
+
} else {
|
|
281
|
+
this.#state.hour = this.#state.hour % 12 + (value === PM ? 12 : 0);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** Converts seconds with day wrapping into the canonical three-unit state. */
|
|
285
|
+
#setFromSeconds(raw) {
|
|
286
|
+
const seconds = modulo(raw, SECONDS_PER_DAY);
|
|
287
|
+
this.#state.hour = Math.floor(seconds / SECONDS_PER_HOUR);
|
|
288
|
+
this.#state.minute = Math.floor(seconds % SECONDS_PER_HOUR / SECONDS_PER_MINUTE);
|
|
289
|
+
this.#state.second = seconds % SECONDS_PER_MINUTE;
|
|
290
|
+
}
|
|
291
|
+
/** Reflects canonical state to every segment without dispatching user events. */
|
|
292
|
+
#renderSegments() {
|
|
293
|
+
this.#withoutObservation(() => {
|
|
294
|
+
for (const segment of this.segmentTargets) this.#renderSegment(segment);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Reflects canonical state during connection or DOM reconciliation.
|
|
299
|
+
*
|
|
300
|
+
* No user edit reaches here, so `change` never fires from this path. A pass
|
|
301
|
+
* that moves the committed value reports `reconcile` instead.
|
|
302
|
+
*
|
|
303
|
+
* @stimeoRenderRoot
|
|
304
|
+
*/
|
|
305
|
+
#render() {
|
|
306
|
+
this.#renderSegments();
|
|
307
|
+
const value = this.#composedValue;
|
|
308
|
+
const previous = this.#lastValue;
|
|
309
|
+
this.#writeField(value);
|
|
310
|
+
this.#lastValue = value;
|
|
311
|
+
if (previous !== "" && value !== previous) {
|
|
312
|
+
this.dispatch("reconcile", { detail: { value } });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** Reflects one completed user action and dispatches its final change exactly once. */
|
|
316
|
+
#commitRender() {
|
|
317
|
+
this.#renderSegments();
|
|
318
|
+
const value = this.#composedValue;
|
|
319
|
+
const fieldChanged = this.#writeField(value);
|
|
320
|
+
if (fieldChanged) this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
|
|
321
|
+
if (value !== this.#lastValue) this.dispatch("change", { detail: { value } });
|
|
322
|
+
this.#lastValue = value;
|
|
323
|
+
}
|
|
324
|
+
/** Reflects one segment's value and controller-owned ARIA bounds/text. */
|
|
165
325
|
#renderSegment(segment) {
|
|
166
326
|
const kind = this.#kindOf(segment);
|
|
167
327
|
if (!kind) return;
|
|
168
|
-
const value = this.#
|
|
169
|
-
|
|
170
|
-
const text2 = value === PM ? "PM" : "AM";
|
|
171
|
-
segment.setAttribute("aria-valuenow", String(value));
|
|
172
|
-
segment.setAttribute("aria-valuetext", text2);
|
|
173
|
-
segment.setAttribute("aria-valuemin", String(AM));
|
|
174
|
-
segment.setAttribute("aria-valuemax", String(PM));
|
|
175
|
-
segment.textContent = text2;
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
328
|
+
const value = this.#displayValue(kind);
|
|
329
|
+
const text = kind === "meridiem" ? value === PM ? "PM" : "AM" : pad(value);
|
|
178
330
|
const { min, max } = this.#bounds(kind);
|
|
179
|
-
const text = String(value).padStart(2, "0");
|
|
180
331
|
segment.setAttribute("aria-valuenow", String(value));
|
|
181
332
|
segment.setAttribute("aria-valuetext", text);
|
|
182
333
|
segment.setAttribute("aria-valuemin", String(min));
|
|
183
334
|
segment.setAttribute("aria-valuemax", String(max));
|
|
184
335
|
segment.textContent = text;
|
|
185
336
|
}
|
|
186
|
-
/**
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
337
|
+
/** Writes a composed value to the optional form field and reports whether it changed. */
|
|
338
|
+
#writeField(value) {
|
|
339
|
+
if (!this.hasFieldTarget || this.fieldTarget.value === value) return false;
|
|
340
|
+
this.fieldTarget.value = value;
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
/** The canonical form value composed as `HH:MM[:SS]`. */
|
|
344
|
+
get #composedValue() {
|
|
345
|
+
const parts = [pad(this.#state.hour), pad(this.#state.minute)];
|
|
190
346
|
if (this.secondsValue) parts.push(pad(this.#state.second));
|
|
191
|
-
|
|
192
|
-
if (this.hasFieldTarget && this.fieldTarget.value !== value) {
|
|
193
|
-
this.fieldTarget.value = value;
|
|
194
|
-
}
|
|
195
|
-
if (notify && value !== this.#lastValue) this.dispatch("change", { detail: { value } });
|
|
196
|
-
this.#lastValue = value;
|
|
347
|
+
return parts.join(":");
|
|
197
348
|
}
|
|
198
|
-
/**
|
|
199
|
-
#
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
349
|
+
/** Returns a canonical unit in the presentation form exposed by its segment. */
|
|
350
|
+
#displayValue(kind) {
|
|
351
|
+
if (kind === "hour") {
|
|
352
|
+
return this.hourCycleValue === 12 ? this.#state.hour % 12 || 12 : this.#state.hour;
|
|
353
|
+
}
|
|
354
|
+
if (kind === "meridiem") return this.#state.hour >= 12 ? PM : AM;
|
|
355
|
+
return this.#state[kind];
|
|
203
356
|
}
|
|
204
|
-
/** The inclusive
|
|
357
|
+
/** The inclusive controller-owned bounds for a segment kind. */
|
|
205
358
|
#bounds(kind) {
|
|
206
359
|
switch (kind) {
|
|
207
360
|
case "hour":
|
|
@@ -213,6 +366,38 @@ var TimePickerController = class extends Controller {
|
|
|
213
366
|
return { min: AM, max: PM };
|
|
214
367
|
}
|
|
215
368
|
}
|
|
369
|
+
/** Watches retained segment state while excluding this controller's own reflections. */
|
|
370
|
+
#observeMutations() {
|
|
371
|
+
const observer = new MutationObserver((records) => {
|
|
372
|
+
const changed = records.some(
|
|
373
|
+
({ target }) => target instanceof HTMLElement && this.segmentTargets.includes(target)
|
|
374
|
+
);
|
|
375
|
+
if (!changed) return;
|
|
376
|
+
this.#domDirty = true;
|
|
377
|
+
this.#clearTypeBuffer();
|
|
378
|
+
this.#reconcile.schedule();
|
|
379
|
+
});
|
|
380
|
+
this.#observer = observer;
|
|
381
|
+
this.#observeWith(observer);
|
|
382
|
+
}
|
|
383
|
+
/** Registers retained attribute observation on the controller subtree. */
|
|
384
|
+
#observeWith(observer) {
|
|
385
|
+
observer.observe(this.element, {
|
|
386
|
+
subtree: true,
|
|
387
|
+
attributes: true,
|
|
388
|
+
attributeFilter: ["aria-valuenow", "data-segment"]
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/** Temporarily pauses observation so derived ARIA writes cannot become DOM input. */
|
|
392
|
+
#withoutObservation(run) {
|
|
393
|
+
const observer = this.#observer;
|
|
394
|
+
observer?.disconnect();
|
|
395
|
+
try {
|
|
396
|
+
run();
|
|
397
|
+
} finally {
|
|
398
|
+
if (observer && this.#connected) this.#observeWith(observer);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
216
401
|
/** Reads a segment's declared kind, or null when absent/invalid. */
|
|
217
402
|
#kindOf(segment) {
|
|
218
403
|
const kind = segment.getAttribute("data-segment");
|
|
@@ -221,7 +406,14 @@ var TimePickerController = class extends Controller {
|
|
|
221
406
|
}
|
|
222
407
|
return null;
|
|
223
408
|
}
|
|
409
|
+
/** Canonical state as seconds since the start of its nominal day. */
|
|
410
|
+
get #secondsOfDay() {
|
|
411
|
+
return this.#state.hour * SECONDS_PER_HOUR + this.#state.minute * SECONDS_PER_MINUTE + this.#state.second;
|
|
412
|
+
}
|
|
224
413
|
};
|
|
414
|
+
function modulo(value, modulus) {
|
|
415
|
+
return (value % modulus + modulus) % modulus;
|
|
416
|
+
}
|
|
225
417
|
function pad(value) {
|
|
226
418
|
return String(value).padStart(2, "0");
|
|
227
419
|
}
|