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,53 @@
1
+ // exit アニメーション(data-[state=closed]:animate-out)を待ってから要素を実際に隠す。
2
+ // ネイティブの非表示(hidePopover / dialog.close / hidden 属性)は即時なため、
3
+ // 契約クラスの animate-out が終わる前に隠すと退出アニメーションが見えない。
4
+ //
5
+ // 使い方: 事前に data-state=closed を設定してから
6
+ // const cancel = hideAfterExit(content, () => popover.hidePopover())
7
+ // open / disconnect 時に cancel() すると、古い退出処理は何も変更しない。
8
+ /**
9
+ * @param {Element | null | undefined} element
10
+ * @param {() => void} hide
11
+ * @param {string} [exitAnimationName]
12
+ * @returns {() => void}
13
+ */
14
+ export function hideAfterExit(element, hide, exitAnimationName = "exit") {
15
+ if (!element) {
16
+ hide()
17
+ return () => {}
18
+ }
19
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
20
+ hide()
21
+ return () => {}
22
+ }
23
+
24
+ let finished = false
25
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
26
+ let timer
27
+ const cleanup = () => {
28
+ element.removeEventListener("animationend", onEnd)
29
+ element.removeEventListener("animationcancel", onEnd)
30
+ if (timer !== undefined) clearTimeout(timer)
31
+ }
32
+ const done = () => {
33
+ if (finished) return
34
+ finished = true
35
+ cleanup()
36
+ hide()
37
+ }
38
+ /** @param {Event} event */
39
+ const onEnd = (event) => {
40
+ const animationEvent = /** @type {AnimationEvent} */ (event)
41
+ if (event.target === element && animationEvent.animationName === exitAnimationName) done()
42
+ }
43
+ // animate-out が無い環境・クラスの時のフォールバック(tw-animate の既定は 150ms)
44
+ timer = setTimeout(done, 300)
45
+ element.addEventListener("animationend", onEnd)
46
+ element.addEventListener("animationcancel", onEnd)
47
+
48
+ return () => {
49
+ if (finished) return
50
+ finished = true
51
+ cleanup()
52
+ }
53
+ }
@@ -0,0 +1,65 @@
1
+ // 手書きESM。importmapではengineのpin、bundlerでは同じdirectoryの
2
+ // package.jsonの固有名をlocal dependencyとして解決する。
3
+ // Stimulus本体はホストのものを利用する(Application登録時に受け取る)。
4
+ //
5
+ // ホスト側での登録(importmap-rails利用時。エンジンが自動pin):
6
+ // import { register } from "@supermomonga/shadcn-view-components"
7
+ // register(application)
8
+ //
9
+ // コントローラ規約(05-stimulus-hotwire §2):
10
+ // 識別子は shadcn--<component>、ターゲット名は data-slot と同一。
11
+ // コントローラはSSRされたDOMの属性・クラスを変化させるだけ(構造は再構築しない)。
12
+ //
13
+ // NOTE: コントローラのimportはpackage固有名のベア指定子で統一する。
14
+ // importmap環境ではengineのpin、bundler環境ではpackageのself-referenceが解決する。
15
+ // digest付きassetとNode module resolutionのどちらでも同じentry pointを使える。
16
+ import AccordionController from "@supermomonga/shadcn-view-components/controllers/accordion_controller"
17
+ import CalendarController from "@supermomonga/shadcn-view-components/controllers/calendar_controller"
18
+ import CarouselController from "@supermomonga/shadcn-view-components/controllers/carousel_controller"
19
+ import CheckedStateController from "@supermomonga/shadcn-view-components/controllers/checked_state_controller"
20
+ import ComboboxController from "@supermomonga/shadcn-view-components/controllers/combobox_controller"
21
+ import CommandController from "@supermomonga/shadcn-view-components/controllers/command_controller"
22
+ import DialogController from "@supermomonga/shadcn-view-components/controllers/dialog_controller"
23
+ import HoverCardController from "@supermomonga/shadcn-view-components/controllers/hover_card_controller"
24
+ import InputOTPController from "@supermomonga/shadcn-view-components/controllers/input_otp_controller"
25
+ import MenuController from "@supermomonga/shadcn-view-components/controllers/menu_controller"
26
+ import MenubarController from "@supermomonga/shadcn-view-components/controllers/menubar_controller"
27
+ import MessageScrollerController from "@supermomonga/shadcn-view-components/controllers/message_scroller_controller"
28
+ import NavigationMenuController from "@supermomonga/shadcn-view-components/controllers/navigation_menu_controller"
29
+ import PopoverController from "@supermomonga/shadcn-view-components/controllers/popover_controller"
30
+ import ResizableController from "@supermomonga/shadcn-view-components/controllers/resizable_controller"
31
+ import SelectController from "@supermomonga/shadcn-view-components/controllers/select_controller"
32
+ import SidebarController from "@supermomonga/shadcn-view-components/controllers/sidebar_controller"
33
+ import SliderController from "@supermomonga/shadcn-view-components/controllers/slider_controller"
34
+ import TabsController from "@supermomonga/shadcn-view-components/controllers/tabs_controller"
35
+ import ToggleController from "@supermomonga/shadcn-view-components/controllers/toggle_controller"
36
+ import TooltipController from "@supermomonga/shadcn-view-components/controllers/tooltip_controller"
37
+ import ToggleGroupController from "@supermomonga/shadcn-view-components/controllers/toggle_group_controller"
38
+ import ToastController from "@supermomonga/shadcn-view-components/controllers/toast_controller"
39
+
40
+ /** @param {import("@hotwired/stimulus").Application} application */
41
+ export function register(application) {
42
+ application.register("shadcn--accordion", AccordionController)
43
+ application.register("shadcn--calendar", CalendarController)
44
+ application.register("shadcn--carousel", CarouselController)
45
+ application.register("shadcn--checked-state", CheckedStateController)
46
+ application.register("shadcn--combobox", ComboboxController)
47
+ application.register("shadcn--command", CommandController)
48
+ application.register("shadcn--dialog", DialogController)
49
+ application.register("shadcn--hover-card", HoverCardController)
50
+ application.register("shadcn--input-otp", InputOTPController)
51
+ application.register("shadcn--menu", MenuController)
52
+ application.register("shadcn--menubar", MenubarController)
53
+ application.register("shadcn--message-scroller", MessageScrollerController)
54
+ application.register("shadcn--navigation-menu", NavigationMenuController)
55
+ application.register("shadcn--popover", PopoverController)
56
+ application.register("shadcn--resizable", ResizableController)
57
+ application.register("shadcn--select", SelectController)
58
+ application.register("shadcn--sidebar", SidebarController)
59
+ application.register("shadcn--slider", SliderController)
60
+ application.register("shadcn--tabs", TabsController)
61
+ application.register("shadcn--toggle", ToggleController)
62
+ application.register("shadcn--toggle-group", ToggleGroupController)
63
+ application.register("shadcn--tooltip", TooltipController)
64
+ application.register("shadcn--toast", ToastController)
65
+ }
@@ -0,0 +1,352 @@
1
+ import { startFloatingPosition } from "@supermomonga/shadcn-view-components/floating_position"
2
+ import { hideAfterExit } from "@supermomonga/shadcn-view-components/hide_after_exit"
3
+ import { applyStateAttrs } from "@supermomonga/shadcn-view-components/state_attrs"
4
+
5
+ const MENU_ROOT_SELECTOR = [
6
+ "[data-controller~='shadcn--menu']",
7
+ "[data-controller~='shadcn--menubar']",
8
+ "[data-controller~='shadcn--navigation-menu']",
9
+ ].join(",")
10
+
11
+ let associationSequence = 0
12
+
13
+ /** @type {Set<MenuPopoverManager>} */
14
+ const connectedManagers = new Set()
15
+
16
+ /** @param {HTMLElement} root @param {Element} element */
17
+ export function isOwnedMenuElement(root, element) {
18
+ return element.closest(MENU_ROOT_SELECTOR) === root
19
+ }
20
+
21
+ /**
22
+ * 同じメニュー族controllerのrootに属する要素だけを返す。
23
+ * 入れ子のDropdownMenu等は、それ自身のrootが最寄りになるため混入しない。
24
+ *
25
+ * @param {HTMLElement} root
26
+ * @param {string} selector
27
+ * @returns {HTMLElement[]}
28
+ */
29
+ export function ownedMenuElements(root, selector) {
30
+ return /** @type {HTMLElement[]} */ (
31
+ [...root.querySelectorAll(selector)].filter((element) => (
32
+ element instanceof HTMLElement && isOwnedMenuElement(root, element)
33
+ ))
34
+ )
35
+ }
36
+
37
+ /**
38
+ * scope直下の意味的な部品を返す。Portal等の機能ラッパーは許容しつつ、
39
+ * 同種の内側scopeに属する要素は除外する。
40
+ *
41
+ * @param {HTMLElement} root
42
+ * @param {HTMLElement} scope
43
+ * @param {string} scopeSelector
44
+ * @param {string} selector
45
+ * @returns {HTMLElement | null}
46
+ */
47
+ export function scopedMenuElement(root, scope, scopeSelector, selector) {
48
+ return ownedMenuElements(root, selector).find((element) => element.closest(scopeSelector) === scope) ?? null
49
+ }
50
+
51
+ /**
52
+ * 現在のrole=menuに直接属する操作項目だけを返す。閉じたサブメニューの項目は含めない。
53
+ *
54
+ * @param {HTMLElement} root
55
+ * @param {HTMLElement} menu
56
+ * @returns {HTMLElement[]}
57
+ */
58
+ export function directMenuItems(root, menu) {
59
+ return ownedMenuElements(root, "[role^='menuitem']")
60
+ .filter((item) => item.closest("[role='menu']") === menu)
61
+ .filter((item) => !item.hasAttribute("disabled") && item.getAttribute("aria-disabled") !== "true")
62
+ }
63
+
64
+ /**
65
+ * role=menuitemの既定要素(div)にも、ネイティブ操作要素と同じ
66
+ * Enter/Space活性化を与える。既定動作を止めてclickへ一本化するため、
67
+ * button/linkへタグを差し替えた場合も二重発火しない。
68
+ *
69
+ * @param {KeyboardEvent} event
70
+ * @param {HTMLElement | undefined} item
71
+ * @returns {boolean}
72
+ */
73
+ export function activateMenuItemFromKeyboard(event, item) {
74
+ if (!item || !["Enter", " ", "Spacebar"].includes(event.key)) return false
75
+
76
+ event.preventDefault()
77
+ item.click()
78
+ return true
79
+ }
80
+
81
+ /**
82
+ * triggerとcontentをID/ARIAで一対一に関連付ける。利用者が指定したIDは維持する。
83
+ *
84
+ * @param {HTMLElement} trigger
85
+ * @param {HTMLElement} content
86
+ * @param {string} prefix
87
+ * @param {{labelContent?: boolean}} [options]
88
+ */
89
+ export function associateMenuElements(trigger, content, prefix, options = {}) {
90
+ ensureId(trigger, `${prefix}-trigger`)
91
+ ensureId(content, `${prefix}-content`)
92
+ trigger.setAttribute("aria-controls", content.id)
93
+ if (options.labelContent !== false) content.setAttribute("aria-labelledby", trigger.id)
94
+ }
95
+
96
+ /**
97
+ * @param {HTMLElement | undefined} trigger
98
+ * @param {"open" | "closed"} state
99
+ * @param {{expanded?: boolean}} [options]
100
+ */
101
+ export function applyMenuTriggerState(trigger, state, options = {}) {
102
+ if (!trigger) return
103
+
104
+ if (options.expanded === false) trigger.removeAttribute("aria-expanded")
105
+ else trigger.setAttribute("aria-expanded", String(state === "open"))
106
+ trigger.dataset.state = state
107
+ trigger.toggleAttribute("data-popup-open", state === "open")
108
+ applyStateAttrs(trigger, state)
109
+ }
110
+
111
+ /**
112
+ * Popover APIの開閉、退出アニメーション、位置決めだけを管理する。
113
+ * 項目探索・キーボード操作・ARIA状態の意味付けは各controllerが担う。
114
+ */
115
+ export class MenuPopoverManager {
116
+ /**
117
+ * @param {HTMLElement} root
118
+ * @param {(popover: HTMLElement, state: "open" | "closed") => void} onStateChange
119
+ */
120
+ constructor(root, onStateChange) {
121
+ this.root = root
122
+ this.onStateChange = onStateChange
123
+ }
124
+
125
+ /** @type {HTMLElement[]} */
126
+ popovers = []
127
+
128
+ /** @type {Map<HTMLElement, () => void>} */
129
+ pendingExits = new Map()
130
+
131
+ /** @type {Map<HTMLElement, "open" | "closed">} */
132
+ desiredStates = new Map()
133
+
134
+ /** @type {HTMLElement | null} */
135
+ pointerDownOpenTrigger = null
136
+
137
+ /** @type {Map<HTMLElement, { update: () => void, destroy: () => void }>} */
138
+ positionings = new Map()
139
+
140
+ /** @type {(event: Event) => void} */
141
+ onToggle = (event) => {
142
+ if (event.currentTarget instanceof HTMLElement) this.sync(event.currentTarget)
143
+ }
144
+
145
+ connect() {
146
+ this.cancelAllExits()
147
+ this.stopAllPositioning()
148
+ this.desiredStates.clear()
149
+ this.pointerDownOpenTrigger = null
150
+ this.popovers = ownedMenuElements(this.root, "[popover]")
151
+ for (const popover of this.popovers) {
152
+ popover.addEventListener("toggle", this.onToggle)
153
+ const state = popover.matches(":popover-open") ? "open" : "closed"
154
+ this.desiredStates.set(popover, state)
155
+ this.applyState(popover, state)
156
+ }
157
+ connectedManagers.add(this)
158
+ }
159
+
160
+ disconnect() {
161
+ connectedManagers.delete(this)
162
+ this.cancelAllExits()
163
+ this.stopAllPositioning()
164
+ const popovers = this.popovers
165
+ this.popovers = []
166
+ for (const popover of popovers) {
167
+ popover.removeEventListener("toggle", this.onToggle)
168
+ if (popover.matches(":popover-open")) popover.hidePopover()
169
+ this.applyState(popover, "closed")
170
+ }
171
+ this.desiredStates.clear()
172
+ this.pointerDownOpenTrigger = null
173
+ }
174
+
175
+ /**
176
+ * @param {HTMLElement} popover
177
+ * @param {import("@supermomonga/shadcn-view-components/floating_position").Anchor | Element} anchor
178
+ * @param {{side?: "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end", align?: "start" | "center" | "end", sideOffset?: number, alignOffset?: number, collisionPadding?: number}} [placement]
179
+ */
180
+ show(popover, anchor, placement = {}) {
181
+ this.closeUnrelatedMenus()
182
+ if (popover.getAttribute("popover") === "manual") this.closeAutoPopovers(popover)
183
+ this.desiredStates.set(popover, "open")
184
+ this.cancelExit(popover)
185
+ this.applyState(popover, "open")
186
+ if (!popover.matches(":popover-open")) popover.showPopover()
187
+ this.startPositioning(popover, anchor, placement)
188
+ }
189
+
190
+ /** @param {HTMLElement} popover */
191
+ hide(popover) {
192
+ this.closeDescendantMenus(popover)
193
+ this.cancelExit(popover)
194
+ this.desiredStates.set(popover, "closed")
195
+ this.applyState(popover, "closed")
196
+ if (!popover.matches(":popover-open")) {
197
+ this.stopPositioning(popover)
198
+ return
199
+ }
200
+
201
+ let completed = false
202
+ const cancel = hideAfterExit(popover, () => {
203
+ completed = true
204
+ this.pendingExits.delete(popover)
205
+ if (this.desiredStates.get(popover) === "open") return
206
+ this.stopPositioning(popover)
207
+ popover.hidePopover()
208
+ })
209
+ if (!completed) this.pendingExits.set(popover, cancel)
210
+ }
211
+
212
+ /** @param {HTMLElement[]} [except] */
213
+ hideAll(except = []) {
214
+ const exclusions = new Set(except)
215
+ for (const popover of this.popovers) {
216
+ if (!exclusions.has(popover) && this.isOpen(popover)) this.hide(popover)
217
+ }
218
+ }
219
+
220
+ /** @param {HTMLElement} popover */
221
+ isOpen(popover) {
222
+ return popover.matches(":popover-open") && this.desiredStates.get(popover) === "open"
223
+ }
224
+
225
+ /** @returns {HTMLElement[]} */
226
+ openPopovers() {
227
+ return this.popovers.filter((popover) => this.isOpen(popover))
228
+ }
229
+
230
+ /** @param {Node} node @returns {boolean} */
231
+ containsOpenPopover(node) {
232
+ return this.openPopovers().some((popover) => popover.contains(node))
233
+ }
234
+
235
+ beginPointerDown() {
236
+ this.pointerDownOpenTrigger = null
237
+ }
238
+
239
+ /** @param {PointerEvent} event @param {HTMLElement} popover @param {HTMLElement} trigger */
240
+ recordOpenTriggerPointerDown(event, popover, trigger) {
241
+ if (event.button === 0 && popover.getAttribute("popover") !== "manual" && this.isOpen(popover)) {
242
+ this.pointerDownOpenTrigger = trigger
243
+ }
244
+ }
245
+
246
+ /** @param {Event} event @param {HTMLElement} trigger @returns {boolean} */
247
+ consumeOpenTriggerPointer(event, trigger) {
248
+ const recorded = this.pointerDownOpenTrigger
249
+ this.pointerDownOpenTrigger = null
250
+ return event instanceof window.MouseEvent && event.detail > 0 && recorded === trigger
251
+ }
252
+
253
+ /** @param {HTMLElement} popover */
254
+ sync(popover) {
255
+ const state = popover.matches(":popover-open") ? "open" : "closed"
256
+ if (state === "open" && this.desiredStates.get(popover) === "closed" && this.pendingExits.has(popover)) return
257
+
258
+ this.desiredStates.set(popover, state)
259
+ if (state === "open") {
260
+ this.closeUnrelatedMenus()
261
+ this.cancelExit(popover)
262
+ } else {
263
+ this.cancelExit(popover)
264
+ this.stopPositioning(popover)
265
+ }
266
+ this.applyState(popover, state)
267
+ }
268
+
269
+ // manual popoverはブラウザのlight dismiss対象外なので、document内の
270
+ // メニューrootを明示的に調停する。実際に開いたpopover内の子rootだけは維持する。
271
+ closeUnrelatedMenus() {
272
+ for (const manager of connectedManagers) {
273
+ if (manager === this) continue
274
+ if (manager.root.ownerDocument !== this.root.ownerDocument) continue
275
+ // 現在開いている親popoverの中から子rootを開く場合だけ共存させる。
276
+ // DOM上の祖先というだけでは、親の別メニューへ切り替えた際に子だけが残る。
277
+ const ancestors = manager.openPopovers().filter((popover) => popover.contains(this.root))
278
+ manager.hideAll(ancestors)
279
+ }
280
+ }
281
+
282
+ /** @param {HTMLElement} popover */
283
+ closeAutoPopovers(popover) {
284
+ for (const candidate of popover.ownerDocument.querySelectorAll("[popover]")) {
285
+ if (!(candidate instanceof HTMLElement)) continue
286
+ if (candidate.getAttribute("popover") === "manual" || !candidate.matches(":popover-open")) continue
287
+ if (candidate.contains(this.root)) continue
288
+ candidate.hidePopover()
289
+ }
290
+ }
291
+
292
+ /** @param {HTMLElement} popover */
293
+ closeDescendantMenus(popover) {
294
+ for (const manager of connectedManagers) {
295
+ if (manager !== this && popover.contains(manager.root)) manager.hideAll()
296
+ }
297
+ }
298
+
299
+ /** @param {HTMLElement} popover @param {"open" | "closed"} state */
300
+ applyState(popover, state) {
301
+ popover.dataset.state = state
302
+ applyStateAttrs(popover, state)
303
+ this.onStateChange(popover, state)
304
+ }
305
+
306
+ /** @param {HTMLElement} popover */
307
+ cancelExit(popover) {
308
+ this.pendingExits.get(popover)?.()
309
+ this.pendingExits.delete(popover)
310
+ }
311
+
312
+ cancelAllExits() {
313
+ for (const cancel of this.pendingExits.values()) cancel()
314
+ this.pendingExits.clear()
315
+ }
316
+
317
+ /**
318
+ * @param {HTMLElement} popover
319
+ * @param {import("@supermomonga/shadcn-view-components/floating_position").Anchor | Element} anchor
320
+ * @param {{side?: "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end", align?: "start" | "center" | "end", sideOffset?: number, alignOffset?: number, collisionPadding?: number}} placement
321
+ */
322
+ startPositioning(popover, anchor, placement) {
323
+ this.stopPositioning(popover)
324
+ this.positionings.set(popover, startFloatingPosition({
325
+ ...placement,
326
+ anchor,
327
+ collisionPadding: placement.collisionPadding ?? 5,
328
+ floating: popover,
329
+ }))
330
+ }
331
+
332
+ /** @param {HTMLElement} popover */
333
+ stopPositioning(popover) {
334
+ this.positionings.get(popover)?.destroy()
335
+ this.positionings.delete(popover)
336
+ }
337
+
338
+ stopAllPositioning() {
339
+ for (const positioning of this.positionings.values()) positioning.destroy()
340
+ this.positionings.clear()
341
+ }
342
+ }
343
+
344
+ /** @param {HTMLElement} element @param {string} prefix */
345
+ function ensureId(element, prefix) {
346
+ if (element.id) return
347
+
348
+ do {
349
+ associationSequence += 1
350
+ element.id = `${prefix}-${associationSequence}`
351
+ } while (document.getElementById(element.id) !== element)
352
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@supermomonga/shadcn-view-components",
3
+ "private": true,
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./index.js",
7
+ "./aria_relationships": "./aria_relationships.js",
8
+ "./controllers/*": "./controllers/*.js",
9
+ "./floating_position": "./floating_position.js",
10
+ "./hide_after_exit": "./hide_after_exit.js",
11
+ "./menu_popover": "./menu_popover.js",
12
+ "./state_attrs": "./state_attrs.js"
13
+ },
14
+ "peerDependencies": {
15
+ "@hotwired/stimulus": "^3.2.0"
16
+ }
17
+ }
@@ -0,0 +1,13 @@
1
+ // base-nova の契約クラスは開閉状態を data-open / data-closed(属性の存在)で参照する
2
+ // (data-open:animate-in data-closed:animate-out 等)。
3
+ // 旧規約の data-state(open/closed)を使うコントローラが、状態を書き換えるついでに
4
+ // この属性群を同期するためのヘルパ。
5
+ /**
6
+ * @param {Element | null | undefined} element
7
+ * @param {"open" | "closed"} state
8
+ */
9
+ export function applyStateAttrs(element, state) {
10
+ if (!element) return
11
+ element.toggleAttribute("data-open", state === "open")
12
+ element.toggleAttribute("data-closed", state !== "open")
13
+ }