fix(mui): freeze loading state + button label during dialog close (Modal + ConfirmDialog)

On save/delete success the parent flips loading false a beat before the dialog finishes its fade-out, so the button flashed back to 'Uložit změny'/'Smazat'. Both dialogs now freeze the loading flag (and the label props) while open, so the closing dialog keeps showing 'Ukládám…'/'Pracuji…'. Same derive-state-from-props pattern as the title freeze.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 22:11:58 +02:00
parent 305c3fd97b
commit 2efd1c6df4
2 changed files with 31 additions and 20 deletions

View File

@@ -32,21 +32,27 @@ export default function ConfirmDialog({
loading = false, loading = false,
}: ConfirmDialogProps) { }: ConfirmDialogProps) {
useDialogScrollLock(isOpen); useDialogScrollLock(isOpen);
// Freeze the shown title/message while closing, so the fade-out transition // Freeze content + label + loading while open, so neither the message NOR the
// never flashes empty when the caller clears its source data (e.g. sets the // confirm button label flips during the close fade-out (e.g. on confirm,
// selected row to null) at the same instant it closes the dialog. This is // `loading` goes false a beat before the dialog finishes closing — without
// React's "adjust state from props during render" pattern: update only while // this the button would flash back to "Smazat"/confirmText). React
// open, keep the last values once closed. (Self-contained — callers can clear // "adjust state from props during render": update only while open. Also keeps
// their data on close however they like.) // content from flashing empty when the caller clears its row on close.
const [shown, setShown] = useState({ title, message }); const [shown, setShown] = useState({ title, message, confirmText, loading });
if (isOpen && (shown.title !== title || shown.message !== message)) { if (
setShown({ title, message }); isOpen &&
(shown.title !== title ||
shown.message !== message ||
shown.confirmText !== confirmText ||
shown.loading !== loading)
) {
setShown({ title, message, confirmText, loading });
} }
return ( return (
<Dialog <Dialog
open={isOpen} open={isOpen}
onClose={loading ? undefined : onClose} onClose={shown.loading ? undefined : onClose}
disableScrollLock disableScrollLock
slotProps={{ paper: { sx: { borderRadius: 3 } } }} slotProps={{ paper: { sx: { borderRadius: 3 } } }}
> >
@@ -55,16 +61,16 @@ export default function ConfirmDialog({
<DialogContentText>{shown.message}</DialogContentText> <DialogContentText>{shown.message}</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}> <DialogActions sx={{ px: 3, pb: 2 }}>
<MuiButton onClick={onClose} color="inherit" disabled={loading}> <MuiButton onClick={onClose} color="inherit" disabled={shown.loading}>
{cancelText} {cancelText}
</MuiButton> </MuiButton>
<MuiButton <MuiButton
onClick={onConfirm} onClick={onConfirm}
variant="contained" variant="contained"
color={confirmVariant === "danger" ? "error" : "primary"} color={confirmVariant === "danger" ? "error" : "primary"}
disabled={loading} disabled={shown.loading}
> >
{loading ? "Pracuji…" : confirmText} {shown.loading ? "Pracuji…" : shown.confirmText}
</MuiButton> </MuiButton>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -39,20 +39,25 @@ export default function Modal({
// Freeze the shown title/subtitle/submitText while closing, so the fade-out // Freeze the shown title/subtitle/submitText while closing, so the fade-out
// never flashes the "create" variant when the caller clears `editingUser` on // never flashes the "create" variant when the caller clears `editingUser` on
// close (React derive-state-from-props: update only while open). // close (React derive-state-from-props: update only while open).
const [shown, setShown] = useState({ title, subtitle, submitText }); // Freeze label-affecting props (incl. `loading`) while open, so neither the
// title/submitText NOR the loading state flips during the close fade-out
// (e.g. on save, `loading` goes false a beat before the dialog finishes
// closing — without this the button would flash back to "Uložit změny").
const [shown, setShown] = useState({ title, subtitle, submitText, loading });
if ( if (
isOpen && isOpen &&
(shown.title !== title || (shown.title !== title ||
shown.subtitle !== subtitle || shown.subtitle !== subtitle ||
shown.submitText !== submitText) shown.submitText !== submitText ||
shown.loading !== loading)
) { ) {
setShown({ title, subtitle, submitText }); setShown({ title, subtitle, submitText, loading });
} }
return ( return (
<Dialog <Dialog
open={isOpen} open={isOpen}
onClose={loading ? undefined : onClose} onClose={shown.loading ? undefined : onClose}
fullWidth fullWidth
maxWidth={maxWidth} maxWidth={maxWidth}
disableScrollLock disableScrollLock
@@ -68,15 +73,15 @@ export default function Modal({
</DialogTitle> </DialogTitle>
<DialogContent>{children}</DialogContent> <DialogContent>{children}</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}> <DialogActions sx={{ px: 3, pb: 2 }}>
<MuiButton onClick={onClose} color="inherit" disabled={loading}> <MuiButton onClick={onClose} color="inherit" disabled={shown.loading}>
{cancelText} {cancelText}
</MuiButton> </MuiButton>
<MuiButton <MuiButton
onClick={onSubmit} onClick={onSubmit}
variant="contained" variant="contained"
disabled={loading || submitDisabled} disabled={shown.loading || submitDisabled}
> >
{loading ? "Ukládám…" : shown.submitText} {shown.loading ? "Ukládám…" : shown.submitText}
</MuiButton> </MuiButton>
</DialogActions> </DialogActions>
</Dialog> </Dialog>