shadcn_phlexcomponents 0.1.5 → 0.1.11

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.
Files changed (249) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -0
  3. data/app/javascript/controllers/accordion_controller.ts +133 -0
  4. data/app/javascript/controllers/{avatar_controller.js → avatar_controller.ts} +4 -0
  5. data/app/javascript/controllers/checkbox_controller.ts +34 -0
  6. data/app/javascript/controllers/collapsible_controller.ts +45 -0
  7. data/app/javascript/controllers/combobox_controller.ts +145 -0
  8. data/app/javascript/controllers/command_controller.ts +129 -0
  9. data/app/javascript/controllers/command_root_controller.ts +355 -0
  10. data/app/javascript/controllers/date_picker_controller.ts +274 -0
  11. data/app/javascript/controllers/date_range_picker_controller.ts +243 -0
  12. data/app/javascript/controllers/dialog_controller.ts +113 -0
  13. data/app/javascript/controllers/dropdown_menu_controller.ts +133 -0
  14. data/app/javascript/controllers/dropdown_menu_root_controller.ts +234 -0
  15. data/app/javascript/controllers/dropdown_menu_sub_controller.ts +150 -0
  16. data/app/javascript/controllers/form_field_controller.ts +22 -0
  17. data/app/javascript/controllers/hover_card_controller.ts +93 -0
  18. data/app/javascript/controllers/{loading_button_controller.js → loading_button_controller.ts} +2 -2
  19. data/app/javascript/controllers/popover_controller.ts +141 -0
  20. data/app/javascript/controllers/progress_controller.ts +17 -0
  21. data/app/javascript/controllers/radio_group_controller.ts +106 -0
  22. data/app/javascript/controllers/select_controller.ts +200 -0
  23. data/app/javascript/controllers/{sidebar_controller.js → sidebar_controller.ts} +6 -2
  24. data/app/javascript/controllers/sidebar_trigger_controller.ts +21 -0
  25. data/app/javascript/controllers/slider_controller.ts +107 -0
  26. data/app/javascript/controllers/switch_controller.ts +30 -0
  27. data/app/javascript/controllers/tabs_controller.ts +79 -0
  28. data/app/javascript/controllers/{theme_switcher_controller.js → theme_switcher_controller.ts} +12 -9
  29. data/app/javascript/controllers/toast_container_controller.ts +62 -0
  30. data/app/javascript/controllers/toast_controller.ts +28 -0
  31. data/app/javascript/controllers/tooltip_controller.ts +98 -0
  32. data/app/javascript/shadcn_phlexcomponents.ts +57 -0
  33. data/app/javascript/utils.ts +437 -0
  34. data/app/stylesheets/date_picker.css +74 -0
  35. data/app/stylesheets/nouislider.css +173 -0
  36. data/app/stylesheets/tw-animate.css +486 -0
  37. data/lib/install/install_shadcn_phlexcomponents.rb +22 -9
  38. data/lib/shadcn_phlexcomponents/alias.rb +3 -1
  39. data/lib/shadcn_phlexcomponents/components/accordion.rb +129 -0
  40. data/lib/shadcn_phlexcomponents/components/alert.rb +59 -0
  41. data/lib/shadcn_phlexcomponents/components/alert_dialog.rb +276 -0
  42. data/lib/{components → shadcn_phlexcomponents/components}/aspect_ratio.rb +2 -2
  43. data/lib/shadcn_phlexcomponents/components/avatar.rb +63 -0
  44. data/lib/shadcn_phlexcomponents/components/badge.rb +35 -0
  45. data/lib/{components → shadcn_phlexcomponents/components}/base.rb +44 -7
  46. data/lib/shadcn_phlexcomponents/components/breadcrumb.rb +150 -0
  47. data/lib/shadcn_phlexcomponents/components/button.rb +49 -0
  48. data/lib/shadcn_phlexcomponents/components/card.rb +88 -0
  49. data/lib/{components → shadcn_phlexcomponents/components}/checkbox.rb +21 -17
  50. data/lib/{components → shadcn_phlexcomponents/components}/checkbox_group.rb +27 -16
  51. data/lib/shadcn_phlexcomponents/components/collapsible.rb +91 -0
  52. data/lib/shadcn_phlexcomponents/components/combobox.rb +398 -0
  53. data/lib/shadcn_phlexcomponents/components/command.rb +351 -0
  54. data/lib/shadcn_phlexcomponents/components/date_picker.rb +264 -0
  55. data/lib/shadcn_phlexcomponents/components/date_range_picker.rb +126 -0
  56. data/lib/shadcn_phlexcomponents/components/dialog.rb +234 -0
  57. data/lib/shadcn_phlexcomponents/components/dropdown_menu.rb +282 -0
  58. data/lib/shadcn_phlexcomponents/components/dropdown_menu_sub.rb +135 -0
  59. data/lib/shadcn_phlexcomponents/components/form/form_checkbox.rb +82 -0
  60. data/lib/shadcn_phlexcomponents/components/form/form_checkbox_group.rb +116 -0
  61. data/lib/shadcn_phlexcomponents/components/form/form_date_picker.rb +46 -0
  62. data/lib/shadcn_phlexcomponents/components/form/form_date_range_picker.rb +82 -0
  63. data/lib/{components → shadcn_phlexcomponents/components/form}/form_error.rb +7 -3
  64. data/lib/shadcn_phlexcomponents/components/form/form_helpers.rb +143 -0
  65. data/lib/shadcn_phlexcomponents/components/form/form_hint.rb +21 -0
  66. data/lib/{components → shadcn_phlexcomponents/components/form}/form_input.rb +3 -4
  67. data/lib/shadcn_phlexcomponents/components/form/form_radio_group.rb +106 -0
  68. data/lib/shadcn_phlexcomponents/components/form/form_select.rb +64 -0
  69. data/lib/shadcn_phlexcomponents/components/form/form_slider.rb +91 -0
  70. data/lib/shadcn_phlexcomponents/components/form/form_switch.rb +67 -0
  71. data/lib/shadcn_phlexcomponents/components/form/form_textarea.rb +59 -0
  72. data/lib/shadcn_phlexcomponents/components/form.rb +157 -0
  73. data/lib/shadcn_phlexcomponents/components/hover_card.rb +110 -0
  74. data/lib/shadcn_phlexcomponents/components/input.rb +31 -0
  75. data/lib/shadcn_phlexcomponents/components/label.rb +16 -0
  76. data/lib/{components → shadcn_phlexcomponents/components}/link.rb +10 -3
  77. data/lib/shadcn_phlexcomponents/components/loading_button.rb +28 -0
  78. data/lib/shadcn_phlexcomponents/components/pagination.rb +166 -0
  79. data/lib/shadcn_phlexcomponents/components/popover.rb +116 -0
  80. data/lib/{components → shadcn_phlexcomponents/components}/progress.rb +5 -5
  81. data/lib/shadcn_phlexcomponents/components/radio_group.rb +155 -0
  82. data/lib/shadcn_phlexcomponents/components/select.rb +421 -0
  83. data/lib/{components → shadcn_phlexcomponents/components}/separator.rb +9 -8
  84. data/lib/shadcn_phlexcomponents/components/sheet.rb +239 -0
  85. data/lib/{components → shadcn_phlexcomponents/components}/skeleton.rb +1 -1
  86. data/lib/shadcn_phlexcomponents/components/slider.rb +72 -0
  87. data/lib/shadcn_phlexcomponents/components/switch.rb +75 -0
  88. data/lib/shadcn_phlexcomponents/components/table.rb +140 -0
  89. data/lib/shadcn_phlexcomponents/components/tabs.rb +135 -0
  90. data/lib/shadcn_phlexcomponents/components/textarea.rb +24 -0
  91. data/lib/{components → shadcn_phlexcomponents/components}/theme_switcher.rb +2 -2
  92. data/lib/shadcn_phlexcomponents/components/toast.rb +153 -0
  93. data/lib/{components → shadcn_phlexcomponents/components}/toast_container.rb +24 -5
  94. data/lib/shadcn_phlexcomponents/components/tooltip.rb +131 -0
  95. data/lib/shadcn_phlexcomponents/initializers/shadcn_phlexcomponents.rb +25 -0
  96. data/lib/shadcn_phlexcomponents/version.rb +1 -1
  97. data/lib/tasks/install.rake +1 -1
  98. metadata +92 -168
  99. data/app/assets/tailwind/choices.css +0 -324
  100. data/app/assets/tailwind/tailwindcss-animate.css +0 -318
  101. data/app/assets/tailwind/vanilla-calendar-pro.css +0 -466
  102. data/app/javascript/controllers/accordion_controller.js +0 -133
  103. data/app/javascript/controllers/alert_dialog_controller.js +0 -157
  104. data/app/javascript/controllers/checkbox_controller.js +0 -28
  105. data/app/javascript/controllers/collapsible_controller.js +0 -35
  106. data/app/javascript/controllers/combobox_controller.js +0 -34
  107. data/app/javascript/controllers/date_picker_controller.js +0 -118
  108. data/app/javascript/controllers/date_range_picker_controller.js +0 -231
  109. data/app/javascript/controllers/dialog_controller.js +0 -159
  110. data/app/javascript/controllers/dropdown_menu_controller.js +0 -193
  111. data/app/javascript/controllers/hover_card_controller.js +0 -42
  112. data/app/javascript/controllers/popover_controller.js +0 -124
  113. data/app/javascript/controllers/progress_controller.js +0 -14
  114. data/app/javascript/controllers/radio_group_controller.js +0 -90
  115. data/app/javascript/controllers/select_controller.js +0 -294
  116. data/app/javascript/controllers/sheet_controller.js +0 -159
  117. data/app/javascript/controllers/sidebar_trigger_controller.js +0 -15
  118. data/app/javascript/controllers/switch_controller.js +0 -24
  119. data/app/javascript/controllers/tabs_controller.js +0 -73
  120. data/app/javascript/controllers/toast_container_controller.js +0 -22
  121. data/app/javascript/controllers/toast_controller.js +0 -45
  122. data/app/javascript/controllers/tooltip_controller.js +0 -41
  123. data/lib/components/accordion.rb +0 -38
  124. data/lib/components/accordion_content.rb +0 -30
  125. data/lib/components/accordion_item.rb +0 -26
  126. data/lib/components/accordion_trigger.rb +0 -45
  127. data/lib/components/alert.rb +0 -40
  128. data/lib/components/alert_description.rb +0 -11
  129. data/lib/components/alert_dialog.rb +0 -60
  130. data/lib/components/alert_dialog_action.rb +0 -22
  131. data/lib/components/alert_dialog_action_to.rb +0 -40
  132. data/lib/components/alert_dialog_cancel.rb +0 -22
  133. data/lib/components/alert_dialog_content.rb +0 -40
  134. data/lib/components/alert_dialog_description.rb +0 -22
  135. data/lib/components/alert_dialog_footer.rb +0 -11
  136. data/lib/components/alert_dialog_header.rb +0 -11
  137. data/lib/components/alert_dialog_title.rb +0 -22
  138. data/lib/components/alert_dialog_trigger.rb +0 -50
  139. data/lib/components/alert_title.rb +0 -11
  140. data/lib/components/avatar.rb +0 -31
  141. data/lib/components/avatar_fallback.rb +0 -21
  142. data/lib/components/avatar_image.rb +0 -19
  143. data/lib/components/badge.rb +0 -30
  144. data/lib/components/breadcrumb.rb +0 -51
  145. data/lib/components/breadcrumb_ellipsis.rb +0 -23
  146. data/lib/components/breadcrumb_item.rb +0 -11
  147. data/lib/components/breadcrumb_link.rb +0 -7
  148. data/lib/components/breadcrumb_page.rb +0 -21
  149. data/lib/components/breadcrumb_separator.rb +0 -26
  150. data/lib/components/button.rb +0 -53
  151. data/lib/components/card.rb +0 -31
  152. data/lib/components/card_content.rb +0 -11
  153. data/lib/components/card_description.rb +0 -11
  154. data/lib/components/card_footer.rb +0 -11
  155. data/lib/components/card_header.rb +0 -11
  156. data/lib/components/card_title.rb +0 -11
  157. data/lib/components/collapsible.rb +0 -31
  158. data/lib/components/collapsible_content.rb +0 -24
  159. data/lib/components/collapsible_trigger.rb +0 -50
  160. data/lib/components/combobox.rb +0 -57
  161. data/lib/components/combobox_item.rb +0 -9
  162. data/lib/components/date_picker.rb +0 -94
  163. data/lib/components/date_range_picker.rb +0 -113
  164. data/lib/components/dialog.rb +0 -52
  165. data/lib/components/dialog_close.rb +0 -42
  166. data/lib/components/dialog_content.rb +0 -54
  167. data/lib/components/dialog_description.rb +0 -22
  168. data/lib/components/dialog_footer.rb +0 -11
  169. data/lib/components/dialog_header.rb +0 -11
  170. data/lib/components/dialog_title.rb +0 -22
  171. data/lib/components/dialog_trigger.rb +0 -50
  172. data/lib/components/dropdown_menu.rb +0 -50
  173. data/lib/components/dropdown_menu_content.rb +0 -52
  174. data/lib/components/dropdown_menu_item.rb +0 -56
  175. data/lib/components/dropdown_menu_item_to.rb +0 -28
  176. data/lib/components/dropdown_menu_label.rb +0 -11
  177. data/lib/components/dropdown_menu_separator.rb +0 -20
  178. data/lib/components/dropdown_menu_trigger.rb +0 -57
  179. data/lib/components/form.rb +0 -59
  180. data/lib/components/form_hint.rb +0 -17
  181. data/lib/components/hover_card.rb +0 -33
  182. data/lib/components/hover_card_content.rb +0 -32
  183. data/lib/components/hover_card_trigger.rb +0 -44
  184. data/lib/components/input.rb +0 -32
  185. data/lib/components/label.rb +0 -14
  186. data/lib/components/loading_button.rb +0 -21
  187. data/lib/components/pagination.rb +0 -38
  188. data/lib/components/pagination_ellipsis.rb +0 -24
  189. data/lib/components/pagination_link.rb +0 -34
  190. data/lib/components/pagination_next.rb +0 -32
  191. data/lib/components/pagination_previous.rb +0 -32
  192. data/lib/components/popover.rb +0 -34
  193. data/lib/components/popover_content.rb +0 -40
  194. data/lib/components/popover_trigger.rb +0 -51
  195. data/lib/components/radio_group.rb +0 -62
  196. data/lib/components/radio_group_item.rb +0 -66
  197. data/lib/components/select.rb +0 -184
  198. data/lib/components/select_content.rb +0 -64
  199. data/lib/components/select_group.rb +0 -23
  200. data/lib/components/select_item.rb +0 -59
  201. data/lib/components/select_label.rb +0 -24
  202. data/lib/components/select_trigger.rb +0 -56
  203. data/lib/components/sheet.rb +0 -53
  204. data/lib/components/sheet_close.rb +0 -42
  205. data/lib/components/sheet_content.rb +0 -65
  206. data/lib/components/sheet_description.rb +0 -22
  207. data/lib/components/sheet_footer.rb +0 -11
  208. data/lib/components/sheet_header.rb +0 -11
  209. data/lib/components/sheet_title.rb +0 -22
  210. data/lib/components/sheet_trigger.rb +0 -50
  211. data/lib/components/sidebar.rb +0 -108
  212. data/lib/components/sidebar_container.rb +0 -11
  213. data/lib/components/sidebar_content.rb +0 -11
  214. data/lib/components/sidebar_footer.rb +0 -11
  215. data/lib/components/sidebar_group.rb +0 -11
  216. data/lib/components/sidebar_group_content.rb +0 -11
  217. data/lib/components/sidebar_group_label.rb +0 -16
  218. data/lib/components/sidebar_header.rb +0 -11
  219. data/lib/components/sidebar_inset.rb +0 -15
  220. data/lib/components/sidebar_menu.rb +0 -11
  221. data/lib/components/sidebar_menu_button.rb +0 -61
  222. data/lib/components/sidebar_menu_item.rb +0 -9
  223. data/lib/components/sidebar_menu_sub.rb +0 -14
  224. data/lib/components/sidebar_menu_sub_button.rb +0 -48
  225. data/lib/components/sidebar_menu_sub_item.rb +0 -9
  226. data/lib/components/sidebar_trigger.rb +0 -40
  227. data/lib/components/switch.rb +0 -66
  228. data/lib/components/table.rb +0 -75
  229. data/lib/components/table_body.rb +0 -11
  230. data/lib/components/table_caption.rb +0 -11
  231. data/lib/components/table_cell.rb +0 -11
  232. data/lib/components/table_footer.rb +0 -11
  233. data/lib/components/table_head.rb +0 -14
  234. data/lib/components/table_header.rb +0 -11
  235. data/lib/components/table_row.rb +0 -11
  236. data/lib/components/tabs.rb +0 -38
  237. data/lib/components/tabs_content.rb +0 -35
  238. data/lib/components/tabs_list.rb +0 -23
  239. data/lib/components/tabs_trigger.rb +0 -45
  240. data/lib/components/textarea.rb +0 -28
  241. data/lib/components/toast.rb +0 -101
  242. data/lib/components/toast_action.rb +0 -39
  243. data/lib/components/toast_action_to.rb +0 -28
  244. data/lib/components/toast_content.rb +0 -11
  245. data/lib/components/toast_description.rb +0 -11
  246. data/lib/components/toast_title.rb +0 -11
  247. data/lib/components/tooltip.rb +0 -34
  248. data/lib/components/tooltip_content.rb +0 -39
  249. data/lib/components/tooltip_trigger.rb +0 -48
@@ -0,0 +1,150 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import {
3
+ initFloatingUi,
4
+ ON_OPEN_FOCUS_DELAY,
5
+ getSameLevelItems,
6
+ showContent,
7
+ hideContent,
8
+ } from '../utils'
9
+
10
+ export default class extends Controller<HTMLElement> {
11
+ static targets = ['trigger', 'contentContainer', 'content']
12
+
13
+ static values = {
14
+ isOpen: Boolean,
15
+ }
16
+
17
+ declare isOpenValue: boolean
18
+ declare readonly triggerTarget: HTMLElement
19
+ declare readonly contentContainerTarget: HTMLElement
20
+ declare readonly contentTarget: HTMLElement
21
+ declare DOMKeydownListener: (event: KeyboardEvent) => void
22
+ declare cleanup: () => void
23
+ declare closeTimeout: number
24
+ declare items: HTMLElement[]
25
+
26
+ connect() {
27
+ this.items = getSameLevelItems({
28
+ content: this.contentTarget,
29
+ items: Array.from(
30
+ this.contentTarget.querySelectorAll(
31
+ '[data-dropdown-menu-target="item"], [data-dropdown-menu-sub-target="trigger"]',
32
+ ),
33
+ ),
34
+ closestContentSelector: this.closestContentSelector(),
35
+ })
36
+ }
37
+
38
+ closestContentSelector() {
39
+ return '[data-dropdown-menu-sub-target="content"]'
40
+ }
41
+
42
+ open(event: MouseEvent | KeyboardEvent | null = null) {
43
+ clearTimeout(this.closeTimeout)
44
+ this.isOpenValue = true
45
+
46
+ setTimeout(() => {
47
+ if (event instanceof KeyboardEvent) {
48
+ const key = event.key
49
+
50
+ if (['ArrowRight', 'Enter', ' '].includes(key)) {
51
+ this.focusItemByIndex(null, 0)
52
+ }
53
+ }
54
+ }, ON_OPEN_FOCUS_DELAY)
55
+ }
56
+
57
+ close() {
58
+ this.closeTimeout = window.setTimeout(() => {
59
+ this.isOpenValue = false
60
+ }, 250)
61
+ }
62
+
63
+ closeOnLeftKeydown(event: KeyboardEvent) {
64
+ this.closeImmediately()
65
+ this.triggerTarget.focus()
66
+ }
67
+
68
+ focusItemByIndex(event: KeyboardEvent | null, index: number | null) {
69
+ if (event) {
70
+ const key = event.key
71
+
72
+ if (key === 'ArrowUp') {
73
+ this.items[this.items.length - 1].focus()
74
+ } else {
75
+ this.items[0].focus()
76
+ }
77
+ } else if (index !== null) {
78
+ this.items[index].focus()
79
+ }
80
+ }
81
+
82
+ closeParentSubMenu() {
83
+ const parentContent = this.triggerTarget.closest(
84
+ '[data-dropdown-menu-sub-target="content"]',
85
+ )
86
+
87
+ if (parentContent) {
88
+ const subMenu = parentContent.closest(
89
+ '[data-shadcn-phlexcomponents="dropdown-menu-sub"]',
90
+ )
91
+
92
+ if (subMenu) {
93
+ const subMenuController =
94
+ window.Stimulus.getControllerForElementAndIdentifier(
95
+ subMenu,
96
+ 'dropdown-menu-sub',
97
+ )
98
+
99
+ if (subMenuController) {
100
+ // @ts-ignore
101
+ subMenuController.closeImmediately()
102
+ setTimeout(() => {
103
+ // @ts-ignore
104
+ // weird bug where focus goes to body element after closing, and setting focus
105
+ // manually doesn't work
106
+ subMenuController.triggerTarget.focus()
107
+ }, 100)
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ closeImmediately() {
114
+ this.isOpenValue = false
115
+ }
116
+
117
+ cleanupEventListeners() {
118
+ if (this.cleanup) this.cleanup()
119
+ }
120
+
121
+ isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
122
+ if (isOpen) {
123
+ showContent({
124
+ trigger: this.triggerTarget,
125
+ content: this.contentTarget,
126
+ contentContainer: this.contentContainerTarget,
127
+ })
128
+
129
+ this.cleanup = initFloatingUi({
130
+ referenceElement: this.triggerTarget,
131
+ floatingElement: this.contentContainerTarget,
132
+ side: this.contentTarget.dataset.side,
133
+ align: this.contentTarget.dataset.align,
134
+ sideOffset: -2,
135
+ })
136
+ } else {
137
+ this.closeTimeout = window.setTimeout(() => {
138
+ hideContent({
139
+ trigger: this.triggerTarget,
140
+ content: this.contentTarget,
141
+ contentContainer: this.contentContainerTarget,
142
+ })
143
+ })
144
+ }
145
+ }
146
+
147
+ disconnect() {
148
+ this.cleanupEventListeners()
149
+ }
150
+ }
@@ -0,0 +1,22 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ const hintContainer = this.element.querySelector('[data-remove-label]')
6
+ const labelContainer = this.element.querySelector('[data-remove-hint]')
7
+
8
+ if (hintContainer) {
9
+ const label = hintContainer.querySelector('label') as HTMLElement
10
+ const hint = hintContainer.querySelector('p') as HTMLElement
11
+ label.remove()
12
+ hintContainer.replaceWith(hint)
13
+ }
14
+
15
+ if (labelContainer) {
16
+ const hint = labelContainer.querySelector('p') as HTMLElement
17
+ const label = labelContainer.querySelector('label') as HTMLElement
18
+ hint.remove()
19
+ labelContainer.replaceWith(label)
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,93 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import { useHover } from 'stimulus-use'
3
+ import { initFloatingUi, showContent, hideContent } from '../utils'
4
+
5
+ export default class extends Controller<HTMLElement> {
6
+ static targets = ['trigger', 'content', 'contentContainer']
7
+ static values = {
8
+ isOpen: Boolean,
9
+ }
10
+
11
+ declare isOpenValue: boolean
12
+ declare readonly triggerTarget: HTMLElement
13
+ declare readonly contentTarget: HTMLElement
14
+ declare readonly contentContainerTarget: HTMLElement
15
+ declare cleanup: () => void
16
+ declare closeTimeout: number
17
+ declare DOMKeydownListener: (event: KeyboardEvent) => void
18
+
19
+ connect() {
20
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
21
+ useHover(this, { element: this.triggerTarget, dispatchEvent: false })
22
+ }
23
+
24
+ open() {
25
+ window.clearTimeout(this.closeTimeout)
26
+ this.isOpenValue = true
27
+ }
28
+
29
+ close() {
30
+ this.closeTimeout = window.setTimeout(() => {
31
+ this.isOpenValue = false
32
+ }, 250)
33
+ }
34
+
35
+ // for useHover
36
+ mouseEnter() {
37
+ this.open()
38
+ }
39
+
40
+ // for useHover
41
+ mouseLeave() {
42
+ this.close()
43
+ }
44
+
45
+ setupEventListeners() {
46
+ document.addEventListener('keydown', this.DOMKeydownListener)
47
+ }
48
+
49
+ cleanupEventListeners() {
50
+ if (this.cleanup) this.cleanup()
51
+ document.removeEventListener('keydown', this.DOMKeydownListener)
52
+ }
53
+
54
+ onDOMKeydown(event: KeyboardEvent) {
55
+ if (!this.isOpenValue) return
56
+
57
+ const key = event.key
58
+
59
+ if (key === 'Escape') {
60
+ this.close()
61
+ }
62
+ }
63
+
64
+ isOpenValueChanged(isOpen: boolean) {
65
+ if (isOpen) {
66
+ showContent({
67
+ content: this.contentTarget,
68
+ contentContainer: this.contentContainerTarget,
69
+ })
70
+
71
+ this.setupEventListeners()
72
+
73
+ this.cleanup = initFloatingUi({
74
+ referenceElement: this.triggerTarget,
75
+ floatingElement: this.contentContainerTarget,
76
+ side: this.contentTarget.dataset.side,
77
+ align: this.contentTarget.dataset.align,
78
+ sideOffset: 4,
79
+ })
80
+ } else {
81
+ hideContent({
82
+ content: this.contentTarget,
83
+ contentContainer: this.contentContainerTarget,
84
+ })
85
+
86
+ this.cleanupEventListeners()
87
+ }
88
+ }
89
+
90
+ disconnect() {
91
+ this.cleanupEventListeners()
92
+ }
93
+ }
@@ -1,13 +1,13 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
 
3
- export default class extends Controller {
3
+ export default class extends Controller<HTMLButtonElement> {
4
4
  connect() {
5
5
  const el = this.element
6
6
  const form = el.closest('form')
7
7
 
8
8
  if (form && form.dataset.turbo === 'false') {
9
9
  form.addEventListener('submit', () => {
10
- form.ariaBusy = true
10
+ form.ariaBusy = 'true'
11
11
  el.disabled = true
12
12
  })
13
13
  }
@@ -0,0 +1,141 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import { useClickOutside } from 'stimulus-use'
3
+ import {
4
+ initFloatingUi,
5
+ focusTrigger,
6
+ ON_OPEN_FOCUS_DELAY,
7
+ getFocusableElements,
8
+ showContent,
9
+ hideContent,
10
+ } from '../utils'
11
+
12
+ export default class extends Controller<HTMLElement> {
13
+ static targets = ['trigger', 'contentContainer', 'content']
14
+ static values = { isOpen: Boolean }
15
+
16
+ declare isOpenValue: boolean
17
+ declare readonly triggerTarget: HTMLElement
18
+ declare readonly contentContainerTarget: HTMLElement
19
+ declare readonly contentTarget: HTMLElement
20
+ declare DOMKeydownListener: (event: KeyboardEvent) => void
21
+ declare cleanup: () => void
22
+
23
+ connect() {
24
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
25
+ useClickOutside(this, { element: this.contentTarget, dispatchEvent: false })
26
+ }
27
+
28
+ toggle() {
29
+ if (this.isOpenValue) {
30
+ this.close()
31
+ } else {
32
+ this.open()
33
+ }
34
+ }
35
+
36
+ open() {
37
+ this.isOpenValue = true
38
+ this.onOpen()
39
+
40
+ setTimeout(() => {
41
+ this.onOpenFocusedElement().focus()
42
+ }, ON_OPEN_FOCUS_DELAY)
43
+ }
44
+
45
+ close() {
46
+ this.isOpenValue = false
47
+ this.onClose()
48
+ }
49
+
50
+ onDOMKeydown(event: KeyboardEvent) {
51
+ if (!this.isOpenValue) return
52
+
53
+ const key = event.key
54
+
55
+ if (key === 'Escape') {
56
+ this.close()
57
+ } else if (key === 'Tab') {
58
+ const focusableElements = getFocusableElements(this.contentTarget)
59
+
60
+ const firstElement = focusableElements[0]
61
+ const lastElement = focusableElements[focusableElements.length - 1]
62
+
63
+ // If Shift + Tab pressed on first element, go to last element
64
+ if (event.shiftKey && document.activeElement === firstElement) {
65
+ event.preventDefault()
66
+ lastElement.focus()
67
+ }
68
+ // If Tab pressed on last element, go to first element
69
+ else if (!event.shiftKey && document.activeElement === lastElement) {
70
+ event.preventDefault()
71
+ firstElement.focus()
72
+ }
73
+ }
74
+ }
75
+
76
+ clickOutside(event: MouseEvent) {
77
+ const target = event.target
78
+ // Let #toggle to handle state when clicked on trigger
79
+ if (target === this.triggerTarget) return
80
+
81
+ this.close()
82
+ }
83
+
84
+ onOpen() {}
85
+
86
+ onOpenFocusedElement() {
87
+ const focusableElements = getFocusableElements(this.contentTarget)
88
+ return focusableElements[0]
89
+ }
90
+
91
+ onClose() {}
92
+
93
+ referenceElement() {
94
+ return this.triggerTarget
95
+ }
96
+
97
+ isOpenValueChanged(isOpen: boolean, previousIsOpen: boolean) {
98
+ if (isOpen === true) {
99
+ showContent({
100
+ trigger: this.triggerTarget,
101
+ content: this.contentTarget,
102
+ contentContainer: this.contentContainerTarget,
103
+ })
104
+
105
+ this.cleanup = initFloatingUi({
106
+ referenceElement: this.referenceElement(),
107
+ floatingElement: this.contentContainerTarget,
108
+ side: this.contentTarget.dataset.side,
109
+ align: this.contentTarget.dataset.align,
110
+ sideOffset: 4,
111
+ })
112
+ this.setupEventListeners()
113
+ } else {
114
+ hideContent({
115
+ trigger: this.triggerTarget,
116
+ content: this.contentTarget,
117
+ contentContainer: this.contentContainerTarget,
118
+ })
119
+
120
+ this.cleanupEventListeners()
121
+
122
+ // Only focus trigger when is previously opened
123
+ if (previousIsOpen) {
124
+ focusTrigger(this.triggerTarget)
125
+ }
126
+ }
127
+ }
128
+
129
+ setupEventListeners() {
130
+ document.addEventListener('keydown', this.DOMKeydownListener)
131
+ }
132
+
133
+ cleanupEventListeners() {
134
+ if (this.cleanup) this.cleanup()
135
+ document.removeEventListener('keydown', this.DOMKeydownListener)
136
+ }
137
+
138
+ disconnect() {
139
+ this.cleanupEventListeners()
140
+ }
141
+ }
@@ -0,0 +1,17 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static targets = ['indicator']
5
+
6
+ static values = {
7
+ percent: Number,
8
+ }
9
+
10
+ declare readonly indicatorTarget: HTMLElement
11
+ declare percentValue: number
12
+
13
+ percentValueChanged(value: number) {
14
+ this.element.setAttribute('aria-valuenow', `${value}`)
15
+ this.indicatorTarget.style.transform = `translateX(-${100 - value}%)`
16
+ }
17
+ }
@@ -0,0 +1,106 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller<HTMLElement> {
4
+ static targets = ['item', 'input', 'indicator']
5
+ static values = {
6
+ selected: String,
7
+ }
8
+
9
+ declare readonly itemTargets: HTMLInputElement[]
10
+ declare readonly inputTargets: HTMLInputElement[]
11
+ declare readonly indicatorTargets: HTMLInputElement[]
12
+ declare selectedValue: string
13
+
14
+ connect() {
15
+ if (!this.selectedValue) {
16
+ this.itemTargets[0].tabIndex = 0
17
+ }
18
+ }
19
+
20
+ select(event: MouseEvent) {
21
+ const item = event.currentTarget as HTMLInputElement
22
+ this.selectedValue = item.dataset.value as string
23
+ }
24
+
25
+ preventDefault(event: KeyboardEvent) {
26
+ event.preventDefault()
27
+ }
28
+
29
+ selectItem(event: KeyboardEvent) {
30
+ const focusableItems = this.itemTargets.filter(
31
+ (t) => !t.disabled,
32
+ ) as HTMLInputElement[]
33
+
34
+ const item = event.currentTarget as HTMLInputElement
35
+ const index = focusableItems.indexOf(item)
36
+ const key = event.key
37
+ let newIndex = 0
38
+
39
+ if (['ArrowUp', 'ArrowLeft'].includes(key)) {
40
+ newIndex = index - 1
41
+
42
+ if (newIndex < 0) {
43
+ newIndex = focusableItems.length - 1
44
+ }
45
+ } else {
46
+ newIndex = index + 1
47
+
48
+ if (newIndex > focusableItems.length - 1) {
49
+ newIndex = 0
50
+ }
51
+ }
52
+
53
+ this.selectedValue = focusableItems[newIndex].dataset.value as string
54
+ }
55
+
56
+ focusItem() {
57
+ const item = this.itemTargets.find(
58
+ (i) => i.dataset.value === this.selectedValue,
59
+ )
60
+
61
+ if (!item) return
62
+
63
+ // Focus first item that is not disabled and allow it to be focused
64
+ if (item.disabled) {
65
+ item.tabIndex = -1
66
+
67
+ const focusableItems = this.itemTargets.filter(
68
+ (t) => !t.disabled,
69
+ ) as HTMLInputElement[]
70
+
71
+ if (focusableItems.length > 0) {
72
+ focusableItems[0].focus()
73
+ focusableItems[0].tabIndex = 0
74
+ }
75
+ } else {
76
+ item.focus()
77
+ }
78
+ }
79
+
80
+ selectedValueChanged(value: string) {
81
+ this.itemTargets.forEach((item) => {
82
+ const input = item.querySelector(
83
+ '[data-radio-group-target="input"]',
84
+ ) as HTMLInputElement
85
+ const indicator = item.querySelector(
86
+ '[data-radio-group-target="indicator"]',
87
+ ) as HTMLInputElement
88
+
89
+ if (value === item.dataset.value) {
90
+ input.checked = true
91
+ item.tabIndex = 0
92
+ item.ariaChecked = 'true'
93
+ item.dataset.state = 'checked'
94
+ indicator.classList.remove('hidden')
95
+ } else {
96
+ input.checked = false
97
+ item.tabIndex = -1
98
+ item.ariaChecked = 'false'
99
+ item.dataset.state = 'unchecked'
100
+ indicator.classList.add('hidden')
101
+ }
102
+ })
103
+
104
+ this.focusItem()
105
+ }
106
+ }