diff --git a/openhands-ui/bun.lock b/openhands-ui/bun.lock index cd6069eb6c..39883e4560 100644 --- a/openhands-ui/bun.lock +++ b/openhands-ui/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@floating-ui/react": "^0.27.12", "clsx": "^2.1.1", + "focus-trap-react": "^11.0.4", "react": "^19.1.0", "react-bootstrap-icons": "^1.11.6", "react-dom": "^19.1.0", @@ -297,6 +298,8 @@ "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], + "@types/resolve": ["@types/resolve@1.20.6", "", {}, "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="], @@ -417,6 +420,10 @@ "find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], + "focus-trap": ["focus-trap@7.6.5", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg=="], + + "focus-trap-react": ["focus-trap-react@11.0.4", "", { "dependencies": { "focus-trap": "^7.6.5", "tabbable": "^6.2.0" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-tC7jC/yqeAqhe4irNIzdyDf9XCtGSeECHiBSYJBO/vIN0asizbKZCt8TarB6/XqIceu42ajQ/U4lQJ9pZlWjrg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], diff --git a/openhands-ui/components/dialog/Dialog.stories.tsx b/openhands-ui/components/dialog/Dialog.stories.tsx new file mode 100644 index 0000000000..9e4722b969 --- /dev/null +++ b/openhands-ui/components/dialog/Dialog.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Dialog } from "./Dialog"; +import { useState } from "react"; +import { Button } from "../button/Button"; + +const meta = { + title: "Components/Dialog", + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const DialogComponent = () => { + const [open, setOpen] = useState(false); + return ( +
+ + + DialogContent + +
+ ); +}; + +export const Main: Story = { + render: ({}) => , +}; diff --git a/openhands-ui/components/dialog/Dialog.tsx b/openhands-ui/components/dialog/Dialog.tsx new file mode 100644 index 0000000000..316f056524 --- /dev/null +++ b/openhands-ui/components/dialog/Dialog.tsx @@ -0,0 +1,101 @@ +import { + useEffect, + useId, + useRef, + useState, + type PropsWithChildren, +} from "react"; +import type { HTMLProps } from "../../shared/types"; +import { cn } from "../../shared/utils/cn"; +import { Icon } from "../icon/Icon"; +import { createPortal } from "react-dom"; +import { + FloatingOverlay, + FloatingPortal, + useDismiss, + useFloating, + useInteractions, + useRole, + useTransitionStyles, + useFocus, +} from "@floating-ui/react"; +import { FocusTrap } from "focus-trap-react"; + +export type DialogProps = HTMLProps<"div"> & { + open: boolean; + onOpenChange(value: boolean): void; +}; + +export const Dialog = ({ + open, + onOpenChange, + className, + children, +}: PropsWithChildren) => { + const id = useId(); + + const { refs, context } = useFloating({ + open, + onOpenChange, + }); + + const dismiss = useDismiss(context); + const role = useRole(context); + const focusTrap = useFocus(context, {}); + + const { getFloatingProps } = useInteractions([dismiss, role, focusTrap]); + + const { isMounted, styles } = useTransitionStyles(context, { + duration: { + open: 200, + close: 200, + }, + + initial: { + opacity: 0, + }, + open: { + opacity: 1, + }, + close: { + opacity: 0, + }, + }); + + if (!isMounted) { + return null; + } + + return ( + + + +
+
{children}
+ +
+
+
+
+ ); +}; diff --git a/openhands-ui/package.json b/openhands-ui/package.json index 7012f35eea..51ab0db865 100644 --- a/openhands-ui/package.json +++ b/openhands-ui/package.json @@ -31,6 +31,7 @@ "@floating-ui/react": "^0.27.12", "clsx": "^2.1.1", "react": "^19.1.0", + "focus-trap-react": "^11.0.4", "react-bootstrap-icons": "^1.11.6", "react-dom": "^19.1.0", "tailwind-merge": "^3.3.1",