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,541 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { portalContent, resolvePortalContainer, restoreContent } from "@poetry/controllers/helpers/portal"
|
|
3
|
+
import { exitPresence } from "@poetry/controllers/helpers/presence"
|
|
4
|
+
import { setState, stateOf } from "@poetry/controllers/helpers/state"
|
|
5
|
+
|
|
6
|
+
// The Tooltip controller (the popper-consumer trio's timing machine): open
|
|
7
|
+
// DELAYS (provider delay_duration, default 0), the provider-scoped
|
|
8
|
+
// WARM grace (one tooltip open - or closed less than skip_delay_duration ms
|
|
9
|
+
// ago - lets siblings in the same provider scope open instantly), ONE OPEN
|
|
10
|
+
// GLOBALLY (the document-level will-open event),
|
|
11
|
+
// close-on-scroll, and the pointer-vs-focus open paths with their latches.
|
|
12
|
+
//
|
|
13
|
+
// A11y is the strictest of the trio: the content is role=tooltip and the
|
|
14
|
+
// trigger's aria-describedby is set on open and REMOVED on close
|
|
15
|
+
// (describedby must never reference hidden content); focus-scope is NOT
|
|
16
|
+
// composed at all - focus never enters a tooltip; touch NEVER opens one
|
|
17
|
+
// (no long-press path, deliberately). Esc rides a token-activated
|
|
18
|
+
// dismissable layer while open, so a tooltip above a Dialog peels first.
|
|
19
|
+
//
|
|
20
|
+
// THE WARM REGISTRY (the provider mechanism): a module-level WeakMap keyed
|
|
21
|
+
// by the [data-slot=tooltip-provider] ancestor (document fallback) holding
|
|
22
|
+
// {openCount, warmUntil}. The DOM ancestor IS the provider scope; the
|
|
23
|
+
// WeakMap lets morphed/replaced providers garbage-collect (no Turbo leaks).
|
|
24
|
+
const PROVIDER_SELECTOR = '[data-slot="tooltip-provider"]'
|
|
25
|
+
const CONTENT_SELECTOR = '[data-slot="tooltip-content"]'
|
|
26
|
+
const TRIGGER_SELECTOR = '[data-slot="tooltip-trigger"]'
|
|
27
|
+
|
|
28
|
+
const EVENT_PREFIX = "poetry:tooltip"
|
|
29
|
+
const WILL_OPEN_EVENT = "poetry:tooltip:will-open"
|
|
30
|
+
|
|
31
|
+
const DISMISSABLE = "poetry--core--dismissable"
|
|
32
|
+
const POPPER_STRATEGY = "data-poetry--core--popper-strategy-value"
|
|
33
|
+
|
|
34
|
+
// Provider defaults: delayDuration 0 (instant tooltips are the design
|
|
35
|
+
// call); skipDelayDuration 300.
|
|
36
|
+
const DEFAULT_DELAY_DURATION = 0
|
|
37
|
+
const DEFAULT_SKIP_DELAY_DURATION = 300
|
|
38
|
+
|
|
39
|
+
// Hoverable-content grace: the close-intent timer (entering the content
|
|
40
|
+
// cancels it) - the DropdownMenu grace-area reconciliation applied
|
|
41
|
+
// trio-wide; a pointer-transit polygon is the shared fast-follow.
|
|
42
|
+
const HOVERABLE_CLOSE_DELAY = 300
|
|
43
|
+
|
|
44
|
+
// scope element -> { openCount, warmUntil } (module-level, shared by every
|
|
45
|
+
// tooltip instance; WeakMap so detached providers garbage-collect).
|
|
46
|
+
const warmScopes = new WeakMap()
|
|
47
|
+
|
|
48
|
+
const scopeFor = (element) => {
|
|
49
|
+
if (!warmScopes.has(element)) warmScopes.set(element, { openCount: 0, warmUntil: 0 })
|
|
50
|
+
|
|
51
|
+
return warmScopes.get(element)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default class TooltipController extends Controller {
|
|
55
|
+
// The events this controller dispatches (manifest surface;
|
|
56
|
+
// events_declaration.test.js enforces the list stays honest).
|
|
57
|
+
static events = ["poetry:tooltip:closed", "poetry:tooltip:open"]
|
|
58
|
+
|
|
59
|
+
static values = {
|
|
60
|
+
open: { type: Boolean, default: false },
|
|
61
|
+
delayDuration: { type: Number, default: -1 }, // -1 = inherit the provider (default 0)
|
|
62
|
+
disableHoverableContent: { type: Boolean, default: false } // unset = inherit the provider
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#connected = false
|
|
66
|
+
#wired = []
|
|
67
|
+
#openTimer = null
|
|
68
|
+
#closeTimer = null
|
|
69
|
+
#cancelExit = null
|
|
70
|
+
#hasPointerMoveOpened = false
|
|
71
|
+
#isPointerDown = false
|
|
72
|
+
#emittingWillOpen = false
|
|
73
|
+
#scrollListening = false
|
|
74
|
+
#onScroll = (event) => this.#handleScroll(event)
|
|
75
|
+
#onWillOpen = (event) => this.#handleWillOpen(event)
|
|
76
|
+
#onPointerup = () => { this.#isPointerDown = false }
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wires the content listeners plus the document-level will-open
|
|
80
|
+
* listener, and reconciles a server-pinned tooltip (describedby, layer,
|
|
81
|
+
* scope count catch up; the DOM wins).
|
|
82
|
+
*/
|
|
83
|
+
connect() {
|
|
84
|
+
const content = this.#content()
|
|
85
|
+
|
|
86
|
+
if (content) this.#wireContent(content)
|
|
87
|
+
|
|
88
|
+
this.#listen(document, WILL_OPEN_EVENT, this.#onWillOpen)
|
|
89
|
+
|
|
90
|
+
this.#connected = true
|
|
91
|
+
|
|
92
|
+
// Reconcile-on-connect: a server-rendered pinned tooltip (data-open on
|
|
93
|
+
// the content) activates describedby + the layer + the scope count.
|
|
94
|
+
if (this.#isOpen()) {
|
|
95
|
+
this.#activate(content)
|
|
96
|
+
this.openValue = true
|
|
97
|
+
} else if (this.openValue) {
|
|
98
|
+
this.#open({ instant: "delay" })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clears the timers, balances the warm-scope count for an open tooltip,
|
|
104
|
+
* drops the scroll listener, restores portaled content
|
|
105
|
+
* (drop-never-strand), and unwires everything.
|
|
106
|
+
*/
|
|
107
|
+
disconnect() {
|
|
108
|
+
this.#connected = false
|
|
109
|
+
this.#clearOpenTimer()
|
|
110
|
+
this.#clearCloseTimer()
|
|
111
|
+
this.#cancelExit?.()
|
|
112
|
+
this.#cancelExit = null
|
|
113
|
+
|
|
114
|
+
// An open tooltip disconnecting must not strand the scope count.
|
|
115
|
+
if (this.#isOpen()) {
|
|
116
|
+
const scope = this.#scope()
|
|
117
|
+
|
|
118
|
+
scope.openCount = Math.max(0, scope.openCount - 1)
|
|
119
|
+
scope.warmUntil = Date.now() + this.#skipDelayDuration()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.#dropScrollListener()
|
|
123
|
+
|
|
124
|
+
// Never leave content stranded at the container: if the root subtree
|
|
125
|
+
// was removed the placeholder is gone and restore DROPS the node.
|
|
126
|
+
const content = this.#content()
|
|
127
|
+
|
|
128
|
+
if (content) restoreContent(content)
|
|
129
|
+
|
|
130
|
+
for (const [target, type, listener] of this.#wired) target.removeEventListener(type, listener)
|
|
131
|
+
|
|
132
|
+
this.#wired = []
|
|
133
|
+
document.removeEventListener("pointerup", this.#onPointerup)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Stimulus value callback - controllable state: pinned tooltips (Turbo
|
|
138
|
+
* Stream / Outlet ownership).
|
|
139
|
+
*
|
|
140
|
+
* @param {boolean} value
|
|
141
|
+
*/
|
|
142
|
+
openValueChanged(value) {
|
|
143
|
+
if (!this.#connected) return
|
|
144
|
+
|
|
145
|
+
if (value && !this.#isOpen()) this.#open({ instant: "delay" })
|
|
146
|
+
else if (!value && this.#isOpen()) this.#close("none")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- trigger actions: the pointer path ---
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The trigger's pointermove action (not pointerenter, deliberately):
|
|
153
|
+
* opens
|
|
154
|
+
* once per hover via the latch - instantly when the provider scope is
|
|
155
|
+
* warm, else after the delay; touch pointerType is EXCLUDED entirely.
|
|
156
|
+
*
|
|
157
|
+
* @param {PointerEvent} event
|
|
158
|
+
*/
|
|
159
|
+
pointerMove(event) {
|
|
160
|
+
if (event.pointerType === "touch") return
|
|
161
|
+
|
|
162
|
+
this.#clearCloseTimer() // re-entering the trigger cancels a close intent
|
|
163
|
+
|
|
164
|
+
if (this.#hasPointerMoveOpened || this.#isOpen() || this.#openTimer !== null) return
|
|
165
|
+
if (this.#isPointerDown) return
|
|
166
|
+
|
|
167
|
+
if (this.#isWarm()) {
|
|
168
|
+
this.#hasPointerMoveOpened = true
|
|
169
|
+
this.#open({ instant: "delay" })
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const delay = this.#delayDuration()
|
|
174
|
+
|
|
175
|
+
if (delay <= 0) {
|
|
176
|
+
this.#hasPointerMoveOpened = true
|
|
177
|
+
this.#open()
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.#openTimer = window.setTimeout(() => {
|
|
182
|
+
this.#openTimer = null
|
|
183
|
+
this.#hasPointerMoveOpened = true
|
|
184
|
+
this.#open()
|
|
185
|
+
}, delay)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The trigger's pointerleave action: cancels a pending open; with
|
|
190
|
+
* hoverable content the close waits its grace, else it closes now.
|
|
191
|
+
*
|
|
192
|
+
* @param {PointerEvent} [event]
|
|
193
|
+
*/
|
|
194
|
+
pointerLeave(event) {
|
|
195
|
+
if (event?.pointerType === "touch") return
|
|
196
|
+
|
|
197
|
+
this.#clearOpenTimer()
|
|
198
|
+
this.#hasPointerMoveOpened = false
|
|
199
|
+
|
|
200
|
+
if (!this.#isOpen()) return
|
|
201
|
+
|
|
202
|
+
// Traveling into hoverable content gets the close-intent grace; with
|
|
203
|
+
// hoverable content disabled the tooltip closes with the trigger leave.
|
|
204
|
+
if (this.#hoverableContentDisabled()) this.#close("trigger-hover")
|
|
205
|
+
else this.#scheduleClose()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The trigger's pointerdown action: activating the control dismisses
|
|
210
|
+
* its hint; the latch also suppresses the focus-open until pointerup
|
|
211
|
+
* (pointer users never get a focus-opened tooltip - contractual).
|
|
212
|
+
*/
|
|
213
|
+
pointerDown() {
|
|
214
|
+
this.#isPointerDown = true
|
|
215
|
+
document.addEventListener("pointerup", this.#onPointerup, { once: true })
|
|
216
|
+
this.#clearOpenTimer()
|
|
217
|
+
|
|
218
|
+
if (this.#isOpen()) this.#close("trigger-press")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The trigger's click action: an activation dismisses its hint. */
|
|
222
|
+
clickClose() {
|
|
223
|
+
this.#clearOpenTimer()
|
|
224
|
+
|
|
225
|
+
if (this.#isOpen()) this.#close("trigger-press")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- trigger actions: the keyboard path ---
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The trigger's focus action: opens INSTANTLY (skipping all delays) -
|
|
232
|
+
* unless the focus was caused by a pointerdown (the latch).
|
|
233
|
+
*/
|
|
234
|
+
focusOpen() {
|
|
235
|
+
if (this.#isPointerDown) return
|
|
236
|
+
if (this.#isOpen()) return
|
|
237
|
+
|
|
238
|
+
this.#open({ instant: "focus" })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The trigger's blur action: closes (the focus interaction's exit). */
|
|
242
|
+
blurClose() {
|
|
243
|
+
this.#clearOpenTimer()
|
|
244
|
+
|
|
245
|
+
// The close reason for the focus path: trigger-focus.
|
|
246
|
+
if (this.#isOpen()) this.#close("trigger-focus")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- open / close ---
|
|
250
|
+
|
|
251
|
+
// instant: null (a delayed pointer open) | "delay" (warm registry /
|
|
252
|
+
// programmatic - the provider delay was skipped) | "focus" (keyboard) -
|
|
253
|
+
// the data-instant reason vocabulary.
|
|
254
|
+
#open({ instant = null } = {}) {
|
|
255
|
+
const content = this.#content()
|
|
256
|
+
|
|
257
|
+
if (!content || this.#isOpen()) return
|
|
258
|
+
|
|
259
|
+
this.#cancelExit?.()
|
|
260
|
+
this.#cancelExit = null
|
|
261
|
+
this.#clearCloseTimer()
|
|
262
|
+
|
|
263
|
+
// One tooltip page-wide: every other open tooltip hears this and closes
|
|
264
|
+
// (reason: superseded). Dispatched BEFORE this one opens.
|
|
265
|
+
this.#emittingWillOpen = true
|
|
266
|
+
document.dispatchEvent(new CustomEvent(WILL_OPEN_EVENT, { detail: {} }))
|
|
267
|
+
this.#emittingWillOpen = false
|
|
268
|
+
|
|
269
|
+
this.#scope().openCount += 1
|
|
270
|
+
|
|
271
|
+
// Portal-on-open: move BEFORE any
|
|
272
|
+
// visual state lands (reparenting later would restart the enter
|
|
273
|
+
// animation), then re-anchor absolute - static under compositor
|
|
274
|
+
// scroll, transformed-ancestor immune. The strategy attribute write
|
|
275
|
+
// re-arms popper's autoUpdate against the new ancestors.
|
|
276
|
+
portalContent(content, { container: resolvePortalContainer(this.element) })
|
|
277
|
+
this.element.setAttribute(POPPER_STRATEGY, "absolute")
|
|
278
|
+
|
|
279
|
+
const trigger = this.#trigger()
|
|
280
|
+
|
|
281
|
+
content.hidden = false
|
|
282
|
+
if (trigger) {
|
|
283
|
+
setState(trigger, "popup-open")
|
|
284
|
+
trigger.setAttribute("aria-describedby", content.id)
|
|
285
|
+
}
|
|
286
|
+
// The pair plus a reason: data-open via setState, and
|
|
287
|
+
// data-instant="delay|focus" on the content when the open skipped
|
|
288
|
+
// the delay (absent on a delayed open).
|
|
289
|
+
setState(content, "open")
|
|
290
|
+
if (instant) content.setAttribute("data-instant", instant)
|
|
291
|
+
else content.removeAttribute("data-instant")
|
|
292
|
+
|
|
293
|
+
// Token-activated dismissable only - NO focus-scope, focus never enters
|
|
294
|
+
// a tooltip. Esc anywhere peels the tooltip first (topmost layer).
|
|
295
|
+
content.setAttribute(`data-${DISMISSABLE}-disable-outside-pointer-events-value`, "false")
|
|
296
|
+
this.#addControllers(content, [DISMISSABLE])
|
|
297
|
+
this.#armScrollListener()
|
|
298
|
+
this.openValue = true
|
|
299
|
+
|
|
300
|
+
queueMicrotask(() => {
|
|
301
|
+
if (!this.#isOpen()) return
|
|
302
|
+
|
|
303
|
+
this.dispatch("open", { prefix: EVENT_PREFIX, detail: { state: "open", instant } })
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#close(reason) {
|
|
308
|
+
const content = this.#content()
|
|
309
|
+
|
|
310
|
+
if (!content || !this.#isOpen()) return
|
|
311
|
+
|
|
312
|
+
this.#clearOpenTimer()
|
|
313
|
+
this.#clearCloseTimer()
|
|
314
|
+
this.#hasPointerMoveOpened = false
|
|
315
|
+
|
|
316
|
+
const scope = this.#scope()
|
|
317
|
+
|
|
318
|
+
scope.openCount = Math.max(0, scope.openCount - 1)
|
|
319
|
+
scope.warmUntil = Date.now() + this.#skipDelayDuration()
|
|
320
|
+
|
|
321
|
+
const trigger = this.#trigger()
|
|
322
|
+
|
|
323
|
+
if (trigger) setState(trigger, "popup-closed")
|
|
324
|
+
content.removeAttribute("data-instant")
|
|
325
|
+
this.openValue = false
|
|
326
|
+
|
|
327
|
+
this.#cancelExit = exitPresence(content, {
|
|
328
|
+
onRemove: () => {
|
|
329
|
+
this.#cancelExit = null
|
|
330
|
+
content.hidden = true
|
|
331
|
+
// Home AFTER the exit finished and hidden landed - never
|
|
332
|
+
// mid-animation, never a visible flash.
|
|
333
|
+
restoreContent(content)
|
|
334
|
+
this.element.setAttribute(POPPER_STRATEGY, "fixed")
|
|
335
|
+
trigger?.removeAttribute("aria-describedby")
|
|
336
|
+
this.#removeControllers(content, [DISMISSABLE])
|
|
337
|
+
this.#dropScrollListener()
|
|
338
|
+
this.dispatch("closed", { prefix: EVENT_PREFIX, detail: { reason } })
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Reconcile-on-connect for a server-rendered open tooltip: the DOM already
|
|
344
|
+
// says open; describedby, the layer, the scroll listener and the scope
|
|
345
|
+
// count catch up without re-running the open path.
|
|
346
|
+
#activate(content) {
|
|
347
|
+
if (!content) return
|
|
348
|
+
|
|
349
|
+
const trigger = this.#trigger()
|
|
350
|
+
|
|
351
|
+
if (trigger && content.id) trigger.setAttribute("aria-describedby", content.id)
|
|
352
|
+
|
|
353
|
+
content.setAttribute(`data-${DISMISSABLE}-disable-outside-pointer-events-value`, "false")
|
|
354
|
+
this.#addControllers(content, [DISMISSABLE])
|
|
355
|
+
this.#armScrollListener()
|
|
356
|
+
this.#scope().openCount += 1
|
|
357
|
+
this.#portalPinned(content)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// The reconcile path portals ONE FRAME LATE: connect order within a boot
|
|
361
|
+
// is unordered, and portaling before the sibling popper's connect would
|
|
362
|
+
// rob it of its content target before it could cache the node.
|
|
363
|
+
#portalPinned(content) {
|
|
364
|
+
window.requestAnimationFrame(() => {
|
|
365
|
+
if (!this.#connected || !this.#isOpen()) return
|
|
366
|
+
|
|
367
|
+
portalContent(content, { container: resolvePortalContainer(this.element) })
|
|
368
|
+
this.element.setAttribute(POPPER_STRATEGY, "absolute")
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// --- the close-intent grace (hoverable content) ---
|
|
373
|
+
|
|
374
|
+
#scheduleClose() {
|
|
375
|
+
this.#clearCloseTimer()
|
|
376
|
+
this.#closeTimer = window.setTimeout(() => {
|
|
377
|
+
this.#closeTimer = null
|
|
378
|
+
this.#close("trigger-hover")
|
|
379
|
+
}, HOVERABLE_CLOSE_DELAY)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#clearOpenTimer() {
|
|
383
|
+
if (this.#openTimer === null) return
|
|
384
|
+
|
|
385
|
+
window.clearTimeout(this.#openTimer)
|
|
386
|
+
this.#openTimer = null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#clearCloseTimer() {
|
|
390
|
+
if (this.#closeTimer === null) return
|
|
391
|
+
|
|
392
|
+
window.clearTimeout(this.#closeTimer)
|
|
393
|
+
this.#closeTimer = null
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// --- content wiring (programmatic: portal-safe) ---
|
|
397
|
+
|
|
398
|
+
#wireContent(content) {
|
|
399
|
+
this.#listen(content, "pointerenter", () => this.#clearCloseTimer())
|
|
400
|
+
this.#listen(content, "pointerleave", () => {
|
|
401
|
+
if (this.#isOpen()) this.#scheduleClose()
|
|
402
|
+
})
|
|
403
|
+
this.#listen(content, `${DISMISSABLE}:dismiss`, this.#onDismiss)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#listen(target, type, listener) {
|
|
407
|
+
target.addEventListener(type, listener)
|
|
408
|
+
this.#wired.push([target, type, listener])
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#onDismiss = (event) => {
|
|
412
|
+
if (event.target !== this.#content()) return
|
|
413
|
+
|
|
414
|
+
const escaped = event.detail?.originalEvent?.type === "keydown"
|
|
415
|
+
|
|
416
|
+
// The outside-press close the dismissable brings along matches tooltip
|
|
417
|
+
// UX anyway (contract note); it reports as "outside-press".
|
|
418
|
+
this.#close(escaped ? "escape-key" : "outside-press")
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Another tooltip is about to open: one open page-wide.
|
|
422
|
+
#handleWillOpen(event) {
|
|
423
|
+
if (this.#emittingWillOpen) return
|
|
424
|
+
if (event.target === this.element) return
|
|
425
|
+
|
|
426
|
+
this.#clearOpenTimer()
|
|
427
|
+
|
|
428
|
+
// A sibling tooltip opening closes this one (reason: sibling-open).
|
|
429
|
+
if (this.#isOpen()) this.#close("sibling-open")
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// --- close-on-scroll (capture-phase, armed only while open) ---
|
|
433
|
+
|
|
434
|
+
#armScrollListener() {
|
|
435
|
+
if (this.#scrollListening) return
|
|
436
|
+
|
|
437
|
+
window.addEventListener("scroll", this.#onScroll, { capture: true })
|
|
438
|
+
this.#scrollListening = true
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#dropScrollListener() {
|
|
442
|
+
if (!this.#scrollListening) return
|
|
443
|
+
|
|
444
|
+
window.removeEventListener("scroll", this.#onScroll, { capture: true })
|
|
445
|
+
this.#scrollListening = false
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#handleScroll(event) {
|
|
449
|
+
const trigger = this.#trigger()
|
|
450
|
+
const target = event.target
|
|
451
|
+
|
|
452
|
+
if (!trigger || !target || typeof target.contains !== "function") return
|
|
453
|
+
if (target.contains(trigger)) this.#close("scroll")
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// --- the warm scope (the provider mechanism) ---
|
|
457
|
+
|
|
458
|
+
#provider() {
|
|
459
|
+
return this.element.closest(PROVIDER_SELECTOR)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#scope() {
|
|
463
|
+
return scopeFor(this.#provider() ?? document)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
#isWarm() {
|
|
467
|
+
const scope = this.#scope()
|
|
468
|
+
|
|
469
|
+
return scope.openCount > 0 || Date.now() < scope.warmUntil
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
#providerNumber(attribute, fallback) {
|
|
473
|
+
const raw = this.#provider()?.getAttribute(attribute)
|
|
474
|
+
const parsed = raw === null || raw === undefined || raw === "" ? NaN : Number(raw)
|
|
475
|
+
|
|
476
|
+
return Number.isNaN(parsed) ? fallback : parsed
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#delayDuration() {
|
|
480
|
+
if (this.delayDurationValue >= 0) return this.delayDurationValue
|
|
481
|
+
|
|
482
|
+
return this.#providerNumber("data-delay-duration", DEFAULT_DELAY_DURATION)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#skipDelayDuration() {
|
|
486
|
+
return this.#providerNumber("data-skip-delay-duration", DEFAULT_SKIP_DELAY_DURATION)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#hoverableContentDisabled() {
|
|
490
|
+
// Attribute presence, not hasDisableHoverableContentValue: an unset
|
|
491
|
+
// per-tooltip value INHERITS the provider setting.
|
|
492
|
+
const attribute = "data-poetry--core--tooltip-disable-hoverable-content-value"
|
|
493
|
+
|
|
494
|
+
if (this.element.hasAttribute(attribute)) return this.disableHoverableContentValue
|
|
495
|
+
|
|
496
|
+
return this.#provider()?.getAttribute("data-disable-hoverable-content") === "true"
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// --- structural resolution (the server id pair, portal-safe) ---
|
|
500
|
+
|
|
501
|
+
#trigger() {
|
|
502
|
+
return this.element.querySelector(TRIGGER_SELECTOR)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
#content() {
|
|
506
|
+
const triggerId = this.#trigger()?.id
|
|
507
|
+
|
|
508
|
+
if (triggerId?.endsWith("-trigger")) {
|
|
509
|
+
const byId = document.getElementById(triggerId.replace(/-trigger$/, "-content"))
|
|
510
|
+
|
|
511
|
+
if (byId) return byId
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return this.element.querySelector(CONTENT_SELECTOR)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#isOpen() {
|
|
518
|
+
const content = this.#content()
|
|
519
|
+
const state = content ? stateOf(content) : undefined
|
|
520
|
+
|
|
521
|
+
return Boolean(state) && state !== "closed"
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#addControllers(element, identifiers) {
|
|
525
|
+
const tokens = (element.getAttribute("data-controller") ?? "").split(/\s+/).filter(Boolean)
|
|
526
|
+
|
|
527
|
+
for (const identifier of identifiers) {
|
|
528
|
+
if (!tokens.includes(identifier)) tokens.push(identifier)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
element.setAttribute("data-controller", tokens.join(" "))
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#removeControllers(element, identifiers) {
|
|
535
|
+
const tokens = (element.getAttribute("data-controller") ?? "")
|
|
536
|
+
.split(/\s+/)
|
|
537
|
+
.filter((token) => token && !identifiers.includes(token))
|
|
538
|
+
|
|
539
|
+
element.setAttribute("data-controller", tokens.join(" "))
|
|
540
|
+
}
|
|
541
|
+
}
|