diff --git a/blocks/v2-breadcrumb/v2-breadcrumb.css b/blocks/v2-breadcrumb/v2-breadcrumb.css new file mode 100644 index 000000000..968b10aac --- /dev/null +++ b/blocks/v2-breadcrumb/v2-breadcrumb.css @@ -0,0 +1,63 @@ +.v2-breadcrumb-wrapper{ + background-color: #333; +} + +.v2-breadcrumb { + padding: 16px; + color: var(--c-primary-white); + max-width: var(--wrapper-width); + margin: 0 auto; + display: flex; +} + +.v2-breadcrumb .v2-breadcrumb__crumb-list { + display: flex; +} + +.v2-breadcrumb .v2-breadcrumb__crumb-item { + display: flex; + margin: 0; +} + +.v2-breadcrumb__crumb-item + .v2-breadcrumb__crumb-item::before { + content: '/'; + margin: 0 10px; +} + +.v2-breadcrumb .v2-breadcrumb__crumb-item--hidden::before { + content: ''; + margin: 0; +} + +.v2-breadcrumb .v2-breadcrumb__crumb { + text-transform: capitalize; + color: inherit; + display: flex; +} + +.v2-breadcrumb__crumb:focus { + outline: none; +} + +.v2-breadcrumb__crumb:focus-visible { + outline: 2px solid var(--border-focus); + border-radius: 2px; + outline-offset: 2px +} + +.v2-breadcrumb__crumb--active { + color: var(--c-primary-white); + font-family: var(--ff-body-bold); + white-space: nowrap; +} + +.v2-breadcrumb__crumb--active:hover { + text-decoration: none; +} + + +@media (min-width: 1200px) { + .v2-breadcrumb { + padding: 16px 0; + } +} \ No newline at end of file diff --git a/blocks/v2-breadcrumb/v2-breadcrumb.js b/blocks/v2-breadcrumb/v2-breadcrumb.js new file mode 100644 index 000000000..653113387 --- /dev/null +++ b/blocks/v2-breadcrumb/v2-breadcrumb.js @@ -0,0 +1,122 @@ +import { readBlockConfig } from '../../scripts/lib-franklin.js'; +import { createElement, getTextLabel } from '../../scripts/common.js'; + +const blockName = 'v2-breadcrumb'; +const sectionStatus = 'data-section-status'; +const breadcrumb = getTextLabel('breadcrumb'); +const homeText = { + home: getTextLabel('home'), + ellipsis: '…', // unicode ellipsis +}; + +const formatText = (str) => str.replace(/-/g, ' ').toLowerCase(); + +const getPadding = (elCompCSS) => parseInt(elCompCSS.getPropertyValue('padding-left'), 10) + + parseInt(elCompCSS.getPropertyValue('padding-right'), 10); + +const getCrumbsWidth = (block) => { + const crumbs = block.querySelectorAll(`.${blockName}__crumb-item`); + return [...crumbs].reduce((acc, item) => { + const itemCompCSS = window.getComputedStyle(item); + return acc + parseInt(itemCompCSS.getPropertyValue('width'), 10); + }, 0); +}; + +const getBlockWidth = (block) => { + const computedCSS = window.getComputedStyle(block); + const blockWidth = parseInt(computedCSS.getPropertyValue('width'), 10); + const boxSizing = computedCSS.getPropertyValue('box-sizing'); + const padding = boxSizing === 'border-box' ? getPadding(computedCSS) : 0; + return blockWidth - padding; +}; + +const fitting = (block) => getCrumbsWidth(block) < getBlockWidth(block); +export default function decorate(block) { + const cfg = readBlockConfig(block); + const hasPath = cfg && Object.hasOwn(cfg, 'path'); + const url = hasPath ? cfg.path : window.location.pathname; + const path = url.split('/').filter(Boolean); + const nav = createElement('nav', { classes: [`${blockName}__crumb-nav`] }); + const ul = createElement('ul', { classes: [`${blockName}__crumb-list`] }); + const crumbs = path.map((_, i) => { + const liEl = createElement('li', { classes: [`${blockName}__crumb-item`] }); + const content = formatText(path[i]); + const crumbProps = { 'data-content': content }; + const crumbClasses = [`${blockName}__crumb`]; + if (i !== path.length - 1) { + crumbProps.href = `/${path.slice(0, i + 1).join('/')}/`; + } else { + crumbClasses.push(`${blockName}__crumb--active`); + crumbProps['aria-current'] = 'page'; + } + const crumb = createElement('a', { classes: crumbClasses, props: crumbProps }); + crumb.textContent = content; + liEl.append(crumb); + return liEl; + }); + const homeItem = createElement('li', { classes: [`${blockName}__crumb-item`] }); + const homeEl = createElement('a', { + classes: [`${blockName}__crumb`, `${blockName}__crumb--home`], + props: { href: '/' }, + }); + + homeEl.textContent = homeText.home; + homeItem.append(homeEl); + crumbs.unshift(homeItem); + ul.append(...crumbs); + nav.append(ul); + block.textContent = ''; + block.append(nav); + block.parentElement.classList.add('full-width'); + block.setAttribute('aria-label', breadcrumb); + + const checkCrumbsFits = () => { + // 1st check if home fits, if not it become an ellipsis + if (!fitting(block) && crumbs.length > 2) homeEl.textContent = homeText.ellipsis; + // if still doesn't fit, remove active crumb + if (!fitting(block)) { + crumbs.at(-1).firstElementChild.textContent = ''; + crumbs.at(-1).classList.add(`${blockName}__crumb-item--hidden`); + } + // if it still doesn't fit again, remove the crumbs from the middle + if (!fitting(block)) { + let i = 1; + while (i < crumbs.length - 2 && !fitting(block)) { + crumbs[i].firstElementChild.textContent = ''; + crumbs[i].classList.add(`${blockName}__crumb-item--hidden`); + i += 1; + } + } + }; + + const rObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => { + if (!entry.contentBoxSize) return; + // add again the content from each item and check if it fits again or not + homeEl.textContent = homeText.home; + crumbs.forEach((crumb, i) => { + const link = crumb.firstElementChild; + if (i > 0) { + crumb.classList.remove(`${blockName}__crumb-item--hidden`); + link.textContent = link.dataset.content; + } + }); + checkCrumbsFits(); + }); + }); + + const mObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // check if the attribute data-section-status has the value 'loaded' + if (mutation.attributeName !== sectionStatus) return; + const section = mutation.target; + const status = section.getAttribute(sectionStatus); + if (status !== 'loaded') return; + rObserver.observe(block); + mObserver.disconnect(); + }); + }); + mObserver.observe(block.closest('.section'), { + childList: true, attributeFilter: [sectionStatus], + }); +} diff --git a/placeholder.json b/placeholder.json index 180cb450e..f99a2a7f7 100644 --- a/placeholder.json +++ b/placeholder.json @@ -1,7 +1,7 @@ { - "total": 26, + "total": 52, "offset": 0, - "limit": 26, + "limit": 52, "data": [ { "Key": "Low resolution video message", @@ -102,19 +102,19 @@ { "Key": "vinlabel", "Text": "Enter your 17-digit VIN (Vehicle Identification Number)" - }, + }, { "Key": "submit", "Text": "Submit" - }, + }, { "Key": "vinformat", "Text": "Please provide correct vin number format" - }, + }, { "Key": "result text", "Text": "${count} recalls available for \"${vin}\" VIN" - }, + }, { "Key": "recalls", "Text": "Recalls" @@ -134,7 +134,7 @@ { "Key": "remedy_description", "Text": "Remedy" - }, + }, { "Key": "published_info", "Text": "Information last updated" @@ -150,11 +150,11 @@ { "Key": "recall_incomplete", "Text": "Recall Incomplete" - }, + }, { "Key": "recall_incomplete_no_remedy", "Text": "Recall In Complete, remedy not available" - }, + }, { "Key": "loading recalls", "Text": " Loading Recalls ....." @@ -166,11 +166,11 @@ { "Key": "mfr_notes", "Text": "Next steps" - }, + }, { "Key": "mfr_recall_number", "Text": "Brand Recall Number" - }, + }, { "Key": "nhtsa_recall_number", "Text": "NHTSA recall number" @@ -182,7 +182,7 @@ { "Key": "All trucks", "Text": "All trucks" - }, + }, { "Key": "recall_available_info", "Text": "since:" @@ -202,6 +202,14 @@ { "Key": "tc_recall_nbr", "Text": "Transport Canada Number" + }, + { + "Key": "home", + "Text": "home" + }, + { + "Key": "breadcrumb", + "Text": "Breadcrumb" } ], ":type": "sheet"