Skip to content

Commit

Permalink
fix(list): rework transitions
Browse files Browse the repository at this point in the history
  • Loading branch information
dvcol committed Feb 1, 2025
1 parent bcf1c8f commit 3bf6de9
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 59 deletions.
65 changes: 58 additions & 7 deletions demo/components/DemoLists.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { debounce } from '@dvcol/common-utils/common/debounce';
import { getUUID } from '@dvcol/common-utils/common/string';
import { fade } from 'svelte/transition';
Expand All @@ -8,6 +9,8 @@
import NeoButtonGroup from '~/buttons/NeoButtonGroup.svelte';
import NeoCard from '~/cards/NeoCard.svelte';
import IconAccount from '~/icons/IconAccount.svelte';
import IconAlignBottom from '~/icons/IconAlignBottom.svelte';
import IconAlignTop from '~/icons/IconAlignTop.svelte';
import IconCircleLoading from '~/icons/IconCircleLoading.svelte';
import NeoInput from '~/inputs/common/NeoInput.svelte';
import NeoList from '~/list/NeoList.svelte';
Expand Down Expand Up @@ -114,7 +117,6 @@
{ label: 'John Doe', value: 'John', description: '[email protected]' },
{ label: 'Peter Jackson', value: 'Peter', description: '[email protected]' },
{ label: 'John Smith', value: 'Smith', description: '[email protected]' },
{ label: 'Jane Doe', value: 'Jane', description: '[email protected]' },
{ label: 'Alice Johnson', value: 'Alice', description: '[email protected]' },
{ label: 'Bob Brown', value: 'Bob', description: '[email protected]' },
{ label: 'Charlie Davis', value: 'Charlie', description: '[email protected]' },
Expand All @@ -126,12 +128,38 @@
{ label: 'Ivy Johnson', value: 'Ivy', description: '[email protected]' },
{ label: 'Jack King', value: 'Jack', description: '[email protected]' },
{ label: 'Karen Lee', value: 'Karen', description: '[email protected]' },
{
label: 'Directors',
divider: true,
items: [
{ label: 'Denis VVilleneuve', value: 'Denis', description: '+33 1 25 48 45 45' },
{ label: 'Christopher Nolan', value: 'Christopher', description: '+44 2 07 94 60 95' },
{ label: 'Quentin Tarantino', value: 'Quentin', description: '+33 1 05 55 12 34' },
{ label: 'Martin Scorsese', value: 'Martin', description: '+33 1 25 55 56 78' },
{ label: 'Steven Spielberg', value: 'Steven', description: '+33 1 85 55 87 65' },
].map(item => ({ ...item, id: getUUID(), before: avatar })),
},
{
label: 'Actors',
divider: true,
items: [
{ label: 'Leonardo DiCaprio', value: 'Leonardo', description: '+1 310 555 1234' },
{ label: 'Brad Pitt', value: 'Brad', description: '+1 323 555 5678' },
{ label: 'Meryl Streep', value: 'Meryl', description: '+1 212 555 8765' },
{ label: 'Tom Hanks', value: 'Tom', description: '+1 310 555 4321' },
{ label: 'Natalie Portman', value: 'Natalie', description: '+1 818 555 6789' },
].map(item => ({ ...item, id: getUUID(), before: avatar })),
},
].map(item => ({ ...item, id: getUUID(), before: avatar })),
);
let hovered = $state(false);
let focused = $state(false);
let filter = $state('');
const setFilter = debounce((value: string) => {
filter = value;
}, 300);
let sort = $state(false);
const withComplexList = $derived(isEmpty ? [] : complexList);
const onAdd = () => {
Expand Down Expand Up @@ -175,7 +203,7 @@

{#snippet renderSection(_children, _context)}
<h2>Custom Section</h2>
<h4>{_context?.section?.title}</h4>
<h4>{_context?.section?.label}</h4>
<ul>
{@render _children(_context)}
</ul>
Expand Down Expand Up @@ -209,7 +237,7 @@
<!-- multi line loader-->
<div class="column content">
<span class="label">Multi-line loader</span>
<NeoList {items} {...options} loaderProps={{ lines: 2, items: 10 }} />
<NeoList {items} {...options} loaderProps={{ lines: 2, items: 5 }} />
</div>

<!-- custom loader-->
Expand Down Expand Up @@ -283,7 +311,7 @@
<span class="label">Custom section</span>
<NeoList items={withCustomSection} {...options} {item}>
{#snippet section(_children, _context)}
<h2>{_context?.section?.title}</h2>
<h2>{_context?.section?.label}</h2>
<ul>
{@render _children(_context)}
</ul>
Expand Down Expand Up @@ -317,17 +345,40 @@
beforeProps: { width: '1.875rem', height: '1.875rem' },
}}
buttonProps={{ rounded: true }}
filter={i => i.label.toLowerCase().includes(filter.toLowerCase())}
filter={i =>
i.items?.some(j => j.label.toLowerCase().includes(filter.toLowerCase())) ||
(i.label && i.label.toLowerCase().includes(filter.toLowerCase()))}
sort={sort ? (a, b) => a.label.localeCompare(b.label) : (b, a) => a.label.localeCompare(b.label)}
>
{#snippet before(ctx)}
<NeoInput
placeholder="Placeholder"
bind:value={filter}
oninput={e => setFilter(e.target.value)}
elevation={hovered || focused ? 1 : 0}
hover="0"
rounded
containerProps={{ style: 'margin-bottom: 0' }}
/>
>
{#snippet after()}
<NeoButton
text
rounded
shallow
title="Sort alphabetically"
onclick={() => {
sort = !sort;
}}
>
{#snippet icon()}
{#if sort}
<IconAlignTop />
{:else}
<IconAlignBottom />
{/if}
{/snippet}
</NeoButton>
{/snippet}
</NeoInput>
{@render values(ctx)}
{/snippet}
</NeoList>
Expand Down
21 changes: 21 additions & 0 deletions src/lib/icons/IconAlignBottom.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width={$$props.size ?? '1em'}
height={$$props.size ?? '1em'}
viewBox="0 0 24 24"
{...$$props}
style:scale={$$props.scale}
scale={undefined}
>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width={$$props.stroke ?? 2}>
<path stroke-dasharray="20" stroke-dashoffset="20" d="M3 21l18 0">
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="20;0" />
</path>
<path stroke-dasharray="16" stroke-dashoffset="16" d="M12 3l0 13.5">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.3s" dur="0.2s" values="16;0" />
</path>
<path stroke-dasharray="8" stroke-dashoffset="8" d="M12 17l4 -4M12 17l-4 -4">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.5s" dur="0.2s" values="8;0" />
</path>
</g>
</svg>
21 changes: 21 additions & 0 deletions src/lib/icons/IconAlignTop.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width={$$props.size ?? '1em'}
height={$$props.size ?? '1em'}
viewBox="0 0 24 24"
{...$$props}
style:scale={$$props.scale}
scale={undefined}
>
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width={$$props.stroke ?? 2}>
<path stroke-dasharray="20" stroke-dashoffset="20" d="M3 3l18 0">
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="20;0" />
</path>
<path stroke-dasharray="16" stroke-dashoffset="16" d="M12 21l0 -13.5">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.3s" dur="0.2s" values="16;0" />
</path>
<path stroke-dasharray="8" stroke-dashoffset="8" d="M12 7l4 4M12 7l-4 4">
<animate fill="freeze" attributeName="stroke-dashoffset" begin="0.5s" dur="0.2s" values="8;0" />
</path>
</g>
</svg>
31 changes: 17 additions & 14 deletions src/lib/list/NeoList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@
scrollbar = true,
// Animation
transition,
animate,
in: inAction = { use: scale, props: scaleTransitionProps },
out: outAction = { use: fade, props: { ...scaleTransitionProps, delay: scaleTransitionProps?.duration } },
animate = { use: flipToggle, props: flipTransitionProps },
// Events
onselect,
Expand All @@ -73,8 +74,7 @@
// Todo - keep selected on filter
// TODO - arrow navigation
const visible = $derived<NeoListItemOrSection[]>(items?.filter(filter).sort(sort));
const empty = $derived(!visible?.length);
const empty = $derived(!items?.length);
const missing = $derived(items?.some(item => item.id === undefined || item.id === null));
const scrollTop = debounce(() => {
Expand Down Expand Up @@ -211,10 +211,12 @@
scrollBottom,
});
});
const animateFn = $derived(missing ? emptyAnimation : toAnimation(animate, flipToggle));
const animateProps = $derived(toTransitionProps(animate, flipTransitionProps));
const transitionFn = $derived(missing ? emptyTransition : toTransition(transition, scale));
const transitionProps = $derived(toTransitionProps(transition, scaleTransitionProps));
const animateFn = $derived(missing ? emptyAnimation : toAnimation(animate));
const animateProps = $derived(toTransitionProps(animate));
const inFn = $derived(missing ? emptyTransition : toTransition(inAction));
const inProps = $derived(toTransitionProps(inAction));
const outFn = $derived(missing ? emptyTransition : toTransition(outAction));
const outProps = $derived(toTransitionProps(outAction));
</script>

{#snippet loader(show = loading)}
Expand All @@ -223,14 +225,15 @@
{#if show && customLoader}
{@render customLoader(context)}
{:else}
<NeoListBaseLoader loading={show} {select} {transition} {...loaderProps} />
<NeoListBaseLoader loading={show} {select} in={inAction} out={outAction} {...loaderProps} />
{/if}
</li>
{/snippet}

{#snippet list({ items: array, section, index: sectionIndex }: NeoListRenderContext)}
{@const visible = array?.filter(filter).sort(sort)}
<!-- Items -->
{#each array as item, index (item.id ?? index)}
{#each visible as item, index (item.id ?? index)}
<svelte:element
this={item.tag ?? 'li'}
role={select ? 'option' : 'listitem'}
Expand All @@ -239,8 +242,8 @@
class:neo-list-item-select={select}
style:--neo-list-item-color={getColorVariable(item.color)}
animate:animateFn={{ ...animateProps, enabled: !section }}
out:transitionFn={transitionProps}
in:transitionFn={{ ...transitionProps, delay: transitionProps?.duration }}
out:inFn={inProps}
in:outFn={outProps}
{...item.containerProps}
>
{#if item.divider}
Expand Down Expand Up @@ -289,7 +292,7 @@
{...rest}
>
{@render children?.(context)}
{@render list({ items: visible, context })}
{@render list({ items, context })}
{@render loader(loading || (empty && skeleton))}
</svelte:element>
{:else}
Expand Down Expand Up @@ -341,7 +344,7 @@
&.neo-scroll {
@include mixin.scrollbar($button-height: 0.375rem);
padding-block: 0.25rem;
padding-block: 0.5rem;
&.neo-shadow {
@include mixin.fade-scroll(1rem);
Expand Down
19 changes: 11 additions & 8 deletions src/lib/list/NeoListBaseLoader.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<script lang="ts">
import { clamp, randomInt } from '@dvcol/common-utils/common/math';
import { circIn } from 'svelte/easing';
import { circIn, circOut } from 'svelte/easing';
import { scale } from 'svelte/transition';
import { fade } from 'svelte/transition';
import type { NeoListBaseLoaderProps } from '~/list/neo-list-base-loader.model.js';
import NeoSkeletonText from '~/skeletons/NeoSkeletonText.svelte';
import { toTransition, toTransitionProps } from '~/utils/action.utils.js';
import { scaleTransitionProps } from '~/utils/transition.utils.js';
import { scaleEnterProps, scaleLeaveProps } from '~/utils/transition.utils.js';
const {
loading,
Expand All @@ -23,24 +23,27 @@
items = 3,
flex = items > 1 ? undefined : '0 0 70%',
transition,
in: inAction = { use: fade, props: scaleEnterProps },
out: outAction = { use: fade, props: scaleLeaveProps },
beforeProps,
afterProps,
...rest
}: NeoListBaseLoaderProps = $props();
const transitionFn = $derived(toTransition(transition, scale));
const transitionProps = $derived(toTransitionProps(transition, scaleTransitionProps));
const inFn = $derived(toTransition(inAction));
const inProps = $derived(toTransitionProps(inAction));
const outFn = $derived(toTransition(outAction));
const outProps = $derived(toTransitionProps(outAction));
</script>

{#each { length: items } as _, i (i)}
{#if loading}
<div
class="neo-list-base-loader"
class:neo-select={select}
in:transitionFn={{ ...transitionProps, delay: Math.min(300 + circIn(clamp(i / 10, 0, 1)) * 2000, 600) }}
out:transitionFn={{ ...transitionProps, delay: Math.min(circIn(clamp((items - i) / 10, 0, 1)) * 2000, 600) }}
in:inFn={{ ...inProps, delay: Math.min((inProps?.duration ?? 0) + circIn(clamp(i / 10, 0, 1)) * 2000, 400) }}
out:outFn={{ ...outProps, delay: Math.min(circOut(clamp((items - i) / 10, 0, 1)) * 200, 400) }}
>
<div class="neo-list-base-loader-content" class:neo-description={description}>
{#if before}
Expand Down
6 changes: 3 additions & 3 deletions src/lib/list/NeoListBaseSection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@
...rest
}: NeoListBaseSectionProps = $props();
const labelId = $derived(section.title ? `neo-list-section-label-${getUUID()}` : undefined);
const labelId = $derived(section.label ? `neo-list-section-label-${getUUID()}` : undefined);
</script>

{#if section?.render}
{@render section?.render(list, { items: section.items, section, index, context })}
{:else}
{#if section.title}
{#if section.label}
<NeoSkeletonText loading={skeleton} lines={1} align="center" {...skeletonProps} class={['neo-list-item-skeleton', skeletonProps?.class]}>
<span id={labelId} class="neo-list-item-section-title">{section.title}</span>
<span id={labelId} class="neo-list-item-section-title">{section.label}</span>
</NeoSkeletonText>
{/if}
<ul role="group" aria-labelledby={labelId} class:neo-list-item-section={true} {...rest}>
Expand Down
8 changes: 6 additions & 2 deletions src/lib/list/neo-list-base-loader.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ export type NeoListBaseLoaderProps = {
items?: number;

/**
* Transition properties to apply to the loader.
* Transition function to apply when adding items to the loader.
*/
transition?: HTMLTransitionProps['transition'];
in: HTMLTransitionProps['in'];
/**
* Transition function to apply when removing items from the loader.
*/
out: HTMLTransitionProps['out'];

/**
* Weather to style each 2n item as a description.
Expand Down
15 changes: 13 additions & 2 deletions src/lib/list/neo-list.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,14 @@ export type NeoListRender<Value = unknown> = Snippet<[NeoListRenderContext<Value

export type NeoListSectionRender<Value = unknown> = Snippet<[NeoListRender<Value>, NeoListRenderContext<Value>]>;
export type NeoListSection<Value = unknown, Tag extends keyof HTMLElementTagNameMap = 'ul'> = {
title?: string;
/**
* Array of child list items to display.
*/
items: NeoListItem<Value>[];
/**
* Optional label to display in the list item.
*/
label: string;
/**
* Optional snippet to display in place of the list section.
* @param list - The list snippet that render items.
Expand Down Expand Up @@ -251,7 +257,12 @@ export type NeoListProps<Value = unknown, Tag extends keyof HTMLElementTagNameMa
* Transition function to apply when adding items to the list.
* Note: unique `id` is required for entering/leaving transitions.
*/
transition: HTMLTransitionProps['transition'];
in: HTMLTransitionProps['in'];
/**
* Transition function to apply when removing items from the list.
* Note: unique `id` is required for entering/leaving transitions.
*/
out: HTMLTransitionProps['out'];

// Styles
/**
Expand Down
Loading

0 comments on commit 3bf6de9

Please sign in to comment.