Skip to content

Commit a917e60

Browse files
feat(docsearch): animate cards on action
1 parent 4d743bc commit a917e60

3 files changed

Lines changed: 164 additions & 89 deletions

File tree

src/Results.tsx

Lines changed: 128 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ interface ResultsProps<TItem>
2121
title: string;
2222
suggestion: AutocompleteState<TItem>['suggestions'][0];
2323
renderIcon(props: { item: TItem; index: number }): React.ReactNode;
24-
renderAction(props: { item: TItem }): React.ReactNode;
24+
renderAction(props: {
25+
item: TItem;
26+
runDeleteTransition: (cb: () => void) => void;
27+
runFavoriteTransition: (cb: () => void) => void;
28+
}): React.ReactNode;
2529
onItemClick(item: TItem): void;
2630
hitComponent(props: {
2731
hit: DocSearchHit;
@@ -36,95 +40,139 @@ export function Results<TItem extends StoredDocSearchHit>(
3640
return null;
3741
}
3842

39-
const Hit = props.hitComponent;
40-
4143
return (
4244
<section className="DocSearch-Hits">
4345
<div className="DocSearch-Hit-source">{props.title}</div>
4446

4547
<ul {...props.getMenuProps()}>
4648
{props.suggestion.items.map((item, index) => {
4749
return (
48-
<li
49-
key={[item.objectID, index].join(':')}
50-
className={[
51-
'DocSearch-Hit',
52-
((item as unknown) as InternalDocSearchHit)
53-
.__docsearch_parent && 'DocSearch-Hit--Child',
54-
]
55-
.filter(Boolean)
56-
.join(' ')}
57-
{...props.getItemProps({
58-
item,
59-
source: props.suggestion.source,
60-
onClick() {
61-
props.onItemClick(item);
62-
},
63-
})}
64-
>
65-
<Hit hit={item}>
66-
<div className="DocSearch-Hit-Container">
67-
{props.renderIcon({ item, index })}
68-
69-
{item.hierarchy[item.type] && item.type === 'lvl1' && (
70-
<div className="DocSearch-Hit-content-wrapper">
71-
<Snippet
72-
className="DocSearch-Hit-title"
73-
hit={item}
74-
attribute="hierarchy.lvl1"
75-
/>
76-
{item.content && (
77-
<Snippet
78-
className="DocSearch-Hit-path"
79-
hit={item}
80-
attribute="content"
81-
/>
82-
)}
83-
</div>
84-
)}
85-
86-
{item.hierarchy[item.type] &&
87-
(item.type === 'lvl2' ||
88-
item.type === 'lvl3' ||
89-
item.type === 'lvl4' ||
90-
item.type === 'lvl5' ||
91-
item.type === 'lvl6') && (
92-
<div className="DocSearch-Hit-content-wrapper">
93-
<Snippet
94-
className="DocSearch-Hit-title"
95-
hit={item}
96-
attribute={`hierarchy.${item.type}`}
97-
/>
98-
<Snippet
99-
className="DocSearch-Hit-path"
100-
hit={item}
101-
attribute="hierarchy.lvl1"
102-
/>
103-
</div>
104-
)}
105-
106-
{item.type === 'content' && (
107-
<div className="DocSearch-Hit-content-wrapper">
108-
<Snippet
109-
className="DocSearch-Hit-title"
110-
hit={item}
111-
attribute="content"
112-
/>
113-
<Snippet
114-
className="DocSearch-Hit-path"
115-
hit={item}
116-
attribute="hierarchy.lvl1"
117-
/>
118-
</div>
119-
)}
120-
121-
{props.renderAction({ item })}
122-
</div>
123-
</Hit>
124-
</li>
50+
<Result
51+
key={[props.title, item.objectID].join(':')}
52+
item={item}
53+
index={index}
54+
{...props}
55+
/>
12556
);
12657
})}
12758
</ul>
12859
</section>
12960
);
13061
}
62+
63+
interface ResultProps<TItem> extends ResultsProps<TItem> {
64+
item: TItem;
65+
index: number;
66+
}
67+
68+
function Result<TItem extends StoredDocSearchHit>({
69+
item,
70+
index,
71+
renderIcon,
72+
renderAction,
73+
getItemProps,
74+
onItemClick,
75+
suggestion,
76+
hitComponent,
77+
}: ResultProps<TItem>) {
78+
const [isDeleting, setIsDeleting] = React.useState(false);
79+
const [isFavoriting, setIsFavoriting] = React.useState(false);
80+
const action = React.useRef<(() => void) | null>(null);
81+
const Hit = hitComponent;
82+
83+
function runDeleteTransition(cb: () => void) {
84+
setIsDeleting(true);
85+
action.current = cb;
86+
}
87+
88+
function runFavoriteTransition(cb: () => void) {
89+
setIsFavoriting(true);
90+
action.current = cb;
91+
}
92+
93+
return (
94+
<li
95+
className={[
96+
'DocSearch-Hit',
97+
((item as unknown) as InternalDocSearchHit).__docsearch_parent &&
98+
'DocSearch-Hit--Child',
99+
isDeleting && 'DocSearch-Hit--deleting',
100+
isFavoriting && 'DocSearch-Hit--favoriting',
101+
]
102+
.filter(Boolean)
103+
.join(' ')}
104+
onTransitionEnd={() => {
105+
if (action.current) {
106+
action.current();
107+
}
108+
}}
109+
{...getItemProps({
110+
item,
111+
source: suggestion.source,
112+
onClick() {
113+
onItemClick(item);
114+
},
115+
})}
116+
>
117+
<Hit hit={item}>
118+
<div className="DocSearch-Hit-Container">
119+
{renderIcon({ item, index })}
120+
121+
{item.hierarchy[item.type] && item.type === 'lvl1' && (
122+
<div className="DocSearch-Hit-content-wrapper">
123+
<Snippet
124+
className="DocSearch-Hit-title"
125+
hit={item}
126+
attribute="hierarchy.lvl1"
127+
/>
128+
{item.content && (
129+
<Snippet
130+
className="DocSearch-Hit-path"
131+
hit={item}
132+
attribute="content"
133+
/>
134+
)}
135+
</div>
136+
)}
137+
138+
{item.hierarchy[item.type] &&
139+
(item.type === 'lvl2' ||
140+
item.type === 'lvl3' ||
141+
item.type === 'lvl4' ||
142+
item.type === 'lvl5' ||
143+
item.type === 'lvl6') && (
144+
<div className="DocSearch-Hit-content-wrapper">
145+
<Snippet
146+
className="DocSearch-Hit-title"
147+
hit={item}
148+
attribute={`hierarchy.${item.type}`}
149+
/>
150+
<Snippet
151+
className="DocSearch-Hit-path"
152+
hit={item}
153+
attribute="hierarchy.lvl1"
154+
/>
155+
</div>
156+
)}
157+
158+
{item.type === 'content' && (
159+
<div className="DocSearch-Hit-content-wrapper">
160+
<Snippet
161+
className="DocSearch-Hit-title"
162+
hit={item}
163+
attribute="content"
164+
/>
165+
<Snippet
166+
className="DocSearch-Hit-path"
167+
hit={item}
168+
attribute="hierarchy.lvl1"
169+
/>
170+
</div>
171+
)}
172+
173+
{renderAction({ item, runDeleteTransition, runFavoriteTransition })}
174+
</div>
175+
</Hit>
176+
</li>
177+
);
178+
}

src/StartScreen.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ export function StartScreen(props: StartScreenProps) {
4747
<RecentIcon />
4848
</div>
4949
)}
50-
renderAction={({ item }) => (
50+
renderAction={({
51+
item,
52+
runFavoriteTransition,
53+
runDeleteTransition,
54+
}) => (
5155
<>
5256
<div className="DocSearch-Hit-action">
5357
<button
@@ -56,9 +60,12 @@ export function StartScreen(props: StartScreenProps) {
5660
onClick={event => {
5761
event.preventDefault();
5862
event.stopPropagation();
59-
props.favoriteSearches.add(item);
60-
props.recentSearches.remove(item);
61-
props.refresh();
63+
64+
runFavoriteTransition(() => {
65+
props.favoriteSearches.add(item);
66+
props.recentSearches.remove(item);
67+
props.refresh();
68+
});
6269
}}
6370
>
6471
<StarIcon />
@@ -71,8 +78,11 @@ export function StartScreen(props: StartScreenProps) {
7178
onClick={event => {
7279
event.preventDefault();
7380
event.stopPropagation();
74-
props.recentSearches.remove(item);
75-
props.refresh();
81+
82+
runDeleteTransition(() => {
83+
props.recentSearches.remove(item);
84+
props.refresh();
85+
});
7686
}}
7787
>
7888
<ResetIcon />
@@ -91,16 +101,19 @@ export function StartScreen(props: StartScreenProps) {
91101
<StarIcon />
92102
</div>
93103
)}
94-
renderAction={({ item }) => (
104+
renderAction={({ item, runDeleteTransition }) => (
95105
<div className="DocSearch-Hit-action">
96106
<button
97107
className="DocSearch-Hit-action-button"
98108
title="Remove this search from favorites"
99109
onClick={event => {
100110
event.preventDefault();
101111
event.stopPropagation();
102-
props.favoriteSearches.remove(item);
103-
props.refresh();
112+
113+
runDeleteTransition(() => {
114+
props.favoriteSearches.remove(item);
115+
props.refresh();
116+
});
104117
}}
105118
>
106119
<ResetIcon />

src/style.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,20 @@ html[data-theme='dark'] {
444444
padding-bottom: 4px;
445445
}
446446

447+
.DocSearch-Hit--deleting {
448+
transition: all 500ms cubic-bezier(0.88, -0.05, 0.85, 0.2);
449+
opacity: 0;
450+
transform: translateX(50%);
451+
transform-origin: right;
452+
}
453+
454+
.DocSearch-Hit--favoriting {
455+
transition: all 500ms cubic-bezier(0.88, -0.05, 0.85, 0.2);
456+
opacity: 0;
457+
transform: scale(0);
458+
transform-origin: bottom;
459+
}
460+
447461
.DocSearch-Hit a {
448462
display: block;
449463
border-radius: 4px;

0 commit comments

Comments
 (0)