@blueprintjs/core
Version:
Core styles & components
474 lines (400 loc) • 21.5 kB
text/typescript
/* !
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*/
import { describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import type { MiddlewareConfig } from "../..";
import { PopoverPosition } from "../popover/popoverPosition";
import type { PopoverProps } from "../popover/popoverProps";
import type { PopperModifierOverrides } from "../popover/popoverSharedProps";
import {
popoverPlacementToNextPlacement,
popoverPositionToNextPlacement,
popoverPropsToNextProps,
popperModifiersToNextMiddleware,
} from "./popoverNextMigrationUtils";
describe("popoverPositionToNextPlacement", () => {
it("should convert top-left to top-start", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.TOP_LEFT)).to.equal("top-start");
});
it("should convert top to top", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.TOP)).to.equal("top");
});
it("should convert top-right to top-end", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.TOP_RIGHT)).to.equal("top-end");
});
it("should convert right-top to right-start", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.RIGHT_TOP)).to.equal("right-start");
});
it("should convert right to right", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.RIGHT)).to.equal("right");
});
it("should convert right-bottom to right-end", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.RIGHT_BOTTOM)).to.equal("right-end");
});
it("should convert bottom-right to bottom-end", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.BOTTOM_RIGHT)).to.equal("bottom-end");
});
it("should convert bottom to bottom", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.BOTTOM)).to.equal("bottom");
});
it("should convert bottom-left to bottom-start", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.BOTTOM_LEFT)).to.equal("bottom-start");
});
it("should convert left-bottom to left-end", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.LEFT_BOTTOM)).to.equal("left-end");
});
it("should convert left to left", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.LEFT)).to.equal("left");
});
it("should convert left-top to left-start", () => {
expect(popoverPositionToNextPlacement(PopoverPosition.LEFT_TOP)).to.equal("left-start");
});
it("should return undefined for auto", () => {
expect(popoverPositionToNextPlacement("auto")).to.be.undefined;
});
it("should return undefined for auto-start", () => {
expect(popoverPositionToNextPlacement("auto-start")).to.be.undefined;
});
it("should return undefined for auto-end", () => {
expect(popoverPositionToNextPlacement("auto-end")).to.be.undefined;
});
});
describe("popoverPlacementToNextPlacement", () => {
it("should pass through non-auto placements unchanged", () => {
expect(popoverPlacementToNextPlacement("top")).to.equal("top");
expect(popoverPlacementToNextPlacement("top-start")).to.equal("top-start");
expect(popoverPlacementToNextPlacement("bottom-end")).to.equal("bottom-end");
expect(popoverPlacementToNextPlacement("left-start")).to.equal("left-start");
});
it("should return undefined for auto", () => {
expect(popoverPlacementToNextPlacement("auto")).to.be.undefined;
});
it("should return undefined for auto-start", () => {
expect(popoverPlacementToNextPlacement("auto-start")).to.be.undefined;
});
it("should return undefined for auto-end", () => {
expect(popoverPlacementToNextPlacement("auto-end")).to.be.undefined;
});
});
describe("popperModifiersToNextMiddleware", () => {
it("should return empty config when given empty modifiers", () => {
expect(popperModifiersToNextMiddleware({})).to.deep.equal({});
});
describe("flip", () => {
it("should omit flip middleware when enabled is false", () => {
const value: PopperModifierOverrides = { flip: { enabled: false } };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map flip modifier to flip middleware", () => {
const value: PopperModifierOverrides = { flip: {} };
const expected: MiddlewareConfig = { flip: {} };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map flip padding option", () => {
const value: PopperModifierOverrides = { flip: { options: { padding: 8 } } };
const expected: MiddlewareConfig = { flip: { padding: 8 } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map flip fallbackPlacements option", () => {
const value: PopperModifierOverrides = { flip: { options: { fallbackPlacements: ["top", "bottom"] } } };
const expected: MiddlewareConfig = { flip: { fallbackPlacements: ["top", "bottom"] } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map flip flipVariations to flipAlignment", () => {
const value: PopperModifierOverrides = { flip: { options: { flipVariations: false } } };
const expected: MiddlewareConfig = { flip: { flipAlignment: false } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map flip mainAxis option", () => {
const value: PopperModifierOverrides = { flip: { options: { mainAxis: false } } };
const expected: MiddlewareConfig = { flip: { mainAxis: false } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map flip altAxis to crossAxis", () => {
const value: PopperModifierOverrides = { flip: { options: { altAxis: false } } };
const expected: MiddlewareConfig = { flip: { crossAxis: false } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
});
describe("preventOverflow", () => {
it("should omit shift middleware when enabled is false", () => {
const value: PopperModifierOverrides = { preventOverflow: { enabled: false } };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map preventOverflow modifier to shift middleware", () => {
const value: PopperModifierOverrides = { preventOverflow: {} };
const expected: MiddlewareConfig = { shift: {} };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map preventOverflow padding option", () => {
const value: PopperModifierOverrides = { preventOverflow: { options: { padding: 4 } } };
const expected: MiddlewareConfig = { shift: { padding: 4 } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map preventOverflow mainAxis option", () => {
const value: PopperModifierOverrides = { preventOverflow: { options: { mainAxis: false } } };
const expected: MiddlewareConfig = { shift: { mainAxis: false } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map preventOverflow altAxis to crossAxis", () => {
const value: PopperModifierOverrides = { preventOverflow: { options: { altAxis: true } } };
const expected: MiddlewareConfig = { shift: { crossAxis: true } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
});
describe("offset", () => {
it("should omit offset middleware when enabled is false", () => {
const value: PopperModifierOverrides = { offset: { enabled: false, options: { offset: [0, 10] } } };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map offset [skidding, distance] tuple to { crossAxis, mainAxis }", () => {
const value: PopperModifierOverrides = { offset: { options: { offset: [5, 10] } } };
const expected: MiddlewareConfig = { offset: { crossAxis: 5, mainAxis: 10 } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map offset [0, distance] tuple with zero skidding", () => {
const value: PopperModifierOverrides = { offset: { options: { offset: [0, 15] } } };
const expected: MiddlewareConfig = { offset: { crossAxis: 0, mainAxis: 15 } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should warn and omit offset when offset is a function", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
const value: PopperModifierOverrides = { offset: { options: { offset: () => [0, 0] } } };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
expect(warnSpy).toHaveBeenCalledOnce();
warnSpy.mockRestore();
});
it("should fall back to Blueprint's legacy default offset (15px main-axis) when modifier is enabled with no explicit options", () => {
const expected: MiddlewareConfig = { offset: { mainAxis: 15 } };
expect(popperModifiersToNextMiddleware({ offset: {} })).to.deep.equal(expected);
expect(popperModifiersToNextMiddleware({ offset: { enabled: true } })).to.deep.equal(expected);
});
});
describe("arrow", () => {
it("should omit arrow middleware when enabled is false", () => {
const element = document.createElement("div");
const value: PopperModifierOverrides = { arrow: { enabled: false, options: { element } } };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map arrow modifier with element to arrow middleware", () => {
const element = document.createElement("div");
const value: PopperModifierOverrides = { arrow: { options: { element } } };
const expected: MiddlewareConfig = { arrow: { element } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map arrow padding option", () => {
const element = document.createElement("div");
const value: PopperModifierOverrides = { arrow: { options: { element, padding: 5 } } };
const expected: MiddlewareConfig = { arrow: { element, padding: 5 } };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should omit arrow from result when no element provided", () => {
const value: PopperModifierOverrides = { arrow: {} };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
});
describe("hide", () => {
it("should omit hide middleware when enabled is false", () => {
const value: PopperModifierOverrides = { hide: { enabled: false } };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should map hide modifier to hide middleware", () => {
const value: PopperModifierOverrides = { hide: {} };
const expected: MiddlewareConfig = { hide: {} };
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
});
describe("ignored modifiers", () => {
it("should not map computeStyles", () => {
const value: PopperModifierOverrides = { computeStyles: {} };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should not map eventListeners", () => {
const value: PopperModifierOverrides = { eventListeners: {} };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
it("should not map popperOffsets", () => {
const value: PopperModifierOverrides = { popperOffsets: {} };
const expected: MiddlewareConfig = {};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
});
it("should handle multiple modifiers together", () => {
const value: PopperModifierOverrides = {
flip: { options: { padding: 8 } },
hide: { enabled: false },
offset: { options: { offset: [0, 10] } },
preventOverflow: { options: { padding: 4 } },
};
const expected: MiddlewareConfig = {
flip: { padding: 8 },
offset: { crossAxis: 0, mainAxis: 10 },
shift: { padding: 4 },
};
expect(popperModifiersToNextMiddleware(value)).to.deep.equal(expected);
});
});
describe("popoverPropsToNextProps", () => {
it("should pin shouldReturnFocusOnClose to false when not supplied (legacy default)", () => {
expect(popoverPropsToNextProps({})).to.deep.equal({ shouldReturnFocusOnClose: false });
});
it("should pass through shouldReturnFocusOnClose when supplied", () => {
const result = popoverPropsToNextProps({ shouldReturnFocusOnClose: true });
expect(result.shouldReturnFocusOnClose).to.equal(true);
});
it("should pass through 1:1 props as-is", () => {
const result = popoverPropsToNextProps({
content: "hello",
hasBackdrop: true,
isOpen: true,
matchTargetWidth: true,
positioningStrategy: "fixed",
rootBoundary: "viewport",
usePortal: false,
});
expect(result.content).to.equal("hello");
expect(result.hasBackdrop).to.equal(true);
expect(result.isOpen).to.equal(true);
expect(result.matchTargetWidth).to.equal(true);
expect(result.positioningStrategy).to.equal("fixed");
expect(result.rootBoundary).to.equal("viewport");
expect(result.usePortal).to.equal(false);
});
describe("onClose", () => {
it("should wrap onClose so legacy callbacks are only invoked with a defined event", () => {
const onClose = vi.fn();
const result = popoverPropsToNextProps({ onClose });
// Wrapper exists, and it's not the original.
expect(result.onClose).to.be.a("function");
expect(result.onClose).to.not.equal(onClose);
const wrapped = result.onClose!;
// Forwards real events.
const event = { type: "click" } as React.SyntheticEvent<HTMLElement>;
wrapped(event);
expect(onClose).toHaveBeenCalledOnce();
expect(onClose).toHaveBeenCalledWith(event);
// Drops undefined events to honor legacy `(event: SyntheticEvent) => void` signature.
wrapped(undefined);
expect(onClose).toHaveBeenCalledOnce();
});
it("should leave onClose undefined when not supplied", () => {
const result = popoverPropsToNextProps({});
expect(result.onClose).to.be.undefined;
});
});
describe("placement / position", () => {
it("should convert position to placement", () => {
const result = popoverPropsToNextProps({ position: PopoverPosition.TOP_LEFT });
expect(result.placement).to.equal("top-start");
});
it("should leave placement undefined when position is auto", () => {
const result = popoverPropsToNextProps({ position: "auto" });
expect(result.placement).to.be.undefined;
});
it("should leave placement undefined when placement is auto", () => {
const result = popoverPropsToNextProps({ placement: "auto" });
expect(result.placement).to.be.undefined;
});
it("should prefer placement over position when both are supplied (placement ?? position)", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
const result = popoverPropsToNextProps({
placement: "right",
position: PopoverPosition.TOP_LEFT,
});
expect(result.placement).to.equal("right");
warnSpy.mockRestore();
});
it("should prefer explicit placement: 'auto' over position (legacy placement ?? position rule)", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
const result = popoverPropsToNextProps({
placement: "auto",
position: PopoverPosition.TOP_LEFT,
});
// Explicit `placement: "auto"` wins, mapping to undefined (autoPlacement default).
expect(result.placement).to.be.undefined;
warnSpy.mockRestore();
});
it("should warn when both placement and position are supplied (mutex)", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
popoverPropsToNextProps({ placement: "right", position: PopoverPosition.TOP_LEFT });
expect(warnSpy).toHaveBeenCalledOnce();
warnSpy.mockRestore();
});
});
describe("modifiers → middleware", () => {
it("should convert modifiers to middleware", () => {
const result = popoverPropsToNextProps({
modifiers: { flip: { options: { padding: 8 } } },
});
expect(result.middleware).to.deep.equal({ flip: { padding: 8 } });
});
it("should leave middleware undefined when modifiers is not supplied", () => {
const result = popoverPropsToNextProps({});
expect(result.middleware).to.be.undefined;
});
});
describe("minimal", () => {
it("should map minimal: true to animation: 'minimal' and arrow: false", () => {
const result = popoverPropsToNextProps({ minimal: true });
expect(result.animation).to.equal("minimal");
expect(result.arrow).to.equal(false);
});
it("should not set animation or arrow when minimal is false", () => {
const result = popoverPropsToNextProps({ minimal: false });
expect(result.animation).to.be.undefined;
expect(result.arrow).to.be.undefined;
});
});
describe("boundary", () => {
it("should remap 'clippingParents' to 'clippingAncestors'", () => {
const result = popoverPropsToNextProps({ boundary: "clippingParents" });
expect(result.boundary).to.equal("clippingAncestors");
});
it("should pass through Element boundary", () => {
const element = document.createElement("div");
const result = popoverPropsToNextProps({ boundary: element });
expect(result.boundary).to.equal(element);
});
it("should leave boundary undefined when not supplied (PopoverNext default = clippingAncestors)", () => {
const result = popoverPropsToNextProps({});
expect(result.boundary).to.be.undefined;
});
});
describe("popoverRef", () => {
it("should pass through popoverRef to PopoverNext", () => {
const ref: React.RefObject<HTMLElement> = { current: null };
const result = popoverPropsToNextProps({ popoverRef: ref });
expect(result.popoverRef).to.equal(ref);
});
it("should leave popoverRef undefined when not supplied", () => {
const result = popoverPropsToNextProps({});
expect(result.popoverRef).to.be.undefined;
});
});
describe("dropped props", () => {
it("should drop modifiersCustom with a dev warning", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
const props: Partial<PopoverProps> = { modifiersCustom: [{ name: "custom" }] };
const result = popoverPropsToNextProps(props);
expect(result).not.to.have.property("modifiersCustom");
expect(warnSpy).toHaveBeenCalledOnce();
warnSpy.mockRestore();
});
it("should drop portalStopPropagationEvents with a dev warning", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
const result = popoverPropsToNextProps({ portalStopPropagationEvents: ["click"] });
expect(result).not.to.have.property("portalStopPropagationEvents");
expect(warnSpy).toHaveBeenCalledOnce();
warnSpy.mockRestore();
});
});
});