srcdev-nuxt-components 9.1.10 → 9.1.11
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/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +10 -21
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +1 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +9 -33
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +1 -1
- package/app/composables/useNavCollapse.ts +6 -14
- package/package.json +1 -1
- package/app/stores/useNavigationStore.ts +0 -113
|
@@ -178,13 +178,6 @@ const moveNavHoveredIndicator = () => {
|
|
|
178
178
|
const handleNavLinkClick = (event: MouseEvent) => {
|
|
179
179
|
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-nav-item]");
|
|
180
180
|
if (!target) return;
|
|
181
|
-
|
|
182
|
-
// Update store with clicked href for reliable active state tracking
|
|
183
|
-
const href = target.getAttribute("href");
|
|
184
|
-
if (href) {
|
|
185
|
-
navigationStore.handleNavLinkClick(href);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
181
|
currentActiveNavLink = target;
|
|
189
182
|
currentHoveredNavLink = target;
|
|
190
183
|
previousHoveredNavLink = target;
|
|
@@ -206,6 +199,7 @@ const resetHoverNavToActive = () => {
|
|
|
206
199
|
};
|
|
207
200
|
|
|
208
201
|
const initNavDecorators = () => {
|
|
202
|
+
cachedNavLinks = []; // invalidate — list may have been re-rendered (collapse toggle)
|
|
209
203
|
if (!navListRef.value) return;
|
|
210
204
|
const links = getNavLinks();
|
|
211
205
|
if (!links.length) return;
|
|
@@ -287,13 +281,6 @@ const movePanelHoveredIndicator = () => {
|
|
|
287
281
|
const handlePanelLinkClick = (event: MouseEvent) => {
|
|
288
282
|
const target = (event.target as HTMLElement).closest<HTMLElement>("[data-panel-nav-item]");
|
|
289
283
|
if (!target) return;
|
|
290
|
-
|
|
291
|
-
// Update store with clicked href for reliable active state tracking
|
|
292
|
-
const href = target.getAttribute("href");
|
|
293
|
-
if (href) {
|
|
294
|
-
navigationStore.handleNavLinkClick(href);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
284
|
currentActivePanelLink = target;
|
|
298
285
|
currentHoveredPanelLink = target;
|
|
299
286
|
previousHoveredPanelLink = target;
|
|
@@ -338,13 +325,15 @@ const initPanelDecorators = () => {
|
|
|
338
325
|
|
|
339
326
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
340
327
|
|
|
341
|
-
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, activeHref, isActiveItem, toggleMenu, closeMenu
|
|
342
|
-
useNavCollapse(
|
|
343
|
-
onResize: () => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
328
|
+
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, activeHref, isActiveItem, toggleMenu, closeMenu } =
|
|
329
|
+
useNavCollapse("site-nav-loaded", {
|
|
330
|
+
onResize: async () => {
|
|
331
|
+
await nextTick(); // wait for Vue to re-render after any isCollapsed change
|
|
332
|
+
initNavDecorators();
|
|
333
|
+
if (isMenuOpen.value) {
|
|
334
|
+
setFinalPanelActivePositions(true);
|
|
335
|
+
setFinalPanelHoveredPositions(true);
|
|
336
|
+
}
|
|
348
337
|
},
|
|
349
338
|
onRouteChange: () => {
|
|
350
339
|
requestAnimationFrame(() => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`SiteNavigation > renders correct HTML structure 1`] = `
|
|
4
4
|
"<nav class="site-navigation site-navigation--left is-loaded" aria-label="Site navigation">
|
|
5
5
|
<ul class="site-nav-list" style="--_transition-duration: 0ms; --_x-active: 0px; --_width-active: NaN; --_x-hovered: 0px; --_width-hovered: NaN;">
|
|
6
|
-
<li class=""><a href="/" class="site-nav-link" data-nav-item="">
|
|
6
|
+
<li class="is-active"><a href="/" class="site-nav-link" data-nav-item="">
|
|
7
7
|
<!--v-if--> Home
|
|
8
8
|
</a></li>
|
|
9
9
|
<li class=""><a href="/about" class="site-nav-link" data-nav-item="">
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
v-if="!isCollapsed || !isLoaded"
|
|
14
14
|
ref="navListRef"
|
|
15
15
|
class="tab-nav-list"
|
|
16
|
-
@mousemove="handleNavMousemove"
|
|
17
16
|
@mouseleave="hoveredItemHref = null"
|
|
18
17
|
>
|
|
19
18
|
<li
|
|
@@ -21,13 +20,13 @@
|
|
|
21
20
|
:key="item.href"
|
|
22
21
|
:data-href="item.href"
|
|
23
22
|
:class="[item.cssName, { 'is-active': isActiveItem(item.href), 'is-hovered': hoveredItemHref === item.href }]"
|
|
23
|
+
@mouseenter="hoveredItemHref = item.href ?? null"
|
|
24
24
|
>
|
|
25
25
|
<NuxtLink
|
|
26
26
|
:href="item.href"
|
|
27
27
|
:external="item.isExternal || undefined"
|
|
28
28
|
class="tab-nav-link"
|
|
29
29
|
data-nav-item
|
|
30
|
-
@click="handleNavLinkClick"
|
|
31
30
|
>
|
|
32
31
|
<Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
|
|
33
32
|
{{ item.text }}
|
|
@@ -38,7 +37,7 @@
|
|
|
38
37
|
</ul>
|
|
39
38
|
|
|
40
39
|
<InputButtonCore
|
|
41
|
-
v-if="
|
|
40
|
+
v-if="showCollapsed"
|
|
42
41
|
class="tab-nav-burger"
|
|
43
42
|
:class="{ 'is-open': isMenuOpen }"
|
|
44
43
|
variant="tertiary"
|
|
@@ -56,7 +55,7 @@
|
|
|
56
55
|
|
|
57
56
|
<Teleport to="body">
|
|
58
57
|
<div
|
|
59
|
-
v-if="
|
|
58
|
+
v-if="showCollapsed"
|
|
60
59
|
class="tab-nav-backdrop"
|
|
61
60
|
:class="{ 'is-open': isMenuOpen }"
|
|
62
61
|
aria-hidden="true"
|
|
@@ -65,7 +64,7 @@
|
|
|
65
64
|
</Teleport>
|
|
66
65
|
|
|
67
66
|
<div
|
|
68
|
-
v-if="
|
|
67
|
+
v-if="showCollapsed"
|
|
69
68
|
id="tab-nav-panel"
|
|
70
69
|
class="tab-nav-panel"
|
|
71
70
|
:class="{ 'is-open': isMenuOpen }"
|
|
@@ -78,7 +77,7 @@
|
|
|
78
77
|
:href="item.href"
|
|
79
78
|
:external="item.isExternal || undefined"
|
|
80
79
|
class="tab-nav-panel-link"
|
|
81
|
-
@click="
|
|
80
|
+
@click="closeMenu"
|
|
82
81
|
>
|
|
83
82
|
<Icon v-if="item.iconName" :name="item.iconName" aria-hidden="true" />
|
|
84
83
|
{{ item.text }}
|
|
@@ -104,33 +103,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
104
103
|
styleClassPassthrough: () => [],
|
|
105
104
|
});
|
|
106
105
|
|
|
107
|
-
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu
|
|
108
|
-
useNavCollapse(
|
|
109
|
-
|
|
110
|
-
// Handle navigation link clicks to update store
|
|
111
|
-
const handleNavLinkClick = (event: MouseEvent) => {
|
|
112
|
-
const target = (event.target as HTMLElement).closest<HTMLElement>("[href]");
|
|
113
|
-
if (!target) return;
|
|
114
|
-
const href = target.getAttribute("href");
|
|
115
|
-
if (href) navigationStore.handleNavLinkClick(href);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Handle panel link clicks to update store and close menu
|
|
119
|
-
const handlePanelLinkClick = (event: MouseEvent) => {
|
|
120
|
-
const target = (event.target as HTMLElement).closest<HTMLElement>("[href]");
|
|
121
|
-
if (!target) return;
|
|
122
|
-
const href = target.getAttribute("href");
|
|
123
|
-
if (href) navigationStore.handleNavLinkClick(href);
|
|
124
|
-
closeMenu();
|
|
125
|
-
};
|
|
106
|
+
const { navRef, navListRef, isCollapsed, isLoaded, isMenuOpen, isActiveItem, toggleMenu, closeMenu } =
|
|
107
|
+
useNavCollapse("tab-nav-loaded");
|
|
126
108
|
|
|
127
109
|
const hoveredItemHref = ref<string | null>(null);
|
|
128
|
-
|
|
129
|
-
const handleNavMousemove = (event: MouseEvent) => {
|
|
130
|
-
const li = (event.target as HTMLElement).closest<HTMLElement>("li[data-href]");
|
|
131
|
-
if (!li) return;
|
|
132
|
-
hoveredItemHref.value = li.dataset.href ?? null;
|
|
133
|
-
};
|
|
110
|
+
const showCollapsed = computed(() => isCollapsed.value && isLoaded.value);
|
|
134
111
|
|
|
135
112
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
136
113
|
|
|
@@ -167,8 +144,7 @@ watch(
|
|
|
167
144
|
|
|
168
145
|
/* Decorators — horizontal nav */
|
|
169
146
|
--_decorator-hovered-bg: var(--tab-nav-decorator-hovered-bg, transparent);
|
|
170
|
-
|
|
171
|
-
--_decorator-indicator-color: red;
|
|
147
|
+
--_decorator-indicator-color: var(--tab-nav-decorator-indicator-color, var(--slate-01, currentColor));
|
|
172
148
|
|
|
173
149
|
/* Horizontal nav */
|
|
174
150
|
--_link-color: var(--tab-nav-link-color, var(--slate-01, currentColor));
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`TabNavigation > renders correct HTML structure 1`] = `
|
|
4
4
|
"<nav class="tab-navigation tab-navigation--left is-loaded" aria-label="Site navigation">
|
|
5
5
|
<ul class="tab-nav-list">
|
|
6
|
-
<li data-href="/" class=""><a href="/" class="tab-nav-link" data-nav-item="">
|
|
6
|
+
<li data-href="/" class="is-active"><a href="/" class="tab-nav-link" data-nav-item="">
|
|
7
7
|
<!--v-if--> Home
|
|
8
8
|
</a></li>
|
|
9
9
|
<li data-href="/about" class=""><a href="/about" class="tab-nav-link" data-nav-item="">
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useResizeObserver, onClickOutside } from "@vueuse/core";
|
|
2
|
-
import type { NavItemData } from "~/types/components";
|
|
3
2
|
|
|
4
3
|
interface NavCollapseOptions {
|
|
5
4
|
onResize?: () => void;
|
|
@@ -7,14 +6,13 @@ interface NavCollapseOptions {
|
|
|
7
6
|
onMounted?: () => void;
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
export const useNavCollapse = (
|
|
9
|
+
export const useNavCollapse = (stateKey: string, options: NavCollapseOptions = {}) => {
|
|
11
10
|
const navRef = ref<HTMLElement | null>(null);
|
|
12
11
|
const navListRef = ref<HTMLUListElement | null>(null);
|
|
13
12
|
|
|
14
13
|
const isCollapsed = ref(false);
|
|
15
14
|
const isLoaded = useState(stateKey, () => false);
|
|
16
15
|
const isMenuOpen = ref(false);
|
|
17
|
-
const isMounted = ref(false);
|
|
18
16
|
|
|
19
17
|
// Stored natural width of the list — used when the list is not in the DOM
|
|
20
18
|
let navListNaturalWidth = 0;
|
|
@@ -35,19 +33,17 @@ export const useNavCollapse = (navItemData: NavItemData, stateKey: string, optio
|
|
|
35
33
|
isMenuOpen.value = false;
|
|
36
34
|
};
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
// useRoute() in Nuxt is SSR-aware — route.path is consistent on server and client,
|
|
37
|
+
// so isActiveItem can be used directly without a isMounted guard.
|
|
39
38
|
const route = useRoute();
|
|
40
39
|
|
|
41
|
-
const activeHref = computed(() =>
|
|
42
|
-
|
|
43
|
-
// Client-side only active state check to prevent hydration mismatch
|
|
44
|
-
const isActiveItem = (href?: string) => isMounted.value && activeHref.value === href;
|
|
40
|
+
const activeHref = computed(() => route.path);
|
|
41
|
+
const isActiveItem = (href?: string) => href === route.path;
|
|
45
42
|
|
|
46
43
|
watch(
|
|
47
44
|
() => route.path,
|
|
48
|
-
(
|
|
45
|
+
() => {
|
|
49
46
|
closeMenu();
|
|
50
|
-
navigationStore.syncWithRoute(navItemData.main ?? [], newPath);
|
|
51
47
|
options.onRouteChange?.();
|
|
52
48
|
},
|
|
53
49
|
{ flush: "post" }
|
|
@@ -68,8 +64,6 @@ export const useNavCollapse = (navItemData: NavItemData, stateKey: string, optio
|
|
|
68
64
|
checkOverflow();
|
|
69
65
|
isLoaded.value = true;
|
|
70
66
|
await router.isReady();
|
|
71
|
-
navigationStore.initializeFromRoute(navItemData.main ?? [], route.path);
|
|
72
|
-
isMounted.value = true;
|
|
73
67
|
options.onMounted?.();
|
|
74
68
|
});
|
|
75
69
|
|
|
@@ -79,12 +73,10 @@ export const useNavCollapse = (navItemData: NavItemData, stateKey: string, optio
|
|
|
79
73
|
isCollapsed,
|
|
80
74
|
isLoaded,
|
|
81
75
|
isMenuOpen,
|
|
82
|
-
isMounted,
|
|
83
76
|
activeHref,
|
|
84
77
|
checkOverflow,
|
|
85
78
|
toggleMenu,
|
|
86
79
|
closeMenu,
|
|
87
80
|
isActiveItem,
|
|
88
|
-
navigationStore,
|
|
89
81
|
};
|
|
90
82
|
};
|
package/package.json
CHANGED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
export const useNavigationStore = defineStore(
|
|
2
|
-
"useNavigationStore",
|
|
3
|
-
() => {
|
|
4
|
-
// State
|
|
5
|
-
const activeHref = ref<string | null>(null);
|
|
6
|
-
const isInitialized = ref(false);
|
|
7
|
-
|
|
8
|
-
// Getters
|
|
9
|
-
const currentActiveHref = computed(() => activeHref.value);
|
|
10
|
-
|
|
11
|
-
// Actions
|
|
12
|
-
const setActiveHref = (href: string | null) => {
|
|
13
|
-
activeHref.value = href;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Initialize active href based on current route and navigation items
|
|
18
|
-
* Used on component mount to handle deep links and initial page loads
|
|
19
|
-
*/
|
|
20
|
-
const initializeFromRoute = (navItems: Array<{ href?: string }>, routePath: string) => {
|
|
21
|
-
if (isInitialized.value) return;
|
|
22
|
-
|
|
23
|
-
const items = navItems.filter((item): item is { href: string } => Boolean(item.href));
|
|
24
|
-
|
|
25
|
-
// Try exact match first
|
|
26
|
-
const exact = items.find((item) => routePath === item.href);
|
|
27
|
-
if (exact) {
|
|
28
|
-
activeHref.value = exact.href;
|
|
29
|
-
isInitialized.value = true;
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Fall back to longest prefix match
|
|
34
|
-
const prefixMatches = items
|
|
35
|
-
.filter((item) => routePath.startsWith(item.href + "/"))
|
|
36
|
-
.sort((a, b) => b.href.length - a.href.length);
|
|
37
|
-
|
|
38
|
-
if (prefixMatches.length > 0) {
|
|
39
|
-
activeHref.value = prefixMatches[0]!.href;
|
|
40
|
-
} else {
|
|
41
|
-
// Default to null if no matches (let component handle fallback)
|
|
42
|
-
activeHref.value = null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
isInitialized.value = true;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Update active href when user clicks a navigation link
|
|
50
|
-
* Provides explicit control over active state
|
|
51
|
-
*/
|
|
52
|
-
const handleNavLinkClick = (href: string) => {
|
|
53
|
-
activeHref.value = href;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Sync with route changes (for programmatic navigation)
|
|
58
|
-
* Re-runs the route matching logic when route changes
|
|
59
|
-
*/
|
|
60
|
-
const syncWithRoute = (navItems: Array<{ href?: string }>, routePath: string) => {
|
|
61
|
-
const items = navItems.filter((item): item is { href: string } => Boolean(item.href));
|
|
62
|
-
|
|
63
|
-
// Try exact match first
|
|
64
|
-
const exact = items.find((item) => routePath === item.href);
|
|
65
|
-
if (exact) {
|
|
66
|
-
activeHref.value = exact.href;
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Fall back to longest prefix match
|
|
71
|
-
const prefixMatches = items
|
|
72
|
-
.filter((item) => routePath.startsWith(item.href + "/"))
|
|
73
|
-
.sort((a, b) => b.href.length - a.href.length);
|
|
74
|
-
|
|
75
|
-
if (prefixMatches.length > 0) {
|
|
76
|
-
activeHref.value = prefixMatches[0]!.href;
|
|
77
|
-
}
|
|
78
|
-
// Don't reset to null if no matches - preserve existing active state
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Reset store state (useful for testing or when navigation data changes)
|
|
83
|
-
*/
|
|
84
|
-
const reset = () => {
|
|
85
|
-
activeHref.value = null;
|
|
86
|
-
isInitialized.value = false;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
// State
|
|
91
|
-
activeHref,
|
|
92
|
-
isInitialized,
|
|
93
|
-
|
|
94
|
-
// Getters
|
|
95
|
-
currentActiveHref,
|
|
96
|
-
|
|
97
|
-
// Actions
|
|
98
|
-
setActiveHref,
|
|
99
|
-
initializeFromRoute,
|
|
100
|
-
handleNavLinkClick,
|
|
101
|
-
syncWithRoute,
|
|
102
|
-
reset,
|
|
103
|
-
};
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
persist: {
|
|
107
|
-
storage: piniaPluginPersistedstate.sessionStorage(),
|
|
108
|
-
// Only persist the active href, not initialization state
|
|
109
|
-
// (we want to re-initialize on page refresh to handle route changes)
|
|
110
|
-
pick: ["activeHref"],
|
|
111
|
-
},
|
|
112
|
-
}
|
|
113
|
-
);
|