From 1fccc0c04e7d1157408fe22328dfc5aeeca420a8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 20 Aug 2025 19:14:09 +0530 Subject: [PATCH] feat: popover component added to propel package --- packages/propel/src/popover/index.ts | 1 + packages/propel/src/popover/root.tsx | 122 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 packages/propel/src/popover/index.ts create mode 100644 packages/propel/src/popover/root.tsx diff --git a/packages/propel/src/popover/index.ts b/packages/propel/src/popover/index.ts new file mode 100644 index 00000000000..50a9c47c01f --- /dev/null +++ b/packages/propel/src/popover/index.ts @@ -0,0 +1 @@ +export * from "./root"; \ No newline at end of file diff --git a/packages/propel/src/popover/root.tsx b/packages/propel/src/popover/root.tsx new file mode 100644 index 00000000000..15ff23df675 --- /dev/null +++ b/packages/propel/src/popover/root.tsx @@ -0,0 +1,122 @@ +import * as React from "react"; +import { Popover as BasePopover } from "@base-ui-components/react/popover"; + +// types +export type Placement = + | "auto" + | "auto-start" + | "auto-end" + | "top-start" + | "top-end" + | "bottom-start" + | "bottom-end" + | "right-start" + | "right-end" + | "left-start" + | "left-end" + | "top" + | "bottom" + | "right" + | "left"; + +type Side = "top" | "bottom" | "left" | "right"; +type Align = "start" | "center" | "end"; + +export interface PopoverContentProps extends React.ComponentProps { + placement?: Placement; + align?: Align; + sideOffset?: BasePopover.Positioner.Props["sideOffset"]; + side?: Side; + containerRef?: React.RefObject; +} + +// placement conversion map +const PLACEMENT_MAP = new Map([ + ["auto", { side: "bottom", align: "center" }], + ["auto-start", { side: "bottom", align: "start" }], + ["auto-end", { side: "bottom", align: "end" }], + ["top", { side: "top", align: "center" }], + ["bottom", { side: "bottom", align: "center" }], + ["left", { side: "left", align: "center" }], + ["right", { side: "right", align: "center" }], + ["top-start", { side: "top", align: "start" }], + ["top-end", { side: "top", align: "end" }], + ["bottom-start", { side: "bottom", align: "start" }], + ["bottom-end", { side: "bottom", align: "end" }], + ["left-start", { side: "left", align: "start" }], + ["left-end", { side: "left", align: "end" }], + ["right-start", { side: "right", align: "start" }], + ["right-end", { side: "right", align: "end" }], +]); + +// conversion function +export function convertPlacementToSideAndAlign(placement: Placement): { + side: Side; + align: Align; +} { + return PLACEMENT_MAP.get(placement) || { side: "bottom", align: "center" }; +} + +// PopoverContent component +const PopoverContent = React.memo(function PopoverContent({ + children, + className, + placement, + side = "bottom", + align = "center", + sideOffset = 8, + containerRef, + ...props +}) { + // side and align calculations + const { finalSide, finalAlign } = React.useMemo(() => { + if (placement) { + const converted = convertPlacementToSideAndAlign(placement); + return { finalSide: converted.side, finalAlign: converted.align }; + } + return { finalSide: side, finalAlign: align }; + }, [placement, side, align]); + + return ( + + + + {children} + + + + ); +}); + +// wrapper components +const PopoverTrigger = React.memo>(function PopoverTrigger(props) { + return ; +}); + +const PopoverPortal = React.memo>(function PopoverPortal(props) { + return ; +}); + +const PopoverPositioner = React.memo>(function PopoverPositioner(props) { + return ; +}); + +// compound components +const Popover = Object.assign( + React.memo>(function Popover(props) { + return ; + }), + { + Button: PopoverTrigger, + Panel: PopoverContent, + } +); + +// display names +PopoverContent.displayName = "PopoverContent"; +Popover.displayName = "Popover"; +PopoverPortal.displayName = "PopoverPortal"; +PopoverTrigger.displayName = "PopoverTrigger"; +PopoverPositioner.displayName = "PopoverPositioner"; + +export { Popover};