shadcn_phlexcomponents 0.1.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.
Files changed (168) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +39 -0
  3. data/Rakefile +12 -0
  4. data/app/assets/tailwind/tailwindcss-animate.css +318 -0
  5. data/app/assets/tailwind/vanilla-calendar-pro.css +461 -0
  6. data/app/javascript/controllers/accordion_controller.js +133 -0
  7. data/app/javascript/controllers/alert_dialog_controller.js +157 -0
  8. data/app/javascript/controllers/avatar_controller.js +15 -0
  9. data/app/javascript/controllers/checkbox_controller.js +28 -0
  10. data/app/javascript/controllers/collapsible_controller.js +35 -0
  11. data/app/javascript/controllers/combobox_controller.js +291 -0
  12. data/app/javascript/controllers/datepicker_controller.js +47 -0
  13. data/app/javascript/controllers/dialog_controller.js +159 -0
  14. data/app/javascript/controllers/dropdown_menu_controller.js +193 -0
  15. data/app/javascript/controllers/hover_card_controller.js +135 -0
  16. data/app/javascript/controllers/loading_button_controller.js +15 -0
  17. data/app/javascript/controllers/popover_controller.js +124 -0
  18. data/app/javascript/controllers/progress_controller.js +14 -0
  19. data/app/javascript/controllers/radio_group_controller.js +90 -0
  20. data/app/javascript/controllers/select_controller.js +294 -0
  21. data/app/javascript/controllers/sheet_controller.js +159 -0
  22. data/app/javascript/controllers/sidebar_controller.js +36 -0
  23. data/app/javascript/controllers/sidebar_trigger_controller.js +15 -0
  24. data/app/javascript/controllers/switch_controller.js +24 -0
  25. data/app/javascript/controllers/tabs_controller.js +73 -0
  26. data/app/javascript/controllers/theme_switcher_controller.js +32 -0
  27. data/app/javascript/controllers/toast_container_controller.js +22 -0
  28. data/app/javascript/controllers/toast_controller.js +45 -0
  29. data/app/javascript/controllers/tooltip_controller.js +135 -0
  30. data/lib/components/accordion.rb +38 -0
  31. data/lib/components/accordion_content.rb +28 -0
  32. data/lib/components/accordion_item.rb +26 -0
  33. data/lib/components/accordion_trigger.rb +45 -0
  34. data/lib/components/alert.rb +40 -0
  35. data/lib/components/alert_description.rb +11 -0
  36. data/lib/components/alert_dialog.rb +60 -0
  37. data/lib/components/alert_dialog_action.rb +22 -0
  38. data/lib/components/alert_dialog_action_to.rb +37 -0
  39. data/lib/components/alert_dialog_cancel.rb +22 -0
  40. data/lib/components/alert_dialog_content.rb +40 -0
  41. data/lib/components/alert_dialog_description.rb +22 -0
  42. data/lib/components/alert_dialog_footer.rb +11 -0
  43. data/lib/components/alert_dialog_header.rb +11 -0
  44. data/lib/components/alert_dialog_title.rb +22 -0
  45. data/lib/components/alert_dialog_trigger.rb +50 -0
  46. data/lib/components/alert_title.rb +11 -0
  47. data/lib/components/aspect_ratio.rb +19 -0
  48. data/lib/components/avatar.rb +31 -0
  49. data/lib/components/avatar_fallback.rb +21 -0
  50. data/lib/components/avatar_image.rb +20 -0
  51. data/lib/components/badge.rb +36 -0
  52. data/lib/components/base.rb +108 -0
  53. data/lib/components/breadcrumb.rb +51 -0
  54. data/lib/components/breadcrumb_ellipsis.rb +23 -0
  55. data/lib/components/breadcrumb_item.rb +11 -0
  56. data/lib/components/breadcrumb_link.rb +7 -0
  57. data/lib/components/breadcrumb_page.rb +21 -0
  58. data/lib/components/breadcrumb_separator.rb +26 -0
  59. data/lib/components/button.rb +53 -0
  60. data/lib/components/card.rb +31 -0
  61. data/lib/components/card_content.rb +11 -0
  62. data/lib/components/card_description.rb +11 -0
  63. data/lib/components/card_footer.rb +11 -0
  64. data/lib/components/card_header.rb +11 -0
  65. data/lib/components/card_title.rb +11 -0
  66. data/lib/components/checkbox.rb +65 -0
  67. data/lib/components/checkbox_group.rb +48 -0
  68. data/lib/components/collapsible.rb +32 -0
  69. data/lib/components/collapsible_content.rb +25 -0
  70. data/lib/components/collapsible_trigger.rb +50 -0
  71. data/lib/components/datepicker.rb +38 -0
  72. data/lib/components/dialog.rb +52 -0
  73. data/lib/components/dialog_close.rb +42 -0
  74. data/lib/components/dialog_content.rb +54 -0
  75. data/lib/components/dialog_description.rb +22 -0
  76. data/lib/components/dialog_footer.rb +11 -0
  77. data/lib/components/dialog_header.rb +11 -0
  78. data/lib/components/dialog_title.rb +22 -0
  79. data/lib/components/dialog_trigger.rb +50 -0
  80. data/lib/components/dropdown_menu.rb +50 -0
  81. data/lib/components/dropdown_menu_content.rb +49 -0
  82. data/lib/components/dropdown_menu_item.rb +57 -0
  83. data/lib/components/dropdown_menu_item_to.rb +25 -0
  84. data/lib/components/dropdown_menu_label.rb +12 -0
  85. data/lib/components/dropdown_menu_separator.rb +20 -0
  86. data/lib/components/dropdown_menu_trigger.rb +58 -0
  87. data/lib/components/hover_card.rb +33 -0
  88. data/lib/components/hover_card_content.rb +36 -0
  89. data/lib/components/hover_card_trigger.rb +50 -0
  90. data/lib/components/input.rb +32 -0
  91. data/lib/components/label.rb +15 -0
  92. data/lib/components/link.rb +26 -0
  93. data/lib/components/loading_button.rb +21 -0
  94. data/lib/components/pagination.rb +38 -0
  95. data/lib/components/pagination_ellipsis.rb +24 -0
  96. data/lib/components/pagination_link.rb +34 -0
  97. data/lib/components/pagination_next.rb +32 -0
  98. data/lib/components/pagination_previous.rb +32 -0
  99. data/lib/components/popover.rb +35 -0
  100. data/lib/components/popover_content.rb +37 -0
  101. data/lib/components/popover_trigger.rb +52 -0
  102. data/lib/components/progress.rb +37 -0
  103. data/lib/components/radio_group.rb +62 -0
  104. data/lib/components/radio_group_item.rb +66 -0
  105. data/lib/components/select.rb +189 -0
  106. data/lib/components/select_content.rb +59 -0
  107. data/lib/components/select_group.rb +23 -0
  108. data/lib/components/select_item.rb +58 -0
  109. data/lib/components/select_label.rb +23 -0
  110. data/lib/components/select_trigger.rb +54 -0
  111. data/lib/components/separator.rb +29 -0
  112. data/lib/components/sheet.rb +53 -0
  113. data/lib/components/sheet_close.rb +42 -0
  114. data/lib/components/sheet_content.rb +67 -0
  115. data/lib/components/sheet_description.rb +22 -0
  116. data/lib/components/sheet_footer.rb +11 -0
  117. data/lib/components/sheet_header.rb +11 -0
  118. data/lib/components/sheet_title.rb +22 -0
  119. data/lib/components/sheet_trigger.rb +50 -0
  120. data/lib/components/sidebar.rb +103 -0
  121. data/lib/components/sidebar_container.rb +11 -0
  122. data/lib/components/sidebar_content.rb +11 -0
  123. data/lib/components/sidebar_footer.rb +11 -0
  124. data/lib/components/sidebar_group.rb +11 -0
  125. data/lib/components/sidebar_group_content.rb +11 -0
  126. data/lib/components/sidebar_group_label.rb +16 -0
  127. data/lib/components/sidebar_header.rb +11 -0
  128. data/lib/components/sidebar_inset.rb +15 -0
  129. data/lib/components/sidebar_menu.rb +11 -0
  130. data/lib/components/sidebar_menu_button.rb +61 -0
  131. data/lib/components/sidebar_menu_item.rb +9 -0
  132. data/lib/components/sidebar_menu_sub.rb +14 -0
  133. data/lib/components/sidebar_menu_sub_button.rb +48 -0
  134. data/lib/components/sidebar_menu_sub_item.rb +9 -0
  135. data/lib/components/sidebar_trigger.rb +40 -0
  136. data/lib/components/skeleton.rb +11 -0
  137. data/lib/components/switch.rb +65 -0
  138. data/lib/components/table.rb +73 -0
  139. data/lib/components/table_body.rb +11 -0
  140. data/lib/components/table_caption.rb +11 -0
  141. data/lib/components/table_cell.rb +11 -0
  142. data/lib/components/table_footer.rb +11 -0
  143. data/lib/components/table_head.rb +14 -0
  144. data/lib/components/table_header.rb +11 -0
  145. data/lib/components/table_row.rb +11 -0
  146. data/lib/components/tabs.rb +38 -0
  147. data/lib/components/tabs_content.rb +35 -0
  148. data/lib/components/tabs_list.rb +23 -0
  149. data/lib/components/tabs_trigger.rb +45 -0
  150. data/lib/components/textarea.rb +28 -0
  151. data/lib/components/theme_switcher.rb +21 -0
  152. data/lib/components/toast.rb +100 -0
  153. data/lib/components/toast_action.rb +38 -0
  154. data/lib/components/toast_action_to.rb +25 -0
  155. data/lib/components/toast_container.rb +44 -0
  156. data/lib/components/toast_content.rb +11 -0
  157. data/lib/components/toast_description.rb +11 -0
  158. data/lib/components/toast_title.rb +11 -0
  159. data/lib/components/tooltip.rb +34 -0
  160. data/lib/components/tooltip_content.rb +42 -0
  161. data/lib/components/tooltip_trigger.rb +50 -0
  162. data/lib/install/install_shadcn_phlexcomponents.rb +12 -0
  163. data/lib/shadcn_phlexcomponents/alias.rb +132 -0
  164. data/lib/shadcn_phlexcomponents/engine.rb +11 -0
  165. data/lib/shadcn_phlexcomponents/version.rb +5 -0
  166. data/lib/shadcn_phlexcomponents.rb +9 -0
  167. data/lib/tasks/install.rake +10 -0
  168. metadata +264 -0
@@ -0,0 +1,159 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static targets = ['trigger', 'content']
5
+
6
+ connect() {
7
+ this.DOMClickListener = this.onDOMClick.bind(this)
8
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
9
+
10
+ this.focusableElements = this.contentTarget.querySelectorAll(
11
+ 'button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])',
12
+ )
13
+
14
+ this.firstElement = this.focusableElements[0]
15
+ this.lastElement = this.focusableElements[this.focusableElements.length - 1]
16
+ this.contentElement = document.querySelector(`#${this.contentTarget.id}`)
17
+ }
18
+
19
+ open() {
20
+ this.showOverlay()
21
+ this.contentElement.classList.remove('hidden')
22
+ this.contentElement.dataset.state = 'open'
23
+ this.triggerTarget.ariaExpanded = true
24
+ this.setupEventListeners()
25
+ this.addInert()
26
+
27
+ if (window.innerHeight < document.documentElement.scrollHeight) {
28
+ document.body.dataset.scrollLocked = 1
29
+ }
30
+ document.body.appendChild(this.contentElement)
31
+ this.firstElement.focus() // must be after appendChild
32
+ }
33
+
34
+ close() {
35
+ this.contentElement.dataset.state = 'closed'
36
+ this.triggerTarget.ariaExpanded = false
37
+ this.cleanupEventListeners()
38
+ this.removeInert()
39
+ this.element.appendChild(this.contentElement)
40
+
41
+ setTimeout(() => {
42
+ this.contentElement.classList.add('hidden')
43
+ }, 100)
44
+
45
+ setTimeout(() => {
46
+ delete document.body.dataset.scrollLocked
47
+ this.removeOverlay()
48
+ }, 150)
49
+
50
+ this.focusTrigger()
51
+ }
52
+
53
+ focusTrigger() {
54
+ if (this.triggerTarget.dataset.asChild === 'false') {
55
+ this.triggerTarget.firstElementChild.focus()
56
+ } else {
57
+ this.triggerTarget.focus()
58
+ }
59
+ }
60
+
61
+ isOpen() {
62
+ return this.triggerTarget.ariaExpanded === 'true'
63
+ }
64
+
65
+ showOverlay() {
66
+ const elem = document.createElement('div')
67
+ elem.classList.add(
68
+ 'fixed',
69
+ 'inset-0',
70
+ 'z-50',
71
+ 'bg-black/80',
72
+ 'data-[state=open]:animate-in',
73
+ 'data-[state=closed]:animate-out',
74
+ 'data-[state=closed]:fade-out-0',
75
+ 'data-[state=open]:fade-in-0',
76
+ 'pointer-events-auto',
77
+ )
78
+ elem.dataset.state = 'open'
79
+ elem.ariaHidden = true
80
+ elem.dataset.overlay = true
81
+ document.body.appendChild(elem)
82
+ }
83
+
84
+ removeOverlay() {
85
+ if (document.querySelector('[data-overlay]')) {
86
+ document.querySelector('[data-overlay]').remove()
87
+ }
88
+ }
89
+
90
+ // Global listeners
91
+ onDOMClick(event) {
92
+ if (!this.isOpen()) return
93
+
94
+ const target = event.target
95
+ const trigger = event.target.closest(
96
+ '[data-shadcn-phlexcomponents--dialog-target="trigger"]',
97
+ )
98
+
99
+ if (trigger) return
100
+
101
+ const close = target.closest(
102
+ '[data-action*="shadcn-phlexcomponents--dialog#close"]',
103
+ )
104
+
105
+ if (
106
+ close ||
107
+ (target.dataset.action &&
108
+ target.dataset.action.includes('shadcn-phlexcomponents--dialog#close'))
109
+ )
110
+ this.close()
111
+
112
+ if (this.contentElement.contains(event.target)) return
113
+
114
+ this.close()
115
+ }
116
+
117
+ onDOMKeydown(event) {
118
+ if (!this.isOpen()) return
119
+
120
+ const key = event.key
121
+
122
+ if (key === 'Escape') {
123
+ this.close()
124
+ } else if (key === 'Tab') {
125
+ // If Shift + Tab pressed on first element, go to last element
126
+ if (event.shiftKey && document.activeElement === this.firstElement) {
127
+ event.preventDefault()
128
+ this.lastElement.focus()
129
+ }
130
+ // If Tab pressed on last element, go to first element
131
+ else if (!event.shiftKey && document.activeElement === this.lastElement) {
132
+ event.preventDefault()
133
+ this.firstElement.focus()
134
+ }
135
+ }
136
+ }
137
+
138
+ setupEventListeners() {
139
+ document.addEventListener('click', this.DOMClickListener)
140
+ document.addEventListener('keydown', this.DOMKeydownListener)
141
+ }
142
+
143
+ cleanupEventListeners() {
144
+ document.removeEventListener('click', this.DOMClickListener)
145
+ document.removeEventListener('keydown', this.DOMKeydownListener)
146
+ }
147
+
148
+ addInert() {
149
+ Array.from(document.body.children)
150
+ .filter((el) => el !== this.contentElement)
151
+ .forEach((el) => el.setAttribute('inert', ''))
152
+ }
153
+
154
+ removeInert() {
155
+ Array.from(document.body.children)
156
+ .filter((el) => el.hasAttribute('inert'))
157
+ .forEach((el) => el.removeAttribute('inert'))
158
+ }
159
+ }
@@ -0,0 +1,193 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import {
3
+ computePosition,
4
+ flip,
5
+ shift,
6
+ offset,
7
+ autoUpdate,
8
+ } from '@floating-ui/dom'
9
+
10
+ export default class extends Controller {
11
+ static targets = ['trigger', 'contentWrapper', 'content', 'item']
12
+
13
+ connect() {
14
+ this.contentTarget.dataset.state = this.isOpen() ? 'open' : 'closed'
15
+ this.DOMClickListener = this.onDOMClick.bind(this)
16
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
17
+ this.items = this.itemTargets.filter(
18
+ (item) => item.dataset.disabled === undefined,
19
+ )
20
+ }
21
+
22
+ toggle(event) {
23
+ const key = event.key
24
+
25
+ if (this.isOpen()) {
26
+ this.close()
27
+ } else {
28
+ this.open()
29
+ }
30
+
31
+ if (event.currentTarget === this.triggerTarget) {
32
+ if (['ArrowDown', 'Enter', ' '].includes(key)) {
33
+ setTimeout(() => {
34
+ this.focusItem(null, 0)
35
+ }, 100)
36
+ }
37
+ }
38
+ }
39
+
40
+ isOpen() {
41
+ return this.triggerTarget.ariaExpanded === 'true'
42
+ }
43
+
44
+ open() {
45
+ this.contentWrapperTarget.classList.remove('hidden')
46
+ this.triggerTarget.ariaExpanded = true
47
+ this.triggerTarget.dataset.state = 'open'
48
+ this.contentTarget.dataset.state = 'open'
49
+
50
+ setTimeout(() => {
51
+ this.focusContent()
52
+ }, 100)
53
+
54
+ this.setupEventListeners()
55
+
56
+ if (window.innerHeight < document.documentElement.scrollHeight) {
57
+ document.body.dataset.scrollLocked = 1
58
+ }
59
+
60
+ this.cleanup = autoUpdate(
61
+ this.triggerTarget,
62
+ this.contentWrapperTarget,
63
+ () => {
64
+ computePosition(this.triggerTarget, this.contentWrapperTarget, {
65
+ placement: this.element.dataset.side,
66
+ strategy: 'fixed',
67
+ middleware: [flip(), shift(), offset(4)],
68
+ }).then(({ x, y }) => {
69
+ Object.assign(this.contentWrapperTarget.style, {
70
+ left: `${x}px`,
71
+ top: `${y}px`,
72
+ })
73
+ })
74
+ },
75
+ )
76
+ }
77
+
78
+ close() {
79
+ this.triggerTarget.ariaExpanded = false
80
+ this.triggerTarget.dataset.state = 'closed'
81
+ this.contentTarget.dataset.state = 'closed'
82
+ this.cleanup()
83
+ this.cleanupEventListeners()
84
+ delete document.body.dataset.scrollLocked
85
+
86
+ setTimeout(() => {
87
+ this.contentWrapperTarget.classList.add('hidden')
88
+ }, 100)
89
+
90
+ this.focusTrigger()
91
+ }
92
+
93
+ focusTrigger() {
94
+ if (this.triggerTarget.dataset.asChild === 'false') {
95
+ this.triggerTarget.firstElementChild.focus()
96
+ } else {
97
+ this.triggerTarget.focus()
98
+ }
99
+ }
100
+
101
+ focusItem(event = null, index = null) {
102
+ let itemIndex = index
103
+
104
+ if (event) {
105
+ const item = event.currentTarget || event.target
106
+ itemIndex = this.items.indexOf(item)
107
+ }
108
+
109
+ this.items.forEach((item, index) => {
110
+ if (index === itemIndex) {
111
+ item.tabIndex = 0
112
+ item.focus()
113
+ } else {
114
+ item.tabIndex = -1
115
+ }
116
+ })
117
+ }
118
+
119
+ focusFirstItem() {
120
+ this.focusItem(null, 0)
121
+ }
122
+
123
+ focusLastItem() {
124
+ this.focusItem(null, this.items.length - 1)
125
+ }
126
+
127
+ focusNextItem(event) {
128
+ const item = event.currentTarget || event.target
129
+ const index = this.items.indexOf(item)
130
+ if (index === this.items.length - 1) return
131
+
132
+ this.focusItem(null, index + 1)
133
+ }
134
+
135
+ focusPrevItem(event) {
136
+ const item = event.currentTarget
137
+ const index = this.items.indexOf(item)
138
+ if (index === 0) return
139
+
140
+ this.focusItem(null, index - 1)
141
+ }
142
+
143
+ focusContent() {
144
+ this.items.forEach((item) => {
145
+ item.blur()
146
+ })
147
+
148
+ this.contentTarget.focus()
149
+ }
150
+
151
+ selectItem(event) {
152
+ if (!this.isOpen()) return
153
+
154
+ const key = event.key
155
+ const item = event.currentTarget || event.target
156
+
157
+ if (key === 'Enter' || key === ' ') {
158
+ item.click()
159
+ }
160
+
161
+ this.close()
162
+ }
163
+
164
+ // Global listeners
165
+ onDOMClick(event) {
166
+ if (!this.isOpen()) return
167
+ if (this.element.contains(event.target)) return
168
+
169
+ this.close()
170
+ }
171
+
172
+ onDOMKeydown(event) {
173
+ if (!this.isOpen()) return
174
+
175
+ const key = event.key
176
+
177
+ if (key === 'Tab') event.preventDefault()
178
+
179
+ if (key === 'Escape') {
180
+ this.close()
181
+ }
182
+ }
183
+
184
+ setupEventListeners() {
185
+ document.addEventListener('click', this.DOMClickListener)
186
+ document.addEventListener('keydown', this.DOMKeydownListener)
187
+ }
188
+
189
+ cleanupEventListeners() {
190
+ document.removeEventListener('click', this.DOMClickListener)
191
+ document.removeEventListener('keydown', this.DOMKeydownListener)
192
+ }
193
+ }
@@ -0,0 +1,135 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import {
3
+ computePosition,
4
+ flip,
5
+ shift,
6
+ offset,
7
+ autoUpdate,
8
+ } from '@floating-ui/dom'
9
+
10
+ export default class extends Controller {
11
+ static targets = ['trigger', 'contentWrapper', 'content']
12
+
13
+ connect() {
14
+ this.DOMClickListener = this.onDOMClick.bind(this)
15
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
16
+
17
+ if (this.triggerTarget.dataset.asChild === 'false') {
18
+ this.triggerTarget.firstElementChild.addEventListener('focus', () => {
19
+ this.open()
20
+ })
21
+
22
+ this.triggerTarget.firstElementChild.addEventListener('blur', () => {
23
+ this.close()
24
+ })
25
+ } else {
26
+ this.triggerTarget.addEventListener('focus', () => {
27
+ this.open()
28
+ })
29
+
30
+ this.triggerTarget.addEventListener('blur', () => {
31
+ this.close()
32
+ })
33
+ }
34
+ }
35
+
36
+ toggle() {
37
+ if (this.isOpen()) {
38
+ this.close()
39
+ } else {
40
+ this.openWithDelay()
41
+ }
42
+ }
43
+
44
+ isOpen() {
45
+ return this.triggerTarget.dataset.state === 'open'
46
+ }
47
+
48
+ open() {
49
+ this.contentWrapperTarget.classList.remove('hidden')
50
+ this.triggerTarget.dataset.state = 'open'
51
+ this.contentTarget.dataset.state = 'open'
52
+ this.setupEventListeners()
53
+
54
+ this.cleanup = autoUpdate(
55
+ this.triggerTarget,
56
+ this.contentWrapperTarget,
57
+ () => {
58
+ computePosition(this.triggerTarget, this.contentWrapperTarget, {
59
+ placement: this.element.dataset.side,
60
+ strategy: 'fixed',
61
+ middleware: [flip(), shift(), offset(4)],
62
+ }).then(({ x, y }) => {
63
+ Object.assign(this.contentWrapperTarget.style, {
64
+ left: `${x}px`,
65
+ top: `${y}px`,
66
+ })
67
+ })
68
+ },
69
+ )
70
+ }
71
+
72
+ close() {
73
+ if (!this.isOpen()) return
74
+
75
+ this.triggerTarget.dataset.state = 'closed'
76
+ this.contentTarget.dataset.state = 'closed'
77
+ this.cleanup()
78
+ this.cleanupEventListeners()
79
+
80
+ setTimeout(() => {
81
+ this.contentWrapperTarget.classList.add('hidden')
82
+ }, 100)
83
+ }
84
+
85
+ clearOpenTimer() {
86
+ window.clearTimeout(this.openTimer)
87
+ }
88
+
89
+ clearCloseTimer() {
90
+ window.clearTimeout(this.closeTimer)
91
+ }
92
+
93
+ openWithDelay() {
94
+ this.clearCloseTimer()
95
+
96
+ this.openTimer = setTimeout(() => {
97
+ this.open()
98
+ }, 500)
99
+ }
100
+
101
+ closeWithDelay() {
102
+ window.clearTimeout(this.openTimer)
103
+
104
+ this.closeTimer = setTimeout(() => {
105
+ this.close()
106
+ }, 300)
107
+ }
108
+
109
+ // Global listeners
110
+ onDOMClick(event) {
111
+ if (!this.isOpen()) return
112
+ if (this.element.contains(event.target)) return
113
+
114
+ this.close()
115
+ }
116
+
117
+ onDOMKeydown(event) {
118
+ if (!this.isOpen()) return
119
+ const key = event.key
120
+
121
+ if (key === 'Escape') {
122
+ this.close()
123
+ }
124
+ }
125
+
126
+ setupEventListeners() {
127
+ document.addEventListener('click', this.DOMClickListener)
128
+ document.addEventListener('keydown', this.DOMKeydownListener)
129
+ }
130
+
131
+ cleanupEventListeners() {
132
+ document.removeEventListener('click', this.DOMClickListener)
133
+ document.removeEventListener('keydown', this.DOMKeydownListener)
134
+ }
135
+ }
@@ -0,0 +1,15 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ const el = this.element
6
+ const form = el.closest('form')
7
+
8
+ if (form && form.dataset.turbo === 'false') {
9
+ form.addEventListener('submit', () => {
10
+ form.ariaBusy = true
11
+ el.disabled = true
12
+ })
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,124 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import {
3
+ computePosition,
4
+ flip,
5
+ shift,
6
+ offset,
7
+ autoUpdate,
8
+ } from '@floating-ui/dom'
9
+
10
+ export default class extends Controller {
11
+ static targets = ['trigger', 'contentWrapper', 'content', 'menuItem']
12
+
13
+ connect() {
14
+ this.DOMClickListener = this.onDOMClick.bind(this)
15
+ this.DOMKeydownListener = this.onDOMKeydown.bind(this)
16
+
17
+ this.focusableElements = this.contentTarget.querySelectorAll(
18
+ 'button, [href], input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])',
19
+ )
20
+
21
+ this.firstElement = this.focusableElements[0]
22
+ this.lastElement = this.focusableElements[this.focusableElements.length - 1]
23
+ }
24
+
25
+ toggle() {
26
+ if (this.isOpen()) {
27
+ this.close()
28
+ } else {
29
+ this.open()
30
+ }
31
+ }
32
+
33
+ onDOMClick(event) {
34
+ if (!this.isOpen()) return
35
+ if (this.element.contains(event.target)) return
36
+
37
+ this.close()
38
+ }
39
+
40
+ onDOMKeydown(event) {
41
+ if (!this.isOpen()) return
42
+
43
+ const key = event.key
44
+
45
+ if (key === 'Escape') {
46
+ this.close()
47
+ } else if (key === 'Tab') {
48
+ // If Shift + Tab pressed on first element, go to last element
49
+ if (event.shiftKey && document.activeElement === this.firstElement) {
50
+ event.preventDefault()
51
+ this.lastElement.focus()
52
+ }
53
+ // If Tab pressed on last element, go to first element
54
+ else if (!event.shiftKey && document.activeElement === this.lastElement) {
55
+ event.preventDefault()
56
+ this.firstElement.focus()
57
+ }
58
+ }
59
+ }
60
+
61
+ isOpen() {
62
+ return this.triggerTarget.ariaExpanded === 'true'
63
+ }
64
+
65
+ open() {
66
+ this.contentWrapperTarget.classList.remove('hidden')
67
+ this.triggerTarget.ariaExpanded = true
68
+ this.contentTarget.dataset.state = 'open'
69
+ this.setupEventListeners()
70
+
71
+ setTimeout(() => {
72
+ this.firstElement.focus()
73
+ }, 100)
74
+
75
+ this.cleanup = autoUpdate(
76
+ this.triggerTarget,
77
+ this.contentWrapperTarget,
78
+ () => {
79
+ computePosition(this.triggerTarget, this.contentWrapperTarget, {
80
+ placement: this.element.dataset.side,
81
+ strategy: 'fixed',
82
+ middleware: [flip(), shift(), offset(4)],
83
+ }).then(({ x, y }) => {
84
+ Object.assign(this.contentWrapperTarget.style, {
85
+ left: `${x}px`,
86
+ top: `${y}px`,
87
+ })
88
+ })
89
+ },
90
+ )
91
+ }
92
+
93
+ close() {
94
+ this.contentTarget.dataset.state = 'closed'
95
+ this.triggerTarget.ariaExpanded = false
96
+ this.cleanup()
97
+ this.cleanupEventListeners()
98
+
99
+ setTimeout(() => {
100
+ this.contentWrapperTarget.classList.add('hidden')
101
+ }, 100)
102
+
103
+ this.focusTrigger()
104
+ }
105
+
106
+ focusTrigger() {
107
+ if (this.triggerTarget.dataset.asChild === 'false') {
108
+ this.triggerTarget.firstElementChild.focus()
109
+ } else {
110
+ this.triggerTarget.focus()
111
+ }
112
+ }
113
+
114
+ // Global listeners
115
+ setupEventListeners() {
116
+ document.addEventListener('click', this.DOMClickListener)
117
+ document.addEventListener('keydown', this.DOMKeydownListener)
118
+ }
119
+
120
+ cleanupEventListeners() {
121
+ document.removeEventListener('click', this.DOMClickListener)
122
+ document.removeEventListener('keydown', this.DOMKeydownListener)
123
+ }
124
+ }
@@ -0,0 +1,14 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static targets = ['bar']
5
+
6
+ static values = {
7
+ progress: Number,
8
+ }
9
+
10
+ progressValueChanged(value) {
11
+ this.element.setAttribute('aria-valuenow', value)
12
+ this.barTarget.style.transform = `translateX(-${100 - value}%)`
13
+ }
14
+ }