add animation for navigation bar indicator (#257)

This commit is contained in:
Nolan Lawson 2018-05-02 21:32:43 -07:00 committed by GitHub
parent 8f5a4aadca
commit b7c90a4206
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 53 deletions

View file

@ -29,6 +29,8 @@
firstTime = false firstTime = false
this.refs.container.focus() this.refs.container.focus()
} }
let { page } = this.get()
this.store.set({currentPage: page})
}, },
store: () => store store: () => store
} }

View file

@ -1,37 +1,16 @@
<nav class="main-nav"> <nav class="main-nav">
<ul class="main-nav-ul"> <ul class="main-nav-ul">
<li class="main-nav-li"> {#each $navPages as navPage (navPage.name)}
<NavItem {page} name="home" href="/" svg="#pinafore-logo" label="Home" />
</li>
<li class="main-nav-li">
<NavItem {page} name="notifications" href="/notifications" svg="#fa-bell" label="Notifications" />
</li>
{#if $pinnedPage === '/local'}
<li class="main-nav-li"> <li class="main-nav-li">
<NavItem {page} name="local" href="/local" svg="#fa-users" label="Local" /> <NavItem
{page}
name={navPage.name}
href={navPage.href}
svg={navPage.svg}
label={navPage.label}
/>
</li> </li>
{:elseif $pinnedPage === '/federated'} {/each}
<li class="main-nav-li">
<NavItem {page} name="federated" href="/federated" svg="#fa-globe" label="Federated" />
</li>
{:elseif $pinnedPage === '/favorites'}
<li class="main-nav-li">
<NavItem {page} name="favorites" href="/favorites" svg="#fa-star" label="Favorites" />
</li>
{:elseif $pinnedPage.startsWith('/lists/')}
<li class="main-nav-li">
<NavItem {page} name="lists" href={$pinnedPage} svg="#fa-bars" label={$pinnedListTitle} />
</li>
{/if}
<li class="main-nav-li">
<NavItem {page} name="community" href="/community" svg="#fa-comments" label="Community" />
</li>
<li class="main-nav-li">
<NavItem {page} name="search" href="/search" svg="#fa-search" label="Search" />
</li>
<li class="main-nav-li">
<NavItem {page} name="settings" href="/settings" svg="#fa-gear" label="Settings" />
</li>
</ul> </ul>
</nav> </nav>

View file

@ -3,28 +3,41 @@
aria-current={selected} aria-current={selected}
on:click="onClick(event)" on:click="onClick(event)"
{href} > {href} >
{#if name === 'notifications'} <div class="nav-icon-and-label">
{#if name === 'notifications'}
<div class="nav-link-svg-wrapper"> <div class="nav-link-svg-wrapper">
<svg class="nav-link-svg"> <svg class="nav-link-svg">
<use xlink:href={svg} /> <use xlink:href={svg} />
</svg> </svg>
{#if $hasNotifications} {#if $hasNotifications}
<span class="nav-link-notifications" aria-hidden="true"> <span class="nav-link-notifications" aria-hidden="true">
{$numberOfNotifications} {$numberOfNotifications}
</span> </span>
{/if} {/if}
</div> </div>
{:else} {:else}
<svg class="nav-link-svg"> <svg class="nav-link-svg">
<use xlink:href={svg} /> <use xlink:href={svg} />
</svg> </svg>
{/if} {/if}
<span class="nav-link-label">{label}</span> <span class="nav-link-label">{label}</span>
</div>
<div class="nav-indicator"
nav-indicator-key={name}
ref:indicator
></div>
</a> </a>
<style> <style>
.main-nav-link { .main-nav-link {
border-bottom: 1px solid var(--nav-a-border);
text-decoration: none; text-decoration: none;
display: flex;
justify-content: center;
align-items: center;
flex: 1;
flex-direction: column;
}
.nav-icon-and-label {
padding: 15px 20px; padding: 15px 20px;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -62,18 +75,38 @@
} }
.main-nav-link.selected { .main-nav-link.selected {
border-bottom: 1px solid var(--nav-a-selected-border);
background: var(--nav-a-selected-bg); background: var(--nav-a-selected-bg);
} }
.main-nav-link.selected:hover { .main-nav-link.selected:hover {
border-bottom: 1px solid var(--nav-a-selected-border-hover);
background: var(--nav-a-selected-bg-hover); background: var(--nav-a-selected-bg-hover);
} }
.nav-indicator {
width: 100%;
height: 1px;
background: var(--nav-a-border);
transform-origin: left;
}
.nav-indicator.animate {
transition: transform 333ms ease-in-out;
}
.main-nav-link:hover .nav-indicator {
background: var(--nav-a-border-hover);
}
.main-nav-link.selected .nav-indicator {
background: var(--nav-a-selected-border);
}
.main-nav-link.selected:hover .nav-indicator {
background: var(--nav-a-selected-border-hover);
}
.main-nav-link:hover { .main-nav-link:hover {
background-color: var(--nav-a-bg-hover); background-color: var(--nav-a-bg-hover);
border-bottom: 1px solid var(--nav-a-border-hover);
text-decoration: none; text-decoration: none;
} }
@ -106,7 +139,7 @@
width: 25px; width: 25px;
height: 25px; height: 25px;
} }
.main-nav-link { .nav-icon-and-label {
padding: 20px 0; padding: 20px 0;
} }
.nav-link-notifications { .nav-link-notifications {
@ -118,8 +151,44 @@
<script> <script>
import { store } from '../_store/store' import { store } from '../_store/store'
import { smoothScrollToTop } from '../_utils/smoothScrollToTop' import { smoothScrollToTop } from '../_utils/smoothScrollToTop'
import { on, emit } from '../_utils/eventBus'
import { mark, stop } from '../_utils/marks'
import { doubleRAF } from '../_utils/doubleRAF'
export default { export default {
oncreate () {
let { name } = this.get()
let indicator = this.refs.indicator
on('animateNavPart1', this, ({fromPage, toPage}) => {
if (fromPage !== name) {
return
}
mark('animateNavPart1 gBCR')
let fromRect = indicator.getBoundingClientRect()
stop('animateNavPart1 gBCR')
emit('animateNavPart2', {fromRect, fromPage, toPage})
})
on('animateNavPart2', this, ({fromPage, fromRect, toPage}) => {
if (toPage !== name) {
return
}
mark('animateNavPart2 gBCR')
let toRect = indicator.getBoundingClientRect()
stop('animateNavPart2 gBCR')
let translateX = fromRect.left - toRect.left
let scaleX = fromRect.width / toRect.width
indicator.style.transform = `translateX(${translateX}px) scaleX(${scaleX})`
let onTransitionEnd = () => {
indicator.removeEventListener('transitionend', onTransitionEnd)
indicator.classList.remove('animate')
}
indicator.addEventListener('transitionend', onTransitionEnd)
doubleRAF(() => {
indicator.classList.add('animate')
indicator.style.transform = ''
})
})
},
store: () => store, store: () => store,
computed: { computed: {
selected: ({ page, name }) => page === name, selected: ({ page, name }) => page === name,

View file

@ -1,7 +1,9 @@
import { instanceComputations } from './instanceComputations' import { instanceComputations } from './instanceComputations'
import { timelineComputations } from './timelineComputations' import { timelineComputations } from './timelineComputations'
import { navComputations } from './navComputations'
export function computations (store) { export function computations (store) {
instanceComputations(store) instanceComputations(store)
timelineComputations(store) timelineComputations(store)
navComputations(store)
} }

View file

@ -47,17 +47,4 @@ export function instanceComputations (store) {
['currentInstanceData'], ['currentInstanceData'],
(currentInstanceData) => currentInstanceData && currentInstanceData.access_token (currentInstanceData) => currentInstanceData && currentInstanceData.access_token
) )
store.compute(
'pinnedListTitle',
['lists', 'pinnedPage'],
(lists, pinnedPage) => {
if (!pinnedPage.startsWith('/lists')) {
return
}
let listId = pinnedPage.split('/').slice(-1)[0]
let list = lists.find(_ => _.id === listId)
return list ? list.title : ''
}
)
} }

View file

@ -0,0 +1,85 @@
export function navComputations (store) {
store.compute(
'pinnedListTitle',
['lists', 'pinnedPage'],
(lists, pinnedPage) => {
if (!pinnedPage.startsWith('/lists')) {
return
}
let listId = pinnedPage.split('/').slice(-1)[0]
let list = lists.find(_ => _.id === listId)
return list ? list.title : ''
}
)
store.compute(
'navPages',
['pinnedPage', 'pinnedListTitle'],
(pinnedPage, pinnedListTitle) => {
let pinnedPageObject
if (pinnedPage === '/federated') {
pinnedPageObject = {
name: 'federated',
href: '/federated',
svg: '#fa-globe',
label: 'Federated'
}
} else if (pinnedPage === '/favorites') {
pinnedPageObject = {
name: 'favorites',
href: '/favorites',
svg: '#fa-star',
label: 'Favorites'
}
} else if (pinnedPage.startsWith('/lists/')) {
pinnedPageObject = {
name: 'lists',
href: pinnedPage,
svg: '#fa-bars',
label: pinnedListTitle
}
} else { // local
pinnedPageObject = {
name: 'local',
href: '/local',
svg: '#fa-users',
label: 'Local'
}
}
return [
{
name: 'home',
href: '/',
svg: '#pinafore-logo',
label: 'Home'
},
{
name: 'notifications',
href: '/notifications',
svg: '#fa-bell',
label: 'Notifications'
},
pinnedPageObject,
{
name: 'community',
href: '/community',
svg: '#fa-comments',
label: 'Community'
},
{
name: 'search',
href: '/search',
svg: '#fa-search',
label: 'Search'
},
{
name: 'settings',
href: '/settings',
svg: '#fa-gear',
label: 'Settings'
}
]
}
)
}

View file

@ -0,0 +1,21 @@
import { emit } from '../../_utils/eventBus'
export function navObservers (store) {
function pageIsInNav (store, page) {
let { navPages } = store.get()
return !!navPages.find(_ => _.name === page)
}
store.observe('currentPage', (currentPage, previousPage) => {
if (currentPage && previousPage &&
pageIsInNav(store, currentPage) &&
pageIsInNav(store, previousPage)) {
emit('animateNavPart1', {
fromPage: previousPage,
toPage: currentPage
})
}
}, {
init: false
})
}

View file

@ -2,10 +2,12 @@ import { instanceObservers } from './instanceObservers'
import { timelineObservers } from './timelineObservers' import { timelineObservers } from './timelineObservers'
import { notificationObservers } from './notificationObservers' import { notificationObservers } from './notificationObservers'
import { onlineObservers } from './onlineObservers' import { onlineObservers } from './onlineObservers'
import { navObservers } from './navObservers'
export function observers (store) { export function observers (store) {
instanceObservers(store) instanceObservers(store)
timelineObservers(store) timelineObservers(store)
notificationObservers(store) notificationObservers(store)
onlineObservers(store) onlineObservers(store)
navObservers(store)
} }