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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/DESIGN.md +145 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/THIRD_PARTY_NOTICES.md +507 -0
- data/app/components/poetry/core/box/component.rb +82 -0
- data/app/components/poetry/core/component.rb +390 -0
- data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
- data/app/components/poetry/core/concerns/introspection.rb +533 -0
- data/app/components/poetry/core/concerns/options.rb +262 -0
- data/app/components/poetry/core/concerns/parts.rb +148 -0
- data/app/components/poetry/core/concerns/stimulus.rb +363 -0
- data/app/components/poetry/core/concerns/styles.rb +323 -0
- data/app/components/poetry/core/style.rb +150 -0
- data/app/components/poetry/core/wrapper/component.rb +90 -0
- data/app/javascript/poetry/core/accordion_controller.js +165 -0
- data/app/javascript/poetry/core/action_bar_controller.js +109 -0
- data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
- data/app/javascript/poetry/core/calendar_controller.js +406 -0
- data/app/javascript/poetry/core/carousel_controller.js +167 -0
- data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
- data/app/javascript/poetry/core/checked_controller.js +208 -0
- data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
- data/app/javascript/poetry/core/combobox_controller.js +1095 -0
- data/app/javascript/poetry/core/command_controller.js +510 -0
- data/app/javascript/poetry/core/context_menu_controller.js +204 -0
- data/app/javascript/poetry/core/date_field_controller.js +584 -0
- data/app/javascript/poetry/core/date_picker_controller.js +111 -0
- data/app/javascript/poetry/core/deferred_controller.js +111 -0
- data/app/javascript/poetry/core/dialog_controller.js +206 -0
- data/app/javascript/poetry/core/dismissable_controller.js +182 -0
- data/app/javascript/poetry/core/drawer_controller.js +365 -0
- data/app/javascript/poetry/core/file_input_controller.js +191 -0
- data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
- data/app/javascript/poetry/core/helpers/announce.js +228 -0
- data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
- data/app/javascript/poetry/core/helpers/collection.js +18 -0
- data/app/javascript/poetry/core/helpers/direction.js +15 -0
- data/app/javascript/poetry/core/helpers/escape.js +38 -0
- data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
- data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
- data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
- data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
- data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
- data/app/javascript/poetry/core/helpers/mask.js +283 -0
- data/app/javascript/poetry/core/helpers/portal.js +217 -0
- data/app/javascript/poetry/core/helpers/presence.js +210 -0
- data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
- data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
- data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
- data/app/javascript/poetry/core/helpers/state.js +80 -0
- data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
- data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
- data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
- data/app/javascript/poetry/core/hotkey_controller.js +55 -0
- data/app/javascript/poetry/core/hover_card_controller.js +431 -0
- data/app/javascript/poetry/core/index.js +218 -0
- data/app/javascript/poetry/core/mask_controller.js +500 -0
- data/app/javascript/poetry/core/menu_controller.js +1050 -0
- data/app/javascript/poetry/core/menubar_controller.js +329 -0
- data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
- data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
- data/app/javascript/poetry/core/number_field_controller.js +404 -0
- data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
- data/app/javascript/poetry/core/otp_controller.js +219 -0
- data/app/javascript/poetry/core/popover_controller.js +300 -0
- data/app/javascript/poetry/core/popper_controller.js +439 -0
- data/app/javascript/poetry/core/pressed_controller.js +74 -0
- data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
- data/app/javascript/poetry/core/radio_group_controller.js +197 -0
- data/app/javascript/poetry/core/resizable_controller.js +202 -0
- data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
- data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
- data/app/javascript/poetry/core/search_field_controller.js +95 -0
- data/app/javascript/poetry/core/select_controller.js +924 -0
- data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
- data/app/javascript/poetry/core/sheet_controller.js +24 -0
- data/app/javascript/poetry/core/sidebar_controller.js +272 -0
- data/app/javascript/poetry/core/slider_controller.js +419 -0
- data/app/javascript/poetry/core/state_controller.js +82 -0
- data/app/javascript/poetry/core/table_selection_controller.js +153 -0
- data/app/javascript/poetry/core/tabs_controller.js +151 -0
- data/app/javascript/poetry/core/tag_group_controller.js +134 -0
- data/app/javascript/poetry/core/toast_controller.js +228 -0
- data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
- data/app/javascript/poetry/core/toaster_controller.js +197 -0
- data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
- data/app/javascript/poetry/core/tooltip_controller.js +541 -0
- data/app/javascript/poetry/core/tree_controller.js +265 -0
- data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
- data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
- data/app/views/poetry/core/preview.html.erb +13 -0
- data/config/component_registry.yml +20 -0
- data/config/controllers_manifest.json +1528 -0
- data/config/importmap.rb +11 -0
- data/config/state_vocabulary.json +31 -0
- data/lib/active_model/type/list.rb +33 -0
- data/lib/active_model/type/symbol.rb +36 -0
- data/lib/poetry/core/check/stable_identity.rb +104 -0
- data/lib/poetry/core/check.rb +1445 -0
- data/lib/poetry/core/config.rb +286 -0
- data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
- data/lib/poetry/core/css/bem_merger.rb +45 -0
- data/lib/poetry/core/css/bem_reference.rb +87 -0
- data/lib/poetry/core/css/override_scan.rb +110 -0
- data/lib/poetry/core/css/resolver.rb +161 -0
- data/lib/poetry/core/css/safelist.rb +41 -0
- data/lib/poetry/core/css/tailwind_merger.rb +102 -0
- data/lib/poetry/core/css/template_classes.rb +102 -0
- data/lib/poetry/core/css/theme_coverage.rb +72 -0
- data/lib/poetry/core/css/var_coverage.rb +75 -0
- data/lib/poetry/core/css/verifier.rb +106 -0
- data/lib/poetry/core/design_lint.rb +720 -0
- data/lib/poetry/core/design_md/import.rb +250 -0
- data/lib/poetry/core/design_md.rb +461 -0
- data/lib/poetry/core/engine.rb +71 -0
- data/lib/poetry/core/errors.rb +53 -0
- data/lib/poetry/core/html/attributes.rb +667 -0
- data/lib/poetry/core/icons.rb +157 -0
- data/lib/poetry/core/llms_text.rb +354 -0
- data/lib/poetry/core/page_architectures.rb +304 -0
- data/lib/poetry/core/part_contract.rb +285 -0
- data/lib/poetry/core/preview/abstract.rb +121 -0
- data/lib/poetry/core/preview/base.rb +227 -0
- data/lib/poetry/core/preview/sidecarable.rb +136 -0
- data/lib/poetry/core/preview/template.rb +186 -0
- data/lib/poetry/core/recipe_items.rb +82 -0
- data/lib/poetry/core/registry.rb +363 -0
- data/lib/poetry/core/registry_address.rb +115 -0
- data/lib/poetry/core/registry_client.rb +217 -0
- data/lib/poetry/core/registry_installer.rb +234 -0
- data/lib/poetry/core/registry_items.rb +184 -0
- data/lib/poetry/core/skill_text.rb +288 -0
- data/lib/poetry/core/stable_id.rb +110 -0
- data/lib/poetry/core/stimulus/builder.rb +395 -0
- data/lib/poetry/core/stimulus/declarations.rb +444 -0
- data/lib/poetry/core/stimulus/manifest.rb +68 -0
- data/lib/poetry/core/stimulus/merger.rb +131 -0
- data/lib/poetry/core/stimulus_contract.rb +263 -0
- data/lib/poetry/core/tag_helper.rb +10 -0
- data/lib/poetry/core/template_compile.rb +74 -0
- data/lib/poetry/core/token_import.rb +317 -0
- data/lib/poetry/core/tokens/color.rb +215 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
- data/lib/poetry/core/tokens/generator.rb +211 -0
- data/lib/poetry/core/tokens.rb +117 -0
- data/lib/poetry/core/version.rb +9 -0
- data/lib/poetry/core.rb +52 -0
- data/lib/poetry-core.rb +5 -0
- data/tokens/aliases.css +145 -0
- data/tokens/tailwind-theme.css +47 -0
- data/tokens/tokens.css +78 -0
- data/tokens/tokens.dtcg.json +779 -0
- data/vendor/floating-ui/LICENSE +20 -0
- data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
- data/vendor/shadcn-tailwind/LICENSE +21 -0
- data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
- data/vendor/shadcn-tailwind/tailwind.css +629 -0
- data/vendor/tw-animate-css/LICENSE +21 -0
- data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
- data/vendor/tw-animate-css/tw-animate.css +1 -0
- 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
|
+
}
|