İçeriğe geç
BigBang

Sık açılanlar

    ↑↓Gezin↵AçEscKapat
    EN

    Kalıplar

    Geri alınamayan işlemleri onaylama

    Hangi işlemin onay istediğini, onayın ne söylediğini ve odağın nereye gittiğini belirler.

    Silme ve benzeri işlemler geri alınamaz. Bu kalıp, etkiye göre üç düzey tanımlar; böylece sık yapılan işlemler yavaşlamaz, tehlikeli işlemler yanlışlıkla yapılmaz.

    • Kayıt, rapor veya proje silme gibi geri alınamayan her işlemde.
    • Toplu silme ve yetki kaldırma gibi çok sayıda öğeyi etkileyen işlemlerde.
    • Geri alınabilen işlemlerde onay istemeyin; bunun yerine geri alma sunun.
    Etki Yaklaşım
    Geri alınabilir Onay yok. İşlemi yapın ve Toast içinde “Geri al” seçeneği sunun.
    Geri alınamaz, sınırlı etki Nesneyi ve sonucu adıyla söyleyen bir AlertDialog.
    Geri alınamaz, geniş etki Yazarak onay: AlertDialog içinde kullanıcı nesnenin adını yazar.

    “Projeyi sil” düğmesine basın. Odak “Vazgeç” düğmesine gelir. Yanlış ad yazıp onaylayın: pencere açık kalır ve alanda hata görünür. Escape ile kapatın, odak düğmeye döner.

    Canlı örnek, StorybookStorybook'ta aç
    import { AlertDialog, Button, Text, TextInput, useToast } from '@bigbang/ui';
    import { useEffect, useRef, useState } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    trigger: 'Projeyi sil',
    title: 'Projeyi kalıcı olarak silin',
    titleSimple: 'Raporu silin',
    consequence: 'Proje A ve içindeki 12 kayıt kalıcı olarak silinir.',
    consequenceSimple: 'Rapor 1 kalıcı olarak silinir. Bu işlem geri alınamaz.',
    projectName: 'Proje A',
    field: 'Onaylamak için proje adını yazın',
    mismatch: 'Proje adını Proje A olarak yazın.',
    confirm: 'Projeyi kalıcı olarak sil',
    triggerSimple: 'Raporu sil',
    confirmSimple: 'Raporu kalıcı olarak sil',
    triggerUndo: 'Notu sil',
    done: 'Proje A silindi',
    doneSimple: 'Rapor 1 silindi',
    doneUndo: 'Not 1 silindi',
    undo: 'Geri al',
    restored: 'Not 1 geri alındı',
    deletedState: 'Silindi.',
    },
    en: {
    trigger: 'Delete project',
    title: 'Delete the project permanently',
    titleSimple: 'Delete the report',
    consequence: 'Project A and its 12 records are deleted permanently.',
    consequenceSimple: 'Report 1 is deleted permanently. This cannot be undone.',
    projectName: 'Project A',
    field: 'Type the project name to confirm',
    mismatch: 'Type the project name as Project A.',
    confirm: 'Delete project permanently',
    triggerSimple: 'Delete report',
    confirmSimple: 'Delete report permanently',
    triggerUndo: 'Delete note',
    done: 'Project A deleted',
    doneSimple: 'Report 1 deleted',
    doneUndo: 'Note 1 deleted',
    undo: 'Undo',
    restored: 'Note 1 restored',
    deletedState: 'Deleted.',
    },
    };
    /** Simulated request time, so the busy state of the confirm button is visible. */
    export const DELETE_DELAY = 400;
    type Level = 'type' | 'simple' | 'undo';
    function ConfirmDeleteDemo({ lang, level }: { lang: Lang; level: Level }) {
    const t = copy[lang];
    const toast = useToast();
    const [deleted, setDeleted] = useState(false);
    const [value, setValue] = useState('');
    const [error, setError] = useState('');
    const triggerRef = useRef<HTMLButtonElement>(null);
    const inputRef = useRef<HTMLInputElement>(null);
    const deletedRef = useRef<HTMLDivElement>(null);
    const restoredRef = useRef(false);
    // Focus the "Deleted." line after a delete, or the trigger after an undo; the old trigger is gone or new.
    useEffect(() => {
    if (deleted) deletedRef.current?.focus();
    else if (restoredRef.current) {
    restoredRef.current = false;
    triggerRef.current?.focus();
    }
    }, [deleted]);
    const doneText = level === 'type' ? t.done : level === 'simple' ? t.doneSimple : t.doneUndo;
    const remove = () => {
    setDeleted(true);
    toast.show({
    title: doneText,
    tone: 'success',
    ...(level === 'undo'
    ? {
    action: {
    label: t.undo,
    onClick: () => {
    restoredRef.current = true;
    setDeleted(false);
    toast.show({ title: t.restored, tone: 'info' });
    },
    },
    }
    : {}),
    });
    };
    const confirm = async () => {
    if (level === 'type' && value.trim() !== t.projectName) {
    setError(t.mismatch);
    inputRef.current?.focus();
    // A rejected promise keeps the dialog open; the error is already in the field.
    throw new Error('name mismatch');
    }
    await new Promise((resolve) => setTimeout(resolve, DELETE_DELAY));
    remove();
    };
    if (deleted) {
    return (
    <div
    ref={deletedRef}
    tabIndex={-1}
    className="outline-none focus-visible:focus-outline"
    >
    <Text tone="subtle">{t.deletedState}</Text>
    </div>
    );
    }
    const trigger = (
    <Button ref={triggerRef} variant="tertiary" tone="danger" {...(level === 'undo' ? { onClick: remove } : {})}>
    {level === 'type' ? t.trigger : level === 'simple' ? t.triggerSimple : t.triggerUndo}
    </Button>
    );
    // Reversible: no confirmation, the toast offers a real undo.
    if (level === 'undo') return <div>{trigger}</div>;
    return (
    <div>
    <AlertDialog
    trigger={trigger}
    tone="danger"
    title={level === 'type' ? t.title : t.titleSimple}
    description={level === 'type' ? t.consequence : t.consequenceSimple}
    confirmLabel={level === 'type' ? t.confirm : t.confirmSimple}
    onConfirm={confirm}
    onOpenChange={(open) => {
    if (!open) {
    setValue('');
    setError('');
    }
    }}
    >
    {level === 'type' ? (
    <TextInput
    ref={inputRef}
    label={t.field}
    autoComplete="off"
    value={value}
    onValueChange={(next) => {
    setValue(next);
    setError('');
    }}
    {...(error ? { error } : {})}
    />
    ) : null}
    </AlertDialog>
    </div>
    );
    }
    /**
    * Confirmation for an irreversible action. `type` asks the user to type the object name inside an AlertDialog (wide impact);
    * `simple` only names the object and the consequence (limited impact); `undo` skips the dialog and offers "Undo" in the toast.
    * Focus starts on Cancel, never on the destructive button, and returns to the trigger when the dialog closes.
    */
    export function ConfirmDelete({ lang = 'tr', level = 'type' }: { lang?: Lang; level?: Level }) {
    return (
    <LangProvider lang={lang}>
    <div className="w-full max-w-md">
    <ConfirmDeleteDemo lang={lang} level={level} />
    </div>
    </LangProvider>
    );
    }

    Sınırlı etkide yazma adımı yoktur; pencere nesneyi ve sonucu söyler.

    Canlı örnek, StorybookStorybook'ta aç
    import { AlertDialog, Button, Text, TextInput, useToast } from '@bigbang/ui';
    import { useEffect, useRef, useState } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    trigger: 'Projeyi sil',
    title: 'Projeyi kalıcı olarak silin',
    titleSimple: 'Raporu silin',
    consequence: 'Proje A ve içindeki 12 kayıt kalıcı olarak silinir.',
    consequenceSimple: 'Rapor 1 kalıcı olarak silinir. Bu işlem geri alınamaz.',
    projectName: 'Proje A',
    field: 'Onaylamak için proje adını yazın',
    mismatch: 'Proje adını Proje A olarak yazın.',
    confirm: 'Projeyi kalıcı olarak sil',
    triggerSimple: 'Raporu sil',
    confirmSimple: 'Raporu kalıcı olarak sil',
    triggerUndo: 'Notu sil',
    done: 'Proje A silindi',
    doneSimple: 'Rapor 1 silindi',
    doneUndo: 'Not 1 silindi',
    undo: 'Geri al',
    restored: 'Not 1 geri alındı',
    deletedState: 'Silindi.',
    },
    en: {
    trigger: 'Delete project',
    title: 'Delete the project permanently',
    titleSimple: 'Delete the report',
    consequence: 'Project A and its 12 records are deleted permanently.',
    consequenceSimple: 'Report 1 is deleted permanently. This cannot be undone.',
    projectName: 'Project A',
    field: 'Type the project name to confirm',
    mismatch: 'Type the project name as Project A.',
    confirm: 'Delete project permanently',
    triggerSimple: 'Delete report',
    confirmSimple: 'Delete report permanently',
    triggerUndo: 'Delete note',
    done: 'Project A deleted',
    doneSimple: 'Report 1 deleted',
    doneUndo: 'Note 1 deleted',
    undo: 'Undo',
    restored: 'Note 1 restored',
    deletedState: 'Deleted.',
    },
    };
    /** Simulated request time, so the busy state of the confirm button is visible. */
    export const DELETE_DELAY = 400;
    type Level = 'type' | 'simple' | 'undo';
    function ConfirmDeleteDemo({ lang, level }: { lang: Lang; level: Level }) {
    const t = copy[lang];
    const toast = useToast();
    const [deleted, setDeleted] = useState(false);
    const [value, setValue] = useState('');
    const [error, setError] = useState('');
    const triggerRef = useRef<HTMLButtonElement>(null);
    const inputRef = useRef<HTMLInputElement>(null);
    const deletedRef = useRef<HTMLDivElement>(null);
    const restoredRef = useRef(false);
    // Focus the "Deleted." line after a delete, or the trigger after an undo; the old trigger is gone or new.
    useEffect(() => {
    if (deleted) deletedRef.current?.focus();
    else if (restoredRef.current) {
    restoredRef.current = false;
    triggerRef.current?.focus();
    }
    }, [deleted]);
    const doneText = level === 'type' ? t.done : level === 'simple' ? t.doneSimple : t.doneUndo;
    const remove = () => {
    setDeleted(true);
    toast.show({
    title: doneText,
    tone: 'success',
    ...(level === 'undo'
    ? {
    action: {
    label: t.undo,
    onClick: () => {
    restoredRef.current = true;
    setDeleted(false);
    toast.show({ title: t.restored, tone: 'info' });
    },
    },
    }
    : {}),
    });
    };
    const confirm = async () => {
    if (level === 'type' && value.trim() !== t.projectName) {
    setError(t.mismatch);
    inputRef.current?.focus();
    // A rejected promise keeps the dialog open; the error is already in the field.
    throw new Error('name mismatch');
    }
    await new Promise((resolve) => setTimeout(resolve, DELETE_DELAY));
    remove();
    };
    if (deleted) {
    return (
    <div
    ref={deletedRef}
    tabIndex={-1}
    className="outline-none focus-visible:focus-outline"
    >
    <Text tone="subtle">{t.deletedState}</Text>
    </div>
    );
    }
    const trigger = (
    <Button ref={triggerRef} variant="tertiary" tone="danger" {...(level === 'undo' ? { onClick: remove } : {})}>
    {level === 'type' ? t.trigger : level === 'simple' ? t.triggerSimple : t.triggerUndo}
    </Button>
    );
    // Reversible: no confirmation, the toast offers a real undo.
    if (level === 'undo') return <div>{trigger}</div>;
    return (
    <div>
    <AlertDialog
    trigger={trigger}
    tone="danger"
    title={level === 'type' ? t.title : t.titleSimple}
    description={level === 'type' ? t.consequence : t.consequenceSimple}
    confirmLabel={level === 'type' ? t.confirm : t.confirmSimple}
    onConfirm={confirm}
    onOpenChange={(open) => {
    if (!open) {
    setValue('');
    setError('');
    }
    }}
    >
    {level === 'type' ? (
    <TextInput
    ref={inputRef}
    label={t.field}
    autoComplete="off"
    value={value}
    onValueChange={(next) => {
    setValue(next);
    setError('');
    }}
    {...(error ? { error } : {})}
    />
    ) : null}
    </AlertDialog>
    </div>
    );
    }
    /**
    * Confirmation for an irreversible action. `type` asks the user to type the object name inside an AlertDialog (wide impact);
    * `simple` only names the object and the consequence (limited impact); `undo` skips the dialog and offers "Undo" in the toast.
    * Focus starts on Cancel, never on the destructive button, and returns to the trigger when the dialog closes.
    */
    export function ConfirmDelete({ lang = 'tr', level = 'type' }: { lang?: Lang; level?: Level }) {
    return (
    <LangProvider lang={lang}>
    <div className="w-full max-w-md">
    <ConfirmDeleteDemo lang={lang} level={level} />
    </div>
    </LangProvider>
    );
    }

    Geri alınabilir işlemde pencere yoktur. “Notu sil” işlemi hemen yapılır; Toast “Geri al” sunar ve geri alma gerçekten çalışır.

    Canlı örnek, StorybookStorybook'ta aç
    import { AlertDialog, Button, Text, TextInput, useToast } from '@bigbang/ui';
    import { useEffect, useRef, useState } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    trigger: 'Projeyi sil',
    title: 'Projeyi kalıcı olarak silin',
    titleSimple: 'Raporu silin',
    consequence: 'Proje A ve içindeki 12 kayıt kalıcı olarak silinir.',
    consequenceSimple: 'Rapor 1 kalıcı olarak silinir. Bu işlem geri alınamaz.',
    projectName: 'Proje A',
    field: 'Onaylamak için proje adını yazın',
    mismatch: 'Proje adını Proje A olarak yazın.',
    confirm: 'Projeyi kalıcı olarak sil',
    triggerSimple: 'Raporu sil',
    confirmSimple: 'Raporu kalıcı olarak sil',
    triggerUndo: 'Notu sil',
    done: 'Proje A silindi',
    doneSimple: 'Rapor 1 silindi',
    doneUndo: 'Not 1 silindi',
    undo: 'Geri al',
    restored: 'Not 1 geri alındı',
    deletedState: 'Silindi.',
    },
    en: {
    trigger: 'Delete project',
    title: 'Delete the project permanently',
    titleSimple: 'Delete the report',
    consequence: 'Project A and its 12 records are deleted permanently.',
    consequenceSimple: 'Report 1 is deleted permanently. This cannot be undone.',
    projectName: 'Project A',
    field: 'Type the project name to confirm',
    mismatch: 'Type the project name as Project A.',
    confirm: 'Delete project permanently',
    triggerSimple: 'Delete report',
    confirmSimple: 'Delete report permanently',
    triggerUndo: 'Delete note',
    done: 'Project A deleted',
    doneSimple: 'Report 1 deleted',
    doneUndo: 'Note 1 deleted',
    undo: 'Undo',
    restored: 'Note 1 restored',
    deletedState: 'Deleted.',
    },
    };
    /** Simulated request time, so the busy state of the confirm button is visible. */
    export const DELETE_DELAY = 400;
    type Level = 'type' | 'simple' | 'undo';
    function ConfirmDeleteDemo({ lang, level }: { lang: Lang; level: Level }) {
    const t = copy[lang];
    const toast = useToast();
    const [deleted, setDeleted] = useState(false);
    const [value, setValue] = useState('');
    const [error, setError] = useState('');
    const triggerRef = useRef<HTMLButtonElement>(null);
    const inputRef = useRef<HTMLInputElement>(null);
    const deletedRef = useRef<HTMLDivElement>(null);
    const restoredRef = useRef(false);
    // Focus the "Deleted." line after a delete, or the trigger after an undo; the old trigger is gone or new.
    useEffect(() => {
    if (deleted) deletedRef.current?.focus();
    else if (restoredRef.current) {
    restoredRef.current = false;
    triggerRef.current?.focus();
    }
    }, [deleted]);
    const doneText = level === 'type' ? t.done : level === 'simple' ? t.doneSimple : t.doneUndo;
    const remove = () => {
    setDeleted(true);
    toast.show({
    title: doneText,
    tone: 'success',
    ...(level === 'undo'
    ? {
    action: {
    label: t.undo,
    onClick: () => {
    restoredRef.current = true;
    setDeleted(false);
    toast.show({ title: t.restored, tone: 'info' });
    },
    },
    }
    : {}),
    });
    };
    const confirm = async () => {
    if (level === 'type' && value.trim() !== t.projectName) {
    setError(t.mismatch);
    inputRef.current?.focus();
    // A rejected promise keeps the dialog open; the error is already in the field.
    throw new Error('name mismatch');
    }
    await new Promise((resolve) => setTimeout(resolve, DELETE_DELAY));
    remove();
    };
    if (deleted) {
    return (
    <div
    ref={deletedRef}
    tabIndex={-1}
    className="outline-none focus-visible:focus-outline"
    >
    <Text tone="subtle">{t.deletedState}</Text>
    </div>
    );
    }
    const trigger = (
    <Button ref={triggerRef} variant="tertiary" tone="danger" {...(level === 'undo' ? { onClick: remove } : {})}>
    {level === 'type' ? t.trigger : level === 'simple' ? t.triggerSimple : t.triggerUndo}
    </Button>
    );
    // Reversible: no confirmation, the toast offers a real undo.
    if (level === 'undo') return <div>{trigger}</div>;
    return (
    <div>
    <AlertDialog
    trigger={trigger}
    tone="danger"
    title={level === 'type' ? t.title : t.titleSimple}
    description={level === 'type' ? t.consequence : t.consequenceSimple}
    confirmLabel={level === 'type' ? t.confirm : t.confirmSimple}
    onConfirm={confirm}
    onOpenChange={(open) => {
    if (!open) {
    setValue('');
    setError('');
    }
    }}
    >
    {level === 'type' ? (
    <TextInput
    ref={inputRef}
    label={t.field}
    autoComplete="off"
    value={value}
    onValueChange={(next) => {
    setValue(next);
    setError('');
    }}
    {...(error ? { error } : {})}
    />
    ) : null}
    </AlertDialog>
    </div>
    );
    }
    /**
    * Confirmation for an irreversible action. `type` asks the user to type the object name inside an AlertDialog (wide impact);
    * `simple` only names the object and the consequence (limited impact); `undo` skips the dialog and offers "Undo" in the toast.
    * Focus starts on Cancel, never on the destructive button, and returns to the trigger when the dialog closes.
    */
    export function ConfirmDelete({ lang = 'tr', level = 'type' }: { lang?: Lang; level?: Level }) {
    return (
    <LangProvider lang={lang}>
    <div className="w-full max-w-md">
    <ConfirmDeleteDemo lang={lang} level={level} />
    </div>
    </LangProvider>
    );
    }
    Kural Değer
    Düğme etiketi Fiili ve nesneyi tekrar eder: “Projeyi kalıcı olarak sil”. “Evet” ve “Tamam” kullanılmaz.
    Varsayılan odak Pencere açılınca “Vazgeç” düğmesine gider (AlertDialog); yıkıcı düğmeye hiçbir zaman gitmez.
    Vazgeç Her zaman vardır. Escape de aynı işi yapar ve odağı tetikleyiciye döndürür. Pencere dışına basmak kapatmaz.
    Onay düğmesi Etkin kalır (tone="danger"). Ad uyuşmazsa onConfirm reddedilir: pencere açık kalır, alan altında hata görünür. İşlem sürerken düğme meşgul olur.
    Toplu silme Sayıyı içerir: “12 kaydı kalıcı olarak sil”.
    Geri alınabilir işlem Onay yok; geri alma sunulur.
    Sonuç Toast ile bildirilir: “Proje A silindi”. “Geri al” yalnızca gerçekten geri alınabiliyorsa eklenir.
    YapınDüğme fiili ve nesneyi tekrar eder.
    YapmayınGenel etiket, neyin silineceğini söylemez.
    1. Kullanıcı “Projeyi sil” düğmesine basar. AlertDialog açılır; odak “Vazgeç” düğmesine gider.
    2. Yazarak onayda kullanıcı nesnenin adını yazar ve onay düğmesine basar. Ad uyuşmuyorsa onConfirm reddedilir; pencere açık kalır ve alan hatası görünür.
    3. Ad uyuşursa düğme meşgul olur. İşlem bitince pencere kapanır ve Toast “Proje A silindi” der.
    4. Vazgeç veya Escape pencereyi kapatır; yazılan metin silinir ve odak “Projeyi sil” düğmesine döner.
    5. Silinen nesnenin tetikleyicisi kalmaz; odak “Silindi.” satırına gider. Geri alınabilir işlemde “Geri al” odağı tetikleyiciye döndürür.
    • Pencere role="alertdialog" taşır; başlığıyla adlandırılır ve açıklaması açılışta okunur. Odak pencerede kalır, arkadaki sayfa devre dışıdır.
    • Odak “Vazgeç” düğmesinde başlar ve pencere kapanınca tetikleyiciye döner; klavye kullanıcısı yerini kaybetmez.
    • Hata, aria-invalid ve aria-describedby ile alana bağlıdır (TextInput bunu kendisi yapar).
    • Sonuç Toast ile duyurulur; ekran okuyucu işlemi bölmeden okur.
    • Yazarak onay, panoya yapıştırmayı engellemez; ad görünür ve okunabilir kalır (WCAG 3.3.4).
    • Başlık eylemi söyler: “Projeyi kalıcı olarak silin”.
    • Sonuç metni nesneyi ve etkiyi adlandırır: “Proje A ve içindeki 12 kayıt kalıcı olarak silinir.”
    • Alan etiketi ne yazılacağını söyler: “Onaylamak için proje adını yazın”.
    • Düğmeler fiille başlar, nokta almaz. İptal düğmesi “Vazgeç” olur.
    Öğe Hareket Preset
    AlertDialog Bileşenin kendi açılış ve kapanış hareketi yerleşik
    Alan hata metni 150 ms solma, 4 px aşağı kayma motion.fieldMessage
    Toast Bileşenin kendi giriş ve çıkış hareketi yerleşik
    Düğmeler Basınca 0,97 ölçek motion.press

    Hareketi azaltma tercihinde kaymalar kalkar, yalnızca solma kalır.

    AlertDialog, Toast, Button, TextInput, Text. İlgili kalıp: Formlar ve doğrulama.