ruby_ui 1.4.0 → 1.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.
- checksums.yaml +4 -4
- data/lib/generators/ruby_ui/dependencies.yml +12 -2
- data/lib/generators/ruby_ui/javascript_utils.rb +0 -13
- data/lib/ruby_ui/accordion/accordion_controller.js +16 -9
- data/lib/ruby_ui/bubble/bubble.rb +37 -0
- data/lib/ruby_ui/bubble/bubble_content.rb +23 -0
- data/lib/ruby_ui/bubble/bubble_docs.rb +167 -0
- data/lib/ruby_ui/bubble/bubble_group.rb +18 -0
- data/lib/ruby_ui/bubble/bubble_reactions.rb +38 -0
- data/lib/ruby_ui/combobox/combobox.rb +3 -1
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +8 -1
- data/lib/ruby_ui/combobox/combobox_controller.js +3 -2
- data/lib/ruby_ui/combobox/combobox_trigger.rb +1 -4
- data/lib/ruby_ui/context_menu/context_menu_content.rb +5 -6
- data/lib/ruby_ui/context_menu/context_menu_controller.js +87 -75
- data/lib/ruby_ui/data_table/data_table_column_toggle.rb +8 -4
- data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +12 -2
- data/lib/ruby_ui/dialog/dialog_content.rb +1 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu.rb +0 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +5 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb +1 -1
- data/lib/ruby_ui/empty/empty.rb +18 -0
- data/lib/ruby_ui/empty/empty_content.rb +18 -0
- data/lib/ruby_ui/empty/empty_description.rb +18 -0
- data/lib/ruby_ui/empty/empty_docs.rb +69 -0
- data/lib/ruby_ui/empty/empty_header.rb +18 -0
- data/lib/ruby_ui/empty/empty_media.rb +31 -0
- data/lib/ruby_ui/empty/empty_title.rb +18 -0
- data/lib/ruby_ui/hover_card/hover_card_content.rb +5 -6
- data/lib/ruby_ui/hover_card/hover_card_controller.js +113 -76
- data/lib/ruby_ui/input_otp/input_otp.rb +42 -0
- data/lib/ruby_ui/input_otp/input_otp_controller.js +128 -0
- data/lib/ruby_ui/input_otp/input_otp_docs.rb +213 -0
- data/lib/ruby_ui/input_otp/input_otp_group.rb +15 -0
- data/lib/ruby_ui/input_otp/input_otp_separator.rb +39 -0
- data/lib/ruby_ui/input_otp/input_otp_slot.rb +33 -0
- data/lib/ruby_ui/message/message.rb +23 -0
- data/lib/ruby_ui/message/message_avatar.rb +18 -0
- data/lib/ruby_ui/message/message_content.rb +18 -0
- data/lib/ruby_ui/message/message_docs.rb +173 -0
- data/lib/ruby_ui/message/message_footer.rb +18 -0
- data/lib/ruby_ui/message/message_group.rb +18 -0
- data/lib/ruby_ui/message/message_header.rb +18 -0
- data/lib/ruby_ui/message_scroller/message_scroller.rb +18 -0
- data/lib/ruby_ui/message_scroller/message_scroller_button.rb +56 -0
- data/lib/ruby_ui/message_scroller/message_scroller_content.rb +23 -0
- data/lib/ruby_ui/message_scroller/message_scroller_controller.js +335 -0
- data/lib/ruby_ui/message_scroller/message_scroller_docs.rb +208 -0
- data/lib/ruby_ui/message_scroller/message_scroller_item.rb +28 -0
- data/lib/ruby_ui/message_scroller/message_scroller_provider.rb +34 -0
- data/lib/ruby_ui/message_scroller/message_scroller_viewport.rb +22 -0
- data/lib/ruby_ui/native_select/native_select_icon.rb +1 -1
- data/lib/ruby_ui/popover/popover_content.rb +2 -1
- data/lib/ruby_ui/popover/popover_controller.js +95 -32
- data/lib/ruby_ui/toast/toaster_controller.js +24 -8
- data/lib/ruby_ui.rb +1 -1
- metadata +34 -1
|
@@ -19,13 +19,20 @@ export default class extends Controller {
|
|
|
19
19
|
this.closeTimeout = null;
|
|
20
20
|
this.cleanup = null;
|
|
21
21
|
this.addEventListeners();
|
|
22
|
+
// openValue lives in the DOM, so a reconnect (frame swap, morph, moved element)
|
|
23
|
+
// arrives already open. Re-arm the parts that live on the controller instead of
|
|
24
|
+
// the markup — the keydown listener and the autoUpdate positioning.
|
|
25
|
+
if (this.openValue) this.showPopover();
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
// Teardown that cannot fail comes first: resolving a target throws once the
|
|
29
|
+
// element is gone, and Stimulus swallows that, skipping the rest of disconnect.
|
|
24
30
|
disconnect() {
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
clearTimeout(this.closeTimeout);
|
|
32
|
+
document.removeEventListener("keydown", this.handleKeydown);
|
|
33
|
+
document.removeEventListener("click", this.handleOutsideClick);
|
|
34
|
+
this.stopAutoUpdate();
|
|
35
|
+
this.removeElementEventListeners();
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
addEventListeners() {
|
|
@@ -40,68 +47,124 @@ export default class extends Controller {
|
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
// Each target is guarded on its own: losing one of them must not strand the
|
|
51
|
+
// listeners attached to the other.
|
|
52
|
+
removeElementEventListeners() {
|
|
53
|
+
if (this.hasTriggerTarget) {
|
|
54
|
+
this.triggerTarget.removeEventListener("mouseenter", this.handleMouseEnter);
|
|
55
|
+
this.triggerTarget.removeEventListener("mouseleave", this.handleMouseLeave);
|
|
56
|
+
this.triggerTarget.removeEventListener("click", this.handleClick);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.hasContentTarget) {
|
|
60
|
+
this.contentTarget.removeEventListener("mouseenter", this.handleMouseEnter);
|
|
61
|
+
this.contentTarget.removeEventListener("mouseleave", this.handleMouseLeave);
|
|
62
|
+
}
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
handleMouseEnter = () => {
|
|
53
66
|
clearTimeout(this.closeTimeout);
|
|
54
|
-
this.openValue = true;
|
|
55
67
|
this.showPopover();
|
|
56
68
|
};
|
|
57
69
|
|
|
58
70
|
handleMouseLeave = () => {
|
|
59
|
-
this.closeTimeout = setTimeout(() =>
|
|
60
|
-
this.openValue = false;
|
|
61
|
-
this.hidePopover();
|
|
62
|
-
}, 100);
|
|
71
|
+
this.closeTimeout = setTimeout(() => this.hidePopover(), 100);
|
|
63
72
|
};
|
|
64
73
|
|
|
65
74
|
handleClick = (event) => {
|
|
66
75
|
event.stopPropagation();
|
|
67
|
-
this.openValue
|
|
68
|
-
this.openValue ? this.showPopover() : this.hidePopover();
|
|
76
|
+
this.openValue ? this.hidePopover() : this.showPopover();
|
|
69
77
|
};
|
|
70
78
|
|
|
71
79
|
handleOutsideClick = (event) => {
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
if (this.element.contains(event.target)) return;
|
|
81
|
+
if (!this.openValue) return;
|
|
82
|
+
|
|
83
|
+
this.hidePopover();
|
|
76
84
|
};
|
|
77
85
|
|
|
86
|
+
handleKeydown = (event) => {
|
|
87
|
+
if (event.key !== "Escape") return;
|
|
88
|
+
if (!this.openValue) return;
|
|
89
|
+
|
|
90
|
+
clearTimeout(this.closeTimeout);
|
|
91
|
+
this.hidePopover();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// openValue is set here rather than by the callers, so a guarded early return can
|
|
95
|
+
// never leave the DOM claiming the popover is open while nothing is wired up.
|
|
78
96
|
showPopover() {
|
|
97
|
+
if (!this.hasTriggerTarget || !this.hasContentTarget) return;
|
|
98
|
+
|
|
99
|
+
this.openValue = true;
|
|
79
100
|
this.contentTarget.classList.remove("hidden");
|
|
101
|
+
this.contentTarget.dataset.state = "open";
|
|
102
|
+
document.addEventListener("keydown", this.handleKeydown);
|
|
80
103
|
this.updatePosition();
|
|
81
104
|
}
|
|
82
105
|
|
|
106
|
+
// Same rule as disconnect(): release what is held outside the element first, so a
|
|
107
|
+
// missing content target cannot leave the keydown listener or autoUpdate running.
|
|
83
108
|
hidePopover() {
|
|
109
|
+
this.openValue = false;
|
|
110
|
+
document.removeEventListener("keydown", this.handleKeydown);
|
|
111
|
+
this.stopAutoUpdate();
|
|
112
|
+
|
|
113
|
+
if (!this.hasContentTarget) return;
|
|
114
|
+
|
|
84
115
|
this.contentTarget.classList.add("hidden");
|
|
85
|
-
|
|
86
|
-
this.cleanup();
|
|
87
|
-
}
|
|
116
|
+
this.contentTarget.dataset.state = "closed";
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
updatePosition() {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
this.stopAutoUpdate();
|
|
121
|
+
|
|
122
|
+
// Hold the exact pair this run positions. A target can be detached or swapped
|
|
123
|
+
// while the controller stays connected, and the stale element must not be
|
|
124
|
+
// written to by an observer callback or an in-flight computePosition.
|
|
125
|
+
const trigger = this.triggerTarget;
|
|
126
|
+
const content = this.contentTarget;
|
|
127
|
+
|
|
128
|
+
// Deferred teardown is bound to this run's own handle, so a newer positioning
|
|
129
|
+
// run installed before the microtask drains is never torn down by an older one.
|
|
130
|
+
let stop = null;
|
|
131
|
+
const releaseThisRun = () => {
|
|
132
|
+
stop?.();
|
|
133
|
+
if (this.cleanup === stop) this.cleanup = null;
|
|
134
|
+
};
|
|
94
135
|
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
stop = autoUpdate(trigger, content, () => {
|
|
137
|
+
if (!trigger.isConnected || !content.isConnected) {
|
|
138
|
+
// Release the observers instead of throwing on every scroll and resize.
|
|
139
|
+
// Deferred because autoUpdate runs this once synchronously, before the
|
|
140
|
+
// handle below has been assigned.
|
|
141
|
+
queueMicrotask(releaseThisRun);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
computePosition(trigger, content, {
|
|
97
146
|
placement: this.optionsValue.placement || "bottom",
|
|
98
147
|
middleware: [flip(), shift(), offset(8)],
|
|
99
|
-
}).then(({ x, y }) => {
|
|
100
|
-
|
|
148
|
+
}).then(({ x, y, placement }) => {
|
|
149
|
+
if (!content.isConnected) return;
|
|
150
|
+
|
|
151
|
+
Object.assign(content.style, {
|
|
101
152
|
left: `${x}px`,
|
|
102
153
|
top: `${y}px`,
|
|
103
154
|
});
|
|
155
|
+
// flip() can resolve to the opposite side of the requested placement,
|
|
156
|
+
// so the directional slide-in classes must follow the resolved value.
|
|
157
|
+
content.dataset.side = placement.split("-")[0];
|
|
104
158
|
});
|
|
105
159
|
});
|
|
160
|
+
|
|
161
|
+
this.cleanup = stop;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
stopAutoUpdate() {
|
|
165
|
+
if (!this.cleanup) return;
|
|
166
|
+
|
|
167
|
+
this.cleanup();
|
|
168
|
+
this.cleanup = null;
|
|
106
169
|
}
|
|
107
170
|
}
|
|
@@ -39,9 +39,13 @@ export default class extends Controller {
|
|
|
39
39
|
dir: { type: String, default: "ltr" },
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
initialize() {
|
|
43
|
+
this._expanded = false
|
|
43
44
|
this._heights = new Map()
|
|
44
45
|
this._resizeObservers = new WeakMap()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
connect() {
|
|
45
49
|
this._expanded = this.expandValue
|
|
46
50
|
this._listEl = this.element.querySelector("ol") || (this.element.tagName === "OL" ? this.element : null)
|
|
47
51
|
this._registerGlobalApi()
|
|
@@ -59,6 +63,8 @@ export default class extends Controller {
|
|
|
59
63
|
this._listEl.addEventListener("pointerenter", this._onPointerEnter)
|
|
60
64
|
this._listEl.addEventListener("pointerleave", this._onPointerLeave)
|
|
61
65
|
document.addEventListener("keydown", this._onKey)
|
|
66
|
+
|
|
67
|
+
this._reflow()
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
disconnect() {
|
|
@@ -67,9 +73,21 @@ export default class extends Controller {
|
|
|
67
73
|
this._listEl?.removeEventListener("pointerenter", this._onPointerEnter)
|
|
68
74
|
this._listEl?.removeEventListener("pointerleave", this._onPointerLeave)
|
|
69
75
|
document.removeEventListener("keydown", this._onKey)
|
|
76
|
+
this._listEl = null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
positionValueChanged(value) {
|
|
80
|
+
this.element.setAttribute("data-position", value)
|
|
81
|
+
this._reflow()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
expandValueChanged(value) {
|
|
85
|
+
this._expanded = value
|
|
86
|
+
this._reflow()
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
toastTargetConnected(el) {
|
|
90
|
+
this._resizeObservers.get(el)?.disconnect()
|
|
73
91
|
if (typeof ResizeObserver !== "undefined") {
|
|
74
92
|
const ro = new ResizeObserver(() => {
|
|
75
93
|
this._heights.set(el, el.offsetHeight)
|
|
@@ -90,13 +108,11 @@ export default class extends Controller {
|
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
_spawn(detail) {
|
|
111
|
+
if (!this._listEl) return null
|
|
93
112
|
const variant = VARIANTS.includes(detail.variant) ? detail.variant : "default"
|
|
94
113
|
const tpl = this._skeletonFor(variant)
|
|
95
114
|
if (!tpl) return null
|
|
96
|
-
if (detail.position)
|
|
97
|
-
this.element.setAttribute("data-position", detail.position)
|
|
98
|
-
this.positionValue = detail.position
|
|
99
|
-
}
|
|
115
|
+
if (detail.position) this.positionValue = detail.position
|
|
100
116
|
const node = tpl.content.firstElementChild.cloneNode(true)
|
|
101
117
|
|
|
102
118
|
node.id = detail.id || `toast-${this._uuid()}`
|
|
@@ -151,7 +167,7 @@ export default class extends Controller {
|
|
|
151
167
|
)
|
|
152
168
|
return
|
|
153
169
|
}
|
|
154
|
-
const el = this._listEl
|
|
170
|
+
const el = this._listEl?.querySelector(`#${CSS.escape(id)}`)
|
|
155
171
|
if (el) el.dispatchEvent(new CustomEvent("ruby-ui:toast:force-dismiss", { bubbles: true }))
|
|
156
172
|
}
|
|
157
173
|
|
|
@@ -243,7 +259,7 @@ export default class extends Controller {
|
|
|
243
259
|
if (wantCtrl !== e.ctrlKey) return
|
|
244
260
|
if (wantMeta !== e.metaKey) return
|
|
245
261
|
e.preventDefault()
|
|
246
|
-
const first = this._listEl
|
|
262
|
+
const first = this._listEl?.firstElementChild
|
|
247
263
|
first?.focus()
|
|
248
264
|
}
|
|
249
265
|
|
|
@@ -278,7 +294,7 @@ export default class extends Controller {
|
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
_mutate(id, variant, text) {
|
|
281
|
-
const el = this._listEl
|
|
297
|
+
const el = this._listEl?.querySelector(`#${CSS.escape(id)}`)
|
|
282
298
|
if (!el) return
|
|
283
299
|
el.dataset.variant = variant
|
|
284
300
|
el.setAttribute("role", variant === "error" ? "alert" : "status")
|
data/lib/ruby_ui.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_ui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- George Kettle
|
|
@@ -159,6 +159,11 @@ files:
|
|
|
159
159
|
- lib/ruby_ui/breadcrumb/breadcrumb_list.rb
|
|
160
160
|
- lib/ruby_ui/breadcrumb/breadcrumb_page.rb
|
|
161
161
|
- lib/ruby_ui/breadcrumb/breadcrumb_separator.rb
|
|
162
|
+
- lib/ruby_ui/bubble/bubble.rb
|
|
163
|
+
- lib/ruby_ui/bubble/bubble_content.rb
|
|
164
|
+
- lib/ruby_ui/bubble/bubble_docs.rb
|
|
165
|
+
- lib/ruby_ui/bubble/bubble_group.rb
|
|
166
|
+
- lib/ruby_ui/bubble/bubble_reactions.rb
|
|
162
167
|
- lib/ruby_ui/button/button.rb
|
|
163
168
|
- lib/ruby_ui/button/button_docs.rb
|
|
164
169
|
- lib/ruby_ui/calendar/calendar.rb
|
|
@@ -291,6 +296,13 @@ files:
|
|
|
291
296
|
- lib/ruby_ui/dropdown_menu/dropdown_menu_label.rb
|
|
292
297
|
- lib/ruby_ui/dropdown_menu/dropdown_menu_separator.rb
|
|
293
298
|
- lib/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb
|
|
299
|
+
- lib/ruby_ui/empty/empty.rb
|
|
300
|
+
- lib/ruby_ui/empty/empty_content.rb
|
|
301
|
+
- lib/ruby_ui/empty/empty_description.rb
|
|
302
|
+
- lib/ruby_ui/empty/empty_docs.rb
|
|
303
|
+
- lib/ruby_ui/empty/empty_header.rb
|
|
304
|
+
- lib/ruby_ui/empty/empty_media.rb
|
|
305
|
+
- lib/ruby_ui/empty/empty_title.rb
|
|
294
306
|
- lib/ruby_ui/form/form.rb
|
|
295
307
|
- lib/ruby_ui/form/form_docs.rb
|
|
296
308
|
- lib/ruby_ui/form/form_field.rb
|
|
@@ -305,11 +317,32 @@ files:
|
|
|
305
317
|
- lib/ruby_ui/hover_card/hover_card_trigger.rb
|
|
306
318
|
- lib/ruby_ui/input/input.rb
|
|
307
319
|
- lib/ruby_ui/input/input_docs.rb
|
|
320
|
+
- lib/ruby_ui/input_otp/input_otp.rb
|
|
321
|
+
- lib/ruby_ui/input_otp/input_otp_controller.js
|
|
322
|
+
- lib/ruby_ui/input_otp/input_otp_docs.rb
|
|
323
|
+
- lib/ruby_ui/input_otp/input_otp_group.rb
|
|
324
|
+
- lib/ruby_ui/input_otp/input_otp_separator.rb
|
|
325
|
+
- lib/ruby_ui/input_otp/input_otp_slot.rb
|
|
308
326
|
- lib/ruby_ui/link/link.rb
|
|
309
327
|
- lib/ruby_ui/link/link_docs.rb
|
|
310
328
|
- lib/ruby_ui/masked_input/masked_input.rb
|
|
311
329
|
- lib/ruby_ui/masked_input/masked_input_controller.js
|
|
312
330
|
- lib/ruby_ui/masked_input/masked_input_docs.rb
|
|
331
|
+
- lib/ruby_ui/message/message.rb
|
|
332
|
+
- lib/ruby_ui/message/message_avatar.rb
|
|
333
|
+
- lib/ruby_ui/message/message_content.rb
|
|
334
|
+
- lib/ruby_ui/message/message_docs.rb
|
|
335
|
+
- lib/ruby_ui/message/message_footer.rb
|
|
336
|
+
- lib/ruby_ui/message/message_group.rb
|
|
337
|
+
- lib/ruby_ui/message/message_header.rb
|
|
338
|
+
- lib/ruby_ui/message_scroller/message_scroller.rb
|
|
339
|
+
- lib/ruby_ui/message_scroller/message_scroller_button.rb
|
|
340
|
+
- lib/ruby_ui/message_scroller/message_scroller_content.rb
|
|
341
|
+
- lib/ruby_ui/message_scroller/message_scroller_controller.js
|
|
342
|
+
- lib/ruby_ui/message_scroller/message_scroller_docs.rb
|
|
343
|
+
- lib/ruby_ui/message_scroller/message_scroller_item.rb
|
|
344
|
+
- lib/ruby_ui/message_scroller/message_scroller_provider.rb
|
|
345
|
+
- lib/ruby_ui/message_scroller/message_scroller_viewport.rb
|
|
313
346
|
- lib/ruby_ui/native_select/native_select.rb
|
|
314
347
|
- lib/ruby_ui/native_select/native_select_docs.rb
|
|
315
348
|
- lib/ruby_ui/native_select/native_select_group.rb
|