fix(SW-1446): handle empty history and performance better

This commit is contained in:
Michael Zetterberg
2025-04-08 04:03:07 +02:00
parent b2ff5124ec
commit 2953b3571d
6 changed files with 134 additions and 105 deletions

View File

@@ -89,7 +89,7 @@
border-radius: var(--Corner-radius-Large); border-radius: var(--Corner-radius-Large);
padding: var(--Space-x2); padding: var(--Space-x2);
width: 360px; width: 360px;
max-height: 430px; max-height: 400px;
box-sizing: content-box; box-sizing: content-box;
box-shadow: var(--BoxShadow-Level-4); box-shadow: var(--BoxShadow-Level-4);
position: absolute; position: absolute;

View File

@@ -13,7 +13,7 @@ import { useIntl } from "react-intl"
import { useIsMounted } from "usehooks-ts" import { useIsMounted } from "usehooks-ts"
import { Button } from "@scandic-hotels/design-system/Button" import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { Results } from "../Results" import { Results } from "../Results"
@@ -35,7 +35,8 @@ export function ClientInline({
const isMounted = useIsMounted() const isMounted = useIsMounted()
const showResults = !!results const showResults = !!results
const showHistory = isMounted() && (!results || results.length === 0) const showHistory =
latest.length > 0 && isMounted() && (!results || results.length === 0)
return ( return (
<Autocomplete> <Autocomplete>
@@ -101,32 +102,34 @@ export function ClientInline({
</form> </form>
)} )}
</SearchField> </SearchField>
<div className={styles.results}> {showResults || showHistory ? (
<div <div className={styles.results}>
className={cx({ <div
[styles.menuContainer]: true, className={cx({
[styles.pending]: isPending, [styles.menuContainer]: true,
})} [styles.pending]: isPending,
aria-live="polite" })}
> aria-live="polite"
{showResults ? ( >
<ResultsMemo {showResults ? (
aria-label={intl.formatMessage({ id: "Results" })} <ResultsMemo
results={results} aria-label={intl.formatMessage({ id: "Results" })}
onAction={onAction} results={results}
/> onAction={onAction}
) : null} />
{showHistory ? ( ) : null}
<ResultsMemo {showHistory ? (
aria-label={intl.formatMessage({ <ResultsMemo
id: "Latest searches", aria-label={intl.formatMessage({
})} id: "Latest searches",
results={latest} })}
onAction={onAction} results={latest}
/> onAction={onAction}
) : null} />
) : null}
</div>
</div> </div>
</div> ) : null}
</div> </div>
</Autocomplete> </Autocomplete>
) )

View File

@@ -16,7 +16,7 @@ import {
} from "react-aria-components" } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { Results } from "../Results" import { Results } from "../Results"

View File

@@ -23,51 +23,51 @@ export function ResultsSkeleton() {
<div> <div>
<div className={styles.item}> <div className={styles.item}>
<Typography variant="Body/Paragraph/mdBold"> <Typography variant="Body/Paragraph/mdBold">
<SkeletonShimmer width="50%" /> <SkeletonShimmer width="50%" display="inline-block" />
</Typography> </Typography>
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
<div className={styles.itemDescription}> <div className={styles.itemDescription}>
<SkeletonShimmer width="38%" /> <SkeletonShimmer width="38%" display="inline-block" />
</div> </div>
</Typography> </Typography>
</div> </div>
<div className={styles.item}> <div className={styles.item}>
<Typography variant="Body/Paragraph/mdBold"> <Typography variant="Body/Paragraph/mdBold">
<SkeletonShimmer width="40%" /> <SkeletonShimmer width="40%" display="inline-block" />
</Typography> </Typography>
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
<div className={styles.itemDescription}> <div className={styles.itemDescription}>
<SkeletonShimmer width="23%" /> <SkeletonShimmer width="23%" display="inline-block" />
</div> </div>
</Typography> </Typography>
</div> </div>
<div className={styles.item}> <div className={styles.item}>
<Typography variant="Body/Paragraph/mdBold"> <Typography variant="Body/Paragraph/mdBold">
<SkeletonShimmer width="55%" /> <SkeletonShimmer width="55%" display="inline-block" />
</Typography> </Typography>
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
<div className={styles.itemDescription}> <div className={styles.itemDescription}>
<SkeletonShimmer width="40%" /> <SkeletonShimmer width="40%" display="inline-block" />
</div> </div>
</Typography> </Typography>
</div> </div>
<div className={styles.item}> <div className={styles.item}>
<Typography variant="Body/Paragraph/mdBold"> <Typography variant="Body/Paragraph/mdBold">
<SkeletonShimmer width="27%" /> <SkeletonShimmer width="27%" display="inline-block" />
</Typography> </Typography>
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
<div className={styles.itemDescription}> <div className={styles.itemDescription}>
<SkeletonShimmer width="33%" /> <SkeletonShimmer width="33%" display="inline-block" />
</div> </div>
</Typography> </Typography>
</div> </div>
<div className={styles.item}> <div className={styles.item}>
<Typography variant="Body/Paragraph/mdBold"> <Typography variant="Body/Paragraph/mdBold">
<SkeletonShimmer width="45%" /> <SkeletonShimmer width="45%" display="inline-block" />
</Typography> </Typography>
<Typography variant="Body/Paragraph/mdRegular"> <Typography variant="Body/Paragraph/mdRegular">
<div className={styles.itemDescription}> <div className={styles.itemDescription}>
<SkeletonShimmer width="37%" /> <SkeletonShimmer width="37%" display="inline-block" />
</div> </div>
</Typography> </Typography>
</div> </div>

View File

@@ -3,14 +3,16 @@
import { import {
Collection, Collection,
Header, Header,
ListLayout,
Menu, Menu,
MenuItem, MenuItem,
MenuSection, MenuSection,
Text, Text,
Virtualizer,
} from "react-aria-components" } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./results.module.css" import styles from "./results.module.css"
@@ -25,20 +27,61 @@ export function Results({
const intl = useIntl() const intl = useIntl()
return ( return (
<Menu <Virtualizer
aria-label={ariaLabel} layout={ListLayout}
onAction={onAction} layoutOptions={{
className={styles.menu} estimatedRowHeight: 64,
items={results} estimatedHeadingHeight: 41,
renderEmptyState={() => {
return null
}} }}
> >
{(section) => { <Menu
if (section.id === "actions") { aria-label={ariaLabel}
onAction={onAction}
className={styles.menu}
items={results}
renderEmptyState={() => {
return null
}}
>
{(section) => {
if (section.id === "actions") {
return (
<MenuSection key={section.id} className={styles.actionsSection}>
<Header className={styles.menuDivider}>
<span className="sr-only">{section.name}</span>
</Header>
<Collection items={section.children}>
{(item) => (
<MenuItem
key={item.id}
href={item.url}
className={styles.item}
textValue={item.displayName}
>
{item.id === "clearHistory" ? (
<>
<MaterialIcon icon="delete" color="CurrentColor" />
<Typography variant="Body/Supporting text (caption)/smBold">
<Text slot="label">
{intl.formatMessage({
id: "Clear searches",
})}
</Text>
</Typography>
</>
) : null}
</MenuItem>
)}
</Collection>
</MenuSection>
)
}
return ( return (
<MenuSection key={section.id} className={styles.actionsSection}> <MenuSection key={section.id}>
<Header className="sr-only">{section.name}</Header> <Typography variant="Title/Overline/sm">
<Header className={styles.sectionHeader}>{section.name}</Header>
</Typography>
<Collection items={section.children}> <Collection items={section.children}>
{(item) => ( {(item) => (
<MenuItem <MenuItem
@@ -47,59 +90,28 @@ export function Results({
className={styles.item} className={styles.item}
textValue={item.displayName} textValue={item.displayName}
> >
{item.id === "clearHistory" ? ( <Typography variant="Body/Paragraph/mdBold">
<> <Text slot="label" className={styles.itemLabel}>
<MaterialIcon icon="delete" color="CurrentColor" /> {item.displayName}
<Typography variant="Body/Supporting text (caption)/smBold"> </Text>
<Text slot="label"> </Typography>
{intl.formatMessage({ {item.description ? (
id: "Clear searches", <Typography variant="Body/Paragraph/mdRegular">
})} <Text
</Text> slot="description"
</Typography> className={styles.itemDescription}
</> >
{item.description}
</Text>
</Typography>
) : null} ) : null}
</MenuItem> </MenuItem>
)} )}
</Collection> </Collection>
</MenuSection> </MenuSection>
) )
} }}
</Menu>
return ( </Virtualizer>
<MenuSection key={section.id}>
<Typography variant="Title/Overline/sm">
<Header className={styles.sectionHeader}>{section.name}</Header>
</Typography>
<Collection items={section.children}>
{(item) => (
<MenuItem
key={item.id}
href={item.url}
className={styles.item}
textValue={item.displayName}
>
<Typography variant="Body/Paragraph/mdBold">
<Text slot="label" className={styles.itemLabel}>
{item.displayName}
</Text>
</Typography>
{item.description ? (
<Typography variant="Body/Paragraph/mdRegular">
<Text
slot="description"
className={styles.itemDescription}
>
{item.description}
</Text>
</Typography>
) : null}
</MenuItem>
)}
</Collection>
</MenuSection>
)
}}
</Menu>
) )
} }

View File

@@ -1,13 +1,20 @@
.menu { .menu {
display: flex; height: 100%;
flex-direction: column; overflow-y: auto;
gap: var(--Space-x2);
} }
.sectionHeader { .sectionHeader {
color: var(--UI-Text-Placeholder); color: var(--UI-Text-Placeholder);
padding-left: var(--Space-x1); padding-left: var(--Space-x1);
padding-bottom: var(--Space-x05); padding-bottom: var(--Space-x05);
/* Due to Virtualizer we cannot use gap in .menu,
instead we use padding-top on each section header */
padding-top: var(--Space-x2);
/* Except for the first section header */
.menu > div > div:first-child & {
padding-top: 0;
}
} }
.item { .item {
@@ -45,9 +52,16 @@
color: var(--Text-Tertiary); color: var(--Text-Tertiary);
} }
.actionsSection { .menuDivider {
border-top: solid 1px var(--Border-Divider-Subtle); padding-top: var(--Space-x2);
padding-top: var(--Space-x2); /* match gap of .menu */ padding-bottom: var(--Space-x2);
}
.menuDivider:before {
display: block;
content: "";
height: 1px;
background: var(--Border-Divider-Subtle);
} }
.actionsSection .item { .actionsSection .item {