From b2f432926667466f87ed80198d1075d191dd64ae Mon Sep 17 00:00:00 2001 From: TheThomaas Date: Thu, 5 Feb 2026 15:24:44 +0100 Subject: [PATCH] Create Dialog system --- src/renderer/src/components/Dialog/index.css | 157 +++++++++++++++++++ src/renderer/src/components/Dialog/index.tsx | 109 +++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 src/renderer/src/components/Dialog/index.css create mode 100644 src/renderer/src/components/Dialog/index.tsx diff --git a/src/renderer/src/components/Dialog/index.css b/src/renderer/src/components/Dialog/index.css new file mode 100644 index 0000000..7cae81b --- /dev/null +++ b/src/renderer/src/components/Dialog/index.css @@ -0,0 +1,157 @@ +.modalClassName { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-width: 20rem; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 0 0.5rem 0.25rem hsl(0 0% 0% / 10%); +} +.modalClassName::backdrop { + background: hsl(0 0% 0% / 50%); +} + +.modal-close-btn { + font-size: .75em; + position: absolute; + top: .25em; + right: .25em; +} + + + +.Dialog { + padding: 0; + text-align: start; + + --dialog-margin-horizontal: 32px; + --dialog-margin-vertical: 24px; + --dialog-gap: 24px; + + --modal-border: black; + --modal-background: rgb(25, 25, 25); + --text-default: white; + --modal-backdrop: hsl(0 0% 0% / 10%); + --space-xs: .8rem; + --space-md: 1rem; + --space-lg: 1.2rem; + --text-xl: 1.6rem; + --accent: blue; + --text-hover: grey; +} + +.Dialog__element { + top: 0; + top: 50%; + left: 50%; + transform: translate(calc(-50% + 50px), -50%); + z-index: 8; + display: flex; + flex-direction: column; + padding: 0; + overflow: auto; + padding-top: var(--dialog-margin-vertical); + border: solid 1px var(--modal-border); + border-radius: 10px; + background: var(--modal-background); + color: var(--text-default); + opacity: 0; + /* transform: translateY(50px); */ + transition: + opacity 500ms, + transform 500ms; + max-width: min(700px, 85vw); + max-height: 95vh; + + /* remove padding top when there's a header element, it has its own padding */ + &:has(.Dialog__header) { + padding-top: 0px; + } + + & img { + max-width: 100%; + } +} +.Dialog__element::backdrop { + background: rgba(0, 0, 0, 0.4); +} + +.Dialog__element:popover-open, +.Dialog__element[open] { + opacity: 1; + /* transform: translateY(0); */ + box-shadow: 0px 0px 0px 100vmax var(--modal-backdrop); + + transform: translate(-50%, -50%); +} + +.Dialog__header { + display: flex; + z-index: 2; + padding-bottom: var(--dialog-gap); +} + +.Dialog__headerTitle { + flex: 100% 1 1; + padding: var(--dialog-margin-vertical) 16px 0 var(--dialog-margin-horizontal); + font-size: var(--text-xl); + margin: var(--space-lg) 0 0; + text-align: start; +} + +.Dialog__Close { + padding: 0 var(--dialog-margin-horizontal) 0 0; + z-index: 3; +} + +.Dialog__Close, +.Dialog__header { + position: sticky; + top: 0px; + background: var(--modal-background); +} + +.Dialog__CloseButton { + border: none; + margin: calc(-1 * var(--space-xs)); + padding: var(--space-xs); + border-radius: var(--space-xs); + background: none; + color: var(--text-default); + cursor: pointer; + transition: 250ms color; + position: absolute; + right: var(--space-lg); + top: var(--space-md); +} + +.Dialog__CloseButton:focus-visible { + outline: none; + box-shadow: var(--accent) 0 0 0 2px inset; +} + +.Dialog__CloseButton:hover { + color: var(--text-hover); +} + +.Dialog__CloseButton:active { + color: var(--accent); +} + +.Dialog__CloseIcon { + font-size: var(--text-lg); +} + +.Dialog__content { + padding: 0 var(--dialog-margin-horizontal) var(--dialog-gap); +} + +.Dialog__footer { + display: flex; + gap: 16px; + justify-content: end; + padding: 0 var(--dialog-margin-horizontal) var(--dialog-margin-vertical) + var(--dialog-margin-horizontal); +} \ No newline at end of file diff --git a/src/renderer/src/components/Dialog/index.tsx b/src/renderer/src/components/Dialog/index.tsx new file mode 100644 index 0000000..faf2677 --- /dev/null +++ b/src/renderer/src/components/Dialog/index.tsx @@ -0,0 +1,109 @@ +import { ReactNode, SyntheticEvent, KeyboardEvent, useCallback, useContext, useEffect, useRef, useState } from "react"; +import './index.css' + +interface DialogProps { + className?: string + children: ReactNode + showCloseButton: boolean + onClose: () => void +} + +export const Dialog = ({ + children, + className, + showCloseButton = false, + onClose +}: DialogProps) => { + const dialogRef = useRef(null) + const onCloseRef = useRef(onClose) + onCloseRef.current = onClose + const [focusOnClose, setFocusOnClose] = useState(null) + // const { disableDialogBackdropClose } = useContext(ContextProvider) + + useEffect(() => { + setFocusOnClose(document.querySelector('*:focus')) + }, []) + + const close = () => { + onCloseRef.current() + if (focusOnClose) { + setTimeout(() => focusOnClose.focus(), 200) + } + } + + useEffect(() => { + const dialog = dialogRef.current + console.log(dialog) + if (dialog) { + const cancel = () => { + close() + } + dialog.addEventListener('cancel', cancel) + + // if (disableDialogBackdropClose) { + // dialog['showPopover']() + + // return () => { + // dialog.removeEventListener('cancel', cancel) + // dialog['hidePopover']() + // } + // } else { + dialog.showModal() + console.log('dialog open') + + return () => { + dialog.removeEventListener('cancel', cancel) + dialog.close() + } + // } + } + return + }, [dialogRef.current/*, disableDialogBackdropClose*/]) + + const onDialogClick = useCallback( + (e: SyntheticEvent) => { + if (e.target === dialogRef.current) { + const ev = e.nativeEvent as MouseEvent + const tg = e.target as HTMLElement + if ( + ev.offsetX < 0 || + ev.offsetX > tg.offsetWidth || + ev.offsetY < 0 || + ev.offsetY > tg.offsetHeight + ) { + close() + } + } + }, + [onClose] + ) + + const closeIfEsc = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + close() + } + } + + return ( +
+ + {showCloseButton && ( +
+ +
+ )} + {children} +
+
+ ) +} \ No newline at end of file