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) => (
+
+ ))}
+
+ );
+};