srcdev-nuxt-components 9.4.7 → 9.4.9
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/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/output.html +1 -1
- package/.claude/skills/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/header-block.md +92 -0
- package/.claude/skills/components/rotating-carousel-image.md +101 -0
- package/.claude/skills/components/skip-links.md +82 -0
- package/.claude/skills/components/tabs-core.md +187 -0
- package/.claude/skills/index.md +4 -0
- package/.vscode/srcdev-component-header-block.code-snippets +41 -0
- package/.vscode/srcdev-component-rotating-carousel-image.code-snippets +74 -0
- package/.vscode/srcdev-component-skip-links.code-snippets +55 -0
- package/.vscode/srcdev-component-tabs-core.code-snippets +78 -0
- package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-heading.css +2 -1
- package/app/components/01.atoms/animations/rotating-carousel-image/CONSUMER-STYLING.md +54 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/RotatingCarouselImage.vue +315 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/stories/RotatingCarouselImage.stories.ts +156 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/tests/RotatingCarouselImage.spec.ts +230 -0
- package/app/components/01.atoms/animations/rotating-carousel-image/tests/__snapshots__/RotatingCarouselImage.spec.ts.snap +19 -0
- package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +31 -0
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +98 -0
- package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +113 -0
- package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +71 -0
- package/app/components/01.atoms/navigation/skip-links/tests/__snapshots__/SkipLinks.spec.ts.snap +15 -0
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +99 -0
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +272 -0
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +199 -0
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +274 -0
- package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +43 -0
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +50 -0
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +111 -0
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +119 -0
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +5 -0
- package/app/components/03.organisms/site-header/SiteHeader.vue +1 -1
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +1 -1
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +3 -3
- package/app/composables/useTabs.ts +225 -207
- package/app/types/components/index.ts +2 -0
- package/app/types/components/rotating-carousel-image.d.ts +4 -0
- package/app/types/components/skip-links.d.ts +4 -0
- package/package.json +1 -1
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +0 -11
- package/app/components/rotating-carousel/RotatingCarouselImage.vue +0 -216
- package/app/components/skip-links/SkipLinks.vue +0 -60
- package/app/components/tabs/TabsCore.vue +0 -306
- package/app/components/typography/HeaderBlock.vue +0 -35
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/AutoGrid.vue +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/stories/AutoGrid.stories.ts +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/AutoGrid.spec.ts +0 -0
- /package/app/components/01.atoms/grids/{data-grid → auto-grid}/tests/__snapshots__/AutoGrid.spec.ts.snap +0 -0
|
@@ -1,291 +1,309 @@
|
|
|
1
|
-
import { useResizeObserver } from "@vueuse/core"
|
|
1
|
+
import { useResizeObserver } from "@vueuse/core";
|
|
2
|
+
|
|
3
|
+
interface UseTabsOptions {
|
|
4
|
+
trackHover: boolean;
|
|
5
|
+
trackActive: boolean;
|
|
6
|
+
trackIndicator: boolean;
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
const useTabs = (
|
|
4
10
|
axis: string,
|
|
5
11
|
tabsNavRef: Ref<HTMLElement | null>,
|
|
6
12
|
tabsContentRefs: Ref<HTMLElement[] | null>,
|
|
7
|
-
duration: number
|
|
13
|
+
duration: number,
|
|
14
|
+
options: UseTabsOptions = { trackHover: true, trackActive: true, trackIndicator: true }
|
|
8
15
|
) => {
|
|
9
|
-
const navItems = ref<HTMLElement[] | null>(null)
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
const navItems = ref<HTMLElement[] | null>(null);
|
|
17
|
+
// Plain ref, not useState: this is ephemeral per-instance UI state, not something that
|
|
18
|
+
// needs to survive SSR hydration or be shared by key — useState's fixed key here previously
|
|
19
|
+
// meant every TabsCore instance on the page silently shared the same "previous active tab".
|
|
20
|
+
const previousActiveTab = ref<HTMLElement | null>(null);
|
|
21
|
+
const currentActiveTab = ref<HTMLElement>();
|
|
12
22
|
|
|
13
|
-
const previousHoveredTab = ref<HTMLElement>()
|
|
14
|
-
const currentHoveredTab = ref<HTMLElement>()
|
|
15
|
-
const tagName = ref<string>()
|
|
23
|
+
const previousHoveredTab = ref<HTMLElement>();
|
|
24
|
+
const currentHoveredTab = ref<HTMLElement>();
|
|
25
|
+
const tagName = ref<string>();
|
|
26
|
+
|
|
27
|
+
let activeSettleTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
28
|
+
let hoveredSettleTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
16
29
|
|
|
17
30
|
const initNavDecorators = () => {
|
|
18
31
|
navItems.value = tabsNavRef.value
|
|
19
|
-
? (Array.from(tabsNavRef.value.querySelectorAll("[data-nav-item")) as HTMLElement[])
|
|
20
|
-
: []
|
|
32
|
+
? (Array.from(tabsNavRef.value.querySelectorAll("[data-nav-item]")) as HTMLElement[])
|
|
33
|
+
: [];
|
|
21
34
|
|
|
22
35
|
if (!navItems.value || navItems.value.length === 0) {
|
|
23
|
-
return
|
|
36
|
+
return;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
|
-
const firstNavItem = navItems.value[0]
|
|
27
|
-
if (!firstNavItem) return
|
|
39
|
+
const firstNavItem = navItems.value[0];
|
|
40
|
+
if (!firstNavItem) return;
|
|
28
41
|
|
|
29
|
-
tagName.value = firstNavItem.tagName.toLowerCase()
|
|
42
|
+
tagName.value = firstNavItem.tagName.toLowerCase();
|
|
30
43
|
|
|
31
|
-
const activeIndex = ref(0)
|
|
44
|
+
const activeIndex = ref(0);
|
|
32
45
|
|
|
33
46
|
// Temporarily set the first nav item as active
|
|
34
|
-
firstNavItem.setAttribute("aria-selected", "true")
|
|
47
|
+
firstNavItem.setAttribute("aria-selected", "true");
|
|
35
48
|
|
|
36
49
|
// Test if navItems are hyperlinks
|
|
37
50
|
if (firstNavItem.tagName.toLowerCase() === "a") {
|
|
38
51
|
// Find index of element with class "router-link-active"
|
|
39
|
-
activeIndex.value = navItems.value.findIndex((el) => el.classList.contains("router-link-active"))
|
|
52
|
+
activeIndex.value = navItems.value.findIndex((el) => el.classList.contains("router-link-active"));
|
|
40
53
|
}
|
|
41
|
-
// else {
|
|
42
|
-
// Set actve tab
|
|
43
|
-
// }
|
|
44
54
|
|
|
45
|
-
const activeElement = navItems.value[activeIndex.value]
|
|
46
|
-
if (!activeElement) return
|
|
55
|
+
const activeElement = navItems.value[activeIndex.value];
|
|
56
|
+
if (!activeElement) return;
|
|
47
57
|
|
|
48
|
-
currentActiveTab.value = activeElement
|
|
49
|
-
currentHoveredTab.value = activeElement
|
|
58
|
+
currentActiveTab.value = activeElement;
|
|
59
|
+
currentHoveredTab.value = activeElement;
|
|
50
60
|
|
|
51
|
-
previousActiveTab.value = activeElement
|
|
52
|
-
previousHoveredTab.value = activeElement
|
|
61
|
+
previousActiveTab.value = activeElement;
|
|
62
|
+
previousHoveredTab.value = activeElement;
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
setRovingTabindex();
|
|
65
|
+
addNavDecorators();
|
|
66
|
+
setActiveTabContent();
|
|
67
|
+
};
|
|
57
68
|
|
|
58
69
|
const addNavDecorators = () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
div.classList.add(className)
|
|
66
|
-
}
|
|
67
|
-
tabsNavRef.value.appendChild(div)
|
|
68
|
-
}
|
|
70
|
+
if (!tabsNavRef.value) return;
|
|
71
|
+
|
|
72
|
+
if (options.trackIndicator) {
|
|
73
|
+
tabsNavRef.value.appendChild(
|
|
74
|
+
Object.assign(document.createElement("div"), { className: "nav__active-indicator" })
|
|
75
|
+
);
|
|
69
76
|
}
|
|
70
|
-
|
|
77
|
+
if (options.trackActive) {
|
|
78
|
+
tabsNavRef.value.appendChild(Object.assign(document.createElement("div"), { className: "nav__active" }));
|
|
79
|
+
}
|
|
80
|
+
if (options.trackHover) {
|
|
81
|
+
tabsNavRef.value.appendChild(Object.assign(document.createElement("div"), { className: "nav__hovered" }));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const setRovingTabindex = () => {
|
|
86
|
+
navItems.value?.forEach((tab) => {
|
|
87
|
+
tab.setAttribute("tabindex", tab === currentActiveTab.value ? "0" : "-1");
|
|
88
|
+
});
|
|
89
|
+
};
|
|
71
90
|
|
|
72
91
|
const navItemHovered = (event: Event) => {
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
if (!options.trackHover) return;
|
|
93
|
+
|
|
94
|
+
const target = event.target as HTMLElement;
|
|
95
|
+
const newTabPosition = currentHoveredTab.value ? currentHoveredTab.value.compareDocumentPosition(target) : 0;
|
|
75
96
|
|
|
76
97
|
if (newTabPosition !== 0) {
|
|
77
|
-
previousHoveredTab.value = currentHoveredTab.value
|
|
78
|
-
currentHoveredTab.value = target
|
|
79
|
-
moveHoveredIndicator()
|
|
98
|
+
previousHoveredTab.value = currentHoveredTab.value;
|
|
99
|
+
currentHoveredTab.value = target;
|
|
100
|
+
moveHoveredIndicator();
|
|
80
101
|
}
|
|
81
|
-
}
|
|
102
|
+
};
|
|
82
103
|
|
|
83
104
|
const resetHoverToActivePosition = () => {
|
|
84
|
-
|
|
85
|
-
currentHoveredTab.value = currentActiveTab.value
|
|
86
|
-
moveHoveredIndicator()
|
|
87
|
-
}
|
|
105
|
+
if (!options.trackHover) return;
|
|
88
106
|
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
previousHoveredTab.value = currentHoveredTab.value;
|
|
108
|
+
currentHoveredTab.value = currentActiveTab.value;
|
|
109
|
+
moveHoveredIndicator();
|
|
110
|
+
};
|
|
91
111
|
|
|
92
|
-
|
|
93
|
-
currentActiveTab.value
|
|
112
|
+
const activateTab = (target: HTMLElement) => {
|
|
113
|
+
if (target === currentActiveTab.value) return;
|
|
114
|
+
|
|
115
|
+
previousActiveTab.value = currentActiveTab.value || null;
|
|
116
|
+
currentActiveTab.value = target;
|
|
94
117
|
|
|
95
118
|
navItems.value?.forEach((tab) => {
|
|
96
|
-
tab.setAttribute("aria-selected", currentActiveTab.value === tab ? "true" : "false")
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (element) {
|
|
128
|
-
element.classList.remove("transitioning")
|
|
129
|
-
}
|
|
130
|
-
// }, timeout * (i - navItemsArray.indexOf(previousHoveredTab.value) - 1));
|
|
131
|
-
}, duration * 1.5)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} else {
|
|
136
|
-
for (
|
|
137
|
-
let i = navItemsArray.indexOf(previousHoveredTab.value);
|
|
138
|
-
i > navItemsArray.indexOf(currentHoveredTab.value);
|
|
139
|
-
i--
|
|
140
|
-
) {
|
|
141
|
-
const element = navItemsArray[i]
|
|
142
|
-
if (element) {
|
|
143
|
-
element.classList.add("transitioning")
|
|
144
|
-
if (
|
|
145
|
-
i <= navItemsArray.indexOf(previousHoveredTab.value) &&
|
|
146
|
-
i > navItemsArray.indexOf(currentHoveredTab.value)
|
|
147
|
-
) {
|
|
148
|
-
setTimeout(() => {
|
|
149
|
-
if (element) {
|
|
150
|
-
element.classList.remove("transitioning")
|
|
151
|
-
}
|
|
152
|
-
// }, timeout * (i - navItemsArray.indexOf(previousHoveredTab.value) - 1));
|
|
153
|
-
}, duration * 1.5)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
119
|
+
tab.setAttribute("aria-selected", currentActiveTab.value === tab ? "true" : "false");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
setRovingTabindex();
|
|
123
|
+
moveActiveIndicator();
|
|
124
|
+
setActiveTabContent();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const navItemClicked = (event: Event) => {
|
|
128
|
+
activateTab(event.target as HTMLElement);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// WAI-ARIA Tabs pattern: arrow keys move focus and activate (automatic activation);
|
|
132
|
+
// Home/End jump to the first/last tab.
|
|
133
|
+
const navItemKeydown = (event: KeyboardEvent) => {
|
|
134
|
+
if (!navItems.value || navItems.value.length === 0) return;
|
|
135
|
+
|
|
136
|
+
const previousKey = axis === "y" ? "ArrowUp" : "ArrowLeft";
|
|
137
|
+
const nextKey = axis === "y" ? "ArrowDown" : "ArrowRight";
|
|
138
|
+
|
|
139
|
+
let targetIndex: number | null = null;
|
|
140
|
+
const currentIndex = currentActiveTab.value ? navItems.value.indexOf(currentActiveTab.value) : 0;
|
|
141
|
+
|
|
142
|
+
if (event.key === nextKey) {
|
|
143
|
+
targetIndex = (currentIndex + 1) % navItems.value.length;
|
|
144
|
+
} else if (event.key === previousKey) {
|
|
145
|
+
targetIndex = (currentIndex - 1 + navItems.value.length) % navItems.value.length;
|
|
146
|
+
} else if (event.key === "Home") {
|
|
147
|
+
targetIndex = 0;
|
|
148
|
+
} else if (event.key === "End") {
|
|
149
|
+
targetIndex = navItems.value.length - 1;
|
|
158
150
|
}
|
|
159
|
-
|
|
151
|
+
|
|
152
|
+
if (targetIndex === null) return;
|
|
153
|
+
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
const targetTab = navItems.value[targetIndex];
|
|
156
|
+
if (!targetTab) return;
|
|
157
|
+
|
|
158
|
+
targetTab.focus();
|
|
159
|
+
activateTab(targetTab);
|
|
160
|
+
};
|
|
160
161
|
|
|
161
162
|
const setFinalHoveredPositions = (resized: boolean = false) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
tabsNavRef.value
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
163
|
+
if (!tabsNavRef.value || !currentHoveredTab.value) return;
|
|
164
|
+
|
|
165
|
+
// Batch every layout read before any style write below, so the browser doesn't need to
|
|
166
|
+
// perform a forced synchronous reflow between each read/write pair.
|
|
167
|
+
const { offsetLeft, offsetTop, offsetWidth, offsetHeight } = currentHoveredTab.value;
|
|
168
|
+
const { offsetWidth: navWidth } = tabsNavRef.value;
|
|
169
|
+
const setDuration = resized ? 0 : duration;
|
|
170
|
+
const newTabWidth = navWidth ? offsetWidth / navWidth : 0;
|
|
171
|
+
|
|
172
|
+
const nav = tabsNavRef.value;
|
|
173
|
+
nav.style.setProperty("--_transition-duration", setDuration + "ms");
|
|
174
|
+
nav.style.setProperty("--_x-hovered", offsetLeft + "px");
|
|
175
|
+
nav.style.setProperty("--_width-hovered", newTabWidth.toString());
|
|
176
|
+
nav.style.setProperty("--_y-hovered", offsetTop + "px");
|
|
177
|
+
nav.style.setProperty("--_y-height", offsetHeight + "px");
|
|
178
|
+
nav.style.setProperty("--_y-width", offsetWidth + "px");
|
|
179
|
+
};
|
|
176
180
|
|
|
177
181
|
const setFinalActivePositions = (resized: boolean = false) => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
tabsNavRef.value
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
tabsNavRef.value
|
|
186
|
-
|
|
187
|
-
|
|
182
|
+
if (!tabsNavRef.value || !currentActiveTab.value) return;
|
|
183
|
+
|
|
184
|
+
const { offsetLeft, offsetTop, offsetWidth, offsetHeight } = currentActiveTab.value;
|
|
185
|
+
const { offsetWidth: navWidth } = tabsNavRef.value;
|
|
186
|
+
const setDuration = resized ? 0 : duration;
|
|
187
|
+
const newTabWidth = navWidth ? offsetWidth / navWidth : 0;
|
|
188
|
+
|
|
189
|
+
const nav = tabsNavRef.value;
|
|
190
|
+
nav.style.setProperty("--_transition-duration", setDuration + "ms");
|
|
191
|
+
nav.style.setProperty("--_x-active", offsetLeft + "px");
|
|
192
|
+
nav.style.setProperty("--_width-active", newTabWidth.toString());
|
|
193
|
+
nav.style.setProperty("--_y-active", offsetTop + "px");
|
|
194
|
+
nav.style.setProperty("--_y-height", offsetHeight + "px");
|
|
195
|
+
nav.style.setProperty("--_y-width", offsetWidth + "px");
|
|
196
|
+
};
|
|
188
197
|
|
|
189
198
|
const moveActiveIndicator = () => {
|
|
190
|
-
if (!tabsNavRef.value || !currentActiveTab.value) return
|
|
199
|
+
if (!tabsNavRef.value || !currentActiveTab.value) return;
|
|
200
|
+
if (!options.trackActive && !options.trackIndicator) return;
|
|
201
|
+
|
|
202
|
+
const nav = tabsNavRef.value;
|
|
203
|
+
const current = currentActiveTab.value;
|
|
204
|
+
const previous = previousActiveTab.value;
|
|
191
205
|
|
|
192
|
-
|
|
206
|
+
// Batch reads first.
|
|
207
|
+
const newTabPosition = previous ? previous.compareDocumentPosition(current) : 0;
|
|
208
|
+
const currentLeft = current.offsetLeft;
|
|
209
|
+
const currentWidth = current.offsetWidth;
|
|
210
|
+
const previousLeft = previous?.offsetLeft ?? 0;
|
|
211
|
+
const previousWidth = previous?.offsetWidth ?? 0;
|
|
212
|
+
const navWidth = nav.offsetWidth;
|
|
193
213
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
? previousActiveTab.value.compareDocumentPosition(currentActiveTab.value)
|
|
197
|
-
: 0
|
|
198
|
-
let transitionWidth
|
|
214
|
+
let transitionWidth: number;
|
|
215
|
+
let xActive: string | null = null;
|
|
199
216
|
|
|
200
217
|
if (newTabPosition === 4) {
|
|
201
|
-
transitionWidth =
|
|
202
|
-
currentActiveTab.value && previousActiveTab.value
|
|
203
|
-
? currentActiveTab.value.offsetLeft + currentActiveTab.value.offsetWidth - previousActiveTab.value.offsetLeft
|
|
204
|
-
: 0
|
|
218
|
+
transitionWidth = previous ? currentLeft + currentWidth - previousLeft : 0;
|
|
205
219
|
} else {
|
|
206
|
-
transitionWidth =
|
|
207
|
-
|
|
208
|
-
? previousActiveTab.value.offsetLeft + previousActiveTab.value.offsetWidth - currentActiveTab.value.offsetLeft
|
|
209
|
-
: 0
|
|
210
|
-
tabsNavRef.value.style.setProperty(
|
|
211
|
-
"--_x-active",
|
|
212
|
-
currentActiveTab.value ? currentActiveTab.value.offsetLeft + "px" : "0"
|
|
213
|
-
)
|
|
220
|
+
transitionWidth = previous ? previousLeft + previousWidth - currentLeft : 0;
|
|
221
|
+
xActive = currentLeft + "px";
|
|
214
222
|
}
|
|
215
223
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
224
|
+
// Then batch writes.
|
|
225
|
+
nav.style.setProperty("--_transition-duration", duration + "ms");
|
|
226
|
+
if (xActive !== null) {
|
|
227
|
+
nav.style.setProperty("--_x-active", xActive);
|
|
228
|
+
}
|
|
229
|
+
nav.style.setProperty("--_width-active", String(transitionWidth / navWidth));
|
|
219
230
|
|
|
220
|
-
|
|
231
|
+
if (activeSettleTimeoutId) clearTimeout(activeSettleTimeoutId);
|
|
232
|
+
activeSettleTimeoutId = setTimeout(
|
|
221
233
|
() => {
|
|
222
|
-
setFinalActivePositions()
|
|
234
|
+
setFinalActivePositions();
|
|
223
235
|
},
|
|
224
236
|
Math.floor(duration + 20)
|
|
225
|
-
)
|
|
226
|
-
}
|
|
237
|
+
);
|
|
238
|
+
};
|
|
227
239
|
|
|
228
240
|
const moveHoveredIndicator = () => {
|
|
229
|
-
if (!tabsNavRef.value || !currentHoveredTab.value) return
|
|
241
|
+
if (!tabsNavRef.value || !currentHoveredTab.value) return;
|
|
242
|
+
|
|
243
|
+
const nav = tabsNavRef.value;
|
|
244
|
+
const current = currentHoveredTab.value;
|
|
245
|
+
const previous = previousHoveredTab.value;
|
|
230
246
|
|
|
231
|
-
|
|
247
|
+
// Batch reads first.
|
|
248
|
+
const newTabPosition = previous ? previous.compareDocumentPosition(current) : 0;
|
|
249
|
+
const currentLeft = current.offsetLeft;
|
|
250
|
+
const currentWidth = current.offsetWidth;
|
|
251
|
+
const previousLeft = previous?.offsetLeft ?? 0;
|
|
252
|
+
const previousWidth = previous?.offsetWidth ?? 0;
|
|
253
|
+
const navWidth = nav.offsetWidth;
|
|
232
254
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
? previousHoveredTab.value.compareDocumentPosition(currentHoveredTab.value)
|
|
236
|
-
: 0
|
|
237
|
-
let transitionWidth
|
|
255
|
+
let transitionWidth: number;
|
|
256
|
+
let xHovered: string | null = null;
|
|
238
257
|
|
|
239
258
|
if (newTabPosition === 4) {
|
|
240
|
-
transitionWidth =
|
|
241
|
-
currentHoveredTab.value && previousHoveredTab.value
|
|
242
|
-
? currentHoveredTab.value.offsetLeft +
|
|
243
|
-
currentHoveredTab.value.offsetWidth -
|
|
244
|
-
previousHoveredTab.value.offsetLeft
|
|
245
|
-
: 0
|
|
259
|
+
transitionWidth = previous ? currentLeft + currentWidth - previousLeft : 0;
|
|
246
260
|
} else {
|
|
247
|
-
transitionWidth =
|
|
248
|
-
|
|
249
|
-
? previousHoveredTab.value.offsetLeft +
|
|
250
|
-
previousHoveredTab.value.offsetWidth -
|
|
251
|
-
currentHoveredTab.value.offsetLeft
|
|
252
|
-
: 0
|
|
253
|
-
tabsNavRef.value.style.setProperty(
|
|
254
|
-
"--_x-hovered",
|
|
255
|
-
currentHoveredTab.value ? currentHoveredTab.value.offsetLeft + "px" : "0"
|
|
256
|
-
)
|
|
261
|
+
transitionWidth = previous ? previousLeft + previousWidth - currentLeft : 0;
|
|
262
|
+
xHovered = currentLeft + "px";
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
// Then batch writes.
|
|
266
|
+
nav.style.setProperty("--_transition-duration", duration + "ms");
|
|
267
|
+
if (xHovered !== null) {
|
|
268
|
+
nav.style.setProperty("--_x-hovered", xHovered);
|
|
269
|
+
}
|
|
270
|
+
nav.style.setProperty("--_width-hovered", String(transitionWidth / navWidth));
|
|
262
271
|
|
|
263
|
-
|
|
272
|
+
if (hoveredSettleTimeoutId) clearTimeout(hoveredSettleTimeoutId);
|
|
273
|
+
hoveredSettleTimeoutId = setTimeout(
|
|
264
274
|
() => {
|
|
265
|
-
setFinalHoveredPositions()
|
|
275
|
+
setFinalHoveredPositions();
|
|
266
276
|
},
|
|
267
277
|
Math.floor(duration + 20)
|
|
268
|
-
)
|
|
269
|
-
}
|
|
278
|
+
);
|
|
279
|
+
};
|
|
270
280
|
|
|
271
281
|
const setActiveTabContent = () => {
|
|
272
|
-
const activeIndex = navItems.value?.findIndex((el) => el === currentActiveTab.value)
|
|
282
|
+
const activeIndex = navItems.value?.findIndex((el) => el === currentActiveTab.value);
|
|
273
283
|
tabsContentRefs.value?.forEach((tabContent: HTMLElement, index: number) => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
const isActive = activeIndex === index;
|
|
285
|
+
tabContent.style.display = isActive ? "block" : "none";
|
|
286
|
+
tabContent.setAttribute("aria-hidden", isActive ? "false" : "true");
|
|
287
|
+
});
|
|
288
|
+
};
|
|
277
289
|
|
|
278
290
|
useResizeObserver(tabsNavRef, () => {
|
|
279
|
-
setFinalActivePositions(true)
|
|
280
|
-
setFinalHoveredPositions(true)
|
|
281
|
-
})
|
|
291
|
+
setFinalActivePositions(true);
|
|
292
|
+
setFinalHoveredPositions(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
onUnmounted(() => {
|
|
296
|
+
if (activeSettleTimeoutId) clearTimeout(activeSettleTimeoutId);
|
|
297
|
+
if (hoveredSettleTimeoutId) clearTimeout(hoveredSettleTimeoutId);
|
|
298
|
+
});
|
|
282
299
|
|
|
283
300
|
return {
|
|
284
301
|
initNavDecorators,
|
|
285
302
|
navItemClicked,
|
|
286
303
|
navItemHovered,
|
|
304
|
+
navItemKeydown,
|
|
287
305
|
resetHoverToActivePosition,
|
|
288
|
-
}
|
|
289
|
-
}
|
|
306
|
+
};
|
|
307
|
+
};
|
|
290
308
|
|
|
291
|
-
export default useTabs
|
|
309
|
+
export default useTabs;
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`AutoGrid > renders correct HTML structure (all props and slots set) 1`] = `
|
|
4
|
-
"<section class="auto-grid custom-class" aria-labelledby="v-0-0">
|
|
5
|
-
<div>Item 1</div>
|
|
6
|
-
<div>Item 2</div>
|
|
7
|
-
<div>Item 3</div>
|
|
8
|
-
</section>"
|
|
9
|
-
`;
|
|
10
|
-
|
|
11
|
-
exports[`AutoGrid > renders correct HTML structure (default props) 1`] = `"<div class="auto-grid"></div>"`;
|