Create Dialog system

This commit is contained in:
TheThomaas 2026-02-05 15:24:44 +01:00
parent fdc7880faa
commit b2f4329266
2 changed files with 266 additions and 0 deletions

View file

@ -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);
}

View file

@ -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<HTMLDialogElement | null>(null)
const onCloseRef = useRef(onClose)
onCloseRef.current = onClose
const [focusOnClose, setFocusOnClose] = useState<HTMLElement | null>(null)
// const { disableDialogBackdropClose } = useContext(ContextProvider)
useEffect(() => {
setFocusOnClose(document.querySelector<HTMLElement>('*: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<HTMLDialogElement>) => {
if (event.key === 'Escape') {
close()
}
}
return (
<div className="Dialog">
<dialog
className={`Dialog__element ${className}`}
ref={dialogRef}
onClick={onDialogClick}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore, this feature is new and not yet typed
popover="manual"
onKeyUp={closeIfEsc}
>
{showCloseButton && (
<div className="Dialog__Close">
<button className="Dialog__CloseButton" onClick={close}>
Close
</button>
</div>
)}
{children}
</dialog>
</div>
)
}