feat: add fade effect to overflown lists (#1618)

This commit is contained in:
Phan An 2022-12-06 13:14:45 +01:00 committed by GitHub
parent baa2e45a5d
commit 8be339a23a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 110 additions and 31 deletions

View file

@ -94,7 +94,8 @@ export default abstract class UnitTestCase {
'koel-clickaway': {},
'koel-focus': {},
'koel-tooltip': {},
'koel-hide-broken-icon': {}
'koel-hide-broken-icon': {},
'koel-overflow-fade': {}
},
components: {
icon: this.stub('icon')

View file

@ -1,5 +1,5 @@
import { createApp } from 'vue'
import { clickaway, focus, hideBrokenIcon, tooltip } from '@/directives'
import { clickaway, focus, hideBrokenIcon, overflowFade, tooltip } from '@/directives'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { RouterKey } from '@/symbols'
import { routes } from '@/config'
@ -14,6 +14,7 @@ createApp(App)
.directive('koel-clickaway', clickaway)
.directive('koel-tooltip', tooltip)
.directive('koel-hide-broken-icon', hideBrokenIcon)
.directive('koel-overflow-fade', overflowFade)
/**
* For Ancelot, the ancient cross of war
* for the holy town of Gods

View file

@ -9,6 +9,7 @@
<div
ref="scroller"
v-koel-overflow-fade
:class="`as-${viewMode}`"
class="albums main-scroll-wrap"
data-testid="album-list"

View file

@ -9,6 +9,7 @@
<div
ref="scroller"
v-koel-overflow-fade
:class="`as-${viewMode}`"
class="artists main-scroll-wrap"
data-testid="artist-list"

View file

@ -2,7 +2,7 @@
<section id="homeWrapper">
<ScreenHeader layout="collapsed">{{ greeting }}</ScreenHeader>
<div class="main-scroll-wrap" @scroll="scrolling">
<div v-koel-overflow-fade class="main-scroll-wrap" @scroll="scrolling">
<ScreenEmptyState v-if="libraryEmpty">
<template #icon>
<icon :icon="faVolumeOff" />

View file

@ -17,7 +17,7 @@
</template>
</ScreenHeader>
<div class="main-scroll-wrap">
<div v-koel-overflow-fade class="main-scroll-wrap">
<div
v-if="mediaPathSetUp"
:class="{ droppable }"

View file

@ -14,7 +14,7 @@
</template>
</ScreenHeader>
<div class="main-scroll-wrap">
<div v-koel-overflow-fade class="main-scroll-wrap">
<ul class="users">
<li v-for="user in users" :key="user.id">
<UserCard :user="user" />

View file

@ -3,7 +3,7 @@
<section class="existing-playlists">
<p>Add {{ pluralize(songs, 'song') }} to</p>
<ul>
<ul v-koel-overflow-fade>
<template v-if="config.queue">
<template v-if="queue.length">
<li
@ -103,6 +103,7 @@ watch(songs, () => songs.value.length || close())
}
ul {
position: relative;
max-height: 12rem;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;

View file

@ -22,7 +22,7 @@
<li @click="addSongsToFavorite">Favorites</li>
</template>
<li v-if="normalPlaylists.length" class="separator" />
<ul v-if="normalPlaylists.length" class="normal-playlists">
<ul v-if="normalPlaylists.length" v-koel-overflow-fade class="playlists">
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
</ul>
<li class="separator" />
@ -163,8 +163,9 @@ eventBus.on('SONG_CONTEXT_MENU_REQUESTED', async (e, _songs) => {
</script>
<style lang="scss" scoped>
ul.normal-playlists {
max-height: 256px;
ul.playlists {
position: relative;
max-height: 192px;
overflow-y: auto;
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<div ref="scroller" class="virtual-scroller" @scroll.passive="onScroll">
<div ref="scroller" v-koel-overflow-fade class="virtual-scroller" @scroll.passive="onScroll">
<div :style="{ height: `${totalHeight}px` }">
<div :style="{ transform: `translateY(${offsetY}px)`}">
<template v-for="item in renderedItems">
@ -23,7 +23,7 @@ const scrollTop = ref(0)
const emit = defineEmits<{
(e: 'scrolled-to-end'): void,
(e: 'scroll', event: MouseEvent): void
(e: 'scroll', event: Event): void
}>()
const totalHeight = computed(() => items.value.length * itemHeight.value)
@ -36,7 +36,7 @@ const renderedItems = computed(() => {
return items.value.slice(startPosition.value, startPosition.value + count)
})
const onScroll = e => requestAnimationFrame(() => {
const onScroll = (e: Event) => requestAnimationFrame(() => {
scrollTop.value = (e.target as HTMLElement).scrollTop
if (!scroller.value) return

View file

@ -2,3 +2,4 @@ export * from './clickaway'
export * from './focus'
export * from './tooltip'
export * from './hideBrokenIcon'
export * from './overflowFade'

View file

@ -0,0 +1,18 @@
import { Directive } from 'vue'
const toggleClasses = (el: HTMLElement) => {
el.classList.toggle('fade-top', el.scrollTop !== 0)
el.classList.toggle('fade-bottom', el.scrollTop + el.clientHeight !== el.scrollHeight)
}
const observeVisibility = (el: HTMLElement, callback: Closure) => {
const observer = new IntersectionObserver(entries => entries.forEach(entry => entry.isIntersecting && callback()))
observer.observe(el)
}
export const overflowFade: Directive = {
mounted: async (el: HTMLElement) => {
observeVisibility(el, () => toggleClasses(el))
el.addEventListener('scroll', () => requestAnimationFrame(() => toggleClasses(el)))
}
}

View file

@ -19,32 +19,40 @@ html.non-mac {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-button {
width: 0px;
height: 0px;
}
::-webkit-scrollbar-thumb {
background: var(--color-bg-primary);
border: 1px solid rgba(255, 255, 255, .2);
border-radius: 50px;
}
::-webkit-scrollbar-thumb:hover {
background: #303030;
}
::-webkit-scrollbar-thumb:active {
background: var(--color-bg-primary);
}
::-webkit-scrollbar-track {
background: var(--color-bg-primary);
border: 0px none var(--color-text-primary);
border-radius: 50px;
}
::-webkit-scrollbar-track:hover {
background: var(--color-bg-primary);
}
::-webkit-scrollbar-track:active {
background: #333333;
}
::-webkit-scrollbar-corner {
background: transparent;
}

View file

@ -10,14 +10,14 @@
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
&.as-list {
gap: 0.7em 1em;;
gap: 0.7em 1em;
align-content: start;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
@media only screen and (max-width: 667px) {
display: block;
> * + * {
>*+* {
margin-top: .7rem;
}
}
@ -91,7 +91,8 @@
}
}
.bio, .wiki {
.bio,
.wiki {
margin: 16px 0;
}
@ -106,7 +107,8 @@
font-size: .8rem;
}
.cover, .cool-guys-posing {
.cover,
.cool-guys-posing {
width: 100%;
height: auto;
display: block;
@ -137,7 +139,8 @@
margin: 0 16px 16px 0;
}
.bio, .wiki {
.bio,
.wiki {
margin-top: 0;
}

View file

@ -1,4 +1,6 @@
*, *::before, *::after {
*,
*::before,
*::after {
box-sizing: border-box;
outline: none;
}
@ -7,7 +9,8 @@
display: none !important;
}
body, html {
body,
html {
@include themed-background();
color: var(--color-text-primary);
@ -18,7 +21,11 @@ body, html {
overflow: hidden;
}
input, select, button, textarea, .btn {
input,
select,
button,
textarea,
.btn {
appearance: none;
border: 0;
outline: 0;
@ -29,7 +36,8 @@ input, select, button, textarea, .btn {
border-radius: .3rem;
margin: 0;
&:required, &:invalid {
&:required,
&:invalid {
box-shadow: none;
}
@ -52,7 +60,8 @@ input, select, button, textarea, .btn {
}
}
button, [role=button] {
button,
[role=button] {
cursor: pointer;
background: transparent;
padding: 0;
@ -87,11 +96,13 @@ a {
text-decoration: none;
cursor: pointer;
&:link, &:visited {
&:link,
&:visited {
color: var(--color-text-secondary);
}
&:hover, &:focus {
&:hover,
&:focus {
color: var(--color-accent);
}
}
@ -174,7 +185,7 @@ label {
}
}
.form-row + .form-row {
.form-row+.form-row {
margin-top: 1.125rem;
position: relative;
}
@ -184,12 +195,13 @@ label {
place-content: space-between;
gap: 1rem;
> * {
>* {
flex: 1;
}
}
.form-row input:not([type="checkbox"]), .form-row select {
.form-row input:not([type="checkbox"]),
.form-row select {
margin-top: .7rem;
display: block;
height: 32px;
@ -291,7 +303,9 @@ label {
}
}
.context-menu, .submenu, menu {
.context-menu,
.submenu,
menu {
@include context-menu();
position: fixed;
@ -335,3 +349,29 @@ label {
top: 0;
}
}
:root {
--fade-size: 3rem;
}
.fade-top {
-webkit-mask-image: linear-gradient(to bottom, transparent, black var(--fade-size));
mask-image: linear-gradient(to bottom, transparent, black var(--fade-size));
}
.fade-bottom {
-webkit-mask-image: linear-gradient(to top, transparent, black var(--fade-size));
mask-image: linear-gradient(to top, transparent, black var(--fade-size));
}
.fade-top.fade-bottom {
-webkit-mask: linear-gradient(to bottom, transparent, black var(--fade-size)) top,
linear-gradient(to top, transparent, black var(--fade-size)) bottom;
-webkit-mask-size: 100% 51%;
-webkit-mask-repeat: no-repeat;
mask: linear-gradient(to bottom, transparent, black var(--fade-size)) top,
linear-gradient(to top, transparent, black var(--fade-size)) bottom;
mask-size: 100% 51%;
mask-repeat: no-repeat;
}

View file

@ -1,11 +1,15 @@
.skeleton {
.pulse, &.pulse {
.pulse,
&.pulse {
animation: skeleton-pulse 2s infinite;
background-color: rgba(255, 255, 255, .05);
}
@keyframes skeleton-pulse {
0%, 100% {
0%,
100% {
opacity: 0;
}

View file

@ -34,4 +34,3 @@
display: none !important;
}
}