@energysage/es-ds-components 5.5.3 → 5.5.5
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.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
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* mobile nav hydration issue ([#1760](https://github.com/EnergySage/es-ds/issues/1760)) ([677f043](https://github.com/EnergySage/es-ds/commit/677f0430d82c0732a95811e9cdf7cf2806d8ba03))
|
|
9
|
+
|
|
10
|
+
## [5.5.4](https://github.com/EnergySage/es-ds/compare/es-ds-components-v5.5.3...es-ds-components-v5.5.4) (2026-05-26)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* menu bar no longer causes page shift when mouse/scrollbar is present ([#1756](https://github.com/EnergySage/es-ds/issues/1756)) ([310adce](https://github.com/EnergySage/es-ds/commit/310adced23e3f1bb580e4177e2727ef1304fb7ce))
|
|
16
|
+
|
|
3
17
|
## [5.5.3](https://github.com/EnergySage/es-ds/compare/es-ds-components-v5.5.2...es-ds-components-v5.5.3) (2026-05-26)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import { NavigationMenuList, NavigationMenuRoot, NavigationMenuViewport } from 'reka-ui';
|
|
2
|
+
import { NavigationMenuList, NavigationMenuRoot, NavigationMenuViewport, useBodyScrollLock } from 'reka-ui';
|
|
4
3
|
import {
|
|
5
4
|
ES_MENU_BAR_ALIGNMENT_REGISTRY_KEY,
|
|
6
5
|
ES_MENU_BAR_CLOSE_EVENT_NAME,
|
|
@@ -27,8 +26,12 @@ const props = withDefaults(defineProps<IProps>(), {
|
|
|
27
26
|
// used to ensure we don't close our menu when emitting our own menu open event
|
|
28
27
|
let isOpening = false;
|
|
29
28
|
|
|
29
|
+
// held until the close animation finishes so --scrollbar-width stays on <html> for the
|
|
30
|
+
// duration — otherwise the scrollbar reappears mid-close and EsColorBand shifts inward
|
|
31
|
+
let releaseScrollLockTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
32
|
+
|
|
30
33
|
const emitter = useEsdsEvents();
|
|
31
|
-
const isScrollLocked =
|
|
34
|
+
const isScrollLocked = useBodyScrollLock(false);
|
|
32
35
|
|
|
33
36
|
const activeMenuId = ref('');
|
|
34
37
|
|
|
@@ -50,8 +53,29 @@ provide(ES_MENU_BAR_ALIGNMENT_REGISTRY_KEY, {
|
|
|
50
53
|
const activeAlign = computed<EsMenuBarFlyoutAlign>(() => flyoutAlignments.get(activeMenuId.value) ?? 'center');
|
|
51
54
|
|
|
52
55
|
watch(activeMenuId, async (newVal: string, oldVal: string) => {
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
if (releaseScrollLockTimeoutId) {
|
|
57
|
+
clearTimeout(releaseScrollLockTimeoutId);
|
|
58
|
+
releaseScrollLockTimeoutId = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (newVal) {
|
|
62
|
+
// lock page scrolling when menu is open, unless we're not showing the overlay
|
|
63
|
+
isScrollLocked.value = props.showOverlayWhenOpen;
|
|
64
|
+
} else if (oldVal) {
|
|
65
|
+
// defer scroll lock release until the close animation finishes so --scrollbar-width
|
|
66
|
+
// stays populated; skip the delay when reduced motion is on (no animation to cover)
|
|
67
|
+
const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
68
|
+
if (prefersReducedMotion) {
|
|
69
|
+
isScrollLocked.value = false;
|
|
70
|
+
} else {
|
|
71
|
+
releaseScrollLockTimeoutId = setTimeout(() => {
|
|
72
|
+
releaseScrollLockTimeoutId = null;
|
|
73
|
+
if (!activeMenuId.value) {
|
|
74
|
+
isScrollLocked.value = false;
|
|
75
|
+
}
|
|
76
|
+
}, ES_MENU_BAR_OPEN_CLOSE_DURATION_MS);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
55
79
|
|
|
56
80
|
if (!oldVal && newVal) {
|
|
57
81
|
// if the menu is opening, emit event so:
|
|
@@ -85,6 +109,10 @@ onMounted(() => {
|
|
|
85
109
|
});
|
|
86
110
|
|
|
87
111
|
onUnmounted(() => {
|
|
112
|
+
if (releaseScrollLockTimeoutId) {
|
|
113
|
+
clearTimeout(releaseScrollLockTimeoutId);
|
|
114
|
+
releaseScrollLockTimeoutId = null;
|
|
115
|
+
}
|
|
88
116
|
emitter.off(ES_MENU_BAR_OPEN_EVENT_NAME, handleMenuOpen);
|
|
89
117
|
});
|
|
90
118
|
</script>
|
|
@@ -253,6 +281,9 @@ $switch-menus-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
253
281
|
}
|
|
254
282
|
|
|
255
283
|
.es-menu-bar {
|
|
284
|
+
/* allows interaction when EsMenuBar is open */
|
|
285
|
+
pointer-events: auto;
|
|
286
|
+
|
|
256
287
|
:deep(.es-menu-bar-list) {
|
|
257
288
|
align-items: center;
|
|
258
289
|
display: flex;
|
|
@@ -332,6 +363,11 @@ $switch-menus-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
332
363
|
|
|
333
364
|
&[data-state='closed'] {
|
|
334
365
|
animation: menuClose var(--es-menu-bar-open-close-duration) $open-close-timing-function;
|
|
366
|
+
|
|
367
|
+
/* prevent color band from shifting on scrollbar on/off */
|
|
368
|
+
.es-menu-bar-color-band {
|
|
369
|
+
right: min(0px, calc(-1 * var(--scrollbar-width)));
|
|
370
|
+
}
|
|
335
371
|
}
|
|
336
372
|
}
|
|
337
373
|
|
|
@@ -355,8 +391,10 @@ $switch-menus-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
355
391
|
&.full-width {
|
|
356
392
|
left: 0;
|
|
357
393
|
right: 0;
|
|
394
|
+
width: 100%;
|
|
358
395
|
|
|
359
396
|
.es-menu-bar-flyout {
|
|
397
|
+
padding-right: var(--scrollbar-width);
|
|
360
398
|
width: 100%;
|
|
361
399
|
/* row's content box = pane − 2·wrapper-padding + grid-gutter (row's negative margins). matches <es-col md="6" lg="4" xl="3"> in es-menu-bar-flyout-column so column text stays aligned during the cross-slide */
|
|
362
400
|
--es-menu-bar-flyout-row-width: calc(
|
|
@@ -179,6 +179,8 @@ provide('isElementWithinMenu', isElementWithinMenu);
|
|
|
179
179
|
|
|
180
180
|
.es-mobile-nav-content {
|
|
181
181
|
--es-mobile-nav-header-height: 88px;
|
|
182
|
+
/* allows interaction when nav is is open */
|
|
183
|
+
pointer-events: auto;
|
|
182
184
|
|
|
183
185
|
bottom: 0;
|
|
184
186
|
box-shadow: 0 0 6px 0 rgba(34, 38, 51, 0.2);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
type NavigationMenuContent,
|
|
4
|
+
NavigationMenuItem,
|
|
5
|
+
NavigationMenuList,
|
|
6
|
+
NavigationMenuRoot,
|
|
7
|
+
useBodyScrollLock,
|
|
8
|
+
} from 'reka-ui';
|
|
4
9
|
import type { ShallowRef } from 'vue';
|
|
5
10
|
|
|
6
11
|
interface IProps {
|
|
@@ -24,7 +29,7 @@ const scrollableContentAreaTemplateRef: Ref<Readonly<
|
|
|
24
29
|
const subNavCloseHandlers: Ref<Function[]> = ref([]);
|
|
25
30
|
|
|
26
31
|
const displayedName = computed(() => nameStack.value.at(-1) ?? '');
|
|
27
|
-
const isScrollLocked =
|
|
32
|
+
const isScrollLocked = useBodyScrollLock(false);
|
|
28
33
|
const widthPx = computed(() => `${props.width}px`);
|
|
29
34
|
|
|
30
35
|
// closes the top-level menu
|
|
@@ -109,12 +114,25 @@ provide('width', toRef(props, 'width'));
|
|
|
109
114
|
provide('widthPx', widthPx);
|
|
110
115
|
|
|
111
116
|
watch(activeMenuId, async (newVal: string, oldVal: string) => {
|
|
112
|
-
|
|
117
|
+
if (newVal) {
|
|
118
|
+
isScrollLocked.value = true;
|
|
119
|
+
}
|
|
113
120
|
|
|
114
121
|
// if the menu is closing
|
|
115
122
|
if (!newVal && oldVal) {
|
|
116
|
-
// wait until the mobile nav closing animation is complete
|
|
117
|
-
await
|
|
123
|
+
// wait until the mobile nav closing animation is complete; with reduced motion
|
|
124
|
+
// there is no animation, so skip the await entirely and run synchronously
|
|
125
|
+
const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
126
|
+
if (!prefersReducedMotion) {
|
|
127
|
+
await waitForAnimationDuration();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// release scroll lock now that the close animation is done, so --scrollbar-width
|
|
131
|
+
// stayed populated through the animation and the panel didn't jump inward as the
|
|
132
|
+
// native scrollbar reappeared. guard against a re-open during the wait.
|
|
133
|
+
if (!activeMenuId.value) {
|
|
134
|
+
isScrollLocked.value = false;
|
|
135
|
+
}
|
|
118
136
|
|
|
119
137
|
// reset the state of all submenus by closing all open ones (without animation)
|
|
120
138
|
subNavCloseHandlers.value.forEach((callback: Function) => {
|
|
@@ -137,16 +155,6 @@ watch(activeMenuId, async (newVal: string, oldVal: string) => {
|
|
|
137
155
|
class="es-mobile-nav d-flex"
|
|
138
156
|
disable-hover-trigger
|
|
139
157
|
disable-pointer-leave-close>
|
|
140
|
-
<!-- overlay -->
|
|
141
|
-
<teleport to="body">
|
|
142
|
-
<div
|
|
143
|
-
class="es-mobile-nav-overlay position-fixed"
|
|
144
|
-
:class="{
|
|
145
|
-
active: !!activeMenuId,
|
|
146
|
-
}"
|
|
147
|
-
@click="activeMenuId = ''" />
|
|
148
|
-
</teleport>
|
|
149
|
-
|
|
150
158
|
<!--
|
|
151
159
|
since the first level is a single item - the trigger to open the mobile nav -
|
|
152
160
|
prevent it from being a <ul> and <li>
|
|
@@ -161,39 +169,54 @@ watch(activeMenuId, async (newVal: string, oldVal: string) => {
|
|
|
161
169
|
</navigation-menu-item>
|
|
162
170
|
</navigation-menu-list>
|
|
163
171
|
</navigation-menu-root>
|
|
172
|
+
|
|
173
|
+
<!--
|
|
174
|
+
overlay teleport needs to live outside NavigationMenuRoot to prevent
|
|
175
|
+
a hydration issue caused by NavigationMenuRoot traversing its slot children
|
|
176
|
+
-->
|
|
177
|
+
<teleport to="body">
|
|
178
|
+
<transition name="es-mobile-nav-overlay">
|
|
179
|
+
<div
|
|
180
|
+
v-if="activeMenuId"
|
|
181
|
+
class="es-mobile-nav-overlay position-fixed"
|
|
182
|
+
:style="{ '--es-mobile-nav-animation-duration': ANIMATION_DURATION_MS }"
|
|
183
|
+
@click="activeMenuId = ''" />
|
|
184
|
+
</transition>
|
|
185
|
+
</teleport>
|
|
164
186
|
</template>
|
|
165
187
|
|
|
166
188
|
<style lang="scss" scoped>
|
|
167
189
|
@use '@energysage/es-ds-styles/scss/mixins/breakpoints' as breakpoints;
|
|
168
190
|
@use '@energysage/es-ds-styles/scss/variables';
|
|
169
191
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
.es-mobile-nav-overlay {
|
|
193
|
+
opacity: 0.3;
|
|
194
|
+
pointer-events: all;
|
|
195
|
+
|
|
196
|
+
@include breakpoints.media-breakpoint-up(sm) {
|
|
197
|
+
background-color: variables.$gray-900;
|
|
198
|
+
bottom: 0;
|
|
199
|
+
left: 0;
|
|
200
|
+
right: 0;
|
|
201
|
+
top: 0;
|
|
202
|
+
z-index: 1010;
|
|
203
|
+
}
|
|
178
204
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
205
|
+
@media not (prefers-reduced-motion) {
|
|
206
|
+
&-enter-active,
|
|
207
|
+
&-leave-active {
|
|
208
|
+
transition: opacity var(--es-mobile-nav-animation-duration) ease-in-out;
|
|
182
209
|
}
|
|
183
210
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
left: 0;
|
|
188
|
-
right: 0;
|
|
189
|
-
top: 0;
|
|
190
|
-
z-index: 1010;
|
|
191
|
-
|
|
192
|
-
@media not (prefers-reduced-motion) {
|
|
193
|
-
transition: opacity $animation-duration ease-in-out;
|
|
194
|
-
}
|
|
211
|
+
&-enter-from,
|
|
212
|
+
&-leave-to {
|
|
213
|
+
opacity: 0;
|
|
195
214
|
}
|
|
196
215
|
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.es-mobile-nav {
|
|
219
|
+
--mobile-nav-width: v-bind(widthPx);
|
|
197
220
|
|
|
198
221
|
:deep(> div) {
|
|
199
222
|
/* ensure the button is full-height with the rest of the nav bar items */
|
|
@@ -193,6 +193,10 @@ $shadow: 0 0 6px 0 rgba(34, 38, 51, 0.2);
|
|
|
193
193
|
background-color: variables.$white;
|
|
194
194
|
box-shadow: $shadow;
|
|
195
195
|
left: 0;
|
|
196
|
+
/* prevents scrollbar on/off from shifting the contents */
|
|
197
|
+
padding-right: var(--scrollbar-width);
|
|
198
|
+
/* allows interaction when EsMenuBar is open */
|
|
199
|
+
pointer-events: auto;
|
|
196
200
|
right: 0;
|
|
197
201
|
top: 0;
|
|
198
202
|
z-index: 1000;
|