Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions static/app/components/core/compactSelect/control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import type {
SearchConfig,
SearchMatchResult,
SelectKey,
SelectOption,
SelectOptionOrSection,
SelectOptionWithKey,
} from './types';
Expand Down Expand Up @@ -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 <TriggerLabel>{t('None')}</TriggerLabel>;
}

const selectedValues = new Set(values);
const options: Array<SelectOption<SelectKey>> = [];

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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the identifier too extra

}
}
return item;
})
.filter(item => values.includes(item.value));
}
}

if (options.length === 0) {
return <TriggerLabel>{t('None')}</TriggerLabel>;
Expand Down
19 changes: 15 additions & 4 deletions static/app/components/core/compactSelect/gridList/option.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -25,16 +25,20 @@ export interface GridListOptionProps<
listState: ListState<T>;
node: Node<T>;
size: FormSize;
'data-index'?: number;
ref?: React.Ref<HTMLLIElement>;
}

/**
* A <li /> element with accessibile behaviors & attributes.
* https://react-spectrum.adobe.com/react-aria/useGridList.html
*/
export function GridListOption<T extends ListItemBase>({
function GridListOptionComponent<T extends ListItemBase>({
node,
listState,
size,
ref: refProp,
'data-index': dataIndex,
}: GridListOptionProps<T>) {
const ref = useRef<HTMLLIElement>(null);
const {
Expand Down Expand Up @@ -125,7 +129,8 @@ export function GridListOption<T extends ListItemBase>({
return (
<StyledMenuListItem
{...rowPropsMerged}
ref={ref}
data-index={dataIndex}
ref={mergeRefs(ref, refProp)}
size={size}
label={renderedLabel}
details={details}
Expand All @@ -145,6 +150,12 @@ export function GridListOption<T extends ListItemBase>({
);
}

// 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};
Expand Down
8 changes: 6 additions & 2 deletions static/app/components/core/compactSelect/listBox/option.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,7 +29,7 @@ export interface ListBoxOptionProps extends AriaOptionProps {
* A <li /> element with accessible behaviors & attributes.
* https://react-spectrum.adobe.com/react-aria/useListBox.html
*/
export function ListBoxOption({
function ListBoxOptionComponent({
item,
listState,
size,
Expand Down Expand Up @@ -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};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function useVirtualizedItems<T extends ObjectLike>({
return heightEstimation.regular;
},
enabled: virtualized,
// Keep rich grid rows buffered so fast scrolling does not expose blank space.
overscan: 5,
});

if (virtualized) {
Expand Down
7 changes: 7 additions & 0 deletions static/app/components/core/compactSelect/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import type {
SelectSectionWithKey,
} from './types';

const EMPTY_HIDDEN_OPTIONS = new Set<SelectKey>();
const EMPTY_SEARCH_SCORES = new Map<SelectKey, number>();

/**
* Normalises the `search` prop into a plain config object (or `undefined` if
* search is disabled). Accepts `true` as shorthand for `{}` and treats
Expand Down Expand Up @@ -163,6 +166,10 @@ export function getHiddenOptions<Value extends SelectKey>(
search: string
) => SearchMatchResult
): {hidden: Set<SelectKey>; scores: Map<SelectKey, number>} {
if (!search && limit === Infinity) {
return {hidden: EMPTY_HIDDEN_OPTIONS, scores: EMPTY_SEARCH_SCORES};
}

const scores = new Map<SelectKey, number>();
const matcher = searchMatcher ?? defaultSearchMatcher;

Expand Down
Loading