stimeo-ui 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +178 -0
  3. data/dist/controllers/alert_dialog_controller.js +75 -4
  4. data/dist/controllers/aspect_ratio_controller.js +19 -11
  5. data/dist/controllers/avatar_controller.js +237 -40
  6. data/dist/controllers/carousel_controller.js +85 -9
  7. data/dist/controllers/character_counter_controller.js +338 -63
  8. data/dist/controllers/checkbox_controller.js +136 -25
  9. data/dist/controllers/color_picker_controller.js +35 -9
  10. data/dist/controllers/command_palette_controller.js +75 -4
  11. data/dist/controllers/conditional_fields_controller.js +345 -51
  12. data/dist/controllers/confirm_controller.js +75 -4
  13. data/dist/controllers/date_range_picker_controller.js +157 -30
  14. data/dist/controllers/dialog_controller.js +75 -4
  15. data/dist/controllers/direct_upload_controller.js +201 -45
  16. data/dist/controllers/dirty_form_controller.js +192 -29
  17. data/dist/controllers/dismissible_controller.js +83 -18
  18. data/dist/controllers/drawer_controller.js +75 -4
  19. data/dist/controllers/file_dropzone_controller.js +26 -3
  20. data/dist/controllers/focus_controller.js +75 -4
  21. data/dist/controllers/form_field_controller.js +280 -62
  22. data/dist/controllers/form_validation_controller.js +208 -83
  23. data/dist/controllers/idle_controller.js +27 -5
  24. data/dist/controllers/menubar_controller.js +5 -3
  25. data/dist/controllers/multi_select_controller.js +460 -151
  26. data/dist/controllers/number_input_controller.js +317 -51
  27. data/dist/controllers/overflow_menu_controller.js +6 -1
  28. data/dist/controllers/pagination_controller.js +35 -1
  29. data/dist/controllers/password_strength_controller.js +20 -2
  30. data/dist/controllers/persist_controller.js +449 -120
  31. data/dist/controllers/popover_controller.js +77 -4
  32. data/dist/controllers/radio_group_controller.js +540 -56
  33. data/dist/controllers/rating_controller.js +276 -89
  34. data/dist/controllers/resizable_controller.js +33 -0
  35. data/dist/controllers/roving_controller.js +60 -5
  36. data/dist/controllers/scroll_area_controller.js +557 -125
  37. data/dist/controllers/scroll_visibility_controller.js +33 -0
  38. data/dist/controllers/separator_controller.js +354 -38
  39. data/dist/controllers/sidebar_controller.js +83 -10
  40. data/dist/controllers/submit_once_controller.js +399 -121
  41. data/dist/controllers/tags_input_controller.js +356 -120
  42. data/dist/controllers/theme_controller.js +8 -6
  43. data/dist/controllers/time_picker_controller.js +296 -107
  44. data/dist/controllers/toggle_group_controller.js +378 -55
  45. data/dist/controllers/toolbar_controller.js +5 -3
  46. data/dist/controllers/tree_view_controller.js +7 -4
  47. data/dist/index.js +4963 -1581
  48. data/lib/stimeo/ui/version.rb +1 -1
  49. metadata +2 -2
@@ -2,8 +2,79 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/dismissible_controller.ts
4
4
 
5
- // src/utils/focus_trap.ts
6
- var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
5
+ // src/utils/focus_candidate.ts
6
+ function inheritsFieldsetDisabled(control) {
7
+ let fieldset = control.closest("fieldset[disabled]");
8
+ while (fieldset) {
9
+ const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
10
+ if (!legend?.contains(control)) return true;
11
+ fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
12
+ }
13
+ return false;
14
+ }
15
+ function canTakeFocus(element) {
16
+ if (element.closest("[hidden], [inert]")) return false;
17
+ if (element instanceof HTMLInputElement && element.type === "hidden") return false;
18
+ if (!("disabled" in element)) return true;
19
+ if (element.disabled) return false;
20
+ return !inheritsFieldsetDisabled(element);
21
+ }
22
+ var TAB_STOP_CANDIDATE_SELECTOR = [
23
+ "a[href]",
24
+ "area[href]",
25
+ "button",
26
+ "input",
27
+ "select",
28
+ "textarea",
29
+ "summary",
30
+ "iframe",
31
+ "audio[controls]",
32
+ "video[controls]",
33
+ "[tabindex]",
34
+ "[contenteditable]"
35
+ ].join(",");
36
+ function isRenderedForFocus(element) {
37
+ const check = element.checkVisibility;
38
+ return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
39
+ }
40
+ function authoredTabindex(element) {
41
+ const value = element.getAttribute("tabindex");
42
+ if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
43
+ return Number(value);
44
+ }
45
+ function hasNativeTabStop(element) {
46
+ if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
47
+ return element.hasAttribute("href");
48
+ }
49
+ if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
50
+ return true;
51
+ }
52
+ if (element instanceof HTMLInputElement) return element.type !== "hidden";
53
+ if (element instanceof HTMLIFrameElement) return true;
54
+ if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
55
+ return element.hasAttribute("controls");
56
+ }
57
+ if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
58
+ const details = element.parentElement;
59
+ return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
60
+ }
61
+ return false;
62
+ }
63
+ function hasEditableTabStop(element) {
64
+ const value = element.getAttribute("contenteditable")?.toLowerCase();
65
+ return value === "" || value === "true" || value === "plaintext-only";
66
+ }
67
+ function isTabStop(element) {
68
+ if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
69
+ const tabindex = authoredTabindex(element);
70
+ if (tabindex !== null) return tabindex >= 0;
71
+ return hasNativeTabStop(element) || hasEditableTabStop(element);
72
+ }
73
+ function tabStopsWithin(root) {
74
+ return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
75
+ isTabStop
76
+ );
77
+ }
7
78
 
8
79
  // src/controllers/dismissible_controller.ts
9
80
  var DismissibleController = class extends Controller {
@@ -80,35 +151,29 @@ var DismissibleController = class extends Controller {
80
151
  /** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */
81
152
  *#focusFallbacks(root) {
82
153
  const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;
83
- if (explicitFallback && !root.contains(explicitFallback) && this.#isFocusAvailable(explicitFallback)) {
154
+ if (explicitFallback && !root.contains(explicitFallback) && this.#isExplicitFocusAvailable(explicitFallback)) {
84
155
  yield explicitFallback;
85
156
  }
86
- const candidates = document.querySelectorAll(FOCUSABLE);
157
+ const candidates = tabStopsWithin(document);
87
158
  for (const element of candidates) {
88
159
  if (element === explicitFallback || root.contains(element) || !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING)) {
89
160
  continue;
90
161
  }
91
- if (this.#isFocusAvailable(element)) yield element;
162
+ yield element;
92
163
  }
93
164
  for (let index = candidates.length - 1; index >= 0; index -= 1) {
94
- const element = candidates.item(index);
165
+ const element = candidates[index];
166
+ if (!element) continue;
95
167
  if (element === explicitFallback || root.contains(element) || !(root.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING)) {
96
168
  continue;
97
169
  }
98
- if (this.#isFocusAvailable(element)) yield element;
170
+ yield element;
99
171
  }
100
172
  }
101
- /** Whether focus is allowed by the element and its semantic ancestors. */
102
- #isFocusAvailable(element) {
103
- if (!element.matches(FOCUSABLE) && !element.hasAttribute("tabindex") && !element.isContentEditable) {
104
- return false;
105
- }
106
- if (element.matches(":disabled")) return false;
107
- if (element instanceof HTMLInputElement && element.type === "hidden") return false;
108
- for (let current = element; current; current = current.parentElement) {
109
- if (current.hidden || current.inert) return false;
110
- }
111
- return true;
173
+ /** Whether an explicit fallback can receive programmatic focus right now. */
174
+ #isExplicitFocusAvailable(element) {
175
+ const programmaticallyFocusable = element.hasAttribute("tabindex") || isTabStop(element);
176
+ return programmaticallyFocusable && canTakeFocus(element) && isRenderedForFocus(element);
112
177
  }
113
178
  };
114
179
 
@@ -115,8 +115,81 @@ var EscapeLayer = class _EscapeLayer {
115
115
  }
116
116
  };
117
117
 
118
+ // src/utils/focus_candidate.ts
119
+ function inheritsFieldsetDisabled(control) {
120
+ let fieldset = control.closest("fieldset[disabled]");
121
+ while (fieldset) {
122
+ const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
123
+ if (!legend?.contains(control)) return true;
124
+ fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
125
+ }
126
+ return false;
127
+ }
128
+ function canTakeFocus(element) {
129
+ if (element.closest("[hidden], [inert]")) return false;
130
+ if (element instanceof HTMLInputElement && element.type === "hidden") return false;
131
+ if (!("disabled" in element)) return true;
132
+ if (element.disabled) return false;
133
+ return !inheritsFieldsetDisabled(element);
134
+ }
135
+ var TAB_STOP_CANDIDATE_SELECTOR = [
136
+ "a[href]",
137
+ "area[href]",
138
+ "button",
139
+ "input",
140
+ "select",
141
+ "textarea",
142
+ "summary",
143
+ "iframe",
144
+ "audio[controls]",
145
+ "video[controls]",
146
+ "[tabindex]",
147
+ "[contenteditable]"
148
+ ].join(",");
149
+ function isRenderedForFocus(element) {
150
+ const check = element.checkVisibility;
151
+ return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
152
+ }
153
+ function authoredTabindex(element) {
154
+ const value = element.getAttribute("tabindex");
155
+ if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
156
+ return Number(value);
157
+ }
158
+ function hasNativeTabStop(element) {
159
+ if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
160
+ return element.hasAttribute("href");
161
+ }
162
+ if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
163
+ return true;
164
+ }
165
+ if (element instanceof HTMLInputElement) return element.type !== "hidden";
166
+ if (element instanceof HTMLIFrameElement) return true;
167
+ if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
168
+ return element.hasAttribute("controls");
169
+ }
170
+ if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
171
+ const details = element.parentElement;
172
+ return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
173
+ }
174
+ return false;
175
+ }
176
+ function hasEditableTabStop(element) {
177
+ const value = element.getAttribute("contenteditable")?.toLowerCase();
178
+ return value === "" || value === "true" || value === "plaintext-only";
179
+ }
180
+ function isTabStop(element) {
181
+ if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
182
+ const tabindex = authoredTabindex(element);
183
+ if (tabindex !== null) return tabindex >= 0;
184
+ return hasNativeTabStop(element) || hasEditableTabStop(element);
185
+ }
186
+ function tabStopsWithin(root) {
187
+ return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
188
+ isTabStop
189
+ );
190
+ }
191
+
118
192
  // src/utils/focus_trap.ts
119
- var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
120
193
  var FocusTrap = class {
121
194
  /** The element focused before activation, restored on deactivation. */
122
195
  #previouslyFocused = null;
@@ -278,9 +351,7 @@ var FocusTrap = class {
278
351
  }
279
352
  /** Collects the container's currently focusable descendants in DOM order. */
280
353
  #focusableElements() {
281
- return Array.from(this.#getContainer().querySelectorAll(FOCUSABLE)).filter(
282
- (el) => !el.hidden
283
- );
354
+ return tabStopsWithin(this.#getContainer());
284
355
  }
285
356
  };
286
357
 
@@ -12,18 +12,38 @@ var FileDropzoneController = class extends Controller {
12
12
  static events = ["change", "reject"];
13
13
  /** Selected files paired with their rendered item and any preview objectURL. */
14
14
  #entries = [];
15
+ /** Prevents initial and teardown target callbacks from binding outside controller lifetime. */
16
+ #connected = false;
15
17
  /** Wires file removal as a delegated listener on the list container. */
16
18
  connect() {
17
- if (this.hasListTarget) this.listTarget.addEventListener("click", this.#onItemClick);
19
+ this.#connected = true;
20
+ if (this.hasListTarget) this.#bindList(this.listTarget);
18
21
  }
19
22
  /** Revokes any outstanding preview URLs so none leaks across navigations. */
20
23
  disconnect() {
21
- if (this.hasListTarget) this.listTarget.removeEventListener("click", this.#onItemClick);
24
+ this.#connected = false;
25
+ for (const list of this.listTargets) list.removeEventListener("click", this.#onItemClick);
22
26
  for (const entry of this.#entries) {
23
27
  if (entry.url) URL.revokeObjectURL(entry.url);
24
28
  }
25
29
  this.#entries.length = 0;
26
30
  }
31
+ /** Rebinds removal and restores client-only previews when Turbo replaces the list target. */
32
+ listTargetConnected(list) {
33
+ if (!this.#connected) return;
34
+ this.#bindList(list);
35
+ }
36
+ /** Releases only the list target that actually disconnected. */
37
+ listTargetDisconnected(list) {
38
+ list.removeEventListener("click", this.#onItemClick);
39
+ }
40
+ /** Binds delegated removal once and moves live preview items into the current list. */
41
+ #bindList(list) {
42
+ list.addEventListener("click", this.#onItemClick);
43
+ for (const entry of this.#entries) {
44
+ if (!list.contains(entry.item)) list.appendChild(entry.item);
45
+ }
46
+ }
27
47
  /** Opens the native file dialog. Bound via `data-action` (trigger click). */
28
48
  openDialog() {
29
49
  this.inputTarget.click();
@@ -55,8 +75,11 @@ var FileDropzoneController = class extends Controller {
55
75
  * an item is appended without waiting on Stimulus to wire a freshly created element.
56
76
  */
57
77
  #onItemClick = (event) => {
78
+ const list = event.currentTarget;
58
79
  const button = event.target.closest("button");
59
- if (!button || !this.hasListTarget || !this.listTarget.contains(button)) return;
80
+ if (!this.#connected || !list || !button || !this.hasListTarget || list !== this.listTarget || !list.contains(button)) {
81
+ return;
82
+ }
60
83
  const index = this.#entries.findIndex((entry) => entry.item.contains(button));
61
84
  if (index !== -1) this.#removeAt(index);
62
85
  };
@@ -115,8 +115,81 @@ var EscapeLayer = class _EscapeLayer {
115
115
  }
116
116
  };
117
117
 
118
+ // src/utils/focus_candidate.ts
119
+ function inheritsFieldsetDisabled(control) {
120
+ let fieldset = control.closest("fieldset[disabled]");
121
+ while (fieldset) {
122
+ const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
123
+ if (!legend?.contains(control)) return true;
124
+ fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
125
+ }
126
+ return false;
127
+ }
128
+ function canTakeFocus(element) {
129
+ if (element.closest("[hidden], [inert]")) return false;
130
+ if (element instanceof HTMLInputElement && element.type === "hidden") return false;
131
+ if (!("disabled" in element)) return true;
132
+ if (element.disabled) return false;
133
+ return !inheritsFieldsetDisabled(element);
134
+ }
135
+ var TAB_STOP_CANDIDATE_SELECTOR = [
136
+ "a[href]",
137
+ "area[href]",
138
+ "button",
139
+ "input",
140
+ "select",
141
+ "textarea",
142
+ "summary",
143
+ "iframe",
144
+ "audio[controls]",
145
+ "video[controls]",
146
+ "[tabindex]",
147
+ "[contenteditable]"
148
+ ].join(",");
149
+ function isRenderedForFocus(element) {
150
+ const check = element.checkVisibility;
151
+ return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
152
+ }
153
+ function authoredTabindex(element) {
154
+ const value = element.getAttribute("tabindex");
155
+ if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
156
+ return Number(value);
157
+ }
158
+ function hasNativeTabStop(element) {
159
+ if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
160
+ return element.hasAttribute("href");
161
+ }
162
+ if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
163
+ return true;
164
+ }
165
+ if (element instanceof HTMLInputElement) return element.type !== "hidden";
166
+ if (element instanceof HTMLIFrameElement) return true;
167
+ if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
168
+ return element.hasAttribute("controls");
169
+ }
170
+ if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
171
+ const details = element.parentElement;
172
+ return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
173
+ }
174
+ return false;
175
+ }
176
+ function hasEditableTabStop(element) {
177
+ const value = element.getAttribute("contenteditable")?.toLowerCase();
178
+ return value === "" || value === "true" || value === "plaintext-only";
179
+ }
180
+ function isTabStop(element) {
181
+ if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
182
+ const tabindex = authoredTabindex(element);
183
+ if (tabindex !== null) return tabindex >= 0;
184
+ return hasNativeTabStop(element) || hasEditableTabStop(element);
185
+ }
186
+ function tabStopsWithin(root) {
187
+ return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
188
+ isTabStop
189
+ );
190
+ }
191
+
118
192
  // src/utils/focus_trap.ts
119
- var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
120
193
  var FocusTrap = class {
121
194
  /** The element focused before activation, restored on deactivation. */
122
195
  #previouslyFocused = null;
@@ -278,9 +351,7 @@ var FocusTrap = class {
278
351
  }
279
352
  /** Collects the container's currently focusable descendants in DOM order. */
280
353
  #focusableElements() {
281
- return Array.from(this.#getContainer().querySelectorAll(FOCUSABLE)).filter(
282
- (el) => !el.hidden
283
- );
354
+ return tabStopsWithin(this.#getContainer());
284
355
  }
285
356
  };
286
357