99 lines
3.4 KiB
JavaScript
99 lines
3.4 KiB
JavaScript
import React, { useEffect, useRef } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
const FOCUSABLE = 'button:not(:disabled), [href], input:not(:disabled), [tabindex]:not([tabindex="-1"])';
|
|
|
|
export function ConfirmationPopup({
|
|
open,
|
|
id,
|
|
kicker,
|
|
title,
|
|
description,
|
|
cancelLabel,
|
|
confirmLabel,
|
|
busy = false,
|
|
onCancel,
|
|
onConfirm,
|
|
}) {
|
|
const overlayRef = useRef(null);
|
|
const dialogRef = useRef(null);
|
|
const cancelRef = useRef(null);
|
|
const busyRef = useRef(busy);
|
|
const onCancelRef = useRef(onCancel);
|
|
busyRef.current = busy;
|
|
onCancelRef.current = onCancel;
|
|
|
|
useEffect(() => {
|
|
if (!open) return undefined;
|
|
const previousFocus = document.activeElement;
|
|
const background = [...(overlayRef.current?.parentElement?.children || [])]
|
|
.filter((element) => !element.classList.contains('client-confirmation-popup'))
|
|
.map((element) => [element, element.inert]);
|
|
background.forEach(([element]) => { element.inert = true; });
|
|
const previousOverflow = document.body.style.overflow;
|
|
document.body.style.overflow = 'hidden';
|
|
const frame = requestAnimationFrame(() => cancelRef.current?.focus());
|
|
const onKeyDown = (event) => {
|
|
if (event.key === 'Escape' && !busyRef.current) {
|
|
event.preventDefault();
|
|
onCancelRef.current();
|
|
return;
|
|
}
|
|
if (event.key !== 'Tab') return;
|
|
const controls = [...(dialogRef.current?.querySelectorAll(FOCUSABLE) || [])];
|
|
if (!controls.length) return;
|
|
const first = controls[0];
|
|
const last = controls.at(-1);
|
|
if (!dialogRef.current?.contains(document.activeElement)) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
} else if (event.shiftKey && document.activeElement === first) {
|
|
event.preventDefault();
|
|
last.focus();
|
|
} else if (!event.shiftKey && document.activeElement === last) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
}
|
|
};
|
|
document.addEventListener('keydown', onKeyDown);
|
|
return () => {
|
|
cancelAnimationFrame(frame);
|
|
document.removeEventListener('keydown', onKeyDown);
|
|
background.forEach(([element, inert]) => { element.inert = inert; });
|
|
document.body.style.overflow = previousOverflow;
|
|
requestAnimationFrame(() => previousFocus?.focus?.());
|
|
};
|
|
}, [open]);
|
|
|
|
return createPortal(
|
|
<div
|
|
ref={overlayRef}
|
|
className={`client-confirmation-popup${open ? ' is-open' : ''}`}
|
|
aria-hidden={!open}
|
|
inert={!open ? true : undefined}
|
|
onPointerDown={(event) => {
|
|
if (event.target === event.currentTarget && !busy) onCancel();
|
|
}}
|
|
>
|
|
<section
|
|
ref={dialogRef}
|
|
className="client-confirmation-dialog"
|
|
role="alertdialog"
|
|
aria-modal="true"
|
|
aria-labelledby={`${id}-title`}
|
|
aria-describedby={`${id}-description`}
|
|
aria-busy={busy}
|
|
>
|
|
{kicker && <span className="client-confirmation-kicker">{kicker}</span>}
|
|
<h2 id={`${id}-title`}>{title}</h2>
|
|
<p id={`${id}-description`}>{description}</p>
|
|
<div className="client-confirmation-actions">
|
|
<button ref={cancelRef} type="button" disabled={busy} onClick={onCancel}>{cancelLabel}</button>
|
|
<button className="is-danger" type="button" disabled={busy} onClick={onConfirm}>{confirmLabel}</button>
|
|
</div>
|
|
</section>
|
|
</div>,
|
|
document.querySelector('.app.client-app') || document.body,
|
|
);
|
|
}
|