Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-162519: Nav List - Link to external page #3482

Merged
merged 1 commit into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
73 changes: 52 additions & 21 deletions libs/blocks/tabs/tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createTag, MILO_EVENTS, getConfig } from '../../utils/utils.js';
import { processTrackingLabels } from '../../martech/attributes.js';

const PADDLE = '<svg aria-hidden="true" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.50001 13.25C1.22022 13.25 0.939945 13.1431 0.726565 12.9292C0.299315 12.5019 0.299315 11.8096 0.726565 11.3823L5.10938 7L0.726565 2.61768C0.299315 2.19043 0.299315 1.49805 0.726565 1.0708C1.15333 0.643068 1.84669 0.643068 2.27345 1.0708L7.4297 6.22656C7.63478 6.43164 7.75001 6.70996 7.75001 7C7.75001 7.29004 7.63478 7.56836 7.4297 7.77344L2.27345 12.9292C2.06007 13.1431 1.7798 13.2495 1.50001 13.25Z" fill="currentColor"/></svg>';
const linkedTabs = {};

const isTabInTabListView = (tab) => {
const tabList = tab.closest('[role="tablist"]');
Expand Down Expand Up @@ -43,8 +44,27 @@ const scrollStackedMobile = (content) => {
window.scrollTo({ top: topOffset, behavior: 'smooth' });
};

export function getRedirectionUrl(linkedTabsList, targetId) {
if (!targetId || !linkedTabsList[targetId] || window.location.pathname === linkedTabsList[targetId]) return '';
const currentUrl = new URL(window.location.href);
/* c8 ignore next 4 */
const tabParam = currentUrl.searchParams.get('tab');
if (tabParam) {
currentUrl.searchParams.set('tab', `${tabParam.split('-')[0]}-${targetId.split('-')[2]}`);
}
currentUrl.pathname = linkedTabsList[targetId];
return currentUrl;
}

function changeTabs(e) {
const { target } = e;
const targetId = target.getAttribute('id');
const redirectionUrl = getRedirectionUrl(linkedTabs, targetId);
/* c8 ignore next 4 */
if (redirectionUrl) {
window.location.assign(redirectionUrl);
return;
}
const parent = target.parentNode;
const content = parent.parentNode.parentNode.lastElementChild;
const targetContent = content.querySelector(`#${target.getAttribute('aria-controls')}`);
Expand Down Expand Up @@ -208,6 +228,14 @@ const handlePillSize = (pill) => {
return `${sizes[size]?.[0] ?? sizes[1]}-pill`;
};

export function assignLinkedTabs(linkedTabsList, metaSettings, id, val) {
if (!metaSettings.link || !id || !val || !linkedTabsList) return;
const relativeLinkRegex = /^\/(?:[a-zA-Z0-9-_]+(?:\/[a-zA-Z0-9-_]+)*)?$/;
if (relativeLinkRegex.test(metaSettings.link)) {
linkedTabsList[`tab-${id}-${val}`] = metaSettings.link;
}
}

const init = (block) => {
const rootElem = block.closest('.fragment') || document;
const rows = block.querySelectorAll(':scope > div');
Expand Down Expand Up @@ -289,30 +317,33 @@ const init = (block) => {
allSections.forEach((e) => {
const sectionMetadata = e.querySelector(':scope > .section-metadata');
if (!sectionMetadata) return;
const smRows = sectionMetadata.querySelectorAll(':scope > div');
smRows.forEach((row) => {
const metaSettings = {};
sectionMetadata.querySelectorAll(':scope > div').forEach((row) => {
const key = getStringKeyName(row.children[0].textContent);
if (key !== 'tab') return;
let val = getStringKeyName(row.children[1].textContent);
/* c8 ignore next */
if (!['tab', 'link'].includes(key)) return;
const val = row.children[1].textContent;
if (!val) return;
let id = tabId;
let assocTabItem = rootElem.querySelector(`#tab-panel-${id}-${val}`);
if (config.id) {
const values = row.children[1].textContent.split(',');
[id] = values;
val = getStringKeyName(String(values[1]));
assocTabItem = rootElem.querySelector(`#tab-panel-${id}-${val}`);
}
if (assocTabItem) {
const tabLabel = tabListItems[val - 1]?.innerText;
if (tabLabel) {
assocTabItem.setAttribute('data-nested-lh', `t${val}${processTrackingLabels(tabLabel, getConfig(), 3)}`);
}
const section = sectionMetadata.closest('.section');
assocTabItem.append(section);
}
metaSettings[key] = val;
});
if (!metaSettings.tab) return;
let id = tabId;
let val = getStringKeyName(metaSettings.tab);
let assocTabItem = rootElem.querySelector(`#tab-panel-${id}-${val}`);
if (config.id) {
const values = metaSettings.tab.split(',');
[id] = values;
val = getStringKeyName(String(values[1]));
assocTabItem = rootElem.querySelector(`#tab-panel-${id}-${val}`);
}
if (assocTabItem) {
assignLinkedTabs(linkedTabs, metaSettings, id, val);
const tabLabel = tabListItems[val - 1]?.innerText;
if (tabLabel) {
assocTabItem.setAttribute('data-nested-lh', `t${val}${processTrackingLabels(tabLabel, getConfig(), 3)}`);
}
const section = sectionMetadata.closest('.section');
assocTabItem.append(section);
}
});
handleDeferredImages(block);
initTabs(block, config, rootElem);
Expand Down
6 changes: 6 additions & 0 deletions nala/blocks/tabs/tabs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,11 @@ module.exports = {
path: '/drafts/nala/blocks/tabs/tabs-scrolling',
tags: '@tabs @tabs-scrolling @smoke @regression @milo @bacom',
},
{
tcid: '3',
name: 'Tabs linked to pages',
path: '/drafts/nala/blocks/tabs/linked-tabs/tabs-page-1',
tags: '@tabs @tabs-linked @smoke @regression @milo',
},
],
};
42 changes: 42 additions & 0 deletions nala/blocks/tabs/tabs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,46 @@ test.describe('Milo Tab block feature test suite', () => {
await expect(tab.tab9Panel).not.toBeVisible();
});
});

test(`[Test Id - ${features[3].tcid}] ${features[3].tags}`, async ({ page, baseURL }) => {
console.log(`[Test Page]: ${baseURL}${features[3].path}${miloLibs}`);
await page.goto(`${baseURL}${features[3].path}${miloLibs}`);
await page.waitForLoadState('networkidle');

await test.step('checking the setup', async () => {
await expect(tab.tab1).toBeVisible();
await expect(tab.tab1).toBeInViewport();
await expect(tab.tab2).toBeVisible();
await expect(tab.tab2).toBeInViewport();
await expect(tab.tab3).toBeVisible();
await expect(tab.tab3).toBeInViewport();
await expect(await tab.tab1.getAttribute('aria-selected')).toBe('true');
await expect(await tab.tab2.getAttribute('aria-selected')).toBe('false');
await expect(await tab.tab3.getAttribute('aria-selected')).toBe('false');
});

await test.step('click tab and get redirected to the proper page', async () => {
await expect(await page.url()).toContain('tabs-page-1');
await tab.tab2.click();
await expect(await page.url()).toContain('tabs-page-2');
await tab.tab3.click();
await expect(await page.url()).toContain('tabs-page-3');
await tab.tab1.click();
await expect(await page.url()).toContain('tabs-page-1');
});

await test.step('click tab and get redirected to proper page with a `tab` URL param', async () => {
const newUrl = await new URL(await page.url());
await newUrl.searchParams.set('tab', 'demo-3');
await page.goto(newUrl.toString());
await page.waitForTimeout(3000);
await expect(await page.url()).toContain('tabs-page-3?tab=demo-3');
await tab.tab2.click();
await expect(await page.url()).toContain('tabs-page-2?tab=demo-2');
await tab.tab3.click();
await expect(await page.url()).toContain('tabs-page-3?tab=demo-3');
await tab.tab1.click();
await expect(await page.url()).toContain('tabs-page-1?tab=demo-1');
});
});
});
35 changes: 34 additions & 1 deletion test/blocks/tabs/tabs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const MOBILE_WIDTH = 375;
const HEIGHT = 1500;

document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const { default: init } = await import('../../../libs/blocks/tabs/tabs.js');
const { default: init, getRedirectionUrl, assignLinkedTabs } = await import('../../../libs/blocks/tabs/tabs.js');
loadStyle('../../../libs/blocks/tabs/tabs.css');

describe('tabs', () => {
Expand Down Expand Up @@ -106,4 +106,37 @@ describe('tabs', () => {
expect(newPosition).to.be.above(oldPosition);
});
});

describe('Tabs linked to pages', () => {
it('returns an empty string when targetId or linked page are not valid', () => {
expect(getRedirectionUrl({ 'tab-1': '/testpage-1' }, '')).to.equal('');
expect(getRedirectionUrl({ 'tab-1': '/testpage-1' }, 'id-without-linked-page')).to.equal('');
});

it('replaces window.location.pathname with the linked page url', () => {
const url = getRedirectionUrl({ 'tab-1': '/testpage-1' }, 'tab-1');
expect(url.pathname).to.equal('/testpage-1');
});

it('does not save any data to linkedTabs object if invalid input', () => {
const linkedTabsList = {};
assignLinkedTabs(linkedTabsList, {}, '', '');
expect(linkedTabsList).to.deep.equal({});
assignLinkedTabs(linkedTabsList, { link: '/testpage' }, '', '');
expect(linkedTabsList).to.deep.equal({});
assignLinkedTabs(linkedTabsList, { link: '/testpage' }, 'id', '');
expect(linkedTabsList).to.deep.equal({});
assignLinkedTabs(linkedTabsList, { link: 'invalid link' }, 'id', 'val');
expect(linkedTabsList).to.deep.equal({});
});

it('saves tab id and tab link into linkedTabs object', () => {
const linkedTabsList = {};
const metaSettings = { link: '/testpage-1' };
const id = '1';
const val = 'demo';
assignLinkedTabs(linkedTabsList, metaSettings, id, val);
expect(linkedTabsList).to.deep.equal({ 'tab-1-demo': '/testpage-1' });
});
});
});
Loading