ruby_ui 1.2.0 → 1.3.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 +32 -10
- data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +24 -7
- data/lib/ruby_ui/avatar/avatar.rb +3 -0
- data/lib/ruby_ui/avatar/avatar_controller.js +33 -0
- data/lib/ruby_ui/avatar/avatar_fallback.rb +3 -0
- data/lib/ruby_ui/avatar/avatar_image.rb +4 -0
- data/lib/ruby_ui/base.rb +6 -0
- data/lib/ruby_ui/calendar/calendar.rb +3 -1
- data/lib/ruby_ui/calendar/calendar_controller.js +66 -7
- data/lib/ruby_ui/calendar/calendar_days.rb +20 -0
- data/lib/ruby_ui/calendar/calendar_docs.rb +9 -0
- data/lib/ruby_ui/combobox/combobox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_controller.js +56 -244
- data/lib/ruby_ui/combobox/combobox_item.rb +7 -5
- data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
- data/lib/ruby_ui/combobox/combobox_popover.rb +5 -0
- data/lib/ruby_ui/combobox/combobox_radio.rb +8 -1
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
- data/lib/ruby_ui/command/command_controller.js +10 -19
- data/lib/ruby_ui/command/command_dialog.rb +4 -1
- data/lib/ruby_ui/command/command_dialog_content.rb +2 -2
- data/lib/ruby_ui/command/command_dialog_controller.js +34 -0
- data/lib/ruby_ui/command/command_dialog_trigger.rb +2 -2
- data/lib/ruby_ui/date_picker/date_picker.rb +85 -0
- data/lib/ruby_ui/date_picker/date_picker_docs.rb +23 -0
- data/lib/ruby_ui/masked_input/masked_input.rb +1 -11
- data/lib/ruby_ui/masked_input/masked_input_controller.js +0 -13
- data/lib/ruby_ui/select/select_value.rb +2 -1
- data/lib/ruby_ui/sheet/sheet.rb +9 -1
- data/lib/ruby_ui/sheet/sheet_controller.js +6 -0
- data/lib/ruby_ui/theme_toggle/theme_toggle.rb +14 -2
- data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +27 -19
- data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +12 -42
- data/lib/ruby_ui/toast/toast.rb +18 -0
- data/lib/ruby_ui/toast/toast_action.rb +27 -0
- data/lib/ruby_ui/toast/toast_cancel.rb +27 -0
- data/lib/ruby_ui/toast/toast_close.rb +40 -0
- data/lib/ruby_ui/toast/toast_controller.js +151 -0
- data/lib/ruby_ui/toast/toast_description.rb +18 -0
- data/lib/ruby_ui/toast/toast_docs.rb +12 -0
- data/lib/ruby_ui/toast/toast_icon.rb +65 -0
- data/lib/ruby_ui/toast/toast_item.rb +72 -0
- data/lib/ruby_ui/toast/toast_region.rb +124 -0
- data/lib/ruby_ui/toast/toast_title.rb +18 -0
- data/lib/ruby_ui/toast/toaster_controller.js +306 -0
- data/lib/ruby_ui/toggle/toggle.rb +101 -0
- data/lib/ruby_ui/toggle/toggle_controller.js +33 -0
- data/lib/ruby_ui/toggle_group/toggle_group.rb +119 -0
- data/lib/ruby_ui/toggle_group/toggle_group_controller.js +126 -0
- data/lib/ruby_ui/toggle_group/toggle_group_item.rb +67 -0
- data/lib/ruby_ui/tooltip/tooltip_content.rb +12 -5
- data/lib/ruby_ui/tooltip/tooltip_controller.js +58 -22
- data/lib/ruby_ui/tooltip/tooltip_docs.rb +13 -0
- data/lib/ruby_ui/tooltip/tooltip_trigger.rb +10 -3
- data/lib/ruby_ui.rb +3 -1
- metadata +30 -14
- data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +0 -16
- data/lib/ruby_ui/theme_toggle/set_light_mode.rb +0 -16
|
@@ -4,7 +4,8 @@ import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom";
|
|
|
4
4
|
// Connects to data-controller="ruby-ui--combobox"
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static values = {
|
|
7
|
-
term: String
|
|
7
|
+
term: String,
|
|
8
|
+
minPopoverWidth: { type: Number, default: 240 }
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
static targets = [
|
|
@@ -15,276 +16,90 @@ export default class extends Controller {
|
|
|
15
16
|
"emptyState",
|
|
16
17
|
"searchInput",
|
|
17
18
|
"trigger",
|
|
18
|
-
"triggerContent"
|
|
19
|
-
"badgeContainer",
|
|
20
|
-
"clearButton",
|
|
21
|
-
"badgeInput",
|
|
22
|
-
"inputTrigger"
|
|
19
|
+
"triggerContent"
|
|
23
20
|
]
|
|
24
21
|
|
|
25
22
|
selectedItemIndex = null
|
|
26
23
|
|
|
27
24
|
connect() {
|
|
28
25
|
this.updateTriggerContent()
|
|
29
|
-
this.updateBadges()
|
|
30
|
-
this.updateClearButton()
|
|
31
|
-
this.updateInputTrigger()
|
|
32
|
-
|
|
33
|
-
// Track mouse state to distinguish click-focus from tab-focus
|
|
34
|
-
this._mouseDown = false
|
|
35
|
-
this.element.addEventListener("mousedown", () => { this._mouseDown = true })
|
|
36
|
-
this.element.addEventListener("mouseup", () => { setTimeout(() => { this._mouseDown = false }, 0) })
|
|
37
26
|
}
|
|
38
27
|
|
|
39
28
|
disconnect() {
|
|
40
29
|
if (this.cleanup) { this.cleanup() }
|
|
41
30
|
}
|
|
42
31
|
|
|
43
|
-
// Popover
|
|
44
|
-
|
|
45
|
-
togglePopover(event) {
|
|
46
|
-
event.preventDefault()
|
|
47
|
-
|
|
48
|
-
if (this.triggerTarget.ariaExpanded === "true") {
|
|
49
|
-
this.closePopover()
|
|
50
|
-
} else {
|
|
51
|
-
this.openPopover(event)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
openPopover(event) {
|
|
56
|
-
if (event && event.type !== "focusin" && event.type !== "focus") event.preventDefault()
|
|
57
|
-
|
|
58
|
-
// focusin/focus: only open on keyboard focus (tab), not mouse click
|
|
59
|
-
if (event && (event.type === "focusin" || event.type === "focus")) {
|
|
60
|
-
if (this._mouseDown || this.triggerTarget.ariaExpanded === "true" || this._closingPopover) return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
this.updatePopoverPosition()
|
|
64
|
-
this.updatePopoverWidth()
|
|
65
|
-
this.triggerTarget.ariaExpanded = "true"
|
|
66
|
-
this.selectedItemIndex = null
|
|
67
|
-
this.itemTargets.forEach(item => item.ariaCurrent = "false")
|
|
68
|
-
this.popoverTarget.showPopover()
|
|
69
|
-
|
|
70
|
-
// Always show all items on open; filter only on user typing
|
|
71
|
-
this.applyFilter("")
|
|
72
|
-
|
|
73
|
-
if (this.hasBadgeInputTarget) {
|
|
74
|
-
this.badgeInputTarget.value = ""
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
closePopover() {
|
|
79
|
-
this._closingPopover = true
|
|
80
|
-
this.triggerTarget.ariaExpanded = "false"
|
|
81
|
-
this.popoverTarget.hidePopover()
|
|
82
|
-
setTimeout(() => this._closingPopover = false, 200)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
32
|
handlePopoverToggle(event) {
|
|
86
33
|
// Keep ariaExpanded in sync with the actual popover state
|
|
87
34
|
this.triggerTarget.ariaExpanded = event.newState === 'open' ? 'true' : 'false'
|
|
88
35
|
}
|
|
89
36
|
|
|
90
|
-
updatePopoverPosition() {
|
|
91
|
-
this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => {
|
|
92
|
-
computePosition(this.triggerTarget, this.popoverTarget, {
|
|
93
|
-
placement: 'bottom-start',
|
|
94
|
-
middleware: [offset(4), flip()],
|
|
95
|
-
}).then(({ x, y }) => {
|
|
96
|
-
Object.assign(this.popoverTarget.style, {
|
|
97
|
-
left: `${x}px`,
|
|
98
|
-
top: `${y}px`,
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
updatePopoverWidth() {
|
|
105
|
-
this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px`
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Selection
|
|
109
|
-
|
|
110
37
|
inputChanged(e) {
|
|
111
38
|
this.updateTriggerContent()
|
|
112
39
|
|
|
113
40
|
if (e.target.type == "radio") {
|
|
114
41
|
this.closePopover()
|
|
115
|
-
this.updateInputTrigger()
|
|
116
42
|
}
|
|
117
43
|
|
|
118
44
|
if (this.hasToggleAllTarget && !e.target.checked) {
|
|
119
45
|
this.toggleAllTarget.checked = false
|
|
120
46
|
}
|
|
47
|
+
}
|
|
121
48
|
|
|
122
|
-
|
|
123
|
-
|
|
49
|
+
inputContent(input) {
|
|
50
|
+
return input.dataset.text || input.parentElement.textContent
|
|
124
51
|
}
|
|
125
52
|
|
|
126
53
|
toggleAllItems() {
|
|
127
54
|
const isChecked = this.toggleAllTarget.checked
|
|
128
55
|
this.inputTargets.forEach(input => input.checked = isChecked)
|
|
129
56
|
this.updateTriggerContent()
|
|
130
|
-
this.updateBadges()
|
|
131
|
-
this.updateClearButton()
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
clearAll(event) {
|
|
135
|
-
if (event) event.preventDefault()
|
|
136
|
-
|
|
137
|
-
this.inputTargets.forEach(input => input.checked = false)
|
|
138
|
-
this.updateBadges()
|
|
139
|
-
this.updateClearButton()
|
|
140
|
-
this.updateTriggerContent()
|
|
141
|
-
this.updateInputTrigger()
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
removeBadge(event) {
|
|
145
|
-
event.preventDefault()
|
|
146
|
-
event.stopPropagation()
|
|
147
|
-
|
|
148
|
-
const value = event.currentTarget.closest('[data-value]').dataset.value
|
|
149
|
-
const input = this.inputTargets.find(input => input.value === value)
|
|
150
|
-
|
|
151
|
-
if (input) {
|
|
152
|
-
input.checked = false
|
|
153
|
-
input.dispatchEvent(new Event("change", { bubbles: true }))
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Display
|
|
158
|
-
|
|
159
|
-
inputContent(input) {
|
|
160
|
-
return input.dataset.text || input.parentElement.textContent
|
|
161
57
|
}
|
|
162
58
|
|
|
163
59
|
updateTriggerContent() {
|
|
164
|
-
if (!this.hasTriggerContentTarget) return
|
|
165
|
-
|
|
166
60
|
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
|
167
61
|
|
|
168
62
|
if (checkedInputs.length === 0) {
|
|
169
63
|
this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder
|
|
170
|
-
this.triggerContentTarget.classList.add("text-muted-foreground")
|
|
171
64
|
} else if (this.termValue && checkedInputs.length > 1) {
|
|
172
65
|
this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`
|
|
173
|
-
this.triggerContentTarget.classList.remove("text-muted-foreground")
|
|
174
66
|
} else {
|
|
175
67
|
this.triggerContentTarget.innerText = checkedInputs.map((input) => this.inputContent(input)).join(", ")
|
|
176
|
-
this.triggerContentTarget.classList.remove("text-muted-foreground")
|
|
177
68
|
}
|
|
178
69
|
}
|
|
179
70
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const checked = this.inputTargets.find(i => i.checked)
|
|
183
|
-
this.inputTriggerTarget.value = checked ? this.inputContent(checked) : ""
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// NOTE: badge classes mirror ComboboxBadge Ruby component. Update both if styles change.
|
|
187
|
-
updateBadges() {
|
|
188
|
-
if (!this.hasBadgeContainerTarget) return
|
|
189
|
-
|
|
190
|
-
// Remove existing badges
|
|
191
|
-
this.triggerTarget.querySelectorAll("[data-combobox-badge]").forEach(el => el.remove())
|
|
192
|
-
|
|
193
|
-
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
|
194
|
-
|
|
195
|
-
// Toggle trigger height: h-9 when empty, h-auto min-h-9 when badges exist
|
|
196
|
-
if (checkedInputs.length > 0) {
|
|
197
|
-
this.triggerTarget.classList.remove("h-9")
|
|
198
|
-
this.triggerTarget.classList.add("h-auto", "min-h-9")
|
|
199
|
-
} else {
|
|
200
|
-
this.triggerTarget.classList.remove("h-auto", "min-h-9", "pt-1.5")
|
|
201
|
-
this.triggerTarget.classList.add("h-9")
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
checkedInputs.forEach(input => {
|
|
205
|
-
const badge = document.createElement("span")
|
|
206
|
-
badge.setAttribute("data-combobox-badge", "")
|
|
207
|
-
badge.className = "inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-xs font-medium text-secondary-foreground"
|
|
208
|
-
badge.dataset.value = input.value
|
|
209
|
-
|
|
210
|
-
badge.appendChild(document.createTextNode(this.inputContent(input).trim()))
|
|
211
|
-
|
|
212
|
-
const btn = document.createElement("button")
|
|
213
|
-
btn.type = "button"
|
|
214
|
-
btn.setAttribute("aria-label", "Remove")
|
|
215
|
-
btn.className = "rounded-sm opacity-50 hover:opacity-100 focus-visible:outline-none"
|
|
216
|
-
|
|
217
|
-
btn.addEventListener("click", (e) => {
|
|
218
|
-
e.preventDefault()
|
|
219
|
-
e.stopPropagation()
|
|
220
|
-
e.stopImmediatePropagation()
|
|
221
|
-
const target = this.inputTargets.find(i => i.value === input.value)
|
|
222
|
-
if (target) {
|
|
223
|
-
target.checked = false
|
|
224
|
-
this.updateBadges()
|
|
225
|
-
this.updateClearButton()
|
|
226
|
-
}
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
|
230
|
-
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
|
|
231
|
-
svg.setAttribute("width", "12")
|
|
232
|
-
svg.setAttribute("height", "12")
|
|
233
|
-
svg.setAttribute("viewBox", "0 0 24 24")
|
|
234
|
-
svg.setAttribute("fill", "none")
|
|
235
|
-
svg.setAttribute("stroke", "currentColor")
|
|
236
|
-
svg.setAttribute("stroke-width", "2")
|
|
237
|
-
svg.setAttribute("stroke-linecap", "round")
|
|
238
|
-
svg.setAttribute("stroke-linejoin", "round")
|
|
239
|
-
svg.classList.add("pointer-events-none")
|
|
240
|
-
|
|
241
|
-
const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path")
|
|
242
|
-
path1.setAttribute("d", "M18 6 6 18")
|
|
243
|
-
const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path")
|
|
244
|
-
path2.setAttribute("d", "m6 6 12 12")
|
|
245
|
-
|
|
246
|
-
svg.appendChild(path1)
|
|
247
|
-
svg.appendChild(path2)
|
|
248
|
-
btn.appendChild(svg)
|
|
249
|
-
badge.appendChild(btn)
|
|
250
|
-
|
|
251
|
-
// Insert badge directly in trigger, before the text input
|
|
252
|
-
this.badgeInputTarget.insertAdjacentElement("beforebegin", badge)
|
|
253
|
-
})
|
|
71
|
+
togglePopover(event) {
|
|
72
|
+
event.preventDefault()
|
|
254
73
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const badges = this.triggerTarget.querySelectorAll("[data-combobox-badge]")
|
|
258
|
-
if (badges.length > 0 && this.badgeInputTarget.offsetTop > badges[0].offsetTop) {
|
|
259
|
-
this.triggerTarget.classList.add("pt-1.5")
|
|
74
|
+
if (this.triggerTarget.ariaExpanded === "true") {
|
|
75
|
+
this.closePopover()
|
|
260
76
|
} else {
|
|
261
|
-
this.
|
|
77
|
+
this.openPopover(event)
|
|
262
78
|
}
|
|
263
79
|
}
|
|
264
80
|
|
|
265
|
-
|
|
266
|
-
if (
|
|
81
|
+
openPopover(event) {
|
|
82
|
+
if (event) event.preventDefault()
|
|
267
83
|
|
|
268
|
-
|
|
269
|
-
this.
|
|
84
|
+
this.updatePopoverPosition()
|
|
85
|
+
this.updatePopoverWidth()
|
|
86
|
+
this.triggerTarget.ariaExpanded = "true"
|
|
87
|
+
this.selectedItemIndex = null
|
|
88
|
+
this.itemTargets.forEach(item => item.ariaCurrent = "false")
|
|
89
|
+
this.popoverTarget.showPopover()
|
|
270
90
|
}
|
|
271
91
|
|
|
272
|
-
|
|
92
|
+
closePopover() {
|
|
93
|
+
this.triggerTarget.ariaExpanded = "false"
|
|
94
|
+
this.popoverTarget.hidePopover()
|
|
95
|
+
}
|
|
273
96
|
|
|
274
97
|
filterItems(e) {
|
|
275
|
-
if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key))
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
? this.badgeInputTarget.value
|
|
279
|
-
: this.hasInputTriggerTarget
|
|
280
|
-
? this.inputTriggerTarget.value
|
|
281
|
-
: this.searchInputTarget.value
|
|
282
|
-
|
|
283
|
-
this.applyFilter(term)
|
|
284
|
-
}
|
|
98
|
+
if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
285
101
|
|
|
286
|
-
|
|
287
|
-
const filterTerm = term.toLowerCase()
|
|
102
|
+
const filterTerm = this.searchInputTarget.value.toLowerCase()
|
|
288
103
|
|
|
289
104
|
if (this.hasToggleAllTarget) {
|
|
290
105
|
if (filterTerm) this.toggleAllTarget.parentElement.classList.add("hidden")
|
|
@@ -292,10 +107,12 @@ export default class extends Controller {
|
|
|
292
107
|
}
|
|
293
108
|
|
|
294
109
|
let resultCount = 0
|
|
110
|
+
|
|
295
111
|
this.selectedItemIndex = null
|
|
296
112
|
|
|
297
113
|
this.inputTargets.forEach((input) => {
|
|
298
114
|
const text = this.inputContent(input).toLowerCase()
|
|
115
|
+
|
|
299
116
|
if (text.indexOf(filterTerm) > -1) {
|
|
300
117
|
input.parentElement.classList.remove("hidden")
|
|
301
118
|
resultCount++
|
|
@@ -305,20 +122,9 @@ export default class extends Controller {
|
|
|
305
122
|
})
|
|
306
123
|
|
|
307
124
|
this.emptyStateTarget.classList.toggle("hidden", resultCount !== 0)
|
|
308
|
-
|
|
309
|
-
// Auto-highlight first visible result (without scrolling to avoid page jump)
|
|
310
|
-
this.itemTargets.forEach(item => item.ariaCurrent = "false")
|
|
311
|
-
const firstVisible = this.inputTargets.find(i => !i.parentElement.classList.contains("hidden"))
|
|
312
|
-
if (firstVisible) {
|
|
313
|
-
this.selectedItemIndex = 0
|
|
314
|
-
firstVisible.parentElement.ariaCurrent = "true"
|
|
315
|
-
}
|
|
316
125
|
}
|
|
317
126
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
keyDownPressed(event) {
|
|
321
|
-
event.preventDefault()
|
|
127
|
+
keyDownPressed() {
|
|
322
128
|
if (this.selectedItemIndex !== null) {
|
|
323
129
|
this.selectedItemIndex++
|
|
324
130
|
} else {
|
|
@@ -328,8 +134,7 @@ export default class extends Controller {
|
|
|
328
134
|
this.focusSelectedInput()
|
|
329
135
|
}
|
|
330
136
|
|
|
331
|
-
keyUpPressed(
|
|
332
|
-
event.preventDefault()
|
|
137
|
+
keyUpPressed() {
|
|
333
138
|
if (this.selectedItemIndex !== null) {
|
|
334
139
|
this.selectedItemIndex--
|
|
335
140
|
} else {
|
|
@@ -339,15 +144,6 @@ export default class extends Controller {
|
|
|
339
144
|
this.focusSelectedInput()
|
|
340
145
|
}
|
|
341
146
|
|
|
342
|
-
keyEnterPressed(event) {
|
|
343
|
-
event.preventDefault()
|
|
344
|
-
const option = this.itemTargets.find(item => item.ariaCurrent === "true")
|
|
345
|
-
|
|
346
|
-
if (option) {
|
|
347
|
-
option.click()
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
147
|
focusSelectedInput() {
|
|
352
148
|
const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden"))
|
|
353
149
|
|
|
@@ -363,19 +159,35 @@ export default class extends Controller {
|
|
|
363
159
|
})
|
|
364
160
|
}
|
|
365
161
|
|
|
162
|
+
keyEnterPressed(event) {
|
|
163
|
+
event.preventDefault()
|
|
164
|
+
const option = this.itemTargets.find(item => item.ariaCurrent === "true")
|
|
165
|
+
|
|
166
|
+
if (option) {
|
|
167
|
+
option.click()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
366
171
|
wrapSelectedInputIndex(length) {
|
|
367
172
|
this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length
|
|
368
173
|
}
|
|
369
174
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
175
|
+
updatePopoverPosition() {
|
|
176
|
+
this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => {
|
|
177
|
+
computePosition(this.triggerTarget, this.popoverTarget, {
|
|
178
|
+
placement: 'bottom-start',
|
|
179
|
+
middleware: [offset(4), flip()],
|
|
180
|
+
}).then(({ x, y }) => {
|
|
181
|
+
Object.assign(this.popoverTarget.style, {
|
|
182
|
+
left: `${x}px`,
|
|
183
|
+
top: `${y}px`,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
375
188
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
189
|
+
updatePopoverWidth() {
|
|
190
|
+
const width = Math.max(this.triggerTarget.offsetWidth, this.minPopoverWidthValue)
|
|
191
|
+
this.popoverTarget.style.width = `${width}px`
|
|
380
192
|
}
|
|
381
193
|
}
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class ComboboxItem < Base
|
|
5
5
|
def view_template(&)
|
|
6
|
-
label(**attrs)
|
|
7
|
-
yield if block_given?
|
|
8
|
-
render ComboboxItemIndicator.new
|
|
9
|
-
end
|
|
6
|
+
label(**attrs, &)
|
|
10
7
|
end
|
|
11
8
|
|
|
12
9
|
private
|
|
13
10
|
|
|
14
11
|
def default_attrs
|
|
15
12
|
{
|
|
16
|
-
class:
|
|
13
|
+
class: [
|
|
14
|
+
"flex flex-row w-full text-wrap [&>span,&>div]:truncate gap-2 items-center rounded-sm px-2 py-1 text-sm outline-none cursor-pointer",
|
|
15
|
+
"select-none has-[:checked]:bg-accent hover:bg-accent p-2",
|
|
16
|
+
"[&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 aria-[current=true]:bg-accent aria-[current=true]:ring aria-[current=true]:ring-offset-2",
|
|
17
|
+
"has-disabled:opacity-50 has-disabled:cursor-not-allowed"
|
|
18
|
+
],
|
|
17
19
|
role: "option",
|
|
18
20
|
data: {
|
|
19
21
|
ruby_ui__combobox_target: "item"
|
|
@@ -12,11 +12,16 @@ module RubyUI
|
|
|
12
12
|
{
|
|
13
13
|
class: "inset-auto m-0 absolute border bg-background shadow-lg rounded-lg",
|
|
14
14
|
role: "popover",
|
|
15
|
+
autofocus: true,
|
|
15
16
|
popover: true,
|
|
16
17
|
data: {
|
|
17
18
|
ruby_ui__combobox_target: "popover",
|
|
18
19
|
action: %w[
|
|
19
20
|
toggle->ruby-ui--combobox#handlePopoverToggle
|
|
21
|
+
keydown.down->ruby-ui--combobox#keyDownPressed
|
|
22
|
+
keydown.up->ruby-ui--combobox#keyUpPressed
|
|
23
|
+
keydown.enter->ruby-ui--combobox#keyEnterPressed
|
|
24
|
+
keydown.esc->ruby-ui--combobox#closePopover:prevent
|
|
20
25
|
resize@window->ruby-ui--combobox#updatePopoverWidth
|
|
21
26
|
]
|
|
22
27
|
}
|
|
@@ -10,7 +10,14 @@ module RubyUI
|
|
|
10
10
|
|
|
11
11
|
def default_attrs
|
|
12
12
|
{
|
|
13
|
-
class:
|
|
13
|
+
class: [
|
|
14
|
+
"aspect-square h-4 w-4 rounded-full border border-primary accent-primary text-primary shadow",
|
|
15
|
+
"focus:outline-none",
|
|
16
|
+
"focus-visible:ring-1 focus-visible:ring-ring",
|
|
17
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
18
|
+
"checked:bg-primary checked:text-primary-foreground",
|
|
19
|
+
"aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none"
|
|
20
|
+
],
|
|
14
21
|
data: {
|
|
15
22
|
ruby_ui__combobox_target: "input",
|
|
16
23
|
ruby_ui__form_field_target: "input",
|
|
@@ -10,7 +10,13 @@ module RubyUI
|
|
|
10
10
|
|
|
11
11
|
def default_attrs
|
|
12
12
|
{
|
|
13
|
-
class:
|
|
13
|
+
class: [
|
|
14
|
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
|
|
15
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
+
"checked:bg-primary checked:text-primary-foreground",
|
|
17
|
+
"aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none",
|
|
18
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
19
|
+
],
|
|
14
20
|
data: {
|
|
15
21
|
ruby_ui__combobox_target: "toggleAll",
|
|
16
22
|
action: "change->ruby-ui--combobox#toggleAllItems"
|
|
@@ -9,7 +9,7 @@ module RubyUI
|
|
|
9
9
|
|
|
10
10
|
def view_template
|
|
11
11
|
button(**attrs) do
|
|
12
|
-
span(class: "truncate
|
|
12
|
+
span(class: "truncate", data: {ruby_ui__combobox_target: "triggerContent"}) do
|
|
13
13
|
@placeholder
|
|
14
14
|
end
|
|
15
15
|
icon
|
|
@@ -26,13 +26,12 @@ module RubyUI
|
|
|
26
26
|
"hover:bg-accent hover:text-accent-foreground",
|
|
27
27
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
28
28
|
"aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:cursor-not-allowed",
|
|
29
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
30
|
-
"aria-invalid:border-destructive"
|
|
29
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
31
30
|
],
|
|
32
31
|
data: {
|
|
33
32
|
placeholder: @placeholder,
|
|
34
33
|
ruby_ui__combobox_target: "trigger",
|
|
35
|
-
action: "
|
|
34
|
+
action: "ruby-ui--combobox#togglePopover"
|
|
36
35
|
},
|
|
37
36
|
aria: {
|
|
38
37
|
haspopup: "listbox",
|
|
@@ -42,21 +41,22 @@ module RubyUI
|
|
|
42
41
|
end
|
|
43
42
|
|
|
44
43
|
def icon
|
|
45
|
-
|
|
46
|
-
svg
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
svg(
|
|
45
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
46
|
+
viewbox: "0 0 24 24",
|
|
47
|
+
fill: "none",
|
|
48
|
+
stroke: "currentColor",
|
|
49
|
+
class: "ml-2 h-4 w-4 shrink-0 opacity-50",
|
|
50
|
+
stroke_width: "2",
|
|
51
|
+
stroke_linecap: "round",
|
|
52
|
+
stroke_linejoin: "round"
|
|
53
|
+
) do |s|
|
|
54
|
+
s.path(
|
|
55
|
+
d: "m7 15 5 5 5-5"
|
|
56
|
+
)
|
|
57
|
+
s.path(
|
|
58
|
+
d: "m7 9 5-5 5 5"
|
|
59
|
+
)
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
end
|
|
@@ -3,31 +3,18 @@ import Fuse from "fuse.js";
|
|
|
3
3
|
|
|
4
4
|
// Connects to data-controller="ruby-ui--command"
|
|
5
5
|
export default class extends Controller {
|
|
6
|
-
static targets = ["input", "group", "item", "empty"
|
|
7
|
-
|
|
8
|
-
static values = {
|
|
9
|
-
open: {
|
|
10
|
-
type: Boolean,
|
|
11
|
-
default: false,
|
|
12
|
-
},
|
|
13
|
-
};
|
|
6
|
+
static targets = ["input", "group", "item", "empty"];
|
|
14
7
|
|
|
15
8
|
connect() {
|
|
16
|
-
this.inputTarget.focus();
|
|
17
|
-
this.searchIndex = this.buildSearchIndex();
|
|
18
|
-
this.toggleVisibility(this.emptyTargets, false);
|
|
19
9
|
this.selectedIndex = -1;
|
|
20
10
|
|
|
21
|
-
if (this.
|
|
22
|
-
|
|
11
|
+
if (!this.hasInputTarget) {
|
|
12
|
+
return;
|
|
23
13
|
}
|
|
24
|
-
}
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// prevent scroll on body
|
|
30
|
-
document.body.classList.add("overflow-hidden");
|
|
15
|
+
this.inputTarget.focus();
|
|
16
|
+
this.searchIndex = this.buildSearchIndex();
|
|
17
|
+
this.toggleVisibility(this.emptyTargets, false);
|
|
31
18
|
}
|
|
32
19
|
|
|
33
20
|
dismiss() {
|
|
@@ -37,6 +24,10 @@ export default class extends Controller {
|
|
|
37
24
|
this.element.remove();
|
|
38
25
|
}
|
|
39
26
|
|
|
27
|
+
focusInput() {
|
|
28
|
+
this.inputTarget?.focus();
|
|
29
|
+
}
|
|
30
|
+
|
|
40
31
|
filter(e) {
|
|
41
32
|
// Deselect any previously selected item
|
|
42
33
|
this.deselectAll();
|
|
@@ -10,7 +10,10 @@ module RubyUI
|
|
|
10
10
|
|
|
11
11
|
def default_attrs
|
|
12
12
|
{
|
|
13
|
-
data: {
|
|
13
|
+
data: {
|
|
14
|
+
controller: "ruby-ui--command-dialog",
|
|
15
|
+
ruby_ui__command_dialog_ruby_ui__command_outlet: "[data-ruby-ui--command-dialog-instance]"
|
|
16
|
+
}
|
|
14
17
|
}
|
|
15
18
|
end
|
|
16
19
|
end
|
|
@@ -17,8 +17,8 @@ module RubyUI
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def view_template(&block)
|
|
20
|
-
template(data: {
|
|
21
|
-
div(data: {controller: "ruby-ui--command"}) do
|
|
20
|
+
template(data: {ruby_ui__command_dialog_target: "content"}) do
|
|
21
|
+
div(data: {controller: "ruby-ui--command", ruby_ui__command_dialog_instance: true}) do
|
|
22
22
|
backdrop
|
|
23
23
|
div(**attrs, &block)
|
|
24
24
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="ruby-ui--command-dialog"
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["content"];
|
|
6
|
+
static outlets = ["ruby-ui--command"];
|
|
7
|
+
|
|
8
|
+
rubyUiCommandOutletConnected(controller) {
|
|
9
|
+
this.openOutlet = controller;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
rubyUiCommandOutletDisconnected() {
|
|
13
|
+
this.openOutlet = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
open(e) {
|
|
17
|
+
if (e) {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!this.hasContentTarget) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (this.openOutlet) {
|
|
26
|
+
this.openOutlet.focusInput();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML);
|
|
31
|
+
// prevent scroll on body
|
|
32
|
+
document.body.classList.add("overflow-hidden");
|
|
33
|
+
}
|
|
34
|
+
}
|