mirror of https://github.com/sveltejs/svelte
- Added a new package called svelte-outclick - Replaced the select box with a new custom menu - Paid attention to the menu accessibility -- hover -- focus-within -- handle click(toggle) -- outclick -- inner click -- selected item auto position correction -- disabled user-select and user-dragpull/6851/head
parent
cd7307e2da
commit
106a64ea4f
@ -1,94 +1,225 @@
|
|||||||
<script>
|
<script>
|
||||||
import { goto } from '@sapper/app';
|
import { goto } from '@sapper/app';
|
||||||
import { Icon } from '@sveltejs/site-kit';
|
import { Icon } from '@sveltejs/site-kit';
|
||||||
|
import OutClick from 'svelte-outclick';
|
||||||
|
|
||||||
export let sections;
|
export let sections;
|
||||||
export let slug;
|
|
||||||
export let selected;
|
export let selected;
|
||||||
|
|
||||||
function navigate(e) {
|
let isMenuOpen = false;
|
||||||
goto(`tutorial/${e.target.value}`);
|
let menuToggle;
|
||||||
|
|
||||||
|
// Locate scrollbar at first direct url navigation to make the active menu item visible.
|
||||||
|
let selectedMenuItem;
|
||||||
|
const toggleMenu =_=> {
|
||||||
|
isMenuOpen = !isMenuOpen;
|
||||||
|
// Menu position autocorrects to show the selected tutorial item at the top.
|
||||||
|
if (isMenuOpen) selectedMenuItem.scrollIntoView();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
nav {
|
nav {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2.5em 1fr 2.5em;
|
grid-template-columns: 54px 1fr 54px;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
height: 54px;
|
||||||
|
border-bottom: 1px solid rgb(255, 255, 255, .1);
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
.nav-handle {
|
||||||
position: relative;
|
display: flex;
|
||||||
padding: 1em 0.5em;
|
align-items: center;
|
||||||
font-weight: 300;
|
justify-content: center;
|
||||||
font-size: var(--h6);
|
width: 54px;
|
||||||
color: white;
|
color: white;
|
||||||
|
opacity: .75;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.nav-handle:hover,
|
||||||
display: block;
|
.nav-handle:focus {
|
||||||
padding: 0.7em 0;
|
opacity: 1;
|
||||||
text-align: center;
|
|
||||||
opacity: 0.75;
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
.nav-handle.disabled {
|
||||||
|
color: white !important;
|
||||||
|
opacity: 0.3 !important;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-middle {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle:hover .nav-menu-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.disabled, a.disabled:hover, a.disabled:active {
|
.nav-menu-icon {
|
||||||
color: white;
|
width: 30px;
|
||||||
opacity: 0.3;
|
min-width: 30px;
|
||||||
|
height: 29px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid rgb(255, 255, 255, .2);
|
||||||
|
border-radius: 99px;
|
||||||
|
opacity: .75;
|
||||||
|
color: white;
|
||||||
|
transition: 200ms ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
.nav-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: relative;
|
|
||||||
top: 0.3em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
strong { opacity: 0.7 }
|
.nav-section-title {
|
||||||
|
opacity: .75;
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
.nav-title-divider {
|
||||||
position: absolute;
|
opacity: .3;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-chapter-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 1px);
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
right: 0;
|
||||||
width: 100%;
|
height: 0;
|
||||||
height: 100%;
|
max-height: 50vh;
|
||||||
opacity: 0.0001;
|
padding: 0;
|
||||||
cursor: pointer;
|
overflow: hidden;
|
||||||
-webkit-appearance: none;
|
z-index: 1;
|
||||||
|
border-radius: 0 0 2px 6px;
|
||||||
|
background: var(--second);
|
||||||
|
box-shadow: 4px 16px 32px rgb(80, 80, 94);
|
||||||
|
transition: 100ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO */
|
||||||
|
.menu--open,
|
||||||
|
.menu:focus-within {
|
||||||
|
height: 300px !important;
|
||||||
|
padding-top: 8px !important;
|
||||||
|
padding-bottom: 8px !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu::-webkit-scrollbar,
|
||||||
|
.menu::-webkit-scrollbar-track,
|
||||||
|
.menu::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.menu::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.menu::-webkit-scrollbar-track {
|
||||||
|
background: rgb(255, 255, 255, .2);
|
||||||
|
}
|
||||||
|
.menu::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(255, 255, 255, .4);
|
||||||
|
}
|
||||||
|
.menu::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgb(255, 255, 255, .6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu a {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border: unset !important;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu a:hover,
|
||||||
|
.menu a:focus {
|
||||||
|
background: rgb(255, 255, 255, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu a.selected {
|
||||||
|
position: relative;
|
||||||
|
color: white !important;
|
||||||
|
background: rgb(255, 255, 255, .1);
|
||||||
|
border-left: 2px solid rgb(255, 255, 255, .75) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu ul a {
|
||||||
|
padding-left: 32px !important;
|
||||||
|
font-weight: 400;
|
||||||
|
color: rgb(255, 255, 255, .75);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<a sapper:prefetch aria-label="Previous tutorial step" class="no-underline" href="tutorial/{(selected.prev || selected).slug}" class:disabled={!selected.prev}>
|
<a class="nav-handle no-underline" class:disabled={!selected.prev} sapper:prefetch aria-label="Previous tutorial step" href="tutorial/{(selected.prev || selected).slug}">
|
||||||
<Icon name="arrow-left" />
|
<Icon name="arrow-left" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div>
|
<div class="nav-middle">
|
||||||
<span>
|
<!-- I didn't use <button> because we don't want its extra focus. Users can directly focus on the first menu item and it will open the menu because of the :focus-within. -->
|
||||||
<strong>
|
<div class="menu-toggle" on:click={toggleMenu} bind:this={menuToggle} role="button">
|
||||||
<span style="position: relative; top: -0.1em; margin: 0 0.5em 0 0"><Icon name="menu"/></span>
|
<div class="nav-menu-icon">
|
||||||
{selected.section.title} /
|
<Icon name="menu" size="16" />
|
||||||
</strong>
|
</div>
|
||||||
{selected.chapter.title}
|
|
||||||
</span>
|
<div class="nav-title">
|
||||||
|
<span class="nav-section-title">{selected.section.title}</span>
|
||||||
<select value={slug} on:change={navigate}>
|
<span class="nav-title-divider">:</span>
|
||||||
{#each sections as section, i}
|
<span class="nav-chapter-title">{selected.chapter.title}</span>
|
||||||
<optgroup label="{i + 1}. {section.title}">
|
</div>
|
||||||
{#each section.chapters as chapter, i}
|
</div>
|
||||||
<option value={chapter.slug}>{String.fromCharCode(i + 97)}. {chapter.title}</option>
|
|
||||||
{/each}
|
<OutClick on:outclick={_=> isMenuOpen = false} exclude={[menuToggle]} includeSelf={true}>
|
||||||
</optgroup>
|
<ul class="menu" class:menu--open={isMenuOpen}>
|
||||||
{/each}
|
{#each sections as section, i}
|
||||||
</select>
|
<li>
|
||||||
|
<a href="tutorial/{section.chapters[0].slug}">{section.title}</a>
|
||||||
|
<ul>
|
||||||
|
{#each section.chapters as chapter, i}
|
||||||
|
{#if selected.slug === chapter.slug}
|
||||||
|
<li><a class="selected" bind:this={selectedMenuItem} href="tutorial/{chapter.slug}">
|
||||||
|
{chapter.title}
|
||||||
|
</a></li>
|
||||||
|
{:else}
|
||||||
|
<li><a href="tutorial/{chapter.slug}">
|
||||||
|
{chapter.title}
|
||||||
|
</a></li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</OutClick>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a sapper:prefetch aria-label="Next tutorial step" class="no-underline" href="tutorial/{(selected.next || selected).slug}" class:disabled={!selected.next}>
|
<a class="nav-handle no-underline" class:disabled={!selected.next} sapper:prefetch aria-label="Next tutorial step" href="tutorial/{(selected.next || selected).slug}">
|
||||||
<Icon name="arrow-right" />
|
<Icon name="arrow-right" />
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Loading…
Reference in new issue