Code Review: Olivero Menu

The Olivero theme has a responsive menu, let's see how it works.

Here is what it looks like, with a few dummy links:

Image
Olivero menu - desktop

As the window size shrinks, it switches to the mobile menu when it runs out of space.

Image
Olivero theme - mobile

Olivero has a few js files related to navigation.

navigation-utils.js

/**
 * @file
 * Controls the visibility of desktop navigation.
 *
 * Shows and hides the desktop navigation based on scroll position and controls
 * the functionality of the button that shows/hides the navigation.
 */

Included in olivero/navigation-base library, required by olivero/navigation-primary.

Create global object to track the state.

Drupal.olivero = {}

This adds the isDesktopNav() function, and makes it available globally as Drupal.olivero.isDesktopNav().

/**
 * Checks if the mobile navigation button is visible.
 *
 * @return {boolean}
 *   True if navButtons is hidden, false if not.
 */
function isDesktopNav() {
  const navButtons = document.querySelector(
    '[data-drupal-selector="mobile-buttons"]',
  );
  return navButtons
    ? window.getComputedStyle(navButtons).getPropertyValue('display') ===
        'none'
    : false;
}

Drupal.olivero.isDesktopNav = isDesktopNav;

These two variables are set up for other functions to use.

const stickyHeaderToggleButton = document.querySelector(
  '[data-drupal-selector="sticky-header-toggle"]',
);
const siteHeaderFixable = document.querySelector(
  '[data-drupal-selector="site-header-fixable"]',
);

This one does what it says.

function stickyHeaderIsEnabled() {
  return stickyHeaderToggleButton.getAttribute('aria-checked') === 'true';
}

Uses localStorage to remember expanded state.

/**
 * Save the current sticky header expanded state to localStorage, and set
 * it to expire after two weeks.
 *
 * @param {boolean} expandedState
 *   Current state of the sticky header button.
 */
function setStickyHeaderStorage(expandedState) {}

Just updates the localStorage expiration.

/**
 * Update the expiration date if the sticky header expanded state is set.
 *
 * @param {boolean} expandedState
 *   Current state of the sticky header button.
 */
function updateStickyHeaderStorage(expandedState) {}

toggleStickyHeaderState()

/**
 * Toggle the state of the sticky header between always pinned and
 * only pinned when scrolled to the top of the viewport.
 *
 * @param {boolean} pinnedState
 *   State to change the sticky header to.
 */
function toggleStickyHeaderState(pinnedState) {
  if (isDesktopNav()) {
    siteHeaderFixable.classList.toggle('is-expanded', pinnedState);
    stickyHeaderToggleButton.setAttribute('aria-checked', pinnedState);
  }
}

getStickyHeaderStorage()

/**
 * Return the sticky header's stored state from localStorage.
 *
 * @return {boolean}
 *   Stored state of the sticky header.
 */
function getStickyHeaderStorage() {}

toggleDesktopNavVisibility()

IntersectionObserver callback.

// Only enable scroll interactivity if the browser supports Intersection
// Observer.
// @see https://github.com/w3c/IntersectionObserver/blob/master/polyfill/intersection-observer.js#L19-L21
if (
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype
) {
const fixableElements = document.querySelectorAll(
  '[data-drupal-selector="site-header-fixable"], [data-drupal-selector="social-bar-inner"]',
);

function toggleDesktopNavVisibility(entries) {}

getRootMargin()

/**
 * Gets the root margin by checking for various toolbar classes.
 *
 * @return {string}
 *   Root margin for the Intersection Observer options object.
 */
function getRootMargin() {}

monitorNavPosition()

Set up IntersectionObserver.

/**
 * Monitor the navigation position.
 */
function monitorNavPosition() {}

Set up event subscribers.

if (stickyHeaderToggleButton) {
  stickyHeaderToggleButton.addEventListener('click', () => {
    const pinnedState = !stickyHeaderIsEnabled();
    toggleStickyHeaderState(pinnedState);
    setStickyHeaderStorage(pinnedState);
  });
}

Misc setup.

// If header is pinned open and a header element gains focus, scroll to the
// top of the page to ensure that the header elements can be seen.
const siteHeaderInner = document.querySelector(
  '[data-drupal-selector="site-header-inner"]',
);
if (siteHeaderInner) {
  siteHeaderInner.addEventListener('focusin', () => {
    if (isDesktopNav() && !stickyHeaderIsEnabled()) {
      const header = document.querySelector(
        '[data-drupal-selector="site-header"]',
      );
      const headerNav = header.querySelector(
        '[data-drupal-selector="header-nav"]',
      );
      const headerMargin = header.clientHeight - headerNav.clientHeight;
      if (window.scrollY > headerMargin) {
        window.scrollTo(0, headerMargin);
      }
    }
  });
}

Initialize functions.

monitorNavPosition();
updateStickyHeaderStorage(getStickyHeaderStorage());
toggleStickyHeaderState(getStickyHeaderStorage());

 

nav-resize.js

The file description explains how it works.

/**
 * @file
 * This script watches the desktop version of the primary navigation. If it
 * wraps to two lines, it will automatically transition to a mobile navigation
 * and remember where it wrapped so it can transition back.
 */

The init function sets up a resizeObserver to call checkIfDesktopNavigationWraps() when the primary nav resizes.

/**
 * Set up Resize Observer to listen for changes to the size of the primary
 * navigation.
 *
 * @param {Element} primaryNav - The primary navigation's top-level <ul> element.
 */
function init(primaryNav) {
  const resizeObserver = new ResizeObserver(checkIfDesktopNavigationWraps);
  resizeObserver.observe(primaryNav);
}
/**
 * Callback from Resize Observer. This checks if the primary navigation is
 * wrapping, and if so, transitions to the mobile navigation.
 *
 * @param {ResizeObserverEntry} entries - Object passed from ResizeObserver.
 */

function checkIfDesktopNavigationWraps(entries)

Issues

Olivero: Support second-level navigation submenus on secondary menu

Olivero Doesn't Explain Menu Behavior When Migrating From Other Themes

Unfold the main menu on mobile and improve navigation

Olivero: Header menu should not close if menu item has focus

Provide Olivero theme settings to control the behaviour of the header regions

Only close Olivero sub-menus when resize results in a different menu format

Olivero: Add option for reduced header/branding height

Olivero: Fix long menu (desktop) design

Olivero: Focus outline does not accommodate on long text in primary & secondary navigation

Keyboard Navigation: Arrow Keys in menu for Olivero

Add Olivero-like primary navigation to Starterkit theme

Olivero: Z-index issue with table with sticky header

Olivero's focus state outline can get cut off certain situations

How to keep the menu open when it goes to mobile version ?

Let users disable animation in Olivero

Under certain circumstances the primary navigation can create horizontal scrollbar

Olivero should use Drupal.displace() to place the mobile menu

Get rid of jQuery in displace event

Opportunity to refactor parts of Olivero's second-level-navigation JS

When I'm logged in, the secondary menu does not want to open

Navigation top bar overlaps with Olivero menu

Olivero: Mobile tabs can become out of order if browser is resized

Add a gradient effect when the main menu is not fully displayed

Potential header menu "X" close-icon usability issue in Olivero

Toolbar doesn't stay fixed properly for non-admin users

Tags
Themes