Skip to content

Commit

Permalink
feat(docsearch): animate cards on action
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour committed Apr 9, 2020
1 parent 4d743bc commit a917e60
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 89 deletions.
208 changes: 128 additions & 80 deletions src/Results.tsx
Expand Up @@ -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;
Expand All @@ -36,95 +40,139 @@ 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>

<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>
);
}
31 changes: 22 additions & 9 deletions src/StartScreen.tsx
Expand Up @@ -47,7 +47,11 @@ export function StartScreen(props: StartScreenProps) {
<RecentIcon />
</div>
)}
renderAction={({ item }) => (
renderAction={({
item,
runFavoriteTransition,
runDeleteTransition,
}) => (
<>
<div className="DocSearch-Hit-action">
<button
Expand All @@ -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 />
Expand All @@ -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 />
Expand All @@ -91,16 +101,19 @@ export function StartScreen(props: StartScreenProps) {
<StarIcon />
</div>
)}
renderAction={({ item }) => (
renderAction={({ item, runDeleteTransition }) => (
<div className="DocSearch-Hit-action">
<button
className="DocSearch-Hit-action-button"
title="Remove this search from favorites"
onClick={event => {
event.preventDefault();
event.stopPropagation();
props.favoriteSearches.remove(item);
props.refresh();

runDeleteTransition(() => {
props.favoriteSearches.remove(item);
props.refresh();
});
}}
>
<ResetIcon />
Expand Down
14 changes: 14 additions & 0 deletions src/style.css
Expand Up @@ -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;
Expand Down

0 comments on commit a917e60

Please sign in to comment.