New Select: Extract floating ui setup into hook (#93387)

* New Select: Extratc floating ui setup into hook

* Remove unused exports

* Rename exported floatStyles

* Set maxHeight instead of using js to find it

* Extarct into seperate file
pull/93743/head
Tobias Skarhed 8 months ago committed by GitHub
parent 2651ce5dce
commit 1f7457c02c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 97
      packages/grafana-ui/src/components/Combobox/Combobox.tsx
  2. 8
      packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts
  3. 96
      packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts

@ -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<T extends string | number = string> = {
label: string;
@ -42,16 +42,6 @@ function itemFilter<T extends string | number>(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 = <T extends string | number>({
return null;
}, [selectedItemIndex, options, value]);
const inputRef = useRef<HTMLInputElement>(null);
const floatingRef = useRef<HTMLDivElement>(null);
const menuId = `downshift-${useId().replace(/:/g, '--')}-menu`;
const labelId = `downshift-${useId().replace(/:/g, '--')}-label`;
const styles = useStyles2(getComboboxStyles);
const [popoverMaxWidth, setPopoverMaxWidth] = useState<number | undefined>(undefined);
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
const virtualizerOptions = {
count: items.length,
@ -167,38 +152,12 @@ export const Combobox = <T extends string | number>({
}
},
});
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 (
<div>
<Input
@ -246,12 +205,9 @@ export const Combobox = <T extends string | number>({
})}
/>
<div
className={cx(styles.menu, hasMinHeight && styles.menuHeight, !isOpen && styles.menuClosed)}
className={cx(styles.menu, !isOpen && styles.menuClosed)}
style={{
...floatingStyles,
maxWidth: popoverMaxWidth,
minWidth: inputRef.current?.offsetWidth,
width: popoverWidth,
...floatStyles,
}}
{...getMenuProps({
ref: floatingRef,
@ -294,46 +250,3 @@ export const Combobox = <T extends string | number>({
</div>
);
};
const useDynamicWidth = (
items: Array<ComboboxOption<string | number>>,
range: { startIndex: number; endIndex: number } | null,
setPopoverWidth: { (value: SetStateAction<number | undefined>): 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]);
};

@ -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({

@ -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<ComboboxOption<string | number>>,
range: { startIndex: number; endIndex: number } | null,
isOpen: boolean
) => {
const inputRef = useRef<HTMLInputElement>(null);
const floatingRef = useRef<HTMLDivElement>(null);
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
const [popoverMaxWidth, setPopoverMaxWidth] = useState<number | undefined>(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 };
};
Loading…
Cancel
Save