diff --git a/src/components/Filter/components/Filter/components/Backdrop/backdrop.styles.ts b/src/components/Filter/components/Filter/components/Backdrop/backdrop.styles.ts
new file mode 100644
index 00000000..662030be
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/Backdrop/backdrop.styles.ts
@@ -0,0 +1,9 @@
+import styled from "@emotion/styled";
+import { Backdrop } from "@mui/material";
+
+export const StyledBackdrop = styled(Backdrop)`
+ background-color: transparent;
+ overflow: hidden;
+ overscroll-behavior: none;
+ z-index: 1300;
+`;
diff --git a/src/components/Filter/components/Filter/components/Backdrop/backdrop.tsx b/src/components/Filter/components/Filter/components/Backdrop/backdrop.tsx
new file mode 100644
index 00000000..54264260
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/Backdrop/backdrop.tsx
@@ -0,0 +1,15 @@
+import { Portal } from "@mui/material";
+import { JSX } from "react";
+import { useCloseOnEscape } from "../../hooks/UseCloseOnEscape/hook";
+import { StyledBackdrop } from "./backdrop.styles";
+import { BACKDROP_PROPS } from "./constants";
+import { BackdropProps } from "./types";
+
+export const Backdrop = (props: BackdropProps): JSX.Element => {
+ useCloseOnEscape({ onClose: props.onClick, open: props.open });
+ return (
+
+
+
+ );
+};
diff --git a/src/components/Filter/components/Filter/components/Backdrop/constants.ts b/src/components/Filter/components/Filter/components/Backdrop/constants.ts
new file mode 100644
index 00000000..b588f319
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/Backdrop/constants.ts
@@ -0,0 +1,5 @@
+import { BackdropProps } from "@mui/material";
+
+export const BACKDROP_PROPS: Omit = {
+ slotProps: { transition: { unmountOnExit: true } },
+};
diff --git a/src/components/Filter/components/Filter/components/Backdrop/types.ts b/src/components/Filter/components/Filter/components/Backdrop/types.ts
new file mode 100644
index 00000000..eed58f27
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/Backdrop/types.ts
@@ -0,0 +1,5 @@
+import { BackdropProps as MBackdropProps } from "@mui/material";
+
+export interface BackdropProps extends Omit {
+ onClick: () => void;
+}
diff --git a/src/components/Filter/components/Filter/components/DrawerTransition/constants.ts b/src/components/Filter/components/Filter/components/DrawerTransition/constants.ts
new file mode 100644
index 00000000..5ac7258c
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/DrawerTransition/constants.ts
@@ -0,0 +1,8 @@
+import { SlideProps } from "@mui/material";
+
+export const SIDE_PROPS: Omit = {
+ direction: "right",
+ easing: "ease-out",
+ timeout: { enter: 250, exit: 300 },
+ unmountOnExit: true,
+};
diff --git a/src/components/Filter/components/Filter/components/DrawerTransition/drawerTransition.tsx b/src/components/Filter/components/Filter/components/DrawerTransition/drawerTransition.tsx
index 6d11987d..c4a94079 100644
--- a/src/components/Filter/components/Filter/components/DrawerTransition/drawerTransition.tsx
+++ b/src/components/Filter/components/Filter/components/DrawerTransition/drawerTransition.tsx
@@ -1,24 +1,26 @@
-import { Slide as MSlide, SlideProps as MSlideProps } from "@mui/material";
-import { JSX, forwardRef } from "react";
+import { Slide } from "@mui/material";
+import { JSX } from "react";
+import { SIDE_PROPS } from "./constants";
+import { DrawerTransitionProps } from "./types";
-export const DrawerTransition = forwardRef(
- function DrawerTransition(
- {
- children,
- ...props /* Spread props to allow for Mui SlideProps specific prop overrides. */
- }: MSlideProps,
- ref,
- ): JSX.Element {
- return (
-
- {children}
-
- );
- },
-);
+/**
+ * Slide transition used for the drawer-surface filter popover.
+ * @param props - Component props (extends MUI SlideProps).
+ * @param props.children - Transition child.
+ * @param props.placement - Popper placement; consumed only to keep it off the DOM.
+ * @param props.ref - Forwarded ref.
+ * @returns Slide transition element.
+ */
+export const DrawerTransition = ({
+ children,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- destructured out so it doesn't spread onto the DOM Slide element
+ placement: _placement,
+ ref,
+ ...props
+}: DrawerTransitionProps): JSX.Element => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/Filter/components/Filter/components/DrawerTransition/types.ts b/src/components/Filter/components/Filter/components/DrawerTransition/types.ts
new file mode 100644
index 00000000..f32305ec
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/DrawerTransition/types.ts
@@ -0,0 +1,7 @@
+import { PopperPlacementType, SlideProps } from "@mui/material";
+import { Ref } from "react";
+
+export interface DrawerTransitionProps extends SlideProps {
+ placement?: PopperPlacementType;
+ ref?: Ref;
+}
diff --git a/src/components/Filter/components/Filter/components/MenuTransition/constants.ts b/src/components/Filter/components/Filter/components/MenuTransition/constants.ts
new file mode 100644
index 00000000..b15c233f
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/MenuTransition/constants.ts
@@ -0,0 +1,11 @@
+import { PopperPlacementType } from "@mui/material";
+import { CSSProperties } from "react";
+import { POPPER_PROPS } from "../../../../../../styles/common/mui/popper";
+
+export const PLACEMENT_TRANSFORM_ORIGIN: Partial<
+ Record
+> = {
+ [POPPER_PROPS.PLACEMENT.BOTTOM_START]: "0 0 0",
+ [POPPER_PROPS.PLACEMENT.RIGHT]: "0 50% 0",
+ [POPPER_PROPS.PLACEMENT.TOP_START]: "0 100% 0",
+};
diff --git a/src/components/Filter/components/Filter/components/MenuTransition/menuTransition.tsx b/src/components/Filter/components/Filter/components/MenuTransition/menuTransition.tsx
new file mode 100644
index 00000000..3acacadc
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/MenuTransition/menuTransition.tsx
@@ -0,0 +1,31 @@
+import { Grow } from "@mui/material";
+import { JSX } from "react";
+import { POPPER_PROPS } from "../../../../../../styles/common/mui/popper";
+import { PLACEMENT_TRANSFORM_ORIGIN } from "./constants";
+import { MenuTransitionProps } from "./types";
+
+/**
+ * Grow transition with a placement-driven transform origin.
+ * @param props - Component props.
+ * @param props.placement - Popper placement, drives the transform origin.
+ * @param props.ref - Forwarded ref.
+ * @param props.style - Style merged with the placement-derived transform origin.
+ * @returns Grow transition element.
+ */
+export const MenuTransition = ({
+ placement = POPPER_PROPS.PLACEMENT.BOTTOM_START,
+ ref,
+ style,
+ ...props
+}: MenuTransitionProps): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/src/components/Filter/components/Filter/components/MenuTransition/types.ts b/src/components/Filter/components/Filter/components/MenuTransition/types.ts
new file mode 100644
index 00000000..22ab7148
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/MenuTransition/types.ts
@@ -0,0 +1,7 @@
+import { GrowProps, PopperPlacementType } from "@mui/material";
+import { Ref } from "react";
+
+export interface MenuTransitionProps extends GrowProps {
+ placement?: PopperPlacementType;
+ ref?: Ref;
+}
diff --git a/src/components/Filter/components/Filter/components/Popper/constants.ts b/src/components/Filter/components/Filter/components/Popper/constants.ts
new file mode 100644
index 00000000..eeb30eb0
--- /dev/null
+++ b/src/components/Filter/components/Filter/components/Popper/constants.ts
@@ -0,0 +1,22 @@
+import { PopperProps } from "@mui/material";
+import { POPPER_PROPS as MUI_POPPER_PROPS } from "../../../../../../styles/common/mui/popper";
+
+export const POPPER_PROPS: Omit = {
+ placement: MUI_POPPER_PROPS.PLACEMENT.BOTTOM_START,
+ popperOptions: {
+ modifiers: [
+ {
+ name: "flip",
+ options: {
+ fallbackPlacements: [
+ MUI_POPPER_PROPS.PLACEMENT.TOP_START,
+ MUI_POPPER_PROPS.PLACEMENT.RIGHT,
+ ],
+ },
+ },
+ { name: "offset", options: { offset: [0, 4] } },
+ ],
+ },
+ role: "dialog",
+ transition: true,
+};
diff --git a/src/components/Filter/components/Filter/filter.styles.ts b/src/components/Filter/components/Filter/filter.styles.ts
index a503433d..b700a56c 100644
--- a/src/components/Filter/components/Filter/filter.styles.ts
+++ b/src/components/Filter/components/Filter/filter.styles.ts
@@ -1,26 +1,34 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";
-import { Popover } from "@mui/material";
+import { Popper } from "@mui/material";
import { PALETTE } from "../../../../styles/common/constants/palette";
import { SURFACE_TYPE } from "../surfaces/types";
-import { FilterProps } from "./filter";
+import { FilterProps } from "./types";
-export const StyledPopover = styled(Popover, {
+export const StyledPopper = styled(Popper, {
shouldForwardProp: (prop) => prop !== "surfaceType",
})>`
- .MuiPaper-menu {
- margin: 4px 0;
+ z-index: 1300;
+
+ .MuiPaper-root {
+ overflow: hidden;
+ overscroll-behavior: none;
}
${({ surfaceType }) =>
surfaceType === SURFACE_TYPE.DRAWER &&
css`
+ inset: 0 auto 0 0 !important; // required to override Popper's default positioning for correct placement within the drawer.
+ transform: none !important; // required to override Popper's transform for correct positioning within the drawer.
+
.MuiPaper-root {
background-color: ${PALETTE.SMOKE_LIGHT};
+ border: none;
+ border-radius: 0;
+ box-shadow: none;
height: 100%;
margin: 0;
max-height: 100%;
- overflow: visible; // required; allows backdrop button to render outside of drawer container.
}
`}
`;
diff --git a/src/components/Filter/components/Filter/filter.tsx b/src/components/Filter/components/Filter/filter.tsx
index ffbb5ea1..6faf1398 100644
--- a/src/components/Filter/components/Filter/filter.tsx
+++ b/src/components/Filter/components/Filter/filter.tsx
@@ -1,17 +1,20 @@
-import { Grow, PopoverPosition, PopoverProps } from "@mui/material";
-import { JSX, MouseEvent, ReactNode, useState } from "react";
+import { Paper } from "@mui/material";
+import { JSX } from "react";
import { isRangeCategoryView } from "../../../../common/categories/views/range/typeGuards";
-import { CategoryView } from "../../../../common/categories/views/types";
-import { TrackFilterOpenedFunction } from "../../../../config/entities";
-import { OnFilterFn } from "../../../../hooks/useCategoryFilter";
+import { PAPER_PROPS } from "../../../../styles/common/mui/paper";
import { TEST_IDS } from "../../../../tests/testIds";
+import { PopperProvider } from "../../../common/Popper/provider/provider";
import { FilterLabel } from "../FilterLabel/filterLabel";
import { FilterMenu } from "../FilterMenu/filterMenu";
import { FilterRange } from "../FilterRange/filterRange";
import { IconButton } from "../surfaces/drawer/components/IconButton/iconButton";
import { SURFACE_TYPE } from "../surfaces/types";
+import { Backdrop } from "./components/Backdrop/backdrop";
import { DrawerTransition } from "./components/DrawerTransition/drawerTransition";
-import { StyledPopover } from "./filter.styles";
+import { MenuTransition } from "./components/MenuTransition/menuTransition";
+import { POPPER_PROPS } from "./components/Popper/constants";
+import { StyledPopper } from "./filter.styles";
+import { FilterProps } from "./types";
/**
* Filter component.
@@ -19,24 +22,6 @@ import { StyledPopover } from "./filter.styles";
* TODO(cc) tests: add tests for selected values (rending of tags) for select and range categories.
*/
-const DEFAULT_POSITION: PopoverPosition = { left: 0, top: 0 };
-const DEFAULT_SLOT_PROPS: PopoverProps["slotProps"] = {
- paper: { variant: "menu" },
-};
-const DRAWER_SLOT_PROPS: PopoverProps["slotProps"] = {
- paper: { elevation: 0 },
-};
-
-export interface FilterProps {
- categorySection?: string;
- categoryView: CategoryView;
- closeAncestor?: () => void;
- onFilter: OnFilterFn;
- surfaceType: SURFACE_TYPE;
- tags?: ReactNode; // e.g. filter tags
- trackFilterOpened?: TrackFilterOpenedFunction;
-}
-
export const Filter = ({
categorySection,
categoryView,
@@ -46,97 +31,92 @@ export const Filter = ({
tags,
trackFilterOpened,
}: FilterProps): JSX.Element => {
- const [isOpen, setIsOpen] = useState(false);
- const [position, setPosition] = useState(DEFAULT_POSITION);
const isDrawer = surfaceType === SURFACE_TYPE.DRAWER;
- const anchorPosition = isDrawer ? DEFAULT_POSITION : position;
- const slotProps = isDrawer ? DRAWER_SLOT_PROPS : DEFAULT_SLOT_PROPS;
- const TransitionComponent = isDrawer ? DrawerTransition : Grow;
- const transitionDuration = isOpen ? 250 : 300;
- const TransitionDuration = isDrawer ? transitionDuration : undefined;
const isRangeView = isRangeCategoryView(categoryView);
-
- /**
- * Closes filter popover.
- */
- const onCloseFilter = (): void => {
- setIsOpen(false);
- };
-
- /**
- * Closes filter and all open ancestors e.g. filter drawer.
- */
- const onCloseFilters = (): void => {
- closeAncestor?.();
- onCloseFilter();
- };
-
- /**
- * Opens filter popover and sets popover position.
- * @param event - Mouse event interaction with filter target.
- */
- const onOpenFilter = (event: MouseEvent): void => {
- // Grab the filter target size and position and calculate the popover position.
- const targetDOMRect = event.currentTarget.getBoundingClientRect();
- const popoverLeftPos = targetDOMRect.x;
- const popoverTopPos = targetDOMRect.y + targetDOMRect.height - 1;
- // Set popover position and open state.
- setPosition({ left: popoverLeftPos, top: popoverTopPos });
- setIsOpen(true);
- trackFilterOpened?.({ category: categoryView.key });
- };
-
return (
- <>
-
-
- {isDrawer && }
- {isRangeView ? (
-
- ) : (
-
- )}
-
- {tags}
- >
+
+ {({ anchorEl, onClose, onOpen, open }): JSX.Element => {
+ const TransitionComponent = isDrawer
+ ? DrawerTransition
+ : MenuTransition;
+ return (
+ <>
+ {
+ onOpen(e);
+ trackFilterOpened?.({ category: categoryView.key });
+ }}
+ surfaceType={surfaceType}
+ />
+ {
+ onClose();
+ closeAncestor?.();
+ }}
+ open={open}
+ />
+
+ {({ placement, TransitionProps }): JSX.Element => {
+ return (
+
+
+ {isDrawer && (
+ {
+ onClose();
+ closeAncestor?.();
+ }}
+ />
+ )}
+ {isRangeView ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ }}
+
+ {tags}
+ >
+ );
+ }}
+
);
};
diff --git a/src/components/Filter/components/Filter/hooks/UseCloseOnEscape/hook.ts b/src/components/Filter/components/Filter/hooks/UseCloseOnEscape/hook.ts
new file mode 100644
index 00000000..342158ae
--- /dev/null
+++ b/src/components/Filter/components/Filter/hooks/UseCloseOnEscape/hook.ts
@@ -0,0 +1,29 @@
+import { useEffect } from "react";
+import { UseCloseOnEscapeProps } from "./types";
+
+/**
+ * Closes the popper, and any ancestor drawer, when the Escape key is pressed.
+ * @param props - Hook props.
+ * @param props.onClose - Function to call when the Escape key is pressed.
+ * @param props.open - Whether the popper is open.
+ */
+export const useCloseOnEscape = ({
+ onClose,
+ open,
+}: UseCloseOnEscapeProps): void => {
+ useEffect(() => {
+ if (!open) return;
+
+ const onKeyDown = (e: KeyboardEvent): void => {
+ if (e.key === "Escape") {
+ e.stopPropagation();
+ onClose();
+ }
+ };
+
+ document.addEventListener("keydown", onKeyDown, true);
+ return (): void => {
+ document.removeEventListener("keydown", onKeyDown, true);
+ };
+ }, [onClose, open]);
+};
diff --git a/src/components/Filter/components/Filter/hooks/UseCloseOnEscape/types.ts b/src/components/Filter/components/Filter/hooks/UseCloseOnEscape/types.ts
new file mode 100644
index 00000000..5315bae3
--- /dev/null
+++ b/src/components/Filter/components/Filter/hooks/UseCloseOnEscape/types.ts
@@ -0,0 +1,4 @@
+export interface UseCloseOnEscapeProps {
+ onClose: () => void;
+ open: boolean;
+}
diff --git a/src/components/Filter/components/Filter/types.ts b/src/components/Filter/components/Filter/types.ts
new file mode 100644
index 00000000..26e1c259
--- /dev/null
+++ b/src/components/Filter/components/Filter/types.ts
@@ -0,0 +1,15 @@
+import { ReactNode } from "react";
+import { CategoryView } from "../../../../common/categories/views/types";
+import { TrackFilterOpenedFunction } from "../../../../config/entities";
+import { OnFilterFn } from "../../../../hooks/useCategoryFilter";
+import { SURFACE_TYPE } from "../surfaces/types";
+
+export interface FilterProps {
+ categorySection?: string;
+ categoryView: CategoryView;
+ closeAncestor?: () => void;
+ onFilter: OnFilterFn;
+ surfaceType: SURFACE_TYPE;
+ tags?: ReactNode; // e.g. filter tags
+ trackFilterOpened?: TrackFilterOpenedFunction;
+}
diff --git a/src/components/Filter/components/FilterMenu/filterMenu.styles.ts b/src/components/Filter/components/FilterMenu/filterMenu.styles.ts
index 1557dd06..2278d9b4 100644
--- a/src/components/Filter/components/FilterMenu/filterMenu.styles.ts
+++ b/src/components/Filter/components/FilterMenu/filterMenu.styles.ts
@@ -1,6 +1,6 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";
-import { FilterProps } from "../Filter/filter";
+import { FilterProps } from "../Filter/types";
import { SURFACE_TYPE } from "../surfaces/types";
interface Props {
diff --git a/src/components/Filter/components/FilterRange/filterRange.styles.ts b/src/components/Filter/components/FilterRange/filterRange.styles.ts
index 2b89d0de..8c78bbdf 100644
--- a/src/components/Filter/components/FilterRange/filterRange.styles.ts
+++ b/src/components/Filter/components/FilterRange/filterRange.styles.ts
@@ -99,23 +99,21 @@ export const StyledForm = styled("form")>`
${({ surfaceType }) =>
surfaceType === SURFACE_TYPE.DRAWER &&
css`
- {
- padding: 0 16px;
- width: 312px;
-
- .MuiGrid-root {
- gap: 16px 0;
- grid-template-rows: auto auto;
- margin: 16px 0;
-
- .MuiFormControl-root {
- grid-row: unset;
- grid-template-rows: unset;
- }
+ padding: 0 16px;
+ width: 312px;
- .MuiDivider-root {
- display: none;
- }
+ .MuiGrid-root {
+ gap: 16px 0;
+ grid-template-rows: auto auto;
+ margin: 16px 0;
+
+ .MuiFormControl-root {
+ grid-row: unset;
+ grid-template-rows: unset;
+ }
+
+ .MuiDivider-root {
+ display: none;
}
}
`}
diff --git a/src/components/Filter/components/Filters/stories/filters.stories.tsx b/src/components/Filter/components/Filters/stories/filters.stories.tsx
index 8b193118..01fe108f 100644
--- a/src/components/Filter/components/Filters/stories/filters.stories.tsx
+++ b/src/components/Filter/components/Filters/stories/filters.stories.tsx
@@ -8,11 +8,23 @@ const meta: Meta = {
component: Filters,
decorators: [
(Story): JSX.Element => (
-
-
+
+
+
+
),
],
+ parameters: {
+ layout: "fullscreen",
+ },
};
export default meta;
diff --git a/src/components/Filter/components/VariableSizeList/VariableSizeList.tsx b/src/components/Filter/components/VariableSizeList/VariableSizeList.tsx
index 6b9d2073..b779be5e 100644
--- a/src/components/Filter/components/VariableSizeList/VariableSizeList.tsx
+++ b/src/components/Filter/components/VariableSizeList/VariableSizeList.tsx
@@ -120,6 +120,7 @@ export const VariableSizeList = ({
onItemsRendered={(): void => listRef.current?.resetAfterIndex(0)} // Facilitates correct positioning of list items when list scrolls.
overscanCount={overscanCount}
ref={listRef}
+ style={{ overscrollBehavior: "none" }}
width={width}
>
{renderListItem}
diff --git a/src/components/Layout/components/Header/components/Content/components/Actions/components/Authentication/components/AuthenticationMenu/constants.ts b/src/components/Layout/components/Header/components/Content/components/Actions/components/Authentication/components/AuthenticationMenu/constants.ts
index f9885ef1..90e2e844 100644
--- a/src/components/Layout/components/Header/components/Content/components/Actions/components/Authentication/components/AuthenticationMenu/constants.ts
+++ b/src/components/Layout/components/Header/components/Content/components/Actions/components/Authentication/components/AuthenticationMenu/constants.ts
@@ -1,5 +1,5 @@
import { MenuProps } from "@mui/material";
-import { VARIANT } from "../../../../../../../../../../../../styles/common/mui/paper";
+import { PAPER_PROPS } from "../../../../../../../../../../../../styles/common/mui/paper";
import {
POPOVER_ORIGIN_HORIZONTAL,
POPOVER_ORIGIN_VERTICAL,
@@ -11,7 +11,7 @@ export const MENU_PROPS: Partial = {
vertical: POPOVER_ORIGIN_VERTICAL.BOTTOM,
},
autoFocus: false,
- slotProps: { paper: { variant: VARIANT.MENU } },
+ slotProps: { paper: { variant: PAPER_PROPS.VARIANT.MENU } },
transformOrigin: {
horizontal: POPOVER_ORIGIN_HORIZONTAL.RIGHT,
vertical: POPOVER_ORIGIN_VERTICAL.TOP,
diff --git a/src/components/Layout/components/Sidebar/components/SidebarDrawer/sidebarDrawer.tsx b/src/components/Layout/components/Sidebar/components/SidebarDrawer/sidebarDrawer.tsx
index ac6613ca..28e8d8b5 100644
--- a/src/components/Layout/components/Sidebar/components/SidebarDrawer/sidebarDrawer.tsx
+++ b/src/components/Layout/components/Sidebar/components/SidebarDrawer/sidebarDrawer.tsx
@@ -33,7 +33,6 @@ export const SidebarDrawer = ({
open={open}
slotProps={DRAWER_SLOT_PROPS}
TransitionComponent={DrawerTransition}
- transitionDuration={open ? 250 : 300}
>
{children}
diff --git a/src/components/common/Popper/provider/context.ts b/src/components/common/Popper/provider/context.ts
new file mode 100644
index 00000000..a691e343
--- /dev/null
+++ b/src/components/common/Popper/provider/context.ts
@@ -0,0 +1,9 @@
+import { createContext } from "react";
+import { PopperContextProps } from "./types";
+
+export const PopperContext = createContext({
+ anchorEl: null,
+ onClose: () => {},
+ onOpen: () => {},
+ open: false,
+});
diff --git a/src/components/common/Popper/provider/hook.ts b/src/components/common/Popper/provider/hook.ts
new file mode 100644
index 00000000..c9943180
--- /dev/null
+++ b/src/components/common/Popper/provider/hook.ts
@@ -0,0 +1,11 @@
+import { useContext } from "react";
+import { PopperContext } from "./context";
+import { PopperContextProps } from "./types";
+
+/**
+ * Returns popper context.
+ * @returns popper context.
+ */
+export const usePopper = (): PopperContextProps => {
+ return useContext(PopperContext);
+};
diff --git a/src/components/common/Popper/provider/provider.tsx b/src/components/common/Popper/provider/provider.tsx
new file mode 100644
index 00000000..5295ed9f
--- /dev/null
+++ b/src/components/common/Popper/provider/provider.tsx
@@ -0,0 +1,24 @@
+import { JSX, MouseEvent, useCallback, useState } from "react";
+import { PopperContext } from "./context";
+import { PopperProviderProps } from "./types";
+
+export function PopperProvider({ children }: PopperProviderProps): JSX.Element {
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const open = Boolean(anchorEl);
+
+ const onClose = useCallback(() => setAnchorEl(null), []);
+
+ const onOpen = useCallback(
+ (event: MouseEvent) => setAnchorEl(event.currentTarget),
+ [],
+ );
+
+ return (
+
+ {typeof children === "function"
+ ? children({ anchorEl, onClose, onOpen, open })
+ : children}
+
+ );
+}
diff --git a/src/components/common/Popper/provider/types.ts b/src/components/common/Popper/provider/types.ts
new file mode 100644
index 00000000..6d9df5f3
--- /dev/null
+++ b/src/components/common/Popper/provider/types.ts
@@ -0,0 +1,11 @@
+import { PopperProps } from "@mui/material";
+import { MouseEvent, ReactNode } from "react";
+
+export type PopperContextProps = Pick & {
+ onClose: () => void;
+ onOpen: (e: MouseEvent) => void;
+};
+
+export type PopperProviderProps = {
+ children: ReactNode | ((props: PopperContextProps) => ReactNode);
+};
diff --git a/src/styles/common/mui/paper.ts b/src/styles/common/mui/paper.ts
index 86f7c29b..3897d8ab 100644
--- a/src/styles/common/mui/paper.ts
+++ b/src/styles/common/mui/paper.ts
@@ -1,10 +1,18 @@
import { PaperProps } from "@mui/material";
-export const VARIANT: Record = {
+type PaperPropsOptions = {
+ VARIANT: typeof VARIANT;
+};
+
+const VARIANT = {
ELEVATION: "elevation",
FOOTER: "footer",
MENU: "menu",
OUTLINED: "outlined",
PANEL: "panel",
SEARCH_BAR: "searchbar",
+} as const satisfies Record;
+
+export const PAPER_PROPS: PaperPropsOptions = {
+ VARIANT,
};
diff --git a/src/styles/common/mui/popper.ts b/src/styles/common/mui/popper.ts
new file mode 100644
index 00000000..b335d626
--- /dev/null
+++ b/src/styles/common/mui/popper.ts
@@ -0,0 +1,27 @@
+import { PopperProps } from "@mui/material";
+
+type PopperPropsOptions = {
+ PLACEMENT: typeof PLACEMENT;
+};
+
+const PLACEMENT = {
+ AUTO: "auto",
+ AUTO_END: "auto-end",
+ AUTO_START: "auto-start",
+ BOTTOM: "bottom",
+ BOTTOM_END: "bottom-end",
+ BOTTOM_START: "bottom-start",
+ LEFT: "left",
+ LEFT_END: "left-end",
+ LEFT_START: "left-start",
+ RIGHT: "right",
+ RIGHT_END: "right-end",
+ RIGHT_START: "right-start",
+ TOP: "top",
+ TOP_END: "top-end",
+ TOP_START: "top-start",
+} as const satisfies Record;
+
+export const POPPER_PROPS: PopperPropsOptions = {
+ PLACEMENT,
+};