ruby_ui 1.0.2 → 1.2.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/LICENSE.txt +1 -1
- data/README.md +4 -0
- data/lib/generators/ruby_ui/component_generator.rb +5 -1
- data/lib/generators/ruby_ui/dependencies.yml +10 -0
- data/lib/generators/ruby_ui/install/docs_generator.rb +33 -0
- data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +4 -0
- data/lib/ruby_ui/accordion/accordion_docs.rb +53 -0
- data/lib/ruby_ui/alert/alert_docs.rb +135 -0
- data/lib/ruby_ui/alert_dialog/alert_dialog_docs.rb +35 -0
- data/lib/ruby_ui/aspect_ratio/aspect_ratio_docs.rb +64 -0
- data/lib/ruby_ui/avatar/avatar_docs.rb +92 -0
- data/lib/ruby_ui/badge/badge_docs.rb +80 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_docs.rb +116 -0
- data/lib/ruby_ui/button/button_docs.rb +143 -0
- data/lib/ruby_ui/calendar/calendar_docs.rb +34 -0
- data/lib/ruby_ui/card/card_docs.rb +114 -0
- data/lib/ruby_ui/carousel/carousel_docs.rb +104 -0
- data/lib/ruby_ui/chart/chart_docs.rb +115 -0
- data/lib/ruby_ui/checkbox/checkbox.rb +2 -2
- data/lib/ruby_ui/checkbox/checkbox_docs.rb +41 -0
- data/lib/ruby_ui/clipboard/clipboard_docs.rb +30 -0
- data/lib/ruby_ui/codeblock/codeblock_docs.rb +55 -0
- data/lib/ruby_ui/collapsible/collapsible_docs.rb +96 -0
- data/lib/ruby_ui/combobox/combobox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_badge.rb +17 -0
- data/lib/ruby_ui/combobox/combobox_badge_trigger.rb +47 -0
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
- data/lib/ruby_ui/combobox/combobox_controller.js +252 -47
- data/lib/ruby_ui/combobox/combobox_docs.rb +286 -0
- data/lib/ruby_ui/combobox/combobox_input_trigger.rb +64 -0
- data/lib/ruby_ui/combobox/combobox_item.rb +5 -7
- data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
- data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
- data/lib/ruby_ui/combobox/combobox_popover.rb +1 -5
- data/lib/ruby_ui/combobox/combobox_radio.rb +1 -8
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +1 -6
- data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
- data/lib/ruby_ui/command/command_docs.rb +154 -0
- data/lib/ruby_ui/context_menu/context_menu.rb +1 -1
- data/lib/ruby_ui/context_menu/context_menu_docs.rb +85 -0
- data/lib/ruby_ui/data_table/data_table.rb +29 -0
- data/lib/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
- data/lib/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
- data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +14 -0
- data/lib/ruby_ui/data_table/data_table_controller.js +57 -0
- data/lib/ruby_ui/data_table/data_table_docs.rb +180 -0
- data/lib/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
- data/lib/ruby_ui/data_table/data_table_form.rb +39 -0
- data/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_pagination.rb +100 -0
- data/lib/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
- data/lib/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
- data/lib/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
- data/lib/ruby_ui/data_table/data_table_search.rb +57 -0
- data/lib/ruby_ui/data_table/data_table_search_controller.js +62 -0
- data/lib/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
- data/lib/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
- data/lib/ruby_ui/data_table/data_table_sort_head.rb +112 -0
- data/lib/ruby_ui/data_table/data_table_toolbar.rb +15 -0
- data/lib/ruby_ui/dialog/dialog_docs.rb +102 -0
- data/lib/ruby_ui/docs/base.rb +90 -0
- data/lib/ruby_ui/docs/component_setup_tabs.rb +15 -0
- data/lib/ruby_ui/docs/components_table.rb +13 -0
- data/lib/ruby_ui/docs/header.rb +17 -0
- data/lib/ruby_ui/docs/sidebar_examples.rb +22 -0
- data/lib/ruby_ui/docs/visual_code_example.rb +22 -0
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb +212 -0
- data/lib/ruby_ui/form/form_docs.rb +178 -0
- data/lib/ruby_ui/form/form_field.rb +1 -1
- data/lib/ruby_ui/form/form_field_error.rb +1 -1
- data/lib/ruby_ui/form/form_field_hint.rb +1 -1
- data/lib/ruby_ui/form/form_field_label.rb +1 -1
- data/lib/ruby_ui/hover_card/hover_card_docs.rb +71 -0
- data/lib/ruby_ui/input/input.rb +4 -3
- data/lib/ruby_ui/input/input_docs.rb +68 -0
- data/lib/ruby_ui/link/link_docs.rb +106 -0
- data/lib/ruby_ui/masked_input/masked_input.rb +11 -1
- data/lib/ruby_ui/masked_input/masked_input_controller.js +13 -0
- data/lib/ruby_ui/masked_input/masked_input_docs.rb +47 -0
- data/lib/ruby_ui/native_select/native_select.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_docs.rb +83 -0
- data/lib/ruby_ui/native_select/native_select_group.rb +15 -0
- data/lib/ruby_ui/native_select/native_select_icon.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_option.rb +15 -0
- data/lib/ruby_ui/pagination/pagination_docs.rb +127 -0
- data/lib/ruby_ui/popover/popover_docs.rb +971 -0
- data/lib/ruby_ui/progress/progress_docs.rb +27 -0
- data/lib/ruby_ui/radio_button/radio_button.rb +1 -1
- data/lib/ruby_ui/radio_button/radio_button_docs.rb +53 -0
- data/lib/ruby_ui/select/select_docs.rb +129 -0
- data/lib/ruby_ui/separator/separator_docs.rb +36 -0
- data/lib/ruby_ui/sheet/sheet_content.rb +1 -1
- data/lib/ruby_ui/sheet/sheet_docs.rb +76 -0
- data/lib/ruby_ui/shortcut_key/shortcut_key_docs.rb +29 -0
- data/lib/ruby_ui/sidebar/sidebar_docs.rb +176 -0
- data/lib/ruby_ui/skeleton/skeleton_docs.rb +29 -0
- data/lib/ruby_ui/switch/switch_docs.rb +46 -0
- data/lib/ruby_ui/table/table_docs.rb +102 -0
- data/lib/ruby_ui/tabs/tabs_docs.rb +211 -0
- data/lib/ruby_ui/textarea/textarea_docs.rb +54 -0
- data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +71 -0
- data/lib/ruby_ui/tooltip/tooltip_docs.rb +52 -0
- data/lib/ruby_ui/typography/typography_docs.rb +107 -0
- data/lib/ruby_ui.rb +1 -1
- metadata +90 -3
|
@@ -15,75 +15,276 @@ export default class extends Controller {
|
|
|
15
15
|
"emptyState",
|
|
16
16
|
"searchInput",
|
|
17
17
|
"trigger",
|
|
18
|
-
"triggerContent"
|
|
18
|
+
"triggerContent",
|
|
19
|
+
"badgeContainer",
|
|
20
|
+
"clearButton",
|
|
21
|
+
"badgeInput",
|
|
22
|
+
"inputTrigger"
|
|
19
23
|
]
|
|
20
24
|
|
|
21
25
|
selectedItemIndex = null
|
|
22
26
|
|
|
23
27
|
connect() {
|
|
24
28
|
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) })
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
disconnect() {
|
|
28
40
|
if (this.cleanup) { this.cleanup() }
|
|
29
41
|
}
|
|
30
42
|
|
|
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
|
+
handlePopoverToggle(event) {
|
|
86
|
+
// Keep ariaExpanded in sync with the actual popover state
|
|
87
|
+
this.triggerTarget.ariaExpanded = event.newState === 'open' ? 'true' : 'false'
|
|
88
|
+
}
|
|
89
|
+
|
|
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
|
+
|
|
31
110
|
inputChanged(e) {
|
|
32
111
|
this.updateTriggerContent()
|
|
33
112
|
|
|
34
113
|
if (e.target.type == "radio") {
|
|
35
114
|
this.closePopover()
|
|
115
|
+
this.updateInputTrigger()
|
|
36
116
|
}
|
|
37
117
|
|
|
38
118
|
if (this.hasToggleAllTarget && !e.target.checked) {
|
|
39
119
|
this.toggleAllTarget.checked = false
|
|
40
120
|
}
|
|
41
|
-
}
|
|
42
121
|
|
|
43
|
-
|
|
44
|
-
|
|
122
|
+
this.updateBadges()
|
|
123
|
+
this.updateClearButton()
|
|
45
124
|
}
|
|
46
125
|
|
|
47
126
|
toggleAllItems() {
|
|
48
127
|
const isChecked = this.toggleAllTarget.checked
|
|
49
128
|
this.inputTargets.forEach(input => input.checked = isChecked)
|
|
50
129
|
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
|
|
51
161
|
}
|
|
52
162
|
|
|
53
163
|
updateTriggerContent() {
|
|
164
|
+
if (!this.hasTriggerContentTarget) return
|
|
165
|
+
|
|
54
166
|
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
|
55
167
|
|
|
56
168
|
if (checkedInputs.length === 0) {
|
|
57
169
|
this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder
|
|
170
|
+
this.triggerContentTarget.classList.add("text-muted-foreground")
|
|
58
171
|
} else if (this.termValue && checkedInputs.length > 1) {
|
|
59
172
|
this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`
|
|
173
|
+
this.triggerContentTarget.classList.remove("text-muted-foreground")
|
|
60
174
|
} else {
|
|
61
175
|
this.triggerContentTarget.innerText = checkedInputs.map((input) => this.inputContent(input)).join(", ")
|
|
176
|
+
this.triggerContentTarget.classList.remove("text-muted-foreground")
|
|
62
177
|
}
|
|
63
178
|
}
|
|
64
179
|
|
|
65
|
-
|
|
66
|
-
|
|
180
|
+
updateInputTrigger() {
|
|
181
|
+
if (!this.hasInputTriggerTarget) return
|
|
182
|
+
const checked = this.inputTargets.find(i => i.checked)
|
|
183
|
+
this.inputTriggerTarget.value = checked ? this.inputContent(checked) : ""
|
|
184
|
+
}
|
|
67
185
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.
|
|
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
|
+
})
|
|
254
|
+
|
|
255
|
+
// Add top padding only when badges wrap to multiple lines
|
|
256
|
+
// Class "pt-1.5" is referenced in ComboboxBadgeTrigger for Tailwind to compile it
|
|
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")
|
|
260
|
+
} else {
|
|
261
|
+
this.triggerTarget.classList.remove("pt-1.5")
|
|
262
|
+
}
|
|
74
263
|
}
|
|
75
264
|
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
265
|
+
updateClearButton() {
|
|
266
|
+
if (!this.hasClearButtonTarget) return
|
|
267
|
+
|
|
268
|
+
const hasChecked = this.inputTargets.some(input => input.checked)
|
|
269
|
+
this.clearButtonTarget.classList.toggle("hidden", !hasChecked)
|
|
79
270
|
}
|
|
80
271
|
|
|
272
|
+
// Filter
|
|
273
|
+
|
|
81
274
|
filterItems(e) {
|
|
82
|
-
if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key))
|
|
83
|
-
return
|
|
84
|
-
}
|
|
275
|
+
if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) return
|
|
85
276
|
|
|
86
|
-
const
|
|
277
|
+
const term = this.hasBadgeInputTarget
|
|
278
|
+
? this.badgeInputTarget.value
|
|
279
|
+
: this.hasInputTriggerTarget
|
|
280
|
+
? this.inputTriggerTarget.value
|
|
281
|
+
: this.searchInputTarget.value
|
|
282
|
+
|
|
283
|
+
this.applyFilter(term)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
applyFilter(term) {
|
|
287
|
+
const filterTerm = term.toLowerCase()
|
|
87
288
|
|
|
88
289
|
if (this.hasToggleAllTarget) {
|
|
89
290
|
if (filterTerm) this.toggleAllTarget.parentElement.classList.add("hidden")
|
|
@@ -91,12 +292,10 @@ export default class extends Controller {
|
|
|
91
292
|
}
|
|
92
293
|
|
|
93
294
|
let resultCount = 0
|
|
94
|
-
|
|
95
295
|
this.selectedItemIndex = null
|
|
96
296
|
|
|
97
297
|
this.inputTargets.forEach((input) => {
|
|
98
298
|
const text = this.inputContent(input).toLowerCase()
|
|
99
|
-
|
|
100
299
|
if (text.indexOf(filterTerm) > -1) {
|
|
101
300
|
input.parentElement.classList.remove("hidden")
|
|
102
301
|
resultCount++
|
|
@@ -106,9 +305,20 @@ export default class extends Controller {
|
|
|
106
305
|
})
|
|
107
306
|
|
|
108
307
|
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
|
+
}
|
|
109
316
|
}
|
|
110
317
|
|
|
111
|
-
|
|
318
|
+
// Keyboard
|
|
319
|
+
|
|
320
|
+
keyDownPressed(event) {
|
|
321
|
+
event.preventDefault()
|
|
112
322
|
if (this.selectedItemIndex !== null) {
|
|
113
323
|
this.selectedItemIndex++
|
|
114
324
|
} else {
|
|
@@ -118,7 +328,8 @@ export default class extends Controller {
|
|
|
118
328
|
this.focusSelectedInput()
|
|
119
329
|
}
|
|
120
330
|
|
|
121
|
-
keyUpPressed() {
|
|
331
|
+
keyUpPressed(event) {
|
|
332
|
+
event.preventDefault()
|
|
122
333
|
if (this.selectedItemIndex !== null) {
|
|
123
334
|
this.selectedItemIndex--
|
|
124
335
|
} else {
|
|
@@ -128,6 +339,15 @@ export default class extends Controller {
|
|
|
128
339
|
this.focusSelectedInput()
|
|
129
340
|
}
|
|
130
341
|
|
|
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
|
+
|
|
131
351
|
focusSelectedInput() {
|
|
132
352
|
const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden"))
|
|
133
353
|
|
|
@@ -143,34 +363,19 @@ export default class extends Controller {
|
|
|
143
363
|
})
|
|
144
364
|
}
|
|
145
365
|
|
|
146
|
-
keyEnterPressed(event) {
|
|
147
|
-
event.preventDefault()
|
|
148
|
-
const option = this.itemTargets.find(item => item.ariaCurrent === "true")
|
|
149
|
-
|
|
150
|
-
if (option) {
|
|
151
|
-
option.click()
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
366
|
wrapSelectedInputIndex(length) {
|
|
156
367
|
this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length
|
|
157
368
|
}
|
|
158
369
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
computePosition(this.triggerTarget, this.popoverTarget, {
|
|
162
|
-
placement: 'bottom-start',
|
|
163
|
-
middleware: [offset(4), flip()],
|
|
164
|
-
}).then(({ x, y }) => {
|
|
165
|
-
Object.assign(this.popoverTarget.style, {
|
|
166
|
-
left: `${x}px`,
|
|
167
|
-
top: `${y}px`,
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
}
|
|
370
|
+
handleBadgeInputBackspace(event) {
|
|
371
|
+
if (this.badgeInputTarget.value !== "") return
|
|
172
372
|
|
|
173
|
-
|
|
174
|
-
|
|
373
|
+
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
|
374
|
+
const lastChecked = checkedInputs[checkedInputs.length - 1]
|
|
375
|
+
|
|
376
|
+
if (lastChecked) {
|
|
377
|
+
lastChecked.checked = false
|
|
378
|
+
lastChecked.dispatchEvent(new Event("change", { bubbles: true }))
|
|
379
|
+
}
|
|
175
380
|
}
|
|
176
381
|
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Views::Docs::Combobox < Views::Base
|
|
4
|
+
def view_template
|
|
5
|
+
component = "Combobox"
|
|
6
|
+
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
|
|
7
|
+
render Docs::Header.new(title: component, description: "Autocomplete input and command palette with a list of suggestions.")
|
|
8
|
+
|
|
9
|
+
Heading(level: 2) { "Usage" }
|
|
10
|
+
|
|
11
|
+
render Docs::VisualCodeExample.new(title: "Basic", context: self) do
|
|
12
|
+
<<~RUBY
|
|
13
|
+
div(class: "w-96") do
|
|
14
|
+
Combobox do
|
|
15
|
+
ComboboxInputTrigger(placeholder: "Select framework...")
|
|
16
|
+
|
|
17
|
+
ComboboxPopover do
|
|
18
|
+
ComboboxList do
|
|
19
|
+
ComboboxEmptyState { "No results found." }
|
|
20
|
+
|
|
21
|
+
ComboboxItem do
|
|
22
|
+
ComboboxRadio(name: "framework", value: "rails")
|
|
23
|
+
span { "Rails" }
|
|
24
|
+
end
|
|
25
|
+
ComboboxItem do
|
|
26
|
+
ComboboxRadio(name: "framework", value: "hanami")
|
|
27
|
+
span { "Hanami" }
|
|
28
|
+
end
|
|
29
|
+
ComboboxItem do
|
|
30
|
+
ComboboxRadio(name: "framework", value: "nextjs")
|
|
31
|
+
span { "Next.js" }
|
|
32
|
+
end
|
|
33
|
+
ComboboxItem do
|
|
34
|
+
ComboboxRadio(name: "framework", value: "nuxt")
|
|
35
|
+
span { "Nuxt" }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
RUBY
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
render Docs::VisualCodeExample.new(title: "Popup", context: self) do
|
|
45
|
+
<<~RUBY
|
|
46
|
+
div(class: "w-96") do
|
|
47
|
+
Combobox do
|
|
48
|
+
ComboboxTrigger(placeholder: "Select framework...")
|
|
49
|
+
|
|
50
|
+
ComboboxPopover do
|
|
51
|
+
ComboboxSearchInput(placeholder: "Search framework...")
|
|
52
|
+
|
|
53
|
+
ComboboxList do
|
|
54
|
+
ComboboxEmptyState { "No results found." }
|
|
55
|
+
|
|
56
|
+
ComboboxItem do
|
|
57
|
+
ComboboxRadio(name: "fw2", value: "rails")
|
|
58
|
+
span { "Rails" }
|
|
59
|
+
end
|
|
60
|
+
ComboboxItem do
|
|
61
|
+
ComboboxRadio(name: "fw2", value: "hanami")
|
|
62
|
+
span { "Hanami" }
|
|
63
|
+
end
|
|
64
|
+
ComboboxItem do
|
|
65
|
+
ComboboxRadio(name: "fw2", value: "nextjs")
|
|
66
|
+
span { "Next.js" }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
RUBY
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
render Docs::VisualCodeExample.new(title: "Multiple", context: self) do
|
|
76
|
+
<<~RUBY
|
|
77
|
+
div(class: "w-96") do
|
|
78
|
+
Combobox do
|
|
79
|
+
ComboboxBadgeTrigger(clear_button: true)
|
|
80
|
+
|
|
81
|
+
ComboboxPopover do
|
|
82
|
+
ComboboxList do
|
|
83
|
+
ComboboxEmptyState { "No results found." }
|
|
84
|
+
|
|
85
|
+
ComboboxItem do
|
|
86
|
+
ComboboxCheckbox(name: "frameworks[]", value: "rails")
|
|
87
|
+
span { "Rails" }
|
|
88
|
+
end
|
|
89
|
+
ComboboxItem do
|
|
90
|
+
ComboboxCheckbox(name: "frameworks[]", value: "hanami")
|
|
91
|
+
span { "Hanami" }
|
|
92
|
+
end
|
|
93
|
+
ComboboxItem do
|
|
94
|
+
ComboboxCheckbox(name: "frameworks[]", value: "sinatra")
|
|
95
|
+
span { "Sinatra" }
|
|
96
|
+
end
|
|
97
|
+
ComboboxItem do
|
|
98
|
+
ComboboxCheckbox(name: "frameworks[]", value: "nextjs", checked: true)
|
|
99
|
+
span { "Next.js" }
|
|
100
|
+
end
|
|
101
|
+
ComboboxItem do
|
|
102
|
+
ComboboxCheckbox(name: "frameworks[]", value: "nuxt")
|
|
103
|
+
span { "Nuxt" }
|
|
104
|
+
end
|
|
105
|
+
ComboboxItem do
|
|
106
|
+
ComboboxCheckbox(name: "frameworks[]", value: "svelte")
|
|
107
|
+
span { "SvelteKit" }
|
|
108
|
+
end
|
|
109
|
+
ComboboxItem do
|
|
110
|
+
ComboboxCheckbox(name: "frameworks[]", value: "remix")
|
|
111
|
+
span { "Remix" }
|
|
112
|
+
end
|
|
113
|
+
ComboboxItem do
|
|
114
|
+
ComboboxCheckbox(name: "frameworks[]", value: "astro")
|
|
115
|
+
span { "Astro" }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
RUBY
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
render Docs::VisualCodeExample.new(title: "Groups", context: self) do
|
|
125
|
+
<<~RUBY
|
|
126
|
+
div(class: "w-96") do
|
|
127
|
+
Combobox do
|
|
128
|
+
ComboboxInputTrigger(placeholder: "Select food...")
|
|
129
|
+
|
|
130
|
+
ComboboxPopover do
|
|
131
|
+
ComboboxList do
|
|
132
|
+
ComboboxEmptyState { "No results found." }
|
|
133
|
+
|
|
134
|
+
ComboboxListGroup(label: "Fruits") do
|
|
135
|
+
ComboboxItem do
|
|
136
|
+
ComboboxRadio(name: "food", value: "apple")
|
|
137
|
+
span { "Apple" }
|
|
138
|
+
end
|
|
139
|
+
ComboboxItem do
|
|
140
|
+
ComboboxRadio(name: "food", value: "banana")
|
|
141
|
+
span { "Banana" }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
ComboboxListGroup(label: "Vegetables") do
|
|
146
|
+
ComboboxItem do
|
|
147
|
+
ComboboxRadio(name: "food", value: "broccoli")
|
|
148
|
+
span { "Broccoli" }
|
|
149
|
+
end
|
|
150
|
+
ComboboxItem do
|
|
151
|
+
ComboboxRadio(name: "food", value: "carrot")
|
|
152
|
+
span { "Carrot" }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
ComboboxListGroup(label: "Grains") do
|
|
157
|
+
ComboboxItem do
|
|
158
|
+
ComboboxRadio(name: "food", value: "rice")
|
|
159
|
+
span { "Rice" }
|
|
160
|
+
end
|
|
161
|
+
ComboboxItem do
|
|
162
|
+
ComboboxRadio(name: "food", value: "wheat")
|
|
163
|
+
span { "Wheat" }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
RUBY
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
render Docs::VisualCodeExample.new(title: "Custom Items", context: self) do
|
|
174
|
+
<<~RUBY
|
|
175
|
+
div(class: "w-96") do
|
|
176
|
+
Combobox do
|
|
177
|
+
ComboboxInputTrigger(placeholder: "Select status...")
|
|
178
|
+
|
|
179
|
+
ComboboxPopover do
|
|
180
|
+
ComboboxList do
|
|
181
|
+
ComboboxEmptyState { "No results found." }
|
|
182
|
+
|
|
183
|
+
ComboboxItem do
|
|
184
|
+
ComboboxRadio(name: "status", value: "backlog", data: {text: "Backlog"})
|
|
185
|
+
svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", class: "text-muted-foreground") { |s| s.circle(cx: "12", cy: "12", r: "10") }
|
|
186
|
+
span { "Backlog" }
|
|
187
|
+
end
|
|
188
|
+
ComboboxItem do
|
|
189
|
+
ComboboxRadio(name: "status", value: "todo", data: {text: "Todo"})
|
|
190
|
+
svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", class: "text-blue-500") { |s| s.circle(cx: "12", cy: "12", r: "10") }
|
|
191
|
+
span { "Todo" }
|
|
192
|
+
end
|
|
193
|
+
ComboboxItem do
|
|
194
|
+
ComboboxRadio(name: "status", value: "done", data: {text: "Done"})
|
|
195
|
+
svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewbox: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2", class: "text-green-500") { |s| s.path(d: "M22 11.08V12a10 10 0 1 1-5.93-9.14"); s.path(d: "m9 11 3 3L22 4") }
|
|
196
|
+
span { "Done" }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
RUBY
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
render Docs::VisualCodeExample.new(title: "Invalid", context: self) do
|
|
206
|
+
<<~RUBY
|
|
207
|
+
div(class: "w-96") do
|
|
208
|
+
Combobox do
|
|
209
|
+
ComboboxInputTrigger(placeholder: "Required field", aria: {invalid: "true"})
|
|
210
|
+
|
|
211
|
+
ComboboxPopover do
|
|
212
|
+
ComboboxList do
|
|
213
|
+
ComboboxEmptyState { "No results found." }
|
|
214
|
+
|
|
215
|
+
ComboboxItem do
|
|
216
|
+
ComboboxRadio(name: "req", value: "option1")
|
|
217
|
+
span { "Option 1" }
|
|
218
|
+
end
|
|
219
|
+
ComboboxItem do
|
|
220
|
+
ComboboxRadio(name: "req", value: "option2")
|
|
221
|
+
span { "Option 2" }
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
RUBY
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
render Docs::VisualCodeExample.new(title: "Disabled", context: self) do
|
|
231
|
+
<<~RUBY
|
|
232
|
+
div(class: "w-96 space-y-2") do
|
|
233
|
+
Combobox do
|
|
234
|
+
ComboboxTrigger(disabled: true, placeholder: "Disabled trigger")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
Combobox do
|
|
238
|
+
ComboboxInputTrigger(placeholder: "Disabled input", disabled: true)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
RUBY
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
render Docs::VisualCodeExample.new(title: "Auto Highlight", context: self) do
|
|
245
|
+
<<~RUBY
|
|
246
|
+
div(class: "w-96") do
|
|
247
|
+
Combobox do
|
|
248
|
+
ComboboxInputTrigger(placeholder: "Type to search...")
|
|
249
|
+
|
|
250
|
+
ComboboxPopover do
|
|
251
|
+
ComboboxList do
|
|
252
|
+
ComboboxEmptyState { "No results found." }
|
|
253
|
+
|
|
254
|
+
ComboboxItem do
|
|
255
|
+
ComboboxRadio(name: "color", value: "red")
|
|
256
|
+
span { "Red" }
|
|
257
|
+
end
|
|
258
|
+
ComboboxItem do
|
|
259
|
+
ComboboxRadio(name: "color", value: "green")
|
|
260
|
+
span { "Green" }
|
|
261
|
+
end
|
|
262
|
+
ComboboxItem do
|
|
263
|
+
ComboboxRadio(name: "color", value: "blue")
|
|
264
|
+
span { "Blue" }
|
|
265
|
+
end
|
|
266
|
+
ComboboxItem do
|
|
267
|
+
ComboboxRadio(name: "color", value: "yellow")
|
|
268
|
+
span { "Yellow" }
|
|
269
|
+
end
|
|
270
|
+
ComboboxItem do
|
|
271
|
+
ComboboxRadio(name: "color", value: "purple")
|
|
272
|
+
span { "Purple" }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
RUBY
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
render Components::ComponentSetup::Tabs.new(component_name: component)
|
|
282
|
+
|
|
283
|
+
render Docs::ComponentsTable.new(component_files(component))
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|