Skip to content

Commit 04f2329

Browse files
authored
Fix keyboard navigation issues on menu (#86)
* Fix keyboard navigation issues on menu - move menu directory after toggle in DOM order - hide menu from keyboard with CSS when closed - automatically close menu when focus leaves - show focus on section toggles * Please our slithering overlords
1 parent 1f8062b commit 04f2329

File tree

1 file changed

+113
-86
lines changed

1 file changed

+113
-86
lines changed

src/routes/tutorial/[slug]/_/Menu/Menu.svelte

+113-86
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,112 @@
4646
afterNavigate(() => {
4747
search = '';
4848
});
49+
50+
/**
51+
* @param {HTMLElement} node
52+
*/
53+
function close_when_focus_leaves(node) {
54+
function handle_focus_in() {
55+
if (!node.contains(document.activeElement)) {
56+
is_open = false;
57+
}
58+
}
59+
document.addEventListener('focusin', handle_focus_in)
60+
61+
return {
62+
destroy: () => {
63+
document.removeEventListener('focusin', handle_focus_in);
64+
}
65+
}
66+
}
4967
</script>
5068

51-
<div class="menu-toggle-container">
52-
<button
53-
class="menu-toggle"
54-
on:click={() => (is_open = !is_open)}
55-
aria-label="Toggle menu"
56-
aria-expanded={is_open}
57-
>
58-
<Icon name={is_open ? 'close' : 'menu'} />
59-
</button>
69+
<div use:close_when_focus_leaves>
70+
<div class="menu-toggle-container">
71+
<button
72+
class="menu-toggle"
73+
on:click={() => (is_open = !is_open)}
74+
aria-label="Toggle menu"
75+
aria-expanded={is_open}
76+
>
77+
<Icon name={is_open ? 'close' : 'menu'} />
78+
</button>
79+
</div>
80+
81+
<nav class:open={is_open} aria-label="tutorial sections" >
82+
<div class="controls">
83+
<input
84+
type="search"
85+
placeholder="Search"
86+
bind:value={search}
87+
aria-hidden={!is_open ? 'true' : null}
88+
tabindex={!is_open ? -1 : null}
89+
/>
90+
</div>
91+
92+
<div class="sections">
93+
<ul>
94+
{#each filtered as part (part.slug)}
95+
<li
96+
class="part"
97+
class:expanded={part.slug === expanded_part}
98+
aria-current={part.slug === current.part.slug ? 'step' : undefined}
99+
transition:slide|local={{ duration }}
100+
>
101+
<button
102+
on:click={() => {
103+
if (expanded_part !== part.slug) {
104+
expanded_part = part.slug;
105+
expanded_chapter = part.chapters[0].slug;
106+
}
107+
}}
108+
>
109+
Part {part.label}: {part.title}
110+
</button>
111+
112+
{#if search.length >= 2 || part.slug === expanded_part}
113+
<ul class="chapter" transition:slide|local={{ duration }}>
114+
{#each part.chapters as chapter (chapter.slug)}
115+
<li
116+
class="chapter"
117+
class:expanded={chapter.slug === expanded_chapter}
118+
aria-current={chapter.slug === current.chapter.slug ? 'step' : undefined}
119+
>
120+
<img src={arrow} alt="Arrow icon" />
121+
<button on:click={() => (expanded_chapter = chapter.slug)}>
122+
{chapter.title}
123+
</button>
124+
125+
{#if search.length >= 2 || chapter.slug === expanded_chapter}
126+
<ul transition:slide|local={{ duration }}>
127+
{#each chapter.sections as section (section.slug)}
128+
<li
129+
transition:slide|local={{ duration }}
130+
class="section"
131+
aria-current={$page.url.pathname === `/tutorial/${section.slug}`
132+
? 'page'
133+
: undefined}
134+
>
135+
<a
136+
sveltekit:prefetch
137+
href="/tutorial/{section.slug}"
138+
on:click={() => (is_open = false)}
139+
>
140+
{section.title}
141+
</a>
142+
</li>
143+
{/each}
144+
</ul>
145+
{/if}
146+
</li>
147+
{/each}
148+
</ul>
149+
{/if}
150+
</li>
151+
{/each}
152+
</ul>
153+
</div>
154+
</nav>
60155
</div>
61156

62157
<header>
@@ -75,81 +170,6 @@
75170
</a>
76171
</header>
77172

78-
<nav class:open={is_open} aria-label="tutorial sections">
79-
<div class="controls">
80-
<input
81-
type="search"
82-
placeholder="Search"
83-
bind:value={search}
84-
aria-hidden={!is_open ? 'true' : null}
85-
tabindex={!is_open ? -1 : null}
86-
/>
87-
</div>
88-
89-
<div class="sections">
90-
<ul>
91-
{#each filtered as part (part.slug)}
92-
<li
93-
class="part"
94-
class:expanded={part.slug === expanded_part}
95-
aria-current={part.slug === current.part.slug ? 'step' : undefined}
96-
transition:slide|local={{ duration }}
97-
>
98-
<button
99-
on:click={() => {
100-
if (expanded_part !== part.slug) {
101-
expanded_part = part.slug;
102-
expanded_chapter = part.chapters[0].slug;
103-
}
104-
}}
105-
>
106-
Part {part.label}: {part.title}
107-
</button>
108-
109-
{#if search.length >= 2 || part.slug === expanded_part}
110-
<ul class="chapter" transition:slide|local={{ duration }}>
111-
{#each part.chapters as chapter (chapter.slug)}
112-
<li
113-
class="chapter"
114-
class:expanded={chapter.slug === expanded_chapter}
115-
aria-current={chapter.slug === current.chapter.slug ? 'step' : undefined}
116-
>
117-
<img src={arrow} alt="Arrow icon" />
118-
<button on:click={() => (expanded_chapter = chapter.slug)}>
119-
{chapter.title}
120-
</button>
121-
122-
{#if search.length >= 2 || chapter.slug === expanded_chapter}
123-
<ul transition:slide|local={{ duration }}>
124-
{#each chapter.sections as section (section.slug)}
125-
<li
126-
transition:slide|local={{ duration }}
127-
class="section"
128-
aria-current={$page.url.pathname === `/tutorial/${section.slug}`
129-
? 'page'
130-
: undefined}
131-
>
132-
<a
133-
sveltekit:prefetch
134-
href="/tutorial/{section.slug}"
135-
on:click={() => (is_open = false)}
136-
>
137-
{section.title}
138-
</a>
139-
</li>
140-
{/each}
141-
</ul>
142-
{/if}
143-
</li>
144-
{/each}
145-
</ul>
146-
{/if}
147-
</li>
148-
{/each}
149-
</ul>
150-
</div>
151-
</nav>
152-
153173
<style>
154174
header {
155175
display: flex;
@@ -196,21 +216,27 @@
196216
197217
nav {
198218
--menu-width: 5.4rem;
219+
--transform-transition: transform 0.2s;
199220
position: absolute;
200221
width: 100%;
201222
height: 100%;
202-
transition: transform 0.2s;
223+
/* when the nav is closing, wait to change visibility until the slide out completes */
224+
transition: var(--transform-transition), visibility 0s 0.2s;
203225
transform: translate(-100%, 0);
204226
background: var(--light-blue);
205227
z-index: 2;
206228
/* filter: drop-shadow(2px 0 2px rgba(0, 0, 0, 0.1)); */
207229
border-right: 1px solid var(--border-color);
208230
display: flex;
209231
flex-direction: column;
232+
visibility: hidden;
210233
}
211234
212235
nav.open {
213236
transform: none;
237+
visibility: visible;
238+
/* when the nav starts opening, don't transition visibility - set it right away */
239+
transition: var(--transform-transition);
214240
}
215241
216242
.controls {
@@ -325,7 +351,8 @@
325351
box-sizing: border-box;
326352
}
327353
328-
a:focus-visible {
354+
a:focus-visible,
355+
.sections button:focus-visible {
329356
/* outline-color: var(--flash); */
330357
outline: none;
331358
border: 2px solid var(--flash);

0 commit comments

Comments
 (0)