|
1 | 1 | <script lang="ts"> |
2 | 2 | import { browser } from '$app/environment' |
3 | | - import { afterNavigate, goto } from '$app/navigation' |
| 3 | + import { afterNavigate } from '$app/navigation' |
4 | 4 | import Footer from '$lib/components/footer.svelte' |
5 | 5 | import MinecraftIcon from '$lib/components/minecraftIcon.svelte' |
6 | 6 | import { getLanguageName, localizeHref, SUPPORTED_LANGUAGES } from '$lib/docs/docs' |
|
25 | 25 | document.documentElement.lang = lang |
26 | 26 | }) |
27 | 27 |
|
28 | | - function switchLanguage(event: Event & { currentTarget: HTMLSelectElement }): void { |
29 | | - void goto(localizeHref(data.currentPath, event.currentTarget.value) + window.location.hash) |
30 | | - } |
31 | 28 |
|
32 | 29 | const pageTitle = $derived( |
33 | 30 | data.currentDoc?.title ? `${data.currentDoc.title} | ${docsTitle}` : docsTitle |
|
41 | 38 | let activeHeadingId = $state('') |
42 | 39 | let isMobileNavOpen = $state(false) |
43 | 40 | let isMobileSidebarOpen = $state(false) |
| 41 | + let isLanguageMenuOpen = $state(false) |
| 42 | + let languageMenu = $state<HTMLDivElement>() |
44 | 43 | let headingObserver: IntersectionObserver | null = null |
45 | 44 | let observedTargets: HTMLElement[] = [] |
46 | 45 |
|
|
151 | 150 | isMobileNavOpen = false |
152 | 151 | } |
153 | 152 |
|
| 153 | + function toggleLanguageMenu(): void { |
| 154 | + isLanguageMenuOpen = !isLanguageMenuOpen |
| 155 | + } |
| 156 | +
|
| 157 | + function closeLanguageMenu(): void { |
| 158 | + isLanguageMenuOpen = false |
| 159 | + } |
| 160 | +
|
| 161 | + function handleWindowClick(event: MouseEvent): void { |
| 162 | + if (event.target instanceof Node && !languageMenu?.contains(event.target)) { |
| 163 | + closeLanguageMenu() |
| 164 | + } |
| 165 | + } |
| 166 | +
|
| 167 | + function handleWindowKeydown(event: KeyboardEvent): void { |
| 168 | + if (event.key !== 'Escape' || !isLanguageMenuOpen) return |
| 169 | + closeLanguageMenu() |
| 170 | + languageMenu?.querySelector('button')?.focus() |
| 171 | + } |
| 172 | +
|
154 | 173 | function toggleMobileSidebar(): void { |
155 | 174 | isMobileSidebarOpen = !isMobileSidebarOpen |
156 | 175 | } |
|
176 | 195 | afterNavigate(() => { |
177 | 196 | closeMobileNav() |
178 | 197 | closeMobileSidebar() |
| 198 | + closeLanguageMenu() |
179 | 199 | void refreshToc() |
180 | 200 | }) |
181 | 201 |
|
|
213 | 233 | <meta name="twitter:image" content={socialImage} /> |
214 | 234 | </svelte:head> |
215 | 235 |
|
| 236 | +<svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} /> |
| 237 | + |
216 | 238 | <div class="docs-shell"> |
217 | 239 | <header class="docs-header minecraft-box"> |
218 | 240 | <a href="/" class="brand-link"> |
|
251 | 273 | {link.title} |
252 | 274 | </a> |
253 | 275 | {/each} |
254 | | - <!-- A select can't show an icon, so an invisible one covers this button --> |
255 | | - <label class="language-select minecraft-button"> |
256 | | - <MinecraftIcon path="icons/current/lang_file.svg" /> |
257 | | - {getLanguageName(lang)} |
258 | | - <select aria-label="Language" value={lang} onchange={switchLanguage}> |
| 276 | + <div class="language-menu" bind:this={languageMenu}> |
| 277 | + <button |
| 278 | + type="button" |
| 279 | + class="language-toggle minecraft-button" |
| 280 | + aria-label="Language: {getLanguageName(lang)}" |
| 281 | + aria-controls="language-list" |
| 282 | + aria-expanded={isLanguageMenuOpen} |
| 283 | + onclick={toggleLanguageMenu} |
| 284 | + > |
| 285 | + <MinecraftIcon path="icons/current/lang_file.svg" /> |
| 286 | + {getLanguageName(lang)} |
| 287 | + </button> |
| 288 | + <!-- Hidden instead of removed, so the prerendered page still links every language --> |
| 289 | + <ul |
| 290 | + id="language-list" |
| 291 | + class="language-list minecraft-box" |
| 292 | + hidden={!isLanguageMenuOpen} |
| 293 | + > |
259 | 294 | {#each SUPPORTED_LANGUAGES as option} |
260 | | - <option value={option} lang={option}>{getLanguageName(option)}</option> |
| 295 | + <li> |
| 296 | + <a |
| 297 | + class:active={option === lang} |
| 298 | + aria-current={option === lang ? 'page' : undefined} |
| 299 | + href={localizeHref(data.currentPath, option)} |
| 300 | + hreflang={option} |
| 301 | + lang={option} |
| 302 | + > |
| 303 | + {getLanguageName(option)} |
| 304 | + |
| 305 | + {#if option === lang} |
| 306 | + <i class="minecraft-left-arrow"></i> |
| 307 | + {/if} |
| 308 | + </a> |
| 309 | + </li> |
261 | 310 | {/each} |
262 | | - </select> |
263 | | - </label> |
| 311 | + </ul> |
| 312 | + </div> |
264 | 313 | </nav> |
265 | 314 | </header> |
266 | 315 |
|
|
409 | 458 | justify-content: flex-end; |
410 | 459 | } |
411 | 460 |
|
412 | | - .language-select { |
| 461 | + .language-menu { |
413 | 462 | position: relative; |
| 463 | + } |
| 464 | +
|
| 465 | + .language-toggle { |
414 | 466 | font-family: var(--minecraft-font); |
415 | 467 | font-size: var(--font-size-small); |
416 | | - color: var(--minecraft-button-text-color); |
417 | | - cursor: pointer; |
418 | 468 | } |
419 | 469 |
|
420 | | - .language-select select { |
| 470 | + .language-toggle:focus-visible { |
| 471 | + outline: 2px solid #00aced; |
| 472 | + outline-offset: 6px; |
| 473 | + } |
| 474 | +
|
| 475 | + /* Stay pressed while the menu is open */ |
| 476 | + .language-toggle[aria-expanded='true'] { |
| 477 | + /* prettier-ignore */ |
| 478 | + box-shadow: |
| 479 | + 0 0 0 4px var(--minecraft-button-inset-border-color) inset, |
| 480 | + 0 0 0 4px var(--minecraft-border-color); |
| 481 | + padding: calc(4px + 8px) 16px calc(8px + 4px) 16px; |
| 482 | + margin-top: 12px; |
| 483 | + } |
| 484 | +
|
| 485 | + .language-list { |
421 | 486 | position: absolute; |
422 | | - inset: 0; |
423 | | - width: 100%; |
424 | | - height: 100%; |
425 | | - opacity: 0; |
426 | | - cursor: pointer; |
| 487 | + top: calc(100% + 16px); |
| 488 | + right: 0; |
| 489 | + z-index: 10; |
| 490 | + min-width: 100%; |
| 491 | + box-sizing: border-box; |
| 492 | + margin: 0; |
| 493 | + padding: 12px 0; |
| 494 | + gap: 2px; |
427 | 495 | } |
428 | 496 |
|
429 | | - .language-select:focus-within { |
430 | | - outline: 2px solid #00aced; |
431 | | - outline-offset: 2px; |
| 497 | + .language-list[hidden] { |
| 498 | + display: none; |
| 499 | + } |
| 500 | +
|
| 501 | + .language-list a { |
| 502 | + display: flex; |
| 503 | + justify-content: space-between; |
| 504 | + gap: 16px; |
| 505 | + padding: 6px 20px; |
| 506 | + /* Scripts the Minecraft font lacks fall back to taller fonts, so pin the rows */ |
| 507 | + line-height: 24px; |
| 508 | + } |
| 509 | +
|
| 510 | + .language-list a:not(.active):hover { |
| 511 | + color: var(--minecraft-text-color); |
| 512 | + background-color: #ffffff14; |
432 | 513 | } |
433 | 514 |
|
434 | 515 | .docs-nav-toggle { |
|
438 | 519 | line-height: 1; |
439 | 520 | } |
440 | 521 |
|
441 | | - .docs-nav a { |
| 522 | + .docs-nav > a { |
442 | 523 | font-family: var(--minecraft-font); |
443 | 524 | font-size: var(--font-size-small); |
444 | 525 | text-decoration: none; |
445 | 526 | color: var(--minecraft-button-text-color); |
446 | 527 | } |
447 | 528 |
|
448 | | - .docs-nav a.active { |
| 529 | + .docs-nav > a.active { |
449 | 530 | color: var(--minecraft-text-color); |
450 | 531 | background-color: #00aced; |
451 | 532 | /* prettier-ignore */ |
|
456 | 537 | margin-top: 12px; |
457 | 538 | } |
458 | 539 |
|
459 | | - .docs-nav a.active::after { |
| 540 | + .docs-nav > a.active::after { |
460 | 541 | content: ''; |
461 | 542 | display: block; |
462 | 543 | position: relative; |
|
669 | 750 | ); |
670 | 751 | } |
671 | 752 |
|
672 | | - .docs-nav.open a, |
673 | | - .docs-nav.open .language-select { |
| 753 | + .docs-nav.open > a, |
| 754 | + .docs-nav.open .language-toggle { |
674 | 755 | display: flex; |
675 | 756 | flex-direction: column; |
676 | 757 | align-items: center; |
|
681 | 762 | white-space: nowrap; |
682 | 763 | } |
683 | 764 |
|
684 | | - .docs-nav.open a :global(i), |
685 | | - .docs-nav.open .language-select :global(i) { |
| 765 | + .docs-nav.open > a :global(i), |
| 766 | + .docs-nav.open .language-toggle :global(i) { |
686 | 767 | display: flex; |
687 | 768 | align-items: center; |
688 | 769 | height: 32px; |
689 | 770 | margin: 0; |
690 | 771 | } |
691 | 772 |
|
692 | 773 | /* The pressed button is shorter, so sink it to the row's bottom edge like the desktop header */ |
693 | | - .docs-nav.open a.active { |
| 774 | + .docs-nav.open .language-menu { |
| 775 | + display: grid; |
| 776 | + align-content: end; |
| 777 | + } |
| 778 | +
|
| 779 | + /* Grid cells can sit anywhere in the row, so match the button's width instead of overhanging */ |
| 780 | + .docs-nav.open .language-list { |
| 781 | + left: 0; |
| 782 | + } |
| 783 | +
|
| 784 | + .docs-nav.open > a.active { |
694 | 785 | align-self: end; |
695 | 786 | } |
696 | 787 |
|
697 | | - .docs-nav.open a.active::after { |
| 788 | + .docs-nav.open > a.active::after { |
698 | 789 | display: none; |
699 | 790 | } |
700 | 791 |
|
|
752 | 843 | padding: 8px; |
753 | 844 | } |
754 | 845 |
|
755 | | - .docs-nav a { |
| 846 | + .docs-nav > a { |
756 | 847 | display: block; |
757 | 848 | margin-top: 0; |
758 | 849 | } |
759 | 850 |
|
760 | | - .docs-nav a.active { |
| 851 | + .docs-nav > a.active { |
761 | 852 | margin-top: 0; |
762 | 853 | } |
763 | 854 |
|
764 | | - .docs-nav a.active::after { |
| 855 | + .docs-nav > a.active::after { |
765 | 856 | display: none; |
766 | 857 | } |
767 | 858 |
|
|
0 commit comments