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>
|
||||
import { goto } from '@sapper/app';
|
||||
import { Icon } from '@sveltejs/site-kit';
|
||||
import OutClick from 'svelte-outclick';
|
||||
|
||||
export let sections;
|
||||
export let slug;
|
||||
export let selected;
|
||||
|
||||
function navigate(e) {
|
||||
goto(`tutorial/${e.target.value}`);
|
||||
let isMenuOpen = false;
|
||||
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>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: 2.5em 1fr 2.5em;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
grid-template-columns: 54px 1fr 54px;
|
||||
height: 54px;
|
||||
border-bottom: 1px solid rgb(255, 255, 255, .1);
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
div {
|
||||
position: relative;
|
||||
padding: 1em 0.5em;
|
||||
font-weight: 300;
|
||||
font-size: var(--h6);
|
||||
.nav-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 54px;
|
||||
color: white;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.7em 0;
|
||||
text-align: center;
|
||||
opacity: 0.75;
|
||||
.nav-handle:hover,
|
||||
.nav-handle:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-handle.disabled {
|
||||
color: white !important;
|
||||
opacity: 0.3 !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.nav-middle {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
.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;
|
||||
}
|
||||
|
||||
a.disabled, a.disabled:hover, a.disabled:active {
|
||||
.nav-menu-icon {
|
||||
width: 30px;
|
||||
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;
|
||||
opacity: 0.3;
|
||||
transition: 200ms ease-in;
|
||||
}
|
||||
|
||||
span {
|
||||
.nav-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
top: 0.3em;
|
||||
}
|
||||
|
||||
strong { opacity: 0.7 }
|
||||
.nav-section-title {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.nav-title-divider {
|
||||
opacity: .3;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.nav-chapter-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
select {
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 1px);
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.0001;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
right: 0;
|
||||
height: 0;
|
||||
max-height: 50vh;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
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>
|
||||
|
||||
<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" />
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
<strong>
|
||||
<span style="position: relative; top: -0.1em; margin: 0 0.5em 0 0"><Icon name="menu"/></span>
|
||||
{selected.section.title} /
|
||||
</strong>
|
||||
{selected.chapter.title}
|
||||
</span>
|
||||
<div class="nav-middle">
|
||||
<!-- 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. -->
|
||||
<div class="menu-toggle" on:click={toggleMenu} bind:this={menuToggle} role="button">
|
||||
<div class="nav-menu-icon">
|
||||
<Icon name="menu" size="16" />
|
||||
</div>
|
||||
|
||||
<div class="nav-title">
|
||||
<span class="nav-section-title">{selected.section.title}</span>
|
||||
<span class="nav-title-divider">:</span>
|
||||
<span class="nav-chapter-title">{selected.chapter.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select value={slug} on:change={navigate}>
|
||||
<OutClick on:outclick={_=> isMenuOpen = false} exclude={[menuToggle]} includeSelf={true}>
|
||||
<ul class="menu" class:menu--open={isMenuOpen}>
|
||||
{#each sections as section, i}
|
||||
<optgroup label="{i + 1}. {section.title}">
|
||||
<li>
|
||||
<a href="tutorial/{section.chapters[0].slug}">{section.title}</a>
|
||||
<ul>
|
||||
{#each section.chapters as chapter, i}
|
||||
<option value={chapter.slug}>{String.fromCharCode(i + 97)}. {chapter.title}</option>
|
||||
{#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}
|
||||
</optgroup>
|
||||
</ul>
|
||||
</li>
|
||||
{/each}
|
||||
</select>
|
||||
</ul>
|
||||
</OutClick>
|
||||
</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" />
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
Loading…
Reference in new issue