@birdapi/velinstyle 0.6.1 → 0.7.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/README.de.md +315 -316
- package/README.md +8 -9
- package/cli/index.js +4 -3
- package/cli/scanner.js +46 -0
- package/components/focus-manager.js +106 -80
- package/components/index.js +1 -1
- package/components/velin-accordion.js +112 -98
- package/components/velin-carousel.js +40 -5
- package/components/velin-collapse.js +95 -65
- package/components/velin-drawer.js +6 -3
- package/components/velin-dropdown.js +33 -2
- package/components/velin-modal.js +3 -1
- package/components/velin-popover.js +61 -21
- package/components/velin-tooltip-wc.js +26 -2
- package/dist/velinstyle-components.iife.js +220 -51
- package/dist/velinstyle-components.js +222 -51
- package/dist/velinstyle-components.min.js +67 -68
- package/dist/velinstyle.css +168 -41
- package/dist/velinstyle.min.css +1 -1
- package/package.json +6 -3
- package/src/a11y/focus-not-obscured.css +21 -0
- package/src/a11y/forced-colors.css +86 -38
- package/src/a11y/high-contrast-aaa.css +37 -0
- package/src/a11y/preferences.css +106 -85
- package/src/a11y/security.css +101 -104
- package/src/a11y/target-size.css +32 -0
- package/src/base/root.css +4 -4
- package/src/velinstyle.css +6 -1
|
@@ -1,65 +1,95 @@
|
|
|
1
|
-
const styles = `
|
|
2
|
-
:host { display: block; }
|
|
3
|
-
.content {
|
|
4
|
-
overflow: hidden;
|
|
5
|
-
transition: grid-template-rows 300ms ease;
|
|
6
|
-
display: grid;
|
|
7
|
-
grid-template-rows: 0fr;
|
|
8
|
-
}
|
|
9
|
-
:host([open]) .content { grid-template-rows: 1fr; }
|
|
10
|
-
.inner { min-height: 0; }
|
|
11
|
-
@media (prefers-reduced-motion: reduce) { .content { transition: none; } }
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
const styles = `
|
|
2
|
+
:host { display: block; }
|
|
3
|
+
.content {
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
transition: grid-template-rows 300ms ease;
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-rows: 0fr;
|
|
8
|
+
}
|
|
9
|
+
:host([open]) .content { grid-template-rows: 1fr; }
|
|
10
|
+
.inner { min-height: 0; }
|
|
11
|
+
@media (prefers-reduced-motion: reduce) { .content { transition: none; } }
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
let collapseId = 0;
|
|
15
|
+
|
|
16
|
+
function isButtonLike(el) {
|
|
17
|
+
const tag = el.tagName;
|
|
18
|
+
return tag === 'BUTTON' || (tag === 'A' && el.hasAttribute('href')) || el.getAttribute('role') === 'button';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class VelinCollapse extends HTMLElement {
|
|
22
|
+
static get observedAttributes() { return ['open']; }
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
this.attachShadow({ mode: 'open' });
|
|
27
|
+
this._contentId = `velin-collapse-panel-${++collapseId}`;
|
|
28
|
+
this._onTriggerKey = this._onTriggerKey.bind(this);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
connectedCallback() {
|
|
32
|
+
const panelId = this._contentId;
|
|
33
|
+
this.shadowRoot.innerHTML =
|
|
34
|
+
'<style>' + styles + '</style>' +
|
|
35
|
+
'<slot name="trigger"></slot>' +
|
|
36
|
+
'<div class="content" id="' + panelId + '" part="content">' +
|
|
37
|
+
'<div class="inner"><slot></slot></div>' +
|
|
38
|
+
'</div>';
|
|
39
|
+
|
|
40
|
+
const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]');
|
|
41
|
+
triggerSlot.addEventListener('slotchange', () => this._wireTrigger());
|
|
42
|
+
this._wireTrigger();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_wireTrigger() {
|
|
46
|
+
const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
|
|
47
|
+
if (!trigger) return;
|
|
48
|
+
|
|
49
|
+
if (!isButtonLike(trigger)) {
|
|
50
|
+
trigger.setAttribute('role', 'button');
|
|
51
|
+
if (!trigger.hasAttribute('tabindex')) trigger.setAttribute('tabindex', '0');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
trigger.setAttribute('aria-controls', this._contentId);
|
|
55
|
+
trigger.setAttribute('aria-expanded', this.hasAttribute('open') ? 'true' : 'false');
|
|
56
|
+
|
|
57
|
+
trigger.removeEventListener('click', this._onClick);
|
|
58
|
+
trigger.removeEventListener('keydown', this._onTriggerKey);
|
|
59
|
+
this._onClick = () => this.toggle();
|
|
60
|
+
trigger.addEventListener('click', this._onClick);
|
|
61
|
+
trigger.addEventListener('keydown', this._onTriggerKey);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_onTriggerKey(e) {
|
|
65
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
this.toggle();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
attributeChangedCallback(name) {
|
|
72
|
+
if (name === 'open') {
|
|
73
|
+
const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
|
|
74
|
+
if (trigger) trigger.setAttribute('aria-expanded', this.hasAttribute('open') ? 'true' : 'false');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
toggle() {
|
|
79
|
+
if (this.hasAttribute('open')) this.close();
|
|
80
|
+
else this.open();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
open() {
|
|
84
|
+
this.setAttribute('open', '');
|
|
85
|
+
this.dispatchEvent(new CustomEvent('velin-open', { bubbles: true }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
close() {
|
|
89
|
+
this.removeAttribute('open');
|
|
90
|
+
this.dispatchEvent(new CustomEvent('velin-close', { bubbles: true }));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
customElements.define('velin-collapse', VelinCollapse);
|
|
95
|
+
export default VelinCollapse;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { trapFocus, saveFocus, restoreFocus, getFocusableElements } from './focus-manager.js';
|
|
1
|
+
import { trapFocus, saveFocus, restoreFocus, getFocusableElements, setBackgroundInert, clearBackgroundInert } from './focus-manager.js';
|
|
2
2
|
import { escapeHTML } from './sanitize.js';
|
|
3
3
|
|
|
4
4
|
const styles = `
|
|
@@ -61,12 +61,13 @@ class VelinDrawer extends HTMLElement {
|
|
|
61
61
|
connectedCallback() {
|
|
62
62
|
const title = this.getAttribute('title') || '';
|
|
63
63
|
const safeTitle = escapeHTML(title);
|
|
64
|
+
const titleId = 'velin-drawer-title';
|
|
64
65
|
this.shadowRoot.innerHTML = `
|
|
65
66
|
<style>${styles}</style>
|
|
66
67
|
<div class="overlay" part="overlay"></div>
|
|
67
|
-
<div class="drawer" role="dialog" aria-modal="true" aria-
|
|
68
|
+
<div class="drawer" role="dialog" aria-modal="true" aria-labelledby="${titleId}" part="drawer">
|
|
68
69
|
<div class="header" part="header">
|
|
69
|
-
<h2 class="title">${safeTitle}</h2>
|
|
70
|
+
<h2 class="title" id="${titleId}">${safeTitle}</h2>
|
|
70
71
|
<button class="close-btn" aria-label="Close" part="close">×</button>
|
|
71
72
|
</div>
|
|
72
73
|
<div class="body" part="body"><slot></slot></div>
|
|
@@ -85,6 +86,7 @@ class VelinDrawer extends HTMLElement {
|
|
|
85
86
|
|
|
86
87
|
_open() {
|
|
87
88
|
this._prev = saveFocus();
|
|
89
|
+
setBackgroundInert(this);
|
|
88
90
|
document.addEventListener('keydown', this._onKey);
|
|
89
91
|
document.body.style.overflow = 'hidden';
|
|
90
92
|
requestAnimationFrame(() => {
|
|
@@ -96,6 +98,7 @@ class VelinDrawer extends HTMLElement {
|
|
|
96
98
|
_close() {
|
|
97
99
|
document.removeEventListener('keydown', this._onKey);
|
|
98
100
|
document.body.style.overflow = '';
|
|
101
|
+
clearBackgroundInert();
|
|
99
102
|
restoreFocus(this._prev);
|
|
100
103
|
}
|
|
101
104
|
|
|
@@ -62,6 +62,8 @@ class VelinDropdown extends HTMLElement {
|
|
|
62
62
|
this.attachShadow({ mode: 'open', delegatesFocus: true });
|
|
63
63
|
this._onDocClick = this._onDocClick.bind(this);
|
|
64
64
|
this._onKeydown = this._onKeydown.bind(this);
|
|
65
|
+
this._typeahead = '';
|
|
66
|
+
this._typeaheadTimer = null;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
connectedCallback() {
|
|
@@ -75,17 +77,31 @@ class VelinDropdown extends HTMLElement {
|
|
|
75
77
|
`;
|
|
76
78
|
|
|
77
79
|
const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]');
|
|
80
|
+
const menuSlot = this.shadowRoot.querySelector('slot:not([name])');
|
|
78
81
|
triggerSlot.addEventListener('click', () => this.toggle());
|
|
79
82
|
triggerSlot.addEventListener('slotchange', () => {
|
|
80
83
|
const trigger = triggerSlot.assignedElements()[0];
|
|
81
84
|
if (trigger) {
|
|
82
85
|
trigger.setAttribute('aria-haspopup', 'menu');
|
|
83
86
|
trigger.setAttribute('aria-expanded', this.hasAttribute('open') ? 'true' : 'false');
|
|
87
|
+
const menuId = this._menuId || (this._menuId = `velin-dropdown-menu-${Math.random().toString(36).slice(2, 9)}`);
|
|
88
|
+
trigger.setAttribute('aria-controls', menuId);
|
|
89
|
+
this.shadowRoot.querySelector('.menu')?.setAttribute('id', menuId);
|
|
84
90
|
}
|
|
85
91
|
});
|
|
92
|
+
menuSlot?.addEventListener('slotchange', () => this._normalizeMenuItems());
|
|
93
|
+
this._normalizeMenuItems();
|
|
86
94
|
this.addEventListener('keydown', this._onKeydown);
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
_normalizeMenuItems() {
|
|
98
|
+
const items = this._getMenuItems();
|
|
99
|
+
items.forEach((el, i) => {
|
|
100
|
+
if (!el.hasAttribute('role')) el.setAttribute('role', 'menuitem');
|
|
101
|
+
el.setAttribute('tabindex', i === 0 ? '0' : '-1');
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
toggle() {
|
|
90
106
|
if (this.hasAttribute('open')) {
|
|
91
107
|
this.close();
|
|
@@ -138,9 +154,24 @@ class VelinDropdown extends HTMLElement {
|
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
const items = this._getMenuItems();
|
|
141
|
-
if (items.length
|
|
142
|
-
|
|
157
|
+
if (items.length === 0) return;
|
|
158
|
+
|
|
159
|
+
if (event.key.length === 1 && /[a-z0-9]/i.test(event.key)) {
|
|
160
|
+
clearTimeout(this._typeaheadTimer);
|
|
161
|
+
this._typeahead += event.key.toLowerCase();
|
|
162
|
+
this._typeaheadTimer = setTimeout(() => { this._typeahead = ''; }, 500);
|
|
163
|
+
const match = items.find((el) =>
|
|
164
|
+
(el.textContent?.trim().toLowerCase() || '').startsWith(this._typeahead)
|
|
165
|
+
);
|
|
166
|
+
if (match) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
items.forEach((item) => item.setAttribute('tabindex', item === match ? '0' : '-1'));
|
|
169
|
+
match.focus();
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
143
172
|
}
|
|
173
|
+
|
|
174
|
+
rovingTabindex(this, items, event);
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
disconnectedCallback() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { trapFocus, saveFocus, restoreFocus, getFocusableElements } from './focus-manager.js';
|
|
1
|
+
import { trapFocus, saveFocus, restoreFocus, getFocusableElements, setBackgroundInert, clearBackgroundInert } from './focus-manager.js';
|
|
2
2
|
import { escapeHTML } from './sanitize.js';
|
|
3
3
|
|
|
4
4
|
const styles = `
|
|
@@ -141,6 +141,7 @@ class VelinModal extends HTMLElement {
|
|
|
141
141
|
|
|
142
142
|
_open() {
|
|
143
143
|
this._previouslyFocused = saveFocus();
|
|
144
|
+
setBackgroundInert(this);
|
|
144
145
|
document.addEventListener('keydown', this._onKeydown);
|
|
145
146
|
document.body.style.overflow = 'hidden';
|
|
146
147
|
|
|
@@ -153,6 +154,7 @@ class VelinModal extends HTMLElement {
|
|
|
153
154
|
_close() {
|
|
154
155
|
document.removeEventListener('keydown', this._onKeydown);
|
|
155
156
|
document.body.style.overflow = '';
|
|
157
|
+
clearBackgroundInert();
|
|
156
158
|
restoreFocus(this._previouslyFocused);
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHTML } from './sanitize.js';
|
|
2
|
+
import { trapFocus, saveFocus, restoreFocus, getFocusableElements } from './focus-manager.js';
|
|
2
3
|
|
|
3
4
|
const styles = `
|
|
4
5
|
:host { position: relative; display: inline-block; }
|
|
@@ -14,7 +15,6 @@ const styles = `
|
|
|
14
15
|
transition: opacity 150ms ease, visibility 150ms ease;
|
|
15
16
|
}
|
|
16
17
|
:host([open]) .popover { opacity: 1; visibility: visible; }
|
|
17
|
-
/* Placement */
|
|
18
18
|
.popover--top { inset-block-end: calc(100% + 0.5rem); inset-inline-start: 50%; transform: translateX(-50%); }
|
|
19
19
|
.popover--bottom { inset-block-start: calc(100% + 0.5rem); inset-inline-start: 50%; transform: translateX(-50%); }
|
|
20
20
|
.popover--start { inset-inline-end: calc(100% + 0.5rem); inset-block-start: 50%; transform: translateY(-50%); }
|
|
@@ -28,14 +28,19 @@ const styles = `
|
|
|
28
28
|
@media (prefers-reduced-motion: reduce) { .popover { transition: none; } }
|
|
29
29
|
`;
|
|
30
30
|
|
|
31
|
+
let popoverId = 0;
|
|
32
|
+
|
|
31
33
|
class VelinPopover extends HTMLElement {
|
|
32
34
|
static get observedAttributes() { return ['open']; }
|
|
33
35
|
|
|
34
36
|
constructor() {
|
|
35
37
|
super();
|
|
36
38
|
this.attachShadow({ mode: 'open' });
|
|
39
|
+
this._popoverId = `velin-popover-${++popoverId}`;
|
|
37
40
|
this._onOutside = this._onOutside.bind(this);
|
|
38
41
|
this._onKey = this._onKey.bind(this);
|
|
42
|
+
this._prevFocus = null;
|
|
43
|
+
this._isDialog = false;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
connectedCallback() {
|
|
@@ -43,35 +48,44 @@ class VelinPopover extends HTMLElement {
|
|
|
43
48
|
const triggerType = this.getAttribute('trigger') || 'click';
|
|
44
49
|
const title = this.getAttribute('title') || '';
|
|
45
50
|
const role = triggerType === 'hover' ? 'tooltip' : 'dialog';
|
|
51
|
+
this._isDialog = role === 'dialog';
|
|
46
52
|
|
|
47
53
|
this.shadowRoot.innerHTML = `
|
|
48
54
|
<style>${styles}</style>
|
|
49
55
|
<slot name="trigger"></slot>
|
|
50
|
-
<div class="popover popover--${placement}" role="${role}" part="popover">
|
|
56
|
+
<div class="popover popover--${placement}" id="${this._popoverId}" role="${role}" part="popover">
|
|
51
57
|
${title ? `<div class="popover__title" part="title">${escapeHTML(title)}</div>` : ''}
|
|
52
58
|
<slot></slot>
|
|
53
59
|
</div>
|
|
54
60
|
`;
|
|
55
61
|
|
|
56
62
|
const triggerSlot = this.shadowRoot.querySelector('slot[name="trigger"]');
|
|
57
|
-
triggerSlot.addEventListener('slotchange', () =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
63
|
+
triggerSlot.addEventListener('slotchange', () => this._wireTrigger(triggerType));
|
|
64
|
+
this._wireTrigger(triggerType);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_wireTrigger(triggerType) {
|
|
68
|
+
const trigger = this.shadowRoot.querySelector('slot[name="trigger"]')?.assignedElements()[0];
|
|
69
|
+
if (!trigger) return;
|
|
70
|
+
|
|
71
|
+
const isHover = triggerType === 'hover';
|
|
72
|
+
trigger.setAttribute('aria-haspopup', isHover ? 'true' : 'dialog');
|
|
73
|
+
trigger.setAttribute('aria-expanded', this.hasAttribute('open') ? 'true' : 'false');
|
|
74
|
+
if (this._isDialog) {
|
|
75
|
+
trigger.setAttribute('aria-controls', this._popoverId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (triggerType === 'click') {
|
|
79
|
+
trigger.onclick = () => this.toggle();
|
|
80
|
+
} else if (triggerType === 'hover') {
|
|
81
|
+
this.onmouseenter = () => this.open();
|
|
82
|
+
this.onmouseleave = () => this.close();
|
|
83
|
+
this.onfocusin = () => this.open();
|
|
84
|
+
this.onfocusout = (e) => { if (!this.contains(e.relatedTarget)) this.close(); };
|
|
85
|
+
} else if (triggerType === 'focus') {
|
|
86
|
+
trigger.onfocusin = () => this.open();
|
|
87
|
+
trigger.onfocusout = () => this.close();
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
open() {
|
|
@@ -80,18 +94,44 @@ class VelinPopover extends HTMLElement {
|
|
|
80
94
|
if (trigger) trigger.setAttribute('aria-expanded', 'true');
|
|
81
95
|
document.addEventListener('click', this._onOutside, true);
|
|
82
96
|
document.addEventListener('keydown', this._onKey);
|
|
97
|
+
|
|
98
|
+
if (this._isDialog) {
|
|
99
|
+
this._prevFocus = saveFocus();
|
|
100
|
+
requestAnimationFrame(() => {
|
|
101
|
+
const focusable = getFocusableElements(this.shadowRoot.querySelector('.popover'));
|
|
102
|
+
if (focusable.length) focusable[0].focus();
|
|
103
|
+
else this.shadowRoot.querySelector('.popover')?.focus();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
83
106
|
}
|
|
107
|
+
|
|
84
108
|
close() {
|
|
85
109
|
this.removeAttribute('open');
|
|
86
110
|
const trigger = this.querySelector('[slot="trigger"]');
|
|
87
111
|
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
|
88
112
|
document.removeEventListener('click', this._onOutside, true);
|
|
89
113
|
document.removeEventListener('keydown', this._onKey);
|
|
114
|
+
if (this._isDialog && this._prevFocus) {
|
|
115
|
+
restoreFocus(this._prevFocus);
|
|
116
|
+
this._prevFocus = null;
|
|
117
|
+
}
|
|
90
118
|
}
|
|
119
|
+
|
|
91
120
|
toggle() { this.hasAttribute('open') ? this.close() : this.open(); }
|
|
92
121
|
|
|
93
122
|
_onOutside(e) { if (!this.contains(e.target)) this.close(); }
|
|
94
|
-
|
|
123
|
+
|
|
124
|
+
_onKey(e) {
|
|
125
|
+
if (e.key === 'Escape') {
|
|
126
|
+
this.close();
|
|
127
|
+
const trigger = this.querySelector('[slot="trigger"]');
|
|
128
|
+
if (trigger) trigger.focus();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (this._isDialog && this.hasAttribute('open')) {
|
|
132
|
+
trapFocus(this.shadowRoot.querySelector('.popover'), e);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
95
135
|
|
|
96
136
|
disconnectedCallback() {
|
|
97
137
|
document.removeEventListener('click', this._onOutside, true);
|
|
@@ -21,20 +21,24 @@ const styles = `
|
|
|
21
21
|
@media (prefers-reduced-motion: reduce) { .tip { transition: none; } }
|
|
22
22
|
`;
|
|
23
23
|
|
|
24
|
+
let tooltipId = 0;
|
|
25
|
+
|
|
24
26
|
class VelinTooltipWC extends HTMLElement {
|
|
25
27
|
static get observedAttributes() { return ['content', 'placement']; }
|
|
26
28
|
|
|
27
29
|
constructor() {
|
|
28
30
|
super();
|
|
29
31
|
this.attachShadow({ mode: 'open' });
|
|
32
|
+
this._tipId = `velin-tooltip-${++tooltipId}`;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
connectedCallback() {
|
|
33
36
|
const placement = this.getAttribute('placement') || 'top';
|
|
37
|
+
const D = 'div';
|
|
34
38
|
this.shadowRoot.innerHTML = `
|
|
35
39
|
<style>${styles}</style>
|
|
36
40
|
<slot></slot>
|
|
37
|
-
|
|
41
|
+
<${D} class="tip" id="${this._tipId}" role="tooltip" data-placement="${escapeHTML(placement)}" part="tip">${escapeHTML(this.getAttribute('content') || '')}</${D}>
|
|
38
42
|
`;
|
|
39
43
|
|
|
40
44
|
this.addEventListener('mouseenter', () => this._show());
|
|
@@ -42,14 +46,34 @@ class VelinTooltipWC extends HTMLElement {
|
|
|
42
46
|
this.addEventListener('focusin', () => this._show());
|
|
43
47
|
this.addEventListener('focusout', () => this._hide());
|
|
44
48
|
this.addEventListener('keydown', (e) => { if (e.key === 'Escape') this._hide(); });
|
|
49
|
+
|
|
50
|
+
const slot = this.shadowRoot.querySelector('slot');
|
|
51
|
+
slot.addEventListener('slotchange', () => this._linkTrigger());
|
|
52
|
+
this._linkTrigger();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_linkTrigger() {
|
|
56
|
+
const trigger = this.shadowRoot.querySelector('slot')?.assignedElements()[0];
|
|
57
|
+
if (!trigger) return;
|
|
58
|
+
if (this.hasAttribute('visible')) {
|
|
59
|
+
trigger.setAttribute('aria-describedby', this._tipId);
|
|
60
|
+
} else {
|
|
61
|
+
trigger.removeAttribute('aria-describedby');
|
|
62
|
+
}
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
_show() {
|
|
48
66
|
this.setAttribute('visible', '');
|
|
67
|
+
const trigger = this.shadowRoot.querySelector('slot')?.assignedElements()[0];
|
|
68
|
+
if (trigger) trigger.setAttribute('aria-describedby', this._tipId);
|
|
49
69
|
this._flip();
|
|
50
70
|
}
|
|
51
71
|
|
|
52
|
-
_hide() {
|
|
72
|
+
_hide() {
|
|
73
|
+
this.removeAttribute('visible');
|
|
74
|
+
const trigger = this.shadowRoot.querySelector('slot')?.assignedElements()[0];
|
|
75
|
+
if (trigger) trigger.removeAttribute('aria-describedby');
|
|
76
|
+
}
|
|
53
77
|
|
|
54
78
|
_flip() {
|
|
55
79
|
const tip = this.shadowRoot.querySelector('.tip');
|