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: '2', value: 2 },
{ label: '3', value: 3 },
{ label: '4', value: 4 },
{ label: '5', value: 5 },
{ label: '6', value: 6 },
],
value: 'banana',
},

View File

@ -10,6 +10,7 @@ import { Icon } from '../Icon/Icon';
import { AutoSizeInput } from '../Input/AutoSizeInput';
import { Input, Props as InputProps } from '../Input/Input';
import { Stack } from '../Layout/Stack/Stack';
import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
import { Text } from '../Text/Text';
import { getComboboxStyles } from './getComboboxStyles';
@ -132,7 +133,7 @@ export const Combobox = <T extends string | number>({
const virtualizerOptions = {
count: items.length,
getScrollElement: () => floatingRef.current,
getScrollElement: () => scrollRef.current,
estimateSize: () => OPTION_HEIGHT,
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(() => {
setInputValue(selectedItem?.label ?? value?.toString() ?? '');
@ -311,46 +312,48 @@ export const Combobox = <T extends string | number>({
'aria-labelledby': ariaLabelledBy,
})}
>
{isOpen && !asyncError && (
<ul style={{ height: rowVirtualizer.getTotalSize() }} className={styles.menuUlContainer}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
return (
<li
key={`${items[virtualRow.index].value}-${virtualRow.index}`}
data-index={virtualRow.index}
className={cx(
styles.option,
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>
<ScrollContainer showScrollIndicators maxHeight="inherit" ref={scrollRef}>
{isOpen && !asyncError && (
<ul style={{ height: rowVirtualizer.getTotalSize() }} className={styles.menuUlContainer}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
return (
<li
key={`${items[virtualRow.index].value}-${virtualRow.index}`}
data-index={virtualRow.index}
className={cx(
styles.option,
selectedItem && items[virtualRow.index].value === selectedItem.value && styles.optionSelected,
highlightedIndex === virtualRow.index && styles.optionFocused
)}
</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>
)}
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>
</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>
);

View File

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

View File

@ -23,6 +23,7 @@ export const useComboboxFloat = (
) => {
const inputRef = useRef<HTMLInputElement>(null);
const floatingRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [popoverMaxSize, setPopoverMaxSize] = useState<{ width: number; height: number } | undefined>(undefined);
const scrollbarWidth = useMemo(() => getScrollbarWidth(), []);
@ -79,7 +80,7 @@ export const useComboboxFloat = (
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