Kalıplar
Filtreleme
Filtrelerin ne zaman anında, ne zaman toplu uygulanacağı, sonucun nasıl bildirileceği ve filtrelerin adreste tutulması.
Filtreler uzun bir listeyi kullanıcının aradığı kümeye indirir. Bu kalıp filtrelerin ne zaman uygulanacağını, sonucun nasıl bildirileceğini ve boş sonucun nasıl ele alınacağını sabitler.
Ne zaman kullanılır
Bölüm başlığı “Ne zaman kullanılır”- Tablo ve liste ekranlarında, kayıt sayısı bir bakışta taranamayacak kadar çoksa.
- Kullanıcının aynı kümeye geri dönmesi ya da kümeyi paylaşması gerekiyorsa; filtreler adreste tutulur.
- Tek bir arama kutusu yeterliyse
FilterBaryerine yalnızca arama alanını kullanın.
Canlı örnek
Bölüm başlığı “Canlı örnek”Arama kutusuna yazın, durum ya da kategori seçin. Sonuç sayısı çubuğun altında görünür. Hiçbir satır eşleşmezse tablo “Filtreleri temizle” düğmesiyle boş durumu gösterir.
| Durum | Kategori | ||
|---|---|---|---|
| Öğe 1 | Kategori A | 50 | |
| Öğe 2 | Kategori B | 100 | |
| Öğe 3 | Kategori C | 150 | |
| Öğe 4 | Kategori A | 200 | |
| Öğe 5 | Kategori B | 250 | |
| Öğe 6 | Kategori C | 300 | |
| Öğe 7 | Kategori A | 350 | |
| Öğe 8 | Kategori B | 400 | |
| Öğe 9 | Kategori C | 450 | |
| Öğe 10 | Kategori A | 500 | |
| Öğe 11 | Kategori B | 550 | |
| Öğe 12 | Kategori C | 600 |
(boş)import { createDataTableColumnHelper, DataTable, FilterBar, filtersToSearchParams, Stack, StatusBadge, Text, type FilterField } from '@bigbang/ui';import { useEffect, useMemo, useState } from 'react';import { type Lang, LangProvider } from '../lang';
const copy = { tr: { caption: 'Öğe listesi', search: 'Ara', searchPlaceholder: 'Ada göre ara', status: 'Durum', category: 'Kategori', statuses: { active: 'Etkin', pending: 'Beklemede', draft: 'Taslak', completed: 'Tamamlandı' }, categories: { a: 'Kategori A', b: 'Kategori B', c: 'Kategori C' }, name: 'Ad', count: 'Adet', item: 'Öğe', url: 'Adres sorgusu', noFilters: '(boş)', }, en: { caption: 'Item list', search: 'Search', searchPlaceholder: 'Search by name', status: 'Status', category: 'Category', statuses: { active: 'Active', pending: 'Pending', draft: 'Draft', completed: 'Completed' }, categories: { a: 'Category A', b: 'Category B', c: 'Category C' }, name: 'Name', count: 'Count', item: 'Item', url: 'URL query', noFilters: '(empty)', },};
type Filters = { query: string; status: string | null; category: string | null };type Row = { id: string; name: string; status: 'active' | 'pending' | 'draft' | 'completed'; category: 'a' | 'b' | 'c'; count: number };
const empty: Filters = { query: '', status: null, category: null };const statusOrder = ['active', 'pending', 'draft', 'completed'] as const;const categoryOrder = ['a', 'b', 'c'] as const;const DEBOUNCE = 300;
const helper = createDataTableColumnHelper<Row>();
/** * Filters above a table. In `instant` mode every change filters at once; the search text waits 300 ms after the last key. * In `batch` mode the draft is applied with the Apply button. The URL query line shows what an app would keep in the address. */export function FilteringDemo({ lang = 'tr', apply = 'instant' }: { lang?: Lang; apply?: 'instant' | 'batch' }) { const t = copy[lang]; const [filters, setFilters] = useState<Filters>(empty); const [debounced, setDebounced] = useState('');
// Typing is debounced in instant mode only; batch mode filters when Apply is pressed. useEffect(() => { if (apply === 'batch') return; const timer = setTimeout(() => setDebounced(filters.query), DEBOUNCE); return () => clearTimeout(timer); }, [filters.query, apply]);
const fields = useMemo<ReadonlyArray<FilterField<Filters>>>( () => [ { key: 'query', label: t.search, kind: 'search', placeholder: t.searchPlaceholder }, { key: 'status', label: t.status, kind: 'select', options: statusOrder.map((value) => ({ value, label: t.statuses[value] })) }, { key: 'category', label: t.category, kind: 'select', options: categoryOrder.map((value) => ({ value, label: t.categories[value] })) }, ], [t], );
const rows = useMemo<Row[]>( () => Array.from({ length: 12 }, (_, index) => ({ id: `row-${index + 1}`, name: `${t.item} ${index + 1}`, status: statusOrder[index % statusOrder.length] ?? 'draft', category: categoryOrder[index % categoryOrder.length] ?? 'a', count: (index + 1) * 50, })), [t], );
const shown = useMemo(() => { const query = (apply === 'batch' ? filters.query : debounced).trim().toLocaleLowerCase(lang); return rows.filter( (row) => row.name.toLocaleLowerCase(lang).includes(query) && (filters.status === null || row.status === filters.status) && (filters.category === null || row.category === filters.category), ); }, [rows, apply, filters.query, debounced, filters.status, filters.category, lang]);
const columns = useMemo( () => [ helper.accessor('name', { header: t.name, meta: { rowHeader: true } }), helper.accessor('status', { header: t.status, enableSorting: false, cell: (context) => <StatusBadge status={context.getValue()} /> }), helper.accessor('category', { header: t.category, enableSorting: false, cell: (context) => t.categories[context.getValue()] }), helper.accessor('count', { header: t.count, meta: { numeric: true } }), ], [t], );
const clear = () => { setFilters(empty); setDebounced(''); }; const queryString = filtersToSearchParams(filters, fields).toString();
return ( <LangProvider lang={lang}> <Stack gap="lg"> <FilterBar<Filters> fields={fields} value={filters} onValueChange={setFilters} apply={apply} resultCount={shown.length} defaultValue={empty} /> <DataTable<Row> columns={columns} data={shown} getRowId={(row) => row.id} caption={t.caption} captionHidden status={shown.length === 0 ? 'filtered-empty' : 'ready'} onClearFilters={clear} /> <Text size="sm" tone="subtle"> {t.url}: <code>{queryString ? `?${queryString}` : t.noFilters}</code> </Text> </Stack> </LangProvider> );}Her değişikliğin yavaş bir sunucu sorgusu başlattığı veriler için toplu kipi kullanın. Değişiklikler taslakta bekler ve “Uygula” ile birlikte uygulanır.
Kurallar
Bölüm başlığı “Kurallar”| Kural | Değer |
|---|---|
Anında kip (apply="instant") |
Yerel ya da hızlı veri. Her değişiklik hemen uygulanır. |
Toplu kip (apply="batch") |
Her değişiklik yavaş bir sunucu sorgusu başlatıyorsa. “Uygula” yalnızca taslak değiştiğinde etkindir. |
| Arama gecikmesi | Son tuştan 300 ms sonra filtrelenir. |
| Uygulanan filtreler | Kaldırılabilir çip olarak, çubuğun altında gösterilir. |
| Sonuç sayısı | Çipler satırında “12 sonuç” biçiminde gösterilir. |
| Adres | Filtreler filtersToSearchParams ile adrese yazılır, searchParamsToFilters ile okunur. |
| Boş sonuç | status="filtered-empty" ve onClearFilters; mesaj “Filtreleri temizle” düğmesiyle birlikte gelir. |
| Giriş hareketi | Filtre değişince satırlar hareketle girmez. |
Davranış
Bölüm başlığı “Davranış”- Kullanıcı bir filtre seçer ya da arama kutusuna yazar. Anında kipte tablo hemen, arama ise 300 ms sonra güncellenir.
- Toplu kipte değişiklikler taslakta kalır. Kullanıcı “Uygula”ya basar ya da metin alanında Enter’a basar.
- Uygulanan her filtre bir çip olur. Çipi kaldırmak yalnızca o filtreyi kaldırır; “Filtreleri temizle” hepsini kaldırır ve odağı ilk denetime taşır.
- Sonuç sayısı güncellenir. Hiç sonuç yoksa tablo boş durum mesajını ve temizleme düğmesini gösterir.
- Uygulama filtreleri adrese yazar. Sayfa açıldığında adresteki geçerli değerler filtrelere dönüşür; bilinmeyen değerler yok sayılır.
Arama metninin adreste tutulup tutulmayacağına ürün karar verir. Kişisel veri içerebilecek aramaları adrese yazmayın.
Erişilebilirlik
Bölüm başlığı “Erişilebilirlik”- Filtre alanı
role="search"taşır ve “Filtreler” adını alır. - Sonuç sayısı, değeri 500 ms değişmediğinde bir kez ve nazikçe (
aria-live="polite") okunur; yazarken ekran okuyucu sayı yağmuruna tutulmaz. - Veri yüklenirken filtre alanı
aria-busyolur ve sayı yer tutucuyla değişir. - Her çipin kaldır düğmesi filtreyi adıyla söyler: “Durum filtresini kaldır”.
- “Filtreleri temizle”ye basınca odak, kaybolan düğme yerine ilk denetime gider.
- Filtre durumu renkle değil metinle bildirilir (WCAG 1.4.1, 4.1.3).
İçerik
Bölüm başlığı “İçerik”- Alan adları kısa ad öbeğidir: “Durum”, “Kategori”.
- Sonuç sayısı sayıyı ve birimi verir: “12 sonuç”.
- Boş sonuç ne olduğunu söyler: “Filtrelere uyan kayıt yok”. Suçlayıcı dil kullanmayın.
- Düğme metinleri fiille başlar: “Uygula”, “Sıfırla”, “Filtreleri temizle”.
Hareket
Bölüm başlığı “Hareket”| Öğe | Hareket | Preset |
|---|---|---|
| Filtre sonrası satırlar | Giriş hareketi yok; satırlar yerinde değişir | yok |
| Sonuç sayısı | 110 ms çapraz solma | motion.reveal |
| Diğer filtreler paneli | 150 ms açılma, 4 px kayma | motion.collapsible |
| Yükleniyor yer tutucusu | 1500 ms parıltı, 3 kez, sonra durağan | motion.skeleton |
Hareketi azaltma tercihinde kaymalar kalkar, yalnızca 110 ms ve altındaki solmalar kalır.
Kullanılan bileşenler
Bölüm başlığı “Kullanılan bileşenler”FilterBar, DataTable, StatusBadge, Tag, Select, Button. İlgili kalıplar: Boş durumlar, Toplu işlemler, Veri dışa aktarma.