Skip to content

Commit

Permalink
feat: loading state using skeleton span
Browse files Browse the repository at this point in the history
  • Loading branch information
cristinecula committed Nov 18, 2024
1 parent 9bb13bd commit 4b11e45
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 50 deletions.
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"@neovici/cosmoz-dropdown": "^4.4.0",
"@neovici/cosmoz-input": "^5.0.2",
"@neovici/cosmoz-utils": "^6.0.0",
"@pionjs/pion": "^2.0.0",
"@pionjs/pion": "^2.7.1",
"lit-html": "^2.0.0 || ^3.0.0"
},
"devDependencies": {
Expand Down
22 changes: 13 additions & 9 deletions src/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { html, nothing } from 'lit-html'; // eslint-disable-line object-curly-newline
import { html } from 'lit-html'; // eslint-disable-line object-curly-newline
import { live } from 'lit-html/directives/live.js';
import { until } from 'lit-html/directives/until.js';
import { when } from 'lit-html/directives/when.js';
import { useCallback } from '@pionjs/pion';
import { useCallback, useEffect, useState } from '@pionjs/pion';
import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
import { useImperativeApi } from '@neovici/cosmoz-utils/hooks/use-imperative-api';
import '@neovici/cosmoz-input';
Expand Down Expand Up @@ -31,14 +31,13 @@ type AProps<I> = Omit<Props<I>, keyof RProps<I>> &
onInputRef?: (el?: Element) => void;
};

const blank = () => nothing;

const inputParts = ['input', 'control', 'label', 'line', 'error', 'wrap']
.map((part) => `${part}: input-${part}`)
.join();

const autocomplete = <I>(props: AProps<I>) => {
const {
active,
invalid,
errorMessage,
label,
Expand Down Expand Up @@ -68,6 +67,14 @@ const autocomplete = <I>(props: AProps<I>) => {
[host, value],
);

const [loading, setLoading] = useState(false);
useEffect(() => {
// TODO: handle `source$` changes that occur while `loading`
setLoading(true);
const endLoading = setLoading.bind(null, false);
source$.then(endLoading, endLoading);
}, [source$]);

useImperativeApi(
{
focus: () =>
Expand Down Expand Up @@ -121,18 +128,15 @@ const autocomplete = <I>(props: AProps<I>) => {
textual,
disabled,
})}
${until(
source$.then(blank, blank),
html`<div slot="suffix" class="spinner"></div>`,
)}
</cosmoz-input>
${when((!isSingle || showSingle) && items.length, () =>
${when(active && (loading || items.length > 0) && !(isSingle && !showSingle), () =>
listbox<I>({
...props,
anchor,
items,
multi: !isOne,
loading,
}),
)}`;
},
Expand Down
22 changes: 0 additions & 22 deletions src/autocomplete/styles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,26 +53,4 @@ export default css`
slot {
display: contents !important;
}
@keyframes rotateAnimation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spinner {
border-radius: 50%;
width: 22px;
height: 22px;
border: 2px solid rgba(0, 0, 0, 0.1);
border-top: 2px solid #5f5a92;
animation: rotateAnimation 1.2s infinite
cubic-bezier(0.785, 0.135, 0.15, 0.86);
box-sizing: border-box;
margin-top: -3px;
flex: none;
}
`;
21 changes: 14 additions & 7 deletions src/listbox/item-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { html, TemplateResult } from 'lit-html';
import './skeleton-span';

import { identity } from '@neovici/cosmoz-utils/function';
import { mark } from './util';
import { html, TemplateResult } from 'lit-html';
import { loadingSymbol, mark } from './util';

export interface Opts<I> {
highlight: (i: number) => void;
Expand All @@ -13,15 +15,15 @@ export interface Opts<I> {
export type Render<I> = (content: unknown, item: I, i: number) => unknown;

export type ItemRenderer<I> = (
item: I,
item: I | typeof loadingSymbol,
i: number,
opts: Opts<I>
opts: Opts<I>,
) => TemplateResult;

export const itemRenderer =
<I>(render: Render<I> = identity) =>
(
item: I,
item: I | typeof loadingSymbol,
i: number,
{
highlight,
Expand All @@ -35,12 +37,17 @@ export const itemRenderer =
isSelected: (item: I) => void;
query: string;
textual: (i: I) => string;
}
},
): TemplateResult => {
if (item === loadingSymbol) {
return html`<div class="item">
<cosmoz-autocomplete-skeleton-span></cosmoz-autocomplete-skeleton-span>
</div>`;
}

const text = textual(item),
content = mark(text, query),
rendered = render(content, item, i);

return html` <div
class="item"
role="option"
Expand Down
30 changes: 30 additions & 0 deletions src/listbox/skeleton-span.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { component, css } from '@pionjs/pion';
import { nothing } from 'lit-html';

const style = css`
:host {
display: inline-block;
vertical-align: middle;
background-image: linear-gradient(90deg, #e0e0e0, #f5f5f5, #e0e0e0);
background-size: 1000%;
background-position: right;
animation: sweep 1.5s cubic-bezier(0.3, 1, 0.3, 1) infinite;
border-radius: 3px;
width: 100px;
height: 20px;
}
@keyframes sweep {
0% {
background-position: right;
}
100% {
background-position: left;
}
}
`;

customElements.define(
'cosmoz-autocomplete-skeleton-span',
component(() => nothing, { styleSheets: [style] }),
);
15 changes: 11 additions & 4 deletions src/listbox/use-listbox.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from '@pionjs/pion';
import { useHost } from '@neovici/cosmoz-utils/hooks/use-host';
import { usePosition, Placement } from '@neovici/cosmoz-dropdown/use-position';
import { byValue } from './util';
import { byValue, loadingSymbol } from './util';
import { useItems } from './use-items';
import { useRenderItem, ItemRenderer } from './use-render-item';

Expand All @@ -19,6 +19,7 @@ export const properties = [
'defaultIndex',
'value',
'valueProperty',
'loading',
];

export interface Props<I> {
Expand All @@ -35,6 +36,7 @@ export interface Props<I> {
anchor?: () => HTMLElement | null;
confinement?: HTMLElement;
placement?: Placement;
loading?: boolean;
}

export const useListbox = <I>({
Expand All @@ -48,16 +50,21 @@ export const useListbox = <I>({
itemRenderer,
itemHeight = 40,
itemLimit = 5,
loading,
...thru
}: Props<I>) => {
const isSelected = useMemo(
() => byValue(value, valueProperty),
[value, valueProperty]
[value, valueProperty],
),
// TODO: investigate if we can drop this
items = useMemo(() => _items.slice(), [_items, isSelected]),
__items = useMemo(() => _items.slice(), [_items, isSelected]),
items: (I | typeof loadingSymbol)[] = useMemo(
() => (loading ? [...__items, loadingSymbol] : __items),
[loading, __items],
),
{ position, highlight, select } = useItems({
items,
items: __items,
onSelect,
defaultIndex: isNaN(defaultIndex as number)
? undefined
Expand Down
3 changes: 2 additions & 1 deletion src/listbox/use-render-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
ItemRenderer,
Opts,
} from './item-renderer';
import { loadingSymbol } from './util';

export const useRenderItem = <I>({
itemRenderer = mkItemRenderer<I>(),
...meta
}: Opts<I> & { itemRenderer?: ItemRenderer<I> }) => {
const info = useMeta<Opts<I>>(meta);
return useCallback(
(item: I, i: number) => itemRenderer(item, i, info),
(item: I | typeof loadingSymbol, i: number) => itemRenderer(item, i, info),
[info, itemRenderer]
);
};
Expand Down
6 changes: 4 additions & 2 deletions src/listbox/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Rec } from '@neovici/cosmoz-utils/object';

export const byValue = <K extends PropertyKey | undefined, T>(
value: T | T[],
valueProperty: K
valueProperty: K,
) => {
if (!valueProperty) {
return (item: T) => array(value).includes(item);
Expand All @@ -13,7 +13,7 @@ export const byValue = <K extends PropertyKey | undefined, T>(
array(value).find(
(value) =>
(value as Rec<NonNullable<K>>)[valueProperty] ===
(item as Rec<NonNullable<K>>)[valueProperty]
(item as Rec<NonNullable<K>>)[valueProperty],
);
};
export const mark = (text: string, query: string) => {
Expand All @@ -31,3 +31,5 @@ export const mark = (text: string, query: string) => {
text.slice(end),
];
};

export const loadingSymbol: unique symbol = Symbol('loading');

0 comments on commit 4b11e45

Please sign in to comment.