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
@@ -2,21 +2,37 @@
|
|
2
2
|
|
3
3
|
module RubyUI
|
4
4
|
class ComboboxSearchInput < Base
|
5
|
-
def initialize(placeholder:, **
|
5
|
+
def initialize(placeholder:, **)
|
6
6
|
@placeholder = placeholder
|
7
|
-
super(**
|
7
|
+
super(**)
|
8
8
|
end
|
9
9
|
|
10
10
|
def view_template
|
11
|
-
|
12
|
-
|
11
|
+
div class: "flex text-muted-foreground items-center border-b px-3" do
|
12
|
+
icon
|
13
13
|
input(**attrs)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
18
18
|
|
19
|
-
def
|
19
|
+
def default_attrs
|
20
|
+
{
|
21
|
+
type: "search",
|
22
|
+
class: "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none border-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
23
|
+
role: "searchbox",
|
24
|
+
placeholder: @placeholder,
|
25
|
+
data: {
|
26
|
+
ruby_ui__combobox_target: "searchInput",
|
27
|
+
action: "keyup->ruby-ui--combobox#filterItems search->ruby-ui--combobox#filterItems"
|
28
|
+
},
|
29
|
+
autocomplete: "off",
|
30
|
+
autocorrect: "off",
|
31
|
+
spellcheck: "false"
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def icon
|
20
36
|
svg(
|
21
37
|
xmlns: "http://www.w3.org/2000/svg",
|
22
38
|
viewbox: "0 0 24 24",
|
@@ -33,24 +49,5 @@ module RubyUI
|
|
33
49
|
)
|
34
50
|
end
|
35
51
|
end
|
36
|
-
|
37
|
-
def input_container(&)
|
38
|
-
div(class: "flex items-center border-b px-3", &)
|
39
|
-
end
|
40
|
-
|
41
|
-
def default_attrs
|
42
|
-
{
|
43
|
-
class:
|
44
|
-
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
45
|
-
placeholder: @placeholder,
|
46
|
-
data: {
|
47
|
-
action: "input->ruby-ui--combobox#onSearchInput",
|
48
|
-
ruby_ui__combobox_target: "search"
|
49
|
-
},
|
50
|
-
autocomplete: "off",
|
51
|
-
autocorrect: "off",
|
52
|
-
spellcheck: false
|
53
|
-
}
|
54
|
-
end
|
55
52
|
end
|
56
53
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyUI
|
4
|
+
class ComboboxToggleAllCheckbox < 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: "toggleAll",
|
20
|
+
action: "change->ruby-ui--combobox#toggleAllItems"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -2,15 +2,38 @@
|
|
2
2
|
|
3
3
|
module RubyUI
|
4
4
|
class ComboboxTrigger < Base
|
5
|
-
def
|
5
|
+
def initialize(placeholder: "", **)
|
6
|
+
@placeholder = placeholder
|
7
|
+
super(**)
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_template
|
6
11
|
button(**attrs) do
|
7
|
-
|
12
|
+
span(class: "truncate", data: {ruby_ui__combobox_target: "triggerContent"}) do
|
13
|
+
@placeholder
|
14
|
+
end
|
8
15
|
icon
|
9
16
|
end
|
10
17
|
end
|
11
18
|
|
12
19
|
private
|
13
20
|
|
21
|
+
def default_attrs
|
22
|
+
{
|
23
|
+
type: "button",
|
24
|
+
class: "flex h-full w-full items-center whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 justify-between",
|
25
|
+
data: {
|
26
|
+
placeholder: @placeholder,
|
27
|
+
ruby_ui__combobox_target: "trigger",
|
28
|
+
action: "ruby-ui--combobox#openPopover"
|
29
|
+
},
|
30
|
+
aria: {
|
31
|
+
haspopup: "listbox",
|
32
|
+
expanded: "false"
|
33
|
+
}
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
14
37
|
def icon
|
15
38
|
svg(
|
16
39
|
xmlns: "http://www.w3.org/2000/svg",
|
@@ -30,23 +53,5 @@ module RubyUI
|
|
30
53
|
)
|
31
54
|
end
|
32
55
|
end
|
33
|
-
|
34
|
-
def default_attrs
|
35
|
-
{
|
36
|
-
data: {
|
37
|
-
action: "ruby-ui--combobox#onTriggerClick",
|
38
|
-
ruby_ui__combobox_target: "trigger"
|
39
|
-
},
|
40
|
-
type: "button",
|
41
|
-
role: "combobox",
|
42
|
-
aria: {
|
43
|
-
expanded: "false",
|
44
|
-
haspopup: "listbox",
|
45
|
-
autocomplete: "none",
|
46
|
-
activedescendant: true
|
47
|
-
},
|
48
|
-
class: "flex h-full w-full items-center whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 w-[200px] justify-between"
|
49
|
-
}
|
50
|
-
end
|
51
56
|
end
|
52
57
|
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import Fuse from "fuse.js";
|
3
|
+
|
4
|
+
// Connects to data-controller="ruby-ui--command"
|
5
|
+
export default class extends Controller {
|
6
|
+
static targets = ["input", "group", "item", "empty", "content"];
|
7
|
+
|
8
|
+
static values = {
|
9
|
+
open: {
|
10
|
+
type: Boolean,
|
11
|
+
default: false,
|
12
|
+
},
|
13
|
+
};
|
14
|
+
|
15
|
+
connect() {
|
16
|
+
this.inputTarget.focus();
|
17
|
+
this.searchIndex = this.buildSearchIndex();
|
18
|
+
this.toggleVisibility(this.emptyTargets, false);
|
19
|
+
this.selectedIndex = -1;
|
20
|
+
|
21
|
+
if (this.openValue) {
|
22
|
+
this.open();
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
open(e) {
|
27
|
+
e.preventDefault();
|
28
|
+
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML);
|
29
|
+
// prevent scroll on body
|
30
|
+
document.body.classList.add("overflow-hidden");
|
31
|
+
}
|
32
|
+
|
33
|
+
dismiss() {
|
34
|
+
// allow scroll on body
|
35
|
+
document.body.classList.remove("overflow-hidden");
|
36
|
+
// remove the element
|
37
|
+
console.log("this.element", this.element);
|
38
|
+
this.element.remove();
|
39
|
+
}
|
40
|
+
|
41
|
+
filter(e) {
|
42
|
+
// Deselect any previously selected item
|
43
|
+
this.deselectAll();
|
44
|
+
|
45
|
+
const query = e.target.value.toLowerCase();
|
46
|
+
if (query.length === 0) {
|
47
|
+
this.resetVisibility();
|
48
|
+
return;
|
49
|
+
}
|
50
|
+
|
51
|
+
this.toggleVisibility(this.itemTargets, false);
|
52
|
+
|
53
|
+
const results = this.searchIndex.search(query);
|
54
|
+
results.forEach((result) =>
|
55
|
+
this.toggleVisibility([result.item.element], true),
|
56
|
+
);
|
57
|
+
|
58
|
+
this.toggleVisibility(this.emptyTargets, results.length === 0);
|
59
|
+
this.updateGroupVisibility();
|
60
|
+
}
|
61
|
+
|
62
|
+
toggleVisibility(elements, isVisible) {
|
63
|
+
elements.forEach((el) => el.classList.toggle("hidden", !isVisible));
|
64
|
+
}
|
65
|
+
|
66
|
+
updateGroupVisibility() {
|
67
|
+
this.groupTargets.forEach((group) => {
|
68
|
+
const hasVisibleItems =
|
69
|
+
group.querySelectorAll(
|
70
|
+
"[data-ruby-ui--command-target='item']:not(.hidden)",
|
71
|
+
).length > 0;
|
72
|
+
this.toggleVisibility([group], hasVisibleItems);
|
73
|
+
});
|
74
|
+
}
|
75
|
+
|
76
|
+
resetVisibility() {
|
77
|
+
this.toggleVisibility(this.itemTargets, true);
|
78
|
+
this.toggleVisibility(this.groupTargets, true);
|
79
|
+
this.toggleVisibility(this.emptyTargets, false);
|
80
|
+
}
|
81
|
+
|
82
|
+
buildSearchIndex() {
|
83
|
+
const options = {
|
84
|
+
keys: ["value"],
|
85
|
+
threshold: 0.2,
|
86
|
+
includeMatches: true,
|
87
|
+
};
|
88
|
+
const items = this.itemTargets.map((el) => ({
|
89
|
+
value: el.dataset.value,
|
90
|
+
element: el,
|
91
|
+
}));
|
92
|
+
return new Fuse(items, options);
|
93
|
+
}
|
94
|
+
|
95
|
+
handleKeydown(e) {
|
96
|
+
const visibleItems = this.itemTargets.filter(
|
97
|
+
(item) => !item.classList.contains("hidden"),
|
98
|
+
);
|
99
|
+
if (e.key === "ArrowDown") {
|
100
|
+
e.preventDefault();
|
101
|
+
this.updateSelectedItem(visibleItems, 1);
|
102
|
+
} else if (e.key === "ArrowUp") {
|
103
|
+
e.preventDefault();
|
104
|
+
this.updateSelectedItem(visibleItems, -1);
|
105
|
+
} else if (e.key === "Enter" && this.selectedIndex !== -1) {
|
106
|
+
e.preventDefault();
|
107
|
+
visibleItems[this.selectedIndex].click();
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
updateSelectedItem(visibleItems, direction) {
|
112
|
+
if (this.selectedIndex >= 0) {
|
113
|
+
this.toggleAriaSelected(visibleItems[this.selectedIndex], false);
|
114
|
+
}
|
115
|
+
|
116
|
+
this.selectedIndex += direction;
|
117
|
+
|
118
|
+
// Ensure the selected index is within the bounds of the visible items
|
119
|
+
if (this.selectedIndex < 0) {
|
120
|
+
this.selectedIndex = visibleItems.length - 1;
|
121
|
+
} else if (this.selectedIndex >= visibleItems.length) {
|
122
|
+
this.selectedIndex = 0;
|
123
|
+
}
|
124
|
+
|
125
|
+
this.toggleAriaSelected(visibleItems[this.selectedIndex], true);
|
126
|
+
}
|
127
|
+
|
128
|
+
toggleAriaSelected(element, isSelected) {
|
129
|
+
element.setAttribute("aria-selected", isSelected.toString());
|
130
|
+
}
|
131
|
+
|
132
|
+
deselectAll() {
|
133
|
+
this.itemTargets.forEach((item) => this.toggleAriaSelected(item, false));
|
134
|
+
this.selectedIndex = -1;
|
135
|
+
}
|
136
|
+
}
|
@@ -0,0 +1,144 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import tippy from "tippy.js";
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["trigger", "content", "menuItem"];
|
6
|
+
static values = {
|
7
|
+
options: {
|
8
|
+
type: Object,
|
9
|
+
default: {},
|
10
|
+
},
|
11
|
+
// make content width of the trigger element (true/false)
|
12
|
+
matchWidth: {
|
13
|
+
type: Boolean,
|
14
|
+
default: false,
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
connect() {
|
19
|
+
this.boundHandleKeydown = this.handleKeydown.bind(this); // Bind the function so we can remove it later
|
20
|
+
this.initializeTippy();
|
21
|
+
this.selectedIndex = -1;
|
22
|
+
}
|
23
|
+
|
24
|
+
disconnect() {
|
25
|
+
this.destroyTippy();
|
26
|
+
}
|
27
|
+
|
28
|
+
initializeTippy() {
|
29
|
+
const defaultOptions = {
|
30
|
+
content: this.contentTarget.innerHTML,
|
31
|
+
allowHTML: true,
|
32
|
+
interactive: true,
|
33
|
+
onShow: (instance) => {
|
34
|
+
this.matchWidthValue && this.setContentWidth(instance); // ensure content width matches trigger width
|
35
|
+
this.addEventListeners();
|
36
|
+
},
|
37
|
+
onHide: () => {
|
38
|
+
this.removeEventListeners();
|
39
|
+
this.deselectAll();
|
40
|
+
},
|
41
|
+
popperOptions: {
|
42
|
+
modifiers: [
|
43
|
+
{
|
44
|
+
name: "offset",
|
45
|
+
options: {
|
46
|
+
offset: [0, 4]
|
47
|
+
},
|
48
|
+
},
|
49
|
+
],
|
50
|
+
}
|
51
|
+
};
|
52
|
+
|
53
|
+
const mergedOptions = { ...this.optionsValue, ...defaultOptions };
|
54
|
+
this.tippy = tippy(this.triggerTarget, mergedOptions);
|
55
|
+
}
|
56
|
+
|
57
|
+
destroyTippy() {
|
58
|
+
if (this.tippy) {
|
59
|
+
this.tippy.destroy();
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
setContentWidth(instance) {
|
64
|
+
// box-sizing: border-box
|
65
|
+
const content = instance.popper.querySelector('.tippy-content');
|
66
|
+
if (content) {
|
67
|
+
content.style.width = `${instance.reference.offsetWidth}px`;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
handleContextMenu(event) {
|
72
|
+
event.preventDefault();
|
73
|
+
this.open();
|
74
|
+
}
|
75
|
+
|
76
|
+
open() {
|
77
|
+
this.tippy.show();
|
78
|
+
}
|
79
|
+
|
80
|
+
close() {
|
81
|
+
this.tippy.hide();
|
82
|
+
}
|
83
|
+
|
84
|
+
handleKeydown(e) {
|
85
|
+
// return if no menu items (one line fix for)
|
86
|
+
if (this.menuItemTargets.length === 0) { return; }
|
87
|
+
|
88
|
+
if (e.key === 'ArrowDown') {
|
89
|
+
e.preventDefault();
|
90
|
+
this.updateSelectedItem(1);
|
91
|
+
} else if (e.key === 'ArrowUp') {
|
92
|
+
e.preventDefault();
|
93
|
+
this.updateSelectedItem(-1);
|
94
|
+
} else if (e.key === 'Enter' && this.selectedIndex !== -1) {
|
95
|
+
e.preventDefault();
|
96
|
+
this.menuItemTargets[this.selectedIndex].click();
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
updateSelectedItem(direction) {
|
101
|
+
// Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index
|
102
|
+
this.menuItemTargets.forEach((item, index) => {
|
103
|
+
if (item.getAttribute('aria-selected') === 'true') {
|
104
|
+
this.selectedIndex = index;
|
105
|
+
}
|
106
|
+
});
|
107
|
+
|
108
|
+
if (this.selectedIndex >= 0) {
|
109
|
+
this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], false);
|
110
|
+
}
|
111
|
+
|
112
|
+
this.selectedIndex += direction;
|
113
|
+
|
114
|
+
if (this.selectedIndex < 0) {
|
115
|
+
this.selectedIndex = this.menuItemTargets.length - 1;
|
116
|
+
} else if (this.selectedIndex >= this.menuItemTargets.length) {
|
117
|
+
this.selectedIndex = 0;
|
118
|
+
}
|
119
|
+
|
120
|
+
this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], true);
|
121
|
+
}
|
122
|
+
|
123
|
+
toggleAriaSelected(element, isSelected) {
|
124
|
+
// Add or remove attribute
|
125
|
+
if (isSelected) {
|
126
|
+
element.setAttribute('aria-selected', 'true');
|
127
|
+
} else {
|
128
|
+
element.removeAttribute('aria-selected');
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
deselectAll() {
|
133
|
+
this.menuItemTargets.forEach(item => this.toggleAriaSelected(item, false));
|
134
|
+
this.selectedIndex = -1;
|
135
|
+
}
|
136
|
+
|
137
|
+
addEventListeners() {
|
138
|
+
document.addEventListener('keydown', this.boundHandleKeydown);
|
139
|
+
}
|
140
|
+
|
141
|
+
removeEventListeners() {
|
142
|
+
document.removeEventListener('keydown', this.boundHandleKeydown);
|
143
|
+
}
|
144
|
+
}
|
@@ -34,7 +34,7 @@ module RubyUI
|
|
34
34
|
{
|
35
35
|
data_state: "open",
|
36
36
|
class: [
|
37
|
-
"fixed pointer-events-auto left-[50%] top-[50%] z-50
|
37
|
+
"fixed flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
|
38
38
|
SIZES[@size]
|
39
39
|
]
|
40
40
|
}
|
@@ -43,7 +43,7 @@ module RubyUI
|
|
43
43
|
def close_button
|
44
44
|
button(
|
45
45
|
type: "button",
|
46
|
-
class: "absolute
|
46
|
+
class: "absolute end-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
47
47
|
data_action: "click->ruby-ui--dialog#dismiss"
|
48
48
|
) do
|
49
49
|
svg(
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
// Connects to data-controller="dialog"
|
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
|
+
if (this.openValue) {
|
15
|
+
this.open()
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
open(e) {
|
20
|
+
e.preventDefault()
|
21
|
+
document.body.insertAdjacentHTML('beforeend', this.contentTarget.innerHTML)
|
22
|
+
// prevent scroll on body
|
23
|
+
document.body.classList.add('overflow-hidden')
|
24
|
+
}
|
25
|
+
|
26
|
+
dismiss() {
|
27
|
+
// allow scroll on body
|
28
|
+
document.body.classList.remove('overflow-hidden')
|
29
|
+
// remove the element
|
30
|
+
this.element.remove()
|
31
|
+
}
|
32
|
+
}
|
@@ -10,7 +10,7 @@ module RubyUI
|
|
10
10
|
|
11
11
|
def default_attrs
|
12
12
|
{
|
13
|
-
class: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-y-2 sm:gap-y-0"
|
13
|
+
class: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-y-2 sm:gap-y-0 rtl:space-x-reverse"
|
14
14
|
}
|
15
15
|
end
|
16
16
|
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
import { computePosition, flip, shift, offset } from "@floating-ui/dom";
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["trigger", "content", "menuItem"];
|
6
|
+
static values = {
|
7
|
+
open: {
|
8
|
+
type: Boolean,
|
9
|
+
default: false,
|
10
|
+
},
|
11
|
+
options: {
|
12
|
+
type: Object,
|
13
|
+
default: {},
|
14
|
+
},
|
15
|
+
}
|
16
|
+
|
17
|
+
connect() {
|
18
|
+
this.boundHandleKeydown = this.#handleKeydown.bind(this); // Bind the function so we can remove it later
|
19
|
+
this.selectedIndex = -1;
|
20
|
+
}
|
21
|
+
|
22
|
+
#computeTooltip() {
|
23
|
+
computePosition(this.triggerTarget, this.contentTarget, {
|
24
|
+
placement: this.optionsValue.placement || "top",
|
25
|
+
middleware: [flip(), shift(), offset(8)],
|
26
|
+
}).then(({ x, y }) => {
|
27
|
+
Object.assign(this.contentTarget.style, {
|
28
|
+
left: `${x}px`,
|
29
|
+
top: `${y}px`,
|
30
|
+
});
|
31
|
+
});
|
32
|
+
}
|
33
|
+
|
34
|
+
onClickOutside(event) {
|
35
|
+
if (!this.openValue) return;
|
36
|
+
if (this.element.contains(event.target)) return;
|
37
|
+
|
38
|
+
event.preventDefault();
|
39
|
+
this.close();
|
40
|
+
}
|
41
|
+
|
42
|
+
toggle() {
|
43
|
+
this.contentTarget.classList.contains("hidden") ? this.#open() : this.close();
|
44
|
+
}
|
45
|
+
|
46
|
+
#open() {
|
47
|
+
this.openValue = true;
|
48
|
+
this.#deselectAll();
|
49
|
+
this.#addEventListeners();
|
50
|
+
this.#computeTooltip()
|
51
|
+
this.contentTarget.classList.remove("hidden");
|
52
|
+
}
|
53
|
+
|
54
|
+
close() {
|
55
|
+
this.openValue = false;
|
56
|
+
this.#removeEventListeners();
|
57
|
+
this.contentTarget.classList.add("hidden");
|
58
|
+
}
|
59
|
+
|
60
|
+
#handleKeydown(e) {
|
61
|
+
// return if no menu items (one line fix for)
|
62
|
+
if (this.menuItemTargets.length === 0) { return; }
|
63
|
+
|
64
|
+
if (e.key === 'ArrowDown') {
|
65
|
+
e.preventDefault();
|
66
|
+
this.#updateSelectedItem(1);
|
67
|
+
} else if (e.key === 'ArrowUp') {
|
68
|
+
e.preventDefault();
|
69
|
+
this.#updateSelectedItem(-1);
|
70
|
+
} else if (e.key === 'Enter' && this.selectedIndex !== -1) {
|
71
|
+
e.preventDefault();
|
72
|
+
this.menuItemTargets[this.selectedIndex].click();
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
#updateSelectedItem(direction) {
|
77
|
+
// Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index
|
78
|
+
this.menuItemTargets.forEach((item, index) => {
|
79
|
+
if (item.getAttribute('aria-selected') === 'true') {
|
80
|
+
this.selectedIndex = index;
|
81
|
+
}
|
82
|
+
});
|
83
|
+
|
84
|
+
if (this.selectedIndex >= 0) {
|
85
|
+
this.#toggleAriaSelected(this.menuItemTargets[this.selectedIndex], false);
|
86
|
+
}
|
87
|
+
|
88
|
+
this.selectedIndex += direction;
|
89
|
+
|
90
|
+
if (this.selectedIndex < 0) {
|
91
|
+
this.selectedIndex = this.menuItemTargets.length - 1;
|
92
|
+
} else if (this.selectedIndex >= this.menuItemTargets.length) {
|
93
|
+
this.selectedIndex = 0;
|
94
|
+
}
|
95
|
+
|
96
|
+
this.#toggleAriaSelected(this.menuItemTargets[this.selectedIndex], true);
|
97
|
+
}
|
98
|
+
|
99
|
+
#toggleAriaSelected(element, isSelected) {
|
100
|
+
// Add or remove attribute
|
101
|
+
if (isSelected) {
|
102
|
+
element.setAttribute('aria-selected', 'true');
|
103
|
+
} else {
|
104
|
+
element.removeAttribute('aria-selected');
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
#deselectAll() {
|
109
|
+
this.menuItemTargets.forEach(item => this.#toggleAriaSelected(item, false));
|
110
|
+
this.selectedIndex = -1;
|
111
|
+
}
|
112
|
+
|
113
|
+
#addEventListeners() {
|
114
|
+
document.addEventListener('keydown', this.boundHandleKeydown);
|
115
|
+
}
|
116
|
+
|
117
|
+
#removeEventListeners() {
|
118
|
+
document.removeEventListener('keydown', this.boundHandleKeydown);
|
119
|
+
}
|
120
|
+
}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["input", "error"];
|
5
|
+
static values = { shouldValidate: false };
|
6
|
+
|
7
|
+
connect() {
|
8
|
+
if (this.hasErrorTarget) {
|
9
|
+
if (this.errorTarget.textContent) {
|
10
|
+
this.shouldValidateValue = true;
|
11
|
+
} else {
|
12
|
+
this.errorTarget.classList.add("hidden");
|
13
|
+
}
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
onInvalid(error) {
|
18
|
+
error.preventDefault();
|
19
|
+
|
20
|
+
this.shouldValidateValue = true;
|
21
|
+
this.#setErrorMessage();
|
22
|
+
}
|
23
|
+
|
24
|
+
onInput() {
|
25
|
+
this.#setErrorMessage();
|
26
|
+
}
|
27
|
+
|
28
|
+
onChange() {
|
29
|
+
this.#setErrorMessage();
|
30
|
+
}
|
31
|
+
|
32
|
+
#setErrorMessage() {
|
33
|
+
if (!this.shouldValidateValue) return;
|
34
|
+
|
35
|
+
if (this.inputTarget.validity.valid) {
|
36
|
+
this.errorTarget.textContent = "";
|
37
|
+
this.errorTarget.classList.add("hidden");
|
38
|
+
} else {
|
39
|
+
this.errorTarget.textContent = this.#getValidationMessage();
|
40
|
+
this.errorTarget.classList.remove("hidden");
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
#getValidationMessage() {
|
45
|
+
let errorMessage;
|
46
|
+
|
47
|
+
const { validity, dataset, validationMessage } = this.inputTarget;
|
48
|
+
|
49
|
+
if (validity.tooLong) errorMessage = dataset.tooLong;
|
50
|
+
if (validity.tooShort) errorMessage = dataset.tooShort;
|
51
|
+
if (validity.badInput) errorMessage = dataset.badInput;
|
52
|
+
if (validity.typeMismatch) errorMessage = dataset.typeMismatch;
|
53
|
+
if (validity.stepMismatch) errorMessage = dataset.stepMismatch;
|
54
|
+
if (validity.valueMissing) errorMessage = dataset.valueMissing;
|
55
|
+
if (validity.rangeOverflow) errorMessage = dataset.rangeOverflow;
|
56
|
+
if (validity.rangeUnderflow) errorMessage = dataset.rangeUnderflow;
|
57
|
+
if (validity.patternMismatch) errorMessage = dataset.patternMismatch;
|
58
|
+
|
59
|
+
return errorMessage || validationMessage;
|
60
|
+
}
|
61
|
+
}
|