From 8bbc6c15ca4c2b89f7d0ce61f26d3a01efa6cf01 Mon Sep 17 00:00:00 2001 From: Roberto Hernandez Date: Tue, 27 Aug 2024 12:26:48 -0600 Subject: [PATCH 1/6] feat(emulsif-305): add header component --- images/icons/close.svg | 4 + images/icons/search.svg | 3 + src/components/header/_header.scss | 91 ++++++++++++++++++++++ src/components/header/header.component.yml | 21 +++++ src/components/header/header.js | 6 ++ src/components/header/header.stories.js | 36 +++++++++ src/components/header/header.twig | 56 +++++++++++++ 7 files changed, 217 insertions(+) create mode 100644 images/icons/close.svg create mode 100644 images/icons/search.svg create mode 100644 src/components/header/_header.scss create mode 100644 src/components/header/header.component.yml create mode 100644 src/components/header/header.js create mode 100644 src/components/header/header.stories.js create mode 100644 src/components/header/header.twig diff --git a/images/icons/close.svg b/images/icons/close.svg new file mode 100644 index 00000000..ba895733 --- /dev/null +++ b/images/icons/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/icons/search.svg b/images/icons/search.svg new file mode 100644 index 00000000..d26b4986 --- /dev/null +++ b/images/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss new file mode 100644 index 00000000..c9359eae --- /dev/null +++ b/src/components/header/_header.scss @@ -0,0 +1,91 @@ +.header { + display: flex; + width: 100%; +} + +/* Search form */ +.header__search-form { + display: flex; + min-width: 16rem; + height: max-content; + align-items: center; + background-color: var(--color-primary-darker); + padding: var(--spacing-xl) 1.5rem var(--spacing-xl) var(--spacing-xl); +} + +.header__search-input { + border: none; + min-height: 2.25rem; + color: var(--color-white); + background-color: transparent; + margin-right: var(--spacing-lg); + border-bottom: 1px solid var(--color-white); +} + +.header__search-input::placeholder { + opacity: 1; + color: var(--color-white); +} + +.header__search-input::-ms-input-placeholder { + color: var(--color-white); +} + +.header__form-actions button { + cursor: pointer; + border: none; + width: 2.25rem; + height: 2.25rem; + background: transparent; + padding: var(--spacing-md); +} + +.header__form-actions svg { + padding: 0; + width: 100%; + height: 100%; +} + +/* Menu */ +.header__menu ul { + display: flex; + list-style: none; +} + +.header__menu-item { + position: relative; +} + +.header__menu-item a { + font-weight: 700; + text-decoration: none; + text-transform: uppercase; + color: var(--color-primary-dark); +} + +.header__submenu { + position: absolute; + min-width: 18.75rem; + padding: var(--spacing-lg); + border-radius: var(--size-sm); + background-color: var(--color-white); + box-shadow: rgba(17, 12, 46, 0.15) 0px 48px 100px 0px; +} + +.header__submenu-item a { + font-weight: 400; + text-transform: none; + + &:hover, + &:focus { + text-decoration: underline; + } +} + +.header__toggle { + cursor: pointer; + width: var(--size-lg); + height: var(--size-lg); + margin-left: var(--spacing-md); + fill: var(--color-primary-dark); +} diff --git a/src/components/header/header.component.yml b/src/components/header/header.component.yml new file mode 100644 index 00000000..9375af03 --- /dev/null +++ b/src/components/header/header.component.yml @@ -0,0 +1,21 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json + +name: Accordion +group: Components +status: stable +props: + type: object + required: + - accordion__item__heading + - accordion__item__content + properties: + accordion__item__heading: + type: string + title: Heading + description: The heading of the accordion item. + data: Header label text + accordion__item__content: + type: string + title: Accordion item content + description: The content of the accordion item. + data: 'Enim eget nec sit scelerisque lacus. Porttitor senectus vulputate mattis tortor odio vitae. Dui et, ut ipsum aliquet sit tempor. Amet consectetur purus justo feugiat mattis sit ultricies odio. Pellentesque pellentesque sit sed porttitor duis interdum. Bibendum nisl, eu, ornare non. Enim consequat in quisque vestibulum facilisi odio. Elementum elit est, vitae feugiat enim odio cursus. Enim cum dictum gravida amet id eget. Ut ac velit sed nulla leo id. Ante duis pellentesque aliquam massa amet, neque cum. Vitae mi purus placerat nibh purus faucibus imperdiet quisque diam. Elementum urna feugiat rhoncus purus. Consectetur neque auctor eite commodo consequat.' \ No newline at end of file diff --git a/src/components/header/header.js b/src/components/header/header.js new file mode 100644 index 00000000..49332347 --- /dev/null +++ b/src/components/header/header.js @@ -0,0 +1,6 @@ +Drupal.behaviors.siteHeader = { + attach(context) { + const menuItems = context.querySelectorAll('.header__menu-link'); + console.log('menuItems', menuItems); + }, +}; diff --git a/src/components/header/header.stories.js b/src/components/header/header.stories.js new file mode 100644 index 00000000..4d879208 --- /dev/null +++ b/src/components/header/header.stories.js @@ -0,0 +1,36 @@ +import template from './header.twig'; +import { props } from './header.component.yml'; +import figma from '../../../.storybook/configma.json'; +import './header'; + +const headerData = { + header__branding: 'Branding', + header__menu: [ + { label: 'Home', url: '/' }, + { + label: 'About', + url: '/about', + submenu: [{ label: 'Team', url: '/about/team' }], + }, + { + label: 'Services', + url: '/services', + submenu: [{ label: 'Consulting', url: '/services/consulting' }], + }, + ], + header__search: { placeholder: 'Search...', has_focus: false }, +}; + +export default { + title: 'Components/Header', + decorators: [(story) => `${story()}`], +}; + +export const Header = () => template(headerData); + +Header.parameters = { + design: { + type: 'figma', + url: figma.url + figma.header, + }, +}; diff --git a/src/components/header/header.twig b/src/components/header/header.twig new file mode 100644 index 00000000..1c684b11 --- /dev/null +++ b/src/components/header/header.twig @@ -0,0 +1,56 @@ +{% set header__base_class = 'header' %} +{% set header__base_class = 'header' %} +{% set header__search_placeholder = header__search_placeholder|default('Search placeholder') %} + +
+
+ {{ header__branding|raw }} +
+ +
+ +
+ + +
+
+
From 9656d105cf6e50779f4debefb2bc2e2603f06b9c Mon Sep 17 00:00:00 2001 From: robherba Date: Mon, 18 May 2026 08:23:03 -0600 Subject: [PATCH 2/6] chore(EMULSIF-305): update header styles, add logo, and improve component structure --- {images => assets}/icons/close.svg | 4 +- {images => assets}/icons/search.svg | 2 +- assets/images/logo-text.svg | 7 + src/components/header/_header.scss | 91 ----- src/components/header/header.component.yml | 59 ++- src/components/header/header.js | 175 ++++++++- src/components/header/header.scss | 424 +++++++++++++++++++++ src/components/header/header.stories.js | 50 ++- src/components/header/header.twig | 131 ++++--- src/components/logo/logo.twig | 4 +- 10 files changed, 764 insertions(+), 183 deletions(-) rename {images => assets}/icons/close.svg (78%) rename {images => assets}/icons/search.svg (98%) create mode 100644 assets/images/logo-text.svg delete mode 100644 src/components/header/_header.scss create mode 100644 src/components/header/header.scss diff --git a/images/icons/close.svg b/assets/icons/close.svg similarity index 78% rename from images/icons/close.svg rename to assets/icons/close.svg index ba895733..26a0e5ee 100644 --- a/images/icons/close.svg +++ b/assets/icons/close.svg @@ -1,4 +1,4 @@ - - + + diff --git a/images/icons/search.svg b/assets/icons/search.svg similarity index 98% rename from images/icons/search.svg rename to assets/icons/search.svg index d26b4986..4664f857 100644 --- a/images/icons/search.svg +++ b/assets/icons/search.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/images/logo-text.svg b/assets/images/logo-text.svg new file mode 100644 index 00000000..dd78e019 --- /dev/null +++ b/assets/images/logo-text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/components/header/_header.scss b/src/components/header/_header.scss deleted file mode 100644 index c9359eae..00000000 --- a/src/components/header/_header.scss +++ /dev/null @@ -1,91 +0,0 @@ -.header { - display: flex; - width: 100%; -} - -/* Search form */ -.header__search-form { - display: flex; - min-width: 16rem; - height: max-content; - align-items: center; - background-color: var(--color-primary-darker); - padding: var(--spacing-xl) 1.5rem var(--spacing-xl) var(--spacing-xl); -} - -.header__search-input { - border: none; - min-height: 2.25rem; - color: var(--color-white); - background-color: transparent; - margin-right: var(--spacing-lg); - border-bottom: 1px solid var(--color-white); -} - -.header__search-input::placeholder { - opacity: 1; - color: var(--color-white); -} - -.header__search-input::-ms-input-placeholder { - color: var(--color-white); -} - -.header__form-actions button { - cursor: pointer; - border: none; - width: 2.25rem; - height: 2.25rem; - background: transparent; - padding: var(--spacing-md); -} - -.header__form-actions svg { - padding: 0; - width: 100%; - height: 100%; -} - -/* Menu */ -.header__menu ul { - display: flex; - list-style: none; -} - -.header__menu-item { - position: relative; -} - -.header__menu-item a { - font-weight: 700; - text-decoration: none; - text-transform: uppercase; - color: var(--color-primary-dark); -} - -.header__submenu { - position: absolute; - min-width: 18.75rem; - padding: var(--spacing-lg); - border-radius: var(--size-sm); - background-color: var(--color-white); - box-shadow: rgba(17, 12, 46, 0.15) 0px 48px 100px 0px; -} - -.header__submenu-item a { - font-weight: 400; - text-transform: none; - - &:hover, - &:focus { - text-decoration: underline; - } -} - -.header__toggle { - cursor: pointer; - width: var(--size-lg); - height: var(--size-lg); - margin-left: var(--spacing-md); - fill: var(--color-primary-dark); -} diff --git a/src/components/header/header.component.yml b/src/components/header/header.component.yml index 9375af03..928197af 100644 --- a/src/components/header/header.component.yml +++ b/src/components/header/header.component.yml @@ -1,21 +1,54 @@ $schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json -name: Accordion +name: Header group: Components status: stable props: type: object - required: - - accordion__item__heading - - accordion__item__content properties: - accordion__item__heading: + header__menu: + type: array + title: Menu Items + description: The list of menu items to display in the header. + items: + type: object + properties: + label: + type: string + url: + type: string + submenu: + type: array + data: + - label: Academics + submenu: + - label: Undergraduate + url: # + - label: Postgraduate + url: # + - label: Admissions + url: /admissions + submenu: + - label: International + url: # + - label: Domestic + url: # + - label: Research + url: # + - label: About + url: # + header__search_placeholder: type: string - title: Heading - description: The heading of the accordion item. - data: Header label text - accordion__item__content: - type: string - title: Accordion item content - description: The content of the accordion item. - data: 'Enim eget nec sit scelerisque lacus. Porttitor senectus vulputate mattis tortor odio vitae. Dui et, ut ipsum aliquet sit tempor. Amet consectetur purus justo feugiat mattis sit ultricies odio. Pellentesque pellentesque sit sed porttitor duis interdum. Bibendum nisl, eu, ornare non. Enim consequat in quisque vestibulum facilisi odio. Elementum elit est, vitae feugiat enim odio cursus. Enim cum dictum gravida amet id eget. Ut ac velit sed nulla leo id. Ante duis pellentesque aliquam massa amet, neque cum. Vitae mi purus placerat nibh purus faucibus imperdiet quisque diam. Elementum urna feugiat rhoncus purus. Consectetur neque auctor eite commodo consequat.' \ No newline at end of file + title: Search Placeholder + description: The placeholder text for the search input. + data: Search placeholder +slots: + header__branding: + title: Branding + description: Branding content (logo, site name, etc). + header__menu: + title: Menu + description: Navigation menu content. + header__search: + title: Search + description: Search form content. diff --git a/src/components/header/header.js b/src/components/header/header.js index 49332347..4285ff85 100644 --- a/src/components/header/header.js +++ b/src/components/header/header.js @@ -1,6 +1,177 @@ Drupal.behaviors.siteHeader = { attach(context) { - const menuItems = context.querySelectorAll('.header__menu-link'); - console.log('menuItems', menuItems); + // Helper to close open submenus + const closeSubmenus = () => { + document.querySelectorAll('.header__menu-item.open').forEach((item) => { + item.classList.remove('open'); + item.querySelector('.header__menu-item__toggle')?.setAttribute('aria-expanded', 'false'); + }); + }; + + // Helper to close search form + const closeSearch = () => { + document.querySelectorAll('.header.search-active').forEach((header) => { + header.classList.remove('search-active'); + header.querySelectorAll('.header__search-form__toggle').forEach((t) => t.setAttribute('aria-expanded', 'false')); + header.querySelector('.header__search-form__wrapper')?.setAttribute('aria-hidden', 'true'); + }); + }; + + // Helper to close mobile menu + const closeMobileMenu = () => { + document.querySelectorAll('.header.menu-active').forEach((header) => { + header.classList.remove('menu-active'); + header.querySelectorAll('.header__mobile-menu__toggle').forEach((t) => t.setAttribute('aria-expanded', 'false')); + }); + }; + + // Helper to close everything + const closeAll = () => { + closeSubmenus(); + closeSearch(); + closeMobileMenu(); + }; + + // Helper to handle mobile-only links in submenus + const handleMobileLinks = () => { + const isMobile = window.innerWidth < 1040; + document.querySelectorAll('.header__menu-item').forEach((item) => { + const submenu = item.querySelector('.header__menu-item__submenu'); + if (!submenu) return; + + const mobileLink = submenu.querySelector('.header__submenu-item--mobile-only'); + + if (isMobile) { + if (!mobileLink) { + const link = item.querySelector('.header__menu-item__link'); + if (link && link.getAttribute('href')) { + const li = document.createElement('li'); + li.className = 'header__submenu-item header__submenu-item--mobile-only'; + li.innerHTML = `${link.textContent.trim()}`; + submenu.insertBefore(li, submenu.firstChild); + } + } + } else if (mobileLink) { + mobileLink.remove(); + } + }); + }; + + // Helper to handle search form location + const handleSearchLocation = () => { + const isMobile = window.innerWidth < 1040; + const searchForm = document.querySelector('.header__search-form__wrapper'); + const desktopContainer = document.querySelector('.header__search--desktop'); + const mobileContainer = document.querySelector('.header__search--mobile'); + + if (!searchForm || !mobileContainer || !desktopContainer) return; + + if (isMobile) { + if (searchForm.parentElement !== mobileContainer) { + mobileContainer.appendChild(searchForm); + } + } else { + if (searchForm.parentElement !== desktopContainer) { + desktopContainer.appendChild(searchForm); + } + } + }; + + // Initial calls + handleMobileLinks(); + handleSearchLocation(); + + // Menu Toggles + context.querySelectorAll('.header__menu-item').forEach((item) => { + const toggle = item.querySelector('.header__menu-item__toggle'); + if (!toggle) return; + + if (item.dataset.jsProcessed) return; + item.dataset.jsProcessed = true; + + item.addEventListener('click', (e) => { + // Desktop behavior: only trigger if button was clicked + if (window.innerWidth >= 1040 && !e.target.closest('.header__menu-item__toggle')) return; + + // Mobile behavior: clicking the whole item toggles + e.stopPropagation(); + + // Prevent default for top-level link on mobile if it has a submenu + if (window.innerWidth < 1040 && e.target.closest('.header__menu-item__link')) { + e.preventDefault(); + } + + const isOpening = !item.classList.contains('open'); + + closeSubmenus(); + closeSearch(); + + if (isOpening) { + item.classList.add('open'); + toggle.setAttribute('aria-expanded', 'true'); + } + }); + }); + + // Search Toggles + context.querySelectorAll('.header__search-form__toggle').forEach((toggle) => { + if (toggle.dataset.jsProcessed) return; + toggle.dataset.jsProcessed = true; + + toggle.addEventListener('click', (e) => { + e.stopPropagation(); + const header = toggle.closest('.header'); + const isOpening = !header.classList.contains('search-active'); + + closeAll(); + + if (isOpening) { + header.classList.add('search-active'); + toggle.setAttribute('aria-expanded', 'true'); + header.querySelector('.header__search-form__wrapper')?.setAttribute('aria-hidden', 'false'); + header.querySelector('input[type="search"]')?.focus(); + } + }); + }); + + // Mobile Menu Toggles + context.querySelectorAll('.header__mobile-menu__toggle').forEach((toggle) => { + if (toggle.dataset.jsProcessed) return; + toggle.dataset.jsProcessed = true; + + toggle.addEventListener('click', (e) => { + e.stopPropagation(); + const header = toggle.closest('.header'); + const isOpening = !header.classList.contains('menu-active'); + + closeAll(); + + if (isOpening) { + header.classList.add('menu-active'); + toggle.setAttribute('aria-expanded', 'true'); + } + }); + }); + + // Global listeners. + if (!window.siteHeaderInitialized) { + window.siteHeaderInitialized = true; + + const mediaQuery = window.matchMedia('(min-width: 1040px)'); + const handleBreakpointChange = () => { + closeAll(); + handleMobileLinks(); + handleSearchLocation(); + }; + + mediaQuery.addEventListener('change', handleBreakpointChange); + + document.addEventListener('click', (e) => { + if (!e.target.closest('.header__menu-item')) closeSubmenus(); + }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeAll(); + }); + } }, }; diff --git a/src/components/header/header.scss b/src/components/header/header.scss new file mode 100644 index 00000000..84dba01f --- /dev/null +++ b/src/components/header/header.scss @@ -0,0 +1,424 @@ +@use '../../foundation/utility/rem-calc' as *; +@use '../../foundation/breakpoints/breakpoints' as *; + +.header { + display: flex; + width: 100%; + min-height: 5rem; + padding-inline: var(--spacing-md); + justify-content: space-between; + align-items: center; + position: relative; + + @include breakpoint('medium') { + justify-content: flex-start; + align-items: stretch; + padding-inline: 0; + } +} + +/* Branding */ +.header__branding { + display: flex; + align-items: center; + + .logo { + width: auto; + height: 2.5rem; + + @include breakpoint('medium') { + height: 3.5rem; + } + } +} + +/* Menu */ +.header__menu { + display: none; + + .menu-active & { + display: block; + position: absolute; + top: 100%; + left: 0; + width: 100%; + background-color: var(--color-white); + z-index: 10; + padding: var(--spacing-lg); + box-shadow: rgba(17, 12, 46, 0.15) 0px 48px 100px 0px; + } + + @include breakpoint('medium') { + flex: 1; + display: flex; + align-items: end; + flex-direction: column; + justify-content: center; + padding-inline: var(--spacing-xl); + position: static; + background-color: transparent; + width: auto; + box-shadow: none; + z-index: auto; + } +} + +.header__menu > ul { + display: flex; + list-style: none; + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-md); + + @include breakpoint('medium') { + flex-direction: row; + align-items: end; + } +} + +.header__menu-item { + display: flex; + position: relative; + padding: var(--spacing-md) var(--spacing-lg); + gap: var(--spacing-sm); + + cursor: pointer; + + &.open { + flex-wrap: wrap; + } + + @include breakpoint('medium') { + cursor: default; + } +} + +.header__menu-item__link { + font-weight: 700; + text-decoration: none; + text-transform: uppercase; + color: var(--color-primary-dark); + + &:focus, + &:hover { + text-decoration: underline; + } +} + +.header__menu-item__submenu { + display: none; + + .header__menu-item.open & { + width: 100%; + display: flex; + flex-direction: column; + padding-left: var(--spacing-lg); + border-left: 2px solid var(--color-primary-light); + + @include breakpoint('medium') { + position: absolute; + top: 100%; + left: 0; + min-width: 18.75rem; + padding: var(--spacing-lg); + border-radius: var(--size-sm); + background-color: var(--color-white); + box-shadow: rgba(17, 12, 46, 0.15) 0px 48px 100px 0px; + border: none; + z-index: 2; + } + } +} + +.header__submenu-item--mobile-only { + @include breakpoint('medium') { + display: none; + } +} + +.header__submenu-item__link { + text-transform: none; + color: var(--color-text-body); + opacity: 0.85; + padding-block: var(--spacing-md); + display: block; + + &:hover, + &:focus { + text-decoration: underline; + opacity: 1; + } +} + +.header__menu-item__toggle { + cursor: pointer; + width: var(--size-lg); + height: var(--size-lg); + display: flex; + align-items: center; + justify-content: center; + background-color: transparent; + border: none; + padding: 0; + transition: transform 0.3s ease; + + svg { + width: 100%; + height: 100%; + fill: var(--color-primary-dark); + transition: transform 0.2s ease; + transform: rotate(-90deg); + + @include breakpoint('medium') { + transform: rotate(0); + } + } + + &[aria-expanded="true"] { + svg { + transform: rotate(0); + + @include breakpoint('medium') { + transform: rotate(180deg); + } + } + } +} + +/* Mobile Toggle */ +.header__mobile-menu__toggle { + cursor: pointer; + display: flex; + align-items: center; + width: var(--size-xl); + height: var(--size-xl); + background-color: transparent; + fill: var(--color-primary-dark); + border: none; + user-select: none; + padding: 0; + + .header__mobile-menu__hamburger { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + width: var(--size-xl); + height: var(--size-xl); + + span { + display: block; + width: 100%; + height: 3px; + background-color: var(--color-primary-dark); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + } + + &[aria-expanded="true"] { + .header__mobile-menu__hamburger { + span:nth-child(1) { + transform: translateY(11px) rotate(45deg); + } + span:nth-child(2) { + opacity: 0; + } + span:nth-child(3) { + transform: translateY(-11px) rotate(-45deg); + } + } + } + + @include breakpoint('medium') { + display: none; + } +} + +.header__search--mobile { + padding-block: var(--spacing-xl); + padding-inline: var(--spacing-md); + + .header__search-form__wrapper { + position: static; + max-height: none; + visibility: visible; + overflow: visible; + transition: none; + } + + .header__search-form { + background-color: transparent; + padding: 0 var(--spacing-lg); + } + + .header__search-form__input { + color: var(--color-primary-dark); + border-bottom-color: var(--color-primary-dark); + + &::placeholder { + color: var(--color-primary-dark); + opacity: 0.7; + } + + &::-ms-input-placeholder { + color: var(--color-primary-dark); + } + } + + .header__search-form__actions button svg { + color: var(--color-primary-dark); + } + + @include breakpoint('medium') { + display: none; + } +} + +.header__search--desktop { + display: none; + + @include breakpoint('medium') { + display: block; + } +} + +/* Search form */ +.header__search-form__wrapper { + position: absolute; + top: 100%; + left: 0; + width: 100%; + max-height: 0; + overflow: hidden; + visibility: hidden; + z-index: 1; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s; + + .search-active & { + max-height: rem-calc(100); + visibility: visible; + } + + @include breakpoint('medium') { + top: 0; + right: 0; + left: auto; + height: 100%; + max-height: none; + max-width: 0; + transition: max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s; + + .search-active & { + max-width: rem-calc(366); + } + } +} + +.header__search-form { + display: flex; + height: 100%; + width: 100%; + align-items: center; + background-color: var(--color-primary-darker); + padding-inline: var(--spacing-xl) calc(var(--spacing-xl) / 2); + padding-block: calc(var(--spacing-xl) / 2); + + @include breakpoint('medium') { + padding-block: 0; + position: absolute; + right: 0; + width: auto; + min-width: rem-calc(366); + } +} + +.header__search-form__input { + flex: 1; + border: none; + min-height: 2.25rem; + color: var(--color-white); + background-color: transparent; + margin-right: var(--spacing-lg); + border-bottom: 1px solid var(--color-white); +} + +.header__search-form__input::placeholder { + opacity: 1; + color: var(--color-white); +} + +.header__search-form__input::-ms-input-placeholder { + color: var(--color-white); +} + +.header__search-form__actions { + display: flex; + align-items: center; + + button { + cursor: pointer; + border: none; + width: 2.25rem; + height: 2.25rem; + background-color: transparent; + padding: var(--spacing-md); + + &.header__search-form__toggle { + display: none; + + @include breakpoint('medium') { + display: flex; + } + } + + svg { + width: 100%; + height: 100%; + padding: 0; + color: var(--color-white); + user-select: none; + } + } +} + +/* Search toggle button */ +.header__search-form__toggle { + cursor: pointer; + font-weight: 700; + text-decoration: none; + text-transform: uppercase; + color: var(--color-primary-dark); + display: flex; + align-items: center; + gap: var(--spacing-lg); + height: 100%; + background-color: transparent; + border: none; + padding: 0 var(--spacing-md); + margin-inline: auto var(--spacing-md); + + @include breakpoint('medium') { + background-color: var(--color-primary-dark); + padding: 0 var(--spacing-xl); + color: var(--color-white); + margin-inline: 0; + } + + span { + display: none; + + @include breakpoint('medium') { + display: block; + } + } + + svg { + width: rem-calc(24); + height: rem-calc(24); + user-select: none; + + &.icon--close { + display: none; + } + } +} diff --git a/src/components/header/header.stories.js b/src/components/header/header.stories.js index 4d879208..2c09ddfa 100644 --- a/src/components/header/header.stories.js +++ b/src/components/header/header.stories.js @@ -1,36 +1,34 @@ import template from './header.twig'; import { props } from './header.component.yml'; -import figma from '../../../.storybook/configma.json'; import './header'; -const headerData = { - header__branding: 'Branding', - header__menu: [ - { label: 'Home', url: '/' }, - { - label: 'About', - url: '/about', - submenu: [{ label: 'Team', url: '/about/team' }], - }, - { - label: 'Services', - url: '/services', - submenu: [{ label: 'Consulting', url: '/services/consulting' }], - }, - ], - header__search: { placeholder: 'Search...', has_focus: false }, -}; +const headerData = props.properties; +/** + * Storybook Definition. + */ export default { title: 'Components/Header', - decorators: [(story) => `${story()}`], + argTypes: { + menu: { + name: 'Menu Items', + control: { type: 'object' }, + }, + searchPlaceholder: { + name: 'Search Placeholder', + type: 'string', + }, + }, + args: { + menu: headerData.header__menu.data, + searchPlaceholder: headerData.header__search_placeholder.data, + }, }; -export const Header = () => template(headerData); +export const Header = ({ menu, searchPlaceholder }) => + template({ + header__menu: menu, + header__search_placeholder: searchPlaceholder, + header__branding: 'Branding', + }); -Header.parameters = { - design: { - type: 'figma', - url: figma.url + figma.header, - }, -}; diff --git a/src/components/header/header.twig b/src/components/header/header.twig index 1c684b11..f42b5c18 100644 --- a/src/components/header/header.twig +++ b/src/components/header/header.twig @@ -1,56 +1,95 @@ {% set header__base_class = 'header' %} -{% set header__base_class = 'header' %} {% set header__search_placeholder = header__search_placeholder|default('Search placeholder') %} +{% set search_form %} + +{% endset %} +
+ {# Branding #}
- {{ header__branding|raw }} + {% block header__branding %} + {% include "@components/logo/logo.twig" with { + image_src: directory ? '/' ~ directory ~ '/assets/images/logo-text.svg' : 'logo-text.svg', + } %} + {% endblock %}
+ {# Main navigation #} -
- -
- - -
+
+ {# Search toggle button #} + + {{ search_form }}
+ {# Mobile menu toggle button #} +
diff --git a/src/components/logo/logo.twig b/src/components/logo/logo.twig index d7c567e6..f38febe9 100644 --- a/src/components/logo/logo.twig +++ b/src/components/logo/logo.twig @@ -20,8 +20,8 @@ {% block link__content %} {% include "@components/image/responsive-image.twig" with { output_image_tag: true, - image__src: directory ? '/' ~ directory ~ '/assets/images/logo.png' : 'logo.png', - image__alt: 'Logo', + image__src: image_src|default(directory ? '/' ~ directory ~ '/assets/images/logo.png' : 'logo.png'), + image__alt: image_alt|default('Logo'), responsive_image_blockname: 'logo', } %} {% endblock %} From 905d9463cd043bb50690869b2f75d81ad5c501b9 Mon Sep 17 00:00:00 2001 From: robherba Date: Mon, 18 May 2026 08:56:50 -0600 Subject: [PATCH 3/6] chore(EMULSIF-305): update header spacing, add box shadow, and adjust search input text color --- src/components/header/header.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/header/header.scss b/src/components/header/header.scss index 84dba01f..8f780628 100644 --- a/src/components/header/header.scss +++ b/src/components/header/header.scss @@ -5,10 +5,11 @@ display: flex; width: 100%; min-height: 5rem; - padding-inline: var(--spacing-md); + padding-inline: var(--spacing-xl); justify-content: space-between; align-items: center; position: relative; + box-shadow: 0 4px 4px 0 rgba(197, 197, 197, 0.25); @include breakpoint('medium') { justify-content: flex-start; @@ -27,6 +28,7 @@ height: 2.5rem; @include breakpoint('medium') { + margin-left: var(--spacing-xl); height: 3.5rem; } } @@ -252,16 +254,17 @@ } .header__search-form__input { - color: var(--color-primary-dark); + color: var(--color-text-body); border-bottom-color: var(--color-primary-dark); &::placeholder { - color: var(--color-primary-dark); + color: var(--color-text-body); opacity: 0.7; } &::-ms-input-placeholder { - color: var(--color-primary-dark); + color: var(--color-text-body); + opacity: 0.7; } } From 7c44f1bc46bb156f8a6c3e8aed4a9af60f4a3752 Mon Sep 17 00:00:00 2001 From: robherba Date: Mon, 18 May 2026 09:16:16 -0600 Subject: [PATCH 4/6] chore(EMULSIF-305): improve header logo alignment and mobile search layout responsiveness --- src/components/header/header.scss | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/header/header.scss b/src/components/header/header.scss index 8f780628..b0234d63 100644 --- a/src/components/header/header.scss +++ b/src/components/header/header.scss @@ -32,6 +32,11 @@ height: 3.5rem; } } + + .logo__link { + height: 100%; + display: flex; + } } /* Menu */ @@ -237,7 +242,7 @@ } .header__search--mobile { - padding-block: var(--spacing-xl); + padding-block: var(--spacing-md) var(--spacing-xl); padding-inline: var(--spacing-md); .header__search-form__wrapper { @@ -250,10 +255,12 @@ .header__search-form { background-color: transparent; - padding: 0 var(--spacing-lg); + padding: 0 0 0 var(--spacing-lg); } .header__search-form__input { + flex: 1; + width: inherit; color: var(--color-text-body); border-bottom-color: var(--color-primary-dark); From 263a514a564c3ec4ecec3fc3cf45564adfc22d22 Mon Sep 17 00:00:00 2001 From: robherba Date: Fri, 29 May 2026 10:03:30 -0600 Subject: [PATCH 5/6] chore(EMULSIF-305): add search component --- src/components/search/search.component.yml | 23 +++ src/components/search/search.js | 69 ++++++++ src/components/search/search.scss | 174 +++++++++++++++++++++ src/components/search/search.stories.js | 27 ++++ src/components/search/search.twig | 45 ++++++ 5 files changed, 338 insertions(+) create mode 100644 src/components/search/search.component.yml create mode 100644 src/components/search/search.js create mode 100644 src/components/search/search.scss create mode 100644 src/components/search/search.stories.js create mode 100644 src/components/search/search.twig diff --git a/src/components/search/search.component.yml b/src/components/search/search.component.yml new file mode 100644 index 00000000..efe835d7 --- /dev/null +++ b/src/components/search/search.component.yml @@ -0,0 +1,23 @@ +$schema: https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json + +name: HeaderSearch +group: Components +status: stable +props: + type: object + properties: + search__placeholder: + type: string + title: Search Placeholder + description: Placeholder text for search input. + data: Search + search__label: + type: string + title: Search Label + description: Label for the search toggle button. + data: Search +slots: + # Search form markup provided by parent + search_form: + title: Search Form + description: Rendered search form markup. diff --git a/src/components/search/search.js b/src/components/search/search.js new file mode 100644 index 00000000..eaa78ccb --- /dev/null +++ b/src/components/search/search.js @@ -0,0 +1,69 @@ +// Search component behavior for header +Drupal.behaviors.headerSearch = { + attach(context) { + // Helper to close search form + const closeSearch = () => { + document.querySelectorAll('.header.search-active').forEach((header) => { + header.querySelectorAll('.search__toggle').forEach((t) => t.setAttribute('aria-expanded', 'false')); + header.querySelector('.search__form__wrapper')?.setAttribute('aria-hidden', 'true'); + header.classList.remove('search-active'); + }); + }; + + // Helper to handle search form location + const handleSearchLocation = () => { + const isMobile = window.innerWidth < 1040; + const searchForm = document.querySelector('.search__form'); + const desktopContainer = document.querySelector('.search__form__wrapper'); + const mobileContainer = document.querySelector('.header__menu > nav > div'); + + if (!searchForm || !mobileContainer || !desktopContainer) return; + + if (isMobile) { + if (searchForm.parentElement !== mobileContainer) { + mobileContainer.prepend(searchForm); + } + } else { + if (searchForm.parentElement !== desktopContainer) { + desktopContainer.prepend(searchForm); + } + } + }; + + // Initial calls + handleSearchLocation(); + + // Search Toggles + context.querySelectorAll('.search__toggle').forEach((toggle) => { + if (toggle.dataset.jsProcessed) return; + toggle.dataset.jsProcessed = true; + toggle.addEventListener('click', (e) => { + e.stopPropagation(); + const header = toggle.closest('.header'); + const isOpening = !header.classList.contains('search-active'); + closeSearch(); + if (isOpening) { + header.classList.add('search-active'); + toggle.setAttribute('aria-expanded', 'true'); + header.querySelector('input[type="search"]')?.focus(); + header.querySelector('.search__form__wrapper')?.setAttribute('aria-hidden', 'false'); + } + }); + }); + + // Global listeners. + if (!window.headerSearchInitialized) { + window.headerSearchInitialized = true; + const mediaQuery = window.matchMedia('(min-width: 1040px)'); + const handleBreakpointChange = () => { + handleSearchLocation(); + closeSearch(); + }; + + mediaQuery.addEventListener('change', handleBreakpointChange); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeSearch(); + }); + } + } +}; diff --git a/src/components/search/search.scss b/src/components/search/search.scss new file mode 100644 index 00000000..374005bb --- /dev/null +++ b/src/components/search/search.scss @@ -0,0 +1,174 @@ +@use '../../foundation/utility/rem-calc' as *; +@use '../../foundation/breakpoints/breakpoints' as *; + +.search { + display: block; + height: 100%; +} + +/* Search form */ +.search__form__wrapper { + position: absolute; + top: 100%; + left: 0; + width: 100%; + max-height: 0; + overflow: hidden; + visibility: hidden; + z-index: 1; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s; + + @include breakpoint('medium') { + top: 0; + right: 0; + left: auto; + height: 100%; + max-height: none; + max-width: 0; + transition: max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s; + } + + &[aria-hidden='false'] { + max-height: rem-calc(100); + visibility: visible; + + @include breakpoint('medium') { + max-width: rem-calc(560); + } + } +} + +.search__form { + display: flex; + width: 100%; + height: initial; + align-items: center; + padding-block: var(--spacing-lg) var(--spacing-xl); + padding-inline: 0; + background-color: var(--color-white); + + @include breakpoint('medium') { + padding-block: 0; + position: absolute; + right: 0; + width: auto; + height: 100%; + padding-block: var(--spacing-lg); + padding-inline: var(--spacing-xl) calc(var(--spacing-lg)); + background-color: var(--color-primary-darker); + } +} + +.search__form__input { + flex: 1; + border: none; + min-height: 2.25rem; + color: var(--color-text-body); + font-size: var(--font-size-body); + background-color: transparent; + margin-right: var(--spacing-lg); + padding-left: var(--spacing-lg); + border-bottom: 1px solid var(--color-primary-darker); + + @include breakpoint('medium') { + border-bottom: 1px solid var(--color-white); + color: var(--color-white); + } +} + +.search__form__input::placeholder { + opacity: 1; + color: var(--color-primary-dark); + + @include breakpoint('medium') { + color: var(--color-white); + } +} + +.search__form__input::-ms-input-placeholder { + color: var(--color-primary-dark); + + @include breakpoint('medium') { + color: var(--color-white); + } +} + +.search__form__actions { + display: flex; + align-items: center; + + button { + cursor: pointer; + border: none; + width: 2.25rem; + height: 2.25rem; + padding: var(--spacing-md); + background-color: transparent; + + @include breakpoint('medium') { + display: flex; + } + + &.search__toggle { + display: none; + + @include breakpoint('medium') { + display: flex; + } + } + + svg { + width: 100%; + height: 100%; + padding: 0; + user-select: none; + color: var(--color-primary-dark); + + @include breakpoint('medium') { + color: var(--color-white); + } + } + } +} + +/* Search toggle button */ +.search__toggle { + cursor: pointer; + font-weight: 700; + text-decoration: none; + text-transform: uppercase; + color: var(--color-primary-dark); + display: flex; + align-items: center; + gap: var(--spacing-lg); + height: 100%; + background-color: transparent; + border: none; + padding: 0 var(--spacing-md); + margin-inline: auto var(--spacing-md); + + @include breakpoint('medium') { + background-color: var(--color-primary-dark); + padding: 0 var(--spacing-xl); + color: var(--color-white); + margin-inline: 0; + } + + span { + display: none; + + @include breakpoint('medium') { + display: block; + } + } + + svg { + width: rem-calc(24); + height: rem-calc(24); + user-select: none; + + &.icon--close { + display: none; + } + } +} diff --git a/src/components/search/search.stories.js b/src/components/search/search.stories.js new file mode 100644 index 00000000..b290f33a --- /dev/null +++ b/src/components/search/search.stories.js @@ -0,0 +1,27 @@ +import template from './search.twig'; +import { props } from './search.component.yml'; + +import './search'; + +/** + * Storybook definition for Search component. + */ +export default { + title: 'Component/Search', + argTypes: { + header__search_placeholder: { + name: 'Placeholder', + control: { type: 'text' }, + }, + header__search_label: { + name: 'Search Button Label', + control: { type: 'text' }, + }, + }, + args: { + search__placeholder: props.properties.search__placeholder.data, + search__label: props.properties.search__label.data, + }, +}; + +export const Search = (args) => template(args); diff --git a/src/components/search/search.twig b/src/components/search/search.twig new file mode 100644 index 00000000..5f9ecead --- /dev/null +++ b/src/components/search/search.twig @@ -0,0 +1,45 @@ +{# + # Search Component Template + # Available Variables: + # - search__placeholder: (string) placeholder text for the search input + # - search__label: (string) label for the toggle button + #} + +{% set search__base_class = 'search' %} +{% set search__label = search__label|default('Search') %} +{% set search__placeholder = search__placeholder|default('Search') %} + +{% set search_form %} + +{% endset %} + +
+ + {{ search_form }} +
From fa400339e9033708d34920b4df7cd75053cf0691 Mon Sep 17 00:00:00 2001 From: robherba Date: Fri, 29 May 2026 10:03:48 -0600 Subject: [PATCH 6/6] chore(EMULSIF-305): adjust header component --- src/components/header/header.component.yml | 41 +-- src/components/header/header.js | 177 ---------- src/components/header/header.scss | 388 +-------------------- src/components/header/header.stories.js | 35 +- src/components/header/header.twig | 109 ++---- 5 files changed, 71 insertions(+), 679 deletions(-) delete mode 100644 src/components/header/header.js diff --git a/src/components/header/header.component.yml b/src/components/header/header.component.yml index 928197af..5f1ee15e 100644 --- a/src/components/header/header.component.yml +++ b/src/components/header/header.component.yml @@ -6,42 +6,11 @@ status: stable props: type: object properties: - header__menu: - type: array - title: Menu Items - description: The list of menu items to display in the header. - items: - type: object - properties: - label: - type: string - url: - type: string - submenu: - type: array - data: - - label: Academics - submenu: - - label: Undergraduate - url: # - - label: Postgraduate - url: # - - label: Admissions - url: /admissions - submenu: - - label: International - url: # - - label: Domestic - url: # - - label: Research - url: # - - label: About - url: # - header__search_placeholder: - type: string - title: Search Placeholder - description: The placeholder text for the search input. - data: Search placeholder + header__show_search: + type: boolean + title: Show Search + description: Control visibility of the search form in the header. + data: true slots: header__branding: title: Branding diff --git a/src/components/header/header.js b/src/components/header/header.js deleted file mode 100644 index 4285ff85..00000000 --- a/src/components/header/header.js +++ /dev/null @@ -1,177 +0,0 @@ -Drupal.behaviors.siteHeader = { - attach(context) { - // Helper to close open submenus - const closeSubmenus = () => { - document.querySelectorAll('.header__menu-item.open').forEach((item) => { - item.classList.remove('open'); - item.querySelector('.header__menu-item__toggle')?.setAttribute('aria-expanded', 'false'); - }); - }; - - // Helper to close search form - const closeSearch = () => { - document.querySelectorAll('.header.search-active').forEach((header) => { - header.classList.remove('search-active'); - header.querySelectorAll('.header__search-form__toggle').forEach((t) => t.setAttribute('aria-expanded', 'false')); - header.querySelector('.header__search-form__wrapper')?.setAttribute('aria-hidden', 'true'); - }); - }; - - // Helper to close mobile menu - const closeMobileMenu = () => { - document.querySelectorAll('.header.menu-active').forEach((header) => { - header.classList.remove('menu-active'); - header.querySelectorAll('.header__mobile-menu__toggle').forEach((t) => t.setAttribute('aria-expanded', 'false')); - }); - }; - - // Helper to close everything - const closeAll = () => { - closeSubmenus(); - closeSearch(); - closeMobileMenu(); - }; - - // Helper to handle mobile-only links in submenus - const handleMobileLinks = () => { - const isMobile = window.innerWidth < 1040; - document.querySelectorAll('.header__menu-item').forEach((item) => { - const submenu = item.querySelector('.header__menu-item__submenu'); - if (!submenu) return; - - const mobileLink = submenu.querySelector('.header__submenu-item--mobile-only'); - - if (isMobile) { - if (!mobileLink) { - const link = item.querySelector('.header__menu-item__link'); - if (link && link.getAttribute('href')) { - const li = document.createElement('li'); - li.className = 'header__submenu-item header__submenu-item--mobile-only'; - li.innerHTML = `${link.textContent.trim()}`; - submenu.insertBefore(li, submenu.firstChild); - } - } - } else if (mobileLink) { - mobileLink.remove(); - } - }); - }; - - // Helper to handle search form location - const handleSearchLocation = () => { - const isMobile = window.innerWidth < 1040; - const searchForm = document.querySelector('.header__search-form__wrapper'); - const desktopContainer = document.querySelector('.header__search--desktop'); - const mobileContainer = document.querySelector('.header__search--mobile'); - - if (!searchForm || !mobileContainer || !desktopContainer) return; - - if (isMobile) { - if (searchForm.parentElement !== mobileContainer) { - mobileContainer.appendChild(searchForm); - } - } else { - if (searchForm.parentElement !== desktopContainer) { - desktopContainer.appendChild(searchForm); - } - } - }; - - // Initial calls - handleMobileLinks(); - handleSearchLocation(); - - // Menu Toggles - context.querySelectorAll('.header__menu-item').forEach((item) => { - const toggle = item.querySelector('.header__menu-item__toggle'); - if (!toggle) return; - - if (item.dataset.jsProcessed) return; - item.dataset.jsProcessed = true; - - item.addEventListener('click', (e) => { - // Desktop behavior: only trigger if button was clicked - if (window.innerWidth >= 1040 && !e.target.closest('.header__menu-item__toggle')) return; - - // Mobile behavior: clicking the whole item toggles - e.stopPropagation(); - - // Prevent default for top-level link on mobile if it has a submenu - if (window.innerWidth < 1040 && e.target.closest('.header__menu-item__link')) { - e.preventDefault(); - } - - const isOpening = !item.classList.contains('open'); - - closeSubmenus(); - closeSearch(); - - if (isOpening) { - item.classList.add('open'); - toggle.setAttribute('aria-expanded', 'true'); - } - }); - }); - - // Search Toggles - context.querySelectorAll('.header__search-form__toggle').forEach((toggle) => { - if (toggle.dataset.jsProcessed) return; - toggle.dataset.jsProcessed = true; - - toggle.addEventListener('click', (e) => { - e.stopPropagation(); - const header = toggle.closest('.header'); - const isOpening = !header.classList.contains('search-active'); - - closeAll(); - - if (isOpening) { - header.classList.add('search-active'); - toggle.setAttribute('aria-expanded', 'true'); - header.querySelector('.header__search-form__wrapper')?.setAttribute('aria-hidden', 'false'); - header.querySelector('input[type="search"]')?.focus(); - } - }); - }); - - // Mobile Menu Toggles - context.querySelectorAll('.header__mobile-menu__toggle').forEach((toggle) => { - if (toggle.dataset.jsProcessed) return; - toggle.dataset.jsProcessed = true; - - toggle.addEventListener('click', (e) => { - e.stopPropagation(); - const header = toggle.closest('.header'); - const isOpening = !header.classList.contains('menu-active'); - - closeAll(); - - if (isOpening) { - header.classList.add('menu-active'); - toggle.setAttribute('aria-expanded', 'true'); - } - }); - }); - - // Global listeners. - if (!window.siteHeaderInitialized) { - window.siteHeaderInitialized = true; - - const mediaQuery = window.matchMedia('(min-width: 1040px)'); - const handleBreakpointChange = () => { - closeAll(); - handleMobileLinks(); - handleSearchLocation(); - }; - - mediaQuery.addEventListener('change', handleBreakpointChange); - - document.addEventListener('click', (e) => { - if (!e.target.closest('.header__menu-item')) closeSubmenus(); - }); - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') closeAll(); - }); - } - }, -}; diff --git a/src/components/header/header.scss b/src/components/header/header.scss index b0234d63..df4fb829 100644 --- a/src/components/header/header.scss +++ b/src/components/header/header.scss @@ -5,7 +5,7 @@ display: flex; width: 100%; min-height: 5rem; - padding-inline: var(--spacing-xl); + padding-inline: var(--spacing-xl) var(--spacing-lg); justify-content: space-between; align-items: center; position: relative; @@ -39,396 +39,28 @@ } } -/* Menu */ +/* Main navigation */ .header__menu { - display: none; - - .menu-active & { - display: block; - position: absolute; - top: 100%; - left: 0; - width: 100%; - background-color: var(--color-white); - z-index: 10; - padding: var(--spacing-lg); - box-shadow: rgba(17, 12, 46, 0.15) 0px 48px 100px 0px; - } - @include breakpoint('medium') { flex: 1; display: flex; - align-items: end; - flex-direction: column; - justify-content: center; - padding-inline: var(--spacing-xl); - position: static; - background-color: transparent; - width: auto; - box-shadow: none; - z-index: auto; - } -} - -.header__menu > ul { - display: flex; - list-style: none; - flex-direction: column; - align-items: flex-start; - gap: var(--spacing-md); - - @include breakpoint('medium') { - flex-direction: row; - align-items: end; - } -} - -.header__menu-item { - display: flex; - position: relative; - padding: var(--spacing-md) var(--spacing-lg); - gap: var(--spacing-sm); - - cursor: pointer; - - &.open { - flex-wrap: wrap; - } - - @include breakpoint('medium') { - cursor: default; - } -} - -.header__menu-item__link { - font-weight: 700; - text-decoration: none; - text-transform: uppercase; - color: var(--color-primary-dark); - - &:focus, - &:hover { - text-decoration: underline; - } -} - -.header__menu-item__submenu { - display: none; - - .header__menu-item.open & { - width: 100%; - display: flex; - flex-direction: column; - padding-left: var(--spacing-lg); - border-left: 2px solid var(--color-primary-light); - - @include breakpoint('medium') { - position: absolute; - top: 100%; - left: 0; - min-width: 18.75rem; - padding: var(--spacing-lg); - border-radius: var(--size-sm); - background-color: var(--color-white); - box-shadow: rgba(17, 12, 46, 0.15) 0px 48px 100px 0px; - border: none; - z-index: 2; - } - } -} - -.header__submenu-item--mobile-only { - @include breakpoint('medium') { - display: none; - } -} - -.header__submenu-item__link { - text-transform: none; - color: var(--color-text-body); - opacity: 0.85; - padding-block: var(--spacing-md); - display: block; - - &:hover, - &:focus { - text-decoration: underline; - opacity: 1; - } -} - -.header__menu-item__toggle { - cursor: pointer; - width: var(--size-lg); - height: var(--size-lg); - display: flex; - align-items: center; - justify-content: center; - background-color: transparent; - border: none; - padding: 0; - transition: transform 0.3s ease; - - svg { - width: 100%; - height: 100%; - fill: var(--color-primary-dark); - transition: transform 0.2s ease; - transform: rotate(-90deg); - - @include breakpoint('medium') { - transform: rotate(0); - } - } - - &[aria-expanded="true"] { - svg { - transform: rotate(0); - - @include breakpoint('medium') { - transform: rotate(180deg); - } - } - } -} - -/* Mobile Toggle */ -.header__mobile-menu__toggle { - cursor: pointer; - display: flex; - align-items: center; - width: var(--size-xl); - height: var(--size-xl); - background-color: transparent; - fill: var(--color-primary-dark); - border: none; - user-select: none; - padding: 0; - - .header__mobile-menu__hamburger { - display: flex; - flex-direction: column; - justify-content: center; - gap: 8px; - width: var(--size-xl); - height: var(--size-xl); - - span { - display: block; - width: 100%; - height: 3px; - background-color: var(--color-primary-dark); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - } - } - - &[aria-expanded="true"] { - .header__mobile-menu__hamburger { - span:nth-child(1) { - transform: translateY(11px) rotate(45deg); - } - span:nth-child(2) { - opacity: 0; - } - span:nth-child(3) { - transform: translateY(-11px) rotate(-45deg); - } - } - } - - @include breakpoint('medium') { - display: none; + justify-content: end; + margin-right: var(--spacing-xl); } } -.header__search--mobile { - padding-block: var(--spacing-md) var(--spacing-xl); - padding-inline: var(--spacing-md); - - .header__search-form__wrapper { - position: static; - max-height: none; - visibility: visible; - overflow: visible; - transition: none; - } - - .header__search-form { - background-color: transparent; - padding: 0 0 0 var(--spacing-lg); - } - - .header__search-form__input { - flex: 1; - width: inherit; - color: var(--color-text-body); - border-bottom-color: var(--color-primary-dark); - - &::placeholder { - color: var(--color-text-body); - opacity: 0.7; - } - - &::-ms-input-placeholder { - color: var(--color-text-body); - opacity: 0.7; - } - } - - .header__search-form__actions button svg { - color: var(--color-primary-dark); - } - - @include breakpoint('medium') { - display: none; +.main-nav__menu-list-wrapper--level-0 { + .header & { + top: 100%; + padding-right: var(--spacing-lg); } } -.header__search--desktop { +/* Search form */ +.header__search { display: none; @include breakpoint('medium') { display: block; } } - -/* Search form */ -.header__search-form__wrapper { - position: absolute; - top: 100%; - left: 0; - width: 100%; - max-height: 0; - overflow: hidden; - visibility: hidden; - z-index: 1; - transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s; - - .search-active & { - max-height: rem-calc(100); - visibility: visible; - } - - @include breakpoint('medium') { - top: 0; - right: 0; - left: auto; - height: 100%; - max-height: none; - max-width: 0; - transition: max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.4s; - - .search-active & { - max-width: rem-calc(366); - } - } -} - -.header__search-form { - display: flex; - height: 100%; - width: 100%; - align-items: center; - background-color: var(--color-primary-darker); - padding-inline: var(--spacing-xl) calc(var(--spacing-xl) / 2); - padding-block: calc(var(--spacing-xl) / 2); - - @include breakpoint('medium') { - padding-block: 0; - position: absolute; - right: 0; - width: auto; - min-width: rem-calc(366); - } -} - -.header__search-form__input { - flex: 1; - border: none; - min-height: 2.25rem; - color: var(--color-white); - background-color: transparent; - margin-right: var(--spacing-lg); - border-bottom: 1px solid var(--color-white); -} - -.header__search-form__input::placeholder { - opacity: 1; - color: var(--color-white); -} - -.header__search-form__input::-ms-input-placeholder { - color: var(--color-white); -} - -.header__search-form__actions { - display: flex; - align-items: center; - - button { - cursor: pointer; - border: none; - width: 2.25rem; - height: 2.25rem; - background-color: transparent; - padding: var(--spacing-md); - - &.header__search-form__toggle { - display: none; - - @include breakpoint('medium') { - display: flex; - } - } - - svg { - width: 100%; - height: 100%; - padding: 0; - color: var(--color-white); - user-select: none; - } - } -} - -/* Search toggle button */ -.header__search-form__toggle { - cursor: pointer; - font-weight: 700; - text-decoration: none; - text-transform: uppercase; - color: var(--color-primary-dark); - display: flex; - align-items: center; - gap: var(--spacing-lg); - height: 100%; - background-color: transparent; - border: none; - padding: 0 var(--spacing-md); - margin-inline: auto var(--spacing-md); - - @include breakpoint('medium') { - background-color: var(--color-primary-dark); - padding: 0 var(--spacing-xl); - color: var(--color-white); - margin-inline: 0; - } - - span { - display: none; - - @include breakpoint('medium') { - display: block; - } - } - - svg { - width: rem-calc(24); - height: rem-calc(24); - user-select: none; - - &.icon--close { - display: none; - } - } -} diff --git a/src/components/header/header.stories.js b/src/components/header/header.stories.js index 2c09ddfa..a0323026 100644 --- a/src/components/header/header.stories.js +++ b/src/components/header/header.stories.js @@ -1,8 +1,17 @@ +// Markup. import template from './header.twig'; -import { props } from './header.component.yml'; -import './header'; -const headerData = props.properties; +// Data. +import { props as mainMenuProps } from '../navigation/main/main.component.yml'; +import { props as searchProps } from '../search/search.component.yml'; + +// JavaScript. +import '../navigation/base/menu-toggle/menu-toggle'; +import '../navigation/main/main'; +import '../search/search'; + +const mainMenuData = mainMenuProps.properties; +const searchData = searchProps.properties; /** * Storybook Definition. @@ -10,7 +19,7 @@ const headerData = props.properties; export default { title: 'Components/Header', argTypes: { - menu: { + mainMenu: { name: 'Menu Items', control: { type: 'object' }, }, @@ -18,17 +27,25 @@ export default { name: 'Search Placeholder', type: 'string', }, + showSearch: { + name: 'Show Search', + control: { type: 'boolean' }, + }, }, args: { - menu: headerData.header__menu.data, - searchPlaceholder: headerData.header__search_placeholder.data, + mainMenu: mainMenuData.items.data, + searchLabel: searchData.search__label.data, + searchPlaceholder: searchData.search__placeholder.data, + showSearch: true, }, }; -export const Header = ({ menu, searchPlaceholder }) => +export const Header = ({ mainMenu, searchPlaceholder, searchLabel, showSearch }) => template({ - header__menu: menu, - header__search_placeholder: searchPlaceholder, + header__menu: mainMenu, header__branding: 'Branding', + header__show_search: showSearch, + header__search__placeholder: searchPlaceholder, + header__search__label: searchLabel, }); diff --git a/src/components/header/header.twig b/src/components/header/header.twig index f42b5c18..830b355c 100644 --- a/src/components/header/header.twig +++ b/src/components/header/header.twig @@ -1,31 +1,19 @@ -{% set header__base_class = 'header' %} -{% set header__search_placeholder = header__search_placeholder|default('Search placeholder') %} +{# + # Header Component Template + # Available Variables: + # - header__base_class: (string) base BEM class, default 'header' + # - header__search__placeholder: (string) placeholder text for search input + # - header__search__label: (string) label for search toggle button + # - header__menu_label: (string) label for mobile menu toggle + # - additional_attributes: (array) HTML attributes for the header element + # - search_form: (string) rendered search form markup + # + # Available Blocks (Slots): + # - header__branding + # - header__menu + #} -{% set search_form %} - -{% endset %} +{% set header__base_class = 'header' %}
{# Branding #} @@ -37,59 +25,22 @@ {% endblock %} {# Main navigation #} - -
- {# Search toggle button #} - - {{ search_form }} + {% endblock %} +
+ {# Search form #} + {% if header__show_search %} +
+ {% block header__search %} + {% include '@components/search/search.twig' with { + search__placeholder: header__search__placeholder, + search__label: header__search__label, + } %} + {% endblock %}
- {# Mobile menu toggle button #} - + {% endif %}