Combobox: Use ScrollContainer to show scroll indicators (#95962)

Add CustomScrollbar
This commit is contained in:
Tobias Skarhed 2024-11-06 15:35:31 +01:00 committed by GitHub
parent 06bdfe8e96
commit a279220d74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 49 additions and 43 deletions

View File

@ -42,6 +42,9 @@ const meta: Meta<PropsAndCustomArgs> = {
{ label: '1', value: 1 }, { label: '1', value: 1 },
{ label: '2', value: 2 }, { label: '2', value: 2 },
{ label: '3', value: 3 }, { label: '3', value: 3 },
{ label: '4', value: 4 },
{ label: '5', value: 5 },
{ label: '6', value: 6 },
], ],
value: 'banana', value: 'banana',
}, },

View File

@ -10,6 +10,7 @@ import { Icon } from '../Icon/Icon';
import { AutoSizeInput } from '../Input/AutoSizeInput'; import { AutoSizeInput } from '../Input/AutoSizeInput';
import { Input, Props as InputProps } from '../Input/Input'; import { Input, Props as InputProps } from '../Input/Input';
import { Stack } from '../Layout/Stack/Stack'; import { Stack } from '../Layout/Stack/Stack';
import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
import { Text } from '../Text/Text'; import { Text } from '../Text/Text';
import { getComboboxStyles } from './getComboboxStyles'; import { getComboboxStyles } from './getComboboxStyles';
@ -132,7 +133,7 @@ export const Combobox = <T extends string | number>({
const virtualizerOptions = { const virtualizerOptions = {
count: items.length, count: items.length,
getScrollElement: () => floatingRef.current, getScrollElement: () => scrollRef.current,
estimateSize: () => OPTION_HEIGHT, estimateSize: () => OPTION_HEIGHT,
overscan: 4, overscan: 4,
}; };
@ -239,7 +240,7 @@ export const Combobox = <T extends string | number>({
}, },
}); });
const { inputRef, floatingRef, floatStyles } = useComboboxFloat(items, rowVirtualizer.range, isOpen); const { inputRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, rowVirtualizer.range, isOpen);
const onBlur = useCallback(() => { const onBlur = useCallback(() => {
setInputValue(selectedItem?.label ?? value?.toString() ?? ''); setInputValue(selectedItem?.label ?? value?.toString() ?? '');
@ -311,46 +312,48 @@ export const Combobox = <T extends string | number>({
'aria-labelledby': ariaLabelledBy, 'aria-labelledby': ariaLabelledBy,
})} })}
> >
{isOpen && !asyncError && ( <ScrollContainer showScrollIndicators maxHeight="inherit" ref={scrollRef}>
<ul style={{ height: rowVirtualizer.getTotalSize() }} className={styles.menuUlContainer}> {isOpen && !asyncError && (
{rowVirtualizer.getVirtualItems().map((virtualRow) => { <ul style={{ height: rowVirtualizer.getTotalSize() }} className={styles.menuUlContainer}>
return ( {rowVirtualizer.getVirtualItems().map((virtualRow) => {
<li return (
key={`${items[virtualRow.index].value}-${virtualRow.index}`} <li
data-index={virtualRow.index} key={`${items[virtualRow.index].value}-${virtualRow.index}`}
className={cx( data-index={virtualRow.index}
styles.option, className={cx(
selectedItem && items[virtualRow.index].value === selectedItem.value && styles.optionSelected, styles.option,
highlightedIndex === virtualRow.index && styles.optionFocused selectedItem && items[virtualRow.index].value === selectedItem.value && styles.optionSelected,
)} highlightedIndex === virtualRow.index && styles.optionFocused
style={{
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
}}
{...getItemProps({
item: items[virtualRow.index],
index: virtualRow.index,
})}
>
<div className={styles.optionBody}>
<span className={styles.optionLabel}>
{items[virtualRow.index].label ?? items[virtualRow.index].value}
</span>
{items[virtualRow.index].description && (
<span className={styles.optionDescription}>{items[virtualRow.index].description}</span>
)} )}
</div> style={{
</li> height: virtualRow.size,
); transform: `translateY(${virtualRow.start}px)`,
})} }}
</ul> {...getItemProps({
)} item: items[virtualRow.index],
{asyncError && ( index: virtualRow.index,
<Stack justifyContent="center" alignItems="center" height={8}> })}
<Icon name="exclamation-triangle" size="md" className={styles.warningIcon} /> >
<Text color="secondary">{t('combobox.async.error', 'An error occurred while loading options.')}</Text> <div className={styles.optionBody}>
</Stack> <span className={styles.optionLabel}>
)} {items[virtualRow.index].label ?? items[virtualRow.index].value}
</span>
{items[virtualRow.index].description && (
<span className={styles.optionDescription}>{items[virtualRow.index].description}</span>
)}
</div>
</li>
);
})}
</ul>
)}
{asyncError && (
<Stack justifyContent="center" alignItems="center" height={8}>
<Icon name="exclamation-triangle" size="md" className={styles.warningIcon} />
<Text color="secondary">{t('combobox.async.error', 'An error occurred while loading options.')}</Text>
</Stack>
)}
</ScrollContainer>
</div> </div>
</div> </div>
); );

View File

@ -18,7 +18,6 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
background: theme.components.dropdown.background, background: theme.components.dropdown.background,
boxShadow: theme.shadows.z3, boxShadow: theme.shadows.z3,
zIndex: theme.zIndex.dropdown, zIndex: theme.zIndex.dropdown,
overflowY: 'auto',
position: 'relative', position: 'relative',
}), }),
menuUlContainer: css({ menuUlContainer: css({

View File

@ -23,6 +23,7 @@ export const useComboboxFloat = (
) => { ) => {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const floatingRef = useRef<HTMLDivElement>(null); const floatingRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [popoverMaxSize, setPopoverMaxSize] = useState<{ width: number; height: number } | undefined>(undefined); const [popoverMaxSize, setPopoverMaxSize] = useState<{ width: number; height: number } | undefined>(undefined);
const scrollbarWidth = useMemo(() => getScrollbarWidth(), []); const scrollbarWidth = useMemo(() => getScrollbarWidth(), []);
@ -79,7 +80,7 @@ export const useComboboxFloat = (
maxHeight: popoverMaxSize?.height, maxHeight: popoverMaxSize?.height,
}; };
return { inputRef, floatingRef, floatStyles }; return { inputRef, floatingRef, scrollRef, floatStyles };
}; };
// Creates a temporary div with a scrolling inner div to calculate the width of the scrollbar // Creates a temporary div with a scrolling inner div to calculate the width of the scrollbar