|
46 | 46 | afterNavigate(() => {
|
47 | 47 | search = '';
|
48 | 48 | });
|
| 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 | + } |
49 | 67 | </script>
|
50 | 68 |
|
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> |
60 | 155 | </div>
|
61 | 156 |
|
62 | 157 | <header>
|
|
75 | 170 | </a>
|
76 | 171 | </header>
|
77 | 172 |
|
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 |
| - |
153 | 173 | <style>
|
154 | 174 | header {
|
155 | 175 | display: flex;
|
|
196 | 216 |
|
197 | 217 | nav {
|
198 | 218 | --menu-width: 5.4rem;
|
| 219 | + --transform-transition: transform 0.2s; |
199 | 220 | position: absolute;
|
200 | 221 | width: 100%;
|
201 | 222 | 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; |
203 | 225 | transform: translate(-100%, 0);
|
204 | 226 | background: var(--light-blue);
|
205 | 227 | z-index: 2;
|
206 | 228 | /* filter: drop-shadow(2px 0 2px rgba(0, 0, 0, 0.1)); */
|
207 | 229 | border-right: 1px solid var(--border-color);
|
208 | 230 | display: flex;
|
209 | 231 | flex-direction: column;
|
| 232 | + visibility: hidden; |
210 | 233 | }
|
211 | 234 |
|
212 | 235 | nav.open {
|
213 | 236 | transform: none;
|
| 237 | + visibility: visible; |
| 238 | + /* when the nav starts opening, don't transition visibility - set it right away */ |
| 239 | + transition: var(--transform-transition); |
214 | 240 | }
|
215 | 241 |
|
216 | 242 | .controls {
|
|
325 | 351 | box-sizing: border-box;
|
326 | 352 | }
|
327 | 353 |
|
328 |
| - a:focus-visible { |
| 354 | + a:focus-visible, |
| 355 | + .sections button:focus-visible { |
329 | 356 | /* outline-color: var(--flash); */
|
330 | 357 | outline: none;
|
331 | 358 | border: 2px solid var(--flash);
|
|
0 commit comments