Combobox: Squish the menu height in small viewports (#95568)

* Combobox: Squish the menu height in small viewports

* rename constants
This commit is contained in:
Josh Hunt 2024-10-29 17:47:31 +00:00 committed by GitHub
parent 261b4a5564
commit 952957248f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 15 deletions

View File

@ -330,6 +330,52 @@ export const Async: StoryObj<PropsAndCustomArgs> = {
render: AsyncStory, render: AsyncStory,
}; };
const noop = () => {};
const PositioningTestStory: StoryFn<PropsAndCustomArgs> = (args) => {
if (typeof args.options === 'function') {
throw new Error('This story does not support async options');
}
function renderColumnOfComboboxes(pos: string) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
flex: 1,
}}
>
<Combobox placeholder={`${pos} top`} options={args.options} value={null} onChange={noop} />
<Combobox placeholder={`${pos} middle`} options={args.options} value={null} onChange={noop} />
<Combobox placeholder={`${pos} bottom`} options={args.options} value={null} onChange={noop} />
</div>
);
}
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
// approx the height of the dev alert, and three margins. exact doesn't matter
minHeight: 'calc(100vh - (105px + 16px + 16px + 16px))',
justifyContent: 'space-between',
gap: 32,
}}
>
{renderColumnOfComboboxes('Left')}
{renderColumnOfComboboxes('Middle')}
{renderColumnOfComboboxes('Right')}
</div>
);
};
export const PositioningTest: StoryObj<PropsAndCustomArgs> = {
render: PositioningTestStory,
};
export const ComparisonToSelect: StoryObj<PropsAndCustomArgs> = { export const ComparisonToSelect: StoryObj<PropsAndCustomArgs> = {
args: { args: {
numberOfOptions: 100, numberOfOptions: 100,

View File

@ -10,7 +10,7 @@ import { AutoSizeInput } from '../Input/AutoSizeInput';
import { Input, Props as InputProps } from '../Input/Input'; import { Input, Props as InputProps } from '../Input/Input';
import { getComboboxStyles } from './getComboboxStyles'; import { getComboboxStyles } from './getComboboxStyles';
import { estimateSize, useComboboxFloat } from './useComboboxFloat'; import { useComboboxFloat, OPTION_HEIGHT } from './useComboboxFloat';
import { StaleResultError, useLatestAsyncCall } from './useLatestAsyncCall'; import { StaleResultError, useLatestAsyncCall } from './useLatestAsyncCall';
export type ComboboxOption<T extends string | number = string> = { export type ComboboxOption<T extends string | number = string> = {
@ -129,7 +129,7 @@ export const Combobox = <T extends string | number>({
const virtualizerOptions = { const virtualizerOptions = {
count: items.length, count: items.length,
getScrollElement: () => floatingRef.current, getScrollElement: () => floatingRef.current,
estimateSize, estimateSize: () => OPTION_HEIGHT,
overscan: 4, overscan: 4,
}; };

View File

@ -2,8 +2,6 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
const MAX_HEIGHT = 400;
// We need a px font size to accurately measure the width of items. // We need a px font size to accurately measure the width of items.
// This should be in sync with the body font size in the theme. // This should be in sync with the body font size in the theme.
export const MENU_ITEM_FONT_SIZE = 14; export const MENU_ITEM_FONT_SIZE = 14;
@ -20,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,
maxHeight: MAX_HEIGHT,
overflowY: 'auto', overflowY: 'auto',
position: 'relative', position: 'relative',
}), }),

View File

@ -9,12 +9,12 @@ import { MENU_ITEM_FONT_SIZE, MENU_ITEM_FONT_WEIGHT, MENU_ITEM_PADDING_X } from
// Only consider the first n items when calculating the width of the popover. // Only consider the first n items when calculating the width of the popover.
const WIDTH_CALCULATION_LIMIT_ITEMS = 100_000; const WIDTH_CALCULATION_LIMIT_ITEMS = 100_000;
/** // Used with Downshift to get the height of each item
* Used with Downshift to get the height of each item export const OPTION_HEIGHT = 45;
*/ const POPOVER_MAX_HEIGHT = OPTION_HEIGHT * 8.5;
export function estimateSize() {
return 45; // Clearance around the popover to prevent it from being too close to the edge of the viewport
} const POPOVER_PADDING = 16;
export const useComboboxFloat = ( export const useComboboxFloat = (
items: Array<ComboboxOption<string | number>>, items: Array<ComboboxOption<string | number>>,
@ -23,7 +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 [popoverMaxWidth, setPopoverMaxWidth] = useState<number | undefined>(undefined); const [popoverMaxSize, setPopoverMaxSize] = useState<{ width: number; height: number } | undefined>(undefined);
const scrollbarWidth = useMemo(() => getScrollbarWidth(), []); const scrollbarWidth = useMemo(() => getScrollbarWidth(), []);
@ -35,8 +35,14 @@ export const useComboboxFloat = (
boundary: document.body, boundary: document.body,
}), }),
size({ size({
apply({ availableWidth }) { apply({ availableWidth, availableHeight }) {
setPopoverMaxWidth(availableWidth); const preferredMaxWidth = availableWidth - POPOVER_PADDING;
const preferredMaxHeight = availableHeight - POPOVER_PADDING;
const width = Math.max(preferredMaxWidth, 0);
const height = Math.min(Math.max(preferredMaxHeight, OPTION_HEIGHT), POPOVER_MAX_HEIGHT);
setPopoverMaxSize({ width, height });
}, },
}), }),
]; ];
@ -67,8 +73,10 @@ export const useComboboxFloat = (
const floatStyles = { const floatStyles = {
...floatingStyles, ...floatingStyles,
width: longestItemWidth, width: longestItemWidth,
maxWidth: popoverMaxWidth, maxWidth: popoverMaxSize?.width,
minWidth: inputRef.current?.offsetWidth, minWidth: inputRef.current?.offsetWidth,
maxHeight: popoverMaxSize?.height,
}; };
return { inputRef, floatingRef, floatStyles }; return { inputRef, floatingRef, floatStyles };