ruby_ui 1.0.0.beta1 → 1.0.0.rc1
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 +21 -0
- data/README.md +85 -0
- data/lib/generators/ruby_ui/component_generator.rb +4 -40
- data/lib/generators/ruby_ui/dependencies.yml +74 -0
- data/lib/generators/ruby_ui/install/install_generator.rb +21 -22
- data/lib/generators/ruby_ui/install/templates/ruby_ui.rb.erb +18 -0
- data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +156 -0
- data/lib/generators/ruby_ui/javascript_utils.rb +21 -0
- data/lib/ruby_ui/accordion/accordion_controller.js +97 -0
- data/lib/ruby_ui/alert/alert.rb +1 -1
- data/lib/ruby_ui/alert_dialog/alert_dialog_content.rb +1 -1
- data/lib/ruby_ui/alert_dialog/alert_dialog_controller.js +31 -0
- data/lib/ruby_ui/alert_dialog/alert_dialog_footer.rb +1 -1
- data/lib/ruby_ui/alert_dialog/alert_dialog_header.rb +1 -1
- data/lib/ruby_ui/breadcrumb/breadcrumb.rb +17 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_ellipsis.rb +39 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_item.rb +17 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_link.rb +22 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_list.rb +17 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_page.rb +19 -0
- data/lib/ruby_ui/breadcrumb/breadcrumb_separator.rb +38 -0
- data/lib/ruby_ui/calendar/calendar_controller.js +249 -0
- data/lib/ruby_ui/calendar/calendar_input_controller.js +8 -0
- data/lib/ruby_ui/carousel/carousel.rb +44 -0
- data/lib/ruby_ui/carousel/carousel_content.rb +23 -0
- data/lib/ruby_ui/carousel/carousel_controller.js +60 -0
- data/lib/ruby_ui/carousel/carousel_item.rb +23 -0
- data/lib/ruby_ui/carousel/carousel_next.rb +48 -0
- data/lib/ruby_ui/carousel/carousel_previous.rb +49 -0
- data/lib/ruby_ui/chart/chart_controller.js +103 -0
- data/lib/ruby_ui/checkbox/checkbox_group_controller.js +21 -0
- data/lib/ruby_ui/clipboard/clipboard_controller.js +54 -0
- data/lib/ruby_ui/collapsible/collapsible_controller.js +47 -0
- data/lib/ruby_ui/combobox/combobox.rb +8 -6
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +25 -0
- data/lib/ruby_ui/combobox/combobox_controller.js +176 -0
- data/lib/ruby_ui/combobox/{combobox_empty.rb → combobox_empty_state.rb} +2 -2
- data/lib/ruby_ui/combobox/combobox_item.rb +9 -37
- data/lib/ruby_ui/combobox/combobox_list.rb +2 -11
- data/lib/ruby_ui/combobox/combobox_list_group.rb +20 -0
- data/lib/ruby_ui/combobox/combobox_popover.rb +30 -0
- data/lib/ruby_ui/combobox/combobox_radio.rb +26 -0
- data/lib/ruby_ui/combobox/combobox_search_input.rb +21 -24
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +25 -0
- data/lib/ruby_ui/combobox/combobox_trigger.rb +25 -20
- data/lib/ruby_ui/command/command_controller.js +136 -0
- data/lib/ruby_ui/context_menu/context_menu_controller.js +144 -0
- data/lib/ruby_ui/dialog/dialog_content.rb +2 -2
- data/lib/ruby_ui/dialog/dialog_controller.js +32 -0
- data/lib/ruby_ui/dialog/dialog_footer.rb +1 -1
- data/lib/ruby_ui/dialog/dialog_header.rb +1 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +120 -0
- data/lib/ruby_ui/form/form_field_controller.js +61 -0
- data/lib/ruby_ui/hover_card/hover_card_controller.js +144 -0
- data/lib/ruby_ui/masked_input/masked_input_controller.js +9 -0
- data/lib/ruby_ui/popover/popover_controller.js +107 -0
- data/lib/ruby_ui/progress/progress.rb +37 -0
- data/lib/ruby_ui/radio_button/radio_button.rb +4 -1
- data/lib/ruby_ui/select/select_content.rb +1 -1
- data/lib/ruby_ui/select/select_controller.js +171 -0
- data/lib/ruby_ui/select/select_item_controller.js +11 -0
- data/lib/ruby_ui/select/select_value.rb +1 -1
- data/lib/ruby_ui/separator/separator.rb +38 -0
- data/lib/ruby_ui/sheet/sheet_content.rb +1 -1
- data/lib/ruby_ui/sheet/sheet_content_controller.js +7 -0
- data/lib/ruby_ui/sheet/sheet_controller.js +9 -0
- data/lib/ruby_ui/{combobox/combobox_separator.rb → skeleton/skeleton.rb} +4 -2
- data/lib/ruby_ui/switch/switch.rb +24 -0
- data/lib/ruby_ui/tabs/tabs_controller.js +45 -0
- data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +30 -0
- data/lib/ruby_ui/tooltip/tooltip_controller.js +37 -0
- data/lib/ruby_ui.rb +1 -1
- metadata +57 -11
- data/lib/ruby_ui/combobox/combobox_content.rb +0 -31
- data/lib/ruby_ui/combobox/combobox_group.rb +0 -38
- data/lib/ruby_ui/combobox/combobox_input.rb +0 -22
- data/lib/ruby_ui/combobox/combobox_value.rb +0 -27
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class CarouselPrevious < Base
|
5
|
+
def view_template(&)
|
6
|
+
Button(**attrs) do
|
7
|
+
icon
|
8
|
+
span(class: "sr-only") { "Next slide" }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def default_attrs
|
15
|
+
{
|
16
|
+
variant: :outline,
|
17
|
+
icon: true,
|
18
|
+
class: [
|
19
|
+
"absolute h-8 w-8 rounded-full",
|
20
|
+
"group-[.is-horizontal]:-left-12 group-[.is-horizontal]:top-1/2 group-[.is-horizontal]:-translate-y-1/2",
|
21
|
+
"group-[.is-vertical]:-top-12 group-[.is-vertical]:left-1/2 group-[.is-vertical]:-translate-x-1/2 group-[.is-vertical]:rotate-90"
|
22
|
+
],
|
23
|
+
disabled: true,
|
24
|
+
data: {
|
25
|
+
action: "click->ruby-ui--carousel#scrollPrev",
|
26
|
+
ruby_ui__carousel_target: "prevButton"
|
27
|
+
}
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def icon
|
32
|
+
svg(
|
33
|
+
width: "24",
|
34
|
+
height: "24",
|
35
|
+
viewBox: "0 0 24 24",
|
36
|
+
fill: "none",
|
37
|
+
stroke: "currentColor",
|
38
|
+
stroke_width: "2",
|
39
|
+
stroke_linecap: "round",
|
40
|
+
stroke_linejoin: "round",
|
41
|
+
xmlns: "http://www.w3.org/2000/svg",
|
42
|
+
class: "w-4 h-4"
|
43
|
+
) do |s|
|
44
|
+
s.path(d: "m12 19-7-7 7-7")
|
45
|
+
s.path(d: "M19 12H5")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import Chart from 'chart.js/auto'
|
3
|
+
|
4
|
+
// Chart controller
|
5
|
+
export default class extends Controller {
|
6
|
+
static values = {
|
7
|
+
options: {
|
8
|
+
type: Object,
|
9
|
+
default: {},
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
// Function to initialize the chart when the controller is connected
|
14
|
+
connect() {
|
15
|
+
this.initDarkModeObserver()
|
16
|
+
this.initChart()
|
17
|
+
}
|
18
|
+
|
19
|
+
disconnect() {
|
20
|
+
this.darkModeObserver?.disconnect()
|
21
|
+
this.chart?.destroy()
|
22
|
+
}
|
23
|
+
|
24
|
+
// Function to initialize the chart
|
25
|
+
initChart() {
|
26
|
+
this.setColors()
|
27
|
+
const ctx = this.element.getContext('2d');
|
28
|
+
this.chart = new Chart(ctx, this.mergeOptionsWithDefaults());
|
29
|
+
}
|
30
|
+
|
31
|
+
setColors() {
|
32
|
+
this.setDefaultColorsForChart()
|
33
|
+
}
|
34
|
+
|
35
|
+
getThemeColor(name) {
|
36
|
+
const color = getComputedStyle(document.documentElement).getPropertyValue(`--${name}`)
|
37
|
+
const [hue, saturation, lightness] = color.split(' ')
|
38
|
+
return `hsl(${hue}, ${saturation}, ${lightness})`
|
39
|
+
}
|
40
|
+
|
41
|
+
defaultThemeColor() {
|
42
|
+
return {
|
43
|
+
backgroundColor: this.getThemeColor('background'),
|
44
|
+
hoverBackgroundColor: this.getThemeColor('accent'),
|
45
|
+
borderColor: this.getThemeColor('primary'),
|
46
|
+
borderWidth: 1,
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
// Function to set chart default colors
|
51
|
+
setDefaultColorsForChart() {
|
52
|
+
Chart.defaults.color = this.getThemeColor('muted-foreground') // font color
|
53
|
+
Chart.defaults.borderColor = this.getThemeColor('border') // border color
|
54
|
+
Chart.defaults.backgroundColor = this.getThemeColor('background') // background color
|
55
|
+
|
56
|
+
// tooltip colors
|
57
|
+
Chart.defaults.plugins.tooltip.backgroundColor = this.getThemeColor('background')
|
58
|
+
Chart.defaults.plugins.tooltip.borderColor = this.getThemeColor('border')
|
59
|
+
Chart.defaults.plugins.tooltip.titleColor = this.getThemeColor('foreground')
|
60
|
+
Chart.defaults.plugins.tooltip.bodyColor = this.getThemeColor('muted-foreground')
|
61
|
+
Chart.defaults.plugins.tooltip.borderWidth = 1
|
62
|
+
|
63
|
+
// legend
|
64
|
+
// options.plugins.legend.labels
|
65
|
+
Chart.defaults.plugins.legend.labels.boxWidth = 12
|
66
|
+
Chart.defaults.plugins.legend.labels.boxHeight = 12
|
67
|
+
Chart.defaults.plugins.legend.labels.borderWidth = 0
|
68
|
+
Chart.defaults.plugins.legend.labels.useBorderRadius = true
|
69
|
+
Chart.defaults.plugins.legend.labels.borderRadius = this.getThemeColor('radius')
|
70
|
+
}
|
71
|
+
|
72
|
+
// Function to refresh the chart
|
73
|
+
refreshChart() {
|
74
|
+
// Destroy the chart if it's a valid Chart.js instance
|
75
|
+
this.chart?.destroy()
|
76
|
+
// Reinitialize the chart
|
77
|
+
this.initChart()
|
78
|
+
}
|
79
|
+
|
80
|
+
// Function to initialize the dark mode observer
|
81
|
+
initDarkModeObserver() {
|
82
|
+
this.darkModeObserver = new MutationObserver(() => {
|
83
|
+
this.refreshChart()
|
84
|
+
})
|
85
|
+
this.darkModeObserver.observe(document.documentElement, { attributeFilter: ['class'] })
|
86
|
+
}
|
87
|
+
|
88
|
+
// Function to merge the options with the defaults
|
89
|
+
mergeOptionsWithDefaults() {
|
90
|
+
return {
|
91
|
+
...this.optionsValue,
|
92
|
+
data: {
|
93
|
+
...this.optionsValue.data,
|
94
|
+
datasets: this.optionsValue.data.datasets.map((dataset) => {
|
95
|
+
return {
|
96
|
+
...this.defaultThemeColor(),
|
97
|
+
...dataset,
|
98
|
+
}
|
99
|
+
})
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["checkbox"];
|
5
|
+
|
6
|
+
connect() {
|
7
|
+
this.#handleRequired();
|
8
|
+
}
|
9
|
+
|
10
|
+
onChange() {
|
11
|
+
this.#handleRequired();
|
12
|
+
}
|
13
|
+
|
14
|
+
#handleRequired() {
|
15
|
+
if (!this.element.hasAttribute("data-required")) return;
|
16
|
+
|
17
|
+
const checked = this.checkboxTargets.some(({ checked }) => checked);
|
18
|
+
|
19
|
+
this.checkboxTargets.forEach((checkbox) => (checkbox.required = !checked));
|
20
|
+
}
|
21
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { computePosition, flip, shift } from "@floating-ui/dom";
|
3
|
+
|
4
|
+
// Connects to data-controller="accordion"
|
5
|
+
export default class extends Controller {
|
6
|
+
static targets = ['trigger', 'source', 'successPopover', 'errorPopover']
|
7
|
+
static values = {
|
8
|
+
options: {
|
9
|
+
type: Object,
|
10
|
+
default: {},
|
11
|
+
},
|
12
|
+
}
|
13
|
+
|
14
|
+
copy() {
|
15
|
+
let sourceElement = this.sourceTarget.children[0];
|
16
|
+
if (!sourceElement) {
|
17
|
+
this.showErrorPopover();
|
18
|
+
return;
|
19
|
+
}
|
20
|
+
let textToCopy = sourceElement.tagName === 'INPUT' ? sourceElement.value : sourceElement.innerText;
|
21
|
+
navigator.clipboard.writeText(textToCopy).then(() => {
|
22
|
+
this.#showSuccessPopover();
|
23
|
+
}).catch(() => {
|
24
|
+
this.#showErrorPopover();
|
25
|
+
})
|
26
|
+
}
|
27
|
+
|
28
|
+
onClickOutside() {
|
29
|
+
if (!this.successPopoverTarget.classList.contains("hidden")) this.successPopoverTarget.classList.add("hidden");
|
30
|
+
if (!this.errorPopoverTarget.classList.contains("hidden")) this.errorPopoverTarget.classList.add("hidden");
|
31
|
+
}
|
32
|
+
|
33
|
+
#computeTooltip(popoverElement) {
|
34
|
+
computePosition(this.triggerTarget, popoverElement, {
|
35
|
+
placement: this.optionsValue.placement || "top",
|
36
|
+
middleware: [flip(), shift()],
|
37
|
+
}).then(({ x, y }) => {
|
38
|
+
Object.assign(popoverElement.style, {
|
39
|
+
left: `${x}px`,
|
40
|
+
top: `${y}px`,
|
41
|
+
});
|
42
|
+
});
|
43
|
+
}
|
44
|
+
|
45
|
+
#showSuccessPopover() {
|
46
|
+
this.#computeTooltip(this.successPopoverTarget);
|
47
|
+
this.successPopoverTarget.classList.remove("hidden");
|
48
|
+
}
|
49
|
+
|
50
|
+
#showErrorPopover() {
|
51
|
+
this.#computeTooltip(this.errorPopoverTarget);
|
52
|
+
this.errorPopoverTarget.classList.remove("hidden");
|
53
|
+
}
|
54
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
// Connects to data-controller="accordion"
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ['content']
|
6
|
+
static values = {
|
7
|
+
open: {
|
8
|
+
type: Boolean,
|
9
|
+
default: false,
|
10
|
+
},
|
11
|
+
}
|
12
|
+
|
13
|
+
connect() {
|
14
|
+
// Set the initial state of the accordion
|
15
|
+
this.openValue ? this.open() : this.close()
|
16
|
+
}
|
17
|
+
|
18
|
+
// Toggle the 'open' value
|
19
|
+
toggle() {
|
20
|
+
this.openValue = !this.openValue
|
21
|
+
}
|
22
|
+
|
23
|
+
// Handle changes in the 'open' value
|
24
|
+
openValueChanged(isOpen, wasOpen) {
|
25
|
+
if (isOpen) {
|
26
|
+
this.open()
|
27
|
+
} else {
|
28
|
+
this.close()
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
// Open the accordion content
|
33
|
+
open() {
|
34
|
+
if (this.hasContentTarget) {
|
35
|
+
this.contentTarget.classList.remove('hidden')
|
36
|
+
this.openValue = true
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
// Close the accordion content
|
41
|
+
close() {
|
42
|
+
if (this.hasContentTarget) {
|
43
|
+
this.contentTarget.classList.add('hidden')
|
44
|
+
this.openValue = false
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
module RubyUI
|
4
4
|
class Combobox < Base
|
5
|
+
def initialize(term: "items", **)
|
6
|
+
@term = term
|
7
|
+
super(**)
|
8
|
+
end
|
9
|
+
|
5
10
|
def view_template(&)
|
6
11
|
div(**attrs, &)
|
7
12
|
end
|
@@ -10,14 +15,11 @@ module RubyUI
|
|
10
15
|
|
11
16
|
def default_attrs
|
12
17
|
{
|
18
|
+
role: "combobox",
|
13
19
|
data: {
|
14
20
|
controller: "ruby-ui--combobox",
|
15
|
-
|
16
|
-
|
17
|
-
ruby_ui__combobox_ruby_ui__combobox_content_outlet: ".combobox-content",
|
18
|
-
ruby_ui__combobox_ruby_ui__combobox_item_outlet: ".combobox-item"
|
19
|
-
},
|
20
|
-
class: "group/combobox w-full relative"
|
21
|
+
ruby_ui__combobox_term_value: @term.to_s
|
22
|
+
}
|
21
23
|
}
|
22
24
|
end
|
23
25
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class ComboboxCheckbox < Base
|
5
|
+
def view_template
|
6
|
+
input(type: "checkbox", **attrs)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def default_attrs
|
12
|
+
{
|
13
|
+
class: [
|
14
|
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
|
15
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
16
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
17
|
+
],
|
18
|
+
data: {
|
19
|
+
ruby_ui__combobox_target: "input",
|
20
|
+
action: "ruby-ui--combobox#inputChanged"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom";
|
3
|
+
|
4
|
+
// Connects to data-controller="ruby-ui--combobox"
|
5
|
+
export default class extends Controller {
|
6
|
+
static values = {
|
7
|
+
term: String
|
8
|
+
}
|
9
|
+
|
10
|
+
static targets = [
|
11
|
+
"input",
|
12
|
+
"toggleAll",
|
13
|
+
"popover",
|
14
|
+
"item",
|
15
|
+
"emptyState",
|
16
|
+
"searchInput",
|
17
|
+
"trigger",
|
18
|
+
"triggerContent"
|
19
|
+
]
|
20
|
+
|
21
|
+
selectedItemIndex = null
|
22
|
+
|
23
|
+
connect() {
|
24
|
+
this.updateTriggerContent()
|
25
|
+
}
|
26
|
+
|
27
|
+
disconnect() {
|
28
|
+
if (this.cleanup) { this.cleanup() }
|
29
|
+
}
|
30
|
+
|
31
|
+
inputChanged(e) {
|
32
|
+
this.updateTriggerContent()
|
33
|
+
|
34
|
+
if (e.target.type == "radio") {
|
35
|
+
this.closePopover()
|
36
|
+
}
|
37
|
+
|
38
|
+
if (this.hasToggleAllTarget && !e.target.checked) {
|
39
|
+
this.toggleAllTarget.checked = false
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
inputContent(input) {
|
44
|
+
return input.dataset.text || input.parentElement.textContent
|
45
|
+
}
|
46
|
+
|
47
|
+
toggleAllItems() {
|
48
|
+
const isChecked = this.toggleAllTarget.checked
|
49
|
+
this.inputTargets.forEach(input => input.checked = isChecked)
|
50
|
+
this.updateTriggerContent()
|
51
|
+
}
|
52
|
+
|
53
|
+
updateTriggerContent() {
|
54
|
+
const checkedInputs = this.inputTargets.filter(input => input.checked)
|
55
|
+
|
56
|
+
if (checkedInputs.length == 0) {
|
57
|
+
this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder
|
58
|
+
} else if (checkedInputs.length === 1) {
|
59
|
+
this.triggerContentTarget.innerText = this.inputContent(checkedInputs[0])
|
60
|
+
} else {
|
61
|
+
this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
openPopover(event) {
|
66
|
+
event.preventDefault()
|
67
|
+
|
68
|
+
this.updatePopoverPosition()
|
69
|
+
this.updatePopoverWidth()
|
70
|
+
this.triggerTarget.ariaExpanded = "true"
|
71
|
+
this.selectedItemIndex = null
|
72
|
+
this.itemTargets.forEach(item => item.ariaCurrent = "false")
|
73
|
+
this.popoverTarget.showPopover()
|
74
|
+
}
|
75
|
+
|
76
|
+
closePopover() {
|
77
|
+
this.triggerTarget.ariaExpanded = "false"
|
78
|
+
this.popoverTarget.hidePopover()
|
79
|
+
}
|
80
|
+
|
81
|
+
filterItems(e) {
|
82
|
+
if (["ArrowDown", "ArrowUp", "Tab", "Enter"].includes(e.key)) {
|
83
|
+
return
|
84
|
+
}
|
85
|
+
|
86
|
+
const filterTerm = this.searchInputTarget.value.toLowerCase()
|
87
|
+
|
88
|
+
if (this.hasToggleAllTarget) {
|
89
|
+
if (filterTerm) this.toggleAllTarget.parentElement.classList.add("hidden")
|
90
|
+
else this.toggleAllTarget.parentElement.classList.remove("hidden")
|
91
|
+
}
|
92
|
+
|
93
|
+
let resultCount = 0
|
94
|
+
|
95
|
+
this.selectedItemIndex = null
|
96
|
+
|
97
|
+
this.inputTargets.forEach((input) => {
|
98
|
+
const text = this.inputContent(input).toLowerCase()
|
99
|
+
|
100
|
+
if (text.indexOf(filterTerm) > -1) {
|
101
|
+
input.parentElement.classList.remove("hidden")
|
102
|
+
resultCount++
|
103
|
+
} else {
|
104
|
+
input.parentElement.classList.add("hidden")
|
105
|
+
}
|
106
|
+
})
|
107
|
+
|
108
|
+
this.emptyStateTarget.classList.toggle("hidden", resultCount !== 0)
|
109
|
+
}
|
110
|
+
|
111
|
+
keyDownPressed() {
|
112
|
+
if (this.selectedItemIndex !== null) {
|
113
|
+
this.selectedItemIndex++
|
114
|
+
} else {
|
115
|
+
this.selectedItemIndex = 0
|
116
|
+
}
|
117
|
+
|
118
|
+
this.focusSelectedInput()
|
119
|
+
}
|
120
|
+
|
121
|
+
keyUpPressed() {
|
122
|
+
if (this.selectedItemIndex !== null) {
|
123
|
+
this.selectedItemIndex--
|
124
|
+
} else {
|
125
|
+
this.selectedItemIndex = -1
|
126
|
+
}
|
127
|
+
|
128
|
+
this.focusSelectedInput()
|
129
|
+
}
|
130
|
+
|
131
|
+
focusSelectedInput() {
|
132
|
+
const visibleInputs = this.inputTargets.filter(input => !input.parentElement.classList.contains("hidden"))
|
133
|
+
|
134
|
+
this.wrapSelectedInputIndex(visibleInputs.length)
|
135
|
+
|
136
|
+
visibleInputs.forEach((input, index) => {
|
137
|
+
if (index == this.selectedItemIndex) {
|
138
|
+
input.parentElement.ariaCurrent = "true"
|
139
|
+
input.parentElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
|
140
|
+
} else {
|
141
|
+
input.parentElement.ariaCurrent = "false"
|
142
|
+
}
|
143
|
+
})
|
144
|
+
}
|
145
|
+
|
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
|
+
wrapSelectedInputIndex(length) {
|
156
|
+
this.selectedItemIndex = ((this.selectedItemIndex % length) + length) % length
|
157
|
+
}
|
158
|
+
|
159
|
+
updatePopoverPosition() {
|
160
|
+
this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => {
|
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
|
+
}
|
172
|
+
|
173
|
+
updatePopoverWidth() {
|
174
|
+
this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px`
|
175
|
+
}
|
176
|
+
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RubyUI
|
4
|
-
class
|
4
|
+
class ComboboxEmptyState < Base
|
5
5
|
def view_template(&)
|
6
6
|
div(**attrs, &)
|
7
7
|
end
|
@@ -13,7 +13,7 @@ module RubyUI
|
|
13
13
|
role: "presentation",
|
14
14
|
class: "hidden py-6 text-center text-sm",
|
15
15
|
data: {
|
16
|
-
|
16
|
+
ruby_ui__combobox_target: "emptyState"
|
17
17
|
}
|
18
18
|
}
|
19
19
|
end
|
@@ -2,51 +2,23 @@
|
|
2
2
|
|
3
3
|
module RubyUI
|
4
4
|
class ComboboxItem < Base
|
5
|
-
def
|
6
|
-
|
7
|
-
super(**attrs)
|
8
|
-
end
|
9
|
-
|
10
|
-
def view_template(&block)
|
11
|
-
div(**attrs) do
|
12
|
-
div(class: "invisible group-aria-selected:visible") { icon }
|
13
|
-
block.call
|
14
|
-
end
|
5
|
+
def view_template(&)
|
6
|
+
label(**attrs, &)
|
15
7
|
end
|
16
8
|
|
17
9
|
private
|
18
10
|
|
19
|
-
def icon
|
20
|
-
svg(
|
21
|
-
xmlns: "http://www.w3.org/2000/svg",
|
22
|
-
viewbox: "0 0 24 24",
|
23
|
-
fill: "none",
|
24
|
-
stroke: "currentColor",
|
25
|
-
class: "mr-2 h-4 w-4",
|
26
|
-
stroke_width: "2",
|
27
|
-
stroke_linecap: "round",
|
28
|
-
stroke_linejoin: "round"
|
29
|
-
) do |s|
|
30
|
-
s.path(
|
31
|
-
d: "M20 6 9 17l-5-5"
|
32
|
-
)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
11
|
def default_attrs
|
37
12
|
{
|
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
|
+
],
|
38
18
|
role: "option",
|
39
|
-
tabindex: "0",
|
40
|
-
class:
|
41
|
-
"combobox-item group relative flex cursor-pointer select-none items-center gap-x-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground aria-[current]:bg-accent aria-[current]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
42
19
|
data: {
|
43
|
-
|
44
|
-
|
45
|
-
ruby_ui__combobox_content_target: "item",
|
46
|
-
controller: "ruby-ui--combobox-item",
|
47
|
-
action: "click->ruby-ui--combobox#onItemSelected"
|
48
|
-
},
|
49
|
-
aria_selected: "false"
|
20
|
+
ruby_ui__combobox_target: "item"
|
21
|
+
}
|
50
22
|
}
|
51
23
|
end
|
52
24
|
end
|
@@ -2,11 +2,6 @@
|
|
2
2
|
|
3
3
|
module RubyUI
|
4
4
|
class ComboboxList < Base
|
5
|
-
def initialize(**attrs)
|
6
|
-
@id = "list#{SecureRandom.hex(4)}"
|
7
|
-
super
|
8
|
-
end
|
9
|
-
|
10
5
|
def view_template(&)
|
11
6
|
div(**attrs, &)
|
12
7
|
end
|
@@ -15,12 +10,8 @@ module RubyUI
|
|
15
10
|
|
16
11
|
def default_attrs
|
17
12
|
{
|
18
|
-
|
19
|
-
|
20
|
-
ruby_ui__combobox_target: "list"
|
21
|
-
},
|
22
|
-
role: "listbox",
|
23
|
-
tabindex: "-1"
|
13
|
+
class: "flex flex-col gap-1 p-1 max-h-72 overflow-y-auto text-foreground",
|
14
|
+
role: "listbox"
|
24
15
|
}
|
25
16
|
end
|
26
17
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class ComboboxListGroup < Base
|
5
|
+
LABEL_CLASSES = "before:content-[attr(label)] before:px-2 before:py-1.5 before:text-xs before:font-medium before:text-muted-foreground before:not-italic"
|
6
|
+
|
7
|
+
def view_template(&)
|
8
|
+
div(**attrs, &)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def default_attrs
|
14
|
+
{
|
15
|
+
class: ["hidden has-[label:not(.hidden)]:flex flex-col py-1 gap-1 border-b", LABEL_CLASSES],
|
16
|
+
role: "group"
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class ComboboxPopover < Base
|
5
|
+
def view_template(&)
|
6
|
+
div(**attrs, &)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def default_attrs
|
12
|
+
{
|
13
|
+
class: "inset-auto m-0 absolute border bg-background shadow-lg rounded-lg",
|
14
|
+
role: "popover",
|
15
|
+
autofocus: true,
|
16
|
+
popover: true,
|
17
|
+
data: {
|
18
|
+
ruby_ui__combobox_target: "popover",
|
19
|
+
action: %w[
|
20
|
+
keydown.down->ruby-ui--combobox#keyDownPressed
|
21
|
+
keydown.up->ruby-ui--combobox#keyUpPressed
|
22
|
+
keydown.enter->ruby-ui--combobox#keyEnterPressed
|
23
|
+
keydown.esc->ruby-ui--combobox#closeDialog:prevent
|
24
|
+
resize@window->ruby-ui--combobox#updatePopoverWidth
|
25
|
+
]
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class ComboboxRadio < Base
|
5
|
+
def view_template
|
6
|
+
input(type: "radio", **attrs)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def default_attrs
|
12
|
+
{
|
13
|
+
class: "aspect-square h-4 w-4 rounded-full border border-primary accent-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
14
|
+
data: {
|
15
|
+
ruby_ui__combobox_target: "input",
|
16
|
+
ruby_ui__form_field_target: "input",
|
17
|
+
action: %w[
|
18
|
+
ruby-ui--combobox#inputChanged
|
19
|
+
input->ruby-ui--form-field#onInput
|
20
|
+
invalid->ruby-ui--form-field#onInvalid
|
21
|
+
]
|
22
|
+
}
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|