Przejdź do treści

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

KomunikatKomponentPrzykład
Dotyczy konkretnego polaFieldErrorWpisz adres e-mail, np. ada@acme.co.
Dotyczy fragmentu ekranu, widoczny na stałeAlertTwój okres próbny kończy się za 3 dni.
Potwierdzenie akcjitoastZapisano ustawienia.
Wynik zadania w tletoast z akcjąEksport gotowy — Pobierz.
Wymaga decyzji od razuAlertDialogUsunąć „acme-web”?
Globalny, dotyczy całego systemuAlert w nagłówku stronyPlanowane prace serwisowe dziś o 22:00 UTC.

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.
Faktura nr 2041 jest gotowa
Ada skomentowała „Strona główna v3”
Tygodniowy raport zużycia
"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>
  )
}