Skip to content

Commit f1c7707

Browse files
committed
feat: enhance header accessibility
Signed-off-by: David Lima <antdavidlima@gmail.com>
1 parent 61ced7d commit f1c7707

4 files changed

Lines changed: 200 additions & 35 deletions

File tree

source/_assets/js/main.js

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,14 @@ require('aos/dist/aos.css');
5656
e.addEventListener("click", () => {
5757
navbarToggler.classList.remove("active");
5858
navbarCollapse.classList.remove("show");
59+
navbarToggler.setAttribute("aria-expanded", "false");
5960
})
6061
);
62+
6163
navbarToggler.addEventListener("click", function () {
62-
navbarToggler.classList.toggle("active");
64+
const isExpanded = navbarToggler.classList.toggle("active");
6365
navbarCollapse.classList.toggle("show");
66+
navbarToggler.setAttribute("aria-expanded", isExpanded ? "true" : "false");
6467
});
6568

6669
// ===== submenu
@@ -74,8 +77,70 @@ require('aos/dist/aos.css');
7477
// ===== selector
7578
const selectorButton = document.querySelectorAll(".selector");
7679
selectorButton.forEach((elem) => {
77-
elem.querySelector("button").addEventListener("click", () => {
78-
elem.querySelector(".ud-submenu").classList.toggle("show");
80+
const button = elem.querySelector("button");
81+
const submenu = elem.querySelector(".ud-submenu");
82+
83+
button.addEventListener("click", () => {
84+
const isExpanded = submenu.classList.toggle("show");
85+
button.setAttribute("aria-expanded", isExpanded ? "true" : "false");
86+
87+
// Focus first menu item when opened
88+
if (isExpanded) {
89+
const firstLink = submenu.querySelector("a");
90+
if (firstLink) {
91+
setTimeout(() => firstLink.focus(), 50);
92+
}
93+
}
94+
});
95+
96+
button.addEventListener("keydown", (e) => {
97+
if (e.key === "Enter" || e.key === " ") {
98+
e.preventDefault();
99+
button.click();
100+
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
101+
e.preventDefault();
102+
if (!submenu.classList.contains("show")) {
103+
button.click();
104+
}
105+
} else if (e.key === "Escape") {
106+
submenu.classList.remove("show");
107+
button.setAttribute("aria-expanded", "false");
108+
}
109+
});
110+
111+
const menuLinks = submenu.querySelectorAll("a");
112+
menuLinks.forEach((link, index) => {
113+
link.addEventListener("keydown", (e) => {
114+
if (e.key === "ArrowDown") {
115+
e.preventDefault();
116+
const nextLink = menuLinks[index + 1] || menuLinks[0];
117+
nextLink.focus();
118+
} else if (e.key === "ArrowUp") {
119+
e.preventDefault();
120+
const prevLink = menuLinks[index - 1] || menuLinks[menuLinks.length - 1];
121+
prevLink.focus();
122+
} else if (e.key === "Escape") {
123+
e.preventDefault();
124+
submenu.classList.remove("show");
125+
button.setAttribute("aria-expanded", "false");
126+
button.focus();
127+
} else if (e.key === "Tab" && !e.shiftKey && index === menuLinks.length - 1) {
128+
// Close menu when tabbing out of last item
129+
submenu.classList.remove("show");
130+
button.setAttribute("aria-expanded", "false");
131+
} else if (e.key === "Tab" && e.shiftKey && index === 0) {
132+
// Close menu when shift-tabbing out of first item
133+
submenu.classList.remove("show");
134+
button.setAttribute("aria-expanded", "false");
135+
}
136+
});
137+
});
138+
139+
document.addEventListener("click", (e) => {
140+
if (!elem.contains(e.target)) {
141+
submenu.classList.remove("show");
142+
button.setAttribute("aria-expanded", "false");
143+
}
79144
});
80145
});
81146

source/_assets/scss/_header.scss

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,35 @@
11
/* ===== Header CSS ===== */
2+
.skip-to-content {
3+
position: absolute;
4+
left: -9999px;
5+
top: 0;
6+
z-index: 999;
7+
padding: 12px 24px;
8+
background-color: #00a3be;
9+
color: #263238;
10+
font-weight: 600;
11+
text-decoration: none;
12+
border-radius: 0 0 8px 0;
13+
14+
&:focus {
15+
left: 0;
16+
outline: 3px solid #00dbff;
17+
outline-offset: 2px;
18+
}
19+
}
20+
21+
.visually-hidden {
22+
position: absolute;
23+
width: 1px;
24+
height: 1px;
25+
padding: 0;
26+
margin: -1px;
27+
overflow: hidden;
28+
clip: rect(0, 0, 0, 0);
29+
white-space: nowrap;
30+
border-width: 0;
31+
}
32+
233
.ud-header {
334
position: fixed;
435
top: 0;
@@ -16,6 +47,12 @@
1647
.navbar-brand {
1748
padding: 0;
1849

50+
&:focus {
51+
outline: 3px solid #00dbff;
52+
outline-offset: 4px;
53+
border-radius: 4px;
54+
}
55+
1956
img {
2057
max-height: 54px;
2158

@@ -54,6 +91,12 @@
5491
padding: 12px 0;
5592
position: relative;
5693

94+
&:focus {
95+
outline: 3px solid #00dbff;
96+
outline-offset: 4px;
97+
border-radius: 4px;
98+
}
99+
57100
&:hover:not(.link-button),
58101
&.active:not(.link-button) {
59102
color: var(--white);
@@ -69,10 +112,9 @@
69112
}
70113

71114
&.nav-item-has-children {
72-
margin-right: 24px;
73-
74115
& > a {
75116
position: relative;
117+
padding-right: 20px;
76118

77119
&::after {
78120
content: "";
@@ -83,7 +125,7 @@
83125
border-right: 7px solid transparent;
84126
border-top: 7px solid white;
85127
top: 50%;
86-
right: -20px;
128+
right: 0;
87129
transform: translateY(-50%);
88130
}
89131
}
@@ -104,6 +146,11 @@
104146
padding: 0 16px;
105147
}
106148

149+
&:focus {
150+
outline: 3px solid #00dbff;
151+
outline-offset: 4px;
152+
}
153+
107154
&:hover {
108155
color: #263238;
109156
filter: brightness(1.25);
@@ -182,6 +229,14 @@
182229

183230
.navbar-toggler {
184231
padding: 0;
232+
border: none;
233+
background: transparent;
234+
235+
&:focus {
236+
outline: 3px solid #00dbff;
237+
outline-offset: 4px;
238+
border-radius: 4px;
239+
}
185240

186241
& .toggler-icon {
187242
width: 30px;
@@ -221,7 +276,7 @@
221276
& > .ud-submenu {
222277
opacity: 1;
223278
visibility: visible;
224-
top: 125%;
279+
top: 100%;
225280
}
226281
}
227282

@@ -233,6 +288,13 @@
233288
align-items: center;
234289
gap: 8px;
235290
padding-right: 16px;
291+
cursor: pointer;
292+
293+
&:focus {
294+
outline: 3px solid #00dbff;
295+
outline-offset: 4px;
296+
border-radius: 4px;
297+
}
236298

237299
img {
238300
width: 24px;
@@ -265,10 +327,13 @@
265327
border-bottom: 8px solid #00abcb;
266328
}
267329

330+
z-index: 100;
331+
268332
position: absolute;
269333
width: 128px;
270334
background: var(--white);
271335
top: 100%;
336+
margin-top: 16px;
272337
padding: 8px 16px;
273338
box-shadow: 0 15px 44px rgba(140, 140, 140, 0.18);
274339
border-radius: 8px;
@@ -281,9 +346,20 @@
281346
display: flex;
282347
flex-direction: column;
283348
gap: 8px;
349+
350+
&::after {
351+
content: '';
352+
position: absolute;
353+
top: -16px;
354+
left: 0;
355+
right: 0;
356+
height: 16px;
357+
}
284358

285359
&.show {
286-
display: flex;
360+
opacity: 1;
361+
visibility: visible;
362+
top: 100%;
287363
}
288364

289365
.ud-submenu-link {
@@ -293,6 +369,12 @@
293369
align-items: center;
294370
gap: 8px;
295371

372+
&:focus {
373+
outline: 2px solid #00a3be;
374+
outline-offset: 2px;
375+
border-radius: 4px;
376+
}
377+
296378
img {
297379
width: 24px;
298380
aspect-ratio: 1/1;

source/_layouts/header.blade.php

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
1+
<a href="#main-content" class="skip-to-content">{{ $page->t("Skip to main content") }}</a>
12
<header class="ud-header">
23
<div class="container">
3-
<nav class="navbar navbar-expand-lg">
4-
<a class="navbar-brand" href="{{ locale_url($page, '') }}">
4+
<nav class="navbar navbar-expand-lg" role="navigation" aria-label="{{ $page->t('Main navigation') }}">
5+
<a class="navbar-brand" href="{{ locale_url($page, '') }}" aria-label="{{ $page->t('LibreSign home') }}">
56
<img src="{{ $page->baseUrl }}assets/images/logo/logo.svg" alt="LibreSign" />
67
</a>
7-
<div class="navbar-collapse mx-auto">
8-
<ul class="navbar-nav container">
9-
<li class="nav-item nav-item-has-children">
10-
<a class="ud-menu-scroll" href="{{ locale_url($page, 'solutions') }}">{{$page->t("Solutions")}}</a>
8+
<div class="navbar-collapse mx-auto" id="main-navigation">
9+
<ul class="navbar-nav container" role="menubar">
10+
<li class="nav-item nav-item-has-children" role="none">
11+
<a class="ud-menu-scroll" href="{{ locale_url($page, 'solutions') }}" role="menuitem">{{$page->t("Solutions")}}</a>
1112
</li>
12-
<li class="nav-item nav-item-has-children">
13-
<a class="ud-menu-scroll" href="{{ locale_url($page, 'features') }}">{{$page->t("Features")}}</a>
13+
<li class="nav-item nav-item-has-children" role="none">
14+
<a class="ud-menu-scroll" href="{{ locale_url($page, 'features') }}" role="menuitem">{{$page->t("Features")}}</a>
1415
</li>
15-
<li class="nav-item nav-item-has-children"><a class="ud-menu-scroll" href="{{ locale_url($page, 'pricing') }}">{{ $page->t('Plans and Pricing')}}</a>
16+
<li class="nav-item nav-item-has-children" role="none">
17+
<a class="ud-menu-scroll" href="{{ locale_url($page, 'pricing') }}" role="menuitem">{{ $page->t('Plans and Pricing')}}</a>
1618
</li>
17-
<li class="nav-item">
18-
<a class="ud-menu-scroll" href="{{ locale_url($page, 'about') }}">{{ $page->t("About") }}</a>
19+
<li class="nav-item" role="none">
20+
<a class="ud-menu-scroll" href="{{ locale_url($page, 'about') }}" role="menuitem">{{ $page->t("About") }}</a>
1921
</li>
20-
<li class="nav-item nav-item-has-children">
21-
<a class="ud-menu-scroll" href="{{ locale_url($page, 'posts') }}">{{ $page->t("Content") }}</a>
22+
<li class="nav-item nav-item-has-children" role="none">
23+
<a class="ud-menu-scroll" href="{{ locale_url($page, 'posts') }}" role="menuitem">{{ $page->t("Content") }}</a>
2224
</li>
23-
<li class="nav-item nav-item-has-children">
24-
<a class="ud-menu-scroll" href="{{ locale_url($page, 'contact') }}">{{ $page->t("Contact") }}</a>
25+
<li class="nav-item nav-item-has-children" role="none">
26+
<a class="ud-menu-scroll" href="{{ locale_url($page, 'contact') }}" role="menuitem">{{ $page->t("Contact") }}</a>
2527
</li>
26-
<li class="nav-item" id="customer-area-link">
27-
<a class="ud-menu-scroll link-button" href="{{ locale_url($page, 'client-area') }}" aria-label="{{ $page->t("Client Area") }}">
28+
<li class="nav-item" id="customer-area-link" role="none">
29+
<a class="ud-menu-scroll link-button" href="{{ locale_url($page, 'client-area') }}" role="menuitem" aria-label="{{ $page->t("Client Area") }}">
2830
<span class="button-text">{{ $page->t("Client Area") }}</span>
2931
<i class="lni lni-user button-icon" aria-hidden="true"></i>
3032
</a>
@@ -33,22 +35,36 @@
3335
</div>
3436
<div class="d-flex align-items-center">
3537
<div class="selector">
36-
<button type="button"><img src="{{ $page->baseUrl }}assets/images/icon/languages/{{ current_path_locale($page) ?: 'en' }}.svg" alt="{{ $page->t('Language') }}" /></button>
37-
<ul class="ud-submenu">
38+
<button type="button"
39+
aria-label="{{ $page->t('Select language') }}"
40+
aria-haspopup="true"
41+
aria-expanded="false"
42+
aria-controls="language-menu">
43+
<img src="{{ $page->baseUrl }}assets/images/icon/languages/{{ current_path_locale($page) ?: 'en' }}.svg" alt="" role="presentation" />
44+
<span class="visually-hidden">{{ $page->t('Current language') }}: {{ $page->locales[current_path_locale($page)] ?? 'English' }}</span>
45+
</button>
46+
<ul class="ud-submenu" id="language-menu" role="menu" aria-label="{{ $page->t('Language selection') }}">
3847
@foreach($page->locales as $localeCode => $localeName)
39-
<li class="ud-submenu-item">
40-
<a class="ud-submenu-link" href="{{ translate_url($page, $localeCode) }}">
41-
<img src="{{ $page->baseUrl }}assets/images/icon/languages/{{ $localeCode ?: 'en' }}.svg" alt="{{ $localeName }}" />
48+
<li class="ud-submenu-item" role="none">
49+
<a class="ud-submenu-link"
50+
href="{{ translate_url($page, $localeCode) }}"
51+
role="menuitem"
52+
lang="{{ $localeCode ?: 'en' }}"
53+
aria-label="{{ $page->t('Switch to') }} {{ $localeName }}">
54+
<img src="{{ $page->baseUrl }}assets/images/icon/languages/{{ $localeCode ?: 'en' }}.svg" alt="" role="presentation" />
4255
<span>{{ $localeCode ?: 'en'}}</span>
4356
</a>
4457
</li>
4558
@endforeach
4659
</ul>
4760
</div>
48-
<button class="navbar-toggler" title="{{$page->t("Toggle navigation menu")}}">
49-
<span class="toggler-icon"> </span>
50-
<span class="toggler-icon"> </span>
51-
<span class="toggler-icon"> </span>
61+
<button class="navbar-toggler"
62+
aria-label="{{$page->t("Toggle navigation menu")}}"
63+
aria-expanded="false"
64+
aria-controls="main-navigation">
65+
<span class="toggler-icon" aria-hidden="true"> </span>
66+
<span class="toggler-icon" aria-hidden="true"> </span>
67+
<span class="toggler-icon" aria-hidden="true"> </span>
5268
</button>
5369
</div>
5470
</nav>

source/_layouts/main.blade.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ function onScroll(event) {
124124
</head>
125125
<body>
126126
@include('_layouts.header')
127-
@yield('body')
127+
<main id="main-content">
128+
@yield('body')
129+
</main>
128130
@include('_layouts.footer')
129131
</body>
130132
</html>

0 commit comments

Comments
 (0)