[tutorial-ui] Beautified nav and menu

- 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-drag
pull/6851/head
babakfp 5 years ago
parent cd7307e2da
commit 106a64ea4f

@ -56,7 +56,8 @@
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"sapper": "^0.29.3", "sapper": "^0.29.3",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"svelte": "^3.39.0" "svelte": "^3.39.0",
"svelte-outclick": "^2.2.1"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

@ -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…
Cancel
Save