stimeo-ui 0.16.0 → 0.18.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 +136 -0
- data/README.md +27 -11
- data/dist/cable/index.js +384 -449
- data/dist/controllers/accordion_controller.js +174 -55
- data/dist/controllers/alert_dialog_controller.js +903 -235
- data/dist/controllers/announcer_controller.js +238 -205
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +414 -145
- data/dist/controllers/avatar_controller.js +185 -107
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +282 -58
- data/dist/controllers/calendar_controller.js +228 -260
- data/dist/controllers/carousel_controller.js +536 -436
- data/dist/controllers/character_counter_controller.js +493 -207
- data/dist/controllers/checkbox_controller.js +38 -74
- data/dist/controllers/clipboard_controller.js +261 -149
- data/dist/controllers/collapsible_controller.js +304 -120
- data/dist/controllers/color_picker_controller.js +178 -146
- data/dist/controllers/combobox_controller.js +522 -156
- data/dist/controllers/command_palette_controller.js +1156 -395
- data/dist/controllers/conditional_fields_controller.js +300 -171
- data/dist/controllers/confirm_controller.js +980 -262
- data/dist/controllers/context_menu_controller.js +346 -153
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +281 -158
- data/dist/controllers/currency_input_controller.js +575 -225
- data/dist/controllers/data_grid_controller.js +248 -214
- data/dist/controllers/date_range_picker_controller.js +338 -235
- data/dist/controllers/dialog_controller.js +899 -237
- data/dist/controllers/direct_upload_controller.js +97 -167
- data/dist/controllers/dirty_form_controller.js +72 -69
- data/dist/controllers/dismissible_controller.js +196 -24
- data/dist/controllers/drawer_controller.js +974 -355
- data/dist/controllers/dropdown_controller.js +339 -96
- data/dist/controllers/editable_controller.js +315 -88
- data/dist/controllers/empty_state_controller.js +79 -58
- data/dist/controllers/file_dropzone_controller.js +322 -211
- data/dist/controllers/filter_controller.js +77 -42
- data/dist/controllers/flash_controller.js +238 -347
- data/dist/controllers/focus_controller.js +679 -240
- data/dist/controllers/form_field_controller.js +156 -132
- data/dist/controllers/form_validation_controller.js +31 -97
- data/dist/controllers/frame_loading_controller.js +275 -217
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +327 -130
- data/dist/controllers/idle_controller.js +271 -101
- data/dist/controllers/input_mask_controller.js +483 -97
- data/dist/controllers/intersection_controller.js +150 -106
- data/dist/controllers/lazy_frame_controller.js +24 -68
- data/dist/controllers/listbox_controller.js +523 -240
- data/dist/controllers/local_time_controller.js +102 -77
- data/dist/controllers/masonry_controller.js +80 -96
- data/dist/controllers/menu_controller.js +302 -188
- data/dist/controllers/menubar_controller.js +232 -345
- data/dist/controllers/meter_controller.js +417 -87
- data/dist/controllers/multi_select_controller.js +785 -393
- data/dist/controllers/navigation_menu_controller.js +190 -250
- data/dist/controllers/nested_form_controller.js +396 -192
- data/dist/controllers/network_status_controller.js +268 -62
- data/dist/controllers/number_input_controller.js +610 -303
- data/dist/controllers/optimistic_controller.js +45 -87
- data/dist/controllers/otp_controller.js +658 -255
- data/dist/controllers/overflow_indicator_controller.js +122 -113
- data/dist/controllers/overflow_menu_controller.js +470 -295
- data/dist/controllers/pagination_controller.js +332 -123
- data/dist/controllers/password_reveal_controller.js +392 -121
- data/dist/controllers/password_strength_controller.js +111 -158
- data/dist/controllers/persist_controller.js +165 -111
- data/dist/controllers/pointer_drag_controller.js +434 -188
- data/dist/controllers/popover_controller.js +269 -113
- data/dist/controllers/portal_controller.js +215 -76
- data/dist/controllers/preview_guard_controller.js +243 -116
- data/dist/controllers/progress_controller.js +391 -78
- data/dist/controllers/radio_group_controller.js +278 -211
- data/dist/controllers/range_slider_controller.js +220 -113
- data/dist/controllers/rating_controller.js +458 -211
- data/dist/controllers/read_more_controller.js +138 -63
- data/dist/controllers/reading_progress_controller.js +166 -137
- data/dist/controllers/relative_time_controller.js +211 -112
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
- data/dist/controllers/resizable_controller.js +299 -150
- data/dist/controllers/roving_controller.js +49 -88
- data/dist/controllers/scroll_area_controller.js +490 -246
- data/dist/controllers/scroll_restore_controller.js +14 -55
- data/dist/controllers/scroll_visibility_controller.js +432 -149
- data/dist/controllers/scrollspy_controller.js +193 -248
- data/dist/controllers/separator_controller.js +285 -142
- data/dist/controllers/sidebar_controller.js +1069 -404
- data/dist/controllers/skeleton_controller.js +259 -101
- data/dist/controllers/slider_controller.js +187 -86
- data/dist/controllers/smart_sticky_header_controller.js +173 -66
- data/dist/controllers/sortable_controller.js +26 -125
- data/dist/controllers/spinner_controller.js +288 -173
- data/dist/controllers/step_indicator_controller.js +105 -46
- data/dist/controllers/stepper_controller.js +120 -73
- data/dist/controllers/stick_to_bottom_controller.js +275 -140
- data/dist/controllers/sticky_observer_controller.js +220 -49
- data/dist/controllers/submit_once_controller.js +376 -227
- data/dist/controllers/switch_controller.js +201 -84
- data/dist/controllers/tabs_controller.js +41 -30
- data/dist/controllers/tags_input_controller.js +293 -173
- data/dist/controllers/textarea_autosize_controller.js +108 -74
- data/dist/controllers/theme_controller.js +140 -116
- data/dist/controllers/time_picker_controller.js +331 -127
- data/dist/controllers/toast_controller.js +244 -324
- data/dist/controllers/toggle_group_controller.js +252 -191
- data/dist/controllers/toolbar_controller.js +219 -120
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +283 -369
- data/dist/index.js +8573 -10410
- data/dist/positioning/index.js +154 -47
- data/lib/generators/stimeo/install/install_generator.rb +7 -2
- data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,8 +1,158 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function setAttributeIfChanged(element, name, value, beforeWrite) {
|
|
4
|
+
if (element.getAttribute(name) === value) return false;
|
|
5
|
+
if (value === null) element.removeAttribute(name);
|
|
6
|
+
else element.setAttribute(name, value);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sharedRegistry(key, create) {
|
|
11
|
+
const symbol = Symbol.for(key);
|
|
12
|
+
const scope = globalThis;
|
|
13
|
+
const existing = scope[symbol];
|
|
14
|
+
if (existing !== void 0) return existing;
|
|
15
|
+
const registry2 = create();
|
|
16
|
+
Object.defineProperty(globalThis, symbol, { value: registry2 });
|
|
17
|
+
return registry2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var registry = sharedRegistry(
|
|
21
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
22
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
23
|
+
);
|
|
24
|
+
function isAttributeValue(value) {
|
|
25
|
+
return value === null || typeof value === "string";
|
|
26
|
+
}
|
|
27
|
+
function readLeaseRecord(element, record) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
30
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
31
|
+
const [original, written, ...rest] = parsed;
|
|
32
|
+
const linked = rest[0] !== false;
|
|
33
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
34
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
35
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
36
|
+
} catch {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function attributeStack(element, attribute) {
|
|
41
|
+
const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
42
|
+
registry.stacks.set(element, byAttribute);
|
|
43
|
+
const known = byAttribute.get(attribute);
|
|
44
|
+
if (known) return known;
|
|
45
|
+
const found = [];
|
|
46
|
+
for (const record of registry.names.get(attribute) ?? []) {
|
|
47
|
+
const read = readLeaseRecord(element, record);
|
|
48
|
+
if (read) found.push({ ...read, record });
|
|
49
|
+
}
|
|
50
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
51
|
+
const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
52
|
+
lease: null,
|
|
53
|
+
record,
|
|
54
|
+
written,
|
|
55
|
+
original,
|
|
56
|
+
linked: linked && index > 0
|
|
57
|
+
}));
|
|
58
|
+
byAttribute.set(attribute, stack);
|
|
59
|
+
return stack;
|
|
60
|
+
}
|
|
61
|
+
function findHold(stack, lease, record) {
|
|
62
|
+
return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
|
|
63
|
+
}
|
|
64
|
+
var AttributeLease = class {
|
|
65
|
+
#attribute;
|
|
66
|
+
#record;
|
|
67
|
+
#base;
|
|
68
|
+
#preserveAuthor;
|
|
69
|
+
#held = /* @__PURE__ */ new Set();
|
|
70
|
+
constructor(attribute, owner, options = {}) {
|
|
71
|
+
this.#attribute = attribute;
|
|
72
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
73
|
+
this.#base = options.base === true;
|
|
74
|
+
this.#preserveAuthor = options.preserveAuthor === true;
|
|
75
|
+
const names = registry.names;
|
|
76
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
77
|
+
}
|
|
78
|
+
write(element, value) {
|
|
79
|
+
const stack = attributeStack(element, this.#attribute);
|
|
80
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
81
|
+
this.#held.add(element);
|
|
82
|
+
const shows = !this.#covered(element, stack, hold);
|
|
83
|
+
hold.written = value;
|
|
84
|
+
this.#mark(element, stack);
|
|
85
|
+
if (shows) this.#reflect(element, value);
|
|
86
|
+
}
|
|
87
|
+
return(element) {
|
|
88
|
+
const held = this.#held.delete(element);
|
|
89
|
+
const known = registry.stacks.get(element)?.has(this.#attribute);
|
|
90
|
+
const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
|
|
91
|
+
const hold = this.#take(stack);
|
|
92
|
+
if (!hold) return false;
|
|
93
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
94
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
95
|
+
const index = stack.indexOf(hold);
|
|
96
|
+
stack.splice(index, 1);
|
|
97
|
+
const above = stack[index];
|
|
98
|
+
if (above) above.linked &&= hold.linked;
|
|
99
|
+
this.#mark(element, stack);
|
|
100
|
+
if (!owns) return true;
|
|
101
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
102
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
returnAll() {
|
|
106
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
107
|
+
}
|
|
108
|
+
#take(stack) {
|
|
109
|
+
const hold = findHold(stack, this, this.#record);
|
|
110
|
+
if (hold) hold.lease = this;
|
|
111
|
+
return hold;
|
|
112
|
+
}
|
|
113
|
+
#acquire(element, stack) {
|
|
114
|
+
const current = element.getAttribute(this.#attribute);
|
|
115
|
+
const top = stack.at(-1);
|
|
116
|
+
const bottom = stack[0];
|
|
117
|
+
const over = top?.written === current ? top : void 0;
|
|
118
|
+
const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
|
|
119
|
+
const hold = {
|
|
120
|
+
lease: this,
|
|
121
|
+
record: this.#record,
|
|
122
|
+
written: current,
|
|
123
|
+
original: from ? from.original : current,
|
|
124
|
+
linked: !this.#base && over !== void 0
|
|
125
|
+
};
|
|
126
|
+
if (this.#base && bottom) {
|
|
127
|
+
bottom.linked = true;
|
|
128
|
+
stack.unshift(hold);
|
|
129
|
+
} else stack.push(hold);
|
|
130
|
+
return hold;
|
|
131
|
+
}
|
|
132
|
+
#covered(element, stack, hold) {
|
|
133
|
+
const current = element.getAttribute(this.#attribute);
|
|
134
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
135
|
+
}
|
|
136
|
+
#mark(element, stack) {
|
|
137
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
138
|
+
stack.forEach((hold, index) => {
|
|
139
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
140
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
141
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
142
|
+
records.set(
|
|
143
|
+
hold.record,
|
|
144
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
for (const [name, recorded] of records) {
|
|
148
|
+
setAttributeIfChanged(element, name, recorded);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
#reflect(element, value) {
|
|
152
|
+
setAttributeIfChanged(element, this.#attribute, value);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
4
155
|
|
|
5
|
-
// src/utils/escape_layer.ts
|
|
6
156
|
function claimsWhileFocusWithin(element) {
|
|
7
157
|
return () => {
|
|
8
158
|
const active = element.ownerDocument.activeElement;
|
|
@@ -10,89 +160,68 @@ function claimsWhileFocusWithin(element) {
|
|
|
10
160
|
};
|
|
11
161
|
}
|
|
12
162
|
var EscapeLayer = class _EscapeLayer {
|
|
13
|
-
static #registries =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
#claims = null;
|
|
19
|
-
/**
|
|
20
|
-
* Activates this layer at the top of its document's Escape stack, installing
|
|
21
|
-
* the document's shared resolver listener if this is its first layer.
|
|
22
|
-
* Re-activating an already-active layer moves it to the top.
|
|
23
|
-
*/
|
|
163
|
+
static #registries = sharedRegistry(
|
|
164
|
+
"stimeo-ui.escape-layer.registry.v1",
|
|
165
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
166
|
+
);
|
|
167
|
+
#active = null;
|
|
24
168
|
activate(ownerDocument = document, options) {
|
|
25
169
|
this.deactivate();
|
|
26
|
-
let
|
|
27
|
-
if (!
|
|
28
|
-
|
|
29
|
-
_EscapeLayer.#registries.set(ownerDocument,
|
|
30
|
-
ownerDocument.addEventListener("keydown",
|
|
170
|
+
let registry2 = _EscapeLayer.#registries.get(ownerDocument);
|
|
171
|
+
if (!registry2) {
|
|
172
|
+
registry2 = _EscapeLayer.#createRegistry();
|
|
173
|
+
_EscapeLayer.#registries.set(ownerDocument, registry2);
|
|
174
|
+
ownerDocument.addEventListener("keydown", registry2.onKeydown);
|
|
31
175
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.#
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Removes this layer from its document's Escape stack, uninstalling the
|
|
39
|
-
* shared listener when the stack empties. Safe to call when inactive.
|
|
40
|
-
*/
|
|
176
|
+
const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
|
|
177
|
+
registry2.stack.push(entry);
|
|
178
|
+
this.#active = { document: ownerDocument, entry };
|
|
179
|
+
}
|
|
41
180
|
deactivate() {
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
const index =
|
|
47
|
-
if (index >= 0)
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
_EscapeLayer.#registries.delete(
|
|
181
|
+
const active = this.#active;
|
|
182
|
+
if (!active) return;
|
|
183
|
+
const registry2 = _EscapeLayer.#registries.get(active.document);
|
|
184
|
+
if (registry2) {
|
|
185
|
+
const index = registry2.stack.lastIndexOf(active.entry);
|
|
186
|
+
if (index >= 0) registry2.stack.splice(index, 1);
|
|
187
|
+
if (registry2.stack.length === 0) {
|
|
188
|
+
active.document.removeEventListener("keydown", registry2.onKeydown);
|
|
189
|
+
_EscapeLayer.#registries.delete(active.document);
|
|
51
190
|
}
|
|
52
191
|
}
|
|
53
|
-
this.#
|
|
54
|
-
|
|
55
|
-
this.#claims = null;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Whether this active layer would own a press right now: it is the topmost
|
|
59
|
-
* layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
|
|
60
|
-
* and diagnostics — production dismissal goes through the shared listener.
|
|
61
|
-
*/
|
|
192
|
+
this.#active = null;
|
|
193
|
+
}
|
|
62
194
|
get ownsEscape() {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
return _EscapeLayer.#resolveOwner(
|
|
195
|
+
const active = this.#active;
|
|
196
|
+
if (!active) return false;
|
|
197
|
+
const registry2 = _EscapeLayer.#registries.get(active.document);
|
|
198
|
+
if (!registry2) return false;
|
|
199
|
+
return _EscapeLayer.#resolveOwner(registry2.stack) === active.entry;
|
|
68
200
|
}
|
|
69
|
-
/** Builds a document's registry with its shared resolver listener. */
|
|
70
201
|
static #createRegistry() {
|
|
71
|
-
const
|
|
202
|
+
const registry2 = {
|
|
72
203
|
stack: [],
|
|
73
204
|
onKeydown: (event) => {
|
|
74
205
|
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
75
|
-
const owner = _EscapeLayer.#resolveOwner(
|
|
206
|
+
const owner = _EscapeLayer.#resolveOwner(registry2.stack);
|
|
76
207
|
if (!owner) return;
|
|
77
208
|
event.preventDefault();
|
|
78
|
-
owner
|
|
209
|
+
owner.onDismiss();
|
|
79
210
|
}
|
|
80
211
|
};
|
|
81
|
-
return
|
|
212
|
+
return registry2;
|
|
82
213
|
}
|
|
83
|
-
/** The topmost stack layer whose claims predicate passes, or `null`. */
|
|
84
214
|
static #resolveOwner(stack) {
|
|
85
215
|
for (let index = stack.length - 1; index >= 0; index--) {
|
|
86
216
|
const layer = stack[index];
|
|
87
217
|
if (!layer) continue;
|
|
88
|
-
if (layer
|
|
218
|
+
if (layer.claims && !layer.claims()) continue;
|
|
89
219
|
return layer;
|
|
90
220
|
}
|
|
91
221
|
return null;
|
|
92
222
|
}
|
|
93
223
|
};
|
|
94
224
|
|
|
95
|
-
// src/utils/focus_candidate.ts
|
|
96
225
|
function inheritsFieldsetDisabled(control) {
|
|
97
226
|
let fieldset = control.closest("fieldset[disabled]");
|
|
98
227
|
while (fieldset) {
|
|
@@ -102,8 +231,19 @@ function inheritsFieldsetDisabled(control) {
|
|
|
102
231
|
}
|
|
103
232
|
return false;
|
|
104
233
|
}
|
|
234
|
+
function unslotted(element) {
|
|
235
|
+
return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
|
|
236
|
+
}
|
|
237
|
+
function flatTreeParent(element) {
|
|
238
|
+
if (unslotted(element)) return null;
|
|
239
|
+
const parent = element.assignedSlot ?? element.parentNode;
|
|
240
|
+
if (parent instanceof ShadowRoot) return parent.host;
|
|
241
|
+
return parent instanceof Element ? parent : null;
|
|
242
|
+
}
|
|
105
243
|
function canTakeFocus(element) {
|
|
106
|
-
|
|
244
|
+
for (let node = element; node; node = flatTreeParent(node)) {
|
|
245
|
+
if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
|
|
246
|
+
}
|
|
107
247
|
if (element instanceof HTMLInputElement && element.type === "hidden") return false;
|
|
108
248
|
if (!("disabled" in element)) return true;
|
|
109
249
|
if (element.disabled) return false;
|
|
@@ -167,7 +307,17 @@ function firstTabStop(root) {
|
|
|
167
307
|
return null;
|
|
168
308
|
}
|
|
169
309
|
|
|
170
|
-
|
|
310
|
+
var ListenerSet = class {
|
|
311
|
+
#abort = new AbortController();
|
|
312
|
+
add(target, type, handler, options) {
|
|
313
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
314
|
+
}
|
|
315
|
+
dispose() {
|
|
316
|
+
this.#abort.abort();
|
|
317
|
+
this.#abort = new AbortController();
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
171
321
|
function observeScrollDismiss(element, onScroll) {
|
|
172
322
|
const targets = [...scrollParents(element), window];
|
|
173
323
|
const handler = () => onScroll();
|
|
@@ -192,7 +342,6 @@ function isScrollable(element) {
|
|
|
192
342
|
return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);
|
|
193
343
|
}
|
|
194
344
|
|
|
195
|
-
// src/utils/state_reason.ts
|
|
196
345
|
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
197
346
|
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
198
347
|
function stateReasonFor(event) {
|
|
@@ -202,7 +351,6 @@ function stateReasonFor(event) {
|
|
|
202
351
|
return "user";
|
|
203
352
|
}
|
|
204
353
|
|
|
205
|
-
// src/controllers/popover_controller.ts
|
|
206
354
|
var PopoverController = class extends Controller {
|
|
207
355
|
static targets = ["trigger", "panel"];
|
|
208
356
|
static values = {
|
|
@@ -210,34 +358,43 @@ var PopoverController = class extends Controller {
|
|
|
210
358
|
};
|
|
211
359
|
static actions = ["close", "open", "toggle"];
|
|
212
360
|
static events = ["close", "open"];
|
|
213
|
-
/** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */
|
|
214
361
|
#stopScrollDismiss = null;
|
|
215
|
-
/** Escape-stack membership while open; the shared resolver dismisses via it. */
|
|
216
362
|
#escapeLayer = new EscapeLayer();
|
|
217
|
-
|
|
363
|
+
#expanded = new AttributeLease("aria-expanded", this.identifier);
|
|
364
|
+
#shownPanel = null;
|
|
365
|
+
#connected = false;
|
|
218
366
|
#reporting = false;
|
|
219
|
-
|
|
367
|
+
#listeners = new ListenerSet();
|
|
220
368
|
connect() {
|
|
221
369
|
this.#close("api");
|
|
222
|
-
|
|
223
|
-
this.element
|
|
370
|
+
this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
|
|
371
|
+
this.#listeners.add(this.element, "focusout", this.#onFocusOut);
|
|
372
|
+
this.#connected = true;
|
|
224
373
|
this.#reporting = true;
|
|
225
374
|
}
|
|
226
|
-
/**
|
|
227
|
-
* Removes every standing listener registered in {@link connect} plus any active
|
|
228
|
-
* dismiss-on-scroll observers. `removeEventListener` is a no-op when it was
|
|
229
|
-
* never added, so this is safe in the closed state too — no listener outlives
|
|
230
|
-
* the element after a Turbo navigation.
|
|
231
|
-
*/
|
|
232
375
|
disconnect() {
|
|
376
|
+
this.#connected = false;
|
|
233
377
|
this.#reporting = false;
|
|
234
|
-
this.#
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.#
|
|
378
|
+
this.#release();
|
|
379
|
+
this.#listeners.dispose();
|
|
380
|
+
}
|
|
381
|
+
closeOnScrollValueChanged() {
|
|
382
|
+
if (this.#shownPanel) this.#syncScrollDismiss();
|
|
383
|
+
}
|
|
384
|
+
panelTargetDisconnected(panel) {
|
|
385
|
+
if (panel !== this.#shownPanel) return;
|
|
386
|
+
if (this.panelTargets.includes(panel)) return;
|
|
387
|
+
this.#release();
|
|
388
|
+
panel.hidden = true;
|
|
389
|
+
this.#reflectClosed();
|
|
390
|
+
}
|
|
391
|
+
triggerTargetConnected() {
|
|
392
|
+
if (this.#connected) this.#reflectTrigger();
|
|
393
|
+
}
|
|
394
|
+
triggerTargetDisconnected(trigger) {
|
|
395
|
+
if (!this.triggerTargets.includes(trigger)) this.#expanded.return(trigger);
|
|
396
|
+
if (this.#connected) this.#reflectTrigger();
|
|
239
397
|
}
|
|
240
|
-
/** Toggles the popover. Bound via `data-action` (click on the trigger). */
|
|
241
398
|
toggle(event) {
|
|
242
399
|
if (this.#isOpen) {
|
|
243
400
|
this.close(event);
|
|
@@ -245,46 +402,54 @@ var PopoverController = class extends Controller {
|
|
|
245
402
|
this.open(event);
|
|
246
403
|
}
|
|
247
404
|
}
|
|
248
|
-
/** Opens the panel, reflects state, and moves focus inside it. */
|
|
249
405
|
open(event) {
|
|
250
406
|
this.#open(stateReasonFor(event));
|
|
251
407
|
}
|
|
252
|
-
/** Closes the panel and reflects the collapsed state. Bound via `data-action`. */
|
|
253
408
|
close(event) {
|
|
254
409
|
this.#close(stateReasonFor(event));
|
|
255
410
|
}
|
|
256
|
-
/**
|
|
257
|
-
* Opens the panel, reflects state, reports a move, and moves focus inside it.
|
|
258
|
-
*
|
|
259
|
-
* @stimeoRuntimeOnly `closeOnScroll` decides whether this opening wires the scroll dismissal;
|
|
260
|
-
* what is shown does not depend on it.
|
|
261
|
-
*/
|
|
262
411
|
#open(reason) {
|
|
263
412
|
if (!this.hasPanelTarget || this.#isOpen) return;
|
|
264
|
-
this.panelTarget
|
|
265
|
-
|
|
413
|
+
const panel = this.panelTarget;
|
|
414
|
+
panel.hidden = false;
|
|
415
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "true");
|
|
416
|
+
this.#shownPanel = panel;
|
|
266
417
|
if (this.#reporting) this.dispatch("open", { detail: { reason }, cancelable: false });
|
|
267
|
-
if (!this.#isOpen) return;
|
|
418
|
+
if (this.#shownPanel !== panel || !this.#isOpen) return;
|
|
268
419
|
this.#escapeLayer.activate(document, {
|
|
269
420
|
onDismiss: () => this.#closeAndRestore(),
|
|
270
421
|
claims: claimsWhileFocusWithin(this.element)
|
|
271
422
|
});
|
|
272
|
-
|
|
273
|
-
this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.#close("scroll"));
|
|
274
|
-
}
|
|
423
|
+
this.#syncScrollDismiss();
|
|
275
424
|
this.#focusFirst();
|
|
276
425
|
}
|
|
277
|
-
/** Closes the panel, reflects the collapsed state, and reports a move. */
|
|
278
426
|
#close(reason) {
|
|
279
427
|
const was = this.#isOpen;
|
|
428
|
+
this.#release();
|
|
429
|
+
this.#reflectClosed();
|
|
430
|
+
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
431
|
+
}
|
|
432
|
+
#reflectClosed() {
|
|
433
|
+
if (this.hasPanelTarget) this.panelTarget.hidden = true;
|
|
434
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, "false");
|
|
435
|
+
}
|
|
436
|
+
#reflectTrigger() {
|
|
437
|
+
if (this.hasTriggerTarget) this.#expanded.write(this.triggerTarget, String(this.#isOpen));
|
|
438
|
+
}
|
|
439
|
+
#release() {
|
|
440
|
+
this.#shownPanel = null;
|
|
280
441
|
this.#escapeLayer.deactivate();
|
|
281
442
|
this.#stopScrollDismiss?.();
|
|
282
443
|
this.#stopScrollDismiss = null;
|
|
283
|
-
if (this.hasPanelTarget) this.panelTarget.hidden = true;
|
|
284
|
-
if (this.hasTriggerTarget) this.triggerTarget.setAttribute("aria-expanded", "false");
|
|
285
|
-
if (was && this.#reporting) this.dispatch("close", { detail: { reason }, cancelable: false });
|
|
286
444
|
}
|
|
287
|
-
|
|
445
|
+
#syncScrollDismiss() {
|
|
446
|
+
if (this.closeOnScrollValue) {
|
|
447
|
+
this.#stopScrollDismiss ??= observeScrollDismiss(this.element, () => this.#close("scroll"));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
this.#stopScrollDismiss?.();
|
|
451
|
+
this.#stopScrollDismiss = null;
|
|
452
|
+
}
|
|
288
453
|
#focusFirst() {
|
|
289
454
|
const first = firstTabStop(this.panelTarget);
|
|
290
455
|
if (first) {
|
|
@@ -294,38 +459,29 @@ var PopoverController = class extends Controller {
|
|
|
294
459
|
if (!this.panelTarget.hasAttribute("tabindex")) this.panelTarget.tabIndex = -1;
|
|
295
460
|
this.panelTarget.focus();
|
|
296
461
|
}
|
|
297
|
-
/** Closes and restores focus to the trigger for explicit keyboard dismissal. */
|
|
298
462
|
#closeAndRestore() {
|
|
299
463
|
this.#close("escape");
|
|
300
464
|
if (this.hasTriggerTarget) this.triggerTarget.focus();
|
|
301
465
|
}
|
|
302
|
-
/** Closes without moving focus when a click lands outside the controller element. */
|
|
303
466
|
#onOutsideClick = (event) => {
|
|
304
467
|
const target = event.target;
|
|
305
|
-
if (this.#
|
|
468
|
+
if (this.#closable && target instanceof Node && !this.element.contains(target)) {
|
|
306
469
|
this.#close("outside");
|
|
307
470
|
}
|
|
308
471
|
};
|
|
309
|
-
/**
|
|
310
|
-
* Closes when focus leaves the controller for a known external destination
|
|
311
|
-
* (e.g. forward Tab past the panel or reverse Tab past the trigger). Focus is
|
|
312
|
-
* not restored — the natural destination is kept, which is the modeless
|
|
313
|
-
* contract. A null/non-Node destination is indeterminate: browsers use it for
|
|
314
|
-
* clicks on non-focusable content and window deactivation, so the later outside
|
|
315
|
-
* click handler decides pointer dismissal instead.
|
|
316
|
-
*/
|
|
317
472
|
#onFocusOut = (event) => {
|
|
318
|
-
if (!this.#
|
|
473
|
+
if (!this.#closable) return;
|
|
319
474
|
const next = event.relatedTarget;
|
|
320
475
|
if (!(next instanceof Node) || this.element.contains(next)) return;
|
|
321
476
|
this.#close("focus");
|
|
322
477
|
};
|
|
323
|
-
/** Whether the panel is currently visible. */
|
|
324
478
|
get #isOpen() {
|
|
325
479
|
return this.hasPanelTarget && !this.panelTarget.hidden;
|
|
326
480
|
}
|
|
481
|
+
get #closable() {
|
|
482
|
+
return this.#isOpen || this.#shownPanel !== null;
|
|
483
|
+
}
|
|
327
484
|
};
|
|
328
485
|
|
|
329
486
|
export { PopoverController };
|
|
330
|
-
//# sourceMappingURL=popover_controller.js.map
|
|
331
487
|
//# sourceMappingURL=popover_controller.js.map
|