UNPKG

@blueprintjs/core

Version:
239 lines 12.2 kB
/* ! * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. */ import * as Errors from "../../common/errors"; import { isNodeEnv } from "../../common/utils"; import { POPOVER_ARROW_SVG_SIZE } from "../popover/popoverArrow"; import { positionToPlacement } from "../popover/popoverPlacementUtils"; import {} from "../popover/popoverPosition"; /** * Converts Popper.js v2 `modifiers` (used by `Popover`) to a Floating UI `MiddlewareConfig` (used by `PopoverNext`). * * The `modifiers` prop is not supported in `PopoverNext`; use the `middleware` prop instead. * * Modifier → middleware mappings: * - `flip` → `flip` * - `preventOverflow` → `shift` (Floating UI's equivalent "keep within boundary" concept) * - `offset` → `offset` (tuple `[skidding, distance]` is converted to `{ crossAxis, mainAxis }`) * - `arrow` → `arrow` * - `hide` → `hide` * - `computeStyles`, `eventListeners`, `popperOffsets` are not mapped (handled internally by Floating UI) * * **Note on offset:** If the Popper.js `offset` option is a function, it cannot be automatically * converted and will be omitted with a console warning. Migrate it manually to a * `{ mainAxis, crossAxis }` object in the `middleware` prop. * * @example * // Before (Popover) * <Popover modifiers={{ flip: { options: { padding: 8 } }, preventOverflow: { options: { padding: 4 } } }} /> * * // After (PopoverNext) * <PopoverNext middleware={popperModifiersToNextMiddleware({ flip: { options: { padding: 8 } }, preventOverflow: { options: { padding: 4 } } })} /> */ export function popperModifiersToNextMiddleware(modifiers) { const middleware = {}; if (modifiers.flip && modifiers.flip.enabled !== false) { const { options } = modifiers.flip; middleware.flip = { ...(options?.boundary != null ? { boundary: popperBoundaryToNextBoundary(options.boundary) } : {}), ...(options?.rootBoundary != null ? { rootBoundary: options.rootBoundary } : {}), ...(options?.padding != null ? { padding: options.padding } : {}), ...(options?.fallbackPlacements != null ? { fallbackPlacements: options.fallbackPlacements } : {}), ...(options?.flipVariations != null ? { flipAlignment: options.flipVariations } : {}), ...(options?.mainAxis != null ? { mainAxis: options.mainAxis } : {}), ...(options?.altAxis != null ? { crossAxis: options.altAxis } : {}), }; } if (modifiers.preventOverflow && modifiers.preventOverflow.enabled !== false) { const { options } = modifiers.preventOverflow; middleware.shift = { ...(options?.boundary != null ? { boundary: popperBoundaryToNextBoundary(options.boundary) } : {}), ...(options?.rootBoundary != null ? { rootBoundary: options.rootBoundary } : {}), ...(options?.padding != null ? { padding: options.padding } : {}), ...(options?.mainAxis != null ? { mainAxis: options.mainAxis } : {}), ...(options?.altAxis != null ? { crossAxis: options.altAxis } : {}), }; } if (modifiers.offset && modifiers.offset.enabled !== false) { const { options } = modifiers.offset; if (options?.offset != null) { if (typeof options.offset === "function") { console.warn("popperModifiersToNextMiddleware: The Popper.js `offset` function cannot be automatically " + "converted to a Floating UI middleware config. Migrate it manually to a " + "`{ mainAxis, crossAxis }` object in the `middleware` prop."); } else { const [skidding, distance] = options.offset; middleware.offset = { ...(skidding != null ? { crossAxis: skidding } : {}), ...(distance != null ? { mainAxis: distance } : {}), }; } } else { // Legacy `Popover` always populated `options.offset` with a default of // `[0, POPOVER_ARROW_SVG_SIZE / 2]`, so `{ offset: { enabled: true } }` (a common pattern // for forcing the offset on when the arrow is disabled) implicitly produced a 15px main-axis // gap. Preserve that behavior here so the visual gap is not silently dropped. middleware.offset = { mainAxis: POPOVER_ARROW_SVG_SIZE / 2 }; } } if (modifiers.arrow && modifiers.arrow.enabled !== false) { const { options } = modifiers.arrow; // Popper.js arrow element can be HTMLElement | string | null; string selectors are not supported by Floating UI. if (options?.element != null && typeof options.element !== "string") { middleware.arrow = { element: options.element, ...(options.padding != null && typeof options.padding !== "function" ? { padding: options.padding } : {}), }; } } if (modifiers.hide && modifiers.hide.enabled !== false) { middleware.hide = {}; } return middleware; } /** * Converts a partial legacy `PopoverProps` bag into a partial `PopoverNextProps` bag suitable * for spreading onto `PopoverNext`. Preserves legacy default behavior where it differs from * `PopoverNext`'s defaults (`shouldReturnFocusOnClose`). * * Transformations: * - `placement` ?? `position` → `placement`, mirroring legacy `Popover`'s resolution * (`placement ?? positionToPlacement(position)`). When `placement` is defined it always wins. * - `modifiers` → `middleware` (via {@link popperModifiersToNextMiddleware}). * - `minimal: true` → `animation: "minimal"` and `arrow: false` (legacy `minimal` disables the arrow). * - `boundary: "clippingParents"` → `"clippingAncestors"` (the Floating UI equivalent). * * Dropped (with dev-only `console.warn`): * - `modifiersCustom` — no Floating UI equivalent; migrate manually to `middleware`. * - `portalStopPropagationEvents` — already deprecated and non-functional in React 17+. * * Intended for use inside Blueprint components that wrap `Popover` internally and pass * through a `popoverProps` prop, so they can swap to `PopoverNext` without changing their public API. */ export function popoverPropsToNextProps(props) { // Pull out every prop whose legacy type is structurally incompatible with its // PopoverNext counterpart. After this destructure, `rest` contains only fields that // share an identical type between Popover and PopoverNext, so the spread below is // sound without any blanket cast. const { boundary, minimal, modifiers, modifiersCustom, onClose, placement, popoverRef, // eslint-disable-next-line @typescript-eslint/no-deprecated portalStopPropagationEvents, position, shouldReturnFocusOnClose, ...rest } = props; if (!isNodeEnv("production")) { if (modifiersCustom !== undefined) { console.warn("[Blueprint] popoverPropsToNextProps: `modifiersCustom` has no equivalent in PopoverNext and will be dropped. " + "Migrate to the `middleware` prop manually."); } if (portalStopPropagationEvents !== undefined) { console.warn("[Blueprint] popoverPropsToNextProps: `portalStopPropagationEvents` has no equivalent in PopoverNext and will be dropped."); } if (placement !== undefined && position !== undefined) { console.warn(Errors.POPOVER_WARN_PLACEMENT_AND_POSITION_MUTEX); } } const nextProps = { ...rest }; if (boundary !== undefined) { nextProps.boundary = popperBoundaryToNextBoundary(boundary); } // Mirror legacy `Popover`'s `placement ?? positionToPlacement(position)` rule: an explicit // `placement` always wins, even `"auto*"` (which maps to `undefined` = autoPlacement default). if (placement !== undefined) { nextProps.placement = popoverPlacementToNextPlacement(placement); } else if (position !== undefined) { nextProps.placement = popoverPositionToNextPlacement(position); } if (modifiers !== undefined) { nextProps.middleware = popperModifiersToNextMiddleware(modifiers); } if (minimal === true) { nextProps.animation ??= "minimal"; nextProps.arrow ??= false; } if (onClose !== undefined) { // Legacy `onClose` requires a non-undefined `event`; PopoverNext's signature allows // `event` to be undefined. Wrap to honor the legacy contract — invoke the legacy // handler only when an event is actually present. nextProps.onClose = event => { if (event !== undefined) { onClose(event); } }; } if (popoverRef !== undefined) { // Legacy `popoverRef` is `React.Ref<HTMLElement>`; PopoverNext's is `React.Ref<HTMLDivElement>`. // `RefObject<T>` is invariant in `T` (`.current` is mutable, so `T` appears in both read // and write positions), so `RefObject<HTMLElement>` is not assignable to // `RefObject<HTMLDivElement>` even though `HTMLDivElement` is a subtype of `HTMLElement`. // In practice the underlying DOM node is a `<div>` (the `Classes.POPOVER` element), so a // ref slot typed for the wider `HTMLElement` safely receives it. Cast narrowly here so // the unsoundness stays scoped to this one prop instead of the whole bag. nextProps.popoverRef = popoverRef; } // Legacy default for `shouldReturnFocusOnClose` is `false`; PopoverNext's is `true`. nextProps.shouldReturnFocusOnClose = shouldReturnFocusOnClose ?? false; return nextProps; } /** * Converts a Popper.js `Boundary` value to a Floating UI `PopoverNextBoundary` value. * * The two systems use different names for the "all clipping ancestors" sentinel: * - Popper.js: `"clippingParents"` * - Floating UI: `"clippingAncestors"` * * Element / `Element[]` values pass through unchanged. */ export function popperBoundaryToNextBoundary(boundary) { return boundary === "clippingParents" ? "clippingAncestors" : boundary; } /** * Converts a Popper.js `Placement` value to a `PopoverNextPlacement` value for use with `PopoverNext`. * * `"auto"`, `"auto-start"`, and `"auto-end"` have no direct equivalent in Floating UI — they return * `undefined`, which causes `PopoverNext` to use its default automatic placement behavior. * All other values pass through unchanged (the residual literal union is identical to `PopoverNextPlacement`). * * @example * // Before (Popover) * <Popover placement="top-start" /> * * // After (PopoverNext) * <PopoverNext placement={popoverPlacementToNextPlacement("top-start")} /> */ export function popoverPlacementToNextPlacement(placement) { switch (placement) { case "auto": case "auto-start": case "auto-end": // PopoverNext uses autoPlacement middleware by default when placement is undefined. return undefined; default: return placement; } } /** * Converts a legacy `PopoverPosition` value to a `PopoverNextPlacement` value for use with `PopoverNext`. * * The `position` prop is not supported in `PopoverNext`; use the `placement` prop instead. * `"auto"`, `"auto-start"`, and `"auto-end"` have no direct equivalent — they return `undefined`, * which causes `PopoverNext` to use its default automatic placement behavior. * * @example * // Before (Popover) * <Popover position={PopoverPosition.TOP_LEFT} /> * * // After (PopoverNext) * <PopoverNext placement={popoverPositionToNextPlacement(PopoverPosition.TOP_LEFT)} /> */ export function popoverPositionToNextPlacement(position) { // `positionToPlacement` translates PopoverPosition's `"top-left"`/`"bottom-right"` forms to // Popper's `"top-start"`/`"bottom-end"` forms, and passes `"auto"`/`"auto-start"`/`"auto-end"` // through unchanged. `popoverPlacementToNextPlacement` then filters out the auto* values. return popoverPlacementToNextPlacement(positionToPlacement(position)); } //# sourceMappingURL=popoverNextMigrationUtils.js.map