From 5fddd685271664aff78fd88f31b66c8a0de2cadd Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Thu, 14 Aug 2025 13:49:05 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat=20[#36]=20Popover=20layer=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/index.html b/index.html index 80aec14..07a9876 100644 --- a/index.html +++ b/index.html @@ -16,6 +16,7 @@
+
From 04c43dc216d12ed9a6750dba6bc11acf6cda1289 Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Thu, 14 Aug 2025 13:49:31 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat=20[#36]=20Popover=20=EC=A0=84=EC=97=AD?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EA=B4=80=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20context=20=EB=B0=8F=20provider=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.jsx | 5 ++++- src/components/popover/popover-context.js | 5 +++++ src/components/popover/popover-provider.jsx | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/components/popover/popover-context.js create mode 100644 src/components/popover/popover-provider.jsx diff --git a/src/app.jsx b/src/app.jsx index d3303ef..0052846 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -1,5 +1,6 @@ import { BrowserRouter, Route, Routes } from "react-router"; import ModalProvider from "./components/modal/modal-provider"; +import PopoverProvider from "./components/popover/popover-provider"; import DropdownProvider from "./components/text-field/dropdown-input/dropdown-provider"; import ContentLayout from "./layouts/content-layout"; import OnboardingLayout from "./layouts/onboarding-layout"; @@ -13,7 +14,9 @@ import TestPage from "./pages/test-page"; function Provider({ children }) { return ( - {children} + + {children} + ); } diff --git a/src/components/popover/popover-context.js b/src/components/popover/popover-context.js new file mode 100644 index 0000000..06b8815 --- /dev/null +++ b/src/components/popover/popover-context.js @@ -0,0 +1,5 @@ +import { createContext } from "react"; + +const PopoverContext = createContext(); + +export default PopoverContext; diff --git a/src/components/popover/popover-provider.jsx b/src/components/popover/popover-provider.jsx new file mode 100644 index 0000000..4529615 --- /dev/null +++ b/src/components/popover/popover-provider.jsx @@ -0,0 +1,10 @@ +import { useState } from "react"; +import PopoverContext from "./popover-context"; + +function PopoverProvider({ children }) { + const [showsPopover, setShowsPopover] = useState(false); + const value = { showsPopover, setShowsPopover }; + return {children}; +} + +export default PopoverProvider; From a2c0d7fbc1ddf9cff54b7c91ba7252f8774ea80a Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Thu, 14 Aug 2025 13:50:44 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat=20[#36]=20Popover=EA=B0=80=20trigger?= =?UTF-8?q?=20=EB=90=A0=20=EB=95=8C=20left/right=20alignment=EB=A1=9C=20po?= =?UTF-8?q?pover=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/popover/popover-alignment.js | 6 +++ src/components/popover/popover.jsx | 39 ++++++++++++++++ src/hooks/use-popover.jsx | 49 +++++++++++++++++++++ src/pages/test-page.jsx | 49 +++++++++++++++++++-- 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/components/popover/popover-alignment.js create mode 100644 src/components/popover/popover.jsx create mode 100644 src/hooks/use-popover.jsx diff --git a/src/components/popover/popover-alignment.js b/src/components/popover/popover-alignment.js new file mode 100644 index 0000000..a08fdf5 --- /dev/null +++ b/src/components/popover/popover-alignment.js @@ -0,0 +1,6 @@ +const POPOVER_ALIGNMENT = Object.freeze({ + left: "left", + right: "right", +}); + +export default POPOVER_ALIGNMENT; diff --git a/src/components/popover/popover.jsx b/src/components/popover/popover.jsx new file mode 100644 index 0000000..2258612 --- /dev/null +++ b/src/components/popover/popover.jsx @@ -0,0 +1,39 @@ +import { createPortal } from "react-dom"; +import styled from "styled-components"; + +const Container = styled.div` + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +`; + +function PopoverPortal({ children }) { + return createPortal(children, document.getElementById("popover")); +} + +const StyledPopover = styled.div` + position: absolute; + top: ${({ $position }) => $position.top}px; + ${({ $position }) => ($position.left ? `left: ${$position.left}px` : "")}; + ${({ $position }) => ($position.right ? `right: ${$position.right}px` : "")}; + border-radius: 8px; + border: 1px solid #b6b6b6; + background-color: white; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); +`; + +function Popover({ isOpen, onClose, position, children }) { + return ( + isOpen && ( + + + {children} + + + ) + ); +} + +export default Popover; diff --git a/src/hooks/use-popover.jsx b/src/hooks/use-popover.jsx new file mode 100644 index 0000000..6efc314 --- /dev/null +++ b/src/hooks/use-popover.jsx @@ -0,0 +1,49 @@ +import { useContext, useState } from "react"; +import POPOVER_ALIGNMENT from "../components/popover/popover-alignment"; +import PopoverContext from "../components/popover/popover-context"; + +function calculatePopoverPosition(target, alignment) { + if (!target) { + return { top: 0, left: 0, right: 0 }; + } + + const targetRect = target.getBoundingClientRect(); + const position = { + top: targetRect.bottom + 8, + }; + + switch (alignment) { + case POPOVER_ALIGNMENT.right: + position.right = window.innerWidth - targetRect.right; + break; + default: + position.left = targetRect.left; + break; + } + + return position; +} + +function usePopover() { + const { showsPopover, setShowsPopover } = useContext(PopoverContext); + const [popoverPosition, setPopoverPosition] = useState(); + + const openPopopver = ({ target, alignment }) => { + const position = calculatePopoverPosition(target, alignment); + setShowsPopover(true); + setPopoverPosition(position); + }; + + const closePopover = () => { + setShowsPopover(false); + }; + + return { + popoverPosition, + showsPopover, + openPopopver, + closePopover, + }; +} + +export { usePopover }; diff --git a/src/pages/test-page.jsx b/src/pages/test-page.jsx index c254524..acbb892 100644 --- a/src/pages/test-page.jsx +++ b/src/pages/test-page.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import styled from "styled-components"; import smileAddImg from "../assets/ic-face-smile-add.svg"; import Badge from "../components/badge/badge"; @@ -15,10 +15,13 @@ import BUTTON_SIZE from "../components/button/button-size"; import ToggleButton from "../components/button/toggle-button"; import Header from "../components/header/header"; import Modal from "../components/modal/modal"; +import Popover from "../components/popover/popover"; +import POPOVER_ALIGNMENT from "../components/popover/popover-alignment"; import TextField from "../components/text-field/text-field"; import TEXT_FIELD_TYPE from "../components/text-field/text-field-type"; import Toast from "../components/toast/toast"; import { useModal } from "../hooks/use-modal"; +import { usePopover } from "../hooks/use-popover"; import { useToast } from "../hooks/use-toast"; const OutlinedHeader = styled(Header)` @@ -44,11 +47,30 @@ function TestPage() { const handleToastClick = () => setShowsToast(true); const handleToastDismiss = () => setShowsToast(false); - + /* Modal */ const { showsModal, setShowsModal } = useModal(); const handleModalClick = () => setShowsModal(true); - + + /* Popover */ + const { popoverPosition, showsPopover, openPopopver, closePopover } = + usePopover(); + const popoverLeftRef = useRef(); + const popoverRightRef = useRef(); + + const handlePopoverLeftClick = () => { + openPopopver({ + target: popoverLeftRef.current, + alignment: POPOVER_ALIGNMENT.left, + }); + }; + const handlePopoverRightClick = () => { + openPopopver({ + target: popoverRightRef.current, + alignment: POPOVER_ALIGNMENT.right, + }); + }; + return (
)}
+
+ + + +

This is Popover.

+
+
); } From 9aae688abbb429a42d62447d688518e82ba1037c Mon Sep 17 00:00:00 2001 From: Chamsol Kim Date: Thu, 14 Aug 2025 15:01:41 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat=20[#36]=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=20=EC=B0=BD=20=ED=81=AC=EA=B8=B0=EB=A5=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=A0=20=EB=95=8C=20popover=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/use-popover.jsx | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/hooks/use-popover.jsx b/src/hooks/use-popover.jsx index 6efc314..70cea0d 100644 --- a/src/hooks/use-popover.jsx +++ b/src/hooks/use-popover.jsx @@ -1,4 +1,4 @@ -import { useContext, useState } from "react"; +import { useContext, useState, useEffect } from "react"; import POPOVER_ALIGNMENT from "../components/popover/popover-alignment"; import PopoverContext from "../components/popover/popover-context"; @@ -27,17 +27,36 @@ function calculatePopoverPosition(target, alignment) { function usePopover() { const { showsPopover, setShowsPopover } = useContext(PopoverContext); const [popoverPosition, setPopoverPosition] = useState(); + const [target, setTarget] = useState(); + const [alignment, setAlignment] = useState(POPOVER_ALIGNMENT.left); const openPopopver = ({ target, alignment }) => { - const position = calculatePopoverPosition(target, alignment); + updatePopoverPosition(target, alignment); + setTarget(target); + setAlignment(alignment); setShowsPopover(true); - setPopoverPosition(position); }; const closePopover = () => { setShowsPopover(false); }; + const updatePopoverPosition = (target, alignment) => { + const position = calculatePopoverPosition(target, alignment); + setPopoverPosition(position); + }; + + useEffect(() => { + if (!showsPopover) return; + + function handleWindowResize() { + updatePopoverPosition(target, alignment); + } + + window.addEventListener("resize", handleWindowResize); + return () => window.removeEventListener("resize", handleWindowResize); + }, [showsPopover, target, alignment]); + return { popoverPosition, showsPopover,