fokus-styles 2.3.0 → 2.5.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/CHANGELOG.md +36 -0
- package/dist/css/components.css +583 -83
- package/dist/css/components.css.map +1 -1
- package/dist/css/components.min.css +1 -1
- package/dist/css/components.min.css.map +1 -1
- package/dist/css/fokus-components.css +583 -83
- package/dist/css/fokus-components.css.map +1 -1
- package/dist/css/fokus-components.min.css +1 -1
- package/dist/css/fokus-components.min.css.map +1 -1
- package/dist/css/fokus-core.css +22 -0
- package/dist/css/fokus-core.css.map +1 -1
- package/dist/css/fokus-core.min.css +1 -1
- package/dist/css/fokus-core.min.css.map +1 -1
- package/dist/css/fokus-dark.css +10 -0
- package/dist/css/fokus-dark.css.map +1 -1
- package/dist/css/fokus-dark.min.css +1 -1
- package/dist/css/fokus-dark.min.css.map +1 -1
- package/dist/css/fokus-rtl.css +9166 -0
- package/dist/css/fokus-rtl.css.map +1 -0
- package/dist/css/fokus-rtl.min.css +2 -0
- package/dist/css/fokus-rtl.min.css.map +1 -0
- package/dist/css/fokus-utilities.css +158 -0
- package/dist/css/fokus-utilities.css.map +1 -1
- package/dist/css/fokus-utilities.min.css +1 -1
- package/dist/css/fokus-utilities.min.css.map +1 -1
- package/dist/css/fokus.css +1018 -258
- package/dist/css/fokus.css.map +1 -1
- package/dist/css/fokus.min.css +1 -1
- package/dist/css/fokus.min.css.map +1 -1
- package/dist/css/forms.css +322 -176
- package/dist/css/forms.css.map +1 -1
- package/dist/css/forms.min.css +1 -1
- package/dist/css/forms.min.css.map +1 -1
- package/dist/css/helpers.css +158 -0
- package/dist/css/helpers.css.map +1 -1
- package/dist/css/helpers.min.css +1 -1
- package/dist/css/helpers.min.css.map +1 -1
- package/dist/css/layout.css +22 -0
- package/dist/css/layout.css.map +1 -1
- package/dist/css/layout.min.css +1 -1
- package/dist/css/layout.min.css.map +1 -1
- package/dist/js/fokus.js +270 -25
- package/dist/js/fokus.js.map +2 -2
- package/dist/js/fokus.min.js +18 -18
- package/dist/js/fokus.min.js.map +3 -3
- package/docs/components/alert.md +69 -24
- package/docs/components/badge.md +23 -4
- package/docs/components/checkbox.md +16 -3
- package/docs/components/popover.md +20 -7
- package/docs/components/radio.md +11 -1
- package/docs/components/responsive.md +19 -0
- package/docs/components/switch.md +30 -8
- package/docs/components/tag.md +19 -4
- package/docs/components/toast.md +72 -59
- package/docs/components/tooltip.md +38 -2
- package/docs/guides/accessibility.md +7 -0
- package/docs/guides/rtl-and-system-preferences.md +28 -0
- package/docs/prompts/prompt-plan.md +4 -4
- package/docs/reference/accessibility-matrix.md +7 -7
- package/docs/reference/contrast-report.md +4 -0
- package/docs/reference/definitions.md +5 -1
- package/docs/reference/design-tokens.md +14 -0
- package/docs/reference/size-baseline.json +13 -13
- package/mockup/assets/example-theme.css +11 -0
- package/mockup/assets/showcase-contracts.js +6 -6
- package/mockup/examples/alerts.html +105 -31
- package/mockup/examples/badges-alerts.html +91 -59
- package/mockup/examples/check-radio-switch.html +575 -99
- package/mockup/examples/popover.html +81 -0
- package/mockup/examples/tag.html +97 -87
- package/mockup/examples/toast.html +113 -0
- package/mockup/examples/tooltip.html +68 -0
- package/mockup/forms.html +1 -1
- package/mockup/overlays-commands.html +2 -2
- package/package.json +2 -1
- package/packages/fokus-components/scss/components/_alerts.scss +128 -6
- package/packages/fokus-components/scss/components/_badges.scss +26 -10
- package/packages/fokus-components/scss/components/_index.scss +1 -0
- package/packages/fokus-components/scss/components/_popover.scss +131 -21
- package/packages/fokus-components/scss/components/_responsive.scss +23 -0
- package/packages/fokus-components/scss/components/_tag.scss +37 -14
- package/packages/fokus-components/scss/components/_toasts.scss +156 -11
- package/packages/fokus-components/scss/components/_tooltips.scss +70 -18
- package/packages/fokus-components/scss/forms/_check-radio-switch.scss +313 -215
- package/packages/fokus-components/scss/forms/_forms.scss +16 -0
- package/packages/fokus-core/scss/base/_accessibility.scss +14 -0
- package/packages/fokus-core/scss/tokens/_root.scss +11 -0
- package/packages/fokus-js/js/carousel.d.ts +4 -0
- package/packages/fokus-js/js/carousel.js +8 -0
- package/packages/fokus-js/js/core/positioning.js +18 -9
- package/packages/fokus-js/js/dropdown.d.ts +4 -0
- package/packages/fokus-js/js/dropdown.js +7 -0
- package/packages/fokus-js/js/popover.js +26 -1
- package/packages/fokus-js/js/tabs.d.ts +3 -0
- package/packages/fokus-js/js/tabs.js +5 -0
- package/packages/fokus-js/js/tag.js +17 -0
- package/packages/fokus-js/js/toast.js +77 -3
- package/packages/fokus-js/js/tooltip.d.ts +8 -1
- package/packages/fokus-js/js/tooltip.js +106 -13
- package/packages/fokus-utilities/scss/utilities/_api.scss +48 -0
- package/scss/entries/rtl-entry.scss +11 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
@use "../../../fokus-core/scss/tools" as *;
|
|
3
3
|
|
|
4
4
|
@layer components {
|
|
5
|
+
.fs-form-fieldset {
|
|
6
|
+
min-inline-size: 0;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: var(--fs-space-3);
|
|
9
|
+
border: var(--fs-border-width) solid var(--fs-color-border);
|
|
10
|
+
border-radius: var(--fs-radius-md);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.fs-form-fieldset > legend {
|
|
14
|
+
max-inline-size: 100%;
|
|
15
|
+
padding-inline: var(--fs-space-1);
|
|
16
|
+
color: var(--fs-color-text);
|
|
17
|
+
font-size: $font-size-sm;
|
|
18
|
+
font-weight: $font-weight-semibold;
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
.fs-form-row {
|
|
6
22
|
display: flex;
|
|
7
23
|
flex-direction: row;
|
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
outline: 2px solid CanvasText;
|
|
11
11
|
outline-offset: 2px;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
button,
|
|
15
|
+
[role="button"],
|
|
16
|
+
input,
|
|
17
|
+
select,
|
|
18
|
+
textarea,
|
|
19
|
+
.fs-btn {
|
|
20
|
+
border-color: ButtonText;
|
|
21
|
+
forced-color-adjust: auto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.fs-u-focus-ring:focus-visible {
|
|
25
|
+
outline: 3px solid Highlight;
|
|
26
|
+
}
|
|
13
27
|
}
|
|
14
28
|
}
|
|
15
29
|
|
|
@@ -75,6 +75,8 @@
|
|
|
75
75
|
--fs-component-overlay-backdrop: rgb(0 0 0 / 55%);
|
|
76
76
|
--fs-component-panel-radius: var(--fs-radius-md);
|
|
77
77
|
--fs-component-panel-shadow: var(--fs-shadow-lg);
|
|
78
|
+
--fs-contrast-border-width: var(--fs-border-width);
|
|
79
|
+
--fs-contrast-focus-width: var(--fs-focus-width);
|
|
78
80
|
|
|
79
81
|
// Referência dos limiares usados pelos utilitários `@container` (ver
|
|
80
82
|
// `packages/fokus-utilities/scss/utilities/_container-queries.scss`).
|
|
@@ -128,4 +130,13 @@
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
}
|
|
133
|
+
|
|
134
|
+
@media (prefers-contrast: more) {
|
|
135
|
+
:root {
|
|
136
|
+
--fs-contrast-border-width: 2px;
|
|
137
|
+
--fs-contrast-focus-width: 3px;
|
|
138
|
+
--fs-focus-width: var(--fs-contrast-focus-width);
|
|
139
|
+
--fs-border-width: var(--fs-contrast-border-width);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
131
142
|
}
|
|
@@ -4,6 +4,10 @@ export class Carousel {
|
|
|
4
4
|
constructor(carouselEl: HTMLElement);
|
|
5
5
|
|
|
6
6
|
static getInstance(el: Element): Carousel | undefined;
|
|
7
|
+
static getOrCreateInstance(el: Element): Carousel;
|
|
8
|
+
static next(el: Element): void;
|
|
9
|
+
static prev(el: Element): void;
|
|
10
|
+
static goTo(el: Element, index: number): void;
|
|
7
11
|
|
|
8
12
|
next(): void;
|
|
9
13
|
prev(): void;
|
|
@@ -91,6 +91,11 @@ export class Carousel {
|
|
|
91
91
|
return instances.get(el);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
static getOrCreateInstance(el) { return this.getInstance(el) ?? new Carousel(el); }
|
|
95
|
+
static next(el) { return this.getOrCreateInstance(el).next(); }
|
|
96
|
+
static prev(el) { return this.getOrCreateInstance(el).prev(); }
|
|
97
|
+
static goTo(el, index) { return this.getOrCreateInstance(el).goTo(index); }
|
|
98
|
+
|
|
94
99
|
_isDocumentVisible() {
|
|
95
100
|
return document.visibilityState !== "hidden";
|
|
96
101
|
}
|
|
@@ -165,6 +170,9 @@ export class Carousel {
|
|
|
165
170
|
|
|
166
171
|
this.index = to;
|
|
167
172
|
this._render();
|
|
173
|
+
this.carouselEl.dispatchEvent(
|
|
174
|
+
new CustomEvent("fs:shown", { bubbles: true, detail: { from, to } }),
|
|
175
|
+
);
|
|
168
176
|
this.carouselEl.dispatchEvent(
|
|
169
177
|
new CustomEvent("fs:carousel:slid", { bubbles: true, detail: { from, to } }),
|
|
170
178
|
);
|
|
@@ -32,21 +32,18 @@ function alignCrossAxis(align, referenceStart, referenceSize, floatingSize) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function computePosition(referenceEl, floatingEl, options = {}) {
|
|
35
|
-
const { placement = "bottom", align = "center", offset = 8, padding = 8 } = options;
|
|
35
|
+
const { placement = "bottom", align = "center", offset = 8, padding = 8, fallbackPlacements } = options;
|
|
36
36
|
|
|
37
37
|
const referenceRect = referenceEl.getBoundingClientRect();
|
|
38
38
|
const floatingRect = floatingEl.getBoundingClientRect();
|
|
39
39
|
const viewportWidth = document.documentElement.clientWidth;
|
|
40
40
|
const viewportHeight = document.documentElement.clientHeight;
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
) {
|
|
48
|
-
finalPlacement = OPPOSITE_PLACEMENT[placement];
|
|
49
|
-
}
|
|
42
|
+
const candidates = [...new Set(fallbackPlacements ?? [placement, OPPOSITE_PLACEMENT[placement], "bottom", "top", "right", "left"])]
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
const primaryCandidates = candidates.slice(0, 2);
|
|
45
|
+
const finalPlacement = primaryCandidates.find((candidate) => fitsPlacement(candidate, referenceRect, floatingRect, offset, viewportWidth, viewportHeight))
|
|
46
|
+
?? placement;
|
|
50
47
|
|
|
51
48
|
let top;
|
|
52
49
|
let left;
|
|
@@ -70,10 +67,18 @@ export function computePosition(referenceEl, floatingEl, options = {}) {
|
|
|
70
67
|
left = Math.min(Math.max(left, padding), maxLeft);
|
|
71
68
|
top = Math.min(Math.max(top, padding), maxTop);
|
|
72
69
|
|
|
70
|
+
const crossSize = finalPlacement === "top" || finalPlacement === "bottom" ? floatingRect.width : floatingRect.height;
|
|
71
|
+
const crossStart = finalPlacement === "top" || finalPlacement === "bottom" ? left : top;
|
|
72
|
+
const referenceCenter = finalPlacement === "top" || finalPlacement === "bottom"
|
|
73
|
+
? referenceRect.left + referenceRect.width / 2
|
|
74
|
+
: referenceRect.top + referenceRect.height / 2;
|
|
75
|
+
const crossOffset = Math.min(Math.max(referenceCenter - crossStart, 8), Math.max(8, crossSize - 8));
|
|
76
|
+
|
|
73
77
|
return {
|
|
74
78
|
top: top + window.scrollY,
|
|
75
79
|
left: left + window.scrollX,
|
|
76
80
|
placement: finalPlacement,
|
|
81
|
+
arrowOffset: `${crossOffset}px`,
|
|
77
82
|
};
|
|
78
83
|
}
|
|
79
84
|
|
|
@@ -109,4 +114,8 @@ export function applyPosition(floatingEl, position) {
|
|
|
109
114
|
floatingEl.style.position = "absolute";
|
|
110
115
|
floatingEl.style.top = `${position.top}px`;
|
|
111
116
|
floatingEl.style.left = `${position.left}px`;
|
|
117
|
+
if (position.arrowOffset) {
|
|
118
|
+
floatingEl.style.setProperty("--fs-popover-arrow-offset", position.arrowOffset);
|
|
119
|
+
floatingEl.style.setProperty("--fs-tooltip-arrow-offset", position.arrowOffset);
|
|
120
|
+
}
|
|
112
121
|
}
|
|
@@ -13,6 +13,10 @@ export class Dropdown {
|
|
|
13
13
|
constructor(toggleEl: HTMLElement, options?: DropdownOptions);
|
|
14
14
|
|
|
15
15
|
static getInstance(el: Element): Dropdown | undefined;
|
|
16
|
+
static getOrCreateInstance(el: Element, options?: DropdownOptions): Dropdown;
|
|
17
|
+
static show(el: Element, options?: DropdownOptions): void;
|
|
18
|
+
static hide(el: Element): void;
|
|
19
|
+
static toggle(el: Element, options?: DropdownOptions): void;
|
|
16
20
|
|
|
17
21
|
show(): void;
|
|
18
22
|
hide(): void;
|
|
@@ -57,6 +57,11 @@ export class Dropdown {
|
|
|
57
57
|
return instances.get(el);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
static getOrCreateInstance(el, options = {}) { return this.getInstance(el) ?? new Dropdown(el, options); }
|
|
61
|
+
static show(el, options = {}) { return this.getOrCreateInstance(el, options).show(); }
|
|
62
|
+
static hide(el) { return this.getInstance(el)?.hide(); }
|
|
63
|
+
static toggle(el, options = {}) { return this.getOrCreateInstance(el, options).toggle(); }
|
|
64
|
+
|
|
60
65
|
_handleToggleClick(event) {
|
|
61
66
|
event.preventDefault();
|
|
62
67
|
this.toggle();
|
|
@@ -127,6 +132,7 @@ export class Dropdown {
|
|
|
127
132
|
this.hide();
|
|
128
133
|
});
|
|
129
134
|
|
|
135
|
+
this.toggleEl.dispatchEvent(new CustomEvent("fs:shown", { bubbles: true }));
|
|
130
136
|
this.toggleEl.dispatchEvent(new CustomEvent("fs:dropdown:shown", { bubbles: true }));
|
|
131
137
|
}
|
|
132
138
|
|
|
@@ -145,6 +151,7 @@ export class Dropdown {
|
|
|
145
151
|
this._positionCleanup?.();
|
|
146
152
|
this._positionCleanup = null;
|
|
147
153
|
|
|
154
|
+
this.toggleEl.dispatchEvent(new CustomEvent("fs:hidden", { bubbles: true }));
|
|
148
155
|
this.toggleEl.dispatchEvent(new CustomEvent("fs:dropdown:hidden", { bubbles: true }));
|
|
149
156
|
}
|
|
150
157
|
|
|
@@ -6,6 +6,7 @@ import { autoInit, createInstanceRegistry } from "./core/register.js";
|
|
|
6
6
|
const instances = createInstanceRegistry();
|
|
7
7
|
let idCounter = 0;
|
|
8
8
|
const HOVER_HIDE_DELAY = 100;
|
|
9
|
+
const openPopovers = new Set();
|
|
9
10
|
|
|
10
11
|
export class Popover {
|
|
11
12
|
constructor(triggerEl, options = {}) {
|
|
@@ -25,6 +26,7 @@ export class Popover {
|
|
|
25
26
|
this._outsideClickCleanup = null;
|
|
26
27
|
this._removeEscapeListener = null;
|
|
27
28
|
this._hideTimer = null;
|
|
29
|
+
this._suppressNextFocus = false;
|
|
28
30
|
this._positionCleanup = null;
|
|
29
31
|
this._originalParent = popoverEl.parentNode;
|
|
30
32
|
this._originalNextSibling = popoverEl.nextSibling;
|
|
@@ -43,6 +45,12 @@ export class Popover {
|
|
|
43
45
|
popoverEl.setAttribute("aria-labelledby", headerEl.id);
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
const descriptionEl = popoverEl.querySelector(".fs-popover-text");
|
|
49
|
+
if (descriptionEl) {
|
|
50
|
+
if (!descriptionEl.id) descriptionEl.id = `fokus-popover-text-${idCounter}`;
|
|
51
|
+
popoverEl.setAttribute("aria-describedby", descriptionEl.id);
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
triggerEl.setAttribute("aria-controls", popoverEl.id);
|
|
47
55
|
triggerEl.setAttribute("aria-expanded", "false");
|
|
48
56
|
|
|
@@ -80,7 +88,13 @@ export class Popover {
|
|
|
80
88
|
this.popoverEl.addEventListener("mouseenter", this._handleMouseEnter);
|
|
81
89
|
this.popoverEl.addEventListener("mouseleave", this._handleMouseLeave);
|
|
82
90
|
} else if (this.trigger === "focus") {
|
|
83
|
-
this._handleTriggerFocus = () =>
|
|
91
|
+
this._handleTriggerFocus = () => {
|
|
92
|
+
if (this._suppressNextFocus) {
|
|
93
|
+
this._suppressNextFocus = false;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.show();
|
|
97
|
+
};
|
|
84
98
|
this._handleTriggerFocusOut = (event) => {
|
|
85
99
|
if (this.popoverEl.contains(event.relatedTarget)) return;
|
|
86
100
|
this.hide();
|
|
@@ -105,7 +119,12 @@ export class Popover {
|
|
|
105
119
|
|
|
106
120
|
_handleDismissClick(event) {
|
|
107
121
|
if (event.target.closest('[data-fs-dismiss="popover"]')) {
|
|
122
|
+
const restoreFocus = this.popoverEl.contains(document.activeElement);
|
|
123
|
+
if (restoreFocus && this.trigger === "focus") this._suppressNextFocus = true;
|
|
108
124
|
this.hide();
|
|
125
|
+
if (restoreFocus && typeof this.triggerEl.focus === "function") {
|
|
126
|
+
this.triggerEl.focus();
|
|
127
|
+
}
|
|
109
128
|
}
|
|
110
129
|
}
|
|
111
130
|
|
|
@@ -122,6 +141,10 @@ export class Popover {
|
|
|
122
141
|
|
|
123
142
|
this.popoverEl.classList.add("is-open");
|
|
124
143
|
|
|
144
|
+
openPopovers.forEach((popover) => {
|
|
145
|
+
if (popover !== this) popover.hide();
|
|
146
|
+
});
|
|
147
|
+
|
|
125
148
|
const position = computePosition(this.triggerEl, this.popoverEl, {
|
|
126
149
|
placement: this.placement,
|
|
127
150
|
align: this.align,
|
|
@@ -134,6 +157,7 @@ export class Popover {
|
|
|
134
157
|
offset: 10,
|
|
135
158
|
});
|
|
136
159
|
this.popoverEl.setAttribute("data-placement", position.placement);
|
|
160
|
+
openPopovers.add(this);
|
|
137
161
|
|
|
138
162
|
this.triggerEl.setAttribute("aria-expanded", "true");
|
|
139
163
|
|
|
@@ -150,6 +174,7 @@ export class Popover {
|
|
|
150
174
|
hide() {
|
|
151
175
|
if (!this.isOpen) return;
|
|
152
176
|
this.isOpen = false;
|
|
177
|
+
openPopovers.delete(this);
|
|
153
178
|
|
|
154
179
|
this.popoverEl.classList.remove("is-open");
|
|
155
180
|
this._positionCleanup?.();
|
|
@@ -4,6 +4,9 @@ export class Tabs {
|
|
|
4
4
|
constructor(tablistEl: HTMLElement);
|
|
5
5
|
|
|
6
6
|
static getInstance(el: Element): Tabs | undefined;
|
|
7
|
+
static getOrCreateInstance(el: Element): Tabs;
|
|
8
|
+
static show(el: Element, tab: HTMLElement): void;
|
|
9
|
+
static dispose(el: Element): void;
|
|
7
10
|
|
|
8
11
|
show(tab: HTMLElement): void;
|
|
9
12
|
dispose(): void;
|
|
@@ -45,6 +45,10 @@ export class Tabs {
|
|
|
45
45
|
return instances.get(el);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
static getOrCreateInstance(el) { return this.getInstance(el) ?? new Tabs(el); }
|
|
49
|
+
static show(el, tab) { return this.getOrCreateInstance(el).show(tab); }
|
|
50
|
+
static dispose(el) { return this.getInstance(el)?.dispose(); }
|
|
51
|
+
|
|
48
52
|
_handleClick(event) {
|
|
49
53
|
const tab = event.target.closest(".fs-nav-link:not(.is-disabled)");
|
|
50
54
|
if (!tab || !this.tabs.includes(tab)) return;
|
|
@@ -86,6 +90,7 @@ export class Tabs {
|
|
|
86
90
|
if (pane) pane.hidden = !isActive;
|
|
87
91
|
});
|
|
88
92
|
|
|
93
|
+
tab.dispatchEvent(new CustomEvent("fs:shown", { bubbles: true }));
|
|
89
94
|
tab.dispatchEvent(
|
|
90
95
|
new CustomEvent("fs:tab:changed", { bubbles: true, detail: { target: tab.getAttribute("data-fs-target") } }),
|
|
91
96
|
);
|
|
@@ -33,14 +33,31 @@ export class Tag {
|
|
|
33
33
|
dismiss() {
|
|
34
34
|
if (this._isLoading) return false;
|
|
35
35
|
|
|
36
|
+
const dismissButton = this.tagEl.querySelector('[data-fs-dismiss="tag"]');
|
|
37
|
+
const shouldRestoreFocus = dismissButton && document.activeElement === dismissButton;
|
|
36
38
|
const event = new CustomEvent("fs:tag:dismissed", { bubbles: true, cancelable: true });
|
|
37
39
|
this.tagEl.dispatchEvent(event);
|
|
38
40
|
if (event.defaultPrevented) return false;
|
|
39
41
|
|
|
42
|
+
const focusTarget = shouldRestoreFocus ? this._getFocusTarget() : null;
|
|
40
43
|
this.tagEl.remove();
|
|
44
|
+
focusTarget?.focus();
|
|
41
45
|
return true;
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
_getFocusTarget() {
|
|
49
|
+
const group = this.tagEl.closest(".fs-tag-group");
|
|
50
|
+
if (!group) return null;
|
|
51
|
+
|
|
52
|
+
const buttons = [...group.querySelectorAll('[data-fs-dismiss="tag"]')]
|
|
53
|
+
.filter((button) => !button.disabled && !button.hidden && button.getAttribute("aria-hidden") !== "true");
|
|
54
|
+
const currentButton = this.tagEl.querySelector('[data-fs-dismiss="tag"]');
|
|
55
|
+
const currentIndex = buttons.indexOf(currentButton);
|
|
56
|
+
const remaining = buttons.filter((button) => button !== currentButton);
|
|
57
|
+
|
|
58
|
+
return remaining[currentIndex] || remaining[currentIndex - 1] || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
// Estado útil enquanto uma remoção ou atualização assíncrona está pendente.
|
|
45
62
|
// Além da aparência, bloqueia o dismiss e comunica o estado a AT.
|
|
46
63
|
setLoading(loading) {
|
|
@@ -10,13 +10,39 @@ export class Toast {
|
|
|
10
10
|
this.autohide = toastEl.getAttribute("data-autohide") !== "false";
|
|
11
11
|
this.isOpen = false;
|
|
12
12
|
this._hideTimer = null;
|
|
13
|
+
this._paused = false;
|
|
14
|
+
this._remaining = this.delay;
|
|
15
|
+
this._startedAt = 0;
|
|
16
|
+
this._progressTimer = null;
|
|
13
17
|
|
|
14
18
|
toastEl.setAttribute("role", "status");
|
|
15
19
|
toastEl.setAttribute("aria-live", "polite");
|
|
16
20
|
toastEl.style.display = "none";
|
|
17
21
|
|
|
18
22
|
this._handleDismissClick = this._handleDismissClick.bind(this);
|
|
23
|
+
this._pause = () => {
|
|
24
|
+
if (!this.isOpen || this._paused) return;
|
|
25
|
+
this._paused = true;
|
|
26
|
+
clearTimeout(this._hideTimer);
|
|
27
|
+
if (this.autohide && this._startedAt) {
|
|
28
|
+
this._remaining = Math.max(0, this._remaining - (Date.now() - this._startedAt));
|
|
29
|
+
}
|
|
30
|
+
this._updateProgress();
|
|
31
|
+
clearInterval(this._progressTimer);
|
|
32
|
+
this.toastEl.classList.add("is-paused");
|
|
33
|
+
};
|
|
34
|
+
this._resume = () => {
|
|
35
|
+
if (!this.isOpen || !this._paused) return;
|
|
36
|
+
this._paused = false;
|
|
37
|
+
this.toastEl.classList.remove("is-paused");
|
|
38
|
+
this._startProgress();
|
|
39
|
+
if (this.autohide) this._scheduleHide();
|
|
40
|
+
};
|
|
19
41
|
toastEl.addEventListener("click", this._handleDismissClick);
|
|
42
|
+
toastEl.addEventListener("mouseenter", this._pause);
|
|
43
|
+
toastEl.addEventListener("mouseleave", this._resume);
|
|
44
|
+
toastEl.addEventListener("focusin", this._pause);
|
|
45
|
+
toastEl.addEventListener("focusout", this._resume);
|
|
20
46
|
|
|
21
47
|
instances.set(toastEl, this);
|
|
22
48
|
}
|
|
@@ -35,17 +61,52 @@ export class Toast {
|
|
|
35
61
|
if (this.isOpen) return;
|
|
36
62
|
this.toastEl.dispatchEvent(new CustomEvent("fs:show", { bubbles: true }));
|
|
37
63
|
this.isOpen = true;
|
|
64
|
+
this._remaining = this.delay;
|
|
65
|
+
this._startedAt = Date.now();
|
|
66
|
+
this.toastEl.style.setProperty("--fs-toast-duration", `${this.delay}ms`);
|
|
67
|
+
|
|
68
|
+
const expansion = expand(this.toastEl);
|
|
69
|
+
this.toastEl.classList.remove("is-progressing");
|
|
70
|
+
void this.toastEl.offsetWidth;
|
|
71
|
+
if (this.toastEl.getAttribute("data-toast-progress") === "true" && this.autohide) {
|
|
72
|
+
this.toastEl.classList.add("is-progressing");
|
|
73
|
+
this._startProgress();
|
|
74
|
+
}
|
|
38
75
|
|
|
39
|
-
|
|
76
|
+
expansion.then(() => {
|
|
40
77
|
this.toastEl.dispatchEvent(new CustomEvent("fs:shown", { bubbles: true }));
|
|
41
78
|
this.toastEl.dispatchEvent(new CustomEvent("fs:toast:shown", { bubbles: true }));
|
|
42
79
|
});
|
|
43
80
|
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
this._scheduleHide();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_scheduleHide() {
|
|
85
|
+
clearTimeout(this._hideTimer);
|
|
86
|
+
if (this.autohide && !this._paused) {
|
|
87
|
+
this._startedAt = Date.now();
|
|
88
|
+
this._hideTimer = setTimeout(() => this.hide(), this._remaining);
|
|
46
89
|
}
|
|
47
90
|
}
|
|
48
91
|
|
|
92
|
+
_updateProgress() {
|
|
93
|
+
if (this.toastEl.getAttribute("data-toast-progress") !== "true" || !this.autohide) return;
|
|
94
|
+
const percentage = Math.max(0, Math.min(100, (this._remaining / this.delay) * 100));
|
|
95
|
+
const progressEl = this.toastEl.querySelector(".fs-toast-progress");
|
|
96
|
+
if (progressEl) progressEl.style.inlineSize = `${percentage}%`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_startProgress() {
|
|
100
|
+
if (this.toastEl.getAttribute("data-toast-progress") !== "true" || !this.autohide) return;
|
|
101
|
+
clearInterval(this._progressTimer);
|
|
102
|
+
this._updateProgress();
|
|
103
|
+
this._progressTimer = setInterval(() => {
|
|
104
|
+
if (!this.isOpen || this._paused) return;
|
|
105
|
+
this._remaining = Math.max(0, this.delay - (Date.now() - this._startedAt));
|
|
106
|
+
this._updateProgress();
|
|
107
|
+
}, 50);
|
|
108
|
+
}
|
|
109
|
+
|
|
49
110
|
hide() {
|
|
50
111
|
if (!this.isOpen) return;
|
|
51
112
|
this.toastEl.dispatchEvent(new CustomEvent("fs:hide", { bubbles: true }));
|
|
@@ -53,6 +114,14 @@ export class Toast {
|
|
|
53
114
|
|
|
54
115
|
clearTimeout(this._hideTimer);
|
|
55
116
|
this._hideTimer = null;
|
|
117
|
+
clearInterval(this._progressTimer);
|
|
118
|
+
this._progressTimer = null;
|
|
119
|
+
this._startedAt = 0;
|
|
120
|
+
this._remaining = this.delay;
|
|
121
|
+
this.toastEl.classList.remove("is-paused");
|
|
122
|
+
this.toastEl.classList.remove("is-progressing");
|
|
123
|
+
const progressEl = this.toastEl.querySelector(".fs-toast-progress");
|
|
124
|
+
if (progressEl) progressEl.style.removeProperty("inline-size");
|
|
56
125
|
|
|
57
126
|
collapse(this.toastEl).then(() => {
|
|
58
127
|
this.toastEl.dispatchEvent(new CustomEvent("fs:hidden", { bubbles: true }));
|
|
@@ -70,7 +139,12 @@ export class Toast {
|
|
|
70
139
|
|
|
71
140
|
dispose() {
|
|
72
141
|
clearTimeout(this._hideTimer);
|
|
142
|
+
clearInterval(this._progressTimer);
|
|
73
143
|
this.toastEl.removeEventListener("click", this._handleDismissClick);
|
|
144
|
+
this.toastEl.removeEventListener("mouseenter", this._pause);
|
|
145
|
+
this.toastEl.removeEventListener("mouseleave", this._resume);
|
|
146
|
+
this.toastEl.removeEventListener("focusin", this._pause);
|
|
147
|
+
this.toastEl.removeEventListener("focusout", this._resume);
|
|
74
148
|
instances.delete(this.toastEl);
|
|
75
149
|
}
|
|
76
150
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { Placement } from "./core/positioning.js";
|
|
2
2
|
|
|
3
|
+
export type TooltipAlign = "start" | "center" | "end";
|
|
4
|
+
export type TooltipPlacement = Placement | `${Placement}-${Exclude<TooltipAlign, "center">}`;
|
|
5
|
+
|
|
3
6
|
export interface TooltipOptions {
|
|
4
|
-
placement?:
|
|
7
|
+
placement?: TooltipPlacement;
|
|
8
|
+
align?: TooltipAlign;
|
|
9
|
+
offset?: number;
|
|
5
10
|
title?: string;
|
|
11
|
+
showDelay?: number;
|
|
12
|
+
hideDelay?: number;
|
|
6
13
|
}
|
|
7
14
|
|
|
8
15
|
export class Tooltip {
|
|
@@ -4,6 +4,19 @@ import { autoInit, createInstanceRegistry } from "./core/register.js";
|
|
|
4
4
|
|
|
5
5
|
const instances = createInstanceRegistry();
|
|
6
6
|
let idCounter = 0;
|
|
7
|
+
let openInstance = null;
|
|
8
|
+
const DEFAULT_TOOLTIP_OFFSET = 8;
|
|
9
|
+
|
|
10
|
+
function toDelay(value, fallback = 0) {
|
|
11
|
+
if (value === null || value === undefined || value === "") return fallback;
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parsePlacement(value = "top") {
|
|
17
|
+
const [placement, align = "center"] = value.split("-");
|
|
18
|
+
return { placement, align: ["start", "center", "end"].includes(align) ? align : "center" };
|
|
19
|
+
}
|
|
7
20
|
|
|
8
21
|
export class Tooltip {
|
|
9
22
|
constructor(referenceEl, options = {}) {
|
|
@@ -19,10 +32,18 @@ export class Tooltip {
|
|
|
19
32
|
|
|
20
33
|
this.referenceEl = referenceEl;
|
|
21
34
|
this.title = title;
|
|
22
|
-
|
|
35
|
+
const parsedPlacement = parsePlacement(options.placement ?? referenceEl.getAttribute("data-placement") ?? "top");
|
|
36
|
+
this.placement = parsedPlacement.placement;
|
|
37
|
+
this.align = options.align ?? referenceEl.getAttribute("data-align") ?? parsedPlacement.align;
|
|
38
|
+
this.offset = toDelay(options.offset ?? referenceEl.getAttribute("data-offset"), DEFAULT_TOOLTIP_OFFSET);
|
|
39
|
+
this.showDelay = toDelay(options.showDelay ?? referenceEl.getAttribute("data-show-delay"), 0);
|
|
40
|
+
this.hideDelay = toDelay(options.hideDelay ?? referenceEl.getAttribute("data-hide-delay"));
|
|
23
41
|
this.isOpen = false;
|
|
24
42
|
this.id = `fokus-tooltip-${idCounter}`;
|
|
25
43
|
this._positionCleanup = null;
|
|
44
|
+
this._showTimer = null;
|
|
45
|
+
this._hideTimer = null;
|
|
46
|
+
this._touchOpened = false;
|
|
26
47
|
this._originalDescribedBy = referenceEl.getAttribute("aria-describedby");
|
|
27
48
|
|
|
28
49
|
this.tooltipEl = document.createElement("div");
|
|
@@ -33,16 +54,36 @@ export class Tooltip {
|
|
|
33
54
|
this.tooltipEl.querySelector(".fs-tooltip-inner").textContent = this.title;
|
|
34
55
|
document.body.appendChild(this.tooltipEl);
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this._handleShow = this.
|
|
39
|
-
this._handleHide = this.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
this._addDescribedBy();
|
|
58
|
+
|
|
59
|
+
this._handleShow = () => this._scheduleShow(true);
|
|
60
|
+
this._handleHide = () => this._scheduleHide();
|
|
61
|
+
|
|
62
|
+
this._handlePointerEnter = () => this._scheduleShow();
|
|
63
|
+
this._handlePointerLeave = () => this._scheduleHide();
|
|
64
|
+
this._handlePointerDown = (event) => {
|
|
65
|
+
if (event.pointerType === "touch") {
|
|
66
|
+
if (this._touchOpened && this.isOpen) {
|
|
67
|
+
this.hideImmediate();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this._touchOpened = true;
|
|
71
|
+
this._scheduleShow(true);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
referenceEl.addEventListener("pointerenter", this._handlePointerEnter);
|
|
75
|
+
referenceEl.addEventListener("pointerleave", this._handlePointerLeave);
|
|
76
|
+
referenceEl.addEventListener("pointerdown", this._handlePointerDown);
|
|
77
|
+
referenceEl.addEventListener("mouseenter", this._handlePointerEnter);
|
|
78
|
+
referenceEl.addEventListener("mouseleave", this._handlePointerLeave);
|
|
43
79
|
referenceEl.addEventListener("focus", this._handleShow);
|
|
44
80
|
referenceEl.addEventListener("blur", this._handleHide);
|
|
45
81
|
|
|
82
|
+
this._handleDocumentPointerDown = (event) => {
|
|
83
|
+
if (this._touchOpened && !this.referenceEl.contains(event.target) && !this.tooltipEl.contains(event.target)) this.hide();
|
|
84
|
+
};
|
|
85
|
+
document.addEventListener("pointerdown", this._handleDocumentPointerDown, true);
|
|
86
|
+
|
|
46
87
|
this._removeEscapeListener = onEscapeKey(() => {
|
|
47
88
|
if (this.isOpen) this.hide();
|
|
48
89
|
});
|
|
@@ -54,8 +95,46 @@ export class Tooltip {
|
|
|
54
95
|
return instances.get(el);
|
|
55
96
|
}
|
|
56
97
|
|
|
98
|
+
_clearTimers() {
|
|
99
|
+
clearTimeout(this._showTimer);
|
|
100
|
+
clearTimeout(this._hideTimer);
|
|
101
|
+
this._showTimer = null;
|
|
102
|
+
this._hideTimer = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_addDescribedBy() {
|
|
106
|
+
const ids = new Set((this.referenceEl.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean));
|
|
107
|
+
ids.add(this.id);
|
|
108
|
+
this.referenceEl.setAttribute("aria-describedby", [...ids].join(" "));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_scheduleShow(immediate = false) {
|
|
112
|
+
this._clearTimers();
|
|
113
|
+
if (this.isOpen) return;
|
|
114
|
+
const delay = immediate ? 0 : this.showDelay;
|
|
115
|
+
if (delay === 0) {
|
|
116
|
+
this.show();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this._showTimer = setTimeout(() => this.show(), delay);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_scheduleHide(immediate = false) {
|
|
123
|
+
this._clearTimers();
|
|
124
|
+
if (!this.isOpen) return;
|
|
125
|
+
const delay = immediate ? 0 : this.hideDelay;
|
|
126
|
+
if (delay === 0) {
|
|
127
|
+
this.hide();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this._hideTimer = setTimeout(() => this.hide(), delay);
|
|
131
|
+
}
|
|
132
|
+
|
|
57
133
|
show() {
|
|
134
|
+
this._clearTimers();
|
|
58
135
|
if (this.isOpen) return;
|
|
136
|
+
if (openInstance && openInstance !== this) openInstance.hide(true);
|
|
137
|
+
openInstance = this;
|
|
59
138
|
this.isOpen = true;
|
|
60
139
|
|
|
61
140
|
const theme = this.referenceEl.closest("[data-theme]")?.getAttribute("data-theme");
|
|
@@ -67,26 +146,35 @@ export class Tooltip {
|
|
|
67
146
|
|
|
68
147
|
this.tooltipEl.classList.add("is-open");
|
|
69
148
|
|
|
70
|
-
const position = computePosition(this.referenceEl, this.tooltipEl, { placement: this.placement, offset:
|
|
149
|
+
const position = computePosition(this.referenceEl, this.tooltipEl, { placement: this.placement, align: this.align, offset: this.offset });
|
|
71
150
|
applyPosition(this.tooltipEl, position);
|
|
72
151
|
this._positionCleanup = watchPosition(this.referenceEl, this.tooltipEl, {
|
|
73
152
|
placement: this.placement,
|
|
74
|
-
|
|
153
|
+
align: this.align,
|
|
154
|
+
offset: this.offset,
|
|
75
155
|
});
|
|
76
156
|
this.tooltipEl.setAttribute("data-placement", position.placement);
|
|
157
|
+
this.tooltipEl.setAttribute("data-align", this.align);
|
|
77
158
|
|
|
78
159
|
this.referenceEl.dispatchEvent(new CustomEvent("fs:tooltip:shown", { bubbles: true }));
|
|
79
160
|
}
|
|
80
161
|
|
|
81
162
|
hide() {
|
|
163
|
+
this.hideImmediate();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
hideImmediate() {
|
|
167
|
+
this._clearTimers();
|
|
82
168
|
if (!this.isOpen) return;
|
|
83
169
|
this.isOpen = false;
|
|
170
|
+
this._touchOpened = false;
|
|
84
171
|
|
|
85
172
|
this.tooltipEl.classList.remove("is-open");
|
|
86
173
|
this._positionCleanup?.();
|
|
87
174
|
this._positionCleanup = null;
|
|
88
175
|
|
|
89
176
|
this.referenceEl.dispatchEvent(new CustomEvent("fs:tooltip:hidden", { bubbles: true }));
|
|
177
|
+
if (openInstance === this) openInstance = null;
|
|
90
178
|
}
|
|
91
179
|
|
|
92
180
|
toggle() {
|
|
@@ -98,11 +186,16 @@ export class Tooltip {
|
|
|
98
186
|
}
|
|
99
187
|
|
|
100
188
|
dispose() {
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
103
|
-
this.referenceEl.removeEventListener("
|
|
189
|
+
this.hideImmediate();
|
|
190
|
+
this._clearTimers();
|
|
191
|
+
this.referenceEl.removeEventListener("pointerenter", this._handlePointerEnter);
|
|
192
|
+
this.referenceEl.removeEventListener("pointerleave", this._handlePointerLeave);
|
|
193
|
+
this.referenceEl.removeEventListener("pointerdown", this._handlePointerDown);
|
|
194
|
+
this.referenceEl.removeEventListener("mouseenter", this._handlePointerEnter);
|
|
195
|
+
this.referenceEl.removeEventListener("mouseleave", this._handlePointerLeave);
|
|
104
196
|
this.referenceEl.removeEventListener("focus", this._handleShow);
|
|
105
197
|
this.referenceEl.removeEventListener("blur", this._handleHide);
|
|
198
|
+
document.removeEventListener("pointerdown", this._handleDocumentPointerDown, true);
|
|
106
199
|
this._removeEscapeListener();
|
|
107
200
|
if (this._originalDescribedBy === null) this.referenceEl.removeAttribute("aria-describedby");
|
|
108
201
|
else this.referenceEl.setAttribute("aria-describedby", this._originalDescribedBy);
|