Files
medassist-ng/frontend/src/components/ShareDialog.tsx
T
Daniel Volz ba36f67371 fix: smooth mobile edit transition and align modal validation behavior (#286)
* fix: reliable Escape key close for all modals via useEscapeKey hook

- Add useEscapeKey hook (document-level keydown listener)
- Retrofit all 12 modal/overlay components to use it
- Remove redundant overlay onKeyDown Escape handlers
- Simplify modal-content onKeyDown to plain stopPropagation
- Replace MedDetailModal's capture-phase useEffect with 3 useEscapeKey calls
- Replace SharedSchedule's inline useEffect with useEscapeKey
- Add mandatory modal rules to UI Consistency skill
- All 777 frontend + 569 backend tests pass

* fix: smooth mobile edit transition and align modal validation behavior

* fix: keep overlay keydown non-closing for Enter key

* fix: show mobile name error when validation already exists

* fix: restore app-level escape priority handling

* fix: prioritize schedule lightbox on Escape
2026-02-23 06:42:06 +01:00

174 lines
4.8 KiB
TypeScript

/**
* ShareDialog - Modal for generating share links for medication schedules
* Allows sharing schedule view for a specific person
*/
import { Check, Copy, Link2, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useEscapeKey } from "../hooks/useEscapeKey";
export interface ShareDialogProps {
show: boolean;
sharePeople: string[];
shareSelectedPerson: string;
onShareSelectedPersonChange: (person: string) => void;
shareSelectedDays: number;
onShareSelectedDaysChange: (days: number) => void;
shareGenerating: boolean;
shareLink: string | null;
onShareLinkChange: (link: string | null) => void;
shareCopied: boolean;
onShareCopiedChange: (copied: boolean) => void;
onClose: () => void;
onGenerateShareLink: () => Promise<void>;
onCopyShareLink: () => void;
}
export function ShareDialog({
show,
sharePeople,
shareSelectedPerson,
onShareSelectedPersonChange,
shareSelectedDays,
onShareSelectedDaysChange,
shareGenerating,
shareLink,
onShareLinkChange,
shareCopied,
onShareCopiedChange,
onClose,
onGenerateShareLink,
onCopyShareLink,
}: ShareDialogProps) {
const { t } = useTranslation();
const closeLabel = t("common.close");
const copyLabel = shareCopied ? t("share.copied") : t("share.copyLink");
useEscapeKey(show, onClose);
if (!show) return null;
return (
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(e) => {
if (e.key !== "Escape") e.stopPropagation();
}}
>
<div
className="modal-content share-dialog-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key !== "Escape") e.stopPropagation();
}}
>
<button
type="button"
className="modal-close tooltip-trigger"
onClick={onClose}
aria-label={closeLabel}
data-tooltip={closeLabel}
>
<X size={18} aria-hidden="true" />
</button>
<div className="share-dialog-header">
<h2>
<Link2 size={18} aria-hidden="true" /> {t("share.title")}
</h2>
<p className="share-dialog-description">{t("share.description")}</p>
</div>
{(() => {
if (sharePeople.length === 0) {
return (
<div className="share-dialog-empty">
<p>{t("share.noPeople")}</p>
</div>
);
}
if (shareLink) {
return (
<div className="share-dialog-result">
<p className="share-success">{t("share.linkGenerated")}</p>
<div className="share-link-box">
<input
type="text"
value={shareLink}
readOnly
className="share-link-input"
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
<button
type="button"
className="btn-copy icon-only tooltip-trigger"
onClick={onCopyShareLink}
aria-label={copyLabel}
data-tooltip={copyLabel}
>
{shareCopied ? <Check size={18} aria-hidden="true" /> : <Copy size={18} aria-hidden="true" />}
</button>
</div>
{shareCopied && <span className="share-copied-hint">{t("share.copied")}</span>}
<div className="share-dialog-footer">
<button
className="ghost"
onClick={() => {
onShareLinkChange(null);
onShareCopiedChange(false);
}}
>
{t("share.generateAnother")}
</button>
<button onClick={onClose}>{t("common.close")}</button>
</div>
</div>
);
}
return (
<div className="share-dialog-form">
<div className="form-group">
<label htmlFor="share-person-select">{t("share.selectPerson")}</label>
<select
id="share-person-select"
value={shareSelectedPerson}
onChange={(e) => onShareSelectedPersonChange(e.target.value)}
>
{sharePeople.map((person) => (
<option key={person} value={person}>
{person}
</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="share-period-select">{t("share.selectPeriod")}</label>
<select
id="share-period-select"
value={shareSelectedDays}
onChange={(e) => onShareSelectedDaysChange(Number(e.target.value))}
>
<option value={30}>{t("dashboard.schedules.1month")}</option>
<option value={90}>{t("dashboard.schedules.3months")}</option>
<option value={180}>{t("dashboard.schedules.6months")}</option>
</select>
</div>
<div className="share-dialog-footer">
<button className="ghost" onClick={onClose}>
{t("common.close")}
</button>
<button onClick={onGenerateShareLink} disabled={shareGenerating || !shareSelectedPerson}>
{shareGenerating ? t("share.generating") : t("share.generateLink")}
</button>
</div>
</div>
);
})()}
</div>
</div>
);
}