Informacja zwrotna
Wybór między komunikatami w treści, alertami, powiadomieniami i oknami dialogowymi — tak, by ludzie zauważali to, co ważne, a nie przerywało im to, co nieważne.
Każdy komunikat walczy o uwagę. Właściwy komponent zależy od tego, jak pilny jest komunikat i czy dotyczy czegoś na ekranie.
Wybór komponentu
Zasady
- Jak najbliżej przyczyny. Pokazuj informację zwrotną tam, gdzie ludzie patrzą: błędy obok pól, wyniki obok przycisku, który je wywołał.
- Przerywaj tylko wtedy, gdy potrzebna jest decyzja. Okna dialogowe wstrzymują wszystko — zostaw je na wybory, które nie mogą czekać. Cała reszta trafia do treści strony albo do powiadomienia.
- Ulotne znaczy drugorzędne. Powiadomienia znikają. Nigdy nie umieszczaj w nich informacji, których nie da się znaleźć gdzie indziej, ani jedynego sposobu na naprawienie błędu.
- Status to nie tylko kolor. Każdy komunikat o stanie ma ikonę i tekst.
Powiadomienia
- Jedna linijka, jeśli się da; najwyżej dwie. Zacznij od rezultatu: Wysłano fakturę, a nie Sukces!
- Proponuj jedną akcję — zwykle Cofnij lub Pokaż.
- Każde powiadomienie domyślnie znika samo po 5 sekundach, także błąd, a gdy jest pod kursorem lub ma fokus, odliczanie się wstrzymuje. Żeby zostało do zamknięcia, przekaż
timeout: 0; rób to dla stanu, który trwa, a nie po to, by błąd trudniej było przeoczyć. - Czytniki ekranu odczytują powiadomienia uprzejmie, po bieżącej wypowiedzi;
priority: "high"ją przerywa i pasuje do błędów. Powiadomienia nigdy nie przejmują fokusu.
"use client"
import * as React from "react"
import { ArchiveIcon, InboxIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemTitle,
} from "@/components/ui/item"
import { toast } from "@/components/ui/toast"
const initial = [
{ id: 1, title: "Faktura nr 2041 jest gotowa", from: "Rozliczenia" },
{ id: 2, title: "Ada skomentowała „Strona główna v3”", from: "Komentarze" },
{ id: 3, title: "Tygodniowy raport zużycia", from: "Raporty" },
]
export default function UndoToast() {
const [items, setItems] = React.useState(initial)
function archive(id: number) {
const index = items.findIndex((item) => item.id === id)
const removed = items[index]
setItems((current) => current.filter((item) => item.id !== id))
const toastId = toast.add({
title: "Zarchiwizowano rozmowę",
actionProps: {
children: "Cofnij",
onClick: () => {
// Closed with the click: a second one would restore the row twice.
toast.close(toastId)
setItems((current) => {
const next = [...current]
next.splice(index, 0, removed)
return next
})
},
},
})
}
return (
<ItemGroup className="w-full max-w-md gap-2">
{items.map((item) => (
<Item key={item.id} variant="outline" size="sm">
<ItemContent>
<ItemTitle>{item.title}</ItemTitle>
<ItemDescription>{item.from}</ItemDescription>
</ItemContent>
<ItemActions>
{/* A word, not a lone icon: archiving isn't universal enough
to go unlabeled. The accessible name adds which message. */}
<Button
variant="ghost"
size="sm"
aria-label={`Archiwizuj „${item.title}”`}
onClick={() => archive(item.id)}
>
<ArchiveIcon data-icon="inline-start" />
Archiwizuj
</Button>
</ItemActions>
</Item>
))}
{items.length === 0 && (
<div className="flex flex-col items-center gap-2 py-8 text-sm text-muted-foreground">
<InboxIcon className="size-5" aria-hidden="true" />
Skrzynka odbiorcza jest pusta.
<Button variant="link" size="sm" onClick={() => setItems(initial)}>
Przywróć przykłady
</Button>
</div>
)}
</ItemGroup>
)
}