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);
padding: var(--Space-x2);
width: 360px;
max-height: 430px;
max-height: 400px;
box-sizing: content-box;
box-shadow: var(--BoxShadow-Level-4);
position: absolute;

View File

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

View File

@@ -16,7 +16,7 @@ import {
} from "react-aria-components"
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 { Results } from "../Results"

View File

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

View File

@@ -3,14 +3,16 @@
import {
Collection,
Header,
ListLayout,
Menu,
MenuItem,
MenuSection,
Text,
Virtualizer,
} from "react-aria-components"
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 styles from "./results.module.css"
@@ -25,20 +27,61 @@ export function Results({
const intl = useIntl()
return (
<Menu
aria-label={ariaLabel}
onAction={onAction}
className={styles.menu}
items={results}
renderEmptyState={() => {
return null
<Virtualizer
layout={ListLayout}
layoutOptions={{
estimatedRowHeight: 64,
estimatedHeadingHeight: 41,
}}
>
{(section) => {
if (section.id === "actions") {
<Menu
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 (
<MenuSection key={section.id} className={styles.actionsSection}>
<Header className="sr-only">{section.name}</Header>
<MenuSection key={section.id}>
<Typography variant="Title/Overline/sm">
<Header className={styles.sectionHeader}>{section.name}</Header>
</Typography>
<Collection items={section.children}>
{(item) => (
<MenuItem
@@ -47,59 +90,28 @@ export function Results({
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>
</>
<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>
)
}
return (
<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>
}}
</Menu>
</Virtualizer>
)
}

View File

@@ -1,13 +1,20 @@
.menu {
display: flex;
flex-direction: column;
gap: var(--Space-x2);
height: 100%;
overflow-y: auto;
}
.sectionHeader {
color: var(--UI-Text-Placeholder);
padding-left: var(--Space-x1);
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 {
@@ -45,9 +52,16 @@
color: var(--Text-Tertiary);
}
.actionsSection {
border-top: solid 1px var(--Border-Divider-Subtle);
padding-top: var(--Space-x2); /* match gap of .menu */
.menuDivider {
padding-top: var(--Space-x2);
padding-bottom: var(--Space-x2);
}
.menuDivider:before {
display: block;
content: "";
height: 1px;
background: var(--Border-Divider-Subtle);
}
.actionsSection .item {