diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index cb8a5f7bbba..33eeed308eb 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -1,8 +1,7 @@ import { cx } from '@emotion/css'; -import { autoUpdate, flip, size, useFloating } from '@floating-ui/react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useCombobox } from 'downshift'; -import { SetStateAction, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; +import { useCallback, useId, useMemo, useState } from 'react'; import { useStyles2 } from '../../themes'; import { t } from '../../utils/i18n'; @@ -10,6 +9,7 @@ import { Icon } from '../Icon/Icon'; import { Input, Props as InputProps } from '../Input/Input'; import { getComboboxStyles } from './getComboboxStyles'; +import { estimateSize, useComboboxFloat } from './useComboboxFloat'; export type ComboboxOption = { label: string; @@ -42,16 +42,6 @@ function itemFilter(inputValue: string) { }; } -function estimateSize() { - return 45; -} - -const MIN_HEIGHT = 400; -// On every 100th index we will recalculate the width of the popover. -const INDEX_WIDTH_CALCULATION = 100; -// A multiplier guesstimate times the amount of characters. If any padding or image support etc. is added this will need to be updated. -const WIDTH_MULTIPLIER = 7.3; - /** * A performant Select replacement. * @@ -97,15 +87,10 @@ export const Combobox = ({ return null; }, [selectedItemIndex, options, value]); - const inputRef = useRef(null); - const floatingRef = useRef(null); - const menuId = `downshift-${useId().replace(/:/g, '--')}-menu`; const labelId = `downshift-${useId().replace(/:/g, '--')}-label`; const styles = useStyles2(getComboboxStyles); - const [popoverMaxWidth, setPopoverMaxWidth] = useState(undefined); - const [popoverWidth, setPopoverWidth] = useState(undefined); const virtualizerOptions = { count: items.length, @@ -167,38 +152,12 @@ export const Combobox = ({ } }, }); + const { inputRef, floatingRef, floatStyles } = useComboboxFloat(items, rowVirtualizer.range, isOpen); const onBlur = useCallback(() => { setInputValue(selectedItem?.label ?? value?.toString() ?? ''); }, [selectedItem, setInputValue, value]); - // the order of middleware is important! - const middleware = [ - flip({ - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: true, - boundary: document.body, - }), - size({ - apply({ availableWidth }) { - setPopoverMaxWidth(availableWidth); - }, - }), - ]; - const elements = { reference: inputRef.current, floating: floatingRef.current }; - const { floatingStyles } = useFloating({ - strategy: 'fixed', - open: isOpen, - placement: 'bottom-start', - middleware, - elements, - whileElementsMounted: autoUpdate, - }); - - const hasMinHeight = isOpen && rowVirtualizer.getTotalSize() >= MIN_HEIGHT; - - useDynamicWidth(items, rowVirtualizer.range, setPopoverWidth); - return (
({ })} />
({
); }; - -const useDynamicWidth = ( - items: Array>, - range: { startIndex: number; endIndex: number } | null, - setPopoverWidth: { (value: SetStateAction): void } -) => { - useEffect(() => { - if (range === null) { - return; - } - const startVisibleIndex = range?.startIndex; - const endVisibleIndex = range?.endIndex; - - if (typeof startVisibleIndex === 'undefined' || typeof endVisibleIndex === 'undefined') { - return; - } - - // Scroll down and default case - if ( - startVisibleIndex === 0 || - (startVisibleIndex % INDEX_WIDTH_CALCULATION === 0 && startVisibleIndex >= INDEX_WIDTH_CALCULATION) - ) { - let maxLength = 0; - const calculationEnd = Math.min(items.length, endVisibleIndex + INDEX_WIDTH_CALCULATION); - - for (let i = startVisibleIndex; i < calculationEnd; i++) { - maxLength = Math.max(maxLength, items[i].label.length); - } - - setPopoverWidth(maxLength * WIDTH_MULTIPLIER); - } else if (endVisibleIndex % INDEX_WIDTH_CALCULATION === 0 && endVisibleIndex >= INDEX_WIDTH_CALCULATION) { - // Scroll up case - let maxLength = 0; - const calculationStart = Math.max(0, startVisibleIndex - INDEX_WIDTH_CALCULATION); - - for (let i = calculationStart; i < endVisibleIndex; i++) { - maxLength = Math.max(maxLength, items[i].label.length); - } - - setPopoverWidth(maxLength * WIDTH_MULTIPLIER); - } - }, [items, range, setPopoverWidth]); -}; diff --git a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts index ae5c94bc409..d6de3c76dfd 100644 --- a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts +++ b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts @@ -2,6 +2,8 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; +const MAX_HEIGHT = 400; + export const getComboboxStyles = (theme: GrafanaTheme2) => { return { menuClosed: css({ @@ -12,10 +14,8 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => { background: theme.components.dropdown.background, boxShadow: theme.shadows.z3, zIndex: theme.zIndex.dropdown, - }), - menuHeight: css({ - height: 400, - overflowY: 'scroll', + maxHeight: MAX_HEIGHT, + overflowY: 'auto', position: 'relative', }), menuUlContainer: css({ diff --git a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts new file mode 100644 index 00000000000..bdac3088f96 --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts @@ -0,0 +1,96 @@ +import { autoUpdate, flip, size, useFloating } from '@floating-ui/react'; +import { useEffect, useRef, useState } from 'react'; + +import { ComboboxOption } from './Combobox'; + +// On every 100th index we will recalculate the width of the popover. +const INDEX_WIDTH_CALCULATION = 100; +// A multiplier guesstimate times the amount of characters. If any padding or image support etc. is added this will need to be updated. +const WIDTH_MULTIPLIER = 7.3; + +/** + * Used with Downshift to get the height of each item + */ +export function estimateSize() { + return 45; +} + +export const useComboboxFloat = ( + items: Array>, + range: { startIndex: number; endIndex: number } | null, + isOpen: boolean +) => { + const inputRef = useRef(null); + const floatingRef = useRef(null); + const [popoverWidth, setPopoverWidth] = useState(undefined); + const [popoverMaxWidth, setPopoverMaxWidth] = useState(undefined); + + // the order of middleware is important! + const middleware = [ + flip({ + // see https://floating-ui.com/docs/flip#combining-with-shift + crossAxis: true, + boundary: document.body, + }), + size({ + apply({ availableWidth }) { + setPopoverMaxWidth(availableWidth); + }, + }), + ]; + const elements = { reference: inputRef.current, floating: floatingRef.current }; + const { floatingStyles } = useFloating({ + strategy: 'fixed', + open: isOpen, + placement: 'bottom-start', + middleware, + elements, + whileElementsMounted: autoUpdate, + }); + + useEffect(() => { + if (range === null) { + return; + } + const startVisibleIndex = range?.startIndex; + const endVisibleIndex = range?.endIndex; + + if (typeof startVisibleIndex === 'undefined' || typeof endVisibleIndex === 'undefined') { + return; + } + + // Scroll down and default case + if ( + startVisibleIndex === 0 || + (startVisibleIndex % INDEX_WIDTH_CALCULATION === 0 && startVisibleIndex >= INDEX_WIDTH_CALCULATION) + ) { + let maxLength = 0; + const calculationEnd = Math.min(items.length, endVisibleIndex + INDEX_WIDTH_CALCULATION); + + for (let i = startVisibleIndex; i < calculationEnd; i++) { + maxLength = Math.max(maxLength, items[i].label.length); + } + + setPopoverWidth(maxLength * WIDTH_MULTIPLIER); + } else if (endVisibleIndex % INDEX_WIDTH_CALCULATION === 0 && endVisibleIndex >= INDEX_WIDTH_CALCULATION) { + // Scroll up case + let maxLength = 0; + const calculationStart = Math.max(0, startVisibleIndex - INDEX_WIDTH_CALCULATION); + + for (let i = calculationStart; i < endVisibleIndex; i++) { + maxLength = Math.max(maxLength, items[i].label.length); + } + + setPopoverWidth(maxLength * WIDTH_MULTIPLIER); + } + }, [items, range, setPopoverWidth]); + + const floatStyles = { + ...floatingStyles, + width: popoverWidth, + maxWidth: popoverMaxWidth, + minWidth: inputRef.current?.offsetWidth, + }; + + return { inputRef, floatingRef, floatStyles }; +};