From 96768197eeaf16f60380fcd735f108d0ba1f8e59 Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Tue, 15 Jul 2025 08:48:03 +0200 Subject: [PATCH 1/2] Fix scroll behavior when keyboard navigating without sticky headers --- src/store.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/store.ts b/src/store.ts index 11b911d..8abd9d4 100644 --- a/src/store.ts +++ b/src/store.ts @@ -200,6 +200,7 @@ export function createEmojiPickerStore( const { listRef, viewportRef, + sticky, rowHeight, viewportHeight, categoryHeaderHeight, @@ -244,8 +245,8 @@ export function createEmojiPickerStore( let viewportStartY = viewportScrollY + rowScrollMarginTop; - // Account for sticky headers if the row is in the upper half of the viewport - if (rowY < viewportScrollY + viewportHeight / 2) { + // Account for headers if they are sticky and if the row is in the upper half of the viewport + if (sticky && rowY < viewportScrollY + viewportHeight / 2) { viewportStartY += categoryHeaderHeight; } @@ -257,7 +258,11 @@ export function createEmojiPickerStore( // Align to the viewport's top or bottom based on the row's position top: Math.max( rowY < viewportStartY + categoryHeaderHeight - ? rowY - Math.max(categoryHeaderHeight, rowScrollMarginTop) + ? rowY - + Math.max( + sticky ? categoryHeaderHeight : 0, + rowScrollMarginTop, + ) : rowY - viewportHeight + rowHeight + rowScrollMarginBottom, 0, ), From 40323065e39bdac3cf543658d296adf82c7f8812 Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Tue, 15 Jul 2025 09:30:49 +0200 Subject: [PATCH 2/2] Add more non-sticky tests --- .../__tests__/emoji-picker.test.browser.tsx | 171 ++++++++++-------- 1 file changed, 94 insertions(+), 77 deletions(-) diff --git a/src/components/__tests__/emoji-picker.test.browser.tsx b/src/components/__tests__/emoji-picker.test.browser.tsx index 378559b..107e099 100644 --- a/src/components/__tests__/emoji-picker.test.browser.tsx +++ b/src/components/__tests__/emoji-picker.test.browser.tsx @@ -438,23 +438,22 @@ describe("EmojiPicker.Root", () => { it("should support disabling sticky category headers", async () => { page.render( ( -
+
{category.label}
), }} + sticky={false} />, ); - await expect.element(page.getByTestId("category-header").nth(1)).not.toHaveStyle({ - position: "sticky", - }); + await expect + .element(page.getByTestId("category-header").nth(1)) + .not.toHaveStyle({ + position: "sticky", + }); }); }); @@ -531,82 +530,100 @@ describe("EmojiPicker.Search", () => { }); describe("EmojiPicker.Viewport", () => { - it("should virtualize rows based on the viewport height", async () => { - function Page() { - const [viewportHeight, setViewportHeight] = useState(400); - const [rowHeight, setRowHeight] = useState(30); - const [categoryHeaderHeight, setCategoryHeaderHeight] = useState(30); - - return ( - ( -
- {children} -
- ), - CategoryHeader: ({ category, style, ...props }) => ( -
- {category.label} -
- ), - }} - > - setViewportHeight(Number(event.target.value))} - type="number" - value={viewportHeight} - /> - setRowHeight(Number(event.target.value))} - type="number" - value={rowHeight} - /> - - setCategoryHeaderHeight(Number(event.target.value)) - } - type="number" - value={categoryHeaderHeight} - /> -
- ); - } + it.each([ + ["with sticky headers", true], + ["without sticky headers", false], + ])( + "should virtualize rows based on the viewport height %s", + async (_, sticky) => { + function Page() { + const [viewportHeight, setViewportHeight] = useState(400); + const [rowHeight, setRowHeight] = useState(30); + const [categoryHeaderHeight, setCategoryHeaderHeight] = useState(30); + + return ( + ( +
+ {children} +
+ ), + CategoryHeader: ({ category, style, ...props }) => ( +
+ {category.label} +
+ ), + }} + sticky={sticky} + > + + setViewportHeight(Number(event.target.value)) + } + type="number" + value={viewportHeight} + /> + setRowHeight(Number(event.target.value))} + type="number" + value={rowHeight} + /> + + setCategoryHeaderHeight(Number(event.target.value)) + } + type="number" + value={categoryHeaderHeight} + /> +
+ ); + } - page.render(); + page.render(); - await expect.element(page.getByText("😀")).toBeInTheDocument(); + await expect.element(page.getByText("😀")).toBeInTheDocument(); - await expect.element(page.getByRole("row").nth(10)).toBeInTheDocument(); - await expect.element(page.getByRole("row").nth(20)).not.toBeInTheDocument(); + await expect.element(page.getByRole("row").nth(10)).toBeInTheDocument(); + await expect + .element(page.getByRole("row").nth(20)) + .not.toBeInTheDocument(); - await page.getByTestId("viewport-height").fill("500"); - await page.getByTestId("row-height").fill("20"); - await page.getByTestId("category-header-height").fill("20"); + await page.getByTestId("viewport-height").fill("500"); + await page.getByTestId("row-height").fill("20"); + await page.getByTestId("category-header-height").fill("20"); - await expect.element(page.getByRole("row").nth(10)).toBeInTheDocument(); - await expect.element(page.getByRole("row").nth(20)).toBeInTheDocument(); + await expect.element(page.getByRole("row").nth(10)).toBeInTheDocument(); + await expect.element(page.getByRole("row").nth(20)).toBeInTheDocument(); - await page.getByTestId("viewport-height").fill("200"); - await page.getByTestId("row-height").fill("100"); - await page.getByTestId("category-header-height").fill("400"); + await page.getByTestId("viewport-height").fill("200"); + await page.getByTestId("row-height").fill("100"); + await page.getByTestId("category-header-height").fill("400"); - await expect.element(page.getByRole("row").nth(10)).not.toBeInTheDocument(); - await expect.element(page.getByRole("row").nth(20)).not.toBeInTheDocument(); - }); + await expect + .element(page.getByRole("row").nth(10)) + .not.toBeInTheDocument(); + await expect + .element(page.getByRole("row").nth(20)) + .not.toBeInTheDocument(); + }, + ); - it("should virtualize rows based on scroll", async () => { + it.each([ + ["with sticky headers", true], + ["without sticky headers", false], + ])("should virtualize rows based on scroll %s", async (_, sticky) => { function Page() { const scrollViewport = () => { const viewport = document.querySelector("[data-testid='viewport']"); @@ -618,7 +635,7 @@ describe("EmojiPicker.Viewport", () => { }; return ( - +