@energysage/es-ds-components 5.5.3 → 5.5.4

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [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)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 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))
9
+
3
10
  ## [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
11
 
5
12
 
@@ -1,6 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { useScrollLock } from '@vueuse/core';
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 = useScrollLock(import.meta.client ? document.body : null);
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
- // lock page scrolling when menu is open, unless we're not showing the overlay
54
- isScrollLocked.value = props.showOverlayWhenOpen && !!newVal;
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 { useScrollLock } from '@vueuse/core';
3
- import { type NavigationMenuContent, NavigationMenuItem, NavigationMenuList, NavigationMenuRoot } from 'reka-ui';
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 = useScrollLock(import.meta.client ? document.body : null);
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
- isScrollLocked.value = !!newVal;
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 waitForAnimationDuration();
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) => {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energysage/es-ds-components",
3
- "version": "5.5.3",
3
+ "version": "5.5.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "An EnergySage Vue component library",