Improved header element accessibility when at mobile sizes

Intended to fix issues raised in #2681.
Changes up the tri-layout tabs, and the main header menu toggle,
to be buttons while adding better text and keyboard controls.

Updated the component format of a few elements along the way.
This commit is contained in:
Dan Brown 2021-04-19 21:41:13 +01:00
parent 60ffe6a993
commit 9df4dee1b2
8 changed files with 66 additions and 31 deletions

View File

@ -12,6 +12,7 @@ class DropDown {
this.menu = this.$refs.menu; this.menu = this.$refs.menu;
this.toggle = this.$refs.toggle; this.toggle = this.$refs.toggle;
this.moveMenu = this.$opts.moveMenu; this.moveMenu = this.$opts.moveMenu;
this.bubbleEscapes = this.$opts.bubbleEscapes === 'true';
this.direction = (document.dir === 'rtl') ? 'right' : 'left'; this.direction = (document.dir === 'rtl') ? 'right' : 'left';
this.body = document.body; this.body = document.body;
@ -137,7 +138,9 @@ class DropDown {
} else if (event.key === 'Escape') { } else if (event.key === 'Escape') {
this.hide(); this.hide();
this.toggle.focus(); this.toggle.focus();
event.stopPropagation(); if (!this.bubbleEscapes) {
event.stopPropagation();
}
} }
}; };
this.container.addEventListener('keydown', keyboardNavigation); this.container.addEventListener('keydown', keyboardNavigation);

View File

@ -1,31 +1,41 @@
class HeaderMobileToggle { class HeaderMobileToggle {
constructor(elem) { setup() {
this.elem = elem; this.elem = this.$el;
this.toggleButton = elem.querySelector('.mobile-menu-toggle'); this.toggleButton = this.$refs.toggle;
this.menu = elem.querySelector('.header-links'); this.menu = this.$refs.menu;
this.open = false;
this.open = false;
this.toggleButton.addEventListener('click', this.onToggle.bind(this)); this.toggleButton.addEventListener('click', this.onToggle.bind(this));
this.onWindowClick = this.onWindowClick.bind(this); this.onWindowClick = this.onWindowClick.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
} }
onToggle(event) { onToggle(event) {
this.open = !this.open; this.open = !this.open;
this.menu.classList.toggle('show', this.open); this.menu.classList.toggle('show', this.open);
this.toggleButton.setAttribute('aria-expanded', this.open ? 'true' : 'false');
if (this.open) { if (this.open) {
this.elem.addEventListener('keydown', this.onKeyDown);
window.addEventListener('click', this.onWindowClick) window.addEventListener('click', this.onWindowClick)
} else { } else {
this.elem.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('click', this.onWindowClick) window.removeEventListener('click', this.onWindowClick)
} }
event.stopPropagation(); event.stopPropagation();
} }
onKeyDown(event) {
if (event.code === 'Escape') {
this.onToggle(event);
}
}
onWindowClick(event) { onWindowClick(event) {
this.onToggle(event); this.onToggle(event);
} }
} }
module.exports = HeaderMobileToggle; export default HeaderMobileToggle;

View File

@ -1,8 +1,9 @@
class TriLayout { class TriLayout {
constructor(elem) { setup() {
this.elem = elem; this.container = this.$refs.container;
this.tabs = this.$manyRefs.tab;
this.lastLayoutType = 'none'; this.lastLayoutType = 'none';
this.onDestroy = null; this.onDestroy = null;
@ -43,13 +44,12 @@ class TriLayout {
} }
setupMobile() { setupMobile() {
const layoutTabs = document.querySelectorAll('[tri-layout-mobile-tab]'); for (const tab of this.tabs) {
for (let tab of layoutTabs) {
tab.addEventListener('click', this.mobileTabClick); tab.addEventListener('click', this.mobileTabClick);
} }
this.onDestroy = () => { this.onDestroy = () => {
for (let tab of layoutTabs) { for (const tab of this.tabs) {
tab.removeEventListener('click', this.mobileTabClick); tab.removeEventListener('click', this.mobileTabClick);
} }
} }
@ -65,7 +65,7 @@ class TriLayout {
* @param event * @param event
*/ */
mobileTabClick(event) { mobileTabClick(event) {
const tab = event.target.getAttribute('tri-layout-mobile-tab'); const tab = event.target.dataset.tab;
this.showTab(tab); this.showTab(tab);
} }
@ -79,21 +79,21 @@ class TriLayout {
/** /**
* Show the given tab * Show the given tab
* @param tabName * @param {String} tabName
* @param {Boolean }scroll
*/ */
showTab(tabName, scroll = true) { showTab(tabName, scroll = true) {
this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop; this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;
// Set tab status // Set tab status
const tabs = document.querySelectorAll('.tri-layout-mobile-tab'); for (const tab of this.tabs) {
for (let tab of tabs) { const isActive = (tab.dataset.tab === tabName);
const isActive = (tab.getAttribute('tri-layout-mobile-tab') === tabName); tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
tab.classList.toggle('active', isActive);
} }
// Toggle section // Toggle section
const showInfo = (tabName === 'info'); const showInfo = (tabName === 'info');
this.elem.classList.toggle('show-info', showInfo); this.container.classList.toggle('show-info', showInfo);
// Set the scroll position from cache // Set the scroll position from cache
if (scroll) { if (scroll) {

View File

@ -65,6 +65,7 @@ return [
'breadcrumb' => 'Breadcrumb', 'breadcrumb' => 'Breadcrumb',
// Header // Header
'header_menu_expand' => 'Expand Header Menu',
'profile_menu' => 'Profile Menu', 'profile_menu' => 'Profile Menu',
'view_profile' => 'View Profile', 'view_profile' => 'View Profile',
'edit_profile' => 'Edit Profile', 'edit_profile' => 'Edit Profile',
@ -73,7 +74,9 @@ return [
// Layout tabs // Layout tabs
'tab_info' => 'Info', 'tab_info' => 'Info',
'tab_info_label' => 'Tab: Show Secondary Information',
'tab_content' => 'Content', 'tab_content' => 'Content',
'tab_content_label' => 'Tab: Show Primary Content',
// Email Content // Email Content
'email_action_help' => 'If youre having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:', 'email_action_help' => 'If youre having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:',

View File

@ -191,6 +191,11 @@ header .search-box {
@include lightDark(color, #000, #fff); @include lightDark(color, #000, #fff);
text-decoration: none; text-decoration: none;
} }
&:focus {
@include lightDark(background-color, #eee, #333);
outline-color: var(--color-primary);
color: var(--color-primary);
}
} }
header .dropdown-container { header .dropdown-container {
display: block; display: block;
@ -225,7 +230,7 @@ header .search-box {
&:first-child { &:first-child {
border-inline-end: 1px solid #DDD; border-inline-end: 1px solid #DDD;
} }
&.active { &[aria-selected="true"] {
border-bottom-color: currentColor; border-bottom-color: currentColor;
} }
} }

View File

@ -31,7 +31,7 @@
@include('partials.notifications') @include('partials.notifications')
@include('common.header') @include('common.header')
<div id="content" class="block"> <div id="content" components="@yield('content-components')" class="block">
@yield('content') @yield('content')
</div> </div>

View File

@ -1,4 +1,4 @@
<header id="header" header-mobile-toggle class="primary-background"> <header id="header" component="header-mobile-toggle" class="primary-background">
<div class="grid mx-l"> <div class="grid mx-l">
<div> <div>
@ -10,7 +10,11 @@
<span class="logo-text">{{ setting('app-name') }}</span> <span class="logo-text">{{ setting('app-name') }}</span>
@endif @endif
</a> </a>
<div class="mobile-menu-toggle hide-over-l">@icon('more')</div> <button type="button"
refs="header-mobile-toggle@toggle"
title="{{ trans('common.header_menu_expand') }}"
aria-expanded="false"
class="mobile-menu-toggle hide-over-l">@icon('more')</button>
</div> </div>
<div class="flex-container-row justify-center hide-under-l"> <div class="flex-container-row justify-center hide-under-l">
@ -25,7 +29,7 @@
</div> </div>
<div class="text-right"> <div class="text-right">
<nav class="header-links"> <nav refs="header-mobile-toggle@menu" class="header-links">
<div class="links text-center"> <div class="links text-center">
@if (hasAppAccess()) @if (hasAppAccess())
<a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a> <a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
@ -50,7 +54,7 @@
</div> </div>
@if(signedInUser()) @if(signedInUser())
<?php $currentUser = user(); ?> <?php $currentUser = user(); ?>
<div class="dropdown-container" component="dropdown"> <div class="dropdown-container" component="dropdown" option:dropdown:bubble-escapes="true">
<span class="user-name py-s hide-under-l" refs="dropdown@toggle" <span class="user-name py-s hide-under-l" refs="dropdown@toggle"
aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.profile_menu') }}" tabindex="0"> aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.profile_menu') }}" tabindex="0">
<img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}"> <img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">

View File

@ -1,21 +1,31 @@
@extends('base') @extends('base')
@section('body-class', 'tri-layout') @section('body-class', 'tri-layout')
@section('content-components', 'tri-layout')
@section('content') @section('content')
<div class="tri-layout-mobile-tabs text-primary print-hidden"> <div class="tri-layout-mobile-tabs print-hidden">
<div class="grid half no-break no-gap"> <div class="grid half no-break no-gap">
<div class="tri-layout-mobile-tab px-m py-s" tri-layout-mobile-tab="info"> <button type="button"
refs="tri-layout@tab"
data-tab="info"
aria-label="{{ trans('common.tab_info_label') }}"
class="tri-layout-mobile-tab px-m py-m text-primary">
{{ trans('common.tab_info') }} {{ trans('common.tab_info') }}
</div> </button>
<div class="tri-layout-mobile-tab px-m py-s active" tri-layout-mobile-tab="content"> <button type="button"
refs="tri-layout@tab"
data-tab="content"
aria-label="{{ trans('common.tab_content_label') }}"
aria-selected="true"
class="tri-layout-mobile-tab px-m py-m text-primary active">
{{ trans('common.tab_content') }} {{ trans('common.tab_content') }}
</div> </button>
</div> </div>
</div> </div>
<div class="tri-layout-container" tri-layout @yield('container-attrs') > <div refs="tri-layout@container" class="tri-layout-container" @yield('container-attrs') >
<div class="tri-layout-left print-hidden pt-m" id="sidebar"> <div class="tri-layout-left print-hidden pt-m" id="sidebar">
<aside class="tri-layout-left-contents"> <aside class="tri-layout-left-contents">