shadcn_view_components 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +590 -0
  3. data/LICENSE +49 -0
  4. data/README.md +542 -0
  5. data/app/assets/javascripts/shadcn/aria_relationships.js +88 -0
  6. data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +241 -0
  7. data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +98 -0
  8. data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +305 -0
  9. data/app/assets/javascripts/shadcn/controllers/checked_state_controller.js +121 -0
  10. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +613 -0
  11. data/app/assets/javascripts/shadcn/controllers/command_controller.js +87 -0
  12. data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +208 -0
  13. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +145 -0
  14. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +416 -0
  15. data/app/assets/javascripts/shadcn/controllers/menu_controller.js +286 -0
  16. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +430 -0
  17. data/app/assets/javascripts/shadcn/controllers/message_scroller_controller.js +37 -0
  18. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +254 -0
  19. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +117 -0
  20. data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +256 -0
  21. data/app/assets/javascripts/shadcn/controllers/select_controller.js +491 -0
  22. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +30 -0
  23. data/app/assets/javascripts/shadcn/controllers/slider_controller.js +74 -0
  24. data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +146 -0
  25. data/app/assets/javascripts/shadcn/controllers/toast_controller.js +81 -0
  26. data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +21 -0
  27. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +37 -0
  28. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +123 -0
  29. data/app/assets/javascripts/shadcn/floating_position.js +660 -0
  30. data/app/assets/javascripts/shadcn/hide_after_exit.js +53 -0
  31. data/app/assets/javascripts/shadcn/index.js +65 -0
  32. data/app/assets/javascripts/shadcn/menu_popover.js +352 -0
  33. data/app/assets/javascripts/shadcn/package.json +17 -0
  34. data/app/assets/javascripts/shadcn/state_attrs.js +13 -0
  35. data/app/assets/stylesheets/shadcn/shadcn.css +766 -0
  36. data/app/assets/tailwind/shadcn_view_components/engine.css +11 -0
  37. data/app/components/shadcn/accordion.rb +121 -0
  38. data/app/components/shadcn/alert.rb +31 -0
  39. data/app/components/shadcn/alert_dialog.rb +165 -0
  40. data/app/components/shadcn/aspect_ratio.rb +34 -0
  41. data/app/components/shadcn/attachment.rb +86 -0
  42. data/app/components/shadcn/avatar.rb +55 -0
  43. data/app/components/shadcn/badge.rb +32 -0
  44. data/app/components/shadcn/base_component.rb +239 -0
  45. data/app/components/shadcn/breadcrumb.rb +70 -0
  46. data/app/components/shadcn/bubble.rb +48 -0
  47. data/app/components/shadcn/button.html.erb +1 -0
  48. data/app/components/shadcn/button.rb +41 -0
  49. data/app/components/shadcn/button_group.rb +41 -0
  50. data/app/components/shadcn/button_styled.rb +51 -0
  51. data/app/components/shadcn/calendar.rb +276 -0
  52. data/app/components/shadcn/card.rb +18 -0
  53. data/app/components/shadcn/carousel.rb +236 -0
  54. data/app/components/shadcn/chart.rb +57 -0
  55. data/app/components/shadcn/checkbox.rb +64 -0
  56. data/app/components/shadcn/collapsible.rb +25 -0
  57. data/app/components/shadcn/combobox.rb +552 -0
  58. data/app/components/shadcn/command.rb +149 -0
  59. data/app/components/shadcn/context_menu.rb +79 -0
  60. data/app/components/shadcn/dialog.rb +180 -0
  61. data/app/components/shadcn/direction_provider.rb +18 -0
  62. data/app/components/shadcn/drawer.rb +150 -0
  63. data/app/components/shadcn/dropdown_menu.rb +242 -0
  64. data/app/components/shadcn/empty.rb +40 -0
  65. data/app/components/shadcn/field.rb +106 -0
  66. data/app/components/shadcn/floating_position_options.rb +132 -0
  67. data/app/components/shadcn/form.rb +105 -0
  68. data/app/components/shadcn/hover_card.rb +66 -0
  69. data/app/components/shadcn/input.rb +13 -0
  70. data/app/components/shadcn/input_group/button.rb +48 -0
  71. data/app/components/shadcn/input_group.rb +51 -0
  72. data/app/components/shadcn/input_otp.rb +265 -0
  73. data/app/components/shadcn/item.rb +108 -0
  74. data/app/components/shadcn/kbd.rb +9 -0
  75. data/app/components/shadcn/label.rb +12 -0
  76. data/app/components/shadcn/marker.rb +37 -0
  77. data/app/components/shadcn/menubar.rb +110 -0
  78. data/app/components/shadcn/message.rb +29 -0
  79. data/app/components/shadcn/message_scroller.rb +52 -0
  80. data/app/components/shadcn/native_checked_state.rb +74 -0
  81. data/app/components/shadcn/native_select.rb +78 -0
  82. data/app/components/shadcn/navigation_menu.rb +122 -0
  83. data/app/components/shadcn/pagination.rb +160 -0
  84. data/app/components/shadcn/popover.rb +68 -0
  85. data/app/components/shadcn/preview_base.rb +107 -0
  86. data/app/components/shadcn/progress.rb +49 -0
  87. data/app/components/shadcn/radio_group.rb +74 -0
  88. data/app/components/shadcn/resizable.rb +77 -0
  89. data/app/components/shadcn/scroll_area.rb +59 -0
  90. data/app/components/shadcn/select.rb +356 -0
  91. data/app/components/shadcn/separator.rb +42 -0
  92. data/app/components/shadcn/sheet.rb +157 -0
  93. data/app/components/shadcn/sidebar.rb +332 -0
  94. data/app/components/shadcn/skeleton.rb +13 -0
  95. data/app/components/shadcn/slider.rb +279 -0
  96. data/app/components/shadcn/sonner.rb +30 -0
  97. data/app/components/shadcn/spinner.rb +32 -0
  98. data/app/components/shadcn/switch.rb +79 -0
  99. data/app/components/shadcn/table.rb +46 -0
  100. data/app/components/shadcn/tabs.rb +119 -0
  101. data/app/components/shadcn/textarea.rb +7 -0
  102. data/app/components/shadcn/toggle.rb +66 -0
  103. data/app/components/shadcn/toggle_group.rb +127 -0
  104. data/app/components/shadcn/tooltip.rb +74 -0
  105. data/config/importmap.rb +5 -0
  106. data/lib/generators/shadcn_view_components/install_generator.rb +182 -0
  107. data/lib/shadcn_view_components/classes.rb +89 -0
  108. data/lib/shadcn_view_components/component_naming.rb +21 -0
  109. data/lib/shadcn_view_components/contracts/calendar.rb +95 -0
  110. data/lib/shadcn_view_components/engine.rb +56 -0
  111. data/lib/shadcn_view_components/generated/contracts/accordion.rb +112 -0
  112. data/lib/shadcn_view_components/generated/contracts/alert-dialog.rb +300 -0
  113. data/lib/shadcn_view_components/generated/contracts/alert.rb +104 -0
  114. data/lib/shadcn_view_components/generated/contracts/aspect-ratio.rb +34 -0
  115. data/lib/shadcn_view_components/generated/contracts/attachment.rb +224 -0
  116. data/lib/shadcn_view_components/generated/contracts/avatar.rb +149 -0
  117. data/lib/shadcn_view_components/generated/contracts/badge.rb +39 -0
  118. data/lib/shadcn_view_components/generated/contracts/breadcrumb.rb +172 -0
  119. data/lib/shadcn_view_components/generated/contracts/bubble.rb +112 -0
  120. data/lib/shadcn_view_components/generated/contracts/button-group.rb +81 -0
  121. data/lib/shadcn_view_components/generated/contracts/button.rb +81 -0
  122. data/lib/shadcn_view_components/generated/contracts/card.rb +172 -0
  123. data/lib/shadcn_view_components/generated/contracts/carousel.rb +133 -0
  124. data/lib/shadcn_view_components/generated/contracts/chart.rb +109 -0
  125. data/lib/shadcn_view_components/generated/contracts/checkbox.rb +35 -0
  126. data/lib/shadcn_view_components/generated/contracts/collapsible.rb +89 -0
  127. data/lib/shadcn_view_components/generated/contracts/combobox.rb +368 -0
  128. data/lib/shadcn_view_components/generated/contracts/command.rb +222 -0
  129. data/lib/shadcn_view_components/generated/contracts/context-menu.rb +378 -0
  130. data/lib/shadcn_view_components/generated/contracts/dialog.rb +258 -0
  131. data/lib/shadcn_view_components/generated/contracts/direction.rb +34 -0
  132. data/lib/shadcn_view_components/generated/contracts/drawer.rb +280 -0
  133. data/lib/shadcn_view_components/generated/contracts/dropdown-menu.rb +380 -0
  134. data/lib/shadcn_view_components/generated/contracts/empty.rb +150 -0
  135. data/lib/shadcn_view_components/generated/contracts/field.rb +244 -0
  136. data/lib/shadcn_view_components/generated/contracts/form.rb +62 -0
  137. data/lib/shadcn_view_components/generated/contracts/hover-card.rb +90 -0
  138. data/lib/shadcn_view_components/generated/contracts/input-group.rb +155 -0
  139. data/lib/shadcn_view_components/generated/contracts/input-otp.rb +106 -0
  140. data/lib/shadcn_view_components/generated/contracts/input.rb +34 -0
  141. data/lib/shadcn_view_components/generated/contracts/item.rb +251 -0
  142. data/lib/shadcn_view_components/generated/contracts/kbd.rb +57 -0
  143. data/lib/shadcn_view_components/generated/contracts/label.rb +34 -0
  144. data/lib/shadcn_view_components/generated/contracts/marker.rb +82 -0
  145. data/lib/shadcn_view_components/generated/contracts/menubar.rb +394 -0
  146. data/lib/shadcn_view_components/generated/contracts/message-scroller.rb +149 -0
  147. data/lib/shadcn_view_components/generated/contracts/message.rb +149 -0
  148. data/lib/shadcn_view_components/generated/contracts/native-select.rb +82 -0
  149. data/lib/shadcn_view_components/generated/contracts/navigation-menu.rb +195 -0
  150. data/lib/shadcn_view_components/generated/contracts/pagination.rb +176 -0
  151. data/lib/shadcn_view_components/generated/contracts/popover.rb +159 -0
  152. data/lib/shadcn_view_components/generated/contracts/progress.rb +126 -0
  153. data/lib/shadcn_view_components/generated/contracts/radio-group.rb +58 -0
  154. data/lib/shadcn_view_components/generated/contracts/resizable.rb +85 -0
  155. data/lib/shadcn_view_components/generated/contracts/scroll-area.rb +59 -0
  156. data/lib/shadcn_view_components/generated/contracts/select.rb +245 -0
  157. data/lib/shadcn_view_components/generated/contracts/separator.rb +34 -0
  158. data/lib/shadcn_view_components/generated/contracts/sheet.rb +209 -0
  159. data/lib/shadcn_view_components/generated/contracts/sidebar.rb +558 -0
  160. data/lib/shadcn_view_components/generated/contracts/skeleton.rb +34 -0
  161. data/lib/shadcn_view_components/generated/contracts/slider.rb +38 -0
  162. data/lib/shadcn_view_components/generated/contracts/sonner.rb +36 -0
  163. data/lib/shadcn_view_components/generated/contracts/spinner.rb +34 -0
  164. data/lib/shadcn_view_components/generated/contracts/switch.rb +35 -0
  165. data/lib/shadcn_view_components/generated/contracts/table.rb +199 -0
  166. data/lib/shadcn_view_components/generated/contracts/tabs.rb +104 -0
  167. data/lib/shadcn_view_components/generated/contracts/textarea.rb +34 -0
  168. data/lib/shadcn_view_components/generated/contracts/toggle-group.rb +62 -0
  169. data/lib/shadcn_view_components/generated/contracts/toggle.rb +39 -0
  170. data/lib/shadcn_view_components/generated/contracts/tooltip.rb +116 -0
  171. data/lib/shadcn_view_components/numeric_property_validation.rb +124 -0
  172. data/lib/shadcn_view_components/property_contract_definitions.rb +92 -0
  173. data/lib/shadcn_view_components/property_contracts.rb +42 -0
  174. data/lib/shadcn_view_components/property_validation.rb +55 -0
  175. data/lib/shadcn_view_components/version.rb +6 -0
  176. data/lib/shadcn_view_components.rb +21 -0
  177. metadata +446 -0
@@ -0,0 +1,305 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ import {
4
+ ensureId,
5
+ ensureRootId,
6
+ ownedElements,
7
+ setDefaultAttribute,
8
+ } from "@supermomonga/shadcn-view-components/aria_relationships"
9
+
10
+ const IDENTIFIER = "shadcn--carousel"
11
+ const POSITION_TOLERANCE = 1
12
+ const rtlScrollTypes = new WeakMap()
13
+
14
+ /** @typedef {"negative" | "positive-ascending" | "positive-descending"} RtlScrollType */
15
+
16
+ /** @param {number} value @param {number} maximum */
17
+ function clampPosition(value, maximum) {
18
+ const limit = Math.max(0, maximum)
19
+ return Math.min(limit, Math.max(0, Number.isFinite(value) ? value : 0))
20
+ }
21
+
22
+ /**
23
+ * RTLブラウザごとに異なるscrollLeftを、inline-startを0とする論理位置へ変換する。
24
+ * Safariのoverscrollを含む範囲外値も0..maximumへ丸める。
25
+ *
26
+ * @param {number} raw
27
+ * @param {number} maximum
28
+ * @param {RtlScrollType} type
29
+ */
30
+ export function logicalRtlScrollLeft(raw, maximum, type) {
31
+ if (type === "negative") return clampPosition(-raw, maximum)
32
+ if (type === "positive-descending") return clampPosition(maximum - raw, maximum)
33
+ return clampPosition(raw, maximum)
34
+ }
35
+
36
+ /** @param {number} logical @param {number} maximum @param {RtlScrollType} type */
37
+ export function rawRtlScrollLeft(logical, maximum, type) {
38
+ const position = clampPosition(logical, maximum)
39
+ if (type === "negative") return position === 0 ? 0 : -position
40
+ if (type === "positive-descending") return maximum - position
41
+ return position
42
+ }
43
+
44
+ /** @param {Document} ownerDocument @returns {RtlScrollType} */
45
+ export function detectRtlScrollType(ownerDocument) {
46
+ const viewport = ownerDocument.createElement("div")
47
+ const content = ownerDocument.createElement("div")
48
+ viewport.dir = "rtl"
49
+ viewport.style.cssText = "position:absolute;top:-9999px;width:4px;height:1px;overflow:scroll;visibility:hidden"
50
+ content.style.width = "8px"
51
+ content.style.height = "1px"
52
+ viewport.append(content)
53
+ ;(ownerDocument.body || ownerDocument.documentElement).append(viewport)
54
+
55
+ /** @type {RtlScrollType} */
56
+ let type
57
+ if (viewport.scrollLeft > 0) {
58
+ type = "positive-descending"
59
+ } else {
60
+ viewport.scrollLeft = 1
61
+ type = viewport.scrollLeft === 0 ? "negative" : "positive-ascending"
62
+ }
63
+
64
+ viewport.remove()
65
+ return type
66
+ }
67
+
68
+ /** @param {Document} ownerDocument @returns {RtlScrollType} */
69
+ function rtlScrollType(ownerDocument) {
70
+ const cached = rtlScrollTypes.get(ownerDocument)
71
+ if (cached) return cached
72
+
73
+ const detected = detectRtlScrollType(ownerDocument)
74
+ rtlScrollTypes.set(ownerDocument, detected)
75
+ return detected
76
+ }
77
+
78
+ // Carouselルートをorientation/directionの唯一の情報源とし、実レイアウトから
79
+ // 各itemの絶対snap位置を測る。生scrollLeftに依存する判断はRTL変換関数へ集約する。
80
+ /** @extends {Controller<HTMLElement>} */
81
+ export default class CarouselController extends Controller {
82
+ /** @type {HTMLElement | null} */
83
+ viewport = null
84
+
85
+ /** @type {HTMLElement | null} */
86
+ track = null
87
+
88
+ /** @type {HTMLElement[]} */
89
+ items = []
90
+
91
+ /** @type {HTMLButtonElement | null} */
92
+ previous = null
93
+
94
+ /** @type {HTMLButtonElement | null} */
95
+ next = null
96
+
97
+ /** @type {ResizeObserver | null} */
98
+ resizeObserver = null
99
+
100
+ /** @type {number[]} */
101
+ snapPositions = []
102
+
103
+ /** @type {RtlScrollType | null} */
104
+ rtlType = null
105
+
106
+ onScroll = () => this.refreshState()
107
+
108
+ onResize = () => this.refreshLayout()
109
+
110
+ connect() {
111
+ this.viewport = /** @type {HTMLElement | null} */ (
112
+ ownedElements(this.element, "[data-slot='carousel-content']", IDENTIFIER)[0] ?? null
113
+ )
114
+ this.items = ownedElements(this.element, "[data-slot='carousel-item']", IDENTIFIER)
115
+ this.previous = /** @type {HTMLButtonElement | null} */ (
116
+ ownedElements(this.element, "button[data-slot='carousel-previous']", IDENTIFIER)[0] ?? null
117
+ )
118
+ this.next = /** @type {HTMLButtonElement | null} */ (
119
+ ownedElements(this.element, "button[data-slot='carousel-next']", IDENTIFIER)[0] ?? null
120
+ )
121
+ this.track = /** @type {HTMLElement | null} */ (this.viewport?.firstElementChild ?? null)
122
+ this.rtlType = this.horizontal && this.rtl
123
+ ? rtlScrollType(this.element.ownerDocument)
124
+ : null
125
+
126
+ if (!this.viewport) {
127
+ this.refreshState()
128
+ return
129
+ }
130
+
131
+ this.ensureRelationships()
132
+ this.viewport.addEventListener("scroll", this.onScroll, { passive: true })
133
+ this.observeLayout()
134
+ this.refreshLayout()
135
+ }
136
+
137
+ disconnect() {
138
+ this.viewport?.removeEventListener("scroll", this.onScroll)
139
+ this.resizeObserver?.disconnect()
140
+
141
+ this.viewport = null
142
+ this.track = null
143
+ this.items = []
144
+ this.previous = null
145
+ this.next = null
146
+ this.resizeObserver = null
147
+ this.snapPositions = []
148
+ this.rtlType = null
149
+ }
150
+
151
+ scrollPrevious() {
152
+ this.scrollToAdjacent(-1)
153
+ }
154
+
155
+ scrollNext() {
156
+ this.scrollToAdjacent(1)
157
+ }
158
+
159
+ /** @param {KeyboardEvent} event */
160
+ navigate(event) {
161
+ if (!this.ownsEvent(event) || this.editableTarget(event.target)) return
162
+
163
+ let direction = 0
164
+ if (this.vertical) {
165
+ if (event.key === "ArrowUp") direction = -1
166
+ if (event.key === "ArrowDown") direction = 1
167
+ } else if (this.rtl) {
168
+ if (event.key === "ArrowRight") direction = -1
169
+ if (event.key === "ArrowLeft") direction = 1
170
+ } else {
171
+ if (event.key === "ArrowLeft") direction = -1
172
+ if (event.key === "ArrowRight") direction = 1
173
+ }
174
+
175
+ if (direction === 0) return
176
+ event.preventDefault()
177
+ this.scrollToAdjacent(direction)
178
+ }
179
+
180
+ /** @param {number} direction */
181
+ scrollToAdjacent(direction) {
182
+ const viewport = this.viewport
183
+ if (!viewport || this.items.length < 2) return
184
+
185
+ // action直前にも測り直し、ResizeObserver通知と同じframeで寸法が変わった場合も
186
+ // 古いitem幅・高さを使わない。
187
+ this.snapPositions = this.measureSnapPositions()
188
+ const current = this.logicalPosition
189
+ const target = direction > 0
190
+ ? this.snapPositions.find((position) => position > current + POSITION_TOLERANCE)
191
+ : [...this.snapPositions].reverse().find((position) => position < current - POSITION_TOLERANCE)
192
+ if (target === undefined) return
193
+
194
+ if (this.vertical) {
195
+ viewport.scrollTo({ behavior: "smooth", top: target })
196
+ } else {
197
+ viewport.scrollTo({ behavior: "smooth", left: this.rawHorizontalPosition(target) })
198
+ }
199
+ }
200
+
201
+ refreshLayout() {
202
+ this.snapPositions = this.measureSnapPositions()
203
+ this.refreshState()
204
+ }
205
+
206
+ refreshState() {
207
+ const movable = Boolean(this.viewport) && this.items.length > 1 && this.maximum > POSITION_TOLERANCE
208
+ const position = this.logicalPosition
209
+ if (this.previous) this.previous.disabled = !movable || position <= POSITION_TOLERANCE
210
+ if (this.next) this.next.disabled = !movable || position >= this.maximum - POSITION_TOLERANCE
211
+ }
212
+
213
+ observeLayout() {
214
+ const ownerWindow = this.element.ownerDocument.defaultView
215
+ if (!this.viewport || typeof ownerWindow?.ResizeObserver !== "function") return
216
+
217
+ this.resizeObserver = new ownerWindow.ResizeObserver(this.onResize)
218
+ this.resizeObserver.observe(this.viewport)
219
+ if (this.track) this.resizeObserver.observe(this.track)
220
+ for (const item of this.items) this.resizeObserver.observe(item)
221
+ }
222
+
223
+ ensureRelationships() {
224
+ if (!this.viewport) return
225
+
226
+ const rootId = ensureRootId(this.element, "carousel")
227
+ const viewportId = ensureId(this.viewport, `${rootId}-viewport`)
228
+ if (this.previous) setDefaultAttribute(this.previous, "aria-controls", viewportId)
229
+ if (this.next) setDefaultAttribute(this.next, "aria-controls", viewportId)
230
+ }
231
+
232
+ /** @returns {number[]} */
233
+ measureSnapPositions() {
234
+ if (!this.viewport || this.items.length === 0) return []
235
+
236
+ const first = this.items[0].getBoundingClientRect()
237
+ const positions = this.items.map((item) => {
238
+ const rect = item.getBoundingClientRect()
239
+ const distance = this.vertical
240
+ ? rect.top - first.top
241
+ : this.rtl
242
+ ? first.right - rect.right
243
+ : rect.left - first.left
244
+ return clampPosition(distance, this.maximum)
245
+ })
246
+ positions.push(0, this.maximum)
247
+ positions.sort((left, right) => left - right)
248
+
249
+ return positions.filter(
250
+ (position, index) => index === 0 || position - positions[index - 1] > POSITION_TOLERANCE,
251
+ )
252
+ }
253
+
254
+ /** @param {Event} event */
255
+ ownsEvent(event) {
256
+ const target = event.target
257
+ return target instanceof Element && target.closest(`[data-controller~='${IDENTIFIER}']`) === this.element
258
+ }
259
+
260
+ /** @param {EventTarget | null} target */
261
+ editableTarget(target) {
262
+ return target instanceof Element && Boolean(
263
+ target.closest("input, textarea, select, [contenteditable]:not([contenteditable='false'])"),
264
+ )
265
+ }
266
+
267
+ /** @param {number} logical */
268
+ rawHorizontalPosition(logical) {
269
+ if (!this.rtl || !this.rtlType) return clampPosition(logical, this.maximum)
270
+ return rawRtlScrollLeft(logical, this.maximum, this.rtlType)
271
+ }
272
+
273
+ /** @returns {number} */
274
+ get logicalPosition() {
275
+ const viewport = this.viewport
276
+ if (!viewport) return 0
277
+ if (this.vertical) return clampPosition(viewport.scrollTop, this.maximum)
278
+ if (!this.rtl || !this.rtlType) return clampPosition(viewport.scrollLeft, this.maximum)
279
+ return logicalRtlScrollLeft(viewport.scrollLeft, this.maximum, this.rtlType)
280
+ }
281
+
282
+ /** @returns {number} */
283
+ get maximum() {
284
+ const viewport = this.viewport
285
+ if (!viewport) return 0
286
+ return this.vertical
287
+ ? Math.max(0, viewport.scrollHeight - viewport.clientHeight)
288
+ : Math.max(0, viewport.scrollWidth - viewport.clientWidth)
289
+ }
290
+
291
+ /** @returns {boolean} */
292
+ get vertical() {
293
+ return this.element.dataset.orientation === "vertical"
294
+ }
295
+
296
+ /** @returns {boolean} */
297
+ get horizontal() {
298
+ return !this.vertical
299
+ }
300
+
301
+ /** @returns {boolean} */
302
+ get rtl() {
303
+ return this.element.dir === "rtl"
304
+ }
305
+ }
@@ -0,0 +1,121 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /** @param {Element} element @param {boolean} checked */
4
+ function applyDataState(element, checked) {
5
+ element.toggleAttribute("data-checked", checked)
6
+ element.toggleAttribute("data-unchecked", !checked)
7
+ }
8
+
9
+ /** @param {EventTarget | null} target @returns {target is HTMLInputElement} */
10
+ function isRadioInput(target) {
11
+ return target instanceof Element && target.matches("input[type='radio']")
12
+ }
13
+
14
+ // Checkbox・RadioGroup::Item・Switch の見た目用属性を、native checked propertyから導出する。
15
+ // checked content attributeはreset用のdefaultCheckedなので、このcontrollerからは変更しない。
16
+ /** @extends {Controller<HTMLElement>} */
17
+ export default class CheckedStateController extends Controller {
18
+ /** @type {HTMLInputElement | null} */
19
+ input = null
20
+
21
+ /** @type {HTMLFormElement | null} */
22
+ form = null
23
+
24
+ /** @type {Node | null} */
25
+ radioTree = null
26
+
27
+ /** @type {number | null} */
28
+ resetFrame = null
29
+
30
+ connectionVersion = 0
31
+
32
+ /** @param {Event} event */
33
+ onRadioGroupChange = (event) => {
34
+ const input = this.input
35
+ const changed = event.target
36
+ if (
37
+ !input ||
38
+ input.type !== "radio" ||
39
+ input.name === "" ||
40
+ !isRadioInput(changed) ||
41
+ changed === input ||
42
+ changed.name !== input.name ||
43
+ changed.form !== input.form ||
44
+ changed.getRootNode() !== this.radioTree
45
+ ) return
46
+
47
+ // checkedになったradio以外はchangeを発火しないため、同じnative groupで
48
+ // 発生したchangeを各Itemが受け取り、自分自身の現在propertyだけを反映する。
49
+ this.applyInputState(input)
50
+ }
51
+
52
+ onFormReset = () => {
53
+ const input = this.input
54
+ const form = this.form
55
+ const version = this.connectionVersion
56
+ const ownerWindow = input?.ownerDocument.defaultView
57
+ if (!input || !ownerWindow) return
58
+
59
+ // Chromeはreset event listener中に予約したmicrotaskより後でcheckedを復元する。
60
+ // 次の描画前に読むことで、既定reset後のpropertyを見た目へ反映する。
61
+ if (this.resetFrame !== null) ownerWindow.cancelAnimationFrame(this.resetFrame)
62
+ this.resetFrame = ownerWindow.requestAnimationFrame(() => {
63
+ this.resetFrame = null
64
+ if (
65
+ this.connectionVersion !== version ||
66
+ this.input !== input ||
67
+ this.form !== form ||
68
+ !input.isConnected
69
+ ) return
70
+
71
+ this.sync()
72
+ })
73
+ }
74
+
75
+ connect() {
76
+ this.connectionVersion += 1
77
+ this.input = this.element.matches("input[type='checkbox'], input[type='radio']")
78
+ ? /** @type {HTMLInputElement} */ (this.element)
79
+ : null
80
+ this.form = this.input?.form ?? null
81
+ this.radioTree = this.input?.type === "radio" && this.input.name !== ""
82
+ ? this.input.getRootNode()
83
+ : null
84
+
85
+ this.form?.addEventListener("reset", this.onFormReset)
86
+ this.radioTree?.addEventListener("change", this.onRadioGroupChange, true)
87
+ this.sync()
88
+ }
89
+
90
+ disconnect() {
91
+ this.connectionVersion += 1
92
+ const ownerWindow = this.input?.ownerDocument.defaultView
93
+ if (ownerWindow && this.resetFrame !== null) ownerWindow.cancelAnimationFrame(this.resetFrame)
94
+ this.form?.removeEventListener("reset", this.onFormReset)
95
+ this.radioTree?.removeEventListener("change", this.onRadioGroupChange, true)
96
+ this.input = null
97
+ this.form = null
98
+ this.radioTree = null
99
+ this.resetFrame = null
100
+ }
101
+
102
+ // Programmaticなchecked変更はnative eventを発火しないため、利用者は変更後に
103
+ // bubbling change eventをdispatchしてこのactionを呼ぶ。
104
+ sync() {
105
+ const input = this.input
106
+ if (!input) return
107
+
108
+ this.applyInputState(input)
109
+ }
110
+
111
+ /** @param {HTMLInputElement} input */
112
+ applyInputState(input) {
113
+ applyDataState(input, input.checked)
114
+ if (input.hasAttribute("aria-checked")) {
115
+ input.setAttribute("aria-checked", String(input.checked))
116
+ }
117
+
118
+ const mirror = input.nextElementSibling
119
+ if (mirror?.matches("[data-slot='switch-thumb']")) applyDataState(mirror, input.checked)
120
+ }
121
+ }