From 65bfefb5264be20b8bc5e5f1b9863b256ab35641 Mon Sep 17 00:00:00 2001 From: Josh Wooding <12938082+joshwooding@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:56:09 +0100 Subject: [PATCH] Improve list based control's performance when lots of items are displayed (#4246) --- .changeset/tiny-fishes-guess.md | 5 +++ .../__e2e__/combo-box/ComboBox.cy.tsx | 21 +++++++++++ .../core/src/list-control/ListControlState.ts | 36 +++++++++++++++---- .../stories/combo-box/combo-box.stories.tsx | 12 +++++++ 4 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 .changeset/tiny-fishes-guess.md diff --git a/.changeset/tiny-fishes-guess.md b/.changeset/tiny-fishes-guess.md new file mode 100644 index 00000000000..790e559b327 --- /dev/null +++ b/.changeset/tiny-fishes-guess.md @@ -0,0 +1,5 @@ +--- +"@salt-ds/core": patch +--- + +Improved list based control's performance when lots of items are displayed. diff --git a/packages/core/src/__tests__/__e2e__/combo-box/ComboBox.cy.tsx b/packages/core/src/__tests__/__e2e__/combo-box/ComboBox.cy.tsx index 70fec507524..e3f2bec7e33 100644 --- a/packages/core/src/__tests__/__e2e__/combo-box/ComboBox.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/combo-box/ComboBox.cy.tsx @@ -21,6 +21,7 @@ const { MultiplePillsTruncated, SelectOnTab, LongList, + PerformanceTest, } = composeStories(comboBoxStories); describe("Given a ComboBox", () => { @@ -730,4 +731,24 @@ describe("Given a ComboBox", () => { cy.findAllByRole("option").eq(0).realClick(); cy.get("@blurSpy").should("not.have.been.called"); }); + + it("should support 10000 items without much delay", () => { + cy.mount(); + + cy.findByRole("combobox").should("be.visible"); + + cy.window().its("performance").invoke("mark", "open_start"); + + cy.findByRole("combobox").realClick(); + + cy.findByRole("listbox", { timeout: 30000 }).should("be.visible"); + + cy.window().its("performance").invoke("mark", "open_end"); + + cy.window() + .its("performance") + .invoke("measure", "open_duration", "open_start", "open_end") + .its("duration", { timeout: 0 }) + .should("be.lessThan", 5000); + }); }); diff --git a/packages/core/src/list-control/ListControlState.ts b/packages/core/src/list-control/ListControlState.ts index 66970747027..4d045b58632 100644 --- a/packages/core/src/list-control/ListControlState.ts +++ b/packages/core/src/list-control/ListControlState.ts @@ -53,6 +53,35 @@ export type ListControlProps = { valueToString?: (item: Item) => string; }; +function findElementPosition( + elements: { element: HTMLElement }[], + element: HTMLElement, +) { + if (elements.length === 0) { + return 0; + } + + if ( + element.compareDocumentPosition(elements[elements.length - 1].element) & + Node.DOCUMENT_POSITION_PRECEDING + ) { + return -1; + } + + if ( + element.compareDocumentPosition(elements[0].element) & + Node.DOCUMENT_POSITION_FOLLOWING + ) { + return 0; + } + + return elements.findIndex( + (option) => + option.element.compareDocumentPosition(element) & + Node.DOCUMENT_POSITION_PRECEDING, + ); +} + export function defaultValueToString(item: Item): string { return String(item); } @@ -165,12 +194,7 @@ export function useListControl(props: ListControlProps) { (optionValue: OptionValue, element: HTMLElement) => { const { id } = optionValue; const option = optionsRef.current.find((item) => item.data.id === id); - const index = optionsRef.current.findIndex((option) => { - return ( - option.element.compareDocumentPosition(element) & - Node.DOCUMENT_POSITION_PRECEDING - ); - }); + const index = findElementPosition(optionsRef.current, element); if (!option) { if (index === -1) { diff --git a/packages/core/stories/combo-box/combo-box.stories.tsx b/packages/core/stories/combo-box/combo-box.stories.tsx index 3051a18b9c3..63521e56576 100644 --- a/packages/core/stories/combo-box/combo-box.stories.tsx +++ b/packages/core/stories/combo-box/combo-box.stories.tsx @@ -911,3 +911,15 @@ export const Bordered = () => { ); }; + +export const PerformanceTest = () => { + return ( + + {Array.from({ length: 10000 }).map((_, index) => ( + + ))} + + ); +};