Refactored header autohiding and split overrides

This commit is contained in:
squidfunk
2021-02-12 15:44:53 +01:00
parent f7907fc09c
commit 28d507ecfe
28 changed files with 183 additions and 77 deletions

View File

@@ -21,23 +21,34 @@
*/
import {
NEVER,
Observable,
Subject,
animationFrameScheduler,
combineLatest,
defer,
of
} from "rxjs"
import {
bufferCount,
combineLatestWith,
distinctUntilChanged,
distinctUntilKeyChanged,
filter,
map,
observeOn,
shareReplay
shareReplay,
startWith,
switchMap
} from "rxjs/operators"
import { feature } from "~/_"
import { resetHeaderState, setHeaderState } from "~/actions"
import { Viewport, watchElementSize } from "~/browser"
import {
Viewport,
watchElementSize,
watchToggle
} from "~/browser"
import { Component } from "../../_"
import { Main } from "../../main"
@@ -50,14 +61,22 @@ import { Main } from "../../main"
* Header
*/
export interface Header {
sticky: boolean /* Header stickyness */
height: number /* Header visible height */
sticky: boolean /* Header stickyness */
hidden: boolean /* User scrolled past threshold */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Watch options
*/
interface WatchOptions {
viewport$: Observable<Viewport> /* Viewport observable */
}
/**
* Mount options
*/
@@ -67,6 +86,52 @@ interface MountOptions {
main$: Observable<Main> /* Main area observable */
}
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Compute whether the header is hidden
*
* If the user scrolls past a certain threshold, the header can be hidden when
* scrolling down, and shown when scrolling up.
*
* @param options - Options
*
* @returns Toggle observable
*/
function isHidden({ viewport$ }: WatchOptions): Observable<boolean> {
if (!feature("header.autohide"))
return of(false)
/* Compute direction and turning point */
const direction$ = viewport$
.pipe(
map(({ offset: { y } }) => y),
bufferCount(2, 1),
map(([a, b]) => [a < b, b] as const),
distinctUntilKeyChanged(0)
)
/* Compute whether header should be hidden */
const hidden$ = combineLatest([viewport$, direction$])
.pipe(
filter(([{ offset }, [, y]]) => Math.abs(y - offset.y) > 100),
map(([, [direction]]) => direction),
distinctUntilChanged(),
)
/* Compute threshold for autohiding */
const search$ = watchToggle("search")
return combineLatest([viewport$, search$])
.pipe(
map(([{ offset }, search]) => offset.y > 400 && !search),
distinctUntilChanged(),
switchMap(active => active ? hidden$ : NEVER),
startWith(false)
)
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@@ -75,11 +140,12 @@ interface MountOptions {
* Watch header
*
* @param el - Header element
* @param options - Options
*
* @returns Header observable
*/
export function watchHeader(
el: HTMLElement
el: HTMLElement, options: WatchOptions
): Observable<Header> {
return defer(() => {
const styles = getComputedStyle(el)
@@ -89,14 +155,16 @@ export function watchHeader(
)
})
.pipe(
combineLatestWith(watchElementSize(el)),
map(([sticky, { height }]) => ({
combineLatestWith(watchElementSize(el), isHidden(options)),
map(([sticky, { height }, hidden]) => ({
height: sticky ? height : 0,
sticky,
height: sticky ? height : 0
hidden
})),
distinctUntilChanged((a, b) => (
a.sticky === b.sticky &&
a.height === b.height
a.height === b.height &&
a.hidden === b.hidden
)),
shareReplay(1)
)
@@ -122,11 +190,12 @@ export function mountHeader(
internal$
.pipe(
distinctUntilKeyChanged("active"),
combineLatestWith(header$),
observeOn(animationFrameScheduler)
)
.subscribe(({ active }) => {
.subscribe(([{ active }, { hidden }]) => {
if (active)
setHeaderState(el, "shadow")
setHeaderState(el, hidden ? "hidden" : "shadow")
else
resetHeaderState(el)
})