Menu
Menu is a component that contains a button that shows a hidden menu comprised of options.
Menu conforms to the W3C WAI-ARIA authoring practices.
Set up
First import the styles into your main SASS file, replacing <path-to-node_modules>
with the path to the node_modules directory relative to the file:
@import '<path-to-node_modules>/@potato/ace/components/menu/menu';
Alternatively ace.scss includes all ACE component SASS files, so if using multiple ACE components it can be imported instead:
@import '<path-to-node_modules>/@potato/ace/ace';
A CSS file is also provided for convenience and is located at <path-to-node_modules>/@potato/ace/components/menu/ace-menu.css
.
Then import the class into your JavaScript entry point:
import '<path-to-node_modules>/@potato/ace/components/menu/menu';
For convenience the ES6 class is exported as Menu
and the attribute names used by the class are exported as properties of ATTRS
.
After the event DOMContentLoaded
is fired on document
an instance of Menu is instantiated within each <ace-menu>
element and an ID ace-menu-<n>
is added for any instance without one, where <n>
is a unique integer. Once instantiation is complete a custom event ace-menu-ready
is dispatched to window
. See the Custom events section below for more details.
Menu must have both a descendant button, to show the hidden list of options, and a the descendant list and will use the first descendant <ul>
for this. This list can be empty upon instantiation and options can be dynamically added to, or removed from, it later as long as custom event ace-menu-update-options
is dispatched to the Menu instance afterwards.
Usage
The list of options is displayed when users click on the trigger or press ↑, ↓, Enter or Space while the trigger is focused, with ↑ also selecting the last option in the list and the other three keys also selecting the first option. The list is aware of it's position within the window and ensures that it is fully visible in the viewport. It will hence appear below the trigger and aligned to it's left edge if there is enough space, otherwise it will appear above and/or aligned to the right edge, as necessary. Clicking outside a shown list or pressing
Clicking on an option or navigating to it using ↑ or ↓ and pressing Enter will select the option, hide the list and dispatch the ace-menu-option-chosen
custom event.
Type-ahead can also be used to select an option by typing one or more characters that the option's text starts with. Repeatedly typing the same character with a short delay in-between will cycle through all matching options.
Styles
The following SASS is applied to Menu. The SASS variables use !default
so can also be easily overridden by developers. SASS variables used that are not defined here are defined in <path-to-node_modules>/@potato/ace/common/constants.scss.
@import '../../common/constants';
// VARIABLES
$ace-menu-list-bg-color: #fff !default;
$ace-menu-option-text-color: #000 !default;
$ace-menu-selected-option-bg-color: $ace-color-selected !default;
$ace-menu-selected-option-text-color: #fff !default;
// STYLES
ace-menu {
position: relative;
}
[ace-menu-list] {
background: $ace-menu-list-bg-color;
color: $ace-menu-option-text-color;
left: 0;
list-style: none;
position: absolute;
user-select: none;
white-space: nowrap;
z-index: $ace-menu-list-z-index;
&:focus {
outline: none;
}
&:not([ace-menu-list-visible]) {
display: none;
}
&[ace-u-float-above] {
bottom: 100%;
top: initial;
}
&[ace-u-float-left] {
left: initial;
right: 0;
}
&[ace-u-float-right] {
left: 0;
right: initial;
}
}
[ace-menu-option] {
&:hover,
&[aria-selected="true"] {
background: $ace-menu-selected-option-bg-color;
color: $ace-menu-selected-option-text-color;
}
}
Custom events
Menu uses the following custom events, the names of which are available in its exported EVENTS
object, similar to ATTRS
, so they may be imported into other modules.
Dispatched events
The following events are dispatched to window
by Menu.
Ready
ace-menu-ready
This event is dispatched when Menu finishes initialising just after page load, and after dynamically added options are initialised in response to the ace-menu-update-options
custom event being dispatched. The event name is available as EVENTS.OUT.READY
and its detail
property is composed as follows:
'detail': {
'id': // ID of Menu [string]
}
Option chosen
ace-menu-option-chosen
This event is dispatched when an option is chosen by the user. The event name is available as EVENTS.OUT.OPTION_CHOSEN
and its detail
property is composed as follows:
'detail': {
'id': // ID of Menu [string],
'chosenOption': {
'id': // ID of chosen option [string],
'index': // Index of chosen option [number]
},
}
Listened for event
Menu listens for the following event that should be dispatched to window
.
Update options
ace-menu-update-options
This event should be dispatched when options are added to or removed from the list and causes Menu to initialise them and then dispatch the ace-menu-ready
event. The event name is available as EVENTS.IN.UPDATE_OPTIONS
and its detail
property should be composed as follows:
'detail': {
'id': // ID of target Menu [string]
}
Examples
Each example contains a live demo and the HTML code that produced it. The code shown may differ slightly to that rendered for the demo as some components may alter their HTML when they initialise.
Simple Menu
Example of a simple Menu with options that contain text and an opiton containing a link.
- First Option
- Second Option
- Third Option
- Link to homepage
<ace-menu>
<button>Menu trigger</button>
<ul>
<li>First Option</li>
<li>Second Option</li>
<li>Third Option</li>
<li><a href="/">Link to homepage</a></li>
</ul>
</ace-menu>
Menu with dynamic options
In this example the Menu instantiates with an empty <ul>
that can be populated with options using Add option. The last option can be removed using the Remove option. Both these buttons dispatch the ace-menu-update-options
event that updates the list options. The extra JavaScript code required by this example is also included below.
<button id="add-option">
Add option
</button>
<button id="remove-option">
Remove option
</button>
<hr>
<ace-menu id="ace-custom-events-menu">
<button>Menu Trigger</button>
<ul></ul>
</ace-menu>
import { EVENTS } from '/ace/components/menu/menu.js';
document.addEventListener('DOMContentLoaded', () => {
const MENU_ID = 'ace-custom-events-menu';
const menuEl = document.getElementById(MENU_ID);
const menuListEl = menuEl.querySelector('ul');
const updateOptions = () => window.dispatchEvent(new CustomEvent(
EVENTS.IN.UPDATE_OPTIONS,
{'detail': {'id': MENU_ID}}
));
document.getElementById('add-option').addEventListener('click', () => {
const optionEl = document.createElement('li');
optionEl.textContent = 'Option';
menuListEl.appendChild(optionEl);
updateOptions();
});
document.getElementById('remove-option').addEventListener('click', () => {
const lastOptionEl = menuListEl.querySelector('li:last-child');
if (lastOptionEl) {
menuListEl.removeChild(lastOptionEl);
updateOptions();
}
});
});
Styled Menu
An example of how Menu can be styled, with the applied CSS shown below.
<div class="styled-menu-container">
<ace-menu class="styled-menu">
<button aria-label="View more options" title="View more options" class="styled-menu__trigger">
</button>
<ul class="styled-menu__list">
<li class="styled-menu__option">
<img alt="Potato logo" src="/img/logo.svg" />
First option
</li>
<li class="styled-menu__option">
<img alt="Potato Spuddy with headphones and phone" src="/img/phone-spuddy.png" />
Second option
</li>
<li class="styled-menu__option">
<img alt="Potato Spuddy with virtual reality goggles" src="/img/goggles-spuddy.png" />
Third option
</li>
</ul>
</ace-menu>
</div>
.styled-menu {
&-container {
display: flex;
flex-direction: row-reverse;
}
&__trigger {
background: transparent;
border: none;
border-radius: 50%;
cursor: pointer;
font-size: 35px;
height: 40px;
position: relative;
transition: background-color .2s;
width: 40px;
&:focus,
&:hover {
background-color: #00bed0;
color: white;
}
&:focus {
outline: none;
}
&::after {
content: '\22EE';
display: inline-block;
left: 30%;
position: absolute;
top: 0;
}
}
&__list {
border-radius: 4px;
box-shadow: 0 2px 10px 0 #837b8b;
padding: 8px 0;
}
&__option {
align-items: center;
cursor: pointer;
display: flex;
font-family: 'Roboto', sans-serif;
font-size: 14px;
padding: 10px 16px;
&:hover,
&[aria-selected="true"] {
background: #41354d;
}
img {
height: 2em;
margin-right: 10px;
}
}
}