Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
be5903c
Remove unnecessary slots from the post-megadropdown
alizedebray Dec 8, 2025
b963951
feat(components): Remove unnecessary slots from the post-megadropdown
alizedebray Dec 8, 2025
6a7476e
Merge branch 'main' into update-megadropdown-slots
alizedebray Dec 8, 2025
c38142a
Merge remote-tracking branch 'origin/main' into update-megadropdown-sโ€ฆ
alizedebray Dec 9, 2025
52b1a0b
fix e2e
alizedebray Dec 9, 2025
bd1dd3a
remove wrapping p-tag
alizedebray Dec 9, 2025
ceff4f4
Update label properties
alizedebray Dec 9, 2025
c60ed3b
Merge remote-tracking branch 'origin/main' into update-megadropdown-sโ€ฆ
alizedebray Dec 10, 2025
ca55a07
Apply requested changes.
alizedebray Dec 10, 2025
60573e9
Merge remote-tracking branch 'origin/main' into update-megadropdown-sโ€ฆ
alizedebray Dec 15, 2025
3d64a9e
Merge branch 'main' into update-megadropdown-slots
alizedebray Dec 16, 2025
868ed1a
feat(components): enable shadow DOM for the post-megadropdown
alizedebray Dec 10, 2025
1c41e82
Merge branch 'main' into update-megadropdown-slots
alizedebray Dec 17, 2025
b2e1f91
Merge branch 'update-megadropdown-slots' into enable-megadropdown-triโ€ฆ
alizedebray Dec 17, 2025
8912e7c
Include button in megadropdown trigger shadow DOM
alizedebray Dec 17, 2025
3a79556
Update megadropdown trigger examples
alizedebray Dec 17, 2025
596af36
Merge branch 'main' into update-megadropdown-slots
alizedebray Dec 17, 2025
7f914e4
Update PR
alizedebray Dec 17, 2025
326bbfa
Merge branch 'update-megadropdown-slots' into enable-megadropdown-triโ€ฆ
alizedebray Dec 17, 2025
333d93d
Update PR
alizedebray Dec 17, 2025
5821b3c
Use placeholder for mainnavigation controls
alizedebray Dec 18, 2025
c19eed8
Merge remote-tracking branch 'origin/main' into enable-megadropdown-tโ€ฆ
alizedebray Dec 18, 2025
e7b497e
Apply requested changes
alizedebray Dec 18, 2025
33ed3f4
Fix e2e tests
alizedebray Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/cypress/e2e/header.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ describe('header', () => {
cy.get('post-megadropdown#letters a[href="/kl"]').first().as('lettersSecondLink');
cy.get('post-megadropdown#packages a[href="/sch"]').first().as('packagesLink');

cy.get('post-megadropdown-trigger button').first().as('lettersTrigger');
cy.get('post-megadropdown-trigger button').eq(1).as('packagesTrigger');
cy.get('post-megadropdown-trigger').find('button').first().as('lettersTrigger');
cy.get('post-megadropdown-trigger').find('button').eq(1).as('packagesTrigger');

// Activate first link
cy.get('@lettersFirstLink').then($link => $link.attr('aria-current', 'page'));
Expand Down
58 changes: 36 additions & 22 deletions packages/components/cypress/e2e/mainnavigation.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {
return Math.ceil(left) >= leftEdge && Math.floor(right) <= rightEdge;
}

function clickUntilHidden($el: JQuery<HTMLElement>) {
if ($el.is(':visible')) {
cy.wrap($el).click().then(clickUntilHidden);
}
}

describe('default', () => {
beforeEach(() => {
cy.visit('./cypress/fixtures/post-mainnavigation.test.html');
Expand All @@ -42,10 +48,13 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {
cy.get('post-mainnavigation[data-hydrated]').as('mainnavigation');

cy.get('@mainnavigation')
.find(':is(a,button):not(post-megadropdown *)')
.find('a:not(post-megadropdown *), post-megadropdown-trigger')
.should('have.length', 20)
.as('navigationItems');

cy.get('@navigationItems').first().find('button').as('firstButton');
cy.get('@navigationItems').last().find('button').as('lastButton');

// remove smooth scroll to speed up the tests
cy.get('@mainnavigation')
.shadow()
Expand All @@ -56,10 +65,10 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {
});

it('should always show the navigation item that is currently focused', () => {
cy.get('@navigationItems').last().as('last').focus();
cy.get('@navigationItems').last().as('last').find('button').focus();
cy.get('@last').should('be.visible');

cy.get('@navigationItems').first().as('first').focus();
cy.get('@navigationItems').first().as('first').find('button').focus();
cy.get('@first').should('be.visible');
});

Expand All @@ -79,12 +88,10 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {
it('should click until the last navigation item is visible', () => {
cy.get('@rightScroll').should('be.visible');

cy.get('@navigationItems').each($el => {
if (!isFullyVisible($el)) cy.get('@rightScroll').click();
cy.wrap($el).then(isFullyVisible).should('be.true');
});
cy.get('@rightScroll').then(clickUntilHidden);

cy.get('@rightScroll').should('be.hidden');
cy.get('@navigationItems').last().then(isFullyVisible).should('be.true');
});

it('should scroll continuously until the last navigation item is visible', () => {
Expand All @@ -100,7 +107,12 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {
cy.get('@rightScroll').should('be.visible');

cy.get('@navigationItems').each($el => {
cy.wrap($el).focus().then(isVisible).should('be.true');
if ($el.prop('localName') === 'a') {
cy.wrap($el).focus();
} else {
cy.wrap($el).find('button').focus();
}
cy.wrap($el).then(isVisible).should('be.true');
});

cy.get('@rightScroll').should('be.hidden');
Expand All @@ -120,20 +132,20 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {

// click on the position where the scroll right button was, the last item should not be triggered
click();
cy.get('@navigationItems').last().invoke('attr', 'aria-expanded').should('not.eq', 'true');
cy.get('@lastButton').invoke('attr', 'aria-expanded').should('not.eq', 'true');

// wait and click again on the position where the scroll right button was, the last item should then be triggered
cy.wait(400);
click();
cy.get('@navigationItems').last().invoke('attr', 'aria-expanded').should('eq', 'true');
cy.get('@lastButton').invoke('attr', 'aria-expanded').should('eq', 'true');
});

it('should show the mega-dropdown at the correct position after scroll', () => {
// click until the last navigation item is visible
cy.get('@navigationItems').last().focus();
cy.get('@lastButton').focus();

// open the last mega-dropdown
cy.get('@navigationItems').last().click();
cy.get('@lastButton').click({ force: true });

// check the mega-dropdown visible and position
cy.get('@mainnavigation')
Expand All @@ -148,7 +160,7 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {

describe('left scroll', () => {
beforeEach(() => {
cy.get('@navigationItems').last().focus();
cy.get('@lastButton').focus();

cy.get('@navigationItems')
.then($options => $options.get().reverse())
Expand All @@ -170,15 +182,12 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {
it('should click until the first navigation item is visible', () => {
cy.get('@leftScroll').should('be.visible');

cy.get('@navigationItemsReversed').each($el => {
if (!isFullyVisible($el)) {
cy.get('@leftScroll').click();
}
cy.wrap($el).then(isFullyVisible).should('be.true');
});
cy.get('@leftScroll').then(clickUntilHidden);

cy.get('@leftScroll').should('be.hidden');
cy.get('@navigationItems').first().should('be.visible');
});

it('should scroll continuously until the first navigation item is visible', () => {
const leftScrollPosition = [5, 5];
cy.get('@mainnavigation').trigger('mousedown', ...leftScrollPosition, { button: 0 });
Expand All @@ -192,7 +201,12 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {
cy.get('@leftScroll').should('be.visible');

cy.get('@navigationItemsReversed').each($el => {
cy.wrap($el).focus().then(isVisible).should('be.true');
if ($el.prop('localName') === 'a') {
cy.wrap($el).focus();
} else {
cy.wrap($el).find('button').focus();
}
cy.wrap($el).then(isVisible).should('be.true');
});

cy.get('@leftScroll').should('be.hidden');
Expand All @@ -211,12 +225,12 @@ describe('mainnavigation', { baseUrl: null, includeShadowDom: true }, () => {

// click on the position where the scroll left button was, the first item should not be triggered
click();
cy.get('@navigationItems').first().invoke('attr', 'aria-expanded').should('not.eq', 'true');
cy.get('@firstButton').invoke('attr', 'aria-expanded').should('not.eq', 'true');

// wait and click again on the position where the scroll left button was, the first item should then be triggered
cy.wait(400);
click();
cy.get('@navigationItems').first().invoke('attr', 'aria-expanded').should('eq', 'true');
cy.get('@firstButton').invoke('attr', 'aria-expanded').should('eq', 'true');
});
});

Expand Down
34 changes: 15 additions & 19 deletions packages/components/cypress/e2e/megadropdown.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ describe('megadropdown', () => {
describe('desktop', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.getComponents(
MEGADROPDOWN_ID,
'tests',
'post-megadropdown',
'post-megadropdown-trigger',
);
cy.getComponents(MEGADROPDOWN_ID, 'tests', 'post-megadropdown');
cy.get('post-megadropdown-trigger[data-hydrated]')
.find('button')
.as('megadropdown-trigger');
cy.get('@megadropdown').find('.close-button').as('close-btn');
cy.get('@megadropdown').find('.megadropdown-container').as('megadropdown-container');
});
Expand All @@ -24,22 +22,22 @@ describe('megadropdown', () => {

it('should open on trigger click', () => {
cy.get('@megadropdown-trigger').should('exist');
cy.get('@megadropdown-trigger').click();
cy.get('@megadropdown-trigger').click({ force: true });
cy.get('@megadropdown-container').should('be.visible');
});

it('should show close button', () => {
cy.get('@megadropdown-trigger').click();
cy.get('@megadropdown-trigger').click({ force: true });
cy.get('@close-btn').should('be.visible');
});

it('should not show back button', () => {
cy.get('@megadropdown-trigger').click();
cy.get('@megadropdown-trigger').click({ force: true });
cy.get('@megadropdown').find('.back-button').should('not.exist');
});

it('should close on close button click', () => {
cy.get('@megadropdown-trigger').click();
cy.get('@megadropdown-trigger').click({ force: true });
cy.get('@close-btn').click();
cy.get('@megadropdown-container').should('be.hidden');
});
Expand All @@ -48,27 +46,25 @@ describe('megadropdown', () => {
describe('mobile', () => {
beforeEach(() => {
cy.viewport(500, 1200);
cy.getComponents(
MEGADROPDOWN_ID,
'tests',
'post-megadropdown',
'post-megadropdown-trigger',
);
cy.getComponents(MEGADROPDOWN_ID, 'tests', 'post-megadropdown');
cy.get('post-megadropdown-trigger[data-hydrated]')
.find('button')
.as('megadropdown-trigger');
cy.get('@megadropdown').find('.back-button').as('back-btn');
});

it('should open on trigger click', () => {
cy.get('@megadropdown-trigger').click();
cy.get('@megadropdown-trigger').click({ force: true });
cy.get('@megadropdown').should('be.visible');
});

it('should show back button', () => {
cy.get('@megadropdown-trigger').click();
cy.get('@megadropdown-trigger').click({ force: true });
cy.get('@back-btn').should('be.visible');
});

it('should not show close button', () => {
cy.get('@megadropdown-trigger').click();
cy.get('@megadropdown-trigger').click({ force: true });
cy.get('@megadropdown').find('.close-button').should('not.exist');
});
});
Expand Down
10 changes: 10 additions & 0 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ export namespace Components {
"toggle": () => Promise<void>;
}
interface PostMegadropdownTrigger {
/**
* Sets the trigger state to be active or inactive.
* @default false
*/
"active": boolean;
/**
* ID of the mega dropdown element that this trigger is linked to. Used to open and close the specified mega dropdown.
*/
Expand Down Expand Up @@ -1369,6 +1374,11 @@ declare namespace LocalJSX {
"onPostToggleMegadropdown"?: (event: PostMegadropdownCustomEvent<{ isVisible: boolean; focusParent?: boolean }>) => void;
}
interface PostMegadropdownTrigger {
/**
* Sets the trigger state to be active or inactive.
* @default false
*/
"active"?: boolean;
/**
* ID of the mega dropdown element that this trigger is linked to. Used to open and close the specified mega dropdown.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export class PostMainnavigation {
this.validateCaption();

setTimeout(() => {
this.fixLayoutShift();
this.checkScrollability();
});

Expand Down Expand Up @@ -92,26 +91,13 @@ export class PostMainnavigation {
),
);

this.fixLayoutShift();
this.checkScrollability();
}

private get navigationItems(): HTMLElement[] {
return Array.from(this.host.querySelectorAll(':is(a, button):not(post-megadropdown *)'));
}

/**
* Hack to fix the layout shift due to bold text on active elements
*/
private fixLayoutShift() {
this.navigationItems
.filter(item => !item.matches(':has(.shown-when-inactive)'))
.forEach(item => {
item.innerHTML = `
<span class="shown-when-inactive" aria-hidden="true">${item.innerHTML}</span>
${item.innerHTML}
`;
});
return Array.from(
this.host.querySelectorAll('a:not(post-megadropdown *), post-megadropdown-trigger'),
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
@use '@swisspost/design-system-styles/mixins/media';
@use '@swisspost/design-system-styles/components/header/mixins' as header-mx;
@use '@swisspost/design-system-styles/components/header/placeholders' as *;

post-megadropdown-trigger {
width: 100%;
:host {
position: relative;
display: inline-block;
}

button {
@extend %mainnavigation-control;
@include header-mx.nav-item-rotating-chevron;

// Bellow styles prevent layout shifts caused by the real button toggling between normal and bold font-weights.
> span {
position: relative;

// The hidden text provides size to the host.
> span[aria-hidden] {
font-weight: 700;
visibility: hidden;
}

button {
@include header-mx.nav-item-rotating-chevron;
// The visible text is positioned on top of the hidden one and adopts the same height and width.
> span:not([aria-hidden]) {
position: absolute;
inset: 0;
}
}

@include media.only(desktop) {
@include header-mx.active-state {
font-weight: 700;
}
}

@include media.max(desktop) {
post-icon {
transform: rotate(-90deg);
}
@include media.max(desktop) {
post-icon {
margin-inline-start: auto;
transform: rotate(-90deg);
font-size: var(--post-nav-item-icon-size);
}
}
}
Loading
Loading