diff --git a/frontend/src/metabase/components/Popover/SizeToFitModifier.ts b/frontend/src/metabase/components/Popover/SizeToFitModifier.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c4c31674ff203cb8689b50429389854f47afb13 --- /dev/null +++ b/frontend/src/metabase/components/Popover/SizeToFitModifier.ts @@ -0,0 +1,65 @@ +import * as popper from "@popperjs/core"; + +const PAGE_PADDING = 10; +const SIZE_TO_FIT_MIN_HEIGHT = 200; + +export type SizeToFitOptions = { + minHeight: number; +}; + +export function sizeToFitModifierFn({ + state, + options, +}: popper.ModifierArguments<SizeToFitOptions>) { + const { + placement, + rects: { + popper: { height }, + reference: { height: targetHeight }, + }, + } = state; + const { minHeight = SIZE_TO_FIT_MIN_HEIGHT } = options; + + const topPlacement = placement.startsWith("top"); + const bottomPlacement = placement.startsWith("bottom"); + + if (topPlacement || bottomPlacement) { + const overflow = popper.detectOverflow(state); + const distanceFromEdge = placement.startsWith("top") + ? overflow.top + : overflow.bottom; + + const maxHeight = height - distanceFromEdge - PAGE_PADDING; + const minnedMaxHeight = Math.max(maxHeight, minHeight); + + const oppositeSpace = window.innerHeight - targetHeight - maxHeight; + + if ( + maxHeight < minHeight && + oppositeSpace > minHeight && + !state.modifiersData.sizeToFit.flipped + ) { + state.placement = getAltPlacement(placement); + state.modifiersData.sizeToFit.flipped = true; + state.reset = true; + } else { + state.styles.popper.maxHeight = `${minnedMaxHeight}px`; + } + } +} + +export function getAltPlacement( + placementStr: popper.Placement, +): popper.Placement { + const [targetPlacement, horizontalPlacement] = placementStr.split("-") as + | [string] + | [string, string]; + const newPlacement = [ + targetPlacement === "top" ? "bottom" : "top", + horizontalPlacement, + ] + .filter((str): str is string => Boolean(str)) + .join("-"); + + return newPlacement as popper.Placement; +} diff --git a/frontend/src/metabase/components/Popover/TippyPopover.info.js b/frontend/src/metabase/components/Popover/TippyPopover.info.js index d7d5a454332823316717b65efddb04380912c699..6d6a780d1d88870078b08b8211d6be5c725b14ab 100644 --- a/frontend/src/metabase/components/Popover/TippyPopover.info.js +++ b/frontend/src/metabase/components/Popover/TippyPopover.info.js @@ -119,5 +119,5 @@ export const examples = { {target} </TippyPopover> ), - extra_space: <div style={{ height: 250 }} />, + extra_space: <div style={{ height: 400 }} />, }; diff --git a/frontend/src/metabase/components/Popover/TippyPopover.tsx b/frontend/src/metabase/components/Popover/TippyPopover.tsx index 7ffcbcae73924fb8afe51ba44f88df4dd3f36a71..3ae1d500d91e1c45ee0ec1cb1652ba1aff888d13 100644 --- a/frontend/src/metabase/components/Popover/TippyPopover.tsx +++ b/frontend/src/metabase/components/Popover/TippyPopover.tsx @@ -2,7 +2,6 @@ import React, { useState, useMemo, useCallback, useRef } from "react"; import PropTypes from "prop-types"; import * as TippyReact from "@tippyjs/react"; import * as tippy from "tippy.js"; -import * as popper from "@popperjs/core"; import cx from "classnames"; import { merge } from "icepick"; @@ -12,6 +11,7 @@ import { isCypressActive } from "metabase/env"; import useSequencedContentCloseHandler from "metabase/hooks/use-sequenced-content-close-handler"; import { DEFAULT_Z_INDEX } from "./constants"; +import { sizeToFitModifierFn, SizeToFitOptions } from "./SizeToFitModifier"; const TippyComponent = TippyReact.default; type TippyProps = TippyReact.TippyProps; @@ -21,11 +21,10 @@ export interface ITippyPopoverProps extends TippyProps { disableContentSandbox?: boolean; lazy?: boolean; flip?: boolean; - sizeToFit?: boolean; + sizeToFit?: boolean | SizeToFitOptions; onClose?: () => void; } -const PAGE_PADDING = 10; const OFFSET: [number, number] = [0, 5]; const propTypes = { @@ -48,33 +47,15 @@ function getPopperOptions({ modifiers: [ { name: "flip", - enabled: flip, + enabled: flip && !sizeToFit, }, { name: "sizeToFit", phase: "beforeWrite", - enabled: sizeToFit, - requiresIfExists: ["offset", "flip"], - fn: ({ - state, - options, - }: popper.ModifierArguments<Record<string, unknown>>) => { - const { - placement, - rects: { - popper: { height }, - }, - } = state; - if (placement.startsWith("top") || placement.startsWith("bottom")) { - const overflow = popper.detectOverflow(state, options); - const distanceFromEdge = placement.startsWith("top") - ? overflow.top - : overflow.bottom; - - const maxHeight = height - distanceFromEdge - PAGE_PADDING; - state.styles.popper.maxHeight = `${maxHeight}px`; - } - }, + enabled: sizeToFit !== false, + requiresIfExists: ["offset"], + fn: sizeToFitModifierFn, + options: typeof sizeToFit === "object" ? sizeToFit : undefined, }, ], },