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.
Ne zaman kullanılır
Bölüm başlığı “Ne zaman kullanılır”- 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. |
Canlı örnek
Bölüm başlığı “Canlı örnek”“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.
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.
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.
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> );}Kurallar
Bölüm başlığı “Kurallar”| 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. |
Davranış
Bölüm başlığı “Davranış”- Kullanıcı “Projeyi sil” düğmesine basar. AlertDialog açılır; odak “Vazgeç” düğmesine gider.
- Yazarak onayda kullanıcı nesnenin adını yazar ve onay düğmesine basar. Ad uyuşmuyorsa
onConfirmreddedilir; pencere açık kalır ve alan hatası görünür. - Ad uyuşursa düğme meşgul olur. İşlem bitince pencere kapanır ve Toast “Proje A silindi” der.
- Vazgeç veya Escape pencereyi kapatır; yazılan metin silinir ve odak “Projeyi sil” düğmesine döner.
- Silinen nesnenin tetikleyicisi kalmaz; odak “Silindi.” satırına gider. Geri alınabilir işlemde “Geri al” odağı tetikleyiciye döndürür.
Erişilebilirlik
Bölüm başlığı “Erişilebilirlik”- 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-invalidvearia-describedbyile 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).
İçerik
Bölüm başlığı “İçerik”- 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.
Hareket
Bölüm başlığı “Hareket”| Öğ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.
Kullanılan bileşenler
Bölüm başlığı “Kullanılan bileşenler”AlertDialog, Toast, Button, TextInput, Text. İlgili kalıp: Formlar ve doğrulama.