stimeo-ui 0.4.0 → 0.5.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 +68 -0
- data/dist/controllers/aspect_ratio_controller.js +1 -1
- data/dist/controllers/breadcrumb_controller.js +5 -1
- data/dist/controllers/carousel_controller.js +5 -1
- data/dist/controllers/clipboard_controller.js +8 -3
- data/dist/controllers/collapsible_controller.js +4 -1
- data/dist/controllers/color_picker_controller.js +6 -2
- data/dist/controllers/context_menu_controller.js +2 -2
- data/dist/controllers/countdown_controller.js +5 -1
- data/dist/controllers/date_range_picker_controller.js +5 -1
- data/dist/controllers/direct_upload_controller.js +3 -3
- data/dist/controllers/empty_state_controller.js +107 -16
- data/dist/controllers/flash_controller.js +161 -21
- data/dist/controllers/form_validation_controller.js +8 -2
- data/dist/controllers/frame_loading_controller.js +94 -22
- data/dist/controllers/highlight_controller.js +38 -1
- data/dist/controllers/idle_controller.js +13 -2
- data/dist/controllers/local_time_controller.js +2 -0
- data/dist/controllers/masonry_controller.js +1 -1
- data/dist/controllers/meter_controller.js +3 -1
- data/dist/controllers/network_status_controller.js +1 -3
- data/dist/controllers/number_input_controller.js +191 -24
- data/dist/controllers/overflow_menu_controller.js +1 -1
- data/dist/controllers/pagination_controller.js +5 -1
- data/dist/controllers/password_strength_controller.js +1 -1
- data/dist/controllers/pointer_drag_controller.js +10 -0
- data/dist/controllers/portal_controller.js +10 -0
- data/dist/controllers/progress_controller.js +7 -3
- data/dist/controllers/range_slider_controller.js +385 -94
- data/dist/controllers/rating_controller.js +2 -0
- data/dist/controllers/relative_time_controller.js +2 -0
- data/dist/controllers/scroll_area_controller.js +1 -1
- data/dist/controllers/separator_controller.js +13 -17
- data/dist/controllers/skeleton_controller.js +71 -3
- data/dist/controllers/slider_controller.js +325 -48
- data/dist/controllers/spinner_controller.js +18 -3
- data/dist/controllers/step_indicator_controller.js +3 -1
- data/dist/controllers/stepper_controller.js +2 -0
- data/dist/controllers/switch_controller.js +162 -18
- data/dist/controllers/textarea_autosize_controller.js +1 -1
- data/dist/controllers/time_picker_controller.js +6 -3
- data/dist/controllers/tree_view_controller.js +19 -1
- data/dist/index.js +1214 -307
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -1,39 +1,181 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
3
|
// src/controllers/switch_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/default_attribute.ts
|
|
6
|
+
function setDefaultAttribute(element, name, value) {
|
|
7
|
+
if (element.hasAttribute(name)) return false;
|
|
8
|
+
element.setAttribute(name, value);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/utils/focus_candidate.ts
|
|
13
|
+
function inheritsFieldsetDisabled(control) {
|
|
14
|
+
let fieldset = control.closest("fieldset[disabled]");
|
|
15
|
+
while (fieldset) {
|
|
16
|
+
const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
|
|
17
|
+
if (!legend?.contains(control)) return true;
|
|
18
|
+
fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/utils/interactive_host.ts
|
|
24
|
+
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
25
|
+
function isInteractiveHost(element) {
|
|
26
|
+
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
27
|
+
let current = element;
|
|
28
|
+
while (current) {
|
|
29
|
+
const raw = current.getAttribute("contenteditable");
|
|
30
|
+
if (raw !== null) {
|
|
31
|
+
const value = raw.trim().toLowerCase();
|
|
32
|
+
if (value === "false") return false;
|
|
33
|
+
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
34
|
+
}
|
|
35
|
+
current = current.parentElement;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/controllers/switch_controller.ts
|
|
41
|
+
var OBSERVED_ATTRIBUTES = [
|
|
42
|
+
"role",
|
|
43
|
+
"aria-checked",
|
|
44
|
+
"tabindex",
|
|
45
|
+
"type",
|
|
46
|
+
"href",
|
|
47
|
+
"contenteditable",
|
|
48
|
+
"controls"
|
|
49
|
+
];
|
|
50
|
+
var OBSERVED_ANCESTOR_ATTRIBUTES = ["contenteditable"];
|
|
4
51
|
var SwitchController = class extends Controller {
|
|
5
52
|
static actions = ["onKeydown", "toggle"];
|
|
6
53
|
static events = ["changed"];
|
|
54
|
+
/** Defaults this instance introduced and may therefore remove safely. */
|
|
55
|
+
#ownedDefaults = /* @__PURE__ */ new Set();
|
|
56
|
+
/** Controller writes that must not be mistaken for authored morph changes. */
|
|
57
|
+
#internalAttributeValues = /* @__PURE__ */ new Map();
|
|
58
|
+
#attributeObserver = new MutationObserver((records) => {
|
|
59
|
+
this.#releaseAuthoredDefaults(records);
|
|
60
|
+
this.#attributeObserver.disconnect();
|
|
61
|
+
this.#reconcileDefaults();
|
|
62
|
+
this.#observeAttributes();
|
|
63
|
+
});
|
|
64
|
+
/** Blocks disabled pointer/native-key activation before consumer click handlers. */
|
|
65
|
+
#onClickCapture = (event) => {
|
|
66
|
+
if (!this.#isSupportedHost || !this.#isActivationDisabled) return;
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
event.stopImmediatePropagation();
|
|
69
|
+
};
|
|
7
70
|
/** Ensures the switch exposes a role and is keyboard-reachable. */
|
|
8
71
|
connect() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
72
|
+
this.#reconcileDefaults();
|
|
73
|
+
this.element.addEventListener("click", this.#onClickCapture, true);
|
|
74
|
+
this.#observeAttributes();
|
|
75
|
+
}
|
|
76
|
+
/** Releases the explicit click guard and retained-element attribute observer. */
|
|
77
|
+
disconnect() {
|
|
78
|
+
this.element.removeEventListener("click", this.#onClickCapture, true);
|
|
79
|
+
this.#attributeObserver.disconnect();
|
|
80
|
+
this.#internalAttributeValues.clear();
|
|
18
81
|
}
|
|
19
82
|
/** Toggles the checked state. Bound via `data-action` (click). */
|
|
20
83
|
toggle() {
|
|
84
|
+
if (!this.#isSupportedHost || this.#isActivationDisabled) return;
|
|
21
85
|
this.#checked = !this.#checked;
|
|
22
86
|
}
|
|
23
87
|
/**
|
|
24
|
-
* Activates the switch on Space/Enter for non-native hosts and
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
88
|
+
* Activates the switch on Space/Enter for non-native hosts and suppresses key
|
|
89
|
+
* repeat. Bound via `data-action` (keydown). A native `<button type="button">`
|
|
90
|
+
* owns the initial key-to-click synthesis; repeated keydowns are canceled before
|
|
91
|
+
* the browser can synthesize additional clicks.
|
|
28
92
|
*/
|
|
29
93
|
onKeydown(event) {
|
|
30
94
|
if (event.defaultPrevented) return;
|
|
31
|
-
if (this
|
|
32
|
-
if (event.
|
|
33
|
-
if (
|
|
95
|
+
if (!this.#isSupportedHost) return;
|
|
96
|
+
if (event.key !== " " && event.key !== "Enter") return;
|
|
97
|
+
if (this.#isActivationDisabled) {
|
|
34
98
|
event.preventDefault();
|
|
35
|
-
|
|
99
|
+
event.stopImmediatePropagation();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (this.element instanceof HTMLButtonElement) {
|
|
103
|
+
if (event.repeat) event.preventDefault();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
if (event.repeat) return;
|
|
108
|
+
this.toggle();
|
|
109
|
+
}
|
|
110
|
+
/** Re-adds only missing defaults, preserving every authored attribute value. */
|
|
111
|
+
#reconcileDefaults() {
|
|
112
|
+
if (!this.#isSupportedHost) {
|
|
113
|
+
this.#removeOwnedDefaults();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#setOwnedDefault("role", "switch");
|
|
117
|
+
this.#setOwnedDefault("aria-checked", "false");
|
|
118
|
+
if (!(this.element instanceof HTMLButtonElement)) {
|
|
119
|
+
this.#setOwnedDefault("tabindex", "0");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Records a newly introduced default without claiming authored markup. */
|
|
123
|
+
#setOwnedDefault(name, value) {
|
|
124
|
+
if (setDefaultAttribute(this.element, name, value)) this.#ownedDefaults.add(name);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Gives ownership back when a retained-element morph authors a value. Missing
|
|
128
|
+
* owned attributes stay owned so reconciliation can restore them.
|
|
129
|
+
*/
|
|
130
|
+
#releaseAuthoredDefaults(records) {
|
|
131
|
+
for (const record of records) {
|
|
132
|
+
if (record.target === this.element) {
|
|
133
|
+
const name = record.attributeName;
|
|
134
|
+
if (name && this.#ownedDefaults.has(name)) {
|
|
135
|
+
const value = this.element.getAttribute(name);
|
|
136
|
+
if (value !== null && this.#internalAttributeValues.get(name) !== value) {
|
|
137
|
+
this.#ownedDefaults.delete(name);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
this.#internalAttributeValues.clear();
|
|
143
|
+
}
|
|
144
|
+
/** Removes only defaults introduced by this instance when a host becomes invalid. */
|
|
145
|
+
#removeOwnedDefaults() {
|
|
146
|
+
for (const name of this.#ownedDefaults) this.element.removeAttribute(name);
|
|
147
|
+
this.#ownedDefaults.clear();
|
|
148
|
+
this.#internalAttributeValues.clear();
|
|
149
|
+
}
|
|
150
|
+
/** Watches retained host attributes that Turbo can morph without reconnecting. */
|
|
151
|
+
#observeAttributes() {
|
|
152
|
+
this.#attributeObserver.observe(this.element, {
|
|
153
|
+
attributes: true,
|
|
154
|
+
attributeFilter: OBSERVED_ATTRIBUTES
|
|
155
|
+
});
|
|
156
|
+
let ancestor = this.element.parentElement;
|
|
157
|
+
while (ancestor) {
|
|
158
|
+
this.#attributeObserver.observe(ancestor, {
|
|
159
|
+
attributes: true,
|
|
160
|
+
attributeFilter: OBSERVED_ANCESTOR_ATTRIBUTES
|
|
161
|
+
});
|
|
162
|
+
ancestor = ancestor.parentElement;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Whether the host has one activation model that this controller can own. */
|
|
166
|
+
get #isSupportedHost() {
|
|
167
|
+
if (this.element instanceof HTMLButtonElement) return this.element.type === "button";
|
|
168
|
+
return !isInteractiveHost(this.element);
|
|
169
|
+
}
|
|
170
|
+
/** Whether ARIA or native HTML semantics make the supported switch inoperable. */
|
|
171
|
+
get #isActivationDisabled() {
|
|
172
|
+
let current = this.element;
|
|
173
|
+
while (current) {
|
|
174
|
+
if (current.getAttribute("aria-disabled") === "true") return true;
|
|
175
|
+
current = current.parentElement;
|
|
36
176
|
}
|
|
177
|
+
if (!(this.element instanceof HTMLButtonElement)) return false;
|
|
178
|
+
return this.element.disabled || inheritsFieldsetDisabled(this.element);
|
|
37
179
|
}
|
|
38
180
|
/** Whether the switch is currently on. */
|
|
39
181
|
get #checked() {
|
|
@@ -41,7 +183,9 @@ var SwitchController = class extends Controller {
|
|
|
41
183
|
}
|
|
42
184
|
/** Reflects the new state on `aria-checked` and notifies listeners. */
|
|
43
185
|
set #checked(value) {
|
|
44
|
-
|
|
186
|
+
const reflected = value ? "true" : "false";
|
|
187
|
+
this.#internalAttributeValues.set("aria-checked", reflected);
|
|
188
|
+
this.element.setAttribute("aria-checked", reflected);
|
|
45
189
|
this.dispatch("changed", { detail: { checked: value } });
|
|
46
190
|
}
|
|
47
191
|
};
|
|
@@ -52,7 +52,7 @@ var TextareaAutosizeController = class extends Controller {
|
|
|
52
52
|
} else {
|
|
53
53
|
el.removeAttribute("data-at-max-rows");
|
|
54
54
|
}
|
|
55
|
-
el.style.setProperty("--stimeo
|
|
55
|
+
el.style.setProperty("--stimeo--textarea-rows", String(rows));
|
|
56
56
|
if (height !== this.#lastHeight) {
|
|
57
57
|
this.#lastHeight = height;
|
|
58
58
|
this.dispatch("resize", { detail: { height, rows } });
|
|
@@ -121,7 +121,6 @@ var TimePickerController = class extends Controller {
|
|
|
121
121
|
}
|
|
122
122
|
/** Propagates a wrap carry from `kind` into the next larger segment. */
|
|
123
123
|
#carry(kind, amount) {
|
|
124
|
-
if (amount === 0) return;
|
|
125
124
|
if (kind === "second") this.#step("minute", amount);
|
|
126
125
|
else if (kind === "minute") this.#step("hour", amount);
|
|
127
126
|
}
|
|
@@ -183,15 +182,19 @@ var TimePickerController = class extends Controller {
|
|
|
183
182
|
segment.setAttribute("aria-valuemax", String(max));
|
|
184
183
|
segment.textContent = text;
|
|
185
184
|
}
|
|
186
|
-
/** Composes `HH:MM[:SS]`
|
|
185
|
+
/** Composes `HH:MM[:SS]` and notifies both native-form and widget consumers. */
|
|
187
186
|
#syncField(notify) {
|
|
188
187
|
const h24 = this.#hours24();
|
|
189
188
|
const parts = [pad(h24), pad(this.#state.minute)];
|
|
190
189
|
if (this.secondsValue) parts.push(pad(this.#state.second));
|
|
191
190
|
const value = parts.join(":");
|
|
192
|
-
|
|
191
|
+
const fieldChanged = this.hasFieldTarget && this.fieldTarget.value !== value;
|
|
192
|
+
if (fieldChanged) {
|
|
193
193
|
this.fieldTarget.value = value;
|
|
194
194
|
}
|
|
195
|
+
if (notify && fieldChanged) {
|
|
196
|
+
this.fieldTarget.dispatchEvent(new Event("change", { bubbles: true }));
|
|
197
|
+
}
|
|
195
198
|
if (notify && value !== this.#lastValue) this.dispatch("change", { detail: { value } });
|
|
196
199
|
this.#lastValue = value;
|
|
197
200
|
}
|
|
@@ -30,6 +30,23 @@ function canTakeFocus(element) {
|
|
|
30
30
|
return !inheritsFieldsetDisabled(element);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// src/utils/interactive_host.ts
|
|
34
|
+
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
35
|
+
function isInteractiveHost(element) {
|
|
36
|
+
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
37
|
+
let current = element;
|
|
38
|
+
while (current) {
|
|
39
|
+
const raw = current.getAttribute("contenteditable");
|
|
40
|
+
if (raw !== null) {
|
|
41
|
+
const value = raw.trim().toLowerCase();
|
|
42
|
+
if (value === "false") return false;
|
|
43
|
+
if (value === "" || value === "true" || value === "plaintext-only") return true;
|
|
44
|
+
}
|
|
45
|
+
current = current.parentElement;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
// src/utils/roving_tabindex.ts
|
|
34
51
|
var RovingTabindex = class {
|
|
35
52
|
/** Returns the current ordered item elements; called on every operation. */
|
|
@@ -175,7 +192,7 @@ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel
|
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
// src/controllers/tree_view_controller.ts
|
|
178
|
-
var NESTED_INTERACTIVE =
|
|
195
|
+
var NESTED_INTERACTIVE = INTERACTIVE_HOST_SELECTOR;
|
|
179
196
|
var TreeViewController = class extends Controller {
|
|
180
197
|
static targets = ["item", "group"];
|
|
181
198
|
static actions = ["onClick", "onKeydown", "toggle"];
|
|
@@ -436,6 +453,7 @@ var TreeViewController = class extends Controller {
|
|
|
436
453
|
if (target.closest('[role="treeitem"]') !== item) return null;
|
|
437
454
|
const control = target.closest(NESTED_INTERACTIVE);
|
|
438
455
|
if (control && item.contains(control)) return null;
|
|
456
|
+
if (isInteractiveHost(target)) return null;
|
|
439
457
|
return item;
|
|
440
458
|
}
|
|
441
459
|
/**
|