Skip to content

Commit

Permalink
imp: Improve accessibility of popups
Browse files Browse the repository at this point in the history
This adds:

- automatic focus of the first element of a popup menu when opening a
  menu with the keyboard
- navigation in the menu with the arrows keys
- a new method to simply close the menu programmatically
- fix the selection of the popup__opener (typo)
- support for "radio" items in the menu
  • Loading branch information
marien-probesys committed Sep 29, 2023
1 parent 392db06 commit c723bb3
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 2 deletions.
112 changes: 110 additions & 2 deletions assets/javascripts/controllers/popup_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,39 @@ export default class extends Controller {
});

this.element.addEventListener('keydown', this.closeOnEscape.bind(this));
this.element.addEventListener('keydown', this.toggleMenuOnKeydown.bind(this));
this.element.addEventListener('keydown', this.navigateInMenuOnArrow.bind(this));
}

/**
* Update the aria-expanded attribute on toggle.
*/
update (event) {
const openerElement = this.element.querySelector('.popup_opener');
const openerElement = this.element.querySelector('.popup__opener');
if (openerElement) {
openerElement.setAttribute('aria-expanded', this.element.open);
}
}

/**
* Close the menu.
*/
close (event) {
if (!this.element.open) {
return;
}

this.element.open = false;

const openerElement = this.element.querySelector('.popup__opener');
if (openerElement) {
openerElement.focus();
}
}

/**
* Close the menu when clicking on an element outside of the current popup.
*/
closeOnClickOutside (event) {
if (this.element.contains(event.target)) {
// The user clicked on an element inside the popup menu.
Expand All @@ -45,6 +69,9 @@ export default class extends Controller {
this.element.open = false;
}

/**
* Close the menu on Escape keydown.
*/
closeOnEscape (event) {
if (event.key !== 'Escape') {
return;
Expand All @@ -56,9 +83,90 @@ export default class extends Controller {

this.element.open = false;

const openerElement = this.element.querySelector('.popup_opener');
const openerElement = this.element.querySelector('.popup__opener');
if (openerElement) {
openerElement.focus();
}
}

/**
* Toggle the menu when activating the popup__opener with keyboard, and set
* the focus on the first element if the menu is opened.
*/
toggleMenuOnKeydown (event) {
const openerElement = this.element.querySelector('.popup__opener');
if (!openerElement || event.target !== openerElement) {
return;
}

if (event.code !== 'Enter' && event.code !== 'Space') {
return;
}

event.preventDefault();

this.element.open = !this.element.open;

if (this.element.open) {
const itemElement = this.element.querySelector('.popup__item');
if (itemElement) {
itemElement.focus();
}
}
}

/**
* Handle the navigation in the menu with the arrows.
*/
navigateInMenuOnArrow (event) {
if (!this.element.open) {
return;
}

if (event.code !== 'ArrowDown' && event.code !== 'ArrowUp') {
return;
}

event.preventDefault();

// Get the current focused element from the popup__items (if any).
const itemsElements = this.element.querySelectorAll('.popup__item');
let focusedElementKey = null;
let focusedElement = null;
for (const [key, element] of itemsElements.entries()) {
// If the focused element is a radio button, the popup__item will
// be its label, so we need to check for its "for" attribute.
if (element === document.activeElement || element.getAttribute('for') === document.activeElement.id) {
focusedElementKey = key;
focusedElement = element;
break;
}
}

if (event.code === 'ArrowDown') {
// When pressing ArrowDown, focus the element after the actual
// focused element. If there are no focused element or we are
// already at the end of the list, focus the first element.
if (
!focusedElement ||
focusedElementKey === itemsElements.length - 1
) {
itemsElements[0].focus();
} else {
itemsElements[focusedElementKey + 1].focus();
}
} else if (event.code === 'ArrowUp') {
// When pressing ArrowUp, focus the element before the actual
// focused element. If there are no focused element or we are
// already at the beginning of the list, focus the last element.
if (
!focusedElement ||
focusedElementKey === 0
) {
itemsElements[itemsElements.length - 1].focus();
} else {
itemsElements[focusedElementKey - 1].focus();
}
}
}
};
41 changes: 41 additions & 0 deletions assets/stylesheets/components/popups.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
}

.popup__opener {
height: 100%;

list-style: none;
}

Expand Down Expand Up @@ -81,6 +83,13 @@
margin-top: 1.25rem;
}

.popup__container--top {
bottom: 100%;

margin-top: 0;
margin-bottom: 1.25rem;
}

.popup__container--center::before,
.popup__container--right::before,
.popup__container--left::before {
Expand Down Expand Up @@ -111,6 +120,13 @@
.popup__container--left::before {
left: 2.75rem;
}

.popup__container--top::before {
top: auto;
bottom: -10px;

transform: rotate(180deg);
}
}

.popup__title {
Expand Down Expand Up @@ -175,3 +191,28 @@
.popup__item .icon {
margin-right: 0.25rem;
}

input[type="radio"]:focus + .popup__item {
color: var(--color-grey12);

background-color: var(--color-primary5);

outline: 0.3rem solid var(--color-primary9);
outline-offset: -0.1rem;
}

input[type="radio"] + .popup__item::before {
visibility: hidden;
}

input[type="radio"]:checked + .popup__item::after {
top: 1.75rem;
left: 2.2rem;

height: 1rem;

background-color: transparent;
border-top: none;
border-right: none;
border-radius: 0;
}
24 changes: 24 additions & 0 deletions docs/developers/popups.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ A form to perform an action:
</form>
```

Radio buttons to select an option:

```twig
<div>
<input
id="status-pending"
type="radio"
name="status"
value="pending"
/>
<label class="popup__item" for="status-pending">
{{ 'tickets.status.pending' | trans }}
</label>
</div>
```

## Menu position

You can align the menu with the button either on the right:
Expand All @@ -80,6 +97,13 @@ On the left:
</nav>
```

On the top:

```html
<nav class="popup__container popup__container--top">
</nav>
```

Or centered:

```html
Expand Down

0 comments on commit c723bb3

Please sign in to comment.