@energysage/es-ds-components 5.5.5 → 5.6.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
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.6.0](https://github.com/EnergySage/es-ds/compare/es-ds-components-v5.5.6...es-ds-components-v5.6.0) (2026-06-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* sticky bar now coordinates with other fixed position elements ([#1764](https://github.com/EnergySage/es-ds/issues/1764)) ([3d250de](https://github.com/EnergySage/es-ds/commit/3d250de895097cce81ca1c624ef0306229ebcf97))
|
|
9
|
+
|
|
10
|
+
## [5.5.6](https://github.com/EnergySage/es-ds/compare/es-ds-components-v5.5.5...es-ds-components-v5.5.6) (2026-05-29)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* EsMenuBar and EsMobileNav now only teleport to body on client ([#1762](https://github.com/EnergySage/es-ds/issues/1762)) ([f8ce8ff](https://github.com/EnergySage/es-ds/commit/f8ce8ff04bf82be664d4d9849a627a11565055ab))
|
|
16
|
+
|
|
3
17
|
## [5.5.5](https://github.com/EnergySage/es-ds/compare/es-ds-components-v5.5.4...es-ds-components-v5.5.5) (2026-05-28)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -139,19 +139,28 @@ onUnmounted(() => {
|
|
|
139
139
|
'full-width': fullWidth,
|
|
140
140
|
}" />
|
|
141
141
|
</navigation-menu-root>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
to
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
142
|
+
|
|
143
|
+
<!--
|
|
144
|
+
teleported to body to escape any ancestor stacking context (e.g. es-sticky-bar).
|
|
145
|
+
wrapped in <client-only> because <teleport to="body"> is position-sensitive during
|
|
146
|
+
hydration: any third-party script that injects a node into <body> before Vue
|
|
147
|
+
hydrates will desynchronize the teleport anchor and produce a hydration mismatch.
|
|
148
|
+
-->
|
|
149
|
+
<client-only>
|
|
150
|
+
<teleport
|
|
151
|
+
v-if="showOverlayWhenOpen"
|
|
152
|
+
to="body">
|
|
153
|
+
<transition name="es-menu-bar-overlay">
|
|
154
|
+
<div
|
|
155
|
+
v-if="activeMenuId"
|
|
156
|
+
class="es-menu-bar-overlay"
|
|
157
|
+
:style="{
|
|
158
|
+
'--es-menu-bar-height': heightPx,
|
|
159
|
+
'--es-menu-bar-open-close-duration': `${ES_MENU_BAR_OPEN_CLOSE_DURATION_MS}ms`,
|
|
160
|
+
}"></div>
|
|
161
|
+
</transition>
|
|
162
|
+
</teleport>
|
|
163
|
+
</client-only>
|
|
155
164
|
</div>
|
|
156
165
|
</template>
|
|
157
166
|
|
|
@@ -64,9 +64,13 @@ const isElementWithinMenu = (element: any) => {
|
|
|
64
64
|
return parent.contains(element);
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
// gates the body teleport — see the v-if on the <teleport> in the template
|
|
68
|
+
const isMounted = ref(false);
|
|
69
|
+
|
|
67
70
|
onMounted(() => {
|
|
68
71
|
// give EsMobileNav a reference to the scrollable content area so it can set the scroll position
|
|
69
72
|
registerScrollableContentArea(mobileNavParentElement);
|
|
73
|
+
isMounted.value = true;
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
provide('backButtonSimulatedFocus', backButtonSimulatedFocus);
|
|
@@ -75,8 +79,21 @@ provide('isElementWithinMenu', isElementWithinMenu);
|
|
|
75
79
|
</script>
|
|
76
80
|
|
|
77
81
|
<template>
|
|
78
|
-
<!--
|
|
79
|
-
|
|
82
|
+
<!--
|
|
83
|
+
teleported to body to escape any ancestor stacking context (e.g. es-sticky-bar).
|
|
84
|
+
gated by v-if="isMounted" rather than <client-only> for two reasons:
|
|
85
|
+
1. <teleport to="body"> is position-sensitive during hydration — any third-party
|
|
86
|
+
script that injects a node into <body> before Vue hydrates would
|
|
87
|
+
desynchronize the teleport anchor and cause a mismatch.
|
|
88
|
+
2. <client-only> always emits an empty <span> fallback during SSR. Used as a
|
|
89
|
+
template root, that span gets the component's scope and any v-bind() CSS
|
|
90
|
+
variables, becomes a sibling flex item in the consuming layout, and disrupts
|
|
91
|
+
positioning until hydration completes. v-if emits only a comment marker,
|
|
92
|
+
which is not a flex item.
|
|
93
|
+
-->
|
|
94
|
+
<teleport
|
|
95
|
+
v-if="isMounted"
|
|
96
|
+
to="body">
|
|
80
97
|
<navigation-menu-content
|
|
81
98
|
v-bind="$attrs"
|
|
82
99
|
ref="mobileNavParentElement"
|
|
@@ -32,6 +32,12 @@ const displayedName = computed(() => nameStack.value.at(-1) ?? '');
|
|
|
32
32
|
const isScrollLocked = useBodyScrollLock(false);
|
|
33
33
|
const widthPx = computed(() => `${props.width}px`);
|
|
34
34
|
|
|
35
|
+
// gates the body teleport — see the v-if on the <teleport> in the template
|
|
36
|
+
const isMounted = ref(false);
|
|
37
|
+
onMounted(() => {
|
|
38
|
+
isMounted.value = true;
|
|
39
|
+
});
|
|
40
|
+
|
|
35
41
|
// closes the top-level menu
|
|
36
42
|
const closeMenu = () => {
|
|
37
43
|
// focus the trigger before closing so assistive technologies (e.g. VoiceOver)
|
|
@@ -171,10 +177,21 @@ watch(activeMenuId, async (newVal: string, oldVal: string) => {
|
|
|
171
177
|
</navigation-menu-root>
|
|
172
178
|
|
|
173
179
|
<!--
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
teleported to body to prevent a hydration issue caused by NavigationMenuRoot
|
|
181
|
+
traversing its slot children, and to escape any ancestor stacking context.
|
|
182
|
+
|
|
183
|
+
gated by v-if="isMounted" rather than <client-only> for two reasons:
|
|
184
|
+
1. <teleport to="body"> is position-sensitive during hydration — any third-party
|
|
185
|
+
script that injects a node into <body> before Vue hydrates would
|
|
186
|
+
desynchronize the teleport anchor and cause a mismatch.
|
|
187
|
+
2. <client-only> always emits an empty <span> fallback during SSR. As a second
|
|
188
|
+
root of this multi-root template, that span becomes a sibling flex item in
|
|
189
|
+
the consuming navbar and disrupts justify-content layout until hydration
|
|
190
|
+
completes. v-if emits only a comment marker, which is not a flex item.
|
|
176
191
|
-->
|
|
177
|
-
<teleport
|
|
192
|
+
<teleport
|
|
193
|
+
v-if="isMounted"
|
|
194
|
+
to="body">
|
|
178
195
|
<transition name="es-mobile-nav-overlay">
|
|
179
196
|
<div
|
|
180
197
|
v-if="activeMenuId"
|
|
@@ -77,9 +77,15 @@ function updateVisibility() {
|
|
|
77
77
|
if (currentScrollY <= 0) {
|
|
78
78
|
// back at the very top: switch to absolute so bar scrolls away naturally on next scroll down
|
|
79
79
|
barState.value = 'absolute';
|
|
80
|
+
|
|
81
|
+
// emit event notifying other fixed position elements of the change
|
|
82
|
+
emitter.emit(ES_STICKY_BAR_FIXED_HIDE_EVENT_NAME);
|
|
80
83
|
} else if (barState.value === 'absolute' && currentScrollY >= barHeight.value) {
|
|
81
84
|
// bar has scrolled fully off screen: reposition to fixed-hidden while invisible
|
|
82
85
|
hideWithoutAnimation();
|
|
86
|
+
|
|
87
|
+
// emit event notifying other fixed position elements of the change
|
|
88
|
+
emitter.emit(ES_STICKY_BAR_FIXED_HIDE_EVENT_NAME);
|
|
83
89
|
} else if (
|
|
84
90
|
barState.value === 'fixed-hidden' &&
|
|
85
91
|
scrollDirection < 0 &&
|
|
@@ -87,6 +93,9 @@ function updateVisibility() {
|
|
|
87
93
|
) {
|
|
88
94
|
// scrolling up far enough: slide bar into view
|
|
89
95
|
barState.value = 'fixed-visible';
|
|
96
|
+
|
|
97
|
+
// emit event notifying other fixed position elements of the change
|
|
98
|
+
emitter.emit(ES_STICKY_BAR_FIXED_SHOW_EVENT_NAME);
|
|
90
99
|
} else if (
|
|
91
100
|
barState.value === 'fixed-visible' &&
|
|
92
101
|
scrollDirection > 0 &&
|
|
@@ -94,6 +103,9 @@ function updateVisibility() {
|
|
|
94
103
|
) {
|
|
95
104
|
// scrolling down far enough: slide bar off screen
|
|
96
105
|
barState.value = 'fixed-hidden';
|
|
106
|
+
|
|
107
|
+
// emit event notifying other fixed position elements of the change
|
|
108
|
+
emitter.emit(ES_STICKY_BAR_FIXED_HIDE_EVENT_NAME);
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
lastScrollY = currentScrollY;
|
|
@@ -129,6 +141,12 @@ const onMenuBarClose = () => {
|
|
|
129
141
|
}
|
|
130
142
|
};
|
|
131
143
|
|
|
144
|
+
// expose barHeight to other fixed position elements via a CSS variable on <html> so they can
|
|
145
|
+
// position themselves relative to the bar
|
|
146
|
+
watch(barHeight, (height) => {
|
|
147
|
+
document.documentElement.style.setProperty('--es-sticky-bar-height', `${height}px`);
|
|
148
|
+
});
|
|
149
|
+
|
|
132
150
|
onMounted(() => {
|
|
133
151
|
// measure height before switching position so the placeholder matches exactly
|
|
134
152
|
barHeight.value = bar.value?.offsetHeight ?? 0;
|
|
@@ -154,6 +172,8 @@ onMounted(() => {
|
|
|
154
172
|
onUnmounted(() => {
|
|
155
173
|
window.removeEventListener('scroll', onScroll);
|
|
156
174
|
|
|
175
|
+
document.documentElement.style.removeProperty('--es-sticky-bar-height');
|
|
176
|
+
|
|
157
177
|
emitter.off(ES_MENU_BAR_OPEN_EVENT_NAME, onMenuBarOpen);
|
|
158
178
|
emitter.off(ES_MENU_BAR_CLOSE_EVENT_NAME, onMenuBarClose);
|
|
159
179
|
});
|