1.5.1 • Published 1 year ago

priority-plus v1.5.1

Weekly downloads
112
License
ISC
Repository
github
Last release
1 year ago

priorityPlus

Github build status npm npm bundle size semantic-release

A modern implementation of the priority plus navigation pattern.

Animation showing nav items moving to and from the overflow nav.

You can see a demo on the landing page.

There's also a Glitch pen available here with a different, alternatively styled example. Check out the source.

The short stuff:

  • Vanilla JS, dependency free. Available as an ES6 module, or a drop-in IIFE assigned to the global priorityPlus.
  • Uses the IntersectionObserver API instead of width-based calculations.
  • Toggles the appropriate WAI-ARIA attributes to remain accessible.
  • Provides a class hook to style the menu differently when all items are in the overflow/hidden.
  • Provides a way to update the overflow toggle button with the hidden item count.

Comes in at under 2.5kb after gzip.

What is it

As Brad explains:

The Priority+ pattern...exposes what’s deemed to be the most important navigation elements and tucks away less important items behind a “more” link. The less important items are revealed when the user clicks the “more” link.

Diagram overview

This library implements the pattern by fitting as many navigation items as possible into the 'primary' navigation, and then automatically moving the rest into a dropdown. If more space becomes available, the links are gradually re-instated into the primary navigation.

There are already examples of libraries that follow this behaviour, such as PriorityNav.js. However most of these were written before the advent of modern browser APIs such as the IntersectionObserver, operating by measuring the parent and child elements, then calculating how many items can (and cannot) fit.

This library, however, uses an IntersectionObserver to avoid costly measurements, instead relying on the browser to tell us when an element 'intersects' with the edge of the viewport. The result is faster - and generally snazzier.

How it works

When initiated, the library creates a new version of your navigation with the required markup, including a toggle button:

<div data-main class="p-plus">
  <div class="p-plus__primary-wrapper">
    <ul data-primary-nav class="p-plus__primary" aria-hidden="false">
      <li data-nav-item>
        <a href="#">Home</a>
      </li>
      <!-- etc -->
    </ul>
  </div>
  <button data-toggle-btn class="p-plus__toggle-btn" aria-expanded="false">
    <span aria-label="More">+ (0)</span>
  </button>
  <ul data-overflow-nav class="p-plus__overflow" aria-hidden="true"></ul>
</div>

It also clones this version, so there are actually two versions of the new navigation living on the page. One is the visible navigation that the library will add and remove elements from, and the other is an invisible copy that always retains the full set of nav items (which are forced to overflow horizontally).

Diagram showing how the clone navigation overflows the wrapper.

As the items overflow, they trigger the parent's IntersectionObserver. This means we can easily detect when (and in which direction) a new nav item clashes with the outer boundary of the navigation.

Once we detect a collision, we store which navigation it should now belong to (primary or overflow) and update both in the DOM.

Browser support

This library is designed to work with modern browsers. Both JavaScript bundles are transpiled down to ES6, and include syntax such as template-literals and the spread operator. If you'd like priorityPlus to work in (for instance) Internet Explorer, you'll need to bring your own transpilation down to ES5. When using Webpack, this would usually involve changing your exclude to be non-inclusive of the library:

exclude: /node_modules\/(?!priority-plus)/,

You will need to bring your own support for the IntersectionObserver API through a polyfill.

Installation

Install from NPM:

npm install priority-plus

Or use a CDN if you're feeling old-school:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/priority-plus/dist/priority-plus.css">
<!-- Will be available globally as priorityPlus -->
<script defer src="https://cdn.jsdelivr.net/npm/priority-plus/dist/priority-plus.js"></script>

Setup

You can create a new instance by passing in an HTMLElement that is the direct parent of the navigation items:

<nav>
  <ul class="js-p-target">
    <li><a href="/">Home</a></li>
    <li><a href="/">About</a></li>
    <li><a href="/">Work</a></li>
    <li><a href="/">Services longer nav title</a></li>
    <li><a href="/">Contact</a></li>
  </ul>
</nav>
// Doesn't have to be SASS, just ensure the CSS is included.
@import "node_modules/priority-plus/dist/priority-plus";
import priorityPlus from 'priority-plus';
priorityPlus(document.querySelector('.js-p-target'));

It's important that the element is the immediate parent, since internally the library iterates over the children as the basis for the new navigation items.

Methods

The following methods are available on a new instance, e.g.:

const inst = priorityPlus(document.querySelector('.js-p-target'));
console.log(inst.getNavElements());

getNavElements(): { [key: string]?: HTMLElement|HTMLElement[] }

Retrieves an object containing references to each element in the primary generated navigation.

on(eventType: string, cb: Function)

Sets up an event listener on the instance (not the target element). See events for a list of the events that are triggered.

Example:

inst.on('itemsChanged', () => console.log('Items changed'));

off(eventType: string, cb: Function)

Destroys an event listener.

Example:

const callback = () => console.log('Items changed');
inst.on('itemsChanged', callback);
// etc
inst.off('itemsChanged', callback);

setOverflowNavOpen(open: boolean)

Opens or closes the overflow navigation programatically.

Example:

inst.setOverflowNavOpen(true);

toggleOverflowNav()

Opens the overflow nav if closed, closes it if open.

Example:

inst.toggleOverflowNav();

Options

openOnToggle

You can disable the default behaviour of automatically opening the overflow when the toggle is clicked by passing false. If you wanted to re-implement your own toggle behaviour, you could do so by listening for the toggleClicked event:

const inst = priorityPlus(document.querySelector('.js-p-target'), {
  openOnToggle: false,
})

inst.on('toggleClicked', () => {
  // Re-implement existing behaviour
  inst.toggleOverflowNav();
})

collapseAtCount

If you'd like to collapse into the overflow when the primary navigation becomes depleted, you can do with the collapseAtCount option:

priorityPlus(document.querySelector('.js-p-target'), {
  collapseAtCount: 2,
});

The above will move all menu items into the overflow if only two can 'fit' into the primary. This is essentially a way to avoid orphan nav items.

Classes

If you'd like to override the default classes, you can pass in a classNames object like so:

priorityPlus(document.querySelector('.js-p-target'), {
  classNames: {
    // Will override the p-plus class.
    // Other classes will be un-touched.
    wrapper: ['my-p-plus'],
  },
});

Each class override must be passed as an array.

OptionDefaultExplanation
containerp-plus-containerThis is the wrapper that collects both 'clones' of the navigation. Its purpose is to provide a way to obscure the clone.
mainp-plusThe class applied to each of the top-level navigation wrappers. Be aware it applies to both the clone and the visible copy.
primary-nav-wrapperp-plus__primary-wrapperOuter wrapper for the 'primary' (non-overflow) navigation.
primary-navp-plus__primaryInner wrapper for the 'primary' (non-overflow) navigation.
overflow-navp-plus__overflowWrapper for the overflow navigation.
toggle-btnp-plus__toggle-btnApplied to the dropdown menu toggle button.

Templates

innerToggleTemplate(String|Function)

Default: 'More'

Overrides the inner contents of the 'view more' button. If you pass a string, then it will only render once, but if you pass it a function it will re-render every time the navigation is updated.

The function receives an object containing two parameters, toggleCount (the number of items in the overflow) and totalCount (which is the total number of navigation items).

Example:

priorityPlus(document.querySelector('.js-p-target'), {
  innerToggleTemplate: ({ toggleCount, totalCount }) => `
    Menu${toggleCount && toggleCount !== totalCount ? ` (${toggleCount})` : ''}
  `,
});

Be aware that if you alter the width of the element by changing its content, you could create a loop wherein the button updates, triggering a new intersection, which causes the button to update (and so on). Therefore it's probably a good idea to apply a width to the button so it remains consistent.

Events

Arguments are provided via the details property.

NameArgumentsDescription
showOverflowNoneTriggered when the overflow nav becomes visible.
hideOverflowNoneTriggered when the overflow nav becomes invisible.
itemsChangedoverflowCount (The number of items in the overflow nav)Triggered when the navigation items are updated (either added/removed).
toggleClickedoriginal (The original click event)Triggered when the overflow toggle button is clicked.

Defining a 'mobile' breakpoint

You should never have to base the amount of visible navigation items visible on the viewport size.

However, if you would like to break (early) to the 'mobile' view at a pre-defined point, you can do so with just CSS.

Simply add a rule that causes the first item in the navigation to expand beyond the viewport, like so:

@media (max-width: 40em) {
  .p-plus__primary > li:first-child {
    width: 100%;
  }
}

Troubleshooting

Flex nav collapsing

If your menu is part of an auto-sized flex-child, it will probably need a positive flex-grow value to prevent it reverting to its smallest form. For instance:

<header class="site-header">
  <h1 class="site-header__title">My great site title</h1>
  <nav class="site-header__nav">
    <ul class="site-nav js-site-nav">
      <li><a href="#">Services</a></li>
      <li><a href="#">Thinking</a></li>
      <li><a href="#">Events</a></li>
    </ul>
  </nav>
</header>
.site-header {
  display: flex;
  align-items: center;
}

/**
 * Prevents nav from collapsing.
 */
.site-header__nav {
  flex-grow: 1;
}

Navigation event listeners

priorityPlus makes a copy of your menu, rather than reusing the original. Classes and attributes are carried over, but not event listeners. This means that any additional libraries or JavaScript which operate on the menu and its children needs to be run (or re-run) after initialization:

priorityPlus(document.querySelector('.js-p-target'));
// .js-p-target is *not* the same element, but has been cloned and replaced
loadLibrary(document.querySelector('.js-p-target'));

If your use-case is not covered by this, please raise an issue.

1.5.1

1 year ago

1.5.0

3 years ago

1.4.0

3 years ago

1.3.1

3 years ago

1.1.0

5 years ago

1.0.2

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago