diff --git a/static/app/components/core/compactSelect/control.tsx b/static/app/components/core/compactSelect/control.tsx
index 305eb5caeee4..22e3a8396639 100644
--- a/static/app/components/core/compactSelect/control.tsx
+++ b/static/app/components/core/compactSelect/control.tsx
@@ -38,6 +38,7 @@ import type {
SearchConfig,
SearchMatchResult,
SelectKey,
+ SelectOption,
SelectOptionOrSection,
SelectOptionWithKey,
} from './types';
@@ -435,15 +436,25 @@ export function Control({
* selected, then a count badge will appear.
*/
const triggerLabel: React.ReactNode = useMemo(() => {
- const values = Array.isArray(value) ? value : [value];
- const options = items
- .flatMap(item => {
- if ('options' in item) {
- return item.options;
+ const values = Array.isArray(value) ? value : value === undefined ? [] : [value];
+ if (values.length === 0) {
+ return {t('None')};
+ }
+
+ const selectedValues = new Set(values);
+ const options: Array> = [];
+
+ findSelectedOptions: for (const item of items) {
+ const itemOptions = 'options' in item ? item.options : [item];
+ for (const option of itemOptions) {
+ if (selectedValues.has(option.value)) {
+ options.push(option);
+ if (options.length === selectedValues.size) {
+ break findSelectedOptions;
+ }
}
- return item;
- })
- .filter(item => values.includes(item.value));
+ }
+ }
if (options.length === 0) {
return {t('None')};
diff --git a/static/app/components/core/compactSelect/gridList/option.tsx b/static/app/components/core/compactSelect/gridList/option.tsx
index 84fd1ef8983b..a1d6c85dbc0e 100644
--- a/static/app/components/core/compactSelect/gridList/option.tsx
+++ b/static/app/components/core/compactSelect/gridList/option.tsx
@@ -1,9 +1,9 @@
-import {Fragment, useContext, useMemo, useRef, useState} from 'react';
+import {Fragment, memo, useContext, useMemo, useRef, useState} from 'react';
import styled from '@emotion/styled';
import type {AriaGridListItemOptions} from '@react-aria/gridlist';
import {useGridListItem, useGridListSelectionCheckbox} from '@react-aria/gridlist';
import {useFocusWithin, useHover} from '@react-aria/interactions';
-import {mergeProps} from '@react-aria/utils';
+import {mergeProps, mergeRefs} from '@react-aria/utils';
import type {ListState} from '@react-stately/list';
import type {Node} from '@react-types/shared';
@@ -25,16 +25,20 @@ export interface GridListOptionProps<
listState: ListState;
node: Node;
size: FormSize;
+ 'data-index'?: number;
+ ref?: React.Ref;
}
/**
* A element with accessibile behaviors & attributes.
* https://react-spectrum.adobe.com/react-aria/useGridList.html
*/
-export function GridListOption({
+function GridListOptionComponent({
node,
listState,
size,
+ ref: refProp,
+ 'data-index': dataIndex,
}: GridListOptionProps) {
const ref = useRef(null);
const {
@@ -125,7 +129,8 @@ export function GridListOption({
return (
({
);
}
+// Virtualizer range changes re-render the list. Retained rows keep stable props, so
+// memoization limits each scroll update to rows entering or leaving the buffer.
+export const GridListOption = memo(
+ GridListOptionComponent
+) as typeof GridListOptionComponent;
+
const StyledMenuListItem = styled(MenuListItem)`
> ${InnerWrap} {
padding-left: ${p => p.theme.space.md};
diff --git a/static/app/components/core/compactSelect/listBox/option.tsx b/static/app/components/core/compactSelect/listBox/option.tsx
index 9c33dabf1369..20c8f79a0fc1 100644
--- a/static/app/components/core/compactSelect/listBox/option.tsx
+++ b/static/app/components/core/compactSelect/listBox/option.tsx
@@ -1,4 +1,4 @@
-import {Fragment, useContext, useMemo, useRef} from 'react';
+import {Fragment, memo, useContext, useMemo, useRef} from 'react';
import styled from '@emotion/styled';
import type {AriaOptionProps} from '@react-aria/listbox';
import {useOption} from '@react-aria/listbox';
@@ -29,7 +29,7 @@ export interface ListBoxOptionProps extends AriaOptionProps {
* A element with accessible behaviors & attributes.
* https://react-spectrum.adobe.com/react-aria/useListBox.html
*/
-export function ListBoxOption({
+function ListBoxOptionComponent({
item,
listState,
size,
@@ -129,6 +129,10 @@ export function ListBoxOption({
);
}
+// Virtualizer range changes re-render the list. Retained rows keep stable props, so
+// memoization limits each scroll update to rows entering or leaving the buffer.
+export const ListBoxOption = memo(ListBoxOptionComponent);
+
const StyledMenuListItem = styled(MenuListItem)`
> ${InnerWrap} {
padding-left: ${p => p.theme.space.md};
diff --git a/static/app/components/core/compactSelect/useVirtualizedItems.tsx b/static/app/components/core/compactSelect/useVirtualizedItems.tsx
index 0a1e876db060..17c0fcf02f45 100644
--- a/static/app/components/core/compactSelect/useVirtualizedItems.tsx
+++ b/static/app/components/core/compactSelect/useVirtualizedItems.tsx
@@ -44,6 +44,8 @@ export function useVirtualizedItems({
return heightEstimation.regular;
},
enabled: virtualized,
+ // Keep rich grid rows buffered so fast scrolling does not expose blank space.
+ overscan: 5,
});
if (virtualized) {
diff --git a/static/app/components/core/compactSelect/utils.tsx b/static/app/components/core/compactSelect/utils.tsx
index aaafd22a4b17..b269021d39b0 100644
--- a/static/app/components/core/compactSelect/utils.tsx
+++ b/static/app/components/core/compactSelect/utils.tsx
@@ -22,6 +22,9 @@ import type {
SelectSectionWithKey,
} from './types';
+const EMPTY_HIDDEN_OPTIONS = new Set();
+const EMPTY_SEARCH_SCORES = new Map();
+
/**
* Normalises the `search` prop into a plain config object (or `undefined` if
* search is disabled). Accepts `true` as shorthand for `{}` and treats
@@ -163,6 +166,10 @@ export function getHiddenOptions(
search: string
) => SearchMatchResult
): {hidden: Set; scores: Map} {
+ if (!search && limit === Infinity) {
+ return {hidden: EMPTY_HIDDEN_OPTIONS, scores: EMPTY_SEARCH_SCORES};
+ }
+
const scores = new Map();
const matcher = searchMatcher ?? defaultSearchMatcher;