poetry-core 0.0.2

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 (166) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/DESIGN.md +145 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +30 -0
  6. data/THIRD_PARTY_NOTICES.md +507 -0
  7. data/app/components/poetry/core/box/component.rb +82 -0
  8. data/app/components/poetry/core/component.rb +390 -0
  9. data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
  10. data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
  11. data/app/components/poetry/core/concerns/introspection.rb +533 -0
  12. data/app/components/poetry/core/concerns/options.rb +262 -0
  13. data/app/components/poetry/core/concerns/parts.rb +148 -0
  14. data/app/components/poetry/core/concerns/stimulus.rb +363 -0
  15. data/app/components/poetry/core/concerns/styles.rb +323 -0
  16. data/app/components/poetry/core/style.rb +150 -0
  17. data/app/components/poetry/core/wrapper/component.rb +90 -0
  18. data/app/javascript/poetry/core/accordion_controller.js +165 -0
  19. data/app/javascript/poetry/core/action_bar_controller.js +109 -0
  20. data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
  21. data/app/javascript/poetry/core/calendar_controller.js +406 -0
  22. data/app/javascript/poetry/core/carousel_controller.js +167 -0
  23. data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
  24. data/app/javascript/poetry/core/checked_controller.js +208 -0
  25. data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
  26. data/app/javascript/poetry/core/combobox_controller.js +1095 -0
  27. data/app/javascript/poetry/core/command_controller.js +510 -0
  28. data/app/javascript/poetry/core/context_menu_controller.js +204 -0
  29. data/app/javascript/poetry/core/date_field_controller.js +584 -0
  30. data/app/javascript/poetry/core/date_picker_controller.js +111 -0
  31. data/app/javascript/poetry/core/deferred_controller.js +111 -0
  32. data/app/javascript/poetry/core/dialog_controller.js +206 -0
  33. data/app/javascript/poetry/core/dismissable_controller.js +182 -0
  34. data/app/javascript/poetry/core/drawer_controller.js +365 -0
  35. data/app/javascript/poetry/core/file_input_controller.js +191 -0
  36. data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
  37. data/app/javascript/poetry/core/helpers/announce.js +228 -0
  38. data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
  39. data/app/javascript/poetry/core/helpers/collection.js +18 -0
  40. data/app/javascript/poetry/core/helpers/direction.js +15 -0
  41. data/app/javascript/poetry/core/helpers/escape.js +38 -0
  42. data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
  43. data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
  44. data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
  45. data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
  46. data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
  47. data/app/javascript/poetry/core/helpers/mask.js +283 -0
  48. data/app/javascript/poetry/core/helpers/portal.js +217 -0
  49. data/app/javascript/poetry/core/helpers/presence.js +210 -0
  50. data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
  51. data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
  52. data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
  53. data/app/javascript/poetry/core/helpers/state.js +80 -0
  54. data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
  55. data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
  56. data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
  57. data/app/javascript/poetry/core/hotkey_controller.js +55 -0
  58. data/app/javascript/poetry/core/hover_card_controller.js +431 -0
  59. data/app/javascript/poetry/core/index.js +218 -0
  60. data/app/javascript/poetry/core/mask_controller.js +500 -0
  61. data/app/javascript/poetry/core/menu_controller.js +1050 -0
  62. data/app/javascript/poetry/core/menubar_controller.js +329 -0
  63. data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
  64. data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
  65. data/app/javascript/poetry/core/number_field_controller.js +404 -0
  66. data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
  67. data/app/javascript/poetry/core/otp_controller.js +219 -0
  68. data/app/javascript/poetry/core/popover_controller.js +300 -0
  69. data/app/javascript/poetry/core/popper_controller.js +439 -0
  70. data/app/javascript/poetry/core/pressed_controller.js +74 -0
  71. data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
  72. data/app/javascript/poetry/core/radio_group_controller.js +197 -0
  73. data/app/javascript/poetry/core/resizable_controller.js +202 -0
  74. data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
  75. data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
  76. data/app/javascript/poetry/core/search_field_controller.js +95 -0
  77. data/app/javascript/poetry/core/select_controller.js +924 -0
  78. data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
  79. data/app/javascript/poetry/core/sheet_controller.js +24 -0
  80. data/app/javascript/poetry/core/sidebar_controller.js +272 -0
  81. data/app/javascript/poetry/core/slider_controller.js +419 -0
  82. data/app/javascript/poetry/core/state_controller.js +82 -0
  83. data/app/javascript/poetry/core/table_selection_controller.js +153 -0
  84. data/app/javascript/poetry/core/tabs_controller.js +151 -0
  85. data/app/javascript/poetry/core/tag_group_controller.js +134 -0
  86. data/app/javascript/poetry/core/toast_controller.js +228 -0
  87. data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
  88. data/app/javascript/poetry/core/toaster_controller.js +197 -0
  89. data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
  90. data/app/javascript/poetry/core/tooltip_controller.js +541 -0
  91. data/app/javascript/poetry/core/tree_controller.js +265 -0
  92. data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
  93. data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
  94. data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
  95. data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
  96. data/app/views/poetry/core/preview.html.erb +13 -0
  97. data/config/component_registry.yml +20 -0
  98. data/config/controllers_manifest.json +1528 -0
  99. data/config/importmap.rb +11 -0
  100. data/config/state_vocabulary.json +31 -0
  101. data/lib/active_model/type/list.rb +33 -0
  102. data/lib/active_model/type/symbol.rb +36 -0
  103. data/lib/poetry/core/check/stable_identity.rb +104 -0
  104. data/lib/poetry/core/check.rb +1445 -0
  105. data/lib/poetry/core/config.rb +286 -0
  106. data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
  107. data/lib/poetry/core/css/bem_merger.rb +45 -0
  108. data/lib/poetry/core/css/bem_reference.rb +87 -0
  109. data/lib/poetry/core/css/override_scan.rb +110 -0
  110. data/lib/poetry/core/css/resolver.rb +161 -0
  111. data/lib/poetry/core/css/safelist.rb +41 -0
  112. data/lib/poetry/core/css/tailwind_merger.rb +102 -0
  113. data/lib/poetry/core/css/template_classes.rb +102 -0
  114. data/lib/poetry/core/css/theme_coverage.rb +72 -0
  115. data/lib/poetry/core/css/var_coverage.rb +75 -0
  116. data/lib/poetry/core/css/verifier.rb +106 -0
  117. data/lib/poetry/core/design_lint.rb +720 -0
  118. data/lib/poetry/core/design_md/import.rb +250 -0
  119. data/lib/poetry/core/design_md.rb +461 -0
  120. data/lib/poetry/core/engine.rb +71 -0
  121. data/lib/poetry/core/errors.rb +53 -0
  122. data/lib/poetry/core/html/attributes.rb +667 -0
  123. data/lib/poetry/core/icons.rb +157 -0
  124. data/lib/poetry/core/llms_text.rb +354 -0
  125. data/lib/poetry/core/page_architectures.rb +304 -0
  126. data/lib/poetry/core/part_contract.rb +285 -0
  127. data/lib/poetry/core/preview/abstract.rb +121 -0
  128. data/lib/poetry/core/preview/base.rb +227 -0
  129. data/lib/poetry/core/preview/sidecarable.rb +136 -0
  130. data/lib/poetry/core/preview/template.rb +186 -0
  131. data/lib/poetry/core/recipe_items.rb +82 -0
  132. data/lib/poetry/core/registry.rb +363 -0
  133. data/lib/poetry/core/registry_address.rb +115 -0
  134. data/lib/poetry/core/registry_client.rb +217 -0
  135. data/lib/poetry/core/registry_installer.rb +234 -0
  136. data/lib/poetry/core/registry_items.rb +184 -0
  137. data/lib/poetry/core/skill_text.rb +288 -0
  138. data/lib/poetry/core/stable_id.rb +110 -0
  139. data/lib/poetry/core/stimulus/builder.rb +395 -0
  140. data/lib/poetry/core/stimulus/declarations.rb +444 -0
  141. data/lib/poetry/core/stimulus/manifest.rb +68 -0
  142. data/lib/poetry/core/stimulus/merger.rb +131 -0
  143. data/lib/poetry/core/stimulus_contract.rb +263 -0
  144. data/lib/poetry/core/tag_helper.rb +10 -0
  145. data/lib/poetry/core/template_compile.rb +74 -0
  146. data/lib/poetry/core/token_import.rb +317 -0
  147. data/lib/poetry/core/tokens/color.rb +215 -0
  148. data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
  149. data/lib/poetry/core/tokens/generator.rb +211 -0
  150. data/lib/poetry/core/tokens.rb +117 -0
  151. data/lib/poetry/core/version.rb +9 -0
  152. data/lib/poetry/core.rb +52 -0
  153. data/lib/poetry-core.rb +5 -0
  154. data/tokens/aliases.css +145 -0
  155. data/tokens/tailwind-theme.css +47 -0
  156. data/tokens/tokens.css +78 -0
  157. data/tokens/tokens.dtcg.json +779 -0
  158. data/vendor/floating-ui/LICENSE +20 -0
  159. data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
  160. data/vendor/shadcn-tailwind/LICENSE +21 -0
  161. data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
  162. data/vendor/shadcn-tailwind/tailwind.css +629 -0
  163. data/vendor/tw-animate-css/LICENSE +21 -0
  164. data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
  165. data/vendor/tw-animate-css/tw-animate.css +1 -0
  166. metadata +267 -0
@@ -0,0 +1,329 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { directionOf } from "@poetry/controllers/helpers/direction"
3
+ import { setState } from "@poetry/controllers/helpers/state"
4
+
5
+ // The Menubar cross-menu COORDINATOR - kept
6
+ // deliberately anorexic: ONE piece of state (`value`, which menu is open)
7
+ // plus the three behaviors no other layer can own. The ownership split:
8
+ // - roving-focus (horizontal, manageTabindex TRUE) owns which trigger is
9
+ // tabbable/focused on the bar - it knows nothing about menus.
10
+ // - poetry--core--menu (one instance per menu, modal: false) owns
11
+ // everything inside an open menu - it knows nothing about siblings.
12
+ // - this controller owns:
13
+ // 1. TOGGLE - pointerdown opens (closing any sibling) / closes the open
14
+ // menu. Keyboard open (ArrowDown/Enter/Space -> first item; ArrowUp ->
15
+ // last item) rides the family's open-reason contract; pointer-open
16
+ // leaves focus on the trigger (pointer users keep their context).
17
+ // 2. HOVER-SLIDE - pointerenter on a sibling trigger is a no-op from cold;
18
+ // once ANY menu is open it swaps to the hovered menu (the gated-hover
19
+ // rule) and focus moves to the new trigger.
20
+ // 3. EDGE-NAVIGATE - the family menu controller fires a cancelable
21
+ // poetry:menu:edge-navigate when ArrowLeft/Right has no submenu
22
+ // meaning; this coordinator consumes it (loop-aware, RTL-aware,
23
+ // disabled-skipping) and opens the adjacent menu with its FIRST item
24
+ // focused (both directions - APG menubar). Standalone DropdownMenu
25
+ // leaves the event unconsumed.
26
+ // Dismiss (Esc/outside) and select arrive as the family's poetry:menu:closed
27
+ // - the coordinator nulls value; focus return to the trigger is the family
28
+ // focus-scope's job (its connect snapshot IS the trigger, because every
29
+ // open path here puts focus there first).
30
+ //
31
+ // A press on a sibling trigger must be TOGGLE's, not the open menu's
32
+ // dismissable layer's: the coordinator vetoes interact-outside for presses
33
+ // landing on this bar's triggers (otherwise dismiss-then-toggle would
34
+ // close-and-reopen in the same pointerdown).
35
+ const TRIGGER_SELECTOR = '[data-slot="menubar-trigger"]'
36
+ const MENU = "poetry--core--menu"
37
+ const MENU_SCOPE_SELECTOR = `[data-controller~="${MENU}"]`
38
+ const EVENT_PREFIX = "poetry:menubar"
39
+
40
+ export default class MenubarController extends Controller {
41
+ // The events this controller dispatches (manifest surface;
42
+ // events_declaration.test.js enforces the list stays honest).
43
+ static events = ["poetry:menubar:value-changed"]
44
+
45
+ static values = {
46
+ value: { type: String, default: "" },
47
+ loop: { type: Boolean, default: false }
48
+ }
49
+
50
+ #connected = false
51
+ #swapping = false
52
+ #onInteractOutside = (event) => {
53
+ const target = event.detail?.originalEvent?.target
54
+ const trigger = target instanceof Element ? target.closest(TRIGGER_SELECTOR) : null
55
+
56
+ if (trigger && this.element.contains(trigger)) event.preventDefault()
57
+ }
58
+
59
+ /**
60
+ * Wires the sibling-trigger interact-outside veto and reconciles one
61
+ * microtask behind (the per-menu controllers connect after the bar).
62
+ */
63
+ connect() {
64
+ this.element.addEventListener("poetry--core--dismissable:interact-outside", this.#onInteractOutside)
65
+ this.#connected = true
66
+
67
+ // Reconcile-on-connect (controllable state): a server-rendered value
68
+ // may already mark a menu open; the DOM wins, the coordinator catches
69
+ // up. One microtask behind: the per-menu controllers connect in the
70
+ // same tree pass, AFTER the bar.
71
+ queueMicrotask(() => {
72
+ if (this.#connected) this.#reconcile(this.valueValue)
73
+ })
74
+ }
75
+
76
+ /** Unwires the veto listener. */
77
+ disconnect() {
78
+ this.element.removeEventListener("poetry--core--dismissable:interact-outside", this.#onInteractOutside)
79
+ this.#connected = false
80
+ }
81
+
82
+ /**
83
+ * Stimulus value callback - controllable state: a host (outlet / Turbo
84
+ * Stream / URL param) may own the value; flipping the attribute drives
85
+ * the same machine.
86
+ *
87
+ * @param {string} value
88
+ * @param {string} previous
89
+ */
90
+ valueValueChanged(value, previous) {
91
+ if (!this.#connected || value === previous) return
92
+
93
+ this.#reconcile(value)
94
+ }
95
+
96
+ // --- the three behaviors ---
97
+
98
+ /**
99
+ * Each trigger's pointerdown action: opens (closing any sibling) or
100
+ * closes the open menu - left button only, ctrl-click passes (the macOS
101
+ * context menu); pointer-open leaves focus on the trigger.
102
+ *
103
+ * @param {PointerEvent | MouseEvent} event
104
+ */
105
+ toggle(event) {
106
+ const trigger = event.currentTarget
107
+
108
+ if (this.#isDisabled(trigger)) return
109
+ if (event.button !== undefined && event.button !== 0) return // left button only
110
+ if (event.ctrlKey) return // macOS ctrl-click is a context menu, not a toggle
111
+
112
+ if (trigger.hasAttribute("data-popup-open")) this.#menuFor(trigger)?.close("trigger-press")
113
+ else this.#activate(trigger, "trigger-press", { openReason: "trigger-press", focus: false })
114
+ }
115
+
116
+ /**
117
+ * Each trigger's pointerenter action - the gated hover: a no-op from
118
+ * cold; once ANY menu is open it swaps to the hovered menu.
119
+ *
120
+ * @param {PointerEvent} event
121
+ */
122
+ hoverSlide(event) {
123
+ const trigger = event.currentTarget
124
+
125
+ if (!this.valueValue) return // gated hover: never opens from cold
126
+ if (this.#isDisabled(trigger) || trigger.hasAttribute("data-popup-open")) return
127
+
128
+ this.#activate(trigger, "trigger-hover", { openReason: "trigger-press", focus: false })
129
+ }
130
+
131
+ /**
132
+ * Each trigger's keydown action: Enter/Space/ArrowDown open with the
133
+ * FIRST item focused, ArrowUp with the last (the family's open-reason
134
+ * contract).
135
+ *
136
+ * @param {KeyboardEvent} event
137
+ */
138
+ triggerKeydown(event) {
139
+ const trigger = event.currentTarget
140
+
141
+ if (this.#isDisabled(trigger) || trigger.hasAttribute("data-popup-open")) return
142
+
143
+ if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
144
+ event.preventDefault() // also suppresses the button's synthetic click
145
+ this.#activate(trigger, "list-navigation", { openReason: "list-navigation", openSeed: "first", focus: true })
146
+ } else if (event.key === "ArrowUp") {
147
+ event.preventDefault()
148
+ this.#activate(trigger, "list-navigation", { openReason: "list-navigation", openSeed: "last", focus: true })
149
+ }
150
+ }
151
+
152
+ /**
153
+ * The edge-navigate action (cancelable, bubbling from the open menu's
154
+ * ROOT content). detail.direction is the PHYSICAL arrow; RTL maps it
155
+ * here. ALWAYS consumed once the menu belongs to this bar - an
156
+ * unconsumed edge would fall through to the bar's roving-focus and move
157
+ * trigger focus while the menu stays open (bar arrows are inert while
158
+ * open, by rule). At a no-loop boundary that means: consumed, no
159
+ * move.
160
+ *
161
+ * @param {CustomEvent} event
162
+ */
163
+ slideAdjacent(event) {
164
+ const trigger = this.#triggerFrom(event.target)
165
+
166
+ if (!trigger) return
167
+
168
+ event.preventDefault()
169
+
170
+ const rtl = directionOf(this.element) === "rtl"
171
+ const delta = (event.detail?.direction === "right") === !rtl ? 1 : -1
172
+ const destination = this.#adjacentTrigger(trigger, delta)
173
+
174
+ if (!destination || destination === trigger) return
175
+
176
+ this.#activate(destination, "list-navigation", { openReason: "list-navigation", openSeed: "first", focus: true })
177
+ }
178
+
179
+ /**
180
+ * The family's closed event, from any of the bar's menus: nulls the
181
+ * value unless the close is one half of an in-flight swap.
182
+ *
183
+ * @param {CustomEvent} event
184
+ */
185
+ onMenuClosed(event) {
186
+ if (this.#swapping) return
187
+
188
+ const trigger = this.#triggerFrom(event.target)
189
+
190
+ if (!trigger) return
191
+ if (!this.valueValue || this.#valueOf(trigger) !== this.valueValue) return
192
+
193
+ const previous = this.valueValue
194
+
195
+ this.valueValue = ""
196
+ setState(this.element, "closed")
197
+ // The menu's close reason passes through verbatim (the family vocabulary:
198
+ // escape-key / outside-press / item-press / trigger-press / none).
199
+ this.dispatch("value-changed", {
200
+ prefix: EVENT_PREFIX,
201
+ detail: { value: null, previous, reason: event.detail?.reason ?? "trigger-press" }
202
+ })
203
+ }
204
+
205
+ // --- the swap ---
206
+
207
+ // Close whatever is open (no focus restore, no value-changed spam), put
208
+ // focus + the bar tab stop on the destination trigger BEFORE opening (so
209
+ // the family focus-scope snapshots IT as the Esc return target), open.
210
+ #activate(trigger, reason, { openReason, openSeed = null, focus }) {
211
+ const previous = this.valueValue || null
212
+ const current = this.#openTrigger()
213
+
214
+ if (current && current !== trigger) {
215
+ this.#swapping = true
216
+ this.#menuFor(current)?.close("sibling-open", { restoreFocus: false })
217
+ this.#swapping = false
218
+ }
219
+
220
+ this.#stampTabStop(trigger)
221
+ trigger.focus()
222
+ this.#menuFor(trigger)?.open(openReason, { focus, seed: openSeed })
223
+
224
+ this.valueValue = this.#valueOf(trigger)
225
+ setState(this.element, "open")
226
+ this.dispatch("value-changed", {
227
+ prefix: EVENT_PREFIX,
228
+ detail: { value: this.valueValue, previous, reason }
229
+ })
230
+ }
231
+
232
+ #reconcile(value) {
233
+ const open = this.#openTrigger()
234
+
235
+ if (!value) {
236
+ if (open) this.#menuFor(open)?.close("none")
237
+
238
+ setState(this.element, "closed")
239
+ return
240
+ }
241
+
242
+ const trigger = this.#triggers().find((candidate) => this.#valueOf(candidate) === value)
243
+
244
+ if (!trigger || trigger === open) return
245
+
246
+ if (open) {
247
+ this.#swapping = true
248
+ this.#menuFor(open)?.close("sibling-open", { restoreFocus: false })
249
+ this.#swapping = false
250
+ }
251
+
252
+ this.#stampTabStop(trigger)
253
+ this.#menuFor(trigger)?.open("trigger-press", { focus: false })
254
+ setState(this.element, "open")
255
+ }
256
+
257
+ // --- the bar (DOM is the registry) ---
258
+
259
+ #triggers() {
260
+ return Array.from(this.element.querySelectorAll(TRIGGER_SELECTOR))
261
+ }
262
+
263
+ #enabledTriggers() {
264
+ return this.#triggers().filter((trigger) => !this.#isDisabled(trigger))
265
+ }
266
+
267
+ #openTrigger() {
268
+ return this.#triggers().find((trigger) => trigger.hasAttribute("data-popup-open")) ?? null
269
+ }
270
+
271
+ #adjacentTrigger(trigger, delta) {
272
+ const triggers = this.#enabledTriggers()
273
+ const index = triggers.indexOf(trigger)
274
+
275
+ if (index === -1 || triggers.length === 0) return null
276
+
277
+ const next = index + delta
278
+
279
+ if (this.loopValue) return triggers[(next + triggers.length) % triggers.length]
280
+ if (next < 0 || next >= triggers.length) return null // no loop: the edges hold
281
+
282
+ return triggers[next]
283
+ }
284
+
285
+ // One 0, rest -1 - the bar-level roving tab stop follows the open menu
286
+ // (roving-focus stamps on its own focus moves; coordinator-driven moves
287
+ // stamp here).
288
+ #stampTabStop(current) {
289
+ for (const trigger of this.#triggers()) {
290
+ trigger.setAttribute("tabindex", trigger === current ? "0" : "-1")
291
+ }
292
+ }
293
+
294
+ #valueOf(trigger) {
295
+ return trigger.dataset.value ?? ""
296
+ }
297
+
298
+ #isDisabled(trigger) {
299
+ return trigger.hasAttribute("data-disabled") || trigger.hasAttribute("disabled") ||
300
+ trigger.getAttribute("aria-disabled") === "true"
301
+ }
302
+
303
+ #menuFor(trigger) {
304
+ const scope = trigger.closest(MENU_SCOPE_SELECTOR)
305
+
306
+ return scope ? this.application.getControllerForElementAndIdentifier(scope, MENU) : null
307
+ }
308
+
309
+ // Resolve the owning trigger from any of a menu's event targets: the
310
+ // trigger itself, the content (edge-navigate dispatches from it - matched
311
+ // via the aria-controls pair), or the menu's controller scope (the family
312
+ // open/closed events dispatch from it - it wraps exactly one trigger).
313
+ #triggerFrom(target) {
314
+ if (!(target instanceof Element)) return null
315
+ if (target.matches(TRIGGER_SELECTOR)) return this.element.contains(target) ? target : null
316
+
317
+ if (target.id) {
318
+ const byControls = this.#triggers().find(
319
+ (trigger) => trigger.getAttribute("aria-controls") === target.id
320
+ )
321
+
322
+ if (byControls) return byControls
323
+ }
324
+
325
+ const nested = target.querySelector(TRIGGER_SELECTOR)
326
+
327
+ return nested && this.element.contains(nested) ? nested : null
328
+ }
329
+ }