İçeriğe geç
BigBang

Sık açılanlar

    ↑↓Gezin↵AçEscKapat
    EN

    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.

    • 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 FilterBar yerine yalnızca arama alanını kullanın.

    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.

    Canlı örnek, StorybookStorybook'ta aç
    Öğe listesi
    DurumKategori
    Öğe 1EtkinKategori A50
    Öğe 2BeklemedeKategori B100
    Öğe 3TaslakKategori C150
    Öğe 4TamamlandıKategori A200
    Öğe 5EtkinKategori B250
    Öğe 6BeklemedeKategori C300
    Öğe 7TaslakKategori A350
    Öğe 8TamamlandıKategori B400
    Öğe 9EtkinKategori C450
    Öğe 10BeklemedeKategori A500
    Öğe 11TaslakKategori B550
    Öğe 12TamamlandıKategori C600
    Adres sorgusu: (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.

    Canlı örnek, StorybookStorybook'ta aç
    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.
    YapınBoş sonuç, çıkış yolunu bir eylemle sunar.
    Sonuç yok.
    YapmayınEylemsiz bir mesaj kullanıcıyı çıkmaza sokar.
    1. Kullanıcı bir filtre seçer ya da arama kutusuna yazar. Anında kipte tablo hemen, arama ise 300 ms sonra güncellenir.
    2. Toplu kipte değişiklikler taslakta kalır. Kullanıcı “Uygula”ya basar ya da metin alanında Enter’a basar.
    3. 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.
    4. Sonuç sayısı güncellenir. Hiç sonuç yoksa tablo boş durum mesajını ve temizleme düğmesini gösterir.
    5. 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.

    • 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-busy olur 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).
    • 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”.
    Öğ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.

    FilterBar, DataTable, StatusBadge, Tag, Select, Button. İlgili kalıplar: Boş durumlar, Toplu işlemler, Veri dışa aktarma.