Flow

Components

List

Die List stellt mehrere ListItems übersichtlich in einer Liste dar. Sie bietet eine Sortierung, einen Filter und eine Suche.GitHub

Playground

Verwende <List />, um eine Liste darzustellen.

import { typedList } from "@mittwald/flow-react-components/List";
import {
  type Domain,
  domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import Avatar from "@mittwald/flow-react-components/Avatar";
import Heading from "@mittwald/flow-react-components/Heading";
import Text from "@mittwald/flow-react-components/Text";
import ContextMenu from "@mittwald/flow-react-components/ContextMenu";
import {
  IconDomain,
  IconDownload,
  IconSubdomain,
} from "@mittwald/flow-react-components/Icons";
import AlertBadge from "@mittwald/flow-react-components/AlertBadge";
import MenuItem from "@mittwald/flow-react-components/MenuItem";
import Button from "@mittwald/flow-react-components/Button";
import ActionGroup from "@mittwald/flow-react-components/ActionGroup";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List batchSize={5}>
      <DomainList.StaticData data={domains} />
      <ActionGroup>
        <Button
          color="secondary"
          variant="soft"
          slot="secondary"
        >
          <IconDownload />
        </Button>
        <Button color="accent">Anlegen</Button>
      </ActionGroup>
      <DomainList.Search />
      <DomainList.Filter
        property="type"
        mode="some"
        name="Type"
      />
      <DomainList.Sorting
        property="hostname"
        name="Domain A bis Z"
        direction="asc"
      />
      <DomainList.Sorting
        property="hostname"
        name="Domain Z bis A"
        direction="desc"
      />
      <DomainList.Sorting
        property="type"
        name="Type A bis Z"
        direction="asc"
      />
      <DomainList.Sorting
        property="type"
        name="Type Z bis A"
        direction="desc"
      />
      <DomainList.Table>
        <DomainList.TableHeader>
          <DomainList.TableColumn>
            Name
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Type
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            TLD
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Hostname
          </DomainList.TableColumn>
        </DomainList.TableHeader>

        <DomainList.TableBody>
          <DomainList.TableRow>
            <DomainList.TableCell>
              {(domain) => domain.domain}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.type}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.tld}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.hostname}
            </DomainList.TableCell>
          </DomainList.TableRow>
        </DomainList.TableBody>
      </DomainList.Table>
      <DomainList.Item>
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}

Kombiniere mit ...

Die List kann um eine Sortierung, einen Filter, ein SearchField oder eine Kombination dieser Elemente erweitert werden.

Sortierung

Nutze <List.Sorting /> innerhalb der List, um eine Sortiermethode anzulegen.

import { typedList } from "@mittwald/flow-react-components/List";
import {
  type Domain,
  domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import Avatar from "@mittwald/flow-react-components/Avatar";
import Heading from "@mittwald/flow-react-components/Heading";
import Text from "@mittwald/flow-react-components/Text";
import ContextMenu from "@mittwald/flow-react-components/ContextMenu";
import {
  IconDomain,
  IconSubdomain,
} from "@mittwald/flow-react-components/Icons";
import AlertBadge from "@mittwald/flow-react-components/AlertBadge";
import MenuItem from "@mittwald/flow-react-components/MenuItem";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List batchSize={5}>
      <DomainList.StaticData data={domains} />
      <DomainList.Sorting
        property="hostname"
        name="Domain A bis Z"
        direction="asc"
      />
      <DomainList.Sorting
        property="hostname"
        name="Domain Z bis A"
        direction="desc"
      />
      <DomainList.Sorting
        property="type"
        name="Type A bis Z"
        direction="asc"
      />
      <DomainList.Sorting
        property="type"
        name="Type Z bis A"
        direction="desc"
      />
      <DomainList.Item>
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}

Filter

Über <List.Filter /> lassen sich Filtermöglichkeiten anlegen.

import { typedList } from "@mittwald/flow-react-components/List";
import {
  type Domain,
  domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import Avatar from "@mittwald/flow-react-components/Avatar";
import Heading from "@mittwald/flow-react-components/Heading";
import Text from "@mittwald/flow-react-components/Text";
import ContextMenu from "@mittwald/flow-react-components/ContextMenu";
import {
  IconDomain,
  IconSubdomain,
} from "@mittwald/flow-react-components/Icons";
import AlertBadge from "@mittwald/flow-react-components/AlertBadge";
import MenuItem from "@mittwald/flow-react-components/MenuItem";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List batchSize={5}>
      <DomainList.StaticData data={domains} />
      <DomainList.Filter
        property="type"
        mode="some"
        name="Type"
      />
      <DomainList.Item>
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}

Suche

Verwende <List.Search /> innerhalb der List, um ein SearchField anzuzeigen.

import { typedList } from "@mittwald/flow-react-components/List";
import {
  type Domain,
  domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import Avatar from "@mittwald/flow-react-components/Avatar";
import Heading from "@mittwald/flow-react-components/Heading";
import Text from "@mittwald/flow-react-components/Text";
import ContextMenu from "@mittwald/flow-react-components/ContextMenu";
import {
  IconDomain,
  IconSubdomain,
} from "@mittwald/flow-react-components/Icons";
import AlertBadge from "@mittwald/flow-react-components/AlertBadge";
import MenuItem from "@mittwald/flow-react-components/MenuItem";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List batchSize={5}>
      <DomainList.StaticData data={domains} />
      <DomainList.Search />
      <DomainList.Item>
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}

ActionGroup

Verwende <ActionGroup/> innerhalb der List, um eine ActionGroup anzuzeigen. Hier können Aktionen definiert werden, die sich direkt auf die Liste beziehen."

import { typedList } from "@mittwald/flow-react-components/List";
import {
  type Domain,
  domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import Avatar from "@mittwald/flow-react-components/Avatar";
import Heading from "@mittwald/flow-react-components/Heading";
import Text from "@mittwald/flow-react-components/Text";
import ContextMenu from "@mittwald/flow-react-components/ContextMenu";
import {
  IconDomain,
  IconDownload,
  IconSubdomain,
} from "@mittwald/flow-react-components/Icons";
import AlertBadge from "@mittwald/flow-react-components/AlertBadge";
import MenuItem from "@mittwald/flow-react-components/MenuItem";
import Button from "@mittwald/flow-react-components/Button";
import ActionGroup from "@mittwald/flow-react-components/ActionGroup";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List batchSize={5}>
      <DomainList.StaticData data={domains} />
      <ActionGroup>
        <Button
          color="secondary"
          variant="soft"
          slot="secondary"
        >
          <IconDownload />
        </Button>
        <Button color="accent">Anlegen</Button>
      </ActionGroup>
      <DomainList.Item>
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}

Tabellen-Ansicht

Die List kann als Liste oder als Tabelle dargestellt werden. Nutze <List.Item /> für die Darstellung als Liste und <List.Table />, um sie als Tabelle anzuzeigen

import { typedList } from "@mittwald/flow-react-components/List";
import {
  type Domain,
  domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import Avatar from "@mittwald/flow-react-components/Avatar";
import Heading from "@mittwald/flow-react-components/Heading";
import Text from "@mittwald/flow-react-components/Text";
import ContextMenu from "@mittwald/flow-react-components/ContextMenu";
import {
  IconDomain,
  IconSubdomain,
} from "@mittwald/flow-react-components/Icons";
import AlertBadge from "@mittwald/flow-react-components/AlertBadge";
import MenuItem from "@mittwald/flow-react-components/MenuItem";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List batchSize={5} defaultViewMode="table">
      <DomainList.StaticData data={domains} />
      <DomainList.Table>
        <DomainList.TableHeader>
          <DomainList.TableColumn>
            Name
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Type
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            TLD
          </DomainList.TableColumn>
          <DomainList.TableColumn>
            Hostname
          </DomainList.TableColumn>
        </DomainList.TableHeader>

        <DomainList.TableBody>
          <DomainList.TableRow>
            <DomainList.TableCell>
              {(domain) => domain.domain}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.type}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.tld}
            </DomainList.TableCell>
            <DomainList.TableCell>
              {(domain) => domain.hostname}
            </DomainList.TableCell>
          </DomainList.TableRow>
        </DomainList.TableBody>
      </DomainList.Table>
      <DomainList.Item>
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}

Pagination

Die Pagination wird standardmäßig bei jeder List aktiviert. Sie zeigt im Standard 10 Einträge an und lädt über einen Klick auf "Mehr anzeigen" jeweils 10 Einträge nach. Diese Werte lassen sich über die batchSize Property anpassen.

import { typedList } from "@mittwald/flow-react-components/List";
import {
  type Domain,
  domains,
} from "@/content/03-components/structure/list/examples/domainApi";
import Avatar from "@mittwald/flow-react-components/Avatar";
import Heading from "@mittwald/flow-react-components/Heading";
import Text from "@mittwald/flow-react-components/Text";
import ContextMenu from "@mittwald/flow-react-components/ContextMenu";
import {
  IconDomain,
  IconSubdomain,
} from "@mittwald/flow-react-components/Icons";
import AlertBadge from "@mittwald/flow-react-components/AlertBadge";
import MenuItem from "@mittwald/flow-react-components/MenuItem";

export default () => {
  const DomainList = typedList<Domain>();

  return (
    <DomainList.List batchSize={3}>
      <DomainList.StaticData data={domains} />
      <DomainList.Item>
        {(domain) => (
          <DomainList.ItemView>
            <Avatar
              color={
                domain.type === "Domain" ? "blue" : "teal"
              }
            >
              {domain.type === "Domain" ? (
                <IconDomain />
              ) : (
                <IconSubdomain />
              )}
            </Avatar>
            <Heading>
              {domain.hostname}
              {!domain.verified && (
                <AlertBadge status="warning">
                  Unverifiziert
                </AlertBadge>
              )}
            </Heading>
            <Text>{domain.type}</Text>

            <ContextMenu>
              <MenuItem>Details anzeigen</MenuItem>
              <MenuItem>Löschen</MenuItem>
            </ContextMenu>
          </DomainList.ItemView>
        )}
      </DomainList.Item>
    </DomainList.List>
  );
}

Summary

Verwende eine <ListSummary/> um eine Zusammenfassung, wie beispielsweise die Summe der Beträge, anzuzeigen.

Gesamt: 41,00 €
import {
  ListItemView,
  ListSummary,
  typedList,
} from "@mittwald/flow-react-components/List";
import Heading from "@mittwald/flow-react-components/Heading";
import Text from "@mittwald/flow-react-components/Text";

export default () => {
  const InvoiceList = typedList<{
    id: string;
    date: string;
    amount: string;
  }>();

  return (
    <InvoiceList.List batchSize={5} aria-label="Invoices">
      <ListSummary>
        <Text
          style={{ display: "block", textAlign: "right" }}
        >
          <b>Gesamt: 41,00 €</b>
        </Text>
      </ListSummary>
      <InvoiceList.StaticData
        data={[
          {
            id: "RG100000",
            date: "1.9.2024",
            amount: "25,00 €",
          },
          {
            id: "RG100001",
            date: "12.9.2024",
            amount: "12,00 €",
          },
          {
            id: "RG100002",
            date: "3.10.2024",
            amount: "4,00 €",
          },
        ]}
      />

      <InvoiceList.Item>
        {(invoice) => (
          <ListItemView>
            <Heading>{invoice.id}</Heading>
            <Text>
              {invoice.date} - {invoice.amount}
            </Text>
          </ListItemView>
        )}
      </InvoiceList.Item>
    </InvoiceList.List>
  );
}

Grundlagen

Die List bildet den strukturierten Rahmen der ListItems. ListItems stellen Elemente (z. B. Domains, E-Mail-Adressen, Projekte, ...) dar. Die List gruppiert diese Elemente und bietet Funktionen wie eine Sortierung, Filterung und Suche. Zusätzlich kann der User die Anzahl der angezeigten ListItems über einen „Mehr anzeigen“-Button steuern.

Best practices

  • Wähle eine Standardsortierung, die dem häufigsten Anwendungsfall entspricht (z. B. "Neueste zuerst" für eine Änderungshistorie).
  • Biete nur Sortiermöglichkeiten an, die dem User einen Mehrwert bieten.
  • Verwende bei Listen mit vielen Einträgen einen Filter. Gruppiere Filterkategorien sinnvoll, z. B. nach Typ, Größe, Status.

Verwendung

Verwende eine List, um ...

  • viele Elemente (z.B. Domains, Projekte, Rechnungspositionen ...) in Form von ListItems kompakt darzustellen.
  • einen einfachen Weg zu schaffen, ein spezifisches ListItem über Filter, Sortierung oder Suche zu finden.
  • über ListItems zu der Detailseite eines Eintrags zu navigieren.

Anwendung

Position

  • Die List wird oft auf Übersichtsseiten verwendet, um z. B. einen Überblick über alle Domains zu bekommen. Von dort aus kann dann in tieferliegende Detailseiten navigiert werden.
  • Sie füllt immer die gesamte Breite des Content-Bereichs aus.
  • Wenn neben der List weiterer Inhalt auf der Seite existiert, sollte dieser über ihr stehen. Layout-Verschiebungen durch das Nachladen über den "Mehr anzeigen"-Button werden so vermieden.

Inhalt

Sortierung

  • Der Button zur Sortierung steht immer links neben allen Filtern.
  • Wähle die Standardsortierung nach dem häufigsten Anwendungsfall, z. B. “Neueste zuerst” bei einer Änderungshistorie.
  • Die aktive Sortierung steht im Sortierungs-Button, z. B. “Sortierung: Neueste zuerst”.

Filter

  • Ein Klick auf den Filter-Button öffnet ein ContextMenu, in dem Filter an- und abgewählt werden können. Ermöglicht der Filter eine Mehrfachauswahl, werden die Optionen als Checkbox dargestellt. Verlangt der Filter eine Einfachauswahl, werden die Optionen als RadioGroup dargestellt.
  • Jeder aktive Filter setzt einen Badge, der per Klick einzeln wieder abgewählt werden kann. Sind mindestens zwei Filter aktiv, erscheint ein “Filter zurücksetzen”-Button.
  • Lassen sich mehrere Filter zu einer Gruppe zusammenfassen (z. B. Status, Art, Größe, Abteilung, ...), platziere diese in einem eigenen Filter-Button. Filter, die keiner Kategorie zugewiesen werden können, können unter einem allgemeinen “Filter”-Button zusammengefasst werden.

Do

Wenn es mehrere Filter einer Kategorie gibt, fasse sie in einem eigenen Button mit passender Bezeichnung zusammen.

Do

Hat ein Filter keine spezifische Kategorie, sollte er in einem allgemeinen Filter-Button platziert werden.

IllustratedMessage

  • Wenn keine Such- oder Filterergebnisse vorliegen, zeigt die List an Stelle der ListItems eine IllustratedMessage an, die verdeutlicht, dass es keine Ergebnisse gibt.
  • Sind noch gar keine Einträge vorhanden, wird die List durch eine IllustratedMessage ersetzt. Diese lädt den User mit einem Call-to-Action dazu ein, das erste Element zu erstellen.

Pagination

Standardmäßig werden 10 Einträge angezeigt. Verwende batchSize, um die Anzahl der Einträge anzupassen. Über den "Mehr anzeigen"-Button können bei Bedarf weitere Einträge nachgeladen werden.

Beachte bei der Anpassung der Pagination, dass ...

  • nur in den seltensten Fällen mehr als 10 Einträge gleichzeitig sichtbar sein müssen. Der User kann über Suche, Filter und Sortierung gezielt nach Einträgen suchen.
  • eine List, die beim initialen Seitenaufruf schon viele Einträge darstellen soll, die Performance beeinträchtigt. Gleiches gilt, wenn durch einen Klick auf den “Mehr Anzeigen”-Button viele Einträge auf einmal nachgeladen werden sollen.

Writing guidelines

Sortierung

Ist die Standardsortierung aktiv, steht auf dem Sortierungs-Button nur “Sortierung”. Wählt der User eine spezifische Sortierung aus, so wird diese hinten angehängt. Daher solltest du die Sortiermethode so benennen, dass direkt ersichtlich ist, wonach und in welcher Reihenfolge sie sortiert.

Do

Benenne die Sortierung so, dass eindeutig ersichtlich ist, wonach und in welcher Reihenfolge sortiert wird.

Don't

Verzichte auf Formulierungen, die nicht eindeutig verstanden werden und keine klare Sortierreihenfolge kommunizieren.

Filterung

Gesetzte Filter werden als Badges dargestellt. Diese müssen aus sich selbst heraus verständlich sein. Daher ist es ratsam, bei Begriffen, die mehrdeutig sein können, zusätzlichen Kontext zu liefern. Bei Filtern, die der User intuitiv einer bestimmten Kategorie zuordnen kann, ist dies nicht notwendig.

Domain
Unverifiziert

Do

Intuitiv verständliche Filter benötigen keinen zusätzlichen Kontext. Erklärungsbedürftige Filter sollten mit weiterem Kontext versehen werden, um ihre Nutzung erleichtern.

Type Domain
Verifizierung Unverifiziert

Don't

Zusätzlicher Kontext ist bei intuitiven Filtern nicht notwendig.


Behavior

List im Modal

Öffnet sich eine List im Modal, so sollte automatisch die Suche fokussiert sein, um dem User eine direkte Eingabe zu ermöglichen. Setze daher auf der Suche die Property autoFocus, wenn sich die List in einem Modal öffnet.

Responsive layout

Auf kleinen Bildschirmen (unter 551px) bricht der Header um, sodass die Suche unterhalb der Sortierung und des Filters dargestellt wird. Achte auf das Umbruchverhalten des ListItem's.

Mobile Variante


Accessibility

Häufig wird eine List als alleiniges Element einer Seite verwendet und besitzt somit keine Heading. In diesen Fällen muss sie mit einem aria-label beschrieben werden. Ist die List mit einer eigenen Heading versehen, so muss die Heading mit aria-labelledby der List zugeordnet werden.

Feedback geben