Repository: swisspost/design-system
Pull request #4829: fix(components): fix post-header mobile menu height if scrollParent is not document.body
Author: login: oliverschuerch
Labels: name: 🚂 PR train
description: PR which follows another one.
name: Header
description: Everything related to our composible header components
Created: 2025-02-28T14:59:37Z | Merged: 2025-03-10T12:13:25Z

Depends on #4823 

- This pr should fix the mobile menu height in our post-header docs page.
- It fixes also the mobile menu second level position, if the user opens the menu first level, scrolls down and then opens the second level.
- If the user opens the mobile menu first and second level, then closes the menu with the toggle button, the second level should slideUp together with the first level, before it gets closed. Before, we had the first level sliding up and the second level sliding to the right at the same time, which looked weird.


Diff:
diff --git a/.changeset/nervous-clouds-impress.md b/.changeset/nervous-clouds-impress.md
new file mode 100644
index 0000000000..085b7b00ed
--- /dev/null
+++ b/.changeset/nervous-clouds-impress.md
@@ -0,0 +1,7 @@
+---
+'@swisspost/design-system-documentation': patch
+'@swisspost/design-system-components': patch
+'@swisspost/design-system-styles': patch
+---
+
+Fixed the mobile menu height in the `post-header` component when the scrollable parent is not the document's body.
diff --git a/.changeset/orange-kiwis-breathe.md b/.changeset/orange-kiwis-breathe.md
new file mode 100644
index 0000000000..a26320b5f6
--- /dev/null
+++ b/.changeset/orange-kiwis-breathe.md
@@ -0,0 +1,5 @@
+---
+'@swisspost/design-system-components': patch
+---
+
+Fixed the mobile menu behavior in the `post-header` and `post-megadropdown` components, so the second navigation level is always showing at the correct location in design, no matter if the user has scrolled on the first navigation level.
diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts
index b5c47c868d..06824a87ac 100644
--- a/packages/components/src/components.d.ts
+++ b/packages/components/src/components.d.ts
@@ -301,7 +301,7 @@ export namespace Components {
         /**
           * Hides the dropdown with an animation.
          */
-        "hide": (focusParent?: boolean) => Promise<void>;
+        "hide": (focusParent?: boolean, forceClose?: boolean) => Promise<void>;
         /**
           * Displays the dropdown.
          */
diff --git a/packages/components/src/components/post-header/post-header.scss b/packages/components/src/components/post-header/post-header.scss
index 896b23c812..65061b8908 100644
--- a/packages/components/src/components/post-header/post-header.scss
+++ b/packages/components/src/components/post-header/post-header.scss
@@ -179,8 +179,7 @@
     position: fixed;
     z-index: 1;
     inset-inline: 0;
-    inset-block-start: var(--header-height);
-    overflow: hidden;
+    inset-block-start: calc(var(--header-scroll-top) + var(--header-height));
   }
 
   .mobile-menu {
@@ -203,12 +202,15 @@
     > div {
       display: flex;
       flex-direction: column;
-      height: calc(100dvh - var(--header-height));
-    }
-
-    &.scroll-y > div {
       overflow-y: auto;
       overflow-x: hidden;
+      height: calc(min(var(--header-scroll-parent-height, 100dvh), 100dvh) - var(--header-height));
+    }
+
+    &.megadropdown-open {
+      > div {
+        overflow-y: hidden;
+      }
     }
 
     ::slotted(post-mainnavigation) {
diff --git a/packages/components/src/components/post-header/post-header.tsx b/packages/components/src/components/post-header/post-header.tsx
index 53ab43f3ee..fd3896cf43 100644
--- a/packages/components/src/components/post-header/post-header.tsx
+++ b/packages/components/src/components/post-header/post-header.tsx
@@ -71,7 +71,7 @@ export class PostHeader {
 
   @Watch('device')
   @Watch('mobileMenuExtended')
-  bodyLock(newValue: boolean | string, _oldValue: boolean | string, propName: string) {
+  lockBody(newValue: boolean | string, _oldValue: boolean | string, propName: string) {
     const scrollParent = this.scrollParent;
     const mobileMenuExtended =
       propName === 'mobileMenuExtended' ? newValue : this.mobileMenuExtended;
@@ -113,7 +113,7 @@ export class PostHeader {
     this.handleResize();
     this.handleScrollEvent();
     this.handleScrollParentResize();
-    this.bodyLock(false, this.mobileMenuExtended, 'mobileMenuExtended');
+    this.lockBody(false, this.mobileMenuExtended, 'mobileMenuExtended');
   }
 
   componentDidRender() {
@@ -121,10 +121,6 @@ export class PostHeader {
     this.handleLocalHeaderResize();
   }
 
-  componentDidLoad() {
-    // Check if the mega dropdown is expanded
-  }
-
   // Clean up possible side effects when post-header is disconnected
   disconnectedCallback() {
     const scrollParent = this.scrollParent;
@@ -166,7 +162,12 @@ export class PostHeader {
     // Toggle menu visibility before it slides down and after it slides back up
     if (this.mobileMenuExtended) await this.mobileMenuAnimation.finished;
     this.mobileMenuExtended = force ?? !this.mobileMenuExtended;
-    if (!this.mobileMenuExtended) await this.mobileMenuAnimation.finished;
+
+    if (this.mobileMenuExtended === false) {
+      Array.from(this.host.querySelectorAll('post-megadropdown')).forEach(dropdown => {
+        dropdown.hide(false, true);
+      });
+    }
   }
 
   private megedropdownStateHandler(event: CustomEvent) {
@@ -305,12 +306,21 @@ export class PostHeader {
   private renderNavigation() {
     const navigationClasses = ['navigation'];
 
+    const mobileMenuScrollTop = this.mobileMenu?.scrollTop ?? 0;
+
     if (this.mobileMenuExtended) {
       navigationClasses.push('extended');
     }
 
+    if (this.megadropdownOpen) {
+      navigationClasses.push('megadropdown-open');
+    }
+
     return (
-      <div class={navigationClasses.join(' ')}>
+      <div
+        class={navigationClasses.join(' ')}
+        style={{ '--header-navigation-current-inset': `${mobileMenuScrollTop}px` }}
+      >
         <div class="mobile-menu" ref={el => (this.mobileMenu = el)}>
           <slot name="post-mainnavigation"></slot>
 
diff --git a/packages/components/src/components/post-megadropdown/post-megadropdown.scss b/packages/components/src/components/post-megadropdown/post-megadropdown.scss
index e1242db70b..f3c82f9070 100644
--- a/packages/components/src/components/post-megadropdown/post-megadropdown.scss
+++ b/packages/components/src/components/post-megadropdown/post-megadropdown.scss
@@ -53,8 +53,8 @@
   border: none;
   z-index: 1;
   width: 100%;
-  top: 100%;
-  left: 0;
+  inset-block-start: 100%;
+  inset-inline-start: 0;
   border-radius: 0;
 
   @include utilities.high-contrast-mode() {
@@ -71,14 +71,13 @@
 
   @include media.max(lg) {
     z-index: 4;
-    top: 0 !important;
-    left: 0;
-    width: 100%;
     position: fixed;
-    height: calc(100dvh - var(--header-height));
-    max-height: calc(100dvh - var(--header-height));
+    inset-block-start: var(--header-navigation-current-inset);
+    inset-inline: 0;
+    height: calc(min(var(--header-scroll-parent-height, 100dvh), 100dvh) - var(--header-height));
     border-top: unset;
-    overflow: auto;
+    overflow-y: auto;
+    overscroll-behavior: contain;
 
     &.slide-in {
       animation: slide-in 350ms forwards;
diff --git a/packages/components/src/components/post-megadropdown/post-megadropdown.tsx b/packages/components/src/components/post-megadropdown/post-megadropdown.tsx
index 7963071f76..1d9f7fa6ea 100644
--- a/packages/components/src/components/post-megadropdown/post-megadropdown.tsx
+++ b/packages/components/src/components/post-megadropdown/post-megadropdown.tsx
@@ -95,14 +95,18 @@ export class PostMegadropdown {
    * Hides the dropdown with an animation.
    */
   @Method()
-  async hide(focusParent = true) {
+  async hide(focusParent = true, forceClose = false) {
     this.postToggleMegadropdown.emit({ isVisible: false, focusParent: focusParent });
-    this.animationClass = 'slide-out';
+    if (forceClose) {
+      this.forceClose();
+    } else {
+      this.animationClass = 'slide-out';
+    }
   }
 
   /**
    * Sets focus to the first focusable element within the component.
-  */
+   */
   @Method()
   async focusFirst() {
     this.firstFocusableEl?.focus();
@@ -132,6 +136,8 @@ export class PostMegadropdown {
   }
 
   private handleClickOutside = (event: MouseEvent) => {
+    if (this.device !== 'desktop') return;
+
     const target = event.target as Node;
 
     if (this.host.contains(target)) {
@@ -140,6 +146,7 @@ export class PostMegadropdown {
 
     if (target instanceof HTMLElement) {
       const trigger = target.closest('post-megadropdown-trigger');
+
       if (trigger) {
         const targetDropdownId = trigger.getAttribute('for');
         if (targetDropdownId !== this.host.id) {
diff --git a/packages/components/src/components/post-megadropdown/readme.md b/packages/components/src/components/post-megadropdown/readme.md
index 0c3c9cdf64..8f52ab0ffc 100644
--- a/packages/components/src/components/post-megadropdown/readme.md
+++ b/packages/components/src/components/post-megadropdown/readme.md
@@ -24,7 +24,7 @@ Type: `Promise<void>`
 
 
 
-### `hide(focusParent?: boolean) => Promise<void>`
+### `hide(focusParent?: boolean, forceClose?: boolean) => Promise<void>`
 
 Hides the dropdown with an animation.
 
@@ -33,6 +33,7 @@ Hides the dropdown with an animation.
 | Name          | Type      | Description |
 | ------------- | --------- | ----------- |
 | `focusParent` | `boolean` |             |
+| `forceClose`  | `boolean` |             |
 
 #### Returns
 
diff --git a/packages/documentation/src/stories/modules/header/header.styles.scss b/packages/documentation/src/stories/modules/header/header.styles.scss
index e8977fc763..133eb10dd8 100644
--- a/packages/documentation/src/stories/modules/header/header.styles.scss
+++ b/packages/documentation/src/stories/modules/header/header.styles.scss
@@ -1,10 +1,8 @@
 #storybook-docs {
   .header-story-wrapper {
-    transform: translate(0);
-
     .virtual-body {
-      max-height: 600px;
-      overflow: auto;
+      height: 500px;
+      overflow-y: auto;
     }
   }
 }
diff --git a/packages/styles/src/components/header/index.scss b/packages/styles/src/components/header/index.scss
index 69850c0376..acb704d961 100644
--- a/packages/styles/src/components/header/index.scss
+++ b/packages/styles/src/components/header/index.scss
@@ -11,6 +11,7 @@ tokens.$default-map: elements.$post-link;
 
 [data-post-scroll-locked] {
   overflow: hidden !important;
+  transform: translate3d(0, 0, 0) !important;
 }
 
 post-header {


Reviews:
user: login: alizedebray
body: It would be good to add tests to ensure there is no regression with future PRs.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T13:45:43Z
user: login: oliverschuerch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T09:02:13Z
user: login: alizedebray
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T10:53:07Z
user: login: alizedebray
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T10:53:41Z

Review comments:
diff_hunk: @@ -0,0 +1,7 @@
+---
+'@swisspost/design-system-documentation': patch
+'@swisspost/design-system-components': patch
+'@swisspost/design-system-styles': patch
+---
+
+Fixed mobile menu height in post-header component, if scrollParent is not document.body.
path: .changeset/nervous-clouds-impress.md
user: login: alizedebray
body: ```suggestion
Fixed the mobile menu height in the `post-header` component when the scrollable parent is not the document's body.
```
created_at: 2025-03-07T08:51:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,6 @@
+---
+'@swisspost/design-system-components': patch
+'@swisspost/design-system-styles': patch
+---
+
+Switched the most outer element that can be the `post-header` scrollParent from `html` to the `body` element.
path: .changeset/olive-insects-mix.md
user: login: alizedebray
body: ```suggestion
Switched the outermost scrollable parent of the `post-header` component from the `html` to the `body` element.
```
created_at: 2025-03-07T08:54:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+---
+'@swisspost/design-system-components': patch
+---
+
+Fixed mobile menu behavior in the post-header and post-megadropdown components.
path: .changeset/orange-kiwis-breathe.md
user: login: alizedebray
body: What did you fix? 
created_at: 2025-03-07T08:56:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,17 +69,18 @@ export class PostHeader {
 
   @State() megadropdownOpen: boolean = false;
 
+  @Watch('device')
   @Watch('mobileMenuExtended')
-  frozeBody(isMobileMenuExtended: boolean) {
+  bodyLock(newValue: boolean | string, _oldValue: boolean | string, propName: string) {
path: packages/components/src/components/post-header/post-header.tsx
user: login: alizedebray
body: ```suggestion
  lockBody(newValue: boolean | string, _oldValue: boolean | string, propName: string) {
```
created_at: 2025-03-07T09:00:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,14 +71,14 @@
 
   @include media.max(lg) {
     z-index: 4;
-    top: 0 !important;
-    left: 0;
-    width: 100%;
     position: fixed;
-    height: calc(100dvh - var(--header-height));
-    max-height: calc(100dvh - var(--header-height));
+    top: var(--header-navigation-scroll-top);
+    left: 0;
+    right: 0;
path: packages/components/src/components/post-megadropdown/post-megadropdown.scss
user: login: alizedebray
body: Can you use block/inline properties? 
created_at: 2025-03-07T12:59:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,15 +305,22 @@ export class PostHeader {
 
   private renderNavigation() {
     const navigationClasses = ['navigation'];
+
+    const mobileMenuScrollTop = this.mobileMenu?.scrollTop ?? 0;
+
     if (this.mobileMenuExtended) {
       navigationClasses.push('extended');
     }
-    if (!this.megadropdownOpen) {
-      navigationClasses.push('scroll-y');
+
+    if (this.megadropdownOpen) {
+      navigationClasses.push('megadropdown-open');
     }
 
     return (
-      <div class={navigationClasses.join(' ')}>
+      <div
+        class={navigationClasses.join(' ')}
+        style={{ '--header-navigation-scroll-top': `${mobileMenuScrollTop}px` }}
path: packages/components/src/components/post-header/post-header.tsx
user: login: alizedebray
body: This will be updated only when the `renderNavigation` is called so it would have an incorrect value if you just scroll the page.
created_at: 2025-03-07T13:42:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,15 +305,22 @@ export class PostHeader {
 
   private renderNavigation() {
     const navigationClasses = ['navigation'];
+
+    const mobileMenuScrollTop = this.mobileMenu?.scrollTop ?? 0;
+
     if (this.mobileMenuExtended) {
       navigationClasses.push('extended');
     }
-    if (!this.megadropdownOpen) {
-      navigationClasses.push('scroll-y');
+
+    if (this.megadropdownOpen) {
+      navigationClasses.push('megadropdown-open');
     }
 
     return (
-      <div class={navigationClasses.join(' ')}>
+      <div
+        class={navigationClasses.join(' ')}
+        style={{ '--header-navigation-scroll-top': `${mobileMenuScrollTop}px` }}
path: packages/components/src/components/post-header/post-header.tsx
user: login: oliverschuerch
body: This is intentionally, because the custom-property is only needed, if the mobile menu is open, the user scrolled down the main navigation level and then opens the second navigation level.
created_at: 2025-03-10T09:02:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -307,15 +305,22 @@ export class PostHeader {
 
   private renderNavigation() {
     const navigationClasses = ['navigation'];
+
+    const mobileMenuScrollTop = this.mobileMenu?.scrollTop ?? 0;
+
     if (this.mobileMenuExtended) {
       navigationClasses.push('extended');
     }
-    if (!this.megadropdownOpen) {
-      navigationClasses.push('scroll-y');
+
+    if (this.megadropdownOpen) {
+      navigationClasses.push('megadropdown-open');
     }
 
     return (
-      <div class={navigationClasses.join(' ')}>
+      <div
+        class={navigationClasses.join(' ')}
+        style={{ '--header-navigation-scroll-top': `${mobileMenuScrollTop}px` }}
path: packages/components/src/components/post-header/post-header.tsx
user: login: alizedebray
body: Ok! This could be renamed to something like `--header-navigation-initial-inset`, just to make it clear that the value is not dynamic in opposition to `--header-scroll-top` that is.
created_at: 2025-03-10T10:53:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-02-28T14:59:41Z
body: ###  🦋  Changeset detected

Latest commit: 74dd582c36c55b7f59ff55908baa9c8fe80a3ecb

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 12 packages</summary>

  | Name                                                  | Type  |
| ----------------------------------------------------- | ----- |
| @swisspost/design-system-documentation                | Patch |
| @swisspost/design-system-components                   | Patch |
| @swisspost/design-system-styles                       | Patch |
| @swisspost/design-system-components-angular-workspace | Patch |
| @swisspost/design-system-components-react             | Patch |
| @swisspost/design-system-components-angular           | Patch |
| @swisspost/internet-header                            | Patch |
| @swisspost/design-system-nextjs-integration           | Patch |
| @swisspost/design-system-styles-primeng-workspace     | Patch |
| @swisspost/design-system-styles-primeng               | Patch |
| @swisspost/design-system-tokens                       | Patch |
| @swisspost/design-system-icons                        | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/swisspost/design-system/new/fix/4803-fix-setting-the-post-header-scroll-parent-height-custom-property-if-scrollparent-is-something-else-than-the-html-element?filename=.changeset/new-hairs-join.md&value=---%0A%22%40swisspost%2Fdesign-system-components%22%3A%20patch%0A%22%40swisspost%2Fdesign-system-documentation%22%3A%20patch%0A%22%40swisspost%2Fdesign-system-styles%22%3A%20patch%0A---%0A%0Afix(components)%3A%20fix%20post-header%20mobile%20menu%20height%20if%20scrollParent%20is%20not%20document.body%0A)


author_association: NONE
user: login: swisspost-bot
created_at: 2025-02-28T15:02:45Z
body: **Related Previews**
- https://preview-4829--temp-next-design-system-post.netlify.app
- https://preview-4829--swisspost-design-system-next.netlify.app

author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-10T11:12:38Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=swisspost_common-web-frontend&pullRequest=4829) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=swisspost_common-web-frontend&pullRequest=4829&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=swisspost_common-web-frontend&pullRequest=4829&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=swisspost_common-web-frontend&pullRequest=4829&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=swisspost_common-web-frontend&pullRequest=4829&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=swisspost_common-web-frontend&pullRequest=4829&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=swisspost_common-web-frontend&pullRequest=4829)


author_association: NONE