diff --git a/group/blocks/header/header.css b/group/blocks/header/header.css index 8fbabaa1..c0f5ac39 100644 --- a/group/blocks/header/header.css +++ b/group/blocks/header/header.css @@ -1,348 +1,945 @@ -/* header and nav layout */ +header .header.block { + padding: 0; +} -.header { - position: fixed; - width: 100%; - z-index: 100; +.header-wrapper { + margin-bottom: 40px; } -header .nav-wrapper { - background-color: var(--background-color); +/* ========================= + WRAPPER +========================= */ +header .nav-new-wrapper { width: 100%; - box-shadow: 0 0 7px rgb(0 0 0 / 40%); - position: relative; - z-index: 100; + box-sizing: border-box; + padding: 0 16px; + margin: 0 auto; + max-width: 1360px; + font-family: var(--body-font-family, sans-serif); + color: #17316b; } -header nav { - box-sizing: border-box; - display: grid; - grid-template: - 'brand hamburger' var(--nav-height) / 70% 30% 0; +/* ========================= + UTILITY ROW +========================= */ +header .nav-new-utility { + display: flex; align-items: center; - max-width: 1248px; - height: var(--nav-height); - padding: 0 24px 0 0; - font-family: var(--body-font-family); justify-content: space-between; + gap: 24px; + min-height: 32px; + margin: 0 auto; + padding: 8px 16px 2px; + font-size: 14px; } -header nav[aria-expanded='true'] { - grid-template: - 'brand hamburger' var(--nav-height) - 'sections sections' 1fr / 70% 30%; - overflow-y: auto; - min-height: 100dvh; +header .nav-new-utility-left, +header .nav-new-utility-right { + display: flex; + align-items: center; + gap: 24px; } -@media (width >= 900px) { - header nav { - display: flex; - justify-content: space-between; - max-width: unset; - padding: 0; - } +header .nav-new-language { + position: relative; +} - header nav[aria-expanded='true'] { - min-height: 0; - overflow: visible; - } +header .nav-new-language-label, +header .nav-new-utility-link, +header .nav-new-login, +header .nav-new-language-toggle { + color: #17316b; + text-decoration: none; +} + +header .nav-new-utility-link:hover { + text-decoration: none; } -header nav p { + +/* ========================= + LANGUAGE TOGGLE +========================= */ +header .nav-new-language-toggle { + display: flex; + align-items: center; + gap: 8px; + position: relative; margin: 0; + padding: 10px 8px; + border: 0; + background: transparent; + color: #162c55; + text-decoration: none; + font: inherit; + font-size: 14px; line-height: 1; + font-weight: 300; + cursor: pointer; + white-space: nowrap; + box-sizing: border-box; +} + +header .nav-new-language-toggle:focus-visible { + outline: 2px solid #06c; + outline-offset: 3px; + border-radius: 10px; } -header nav a:any-link { - color: currentcolor; +header .nav-new-language-toggle:hover, +header .nav-new-language-toggle:focus-visible, +header .nav-new-language.is-open .nav-new-language-toggle { + font-weight: 600; text-decoration: none; } -/* hamburger */ -header nav .nav-hamburger { - grid-area: hamburger; - height: 22px; +header .nav-new-language-toggle::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 4px; + background: #b3f8ff; + transition: width 0.25s ease; +} + +header .nav-new-language:hover .nav-new-language-toggle::after, +header .nav-new-language.is-open .nav-new-language-toggle::after { + width: 100%; +} + +header .nav-new-language-label { display: flex; align-items: center; - justify-content: end; + gap: 8px; + padding: 10px 8px; + color: #162c55; + font: inherit; + font-size: 16px; + line-height: 1; + font-weight: 300; + white-space: nowrap; + box-sizing: border-box; } -header nav .nav-hamburger button { - height: 22px; - margin: 0; - border: 0; - border-radius: 0; - padding: 0; - background-color: var(--background-color); - color: inherit; - overflow: initial; - text-overflow: initial; - white-space: initial; +/* ========================= + LOGIN +========================= */ +header .nav-new-login { + display: inline-flex; + align-items: center; + gap: 12px; + font-weight: 600; + font-size: 12px; } -header nav .nav-hamburger-icon, -header nav .nav-hamburger-icon::before, -header nav .nav-hamburger-icon::after { - box-sizing: border-box; +header .nav-new-login:hover { + text-decoration: none; +} + +header .nav-new-login:focus-visible { + outline: 2px solid #06c; + outline-offset: 3px; + border-radius: 10px; +} + +header .nav-new-login-icon { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; +} + +header .nav-new-login-icon img { display: block; + width: 100%; + height: 100%; +} + +/* ========================= + MAIN PILL SHELL +========================= */ +header .nav-new-shell { + display: flex; + justify-content: space-between; + align-items: center; + align-self: stretch; + gap: 32px; + height: 88px; + background: #fbf9f7; + border-radius: 104px; + padding-left: 40px; + padding-right: 32px; + max-width: 1408px; + margin: 0 auto; position: relative; - width: 20px; + box-sizing: border-box; + top: 6px; } -header nav .nav-hamburger-icon::before, -header nav .nav-hamburger-icon::after { - content: ''; - position: absolute; - background: currentcolor; +/* ========================= + STICKY NAV SHELL +========================= */ +header .nav-new-wrapper.has-sticky-shell { + padding-top: 88px; } -header nav[aria-expanded='false'] .nav-hamburger-icon, -header nav[aria-expanded='false'] .nav-hamburger-icon::before, -header nav[aria-expanded='false'] .nav-hamburger-icon::after { - height: 2px; - border-radius: 2px; - background: currentcolor; +header .nav-new-shell.is-sticky { + position: fixed; + top: 6px; + left: 50%; + transform: translateX(-50%); + width: calc(100% - 32px); + max-width: 1408px; + z-index: 1000; + box-shadow: 0 15px 25px 0 rgb(34 10 51 / 5%), 0 0 10px 0 rgb(34 10 51 / 5%); +} + +/* ========================= + BRAND +========================= */ +header .nav-new-brand { + display: flex; + align-items: center; + flex: 0 0 auto; +} + +header .nav-new-brand-link { + display: inline-flex; + align-items: center; + text-decoration: none; + color: inherit; } -header nav[aria-expanded='false'] .nav-hamburger-icon::before { - top: -6px; +header .nav-new-brand-image { + display: block; + width: 110px; + height: auto; } -header nav[aria-expanded='false'] .nav-hamburger-icon::after { - top: 6px; +header .nav-new-brand-image-multi-state { + display: block; + width: 240px; + height: auto; } -header nav[aria-expanded='true'] .nav-hamburger-icon { - height: 22px; +/* ========================= + MAIN NAV AREA +========================= */ +header .nav-new-main { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1 1 auto; + min-width: 0; } -header nav[aria-expanded='true'] .nav-hamburger-icon::before, -header nav[aria-expanded='true'] .nav-hamburger-icon::after { - top: 3px; - left: 1px; - transform: rotate(45deg); - transform-origin: 2px 1px; - width: 24px; - height: 2px; - border-radius: 2px; +header .nav-new-primary { + display: flex; + justify-content: flex-end; + flex: 1 1 auto; + min-width: 0; } -header nav[aria-expanded='true'] .nav-hamburger-icon::after { - top: unset; - bottom: 3px; - transform: rotate(-45deg); +header .nav-new-list { + list-style: none; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 28px; + margin: 0; + padding: 0; + min-width: 0; + width: fit-content; } -@media (width >= 900px) { - header nav .nav-hamburger { - display: none; - visibility: hidden; - } +header .nav-new-item { + position: relative; + list-style: none; } -/* brand */ -header .nav-brand { - grid-area: brand; - font-size: var(--heading-font-size-s); - font-weight: 700; +/* ========================= + NAV ITEMS +========================= */ +header .nav-new-link, +header .nav-new-dropdown-toggle { + display: flex; + align-items: center; + gap: 8px; + height: 88px; + padding: 10px 8px; + margin: 0; + border: 0; + background: transparent; + color: #162c55; + text-decoration: none; + font: inherit; + font-size: 16px; line-height: 1; - padding: 1rem; + font-weight: 300; + cursor: pointer; + white-space: nowrap; + box-sizing: border-box; } +header .nav-new-link:focus-visible, +header .nav-new-dropdown-toggle:focus-visible { + outline: 2px solid #06c; + outline-offset: 3px; + border-radius: 10px; +} -header nav .nav-brand picture img { - width: 164px; - height: auto; +header .nav-new-link:hover, +header .nav-new-dropdown-toggle:hover { + font-weight: 600; } -header nav .nav-brand .icon { - width: 100%; +header .nav-new-item.is-current > .nav-new-link, +header .nav-new-item.is-active > .nav-new-dropdown-toggle, +header .nav-new-item.is-open > .nav-new-dropdown-toggle { + font-weight: 600; } -header nav .nav-brand .icon img { - width: 128px; - height: auto; +header .nav-new-chevron { + width: 6px; + height: 6px; + display: inline-block; + border-right: 1px solid currentcolor; + border-bottom: 1px solid currentcolor; + transform: rotate(45deg) translateY(-1px); + transition: transform 0.2s ease; + margin-top: 2px; } -header nav .nav-brand .icon.icon-multi-state-logo img { - width: 200px; +/* only rotate open chevrons on desktop */ +header .nav-new-item.is-open .nav-new-chevron, +header .nav-new-language.is-open .nav-new-chevron { + transform: rotate(-135deg) translateY(-1px); + margin-top: 2px; } -header .nav-brand div:first-of-type { +/* ========================= + ACTIONS +========================= */ +header .nav-new-actions { display: flex; align-items: center; - gap: 20px; + justify-content: flex-end; + gap: 16px; + flex: 0 0 auto; } -/* sections */ -header nav .nav-sections { - grid-area: sections; - flex: 1 1 auto; - display: none; - visibility: hidden; +header .nav-new-contact { + display: inline-flex; + flex-direction: column; + gap: 2px; + color: #17316b; + text-decoration: none; + font-size: 14px; + line-height: 1.3; + white-space: nowrap; } -header nav[aria-expanded='true'] .nav-sections { - display: block; - visibility: visible; - align-self: start; - width: 100vw; - background-color: var(--link-color); - color:var(--background-color); - padding: var(--spacing-xl); +header .nav-new-contact-phone { + font-weight: 600; +} + +header .nav-new-contact-detail { + color: #42526e; +} + +header .nav-new-cta { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 48px; + padding: 0 24px; + border-radius: 999px; + background: #06c; + color: #fff; + text-decoration: none; + font-size: 16px; + line-height: 1.1; + font-weight: 700; + white-space: nowrap; +} + +header .nav-new-utility-link:focus-visible, +header .nav-new-cta:focus-visible, +header .nav-new-contact:focus-visible { + outline: 2px solid #06c; + outline-offset: 3px; + border-radius: 10px; +} + + +header .nav-new-link::after, +header .nav-new-dropdown-toggle::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 4px; + background: #b3f8ff; + transition: width 0.25s ease; +} + +header .nav-new-item:hover .nav-new-link::after, +header .nav-new-item:hover .nav-new-dropdown-toggle::after { + width: 100%; +} + +/* ========================= + DROPDOWN ANIMATIONS +========================= */ +@keyframes animate-open-dropdown { + 0% { + opacity: 0; + transform: translateY(0) scale(1); + } + + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } } -header nav .nav-sections ul { +@keyframes animate-close-dropdown { + 0% { + opacity: 1; + transform: translateY(0) scale(1); + } + + 100% { + opacity: 0; + transform: translateY(0) scale(1); + } +} + +/* ========================= + DROPDOWNS +========================= */ +header .nav-new-dropdown, +header .nav-new-language-panel { + position: absolute; + top: calc(100% + 14px); + left: 0; + min-width: 238px; + background: #fff; + border-radius: 16px; + box-shadow: 0 15px 25px 0 rgb(34 10 51 / 15%), 0 0 10px 0 rgb(34 10 51 / 5%); + overflow: hidden; + z-index: 10; + margin-top: -30px; + margin-left: -15px; + transform-origin: top left; +} + + +header .nav-new-dropdown-list, +header .nav-new-language-list { list-style: none; - padding-left: 0; - font-size: var(--body-font-size-s); - margin-top: 0; + margin: 0; + padding: 0; } -header nav .nav-sections ul > li { - font-weight: var(--font-weight-medium); +header .nav-new-dropdown-item, +header .nav-new-language-list li { + list-style: none; } -header nav .nav-sections ul > li > ul > li { +header .nav-new-dropdown-link, +header .nav-new-language-link { + display: block; + padding: 16px 24px; + color: #17316b; + text-decoration: none; + font-size: 16px; + line-height: 1; + font-weight: 300; + background: transparent; + transition: background-color 0.2s ease; +} + +header .nav-new-dropdown-link.is-featured { + background: transparent; + font-weight: 500; +} + +.nav-new-dropdown-link-detail { + display: block; + font-size: var(--body-font-size-xs); + line-height: 1.3; + padding-top: 4px; + color: #25282a; font-weight: var(--font-weight-regular); } -header nav[aria-expanded='true'] .nav-sections ul > li { - font-weight: var(--font-weight-bold); - padding: 0 0 var(--spacing-xl); +/* ========================= + LANGUAGE PANEL +========================= */ +header .nav-new-language-panel { + min-width: 148px; + border-radius: 16px; + box-shadow: 0 15px 25px 0 rgb(34 10 51 / 15%), 0 0 10px 0 rgb(34 10 51 / 5%); + margin-top: -10px; + margin-left: -16px; + overflow: hidden; + border: 1px solid #eceded; +} + +header .nav-new-language-list li + li { + border-top: 1px solid #e3e7ef; +} + +header .nav-new-language-link { + padding: 16px 24px; + font-size: 14px; + font-weight: 300; + color: #162c55; + text-align: left; +} + +header .nav-new-dropdown-link:hover, +header .nav-new-dropdown-link:focus-visible, +header .nav-new-language-link:hover, +header .nav-new-language-link:focus-visible { + background: #c9edf4; + outline: none; +} + +header .nav-new-item.is-open .nav-new-dropdown, +header .nav-new-language.is-open .nav-new-language-panel { + animation: animate-open-dropdown 0.7s; +} + + +/* ========================= + HAMBURGER +========================= */ +header .nav-new-hamburger { + display: none; + margin-left: auto; } -header nav .nav-sections ul > li > ul { - margin-top: 0; +header .nav-new-hamburger-button { + width: 48px; + height: 48px; + border: 0; + background: transparent; + padding: 0; + cursor: pointer; } -header nav .nav-sections ul > li > ul > li sub{ +header .nav-new-hamburger-icon, +header .nav-new-hamburger-icon::before, +header .nav-new-hamburger-icon::after { display: block; + width: 22px; + height: 2px; + background: #17316b; + border-radius: 2px; + position: relative; + margin: 0 auto; + transition: transform 0.2s ease, opacity 0.2s; } -header nav[aria-expanded='true'] .nav-sections ul > li > ul > li { - font-weight: var(--font-weight-regular); - padding: var(--spacing-s) 0 0; +header .nav-new-hamburger-icon::before, +header .nav-new-hamburger-icon::after { + content: ''; + position: absolute; + left: 0; +} + +header .nav-new-hamburger-icon::before { + top: -7px; +} + +header .nav-new-hamburger-icon::after { + top: 7px; +} + +header .header.block.nav-new-mobile-open .nav-new-hamburger-icon { + background: transparent; +} + +header .header.block.nav-new-mobile-open .nav-new-hamburger-icon::before { + top: 0; + transform: rotate(45deg); +} + +header .header.block.nav-new-mobile-open .nav-new-hamburger-icon::after { + top: 0; + transform: rotate(-45deg); +} + + +/* ========================= + RESPONSIVE +========================= */ + +@media (width <= 1200px) { + header .nav-new-wrapper { + padding-left: 20px; + padding-right: 20px; + } + + header .nav-new-shell { + padding-left: 28px; + padding-right: 24px; + gap: 24px; + } + + header .nav-new-list { + gap: 22px; + } + + header .nav-new-link, + header .nav-new-dropdown-toggle { + font-size: 14px; + } + + header .nav-new-dropdown-link, + header .nav-new-language-link { + font-size: 14px; + } + + header .nav-new-language-toggle { + font-size: 14px; + } + + header .nav-new-language-link { + font-size: 14px; + } } -@media (width >= 900px) { - header .nav-brand { - padding: 0 0 0 2.5rem; - margin: 0 auto; +@media (width <= 899px) { + header .nav-new-wrapper { + padding: 14px 16px 0; + } + + header .nav-new-utility { + min-height: auto; + padding: 0 8px 14px; + gap: 14px; + align-items: center; + } + + header .nav-new-utility-left, + header .nav-new-utility-right { + gap: 16px; + align-items: center; + } + + header .nav-new-language-toggle, + header .nav-new-language-label { + padding: 4px 0; + font-size: 14px; + font-weight: 400; + } + + header .nav-new-login { + font-size: 14px; + gap: 10px; + } + + header .nav-new-login-icon { + width: 28px; + height: 28px; + } + + header .nav-new-shell { + height: 76px; + padding-left: 40px; + padding-right: 30px; + gap: 16px; + border-radius: 999px; + } + + /* ========================= + STICKY NAV - MOBILE + ========================= */ + header .nav-new-wrapper.has-sticky-shell { + padding-top: 76px; } - header nav .nav-brand picture img { - width: 219px; + header .nav-new-shell.is-sticky { + position: fixed; + top: 8px; + left: 16px; + right: 16px; + transform: none; + width: auto; + max-width: none; + border-radius: 999px; + z-index: 1000; + box-shadow: 0 15px 25px 0 rgb(34 10 51 / 10%), 0 0 10px 0 rgb(34 10 51 / 6%); } - header nav .nav-sections { + header .nav-new-brand-image { + width: 110px; + } + + /* hide desktop actions inside pill */ + header .nav-new-actions { + display: none; + } + + /* hamburger shown in pill */ + header .nav-new-hamburger { display: block; - visibility: visible; - white-space: nowrap; - height: 100%; + margin-left: auto; } - header nav[aria-expanded='true'] .nav-sections { - align-self: unset; - width: unset; - background-color: unset; - color:unset; - padding: 0; + header .nav-new-hamburger-button { + width: 48px; + height: 48px; } - header nav[aria-expanded='true'] .nav-sections ul > li { - font-weight: unset; - padding: 0 var(--spacing-m); + header .nav-new-hamburger-icon, + header .nav-new-hamburger-icon::before, + header .nav-new-hamburger-icon::after { + width: 30px; + height: 2px; + border-radius: 2px; } - header nav .nav-sections .nav-drop { - position: relative; - padding-right: var(--spacing-m); - cursor: pointer; + header .nav-new-hamburger-icon::before { + top: -9px; } - header nav .nav-sections .nav-drop::after { - content: ''; - display: inline-block; + header .nav-new-hamburger-icon::after { + top: 9px; + } + + /* floating menu card */ + header .nav-new-main { position: absolute; - top: 1.5rem; - right: var(--spacing-s); - transform: rotate(135deg); + top: 90px; + right: 25px; + left: auto; + width: 320px; + display: none; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + gap: 24px; + padding: 24px; + background: #fbf9f7; + border-radius: 40px; + box-shadow: 0 15px 25px 0 rgb(34 10 51 / 15%), 0 0 10px 0 rgb(34 10 51 / 5%); + z-index: 999; + box-sizing: border-box; + } + + header .header.block.nav-new-mobile-open .nav-new-main { + display: flex; + } + + header .nav-new-primary, + header .nav-new-actions { + width: 100%; + background: transparent; + border-radius: 0; + box-shadow: none; + } + + header .nav-new-primary { + overflow: visible; + } + + header .nav-new-list { + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + gap: 16px; + width: 100%; + } + + header .nav-new-item { + border-bottom: 0; + } + + header .nav-new-link, + header .nav-new-dropdown-toggle { + width: 100%; + min-height: 0; + height: auto; + justify-content: space-between; + padding: 0; + font-size: 16px; + line-height: 1.15; + font-weight: 400; + } + + header .nav-new-item.is-open > .nav-new-dropdown-toggle, + header .nav-new-item.is-active > .nav-new-dropdown-toggle { + font-weight: 700; + } + + header .nav-new-link::after, + header .nav-new-dropdown-toggle::after { + display: none; + } + + /* keep mobile chevrons downward by default */ + header .nav-new-chevron { width: 6px; height: 6px; - color: var(--link-color); - border: 2px solid currentcolor; - border-radius: 0 1px 0 0; - border-width: 2px 2px 0 0; + border-right: 1px solid currentcolor; + border-bottom: 1px solid currentcolor; + margin-top: 0; + margin-right: 4px; + transform: rotate(45deg); } - header nav .nav-sections .nav-drop[aria-expanded='true']::after { - top: unset; - bottom: 0.4em; - transform: rotate(315deg); + header .nav-new-item.is-open .nav-new-chevron { + transform: rotate(-135deg); } - header nav .nav-sections ul { - display: flex; - gap: 0; + /* dropdown content inside same card */ + header .nav-new-dropdown { + position: static; + min-width: 100%; margin: 0; - justify-content: center; + padding: 16px 0 16px 8px; + background: transparent; + border-radius: 0; + box-shadow: none; + border-top: 0; + overflow: visible; + animation: none; } - header nav .nav-sections .default-content-wrapper { - height: 100%; + header .nav-new-dropdown-list { + display: flex; + flex-direction: column; + gap: 18px; } - header nav .nav-sections .default-content-wrapper > ul { - height: 100%; - align-content: center; - flex-wrap: wrap; + header .nav-new-dropdown-link { + padding: 0; + background: transparent; + font-size: 16px; + line-height: 1.4; + font-weight: 400; + color: #162c55; + text-decoration: none; + border-bottom: 2px solid transparent; } - header nav .nav-sections .default-content-wrapper > ul > li { - flex: 0 1 auto; - position: relative; - font-weight: var(--font-weight-regular); - height: 100%; - align-content: center; - padding: 0 var(--spacing-xl); + header .nav-new-dropdown-link.is-featured { + background: transparent; + font-weight: 400; } - header nav .nav-sections .default-content-wrapper > ul > li > ul { - display: none; - position: relative; + header .nav-new-dropdown-link:hover, + header .nav-new-dropdown-link:focus-visible { + background: transparent; + text-decoration: none; + border-bottom: 2px solid #b3f8ff; + width: fit-content; } - header nav .nav-sections .default-content-wrapper > ul > li[aria-expanded='true'] { - padding: 0 var(--spacing-xl); - background-color: var(--link-color); - color: white; - min-width: unset; - width: inherit; - border-bottom: none; + /* open section accent */ + header .nav-new-item.is-open > .nav-new-dropdown-toggle { + position: relative; } - header nav .nav-sections .default-content-wrapper > ul > li[aria-expanded='true'] > ul { + header .nav-new-item.is-open > .nav-new-dropdown-toggle::after { display: block; + content: ''; position: absolute; left: 0; - width: 22rem; - top: 100%; - background-color: var(--link-color); - white-space: initial; - color: white; + bottom: -12px; + width: 140px; + height: 4px; + background: #b3f8ff; } - header nav .nav-sections .default-content-wrapper > ul > li > ul > li { - padding: var(--spacing-m) var(--spacing-xl); + /* language dropdown stays utility-like */ + header .nav-new-language-panel { + position: absolute; + top: calc(100% + 8px); + left: 0; + min-width: 160px; + border-radius: 16px; + box-shadow: 0 15px 25px 0 rgb(34 10 51 / 15%), 0 0 10px 0 rgb(34 10 51 / 5%); + border: 1px solid #eceded; + background: #fff; + overflow: hidden; + z-index: 1001; } - header nav .nav-sections .default-content-wrapper > ul > li > ul > li:hover { - background-color: #FFFFFF0D; + header .nav-new-language-link { + padding: 14px 18px; + font-size: 15px; + font-weight: 400; } +} - header nav .nav-sections .default-content-wrapper > ul > li > ul > li > a:hover { - text-decoration: none; +@media (width <= 640px) { + header .nav-new-wrapper { + padding-left: 12px; + padding-right: 12px; } -} + + header .nav-new-utility { + flex-wrap: nowrap; + align-items: center; + padding: 0 6px 10px; + } + + header .nav-new-utility-left, + header .nav-new-utility-right { + width: auto; + justify-content: flex-start; + } + + header .nav-new-login { + margin-left: auto; + } + + header .nav-new-shell { + height: 72px; + padding-left: 40px; + padding-right: 32px; + } + + header .nav-new-wrapper.has-sticky-shell { + padding-top: 72px; + } + + header .nav-new-shell.is-sticky { + top: 8px; + left: 40px; + right: 32px; + width: auto; + } + + header .nav-new-brand-image { + width: 91px; + } + + header .nav-new-main { + top: 108px; + right: 12px; + width: calc(100vw - 24px); + max-width: 320px; + padding: 22px; + border-radius: 36px; + } + + header .nav-new-link, + header .nav-new-dropdown-toggle { + font-size: 16px; + } + + header .nav-new-dropdown-link { + font-size: 16px; + } +} \ No newline at end of file diff --git a/group/blocks/header/header.js b/group/blocks/header/header.js index 6e400e1f..b109c12a 100644 --- a/group/blocks/header/header.js +++ b/group/blocks/header/header.js @@ -1,181 +1,1164 @@ import { getMetadata } from '../../scripts/aem.js'; import { loadFragment } from '../fragment/fragment.js'; -import { wrapTextInLinks } from '../../scripts/utils.js'; -import enableRowLinks from '../../scripts/row-link.js'; -// media query match that indicates mobile/tablet width -const isDesktop = window.matchMedia('(min-width: 900px)'); +const DESKTOP = window.matchMedia('(min-width: 900px)'); +const HOME_FALLBACK_URL = '/'; -function closeOnEscape(e) { - if (e.code === 'Escape') { - const nav = document.getElementById('nav'); - const navSections = nav.querySelector('.nav-sections'); - const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]'); - if (navSectionExpanded && isDesktop.matches) { - // eslint-disable-next-line no-use-before-define - toggleAllNavSections(navSections); - navSectionExpanded.focus(); - } else if (!isDesktop.matches) { - // eslint-disable-next-line no-use-before-define - toggleMenu(nav, navSections); - nav.querySelector('button').focus(); +const LOGOS = { + default: { + token: ':logo:', + selector: '.icon-logo, img[src*="/logo.svg"]', + src: '/group/icons/logo.svg', + alt: 'Blue Shield of California', + width: '110', + }, + multiState: { + token: ':multi-state-logo:', + selector: '.icon-multi-state-logo, img[src*="multi-state-logo.svg"]', + src: '/group/icons/multi-state-logo.svg', + alt: 'Blue Shield California and National Coverage', + width: '240', + }, +}; + +const NEW_WINDOW_TEXT = 'Open the link in a new window'; + +/** + * Small DOM helper + * @param {string} tag + * @param {Object} attrs + * @param {string|Node|Array} content + * @returns {HTMLElement} + */ +function createTag(tag, attrs = {}, content = '') { + const el = document.createElement(tag); + + Object.entries(attrs).forEach(([key, value]) => { + if (value !== null && value !== undefined) { + el.setAttribute(key, value); } + }); + + if (Array.isArray(content)) { + el.append(...content); + } else if (content instanceof Node) { + el.append(content); + } else if (content) { + el.innerHTML = content; } + + return el; } -function closeOnFocusLost(e) { - const nav = e.currentTarget; - if (!nav.contains(e.relatedTarget)) { - const navSections = nav.querySelector('.nav-sections'); - const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]'); - if (navSectionExpanded && isDesktop.matches) { - // eslint-disable-next-line no-use-before-define - toggleAllNavSections(navSections, false); - } else if (!isDesktop.matches) { - // eslint-disable-next-line no-use-before-define - toggleMenu(nav, navSections, false); - } +function normalizeText(text = '') { + return text.replace(/\s+/g, ' ').trim(); +} + +/** + * Removes accessibility helper text that may have been added to links before + * this header parses and rebuilds the navigation. + * @param {string} text + * @returns {string} + */ +function cleanNavText(text = '') { + return normalizeText(text.split(NEW_WINDOW_TEXT).join('')); +} + +/** + * Gets the visible label text from an element without screen-reader-only copy. + * @param {Element} el + * @returns {string} + */ +function getElementText(el) { + if (!el) return ''; + + const clone = el.cloneNode(true); + clone.querySelectorAll('.sr-only').forEach((srOnly) => srOnly.remove()); + + return cleanNavText(clone.textContent); +} + +/** + * Gets the logo data based on the logo token authored in the nav document. + * Also supports the case where AEM has already converted :multi-state-logo: + * into an icon span/image before this header parses the fragment. + * @param {Element} section + * @returns {{token: string, selector: string, src: string, alt: string, width: string}} + */ +function getLogoData(section) { + const text = cleanNavText(section?.textContent || '').toLowerCase(); + + if ( + text.includes(LOGOS.multiState.token) + || section?.querySelector(LOGOS.multiState.selector) + ) { + return LOGOS.multiState; } + + return LOGOS.default; +} + +function slugify(text = '') { + return normalizeText(text) + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); +} + +function isExternalUrl(url = '') { + return /^https?:\/\//i.test(url); +} + +function isPhone(text = '') { + return /^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/.test(normalizeText(text)); +} + +function toTelHref(text = '') { + return `tel:${text.replace(/[^\d]/g, '')}`; +} + +function getNavPath() { + const navMeta = getMetadata('nav'); + return navMeta ? new URL(navMeta.toLowerCase(), window.location).pathname : '/nav-new'; +} + +function getTopLevelSections(fragment) { + return [...fragment.children].filter((child) => child.nodeType === 1); +} + +function getFirstLink(el) { + return el?.querySelector('a') || null; +} + +function getFirstList(section) { + return section?.querySelector('ul, ol') || null; +} + +function getDirectListItems(list) { + return list ? [...list.children].filter((li) => li.matches('li')) : []; +} + +function getDirectNestedList(li) { + return [...li.children].find((child) => child.matches?.('ul, ol')) || null; +} + +function getDirectAnchor(li) { + return [...li.children].find((child) => child.matches?.('a')) || li.querySelector(':scope > a'); +} + +function getDirectTextWithoutNestedList(li) { + const clone = li.cloneNode(true); + [...clone.querySelectorAll('ul, ol, .sr-only')].forEach((nested) => nested.remove()); + return cleanNavText(clone.textContent); +} + +function getHrefOrFallback(anchor, fallback = '#') { + const href = anchor?.getAttribute('href'); + return href || fallback; } -function openOnKeydown(e) { - const focused = document.activeElement; - const isNavDrop = focused.className === 'nav-drop'; - if (isNavDrop && (e.code === 'Enter' || e.code === 'Space')) { - const dropExpanded = focused.getAttribute('aria-expanded') === 'true'; - // eslint-disable-next-line no-use-before-define - toggleAllNavSections(focused.closest('.nav-sections')); - focused.setAttribute('aria-expanded', dropExpanded ? 'false' : 'true'); +/** + * Gets a dropdown child label and any sibling detail text that sits next + * to the authored link in the nav document. + * Example authored text: "(800) 334-5847 (TTY 711) 7 a.m. - 8 p.m. PT; 7 days a week" + * The anchor text is used as the label, and the remaining text is used as detail. + * @param {Element} childLi + * @returns {{label: string, detail: string, href: string}} + */ +function getDropdownChildData(childLi) { + const childAnchor = getDirectAnchor(childLi); + const fullText = getDirectTextWithoutNestedList(childLi); + const label = getElementText(childAnchor) || fullText; + const href = getHrefOrFallback(childAnchor, '#'); + + let detail = ''; + + if (label && fullText && fullText !== label) { + detail = fullText.startsWith(label) + ? cleanNavText(fullText.slice(label.length)) + : cleanNavText(fullText.replace(label, '')); } + + return { label, detail, href }; +} + +function getPagePath() { + return window.location.pathname.replace(/\/$/, '') || '/'; +} + +function pathsMatch(a = '', b = '') { + const normalize = (value) => { + if (!value) return ''; + if ( + isExternalUrl(value) + || value.startsWith('mailto:') + || value.startsWith('tel:') + ) return value; + return value.replace(/\/$/, '') || '/'; + }; + + return normalize(a) === normalize(b); } -function focusNavSection() { - document.activeElement.addEventListener('keydown', openOnKeydown); +function isEnglishLabel(label = '') { + return /^english$/i.test(normalizeText(label)); +} + +function isSpanishLabel(label = '') { + return /^(español|spanish)$/i.test(normalizeText(label)); +} + +function getBooleanMetadata(name) { + const value = normalizeText(getMetadata(name) || ''); + + if (!value) return null; + if (/^(yes|true|1|on)$/i.test(value)) return true; + if (/^(no|false|0|off)$/i.test(value)) return false; + + return null; } /** - * Toggles all nav sections - * @param {Element} sections The container element - * @param {Boolean} expanded Whether the element should be expanded or collapsed + * Identify the optional utility section. + * This lets older nav docs work without requiring the utility table. + * If the utility table exists, we parse it. If not, we skip it. */ -function toggleAllNavSections(sections, expanded = false) { - sections.querySelectorAll('.nav-sections .default-content-wrapper > ul > li').forEach((section) => { - section.setAttribute('aria-expanded', expanded); - }); +function isUtilitySection(section) { + if (!section) return false; + + const text = cleanNavText(section.textContent).toLowerCase(); + + return ( + text.includes('select') + || text.includes('login/register') + || text.includes(':profile:') + || ( + text.includes('english') + && (text.includes('spanish') || text.includes('español')) + ) + ); } /** - * Toggles the entire nav - * @param {Element} nav The container element - * @param {Element} navSections The nav sections within the container element - * @param {*} forceExpanded Optional param to force nav expand behavior when not null + * Identify the optional brand section. + * Brand can be authored with a :logo: token, but if older docs do not + * include a brand section, buildBrand() still falls back to the default logo. */ -function toggleMenu(nav, navSections, forceExpanded = null) { - const expanded = forceExpanded !== null ? !forceExpanded : nav.getAttribute('aria-expanded') === 'true'; - const button = nav.querySelector('.nav-hamburger button'); - document.body.style.overflowY = (expanded || isDesktop.matches) ? '' : 'hidden'; - nav.setAttribute('aria-expanded', expanded ? 'false' : 'true'); - toggleAllNavSections(navSections, expanded || isDesktop.matches ? 'false' : 'true'); - if (button) { - button.setAttribute('aria-label', expanded ? 'Open navigation' : 'Close navigation'); - } - // enable nav dropdown keyboard accessibility - const navDrops = navSections.querySelectorAll('.nav-drop'); - if (isDesktop.matches) { - navDrops.forEach((drop) => { - if (!drop.hasAttribute('tabindex')) { - drop.setAttribute('tabindex', 0); - drop.addEventListener('focus', focusNavSection); +function isBrandSection(section) { + if (!section) return false; + + const text = cleanNavText(section.textContent).toLowerCase(); + const list = getFirstList(section); + + return ( + text.includes(LOGOS.default.token) + || text.includes(LOGOS.multiState.token) + || section.querySelector(LOGOS.default.selector) + || section.querySelector(LOGOS.multiState.selector) + || (!list && text.length > 0) + ); +} + +/** + * Score sections to find the best candidate for the main nav. + * This avoids relying on fixed section positions like: + * section[0] = utility, section[1] = brand, section[2] = nav. + */ +function getMainNavSectionScore(section) { + const list = getFirstList(section); + if (!list) return 0; + + const items = getDirectListItems(list); + const nestedCount = items.filter((li) => getDirectNestedList(li)).length; + const linkCount = section.querySelectorAll('a').length; + + return (items.length * 2) + (nestedCount * 4) + linkCount; +} + +function findBestMainNavSection(sections, excludedSections = []) { + const excluded = new Set(excludedSections.filter(Boolean)); + + return sections + .filter((section) => !excluded.has(section)) + .map((section) => ({ + section, + score: getMainNavSectionScore(section), + })) + .sort((a, b) => b.score - a.score)[0]?.section || null; +} + +/** + * Parse utility section. + * Supports: + * 1. Nested list pattern: + * - English + * - Español + * 2. Flat fallback pattern: + * English + * Español + * 3. Login/Register utility link + */ +function parseUtilitySection(section) { + const result = { + language: null, + login: null, + links: [], + }; + + if (!section) return result; + + const addUtilityItem = (label, href) => { + if (!label) return; + + if (/login\/register/i.test(label)) { + result.login = { + label: 'Log in/Register', + href, + }; + return; + } + + const exists = result.links.some((item) => item.label === label && item.href === href); + if (!exists) { + result.links.push({ label, href }); + } + }; + + const list = getFirstList(section); + + if (list) { + const items = getDirectListItems(list); + + items.forEach((li) => { + const labelAnchor = getDirectAnchor(li); + const label = getElementText(labelAnchor) || getDirectTextWithoutNestedList(li); + const href = getHrefOrFallback(labelAnchor, '#'); + const nestedList = getDirectNestedList(li); + + if (nestedList && /english|language/i.test(label)) { + const options = getDirectListItems(nestedList) + .map((childLi) => { + const childAnchor = getDirectAnchor(childLi); + return { + label: getElementText(childAnchor) || getDirectTextWithoutNestedList(childLi), + href: getHrefOrFallback(childAnchor, '#'), + }; + }) + .filter((item) => item.label); + + result.language = { + label: label || 'English', + options, + }; + return; } + + addUtilityItem(label, href); }); + + // Also scan for standalone anchors outside lists. + // This catches authored links like :profile:Login/Register + // that appear below the language list in the same utility section. + [...section.querySelectorAll('a')] + .filter((anchor) => !anchor.closest('ul, ol')) + .forEach((anchor) => { + const label = getElementText(anchor); + const href = getHrefOrFallback(anchor, '#'); + addUtilityItem(label, href); + }); + + // Fallback for standalone text-only paragraphs outside lists. + [...section.querySelectorAll('p')] + .filter((p) => !p.closest('ul, ol') && !p.querySelector('a')) + .forEach((p) => { + const label = normalizeText(p.textContent); + if (/login\/register/i.test(label) && !result.login) { + result.login = { + label: 'Log in/Register', + href: '#', + }; + } + }); } else { - navDrops.forEach((drop) => { - drop.removeAttribute('tabindex'); - drop.removeEventListener('focus', focusNavSection); - }); + const anchors = [...section.querySelectorAll('a')].map((anchor) => ({ + label: getElementText(anchor), + href: getHrefOrFallback(anchor, '#'), + })).filter((item) => item.label); + + if (anchors.length) { + anchors.forEach((item) => { + if (/login\/register/i.test(item.label)) { + result.login = { + label: 'Log in/Register', + href: item.href, + }; + } else { + result.links.push(item); + } + }); + } else { + const lines = [...section.querySelectorAll('p')] + .map((p) => cleanNavText(p.textContent)) + .filter(Boolean); + + lines.forEach((line) => { + if (/login\/register/i.test(line)) { + result.login = { + label: 'Log in/Register', + href: '#', + }; + } else { + result.links.push({ label: line, href: '#' }); + } + }); + } } - // enable menu collapse on escape keypress - if (!expanded || isDesktop.matches) { - // collapse menu on escape press - window.addEventListener('keydown', closeOnEscape); - // collapse menu on focus lost - nav.addEventListener('focusout', closeOnFocusLost); - } else { - window.removeEventListener('keydown', closeOnEscape); - nav.removeEventListener('focusout', closeOnFocusLost); + if (!result.language && result.links.length) { + const englishIndex = result.links.findIndex((item) => isEnglishLabel(item.label)); + const spanishIndex = result.links.findIndex((item) => isSpanishLabel(item.label)); + + if (englishIndex > -1 && spanishIndex > -1) { + const englishItem = result.links[englishIndex]; + const spanishItem = result.links[spanishIndex]; + + result.language = { + label: englishItem.label || 'English', + options: [ + { + label: spanishItem.label, + href: spanishItem.href, + }, + ], + }; + + result.links = result.links.filter( + + (_, index) => index !== englishIndex && index !== spanishIndex, + + ); + } } + + return result; } /** - * loads and decorates the header, mainly the nav - * @param {Element} block The header block element + * Parse brand section. + * Intended for simple authored content like: + * Stanford + * :logo: */ -export default async function decorate(block) { - // load nav as fragment - const navMeta = getMetadata('nav'); - const navPath = navMeta ? new URL(navMeta.toLowerCase(), window.location).pathname : '/group/nav'; - const fragment = await loadFragment(navPath); +function parseBrandSection(section) { + const result = { + label: 'Blue Shield', + href: HOME_FALLBACK_URL, + logo: LOGOS.default, + hasLogoToken: false, + }; - // decorate nav DOM - block.textContent = ''; - const nav = document.createElement('nav'); - nav.id = 'nav'; - while (fragment.firstElementChild) nav.append(fragment.firstElementChild); - - const classes = ['brand', 'sections']; - classes.forEach((c, i) => { - const section = nav.children[i]; - if (section) section.classList.add(`nav-${c}`); - }); - - const navBrand = nav.querySelector('.nav-brand'); - if (navBrand) { - wrapTextInLinks(navBrand); - const brandLink = navBrand.querySelector('.button'); - if (brandLink) { - brandLink.className = ''; - brandLink.closest('.button-container').className = ''; - } - } - - const navSections = nav.querySelector('.nav-sections'); - if (navSections) { - navSections.querySelectorAll(':scope .default-content-wrapper > ul > li').forEach((navSection) => { - if (navSection.querySelector('ul')) navSection.classList.add('nav-drop'); - navSection.addEventListener('click', () => { - if (isDesktop.matches) { - const expanded = navSection.getAttribute('aria-expanded') === 'true'; - toggleAllNavSections(navSections); - navSection.setAttribute('aria-expanded', expanded ? 'false' : 'true'); + if (!section) return result; + + const link = getFirstLink(section); + const logo = getLogoData(section); + const logoTokens = [LOGOS.default.token, LOGOS.multiState.token]; + + const textCandidates = [...section.querySelectorAll('p, h1, h2, h3, h4, h5, h6')] + .map((el) => cleanNavText(el.textContent)) + .filter((text) => text && !logoTokens.includes(text.toLowerCase())); + + const brandLabel = textCandidates[0]; + + result.label = brandLabel || logo.alt || result.label; + result.href = getHrefOrFallback(link, HOME_FALLBACK_URL); + result.logo = logo; + result.hasLogoToken = logoTokens.some((token) => ( + cleanNavText(section.textContent).toLowerCase().includes(token) + )) || !!section.querySelector(logo.selector); + + return result; +} + +/** + * Parse main nav section from a real nested list. + * Top-level li = top-level nav item + * Nested ul/li = dropdown children + */ +function parseMainNavSection(section) { + const result = { + items: [], + contact: null, + cta: null, + }; + + if (!section) return result; + + const list = getFirstList(section); + if (!list) return result; + + getDirectListItems(list).forEach((li) => { + const anchor = getDirectAnchor(li); + const nestedList = getDirectNestedList(li); + const label = getElementText(anchor) || getDirectTextWithoutNestedList(li); + const href = getHrefOrFallback(anchor, '#'); + + if (!label) return; + + if (isPhone(label)) { + const detailText = nestedList + ? getDirectListItems(nestedList) + .map((childLi) => cleanNavText(childLi.textContent)) + .filter(Boolean) + .join(' ') + : ''; + + result.contact = { + label, + href: toTelHref(label), + detail: detailText, + }; + return; + } + + if (/^enroll now$/i.test(label)) { + result.cta = { + label, + href, + }; + return; + } + + const item = { + label, + href, + children: [], + }; + + if (nestedList) { + item.children = getDirectListItems(nestedList) + .map((childLi) => getDropdownChildData(childLi)) + .filter((child) => child.label); + } + + result.items.push(item); + }); + + return result; +} + +function parseNavFragment(fragment) { + const sections = getTopLevelSections(fragment); + + /* + * Flexible nav document parsing: + * - New docs can include utility + brand + nav sections. + * - Older/current docs can omit the utility section. + * - Very old/simple docs can omit the brand section and still render + * the default Blue Shield logo. + */ + const utilitySection = sections.find((section) => isUtilitySection(section)) || null; + const brandSection = sections.find((section) => ( + section !== utilitySection && isBrandSection(section) + )) || null; + const mainSection = findBestMainNavSection(sections, [utilitySection, brandSection]); + + const utility = utilitySection + ? parseUtilitySection(utilitySection) + : { + language: null, + login: null, + links: [], + }; + + const brand = parseBrandSection(brandSection); + const main = parseMainNavSection(mainSection); + + return { + utility, + brand, + navItems: main.items, + contact: main.contact, + cta: main.cta, + }; +} + +function applyUtilityMetadataToggles(data) { + const showLanguage = getBooleanMetadata('nav-show-language'); + const showLogin = getBooleanMetadata('nav-show-login'); + + const nextData = { + ...data, + utility: { + ...(data.utility || {}), + links: [...(data.utility?.links || [])], + }, + utilitySettings: { + showLanguage, + showLogin, + }, + }; + + if (showLanguage === false) { + nextData.utility.language = null; + nextData.utility.links = nextData.utility.links.filter( + (item) => !isEnglishLabel(item.label) && !isSpanishLabel(item.label), + ); + } + + if (showLogin === false) { + nextData.utility.login = null; + } + + return nextData; +} + +function shouldForceStickyShell(data) { + const showLanguage = data.utilitySettings?.showLanguage; + const showLogin = data.utilitySettings?.showLogin; + + return showLanguage === false && showLogin === false; +} + +function buildLanguageControl(languageData) { + if (!languageData?.label) return null; + + const wrapper = createTag('div', { class: 'nav-new-language' }); + + const hasOptions = Array.isArray(languageData.options) && languageData.options.length > 0; + + if (!hasOptions) { + wrapper.append( + createTag('span', { class: 'nav-new-language-label' }, languageData.label), + ); + return wrapper; + } + + const buttonId = `nav-new-language-${slugify(languageData.label)}`; + const panelId = `${buttonId}-panel`; + + const button = createTag( + 'button', + { + class: 'nav-new-language-toggle', + type: 'button', + 'aria-expanded': 'false', + 'aria-controls': panelId, + id: buttonId, + }, + `${languageData.label}`, + ); + + const panel = createTag( + 'div', + { + class: 'nav-new-language-panel', + id: panelId, + hidden: '', + }, + ); + + const list = createTag('ul', { class: 'nav-new-language-list' }); + + languageData.options.forEach((option) => { + const li = createTag('li'); + const link = createTag( + 'a', + { + href: option.href || '#', + class: 'nav-new-language-link', + }, + option.label, + ); + li.append(link); + list.append(li); + }); + + panel.append(list); + wrapper.append(button, panel); + + return wrapper; +} + +function buildUtilityRow(data) { + const row = createTag('div', { class: 'nav-new-utility' }); + const left = createTag('div', { class: 'nav-new-utility-left' }); + const right = createTag('div', { class: 'nav-new-utility-right' }); + + const utilityData = data.utility || { + language: null, + login: null, + links: [], + }; + + const languageControl = buildLanguageControl(utilityData.language); + if (languageControl) left.append(languageControl); + + utilityData.links.forEach((item) => { + const link = createTag( + 'a', + { + href: item.href || '#', + class: 'nav-new-utility-link', + }, + item.label, + ); + left.append(link); + }); + + if (utilityData.login) { + const login = createTag( + 'a', + { + href: utilityData.login.href || '#', + class: 'nav-new-login', + }, + [ + '', + 'Log in/Register', + ].join(''), + ); + right.append(login); + } + + if (!left.childElementCount && !right.childElementCount) { + return null; + } + + row.append(left, right); + return row; +} + +function buildBrand(data) { + const logoData = data.brand.logo || LOGOS.default; + const isMultiStateLogo = logoData.token === LOGOS.multiState.token; + + const brand = createTag('div', { + class: `nav-new-brand${isMultiStateLogo ? ' nav-new-brand-multi-state' : ''}`, + }); + + const link = createTag( + 'a', + { + href: data.brand.href || HOME_FALLBACK_URL, + class: 'nav-new-brand-link', + 'aria-label': data.brand.label || 'Home', + }, + ); + + const logo = createTag('img', { + src: logoData.src || LOGOS.default.src, + alt: logoData.alt || LOGOS.default.alt, + width: logoData.width || LOGOS.default.width, + class: `nav-new-brand-image${isMultiStateLogo ? ' nav-new-brand-image-multi-state' : ''}`, + }); + + link.append(logo); + brand.append(link); + + return brand; +} + +function buildDropdownItem(item) { + const li = createTag('li', { + class: 'nav-new-item has-dropdown', + 'data-nav-item': slugify(item.label), + }); + + const buttonId = `nav-item-${slugify(item.label)}`; + const panelId = `${buttonId}-panel`; + + const button = createTag( + 'button', + { + class: 'nav-new-link nav-new-dropdown-toggle', + type: 'button', + 'aria-expanded': 'false', + 'aria-controls': panelId, + id: buttonId, + }, + `${item.label}`, + ); + + const panel = createTag( + 'div', + { + class: 'nav-new-dropdown', + id: panelId, + hidden: '', + }, + ); + + const list = createTag('ul', { class: 'nav-new-dropdown-list' }); + + item.children.forEach((child, index) => { + const childLi = createTag('li', { class: 'nav-new-dropdown-item' }); + const childLink = createTag( + 'a', + { + href: child.href || '#', + class: `nav-new-dropdown-link${index === 0 ? ' is-featured' : ''}`, + }, + ); + + childLink.append(createTag('span', { class: 'nav-new-dropdown-link-label' }, child.label)); + + if (child.detail) { + childLink.append( + createTag('span', { class: 'nav-new-dropdown-link-detail' }, child.detail), + ); + } + + childLi.append(childLink); + list.append(childLi); + }); + + panel.append(list); + li.append(button, panel); + + return li; +} + +function buildSimpleNavItem(item) { + const li = createTag('li', { + class: 'nav-new-item', + 'data-nav-item': slugify(item.label), + }); + + const link = createTag( + 'a', + { + href: item.href || '#', + class: 'nav-new-link', + }, + item.label, + ); + + li.append(link); + return li; +} + +function buildPrimaryNav(data) { + const navWrap = createTag('div', { class: 'nav-new-main' }); + + const nav = createTag('nav', { + class: 'nav-new-primary', + 'aria-label': 'Primary navigation', + }); + + const list = createTag('ul', { class: 'nav-new-list' }); + + data.navItems.forEach((item) => { + if (item.children?.length) { + list.append(buildDropdownItem(item)); + } else { + list.append(buildSimpleNavItem(item)); + } + }); + + nav.append(list); + + const actions = createTag('div', { class: 'nav-new-actions' }); + + if (data.contact) { + const contact = createTag( + 'a', + { + href: data.contact.href, + class: 'nav-new-contact', + }, + ` + ${data.contact.label} + ${data.contact.detail + ? `${data.contact.detail}` + : ''} + `, + ); + actions.append(contact); + } + + if (data.cta) { + const cta = createTag( + 'a', + { + href: data.cta.href || '#', + class: 'nav-new-cta', + }, + data.cta.label, + ); + actions.append(cta); + } + + navWrap.append(nav, actions); + return navWrap; +} + +function buildHamburger() { + return createTag( + 'div', + { class: 'nav-new-hamburger' }, + ` + + `, + ); +} + +function setCurrentState(block) { + const pagePath = getPagePath(); + + block.querySelectorAll('.nav-new-primary .nav-new-item').forEach((item) => { + const directLink = item.querySelector(':scope > a.nav-new-link'); + + if (directLink && pathsMatch(directLink.getAttribute('href'), pagePath)) { + item.classList.add('is-current'); + } + + const childLinks = item.querySelectorAll('.nav-new-dropdown-link'); + const hasCurrentChild = [...childLinks].some((link) => ( + pathsMatch(link.getAttribute('href'), pagePath) + )); + + if (hasCurrentChild) { + item.classList.add('is-active'); + } + }); +} + +function closeAllDropdowns(block) { + block.querySelectorAll('.nav-new-item.has-dropdown').forEach((item) => { + item.classList.remove('is-open'); + + const button = item.querySelector('.nav-new-dropdown-toggle'); + const panel = item.querySelector('.nav-new-dropdown'); + + if (button) button.setAttribute('aria-expanded', 'false'); + if (panel) panel.hidden = true; + }); + + const language = block.querySelector('.nav-new-language'); + if (language) { + language.classList.remove('is-open'); + const button = language.querySelector('.nav-new-language-toggle'); + const panel = language.querySelector('.nav-new-language-panel'); + if (button) button.setAttribute('aria-expanded', 'false'); + if (panel) panel.hidden = true; + } +} + +function toggleDropdown(item, block, forceOpen = null) { + const isOpen = item.classList.contains('is-open'); + const shouldOpen = forceOpen !== null ? forceOpen : !isOpen; + + closeAllDropdowns(block); + + if (shouldOpen) { + item.classList.add('is-open'); + const button = item.querySelector('.nav-new-dropdown-toggle'); + const panel = item.querySelector('.nav-new-dropdown'); + if (button) button.setAttribute('aria-expanded', 'true'); + if (panel) panel.hidden = false; + } +} + +function toggleLanguage(block, forceOpen = null) { + const language = block.querySelector('.nav-new-language'); + if (!language) return; + + const button = language.querySelector('.nav-new-language-toggle'); + const panel = language.querySelector('.nav-new-language-panel'); + + const isOpen = language.classList.contains('is-open'); + const shouldOpen = forceOpen !== null ? forceOpen : !isOpen; + + closeAllDropdowns(block); + + if (shouldOpen) { + language.classList.add('is-open'); + if (button) button.setAttribute('aria-expanded', 'true'); + if (panel) panel.hidden = false; + } +} + +function closeMobileMenu(block) { + block.classList.remove('nav-new-mobile-open'); + + const button = block.querySelector('.nav-new-hamburger-button'); + if (button) { + button.setAttribute('aria-expanded', 'false'); + button.setAttribute('aria-label', 'Open navigation'); + } + + document.body.style.overflowY = ''; + closeAllDropdowns(block); +} + +function openMobileMenu(block) { + block.classList.add('nav-new-mobile-open'); + + const button = block.querySelector('.nav-new-hamburger-button'); + if (button) { + button.setAttribute('aria-expanded', 'true'); + button.setAttribute('aria-label', 'Close navigation'); + } + + document.body.style.overflowY = 'hidden'; +} + +function toggleMobileMenu(block) { + if (block.classList.contains('nav-new-mobile-open')) { + closeMobileMenu(block); + } else { + openMobileMenu(block); + } +} + +/* ========================= + STICKY NAV SHELL + Makes only .nav-new-shell sticky after 36px scroll + Works on desktop and mobile +========================= */ +function bindStickyShell(block, forceSticky = false) { + const wrapper = block.querySelector('.nav-new-wrapper'); + const shell = block.querySelector('.nav-new-shell'); + + if (!wrapper || !shell) return; + + if (forceSticky) { + shell.classList.add('is-sticky'); + wrapper.classList.add('has-sticky-shell'); + return; + } + + const toggleSticky = () => { + if (window.scrollY > 36) { + shell.classList.add('is-sticky'); + wrapper.classList.add('has-sticky-shell'); + } else { + shell.classList.remove('is-sticky'); + wrapper.classList.remove('has-sticky-shell'); + } + }; + + window.addEventListener('scroll', toggleSticky, { passive: true }); + window.addEventListener('resize', toggleSticky); + toggleSticky(); +} + +function bindEvents(block) { + block.querySelectorAll('.nav-new-item.has-dropdown').forEach((item) => { + const button = item.querySelector('.nav-new-dropdown-toggle'); + if (!button) return; + + button.addEventListener('click', (e) => { + e.preventDefault(); + + if (DESKTOP.matches) { + toggleDropdown(item, block); + } else { + const isOpen = item.classList.contains('is-open'); + if (isOpen) { + item.classList.remove('is-open'); + button.setAttribute('aria-expanded', 'false'); + const panel = item.querySelector('.nav-new-dropdown'); + if (panel) panel.hidden = true; + } else { + item.classList.add('is-open'); + button.setAttribute('aria-expanded', 'true'); + const panel = item.querySelector('.nav-new-dropdown'); + if (panel) panel.hidden = false; } - }); + } + }); + }); + + const languageButton = block.querySelector('.nav-new-language-toggle'); + if (languageButton) { + languageButton.addEventListener('click', (e) => { + e.preventDefault(); + toggleLanguage(block); }); } - // hamburger for mobile - const hamburger = document.createElement('div'); - hamburger.classList.add('nav-hamburger'); - if (navSections.children.length) { - hamburger.innerHTML = ``; + const hamburgerButton = block.querySelector('.nav-new-hamburger-button'); + if (hamburgerButton) { + hamburgerButton.addEventListener('click', () => toggleMobileMenu(block)); } - hamburger.addEventListener('click', () => toggleMenu(nav, navSections)); - nav.append(hamburger); - nav.setAttribute('aria-expanded', 'false'); - // prevent mobile nav behavior on window resize - toggleMenu(nav, navSections, isDesktop.matches); - isDesktop.addEventListener('change', () => toggleMenu(nav, navSections, isDesktop.matches)); - const navWrapper = document.createElement('div'); - navWrapper.className = 'nav-wrapper'; - navWrapper.append(nav); - block.append(navWrapper); + document.addEventListener('click', (e) => { + if (!block.contains(e.target)) { + closeAllDropdowns(block); + if (!DESKTOP.matches) closeMobileMenu(block); + } + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + closeAllDropdowns(block); + if (!DESKTOP.matches) closeMobileMenu(block); + } + }); - // ⬇️ Make entire submenu
  • rows clickable (runs after header DOM is ready) - enableRowLinks({ - root: block, - rows: '.nav-sections ul > li > ul > li', + DESKTOP.addEventListener('change', (e) => { + if (e.matches) { + document.body.style.overflowY = ''; + closeMobileMenu(block); + } else { + closeAllDropdowns(block); + } }); } + +function buildHeader(block, data) { + const wrapper = createTag('div', { class: 'nav-new-wrapper' }); + + const utility = buildUtilityRow(data); + + const shell = createTag('div', { class: 'nav-new-shell' }); + const brand = buildBrand(data); + const main = buildPrimaryNav(data); + const hamburger = buildHamburger(); + + shell.append(brand, main, hamburger); + + if (utility) { + wrapper.append(utility); + } + + wrapper.append(shell); + + block.textContent = ''; + block.append(wrapper); +} + +export default async function decorate(block) { + const navPath = getNavPath(); + const fragment = await loadFragment(navPath); + + if (!fragment) { + block.textContent = ''; + return; + } + + const parsedData = parseNavFragment(fragment); + const data = applyUtilityMetadataToggles(parsedData); + + buildHeader(block, data); + setCurrentState(block); + bindEvents(block); + + bindStickyShell(block, shouldForceStickyShell(data)); + + if (!DESKTOP.matches) { + closeMobileMenu(block); + } else { + closeAllDropdowns(block); + } +}