@getdom/studio 0.1.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.
- package/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { isBrowser, uid } from './base.js';
|
|
2
|
+
import {
|
|
3
|
+
applyFloatingPosition,
|
|
4
|
+
autoUpdateFloating,
|
|
5
|
+
} from './floating.js';
|
|
6
|
+
import {
|
|
7
|
+
beginFloatingEffectEnter,
|
|
8
|
+
beginFloatingEffectLeave,
|
|
9
|
+
clearFloatingEffectTimer,
|
|
10
|
+
prepareFloatingEffect,
|
|
11
|
+
resetFloatingEffectState,
|
|
12
|
+
} from './floating-effect.js';
|
|
13
|
+
|
|
14
|
+
const STYLE_ID = 'dom-tooltip-styles';
|
|
15
|
+
const DEFAULT_OPTIONS = Object.freeze({
|
|
16
|
+
text: '',
|
|
17
|
+
placement: 'top',
|
|
18
|
+
delay: 120,
|
|
19
|
+
offset: 6,
|
|
20
|
+
collisionPadding: 8,
|
|
21
|
+
floatingMode: 'viewport',
|
|
22
|
+
flip: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Install the document-level styles required by headless tooltip bubbles.
|
|
27
|
+
*
|
|
28
|
+
* Tooltip bubbles are appended to `document.body` so they can escape local
|
|
29
|
+
* stacking contexts and overflow containers. Styles are installed lazily when
|
|
30
|
+
* the first target registers a tooltip.
|
|
31
|
+
*
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
function ensureTooltipStyles() {
|
|
35
|
+
if (!isBrowser || document.getElementById(STYLE_ID)) return;
|
|
36
|
+
const style = document.createElement('style');
|
|
37
|
+
style.id = STYLE_ID;
|
|
38
|
+
style.textContent = `
|
|
39
|
+
dom-tooltip {
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
}
|
|
42
|
+
dom-tooltip[for] {
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
[data-dom-tooltip] {
|
|
46
|
+
--dom-tooltip-arrow-size: 0.375rem;
|
|
47
|
+
position: fixed;
|
|
48
|
+
z-index: 70;
|
|
49
|
+
margin: 0;
|
|
50
|
+
max-width: min(18rem, calc(100vw - 1rem));
|
|
51
|
+
padding: 0.375rem 0.625rem;
|
|
52
|
+
border-radius: 0.5rem;
|
|
53
|
+
font-size: 0.75rem;
|
|
54
|
+
line-height: 1rem;
|
|
55
|
+
font-weight: 500;
|
|
56
|
+
background: var(--primary);
|
|
57
|
+
color: var(--primary-fg);
|
|
58
|
+
box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.25);
|
|
59
|
+
white-space: normal;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
62
|
+
[data-dom-tooltip]::before {
|
|
63
|
+
content: "";
|
|
64
|
+
position: absolute;
|
|
65
|
+
display: block;
|
|
66
|
+
width: 0;
|
|
67
|
+
height: 0;
|
|
68
|
+
border: var(--dom-tooltip-arrow-size) solid transparent;
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
}
|
|
71
|
+
[data-dom-tooltip][data-side="top"]::before {
|
|
72
|
+
bottom: calc(var(--dom-tooltip-arrow-size) * -1);
|
|
73
|
+
left: var(--dom-popover-arrow-x, 50%);
|
|
74
|
+
border-top-color: var(--primary);
|
|
75
|
+
border-bottom: 0;
|
|
76
|
+
transform: translateX(-50%);
|
|
77
|
+
}
|
|
78
|
+
[data-dom-tooltip][data-side="bottom"]::before {
|
|
79
|
+
top: calc(var(--dom-tooltip-arrow-size) * -1);
|
|
80
|
+
left: var(--dom-popover-arrow-x, 50%);
|
|
81
|
+
border-top: 0;
|
|
82
|
+
border-bottom-color: var(--primary);
|
|
83
|
+
transform: translateX(-50%);
|
|
84
|
+
}
|
|
85
|
+
[data-dom-tooltip][data-side="left"]::before {
|
|
86
|
+
top: var(--dom-popover-arrow-y, 50%);
|
|
87
|
+
right: calc(var(--dom-tooltip-arrow-size) * -1);
|
|
88
|
+
border-right: 0;
|
|
89
|
+
border-left-color: var(--primary);
|
|
90
|
+
transform: translateY(-50%);
|
|
91
|
+
}
|
|
92
|
+
[data-dom-tooltip][data-side="right"]::before {
|
|
93
|
+
top: var(--dom-popover-arrow-y, 50%);
|
|
94
|
+
left: calc(var(--dom-tooltip-arrow-size) * -1);
|
|
95
|
+
border-right-color: var(--primary);
|
|
96
|
+
border-left: 0;
|
|
97
|
+
transform: translateY(-50%);
|
|
98
|
+
}
|
|
99
|
+
[data-dom-tooltip][popover] {
|
|
100
|
+
margin: 0;
|
|
101
|
+
overflow: visible;
|
|
102
|
+
}
|
|
103
|
+
[data-dom-tooltip][data-floating-effect="fade"] {
|
|
104
|
+
opacity: 0;
|
|
105
|
+
transition: opacity 190ms ease;
|
|
106
|
+
}
|
|
107
|
+
[data-dom-tooltip][data-floating-open="true"] {
|
|
108
|
+
opacity: 1;
|
|
109
|
+
}
|
|
110
|
+
[data-dom-tooltip][data-entering="from"][data-floating-open="true"],
|
|
111
|
+
[data-dom-tooltip][data-leaving="to"][data-floating-open="true"] {
|
|
112
|
+
opacity: 0;
|
|
113
|
+
}
|
|
114
|
+
[data-dom-tooltip][data-entering="to"][data-floating-open="true"],
|
|
115
|
+
[data-dom-tooltip][data-leaving="from"][data-floating-open="true"] {
|
|
116
|
+
opacity: 1;
|
|
117
|
+
}
|
|
118
|
+
[data-dom-tooltip][hidden] {
|
|
119
|
+
display: none;
|
|
120
|
+
}
|
|
121
|
+
@media (prefers-reduced-motion: reduce) {
|
|
122
|
+
[data-dom-tooltip][data-floating-effect="fade"] {
|
|
123
|
+
transition-duration: 1ms;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
document.head.appendChild(style);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Coerce a tooltip number option to a finite non-negative value.
|
|
132
|
+
*
|
|
133
|
+
* @param {unknown} value Candidate option value.
|
|
134
|
+
* @param {number} fallback Default used for invalid values.
|
|
135
|
+
* @returns {number} Normalized option value.
|
|
136
|
+
*/
|
|
137
|
+
function normalizeNumber(value, fallback) {
|
|
138
|
+
const number = Number(value);
|
|
139
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Normalize public tooltip options into the controller's complete contract.
|
|
144
|
+
*
|
|
145
|
+
* @param {Record<string, unknown>} [options] Partial public tooltip options.
|
|
146
|
+
* @returns {{
|
|
147
|
+
* text: string,
|
|
148
|
+
* placement: string,
|
|
149
|
+
* delay: number,
|
|
150
|
+
* offset: number,
|
|
151
|
+
* collisionPadding: number,
|
|
152
|
+
* floatingMode: string,
|
|
153
|
+
* flip: boolean,
|
|
154
|
+
* }} Complete tooltip options.
|
|
155
|
+
*/
|
|
156
|
+
function normalizeOptions(options = {}) {
|
|
157
|
+
return {
|
|
158
|
+
text: String(options.text ?? DEFAULT_OPTIONS.text),
|
|
159
|
+
placement: String(options.placement || DEFAULT_OPTIONS.placement),
|
|
160
|
+
delay: normalizeNumber(options.delay, DEFAULT_OPTIONS.delay),
|
|
161
|
+
offset: normalizeNumber(options.offset, DEFAULT_OPTIONS.offset),
|
|
162
|
+
collisionPadding: normalizeNumber(options.collisionPadding, DEFAULT_OPTIONS.collisionPadding),
|
|
163
|
+
floatingMode: String(options.floatingMode || DEFAULT_OPTIONS.floatingMode),
|
|
164
|
+
flip: options.flip !== false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Dispatch a public tooltip lifecycle event from its declarative owner.
|
|
170
|
+
*
|
|
171
|
+
* @param {EventTarget | null} target Event target supplied by the caller.
|
|
172
|
+
* @param {'dom:show' | 'dom:hide'} type Tooltip lifecycle event name.
|
|
173
|
+
* @returns {void}
|
|
174
|
+
*/
|
|
175
|
+
function emitTooltipEvent(target, type) {
|
|
176
|
+
if (!target?.dispatchEvent) return;
|
|
177
|
+
target.dispatchEvent(new CustomEvent(type, {
|
|
178
|
+
bubbles: true,
|
|
179
|
+
composed: true,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Own the bubble, ARIA relationship, listeners, and positioning for one target.
|
|
185
|
+
*/
|
|
186
|
+
export class DomTooltipController {
|
|
187
|
+
/**
|
|
188
|
+
* Create a tooltip controller for one explicit target.
|
|
189
|
+
*
|
|
190
|
+
* @param {HTMLElement} trigger Element that receives hover and focus interactions.
|
|
191
|
+
* @param {Record<string, unknown>} [options] Tooltip content and positioning options.
|
|
192
|
+
* @param {EventTarget} [eventTarget] Element that emits public tooltip lifecycle events.
|
|
193
|
+
*/
|
|
194
|
+
constructor(trigger, options = {}, eventTarget = trigger) {
|
|
195
|
+
if (!(trigger instanceof HTMLElement)) {
|
|
196
|
+
throw new TypeError('attachTooltip requires an HTMLElement target.');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
ensureTooltipStyles();
|
|
200
|
+
this.trigger = trigger;
|
|
201
|
+
this.eventTarget = eventTarget;
|
|
202
|
+
this.options = normalizeOptions(options);
|
|
203
|
+
this.bubble = null;
|
|
204
|
+
this.timer = 0;
|
|
205
|
+
this.open = false;
|
|
206
|
+
this.hovered = false;
|
|
207
|
+
this.focused = false;
|
|
208
|
+
this.reflowOff = null;
|
|
209
|
+
this.listeners = [];
|
|
210
|
+
|
|
211
|
+
this._createBubble();
|
|
212
|
+
this._bindInteractions();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create the body-level tooltip bubble and its ARIA relationship.
|
|
217
|
+
*
|
|
218
|
+
* @returns {void}
|
|
219
|
+
*/
|
|
220
|
+
_createBubble() {
|
|
221
|
+
const id = uid('tip');
|
|
222
|
+
const bubble = document.createElement('div');
|
|
223
|
+
bubble.id = `${id}-bubble`;
|
|
224
|
+
bubble.setAttribute('role', 'tooltip');
|
|
225
|
+
bubble.setAttribute('data-placement', this.options.placement);
|
|
226
|
+
bubble.dataset.domTooltip = '';
|
|
227
|
+
bubble.dataset.floatingEffect = 'fade';
|
|
228
|
+
bubble.setAttribute('popover', 'manual');
|
|
229
|
+
bubble.setAttribute('aria-hidden', 'true');
|
|
230
|
+
bubble.hidden = true;
|
|
231
|
+
bubble.textContent = this.options.text;
|
|
232
|
+
prepareFloatingEffect(bubble, 'fade');
|
|
233
|
+
document.body.appendChild(bubble);
|
|
234
|
+
this.bubble = bubble;
|
|
235
|
+
this._addDescriptionId(bubble.id);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Add the tooltip bubble without replacing existing descriptions.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} id Tooltip bubble identifier.
|
|
242
|
+
* @returns {void}
|
|
243
|
+
*/
|
|
244
|
+
_addDescriptionId(id) {
|
|
245
|
+
const ids = new Set((this.trigger.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean));
|
|
246
|
+
ids.add(id);
|
|
247
|
+
this.trigger.setAttribute('aria-describedby', [...ids].join(' '));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Remove only this controller's description from the target.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} id Tooltip bubble identifier.
|
|
254
|
+
* @returns {void}
|
|
255
|
+
*/
|
|
256
|
+
_removeDescriptionId(id) {
|
|
257
|
+
const ids = (this.trigger.getAttribute('aria-describedby') || '')
|
|
258
|
+
.split(/\s+/)
|
|
259
|
+
.filter((value) => value && value !== id);
|
|
260
|
+
if (ids.length) {
|
|
261
|
+
this.trigger.setAttribute('aria-describedby', ids.join(' '));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.trigger.removeAttribute('aria-describedby');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Register one interaction listener for cleanup with the controller.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} type DOM event name.
|
|
271
|
+
* @param {EventListener} handler Bound event handler.
|
|
272
|
+
* @returns {void}
|
|
273
|
+
*/
|
|
274
|
+
_listen(type, handler) {
|
|
275
|
+
this.trigger.addEventListener(type, handler);
|
|
276
|
+
this.listeners.push(() => this.trigger.removeEventListener(type, handler));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Bind direct hover, focus, and keyboard interactions to the target.
|
|
281
|
+
*
|
|
282
|
+
* @returns {void}
|
|
283
|
+
*/
|
|
284
|
+
_bindInteractions() {
|
|
285
|
+
this.handleMouseEnter = this.handleMouseEnter.bind(this);
|
|
286
|
+
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
|
287
|
+
this.handleFocus = this.handleFocus.bind(this);
|
|
288
|
+
this.handleBlur = this.handleBlur.bind(this);
|
|
289
|
+
this.handleKeydown = this.handleKeydown.bind(this);
|
|
290
|
+
this._listen('mouseenter', this.handleMouseEnter);
|
|
291
|
+
this._listen('mouseleave', this.handleMouseLeave);
|
|
292
|
+
this._listen('focus', this.handleFocus);
|
|
293
|
+
this._listen('blur', this.handleBlur);
|
|
294
|
+
this._listen('keydown', this.handleKeydown);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Show the tooltip when pointer interest begins.
|
|
299
|
+
*
|
|
300
|
+
* @returns {void}
|
|
301
|
+
*/
|
|
302
|
+
handleMouseEnter() {
|
|
303
|
+
this.hovered = true;
|
|
304
|
+
this.show();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Hide the tooltip after pointer interest ends unless focus remains.
|
|
309
|
+
*
|
|
310
|
+
* @returns {void}
|
|
311
|
+
*/
|
|
312
|
+
handleMouseLeave() {
|
|
313
|
+
this.hovered = false;
|
|
314
|
+
if (!this.focused) this.hide();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Show the tooltip when keyboard focus reaches the target.
|
|
319
|
+
*
|
|
320
|
+
* @returns {void}
|
|
321
|
+
*/
|
|
322
|
+
handleFocus() {
|
|
323
|
+
this.focused = true;
|
|
324
|
+
this.show();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Hide the tooltip after focus leaves unless pointer interest remains.
|
|
329
|
+
*
|
|
330
|
+
* @returns {void}
|
|
331
|
+
*/
|
|
332
|
+
handleBlur() {
|
|
333
|
+
this.focused = false;
|
|
334
|
+
if (!this.hovered) this.hide();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Dismiss an open tooltip with Escape.
|
|
339
|
+
*
|
|
340
|
+
* @param {KeyboardEvent} event Keyboard interaction from the target.
|
|
341
|
+
* @returns {void}
|
|
342
|
+
*/
|
|
343
|
+
handleKeydown(event) {
|
|
344
|
+
if (event.key === 'Escape') this.hide();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Return the shared floating-position options for the current tooltip.
|
|
349
|
+
*
|
|
350
|
+
* @returns {Record<string, unknown>} Floating position options.
|
|
351
|
+
*/
|
|
352
|
+
_positionOptions() {
|
|
353
|
+
return {
|
|
354
|
+
align: 'center',
|
|
355
|
+
offset: this.options.offset,
|
|
356
|
+
placement: this.options.placement,
|
|
357
|
+
padding: this.options.collisionPadding,
|
|
358
|
+
mode: this.options.floatingMode,
|
|
359
|
+
flip: this.options.flip,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Check whether the bubble currently participates in the Popover top layer.
|
|
365
|
+
*
|
|
366
|
+
* @returns {boolean} Whether the bubble is open as a native popover.
|
|
367
|
+
*/
|
|
368
|
+
_isBubblePopoverOpen() {
|
|
369
|
+
try {
|
|
370
|
+
return Boolean(this.bubble?.matches?.(':popover-open'));
|
|
371
|
+
} catch {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Promote the bubble into the native Popover top layer when supported.
|
|
378
|
+
*
|
|
379
|
+
* @returns {void}
|
|
380
|
+
*/
|
|
381
|
+
_showBubblePopover() {
|
|
382
|
+
if (!this.bubble?.showPopover || this._isBubblePopoverOpen()) return;
|
|
383
|
+
try {
|
|
384
|
+
this.bubble.showPopover();
|
|
385
|
+
} catch {
|
|
386
|
+
// Browsers without Popover API support keep the fixed-position fallback.
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Remove the bubble from the native Popover top layer when supported.
|
|
392
|
+
*
|
|
393
|
+
* @returns {void}
|
|
394
|
+
*/
|
|
395
|
+
_hideBubblePopover() {
|
|
396
|
+
if (!this.bubble?.hidePopover || !this._isBubblePopoverOpen()) return;
|
|
397
|
+
try {
|
|
398
|
+
this.bubble.hidePopover();
|
|
399
|
+
} catch {
|
|
400
|
+
// Ignore stale popover state during teardown or browser fallback paths.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Place the visible bubble relative to its registered target.
|
|
406
|
+
*
|
|
407
|
+
* @returns {Record<string, unknown> | null} Floating position result.
|
|
408
|
+
*/
|
|
409
|
+
_place() {
|
|
410
|
+
if (!this.bubble || this.bubble.hidden) return null;
|
|
411
|
+
return applyFloatingPosition(this.trigger, this.bubble, this._positionOptions());
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Keep the visible bubble aligned during resize, scroll, and layout changes.
|
|
416
|
+
*
|
|
417
|
+
* @returns {void}
|
|
418
|
+
*/
|
|
419
|
+
_bindReflow() {
|
|
420
|
+
this._unbindReflow();
|
|
421
|
+
this.reflowOff = autoUpdateFloating(this.trigger, this.bubble, () => this._place());
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Stop automatic floating-position updates.
|
|
426
|
+
*
|
|
427
|
+
* @returns {void}
|
|
428
|
+
*/
|
|
429
|
+
_unbindReflow() {
|
|
430
|
+
this.reflowOff?.();
|
|
431
|
+
this.reflowOff = null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Complete hiding after the leave transition.
|
|
436
|
+
*
|
|
437
|
+
* @param {HTMLElement | null} [bubble] Bubble associated with the transition.
|
|
438
|
+
* @returns {void}
|
|
439
|
+
*/
|
|
440
|
+
_finishHide(bubble = this.bubble) {
|
|
441
|
+
if (!bubble) return;
|
|
442
|
+
this._hideBubblePopover();
|
|
443
|
+
bubble.hidden = true;
|
|
444
|
+
bubble.setAttribute('aria-hidden', 'true');
|
|
445
|
+
resetFloatingEffectState(bubble);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Update tooltip content and positioning without replacing its target.
|
|
450
|
+
*
|
|
451
|
+
* @param {Record<string, unknown>} [options] Complete or partial tooltip options.
|
|
452
|
+
* @returns {void}
|
|
453
|
+
*/
|
|
454
|
+
update(options = {}) {
|
|
455
|
+
this.options = normalizeOptions(options);
|
|
456
|
+
if (!this.bubble) return;
|
|
457
|
+
this.bubble.textContent = this.options.text;
|
|
458
|
+
this.bubble.setAttribute('data-placement', this.options.placement);
|
|
459
|
+
if (!this.options.text) {
|
|
460
|
+
this.hide();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
this._place();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Show the tooltip after its configured intent delay.
|
|
468
|
+
*
|
|
469
|
+
* @returns {void}
|
|
470
|
+
*/
|
|
471
|
+
show() {
|
|
472
|
+
window.clearTimeout(this.timer);
|
|
473
|
+
if (!this.options.text || (this.open && !this.bubble?.hidden)) return;
|
|
474
|
+
this.timer = window.setTimeout(() => {
|
|
475
|
+
const bubble = this.bubble;
|
|
476
|
+
if (!bubble) return;
|
|
477
|
+
this.open = true;
|
|
478
|
+
bubble.removeAttribute('aria-hidden');
|
|
479
|
+
const showNow = () => {
|
|
480
|
+
bubble.hidden = false;
|
|
481
|
+
this._showBubblePopover();
|
|
482
|
+
this._place();
|
|
483
|
+
this._bindReflow();
|
|
484
|
+
};
|
|
485
|
+
const animated = beginFloatingEffectEnter(bubble, {
|
|
486
|
+
animatePlacement: false,
|
|
487
|
+
beforeMeasure: showNow,
|
|
488
|
+
beforeFrame: () => this._place(),
|
|
489
|
+
shouldContinue: () => this.open && this.bubble === bubble,
|
|
490
|
+
});
|
|
491
|
+
if (!animated) showNow();
|
|
492
|
+
emitTooltipEvent(this.eventTarget, 'dom:show');
|
|
493
|
+
}, this.options.delay);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Hide the tooltip and stop active positioning work.
|
|
498
|
+
*
|
|
499
|
+
* @returns {void}
|
|
500
|
+
*/
|
|
501
|
+
hide() {
|
|
502
|
+
window.clearTimeout(this.timer);
|
|
503
|
+
if (!this.bubble || (!this.open && this.bubble.hidden)) return;
|
|
504
|
+
this.open = false;
|
|
505
|
+
this._unbindReflow();
|
|
506
|
+
const bubble = this.bubble;
|
|
507
|
+
bubble.setAttribute('aria-hidden', 'true');
|
|
508
|
+
beginFloatingEffectLeave(bubble, {
|
|
509
|
+
shouldContinue: () => !this.open && this.bubble === bubble,
|
|
510
|
+
finish: () => {
|
|
511
|
+
if (this.bubble === bubble) this._finishHide(bubble);
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
emitTooltipEvent(this.eventTarget, 'dom:hide');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Release every resource owned by this target registration.
|
|
519
|
+
*
|
|
520
|
+
* @returns {void}
|
|
521
|
+
*/
|
|
522
|
+
destroy() {
|
|
523
|
+
window.clearTimeout(this.timer);
|
|
524
|
+
this.open = false;
|
|
525
|
+
this.hovered = false;
|
|
526
|
+
this.focused = false;
|
|
527
|
+
this._unbindReflow();
|
|
528
|
+
for (const removeListener of this.listeners) removeListener();
|
|
529
|
+
this.listeners = [];
|
|
530
|
+
clearFloatingEffectTimer(this.bubble);
|
|
531
|
+
const bubble = this.bubble;
|
|
532
|
+
this._finishHide(bubble);
|
|
533
|
+
if (bubble) this._removeDescriptionId(bubble.id);
|
|
534
|
+
bubble?.remove();
|
|
535
|
+
this.bubble = null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Register tooltip behaviour directly on one element.
|
|
541
|
+
*
|
|
542
|
+
* @param {HTMLElement} trigger Element that receives hover and focus interactions.
|
|
543
|
+
* @param {Record<string, unknown>} [options] Tooltip content and positioning options.
|
|
544
|
+
* @param {EventTarget} [eventTarget] Element that emits public tooltip lifecycle events.
|
|
545
|
+
* @returns {DomTooltipController} Controller used to update or remove the tooltip.
|
|
546
|
+
*/
|
|
547
|
+
export function attachTooltip(trigger, options = {}, eventTarget = trigger) {
|
|
548
|
+
return new DomTooltipController(trigger, options, eventTarget);
|
|
549
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Generated by scripts/generate-types.mjs. Do not edit by hand.
|
|
3
|
+
import { DomElementBase } from './base.js';
|
|
4
|
+
export { attachTooltip, DomTooltipController } from './tooltip-controller.js';
|
|
5
|
+
export type { DomTooltipOptions, DomTooltipPlacement } from './tooltip-controller.js';
|
|
6
|
+
export declare class DomTooltipElement extends DomElementBase {
|
|
7
|
+
static readonly observedAttributes?: Array<string>;
|
|
8
|
+
static readonly __doc?: HeadlessElementDoc;
|
|
9
|
+
}
|
|
10
|
+
export interface HeadlessElementDoc {
|
|
11
|
+
name?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
slots?: Array<Record<string, unknown>>;
|
|
14
|
+
attributes?: Array<Record<string, unknown>>;
|
|
15
|
+
events?: Array<Record<string, unknown>>;
|
|
16
|
+
keyboard?: Array<Record<string, unknown>>;
|
|
17
|
+
example?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { DomElementBase, defineDomElement } from './base.js';
|
|
2
|
+
import { attachTooltip } from './tooltip-controller.js';
|
|
3
|
+
|
|
4
|
+
export { attachTooltip, DomTooltipController } from './tooltip-controller.js';
|
|
5
|
+
|
|
6
|
+
// <dom-tooltip text="…" placement="top|bottom|left|right" delay="150">
|
|
7
|
+
// <button>trigger</button>
|
|
8
|
+
// </dom-tooltip>
|
|
9
|
+
export class DomTooltipElement extends DomElementBase {
|
|
10
|
+
static get observedAttributes() {
|
|
11
|
+
return ['for', 'text', 'placement', 'delay', 'offset', 'collision-padding', 'floating-mode', 'flip'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static __doc = {
|
|
15
|
+
name: 'dom-tooltip',
|
|
16
|
+
description: 'Lightweight tooltip wired through aria-describedby. Wrap a trigger or reference one by id with for.',
|
|
17
|
+
slots: [
|
|
18
|
+
{ name: '(default)', description: 'The trigger — any focusable element wrapped by the tooltip when for is not set.' },
|
|
19
|
+
],
|
|
20
|
+
attributes: [
|
|
21
|
+
{ name: 'for', type: 'string', description: 'ID of an existing trigger element. This avoids adding a wrapper around the trigger.' },
|
|
22
|
+
{ name: 'text', type: 'string', description: 'Tooltip body. Keep it short.' },
|
|
23
|
+
{ name: 'placement', type: "'top' | 'bottom' | 'left' | 'right'", description: 'Position relative to the trigger.' },
|
|
24
|
+
{ name: 'delay', type: 'number', description: 'Open delay in milliseconds (default 120).' },
|
|
25
|
+
{ name: 'offset', type: 'number', description: 'Gap in pixels between trigger and tooltip (default 6).' },
|
|
26
|
+
{ name: 'collision-padding', type: 'number', description: 'Viewport padding used when flipping or shifting the tooltip (default 8).' },
|
|
27
|
+
{ name: 'floating-mode', type: "'viewport' | 'anchor'", description: 'viewport keeps the tooltip inside the browser; anchor keeps it attached while scrolling.' },
|
|
28
|
+
{ name: 'flip', type: 'boolean', description: 'Allow the tooltip to flip to the opposite side when it would collide with the viewport.' },
|
|
29
|
+
],
|
|
30
|
+
events: [
|
|
31
|
+
{ name: 'dom:show', description: 'Fired when the tooltip becomes visible.' },
|
|
32
|
+
{ name: 'dom:hide', description: 'Fired when the tooltip is dismissed.' },
|
|
33
|
+
],
|
|
34
|
+
keyboard: [
|
|
35
|
+
{ keys: 'Focus trigger', action: 'Shows the tooltip.' },
|
|
36
|
+
{ keys: 'Blur trigger', action: 'Hides the tooltip.' },
|
|
37
|
+
],
|
|
38
|
+
example: `<dom-tooltip text="Save your changes" placement="top" delay="120">
|
|
39
|
+
<button>Save</button>
|
|
40
|
+
</dom-tooltip>`,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize target and controller state for this declarative host.
|
|
45
|
+
*/
|
|
46
|
+
constructor() {
|
|
47
|
+
super();
|
|
48
|
+
this._trigger = null;
|
|
49
|
+
this._controller = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Register the wrapper child or explicit for target when connected.
|
|
54
|
+
*
|
|
55
|
+
* @returns {void}
|
|
56
|
+
*/
|
|
57
|
+
connectedCallback() {
|
|
58
|
+
if (this._connectTarget()) return;
|
|
59
|
+
queueMicrotask(() => {
|
|
60
|
+
if (this.isConnected && !this._controller) this._connectTarget();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read the complete tooltip options from host attributes.
|
|
66
|
+
*
|
|
67
|
+
* @returns {Record<string, unknown>} Controller options.
|
|
68
|
+
*/
|
|
69
|
+
_options() {
|
|
70
|
+
return {
|
|
71
|
+
text: this.getAttribute('text') || '',
|
|
72
|
+
placement: this.getAttribute('placement') || 'top',
|
|
73
|
+
delay: Number(this.getAttribute('delay') || 120),
|
|
74
|
+
offset: Number(this.getAttribute('offset') || 6),
|
|
75
|
+
collisionPadding: Number(this.getAttribute('collision-padding') || 8),
|
|
76
|
+
floatingMode: this.getAttribute('floating-mode') || 'viewport',
|
|
77
|
+
flip: this.getAttribute('flip') !== 'false',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the target from for or the existing wrapper child.
|
|
83
|
+
*
|
|
84
|
+
* @returns {HTMLElement | null} Explicit tooltip target.
|
|
85
|
+
*/
|
|
86
|
+
_resolveTarget() {
|
|
87
|
+
const targetId = this.getAttribute('for')?.trim();
|
|
88
|
+
if (targetId) {
|
|
89
|
+
const target = this.ownerDocument.getElementById(targetId);
|
|
90
|
+
return target instanceof HTMLElement ? target : null;
|
|
91
|
+
}
|
|
92
|
+
return [...this.children].find((element) => element instanceof HTMLElement) || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Attach or update the controller for the currently resolved target.
|
|
97
|
+
*
|
|
98
|
+
* @returns {boolean} Whether a target is registered.
|
|
99
|
+
*/
|
|
100
|
+
_connectTarget() {
|
|
101
|
+
const target = this._resolveTarget();
|
|
102
|
+
if (!target) {
|
|
103
|
+
this._disconnectTarget();
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (target === this._trigger && this._controller) {
|
|
107
|
+
this._controller.update(this._options());
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
this._disconnectTarget();
|
|
111
|
+
this._trigger = target;
|
|
112
|
+
this._controller = attachTooltip(target, this._options(), this);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Destroy the current per-target tooltip registration.
|
|
118
|
+
*
|
|
119
|
+
* @returns {void}
|
|
120
|
+
*/
|
|
121
|
+
_disconnectTarget() {
|
|
122
|
+
this._controller?.destroy();
|
|
123
|
+
this._controller = null;
|
|
124
|
+
this._trigger = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reconnect for changes to for and update options for other attributes.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} name Changed attribute name.
|
|
131
|
+
* @returns {void}
|
|
132
|
+
*/
|
|
133
|
+
attributeChangedCallback(name) {
|
|
134
|
+
if (!this.isConnected) return;
|
|
135
|
+
if (name === 'for') {
|
|
136
|
+
this._connectTarget();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this._controller?.update(this._options());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Release the registered target when this host leaves the document.
|
|
144
|
+
*
|
|
145
|
+
* @returns {void}
|
|
146
|
+
*/
|
|
147
|
+
disconnectedCallback() {
|
|
148
|
+
this._disconnectTarget();
|
|
149
|
+
super.disconnectedCallback();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
defineDomElement('dom-tooltip', DomTooltipElement);
|