diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx index 65ed4df6879..2bf489a64c6 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationInfoButton.tsx @@ -1,21 +1,12 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationInfoButton.tsx import { css } from '@emotion/css'; -import { - autoUpdate, - flip, - offset, - shift, - useClick, - useDismiss, - useFloating, - useInteractions, -} from '@floating-ui/react'; +import { autoUpdate, offset, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { memo, useState } from 'react'; import { GrafanaTheme2, renderMarkdown } from '@grafana/data'; import { t } from '@grafana/i18n'; import { FlexItem } from '@grafana/plugin-ui'; -import { Button, Portal, useStyles2 } from '@grafana/ui'; +import { Button, floatingUtils, Portal, useStyles2 } from '@grafana/ui'; import { QueryBuilderOperation, QueryBuilderOperationDef } from './types'; @@ -29,16 +20,7 @@ export const OperationInfoButton = memo(({ def, operation }) => { const [show, setShow] = useState(false); // the order of middleware is important! - const middleware = [ - offset(16), - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = [offset(16), ...floatingUtils.getPositioningMiddleware()]; const { context, refs, floatingStyles } = useFloating({ open: show, diff --git a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts index 760f6bbb4fb..f2bf1f968db 100644 --- a/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts +++ b/packages/grafana-ui/src/components/Combobox/useComboboxFloat.ts @@ -1,6 +1,7 @@ import { autoUpdate, autoPlacement, size, useFloating } from '@floating-ui/react'; import { useMemo, useRef, useState } from 'react'; +import { BOUNDARY_ELEMENT_ID } from '../../utils/floating'; import { measureText } from '../../utils/measureText'; import { @@ -36,7 +37,7 @@ export const useComboboxFloat = (items: Array>, autoPlacement({ // see https://floating-ui.com/docs/autoplacement allowedPlacements: ['bottom-start', 'bottom-end', 'top-start', 'top-end'], - boundary: document.body, + boundary: document.getElementById(BOUNDARY_ELEMENT_ID) ?? undefined, crossAxis: true, }), size({ diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx index 1439992caf5..e86bbd725e6 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/react'; +import { autoUpdate, offset, useFloating } from '@floating-ui/react'; import Prism, { Grammar, LanguageMap } from 'prismjs'; import { memo, useEffect, useRef, useState } from 'react'; import * as React from 'react'; @@ -12,6 +12,7 @@ import { DataLinkBuiltInVars, GrafanaTheme2, VariableOrigin, VariableSuggestion import { SlatePrism } from '../../slate-plugins/slate-prism'; import { useStyles2 } from '../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../utils/floating'; import { SCHEMA, makeValue } from '../../utils/slate'; import { getInputStyles } from '../Input/Input'; import { Portal } from '../Portal/Portal'; @@ -97,13 +98,7 @@ export const DataLinkInput = memo( offset(({ rects }) => ({ alignmentAxis: rects.reference.width, })), - flip({ - fallbackAxisSideDirection: 'start', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), + ...getPositioningMiddleware(), ]; const { refs, floatingStyles } = useFloating({ diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx index ec6758de79c..4d2d6533a31 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx @@ -1,10 +1,11 @@ import { css } from '@emotion/css'; -import { autoUpdate, flip, shift, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; +import { autoUpdate, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { ChangeEvent, forwardRef, useImperativeHandle, useState } from 'react'; import { GrafanaTheme2, dateTime } from '@grafana/data'; import { useStyles2 } from '../../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../../utils/floating'; import { Props as InputProps, Input } from '../../Input/Input'; import { DatePicker } from '../DatePicker/DatePicker'; @@ -31,21 +32,15 @@ export const DatePickerWithInput = forwardRef { const [open, setOpen] = useState(false); const styles = useStyles2(getStyles); + const placement = 'bottom-start'; // the order of middleware is important! // see https://floating-ui.com/docs/arrow#order - const middleware = [ - flip({ - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = getPositioningMiddleware(placement); const { context, refs, floatingStyles } = useFloating({ open, - placement: 'bottom-start', + placement, onOpenChange: setOpen, middleware, whileElementsMounted: autoUpdate, diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx index 9e553274cfc..1f4eb4da47d 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { autoUpdate, flip, shift, useFloating } from '@floating-ui/react'; +import { autoUpdate, useFloating } from '@floating-ui/react'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; @@ -22,6 +22,7 @@ import { Components } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../../utils/floating'; import { Button } from '../../Button/Button'; import { InlineField } from '../../Forms/InlineField'; import { Icon } from '../../Icon/Icon'; @@ -92,22 +93,16 @@ export const DateTimePicker = ({ const theme = useTheme2(); const { modalBackdrop } = useStyles2(getModalStyles); const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.values.lg}px)`); + const placement = 'bottom-start'; const styles = useStyles2(getStyles); // the order of middleware is important! // see https://floating-ui.com/docs/arrow#order - const middleware = [ - flip({ - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = getPositioningMiddleware(placement); const { refs, floatingStyles } = useFloating({ open: isOpen, - placement: 'bottom-start', + placement, onOpenChange: setOpen, middleware, whileElementsMounted: autoUpdate, diff --git a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx index f75c4f1f2c8..74a19dd6efd 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { autoUpdate, flip, shift, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; +import { autoUpdate, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; @@ -9,6 +9,7 @@ import { RelativeTimeRange, GrafanaTheme2, TimeOption } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; import { useStyles2 } from '../../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../../utils/floating'; import { Button } from '../../Button/Button'; import { Field } from '../../Forms/Field'; import { Icon } from '../../Icon/Icon'; @@ -56,21 +57,15 @@ export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps) { ); const { dialogProps } = useDialog({}, ref); const validOptions = getQuickOptions().filter((o) => isRelativeFormat(o.from)); + const placement = 'bottom-start'; // the order of middleware is important! // see https://floating-ui.com/docs/arrow#order - const middleware = [ - flip({ - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = getPositioningMiddleware(placement); const { context, refs, floatingStyles } = useFloating({ open: isOpen, - placement: 'bottom-start', + placement, onOpenChange: setIsOpen, middleware, whileElementsMounted: autoUpdate, diff --git a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx index 48d6788efc8..199aa1c279e 100644 --- a/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Dropdown/ButtonSelect.tsx @@ -1,20 +1,12 @@ import { css } from '@emotion/css'; -import { - autoUpdate, - flip, - offset, - shift, - useClick, - useDismiss, - useFloating, - useInteractions, -} from '@floating-ui/react'; +import { autoUpdate, offset, useClick, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { FocusScope } from '@react-aria/focus'; import { memo, HTMLAttributes, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../utils/floating'; import { Menu } from '../Menu/Menu'; import { MenuItem } from '../Menu/MenuItem'; import { ToolbarButton, ToolbarButtonVariant } from '../ToolbarButton/ToolbarButton'; @@ -39,22 +31,14 @@ const ButtonSelectComponent = (props: Props) => { const { className, options, value, onChange, narrow, variant, ...restProps } = props; const styles = useStyles2(getStyles); const [isOpen, setIsOpen] = useState(false); + const placement = 'bottom-end'; // the order of middleware is important! - const middleware = [ - offset(0), - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = [offset(0), ...getPositioningMiddleware(placement)]; const { context, refs, floatingStyles } = useFloating({ open: isOpen, - placement: 'bottom-end', + placement, onOpenChange: setIsOpen, middleware, whileElementsMounted: autoUpdate, diff --git a/packages/grafana-ui/src/components/Dropdown/Dropdown.tsx b/packages/grafana-ui/src/components/Dropdown/Dropdown.tsx index 4d2846870a6..eff7cdc9454 100644 --- a/packages/grafana-ui/src/components/Dropdown/Dropdown.tsx +++ b/packages/grafana-ui/src/components/Dropdown/Dropdown.tsx @@ -2,9 +2,7 @@ import { css } from '@emotion/css'; import { FloatingFocusManager, autoUpdate, - flip, offset as floatingUIOffset, - shift, useClick, useDismiss, useFloating, @@ -17,6 +15,7 @@ import { CSSTransition } from 'react-transition-group'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../utils/floating'; import { renderOrCallToRender } from '../../utils/reactUtils'; import { getPlacement } from '../../utils/tooltipUtils'; import { Portal } from '../Portal/Portal'; @@ -34,6 +33,7 @@ export interface Props { export const Dropdown = React.memo(({ children, overlay, placement, offset, onVisibleChange }: Props) => { const [show, setShow] = useState(false); const transitionRef = useRef(null); + const floatingUIPlacement = getPlacement(placement); const handleOpenChange = useCallback( (newState: boolean) => { @@ -49,18 +49,12 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, onVi mainAxis: offset?.[0] ?? 8, crossAxis: offset?.[1] ?? 0, }), - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), + ...getPositioningMiddleware(floatingUIPlacement), ]; const { context, refs, floatingStyles } = useFloating({ open: show, - placement: getPlacement(placement), + placement: floatingUIPlacement, onOpenChange: handleOpenChange, middleware, whileElementsMounted: autoUpdate, diff --git a/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx b/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx index 9c6fa9695c9..178b0032b1e 100644 --- a/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx +++ b/packages/grafana-ui/src/components/InlineToast/InlineToast.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { autoUpdate, flip, offset, shift, Side, useFloating, useTransitionStyles } from '@floating-ui/react'; +import { autoUpdate, offset, Side, useFloating, useTransitionStyles } from '@floating-ui/react'; import { useLayoutEffect } from 'react'; import * as React from 'react'; @@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; import { IconName } from '../../types/icon'; +import { getPositioningMiddleware } from '../../utils/floating'; import { Icon } from '../Icon/Icon'; import { Portal } from '../Portal/Portal'; @@ -30,16 +31,7 @@ export function InlineToast({ referenceElement, children, suffixIcon, placement // the order of middleware is important! // `arrow` should almost always be at the end // see https://floating-ui.com/docs/arrow#order - const middleware = [ - offset(8), - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = [offset(8), ...getPositioningMiddleware(placement)]; const { context, refs, floatingStyles } = useFloating({ open: true, diff --git a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx index df29e9b44ed..0276c5595d9 100644 --- a/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx +++ b/packages/grafana-ui/src/components/Table/DataLinksActionsTooltip.tsx @@ -1,11 +1,12 @@ import { css } from '@emotion/css'; -import { flip, shift, useDismiss, useFloating, useInteractions } from '@floating-ui/react'; +import { useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { useMemo, ReactNode } from 'react'; import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../utils/floating'; import { Portal } from '../Portal/Portal'; import { VizTooltipFooter } from '../VizTooltip/VizTooltipFooter'; import { VizTooltipWrapper } from '../VizTooltip/VizTooltipWrapper'; @@ -27,17 +28,10 @@ interface Props { export const DataLinksActionsTooltip = ({ links, actions, value, coords, onTooltipClose }: Props) => { const theme = useTheme2(); const styles = useStyles2(getStyles); + const placement = 'right-start'; // the order of middleware is important! - const middleware = [ - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = getPositioningMiddleware(placement); const virtual = useMemo(() => { const { clientX, clientY } = coords; @@ -66,7 +60,7 @@ export const DataLinksActionsTooltip = ({ links, actions, value, coords, onToolt const { context, refs, floatingStyles } = useFloating({ open: true, - placement: 'right-start', + placement, onOpenChange: onTooltipClose, middleware, // whileElementsMounted: autoUpdate, diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx index 035fe22b6b6..991490e36aa 100644 --- a/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.tsx @@ -2,11 +2,9 @@ import { css, cx } from '@emotion/css'; import { arrow, autoUpdate, - flip, FloatingArrow, FloatingFocusManager, offset, - shift, useClick, useDismiss, useFloating, @@ -19,6 +17,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../utils/floating'; import { buildTooltipTheme, getPlacement } from '../../utils/tooltipUtils'; import { IconButton } from '../IconButton/IconButton'; @@ -69,19 +68,14 @@ export const Toggletip = memo( const style = styles[theme]; const [controlledVisible, setControlledVisible] = useState(show); const isOpen = show ?? controlledVisible; + const floatingUIPlacement = getPlacement(placement); // the order of middleware is important! // `arrow` should almost always be at the end // see https://floating-ui.com/docs/arrow#order const middleware = [ offset(8), - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), + ...getPositioningMiddleware(floatingUIPlacement), arrow({ element: arrowRef, }), @@ -89,7 +83,7 @@ export const Toggletip = memo( const { context, refs, floatingStyles } = useFloating({ open: isOpen, - placement: getPlacement(placement), + placement: floatingUIPlacement, onOpenChange: (open) => { if (show === undefined) { setControlledVisible(open); diff --git a/packages/grafana-ui/src/components/Tooltip/Popover.tsx b/packages/grafana-ui/src/components/Tooltip/Popover.tsx index 369f3b46698..ac86653606a 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popover.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popover.tsx @@ -1,17 +1,9 @@ -import { - FloatingArrow, - arrow, - autoUpdate, - flip, - offset, - shift, - useFloating, - useTransitionStyles, -} from '@floating-ui/react'; +import { FloatingArrow, arrow, autoUpdate, offset, useFloating, useTransitionStyles } from '@floating-ui/react'; import { useLayoutEffect, useRef } from 'react'; import * as React from 'react'; import { useTheme2 } from '../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../utils/floating'; import { getPlacement } from '../../utils/tooltipUtils'; import { Portal } from '../Portal/Portal'; @@ -42,20 +34,12 @@ export function Popover({ }: Props) { const theme = useTheme2(); const arrowRef = useRef(null); + const floatingUIPlacement = getPlacement(placement); // the order of middleware is important! // `arrow` should almost always be at the end // see https://floating-ui.com/docs/arrow#order - const middleware = [ - offset(8), - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = [offset(8), ...getPositioningMiddleware(floatingUIPlacement)]; if (renderArrow) { middleware.push( @@ -67,7 +51,7 @@ export function Popover({ const { context, refs, floatingStyles } = useFloating({ open: show, - placement: getPlacement(placement), + placement: floatingUIPlacement, middleware, whileElementsMounted: autoUpdate, strategy: 'fixed', diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index 530d2cf9bdb..b44d48ddf38 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -1,10 +1,8 @@ import { arrow, autoUpdate, - flip, FloatingArrow, offset, - shift, useDismiss, useFloating, useFocus, @@ -18,6 +16,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../themes/ThemeContext'; +import { getPositioningMiddleware } from '../../utils/floating'; import { buildTooltipTheme, getPlacement } from '../../utils/tooltipUtils'; import { Portal } from '../Portal/Portal'; @@ -40,19 +39,14 @@ export const Tooltip = forwardRef( const arrowRef = useRef(null); const [controlledVisible, setControlledVisible] = useState(show); const isOpen = show ?? controlledVisible; + const floatingUIPlacement = getPlacement(placement); // the order of middleware is important! // `arrow` should almost always be at the end // see https://floating-ui.com/docs/arrow#order const middleware = [ offset(8), - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), + ...getPositioningMiddleware(floatingUIPlacement), arrow({ element: arrowRef, }), @@ -60,7 +54,7 @@ export const Tooltip = forwardRef( const { context, refs, floatingStyles } = useFloating({ open: isOpen, - placement: getPlacement(placement), + placement: floatingUIPlacement, onOpenChange: setControlledVisible, middleware, whileElementsMounted: autoUpdate, diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index d35561e598f..b9082ae9901 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -14,6 +14,7 @@ import { SecretFormField } from './components/SecretFormField/SecretFormField'; import * as commonOptionsBuilder from './options/builder'; import * as styleMixins from './themes/mixins'; import * as DOMUtil from './utils/dom'; +import * as floatingUtils from './utils/floating'; import * as ReactUtils from './utils/reactUtils'; export { Icon } from './components/Icon/Icon'; @@ -437,7 +438,7 @@ export { NodeGraphDataFrameFieldNames } from './utils/nodeGraph'; export { fuzzyMatch } from './utils/fuzzy'; export { logOptions } from './utils/logOptions'; -export { DOMUtil, ReactUtils }; +export { DOMUtil, ReactUtils, floatingUtils }; export { ThemeContext } from '@grafana/data'; export { diff --git a/packages/grafana-ui/src/utils/floating.ts b/packages/grafana-ui/src/utils/floating.ts new file mode 100644 index 00000000000..2a87bde04b6 --- /dev/null +++ b/packages/grafana-ui/src/utils/floating.ts @@ -0,0 +1,25 @@ +import { flip, Placement, shift } from '@floating-ui/react'; + +export const BOUNDARY_ELEMENT_ID = 'floating-boundary'; + +export function getPositioningMiddleware(placement?: Placement) { + const middleware = []; + + const flipMiddleware = flip({ + // Ensure we flip to the perpendicular axis if it doesn't fit + // on narrow viewports. + crossAxis: 'alignment', + fallbackAxisSideDirection: 'end', + boundary: document.getElementById(BOUNDARY_ELEMENT_ID) ?? undefined, + }); + const shiftMiddleware = shift(); + + // Prioritize flip over shift for edge-aligned placements only. + if (placement?.includes('-')) { + middleware.push(flipMiddleware, shiftMiddleware); + } else { + middleware.push(shiftMiddleware, flipMiddleware); + } + + return middleware; +} diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index 1c11f0add3d..923ae866c6d 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -6,7 +6,7 @@ import { PropsWithChildren, useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans } from '@grafana/i18n'; import { locationSearchToObject, locationService, useScopes } from '@grafana/runtime'; -import { ErrorBoundaryAlert, getDragStyles, LinkButton, useStyles2 } from '@grafana/ui'; +import { ErrorBoundaryAlert, floatingUtils, getDragStyles, LinkButton, useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth'; import store from 'app/core/store'; @@ -87,6 +87,7 @@ export function AppChrome({ children }: Props) { // doesn't get re-mounted when chromeless goes from true to false. return (
a - n * Math.floor(a / n); @@ -64,6 +72,7 @@ export const SuggestionsInput = ({ const [scrollTop, setScrollTop] = useState(0); const [inputHeight, setInputHeight] = useState(0); const [startPos, setStartPos] = useState(0); + const placement = 'bottom-start'; const theme = useTheme2(); const styles = getStyles(theme, inputHeight); @@ -75,19 +84,11 @@ export const SuggestionsInput = ({ }, [scrollTop]); // the order of middleware is important! - const middleware = [ - flip({ - fallbackAxisSideDirection: 'start', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = floatingUtils.getPositioningMiddleware(placement); const { refs, floatingStyles } = useFloating({ open: showingSuggestions, - placement: 'bottom-start', + placement, onOpenChange: setShowingSuggestions, middleware, whileElementsMounted: autoUpdate, diff --git a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx index 61c571f6ca1..b6154d9205e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/TracesQueryEditor/Filters.test.tsx @@ -264,7 +264,7 @@ describe(`Traces Filters`, () => { { property: 'client_Browser', filters: [{ count: 100, value: 'test-client' }], operation: 'ne', index: 1 }, rerender ); - }); + }, 10000); it('should delete a trace filter', async () => { let mockQuery = createMockQuery({ azureTraces: { traceTypes: ['customEvents'] } }); diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx index 3ac418177f9..3c8add97943 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx @@ -1,21 +1,12 @@ import { css, cx } from '@emotion/css'; -import { - autoUpdate, - flip, - safePolygon, - shift, - useDismiss, - useFloating, - useHover, - useInteractions, -} from '@floating-ui/react'; +import { autoUpdate, safePolygon, useDismiss, useFloating, useHover, useInteractions } from '@floating-ui/react'; import { useCallback, useEffect, useState } from 'react'; import * as React from 'react'; import { DataFrame, Field, formattedValueToString, GrafanaTheme2, LinkModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { TimeZone } from '@grafana/schema'; -import { Portal, UPlotConfigBuilder, useStyles2 } from '@grafana/ui'; +import { floatingUtils, Portal, UPlotConfigBuilder, useStyles2 } from '@grafana/ui'; import { VizTooltipItem } from '@grafana/ui/internal'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { ExemplarTooltip } from 'app/features/visualization/data-hover/ExemplarTooltip'; @@ -50,21 +41,14 @@ export const ExemplarMarker = ({ const styles = useStyles2(getExemplarMarkerStyles, maxWidth); const [isOpen, setIsOpen] = useState(false); const [isLocked, setIsLocked] = useState(false); + const placement = 'bottom'; // the order of middleware is important! - const middleware = [ - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ]; + const middleware = floatingUtils.getPositioningMiddleware(placement); const { context, refs, floatingStyles } = useFloating({ open: isOpen, - placement: 'bottom', + placement, onOpenChange: setIsOpen, middleware, whileElementsMounted: autoUpdate, diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx index 513a620c985..140283912d6 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationMarker2.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { flip, shift, autoUpdate } from '@floating-ui/dom'; +import { autoUpdate } from '@floating-ui/dom'; import { useFloating } from '@floating-ui/react'; import { useState } from 'react'; import * as React from 'react'; @@ -8,7 +8,7 @@ import { createPortal } from 'react-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { TimeZone } from '@grafana/schema'; -import { useStyles2 } from '@grafana/ui'; +import { floatingUtils, useStyles2 } from '@grafana/ui'; import { AnnotationEditor2 } from './AnnotationEditor2'; import { AnnotationTooltip2 } from './AnnotationTooltip2'; @@ -37,20 +37,13 @@ export const AnnotationMarker2 = ({ portalRoot, }: AnnoBoxProps) => { const styles = useStyles2(getStyles); + const placement = 'bottom'; const [state, setState] = useState(exitWipEdit != null ? STATE_EDITING : STATE_DEFAULT); const { refs, floatingStyles } = useFloating({ open: true, - placement: 'bottom', - middleware: [ - flip({ - fallbackAxisSideDirection: 'end', - // see https://floating-ui.com/docs/flip#combining-with-shift - crossAxis: false, - boundary: document.body, - }), - shift(), - ], + placement, + middleware: floatingUtils.getPositioningMiddleware(placement), whileElementsMounted: autoUpdate, strategy: 'fixed', });