feat(docsearch): animate cards on action
This commit is contained in:
parent
4d743bc48c
commit
a917e607ab
3 changed files with 164 additions and 89 deletions
208
src/Results.tsx
208
src/Results.tsx
|
|
@ -21,7 +21,11 @@ interface ResultsProps<TItem>
|
|||
title: string;
|
||||
suggestion: AutocompleteState<TItem>['suggestions'][0];
|
||||
renderIcon(props: { item: TItem; index: number }): React.ReactNode;
|
||||
renderAction(props: { item: TItem }): React.ReactNode;
|
||||
renderAction(props: {
|
||||
item: TItem;
|
||||
runDeleteTransition: (cb: () => void) => void;
|
||||
runFavoriteTransition: (cb: () => void) => void;
|
||||
}): React.ReactNode;
|
||||
onItemClick(item: TItem): void;
|
||||
hitComponent(props: {
|
||||
hit: DocSearchHit;
|
||||
|
|
@ -36,8 +40,6 @@ export function Results<TItem extends StoredDocSearchHit>(
|
|||
return null;
|
||||
}
|
||||
|
||||
const Hit = props.hitComponent;
|
||||
|
||||
return (
|
||||
<section className="DocSearch-Hits">
|
||||
<div className="DocSearch-Hit-source">{props.title}</div>
|
||||
|
|
@ -45,86 +47,132 @@ export function Results<TItem extends StoredDocSearchHit>(
|
|||
<ul {...props.getMenuProps()}>
|
||||
{props.suggestion.items.map((item, index) => {
|
||||
return (
|
||||
<li
|
||||
key={[item.objectID, index].join(':')}
|
||||
className={[
|
||||
'DocSearch-Hit',
|
||||
((item as unknown) as InternalDocSearchHit)
|
||||
.__docsearch_parent && 'DocSearch-Hit--Child',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
{...props.getItemProps({
|
||||
item,
|
||||
source: props.suggestion.source,
|
||||
onClick() {
|
||||
props.onItemClick(item);
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Hit hit={item}>
|
||||
<div className="DocSearch-Hit-Container">
|
||||
{props.renderIcon({ item, index })}
|
||||
|
||||
{item.hierarchy[item.type] && item.type === 'lvl1' && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
{item.content && (
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
hit={item}
|
||||
attribute="content"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.hierarchy[item.type] &&
|
||||
(item.type === 'lvl2' ||
|
||||
item.type === 'lvl3' ||
|
||||
item.type === 'lvl4' ||
|
||||
item.type === 'lvl5' ||
|
||||
item.type === 'lvl6') && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
hit={item}
|
||||
attribute={`hierarchy.${item.type}`}
|
||||
/>
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'content' && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
hit={item}
|
||||
attribute="content"
|
||||
/>
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.renderAction({ item })}
|
||||
</div>
|
||||
</Hit>
|
||||
</li>
|
||||
<Result
|
||||
key={[props.title, item.objectID].join(':')}
|
||||
item={item}
|
||||
index={index}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface ResultProps<TItem> extends ResultsProps<TItem> {
|
||||
item: TItem;
|
||||
index: number;
|
||||
}
|
||||
|
||||
function Result<TItem extends StoredDocSearchHit>({
|
||||
item,
|
||||
index,
|
||||
renderIcon,
|
||||
renderAction,
|
||||
getItemProps,
|
||||
onItemClick,
|
||||
suggestion,
|
||||
hitComponent,
|
||||
}: ResultProps<TItem>) {
|
||||
const [isDeleting, setIsDeleting] = React.useState(false);
|
||||
const [isFavoriting, setIsFavoriting] = React.useState(false);
|
||||
const action = React.useRef<(() => void) | null>(null);
|
||||
const Hit = hitComponent;
|
||||
|
||||
function runDeleteTransition(cb: () => void) {
|
||||
setIsDeleting(true);
|
||||
action.current = cb;
|
||||
}
|
||||
|
||||
function runFavoriteTransition(cb: () => void) {
|
||||
setIsFavoriting(true);
|
||||
action.current = cb;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={[
|
||||
'DocSearch-Hit',
|
||||
((item as unknown) as InternalDocSearchHit).__docsearch_parent &&
|
||||
'DocSearch-Hit--Child',
|
||||
isDeleting && 'DocSearch-Hit--deleting',
|
||||
isFavoriting && 'DocSearch-Hit--favoriting',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onTransitionEnd={() => {
|
||||
if (action.current) {
|
||||
action.current();
|
||||
}
|
||||
}}
|
||||
{...getItemProps({
|
||||
item,
|
||||
source: suggestion.source,
|
||||
onClick() {
|
||||
onItemClick(item);
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Hit hit={item}>
|
||||
<div className="DocSearch-Hit-Container">
|
||||
{renderIcon({ item, index })}
|
||||
|
||||
{item.hierarchy[item.type] && item.type === 'lvl1' && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
{item.content && (
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
hit={item}
|
||||
attribute="content"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.hierarchy[item.type] &&
|
||||
(item.type === 'lvl2' ||
|
||||
item.type === 'lvl3' ||
|
||||
item.type === 'lvl4' ||
|
||||
item.type === 'lvl5' ||
|
||||
item.type === 'lvl6') && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
hit={item}
|
||||
attribute={`hierarchy.${item.type}`}
|
||||
/>
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'content' && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
hit={item}
|
||||
attribute="content"
|
||||
/>
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderAction({ item, runDeleteTransition, runFavoriteTransition })}
|
||||
</div>
|
||||
</Hit>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,11 @@ export function StartScreen(props: StartScreenProps) {
|
|||
<RecentIcon />
|
||||
</div>
|
||||
)}
|
||||
renderAction={({ item }) => (
|
||||
renderAction={({
|
||||
item,
|
||||
runFavoriteTransition,
|
||||
runDeleteTransition,
|
||||
}) => (
|
||||
<>
|
||||
<div className="DocSearch-Hit-action">
|
||||
<button
|
||||
|
|
@ -56,9 +60,12 @@ export function StartScreen(props: StartScreenProps) {
|
|||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
props.favoriteSearches.add(item);
|
||||
props.recentSearches.remove(item);
|
||||
props.refresh();
|
||||
|
||||
runFavoriteTransition(() => {
|
||||
props.favoriteSearches.add(item);
|
||||
props.recentSearches.remove(item);
|
||||
props.refresh();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StarIcon />
|
||||
|
|
@ -71,8 +78,11 @@ export function StartScreen(props: StartScreenProps) {
|
|||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
props.recentSearches.remove(item);
|
||||
props.refresh();
|
||||
|
||||
runDeleteTransition(() => {
|
||||
props.recentSearches.remove(item);
|
||||
props.refresh();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ResetIcon />
|
||||
|
|
@ -91,7 +101,7 @@ export function StartScreen(props: StartScreenProps) {
|
|||
<StarIcon />
|
||||
</div>
|
||||
)}
|
||||
renderAction={({ item }) => (
|
||||
renderAction={({ item, runDeleteTransition }) => (
|
||||
<div className="DocSearch-Hit-action">
|
||||
<button
|
||||
className="DocSearch-Hit-action-button"
|
||||
|
|
@ -99,8 +109,11 @@ export function StartScreen(props: StartScreenProps) {
|
|||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
props.favoriteSearches.remove(item);
|
||||
props.refresh();
|
||||
|
||||
runDeleteTransition(() => {
|
||||
props.favoriteSearches.remove(item);
|
||||
props.refresh();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ResetIcon />
|
||||
|
|
|
|||
|
|
@ -444,6 +444,20 @@ html[data-theme='dark'] {
|
|||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--deleting {
|
||||
transition: all 500ms cubic-bezier(0.88, -0.05, 0.85, 0.2);
|
||||
opacity: 0;
|
||||
transform: translateX(50%);
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.DocSearch-Hit--favoriting {
|
||||
transition: all 500ms cubic-bezier(0.88, -0.05, 0.85, 0.2);
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.DocSearch-Hit a {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
|
|
|
|||
Loading…
Reference in a new issue