From 944cc87f654dfa1781f42608e45c50b568499ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Thu, 25 Jul 2024 11:17:23 +0200 Subject: [PATCH] Combobox: Styling for dropdown (#90140) * Add getSelectStyles * Modify combobox styles * Fix option with description styles * Add highlightedIndex * Undo estimateSize changes * Create getComboboxStyles * Add floating ui to Combobox * Use elements to apply existing refs * Delete width on styles * Fix menu styling * Update packages/grafana-ui/src/components/Combobox/Combobox.tsx Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> * Changes suggested in reviews * Delete container styles * Delete container styles * Add calculated height to ul element * Show all options in the many options story * Replace deprecated code * Remove console.log * Fix ts error * Fix ts error * Fix val is mull error * Fix ts error * Add comment in the code * Modify the comment --------- Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> --- .../Combobox/Combobox.internal.story.tsx | 6 ++ .../src/components/Combobox/Combobox.tsx | 95 ++++++++++++------- .../components/Combobox/getComboboxStyles.ts | 81 ++++++++++++++++ 3 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx index 120e333e76d..329362149ae 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.internal.story.tsx @@ -44,6 +44,9 @@ const BasicWithState: StoryFn = (args) => { {...args} value={value} onChange={(val) => { + if (!val) { + return; + } setValue(val.value); action('onChange')(val); }} @@ -74,6 +77,9 @@ const ManyOptionsStory: StoryFn = ({ numberOfOptions }) => { options={options} value={value} onChange={(val) => { + if (!val) { + return; + } setValue(val.value); action('onChange')(val); }} diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index af247f7bae1..a045d938ade 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -1,4 +1,5 @@ -import { css } from '@emotion/css'; +import { cx } from '@emotion/css'; +import { autoUpdate, flip, useFloating } from '@floating-ui/react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useCombobox } from 'downshift'; import { useMemo, useRef, useState } from 'react'; @@ -7,6 +8,8 @@ import { useStyles2 } from '../../themes'; import { Icon } from '../Icon/Icon'; import { Input, Props as InputProps } from '../Input/Input'; +import { getComboboxStyles } from './getComboboxStyles'; + export type Value = string | number; export type Option = { label: string; @@ -16,7 +19,7 @@ export type Option = { interface ComboboxProps extends Omit { - onChange: (val: Option) => void; + onChange: (val: Option | null) => void; value: Value; options: Option[]; } @@ -42,20 +45,21 @@ function estimateSize() { } export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxProps) => { + const MIN_WIDTH = 400; const [items, setItems] = useState(options); const selectedItem = useMemo(() => options.find((option) => option.value === value) || null, [options, value]); - const listRef = useRef(null); - - const styles = useStyles2(getStyles); + const inputRef = useRef(null); + const floatingRef = useRef(null); + const styles = useStyles2(getComboboxStyles); const rowVirtualizer = useVirtualizer({ count: items.length, - getScrollElement: () => listRef.current, + getScrollElement: () => floatingRef.current, estimateSize, overscan: 2, }); - const { getInputProps, getMenuProps, getItemProps, isOpen } = useCombobox({ + const { getInputProps, getMenuProps, getItemProps, isOpen, highlightedIndex } = useCombobox({ items, itemToString, selectedItem, @@ -70,12 +74,48 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro } }, }); + + // the order of middleware is important! + const middleware = [ + flip({ + // see https://floating-ui.com/docs/flip#combining-with-shift + crossAxis: false, + boundary: document.body, + fallbackPlacements: ['top'], + }), + ]; + const elements = { reference: inputRef.current, floating: floatingRef.current }; + const { floatingStyles } = useFloating({ + open: isOpen, + placement: 'bottom', + middleware, + elements, + whileElementsMounted: autoUpdate, + }); + + const hasMinHeight = isOpen && rowVirtualizer.getTotalSize() >= MIN_WIDTH; + return (
- } {...restProps} {...getInputProps()} /> -
+ } + {...restProps} + {...getInputProps({ + ref: inputRef, + /* Empty onCall to avoid TS error + * See issue here: https://github.com/downshift-js/downshift/issues/718 + * Downshift repo: https://github.com/downshift-js/downshift/tree/master + */ + onChange: () => {}, + })} + /> +
{isOpen && ( -
    +
      {rowVirtualizer.getVirtualItems().map((virtualRow) => { return (
    • - {items[virtualRow.index].label} - {items[virtualRow.index].description && {items[virtualRow.index].description}} +
      + {items[virtualRow.index].label} + {items[virtualRow.index].description && ( + {items[virtualRow.index].description} + )} +
    • ); })} @@ -99,24 +147,3 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro
); }; - -const getStyles = () => ({ - dropdown: css({ - position: 'absolute', - height: 400, - width: 600, - overflowY: 'scroll', - contain: 'strict', - }), - menuItem: css({ - position: 'absolute', - top: 0, - left: 0, - width: '100%', - display: 'flex', - flexDirection: 'column', - '&:first-child': { - fontWeight: 'bold', - }, - }), -}); diff --git a/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts new file mode 100644 index 00000000000..2deec9dde09 --- /dev/null +++ b/packages/grafana-ui/src/components/Combobox/getComboboxStyles.ts @@ -0,0 +1,81 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; + +export const getComboboxStyles = (theme: GrafanaTheme2) => { + return { + menu: css({ + label: 'grafana-select-menu', + background: theme.components.dropdown.background, + boxShadow: theme.shadows.z3, + position: 'relative', + zIndex: 1, + }), + menuHeight: css({ + height: 400, + overflowY: 'scroll', + position: 'relative', + }), + menuUlContainer: css({ + label: 'grafana-select-menu-ul-container', + listStyle: 'none', + }), + option: css({ + label: 'grafana-select-option', + position: 'absolute', + top: 0, + left: 0, + width: '100%', + whiteSpace: 'nowrap', + cursor: 'pointer', + borderLeft: '2px solid transparent', + padding: theme.spacing.x1, + boxSizing: 'border-box', + height: 'auto', + '&:hover': { + background: theme.colors.action.hover, + '@media (forced-colors: active), (prefers-contrast: more)': { + border: `1px solid ${theme.colors.primary.border}`, + }, + }, + }), + optionBody: css({ + label: 'grafana-select-option-body', + display: 'flex', + fontWeight: theme.typography.fontWeightMedium, + flexDirection: 'column', + flexGrow: 1, + }), + optionDescription: css({ + label: 'grafana-select-option-description', + fontWeight: 'normal', + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + whiteSpace: 'normal', + lineHeight: theme.typography.body.lineHeight, + }), + optionFocused: css({ + label: 'grafana-select-option-focused', + top: 0, + background: theme.colors.action.focus, + '@media (forced-colors: active), (prefers-contrast: more)': { + border: `1px solid ${theme.colors.primary.border}`, + }, + }), + optionSelected: css({ + background: theme.colors.action.selected, + '&::before': { + backgroundImage: theme.colors.gradients.brandVertical, + borderRadius: theme.shape.radius.default, + content: '" "', + display: 'block', + height: '100%', + position: 'absolute', + transform: 'translateX(-50%)', + width: theme.spacing(0.5), + left: 0, + top: 0, + }, + }), + }; +};