priority-plus-selected
v1.0.4
Published
A modern implementation of the priority plus navigation pattern along with selected item handling
Downloads
8
Maintainers
Readme
priorityPlus
A modern implementation of the priority plus navigation pattern, which also displays the selected menu item.
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.
Displaying of selected item: The aim is to highlight the currently selected menu item even if the item exists inside the overflow nav, the solution to this is by interchanging the selected item with the last item in the priority nav. The item, if not selected anymore will revert back to it's original position. This behaviour is enabled by default and can be disabled if needed.
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.
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).
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.
showSelectedMenuItem
If you'd like to disable the rearrangement of items in order to highlight the selected item, you can do it with the boolean showSelectedMenuItem
,
priorityPlus(document.querySelector('.js-p-target'), {
showSelectedMenuItem: false,
});
showSelectedMenuItem
is true by default.
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.
| Option | Default | Explanation |
|:--------|:----------|:-------------|
| container
| p-plus-container
| This is the wrapper that collects both 'clones' of the navigation. Its purpose is to provide a way to obscure the clone.
| main
| p-plus
| The class applied to each of the top-level navigation wrappers. Be aware it applies to both the clone and the visible copy.
| primary-nav-wrapper
| p-plus__primary-wrapper
| Outer wrapper for the 'primary' (non-overflow) navigation.
| primary-nav
| p-plus__primary
| Inner wrapper for the 'primary' (non-overflow) navigation.
| overflow-nav
| p-plus__overflow
| Wrapper for the overflow navigation.
| toggle-btn
| p-plus__toggle-btn
| Applied 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.
| Name | Arguments | Description |
|:----------------|:----------------------------------------------------------|:------------------------------------------------------------------------|
| showOverflow
| None | Triggered when the overflow nav becomes visible. |
| hideOverflow
| None | Triggered when the overflow nav becomes invisible. |
| itemsChanged
| overflowCount
(The number of items in the overflow nav) | Triggered when the navigation items are updated (either added/removed). |
| toggleClicked
| original
(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.