Skip to content

Commit fe142f6

Browse files
Fix keyboard navigation scroll behavior (#22)
1 parent 151814f commit fe142f6

File tree

2 files changed

+102
-80
lines changed

2 files changed

+102
-80
lines changed

src/components/__tests__/emoji-picker.test.browser.tsx

Lines changed: 94 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -438,23 +438,22 @@ describe("EmojiPicker.Root", () => {
438438
it("should support disabling sticky category headers", async () => {
439439
page.render(
440440
<DefaultPage
441-
sticky={false}
442441
listComponents={{
443442
CategoryHeader: ({ category, ...props }) => (
444-
<div
445-
data-testid="category-header"
446-
{...props}
447-
>
443+
<div data-testid="category-header" {...props}>
448444
{category.label}
449445
</div>
450446
),
451447
}}
448+
sticky={false}
452449
/>,
453450
);
454451

455-
await expect.element(page.getByTestId("category-header").nth(1)).not.toHaveStyle({
456-
position: "sticky",
457-
});
452+
await expect
453+
.element(page.getByTestId("category-header").nth(1))
454+
.not.toHaveStyle({
455+
position: "sticky",
456+
});
458457
});
459458
});
460459

@@ -531,82 +530,100 @@ describe("EmojiPicker.Search", () => {
531530
});
532531

533532
describe("EmojiPicker.Viewport", () => {
534-
it("should virtualize rows based on the viewport height", async () => {
535-
function Page() {
536-
const [viewportHeight, setViewportHeight] = useState(400);
537-
const [rowHeight, setRowHeight] = useState(30);
538-
const [categoryHeaderHeight, setCategoryHeaderHeight] = useState(30);
539-
540-
return (
541-
<DefaultPage
542-
listComponents={{
543-
Row: ({ children, style, ...props }) => (
544-
<div
545-
data-testid="custom-row"
546-
{...props}
547-
style={{ ...style, height: rowHeight }}
548-
>
549-
{children}
550-
</div>
551-
),
552-
CategoryHeader: ({ category, style, ...props }) => (
553-
<div
554-
data-testid="custom-category-header"
555-
{...props}
556-
style={{ ...style, height: categoryHeaderHeight }}
557-
>
558-
{category.label}
559-
</div>
560-
),
561-
}}
562-
>
563-
<input
564-
data-testid="viewport-height"
565-
onChange={(event) => setViewportHeight(Number(event.target.value))}
566-
type="number"
567-
value={viewportHeight}
568-
/>
569-
<input
570-
data-testid="row-height"
571-
onChange={(event) => setRowHeight(Number(event.target.value))}
572-
type="number"
573-
value={rowHeight}
574-
/>
575-
<input
576-
data-testid="category-header-height"
577-
onChange={(event) =>
578-
setCategoryHeaderHeight(Number(event.target.value))
579-
}
580-
type="number"
581-
value={categoryHeaderHeight}
582-
/>
583-
</DefaultPage>
584-
);
585-
}
533+
it.each([
534+
["with sticky headers", true],
535+
["without sticky headers", false],
536+
])(
537+
"should virtualize rows based on the viewport height %s",
538+
async (_, sticky) => {
539+
function Page() {
540+
const [viewportHeight, setViewportHeight] = useState(400);
541+
const [rowHeight, setRowHeight] = useState(30);
542+
const [categoryHeaderHeight, setCategoryHeaderHeight] = useState(30);
543+
544+
return (
545+
<DefaultPage
546+
listComponents={{
547+
Row: ({ children, style, ...props }) => (
548+
<div
549+
data-testid="custom-row"
550+
{...props}
551+
style={{ ...style, height: rowHeight }}
552+
>
553+
{children}
554+
</div>
555+
),
556+
CategoryHeader: ({ category, style, ...props }) => (
557+
<div
558+
data-testid="custom-category-header"
559+
{...props}
560+
style={{ ...style, height: categoryHeaderHeight }}
561+
>
562+
{category.label}
563+
</div>
564+
),
565+
}}
566+
sticky={sticky}
567+
>
568+
<input
569+
data-testid="viewport-height"
570+
onChange={(event) =>
571+
setViewportHeight(Number(event.target.value))
572+
}
573+
type="number"
574+
value={viewportHeight}
575+
/>
576+
<input
577+
data-testid="row-height"
578+
onChange={(event) => setRowHeight(Number(event.target.value))}
579+
type="number"
580+
value={rowHeight}
581+
/>
582+
<input
583+
data-testid="category-header-height"
584+
onChange={(event) =>
585+
setCategoryHeaderHeight(Number(event.target.value))
586+
}
587+
type="number"
588+
value={categoryHeaderHeight}
589+
/>
590+
</DefaultPage>
591+
);
592+
}
586593

587-
page.render(<Page />);
594+
page.render(<Page />);
588595

589-
await expect.element(page.getByText("😀")).toBeInTheDocument();
596+
await expect.element(page.getByText("😀")).toBeInTheDocument();
590597

591-
await expect.element(page.getByRole("row").nth(10)).toBeInTheDocument();
592-
await expect.element(page.getByRole("row").nth(20)).not.toBeInTheDocument();
598+
await expect.element(page.getByRole("row").nth(10)).toBeInTheDocument();
599+
await expect
600+
.element(page.getByRole("row").nth(20))
601+
.not.toBeInTheDocument();
593602

594-
await page.getByTestId("viewport-height").fill("500");
595-
await page.getByTestId("row-height").fill("20");
596-
await page.getByTestId("category-header-height").fill("20");
603+
await page.getByTestId("viewport-height").fill("500");
604+
await page.getByTestId("row-height").fill("20");
605+
await page.getByTestId("category-header-height").fill("20");
597606

598-
await expect.element(page.getByRole("row").nth(10)).toBeInTheDocument();
599-
await expect.element(page.getByRole("row").nth(20)).toBeInTheDocument();
607+
await expect.element(page.getByRole("row").nth(10)).toBeInTheDocument();
608+
await expect.element(page.getByRole("row").nth(20)).toBeInTheDocument();
600609

601-
await page.getByTestId("viewport-height").fill("200");
602-
await page.getByTestId("row-height").fill("100");
603-
await page.getByTestId("category-header-height").fill("400");
610+
await page.getByTestId("viewport-height").fill("200");
611+
await page.getByTestId("row-height").fill("100");
612+
await page.getByTestId("category-header-height").fill("400");
604613

605-
await expect.element(page.getByRole("row").nth(10)).not.toBeInTheDocument();
606-
await expect.element(page.getByRole("row").nth(20)).not.toBeInTheDocument();
607-
});
614+
await expect
615+
.element(page.getByRole("row").nth(10))
616+
.not.toBeInTheDocument();
617+
await expect
618+
.element(page.getByRole("row").nth(20))
619+
.not.toBeInTheDocument();
620+
},
621+
);
608622

609-
it("should virtualize rows based on scroll", async () => {
623+
it.each([
624+
["with sticky headers", true],
625+
["without sticky headers", false],
626+
])("should virtualize rows based on scroll %s", async (_, sticky) => {
610627
function Page() {
611628
const scrollViewport = () => {
612629
const viewport = document.querySelector("[data-testid='viewport']");
@@ -618,7 +635,7 @@ describe("EmojiPicker.Viewport", () => {
618635
};
619636

620637
return (
621-
<DefaultPage viewportHeight={200}>
638+
<DefaultPage sticky={sticky} viewportHeight={200}>
622639
<button
623640
data-testid="scroll-viewport"
624641
onClick={scrollViewport}

src/store.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export function createEmojiPickerStore(
200200
const {
201201
listRef,
202202
viewportRef,
203+
sticky,
203204
rowHeight,
204205
viewportHeight,
205206
categoryHeaderHeight,
@@ -244,8 +245,8 @@ export function createEmojiPickerStore(
244245

245246
let viewportStartY = viewportScrollY + rowScrollMarginTop;
246247

247-
// Account for sticky headers if the row is in the upper half of the viewport
248-
if (rowY < viewportScrollY + viewportHeight / 2) {
248+
// Account for headers if they are sticky and if the row is in the upper half of the viewport
249+
if (sticky && rowY < viewportScrollY + viewportHeight / 2) {
249250
viewportStartY += categoryHeaderHeight;
250251
}
251252

@@ -257,7 +258,11 @@ export function createEmojiPickerStore(
257258
// Align to the viewport's top or bottom based on the row's position
258259
top: Math.max(
259260
rowY < viewportStartY + categoryHeaderHeight
260-
? rowY - Math.max(categoryHeaderHeight, rowScrollMarginTop)
261+
? rowY -
262+
Math.max(
263+
sticky ? categoryHeaderHeight : 0,
264+
rowScrollMarginTop,
265+
)
261266
: rowY - viewportHeight + rowHeight + rowScrollMarginBottom,
262267
0,
263268
),

0 commit comments

Comments
 (0)