İçeriğe geç
BigBang

Sık açılanlar

    ↑↓Gezin↵AçEscKapat
    EN

    Kalıplar

    Formlar ve doğrulama

    Alanları ne zaman denetleyeceğiniz, hataları nerede göstereceğiniz ve odağın nereye gideceği.

    Form, kullanıcıdan veri alan her ekranın temelidir. Bu kalıp doğrulamanın zamanlamasını, hata metninin yerini ve odak sırasını sabitler; böylece her formda kullanıcı aynı geri bildirimi alır.

    • Kullanıcının birden fazla alanı doldurup tek seferde gönderdiği her ekranda.
    • Ayarlar, profil, kayıt ve arama dışındaki veri girişlerinde.
    • Tek alanlı satır içi düzenlemede yalnızca satır içi hata kuralı geçerlidir; özet kutusu gerekmez.

    Formu boş gönderin. Hata özeti odağı alır; özetteki bağlantı ilgili alana götürür. Alanı düzelttikçe hata hemen kalkar.

    Canlı örnek, StorybookStorybook'ta aç

    Profil bilgileri

    Bildirimler bu adrese gelir.

    import { Button, Checkbox, Heading, Link, Stack, Text, TextInput, Textarea } from '@bigbang/ui';
    import { useRef, useState, type FormEvent } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    title: 'Profil bilgileri',
    name: 'Ad soyad',
    email: 'E-posta adresi',
    emailHint: 'Bildirimler bu adrese gelir.',
    note: 'Not',
    consent: 'Kullanım koşullarını okudum',
    submit: 'Kaydet',
    saved: 'Kaydedildi',
    summary: (count: number) => `Formda ${count} hata var`,
    errors: {
    name: 'Ad soyad girin.',
    emailEmpty: 'E-posta adresi girin.',
    emailFormat: 'E-posta adresini ad@alanadi.com biçiminde girin.',
    note: 'Not en fazla 200 karakter olabilir.',
    consent: 'Devam etmek için kullanım koşullarını onaylayın.',
    },
    },
    en: {
    title: 'Profile details',
    name: 'Full name',
    email: 'Email address',
    emailHint: 'Notifications go to this address.',
    note: 'Note',
    consent: 'I have read the terms of use',
    submit: 'Save',
    saved: 'Saved',
    summary: (count: number) => `There ${count === 1 ? 'is 1 error' : `are ${count} errors`} in the form`,
    errors: {
    name: 'Enter your full name.',
    emailEmpty: 'Enter an email address.',
    emailFormat: 'Enter the email address as name@domain.com.',
    note: 'The note can be 200 characters at most.',
    consent: 'Accept the terms of use to continue.',
    },
    },
    };
    type Values = { name: string; email: string; note: string; consent: boolean };
    type FieldKey = keyof Values;
    type Errors = Partial<Record<FieldKey, string>>;
    const order: FieldKey[] = ['name', 'email', 'note', 'consent'];
    export function validate(values: Values, lang: Lang): Errors {
    const t = copy[lang].errors;
    const errors: Errors = {};
    if (!values.name.trim()) errors.name = t.name;
    if (!values.email.trim()) errors.email = t.emailEmpty;
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email.trim())) errors.email = t.emailFormat;
    if (values.note.length > 200) errors.note = t.note;
    if (!values.consent) errors.consent = t.consent;
    return errors;
    }
    /**
    * Validate on submit, then re-check a field as it changes so its error clears as soon as it is fixed.
    * The error summary takes focus and links to each field; inline errors repeat the message at the field.
    */
    export function ValidationForm({ lang = 'tr', initial }: { lang?: Lang; initial?: Partial<Values> }) {
    const t = copy[lang];
    const [values, setValues] = useState<Values>({ name: '', email: '', note: '', consent: false, ...initial });
    const [errors, setErrors] = useState<Errors>({});
    const [submitted, setSubmitted] = useState(false);
    const [status, setStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
    const summaryRef = useRef<HTMLDivElement>(null);
    const formRef = useRef<HTMLFormElement>(null);
    const update = <K extends FieldKey>(key: K, value: Values[K]) => {
    const next = { ...values, [key]: value };
    setValues(next);
    setStatus('idle');
    // After the first submit, errors follow the value; before it, nobody is told off while typing.
    if (submitted) setErrors(validate(next, lang));
    };
    const focusField = (key: FieldKey) => {
    formRef.current?.querySelector<HTMLElement>(`[data-field="${key}"] input:not([type="hidden"]), [data-field="${key}"] textarea, [data-field="${key}"] [role="checkbox"]`)?.focus();
    };
    const onSubmit = (event: FormEvent) => {
    event.preventDefault();
    const found = validate(values, lang);
    setErrors(found);
    setSubmitted(true);
    if (Object.keys(found).length > 0) {
    // Focus moves once, to the summary; it is announced as a whole.
    requestAnimationFrame(() => summaryRef.current?.focus());
    return;
    }
    setStatus('saving');
    setTimeout(() => setStatus('saved'), 1200);
    };
    const listed = order.filter((key) => errors[key]);
    // `error` is optional and exact, so it is only passed when there is one.
    const errorOf = (key: FieldKey) => (errors[key] ? { error: errors[key] } : {});
    return (
    <LangProvider lang={lang}>
    <form ref={formRef} noValidate onSubmit={onSubmit} className="w-full max-w-md" aria-labelledby={`form-title-${lang}`}>
    <Stack gap="xl">
    <Heading level={2} size="md" id={`form-title-${lang}`}>
    {t.title}
    </Heading>
    {submitted && listed.length > 0 ? (
    <div
    ref={summaryRef}
    tabIndex={-1}
    role="alert"
    aria-labelledby={`form-summary-${lang}`}
    className="rounded-md border border-status-danger-border bg-status-danger-background p-lg outline-none focus-visible:focus-outline"
    data-part="error-summary"
    >
    <Stack gap="sm">
    <Text weight="medium" tone="danger" id={`form-summary-${lang}`}>
    {t.summary(listed.length)}
    </Text>
    <ul className="m-0 list-disc ps-xl">
    {listed.map((key) => (
    <li key={key}>
    <Link
    href={`#${key}`}
    onClick={(event) => {
    event.preventDefault();
    focusField(key);
    }}
    >
    {errors[key]}
    </Link>
    </li>
    ))}
    </ul>
    </Stack>
    </div>
    ) : null}
    <div data-field="name">
    <TextInput label={t.name} required name="name" autoComplete="name" value={values.name} onValueChange={(value) => update('name', value)} {...errorOf('name')} />
    </div>
    <div data-field="email">
    <TextInput
    label={t.email}
    description={t.emailHint}
    required
    name="email"
    type="email"
    autoComplete="email"
    value={values.email}
    onValueChange={(value) => update('email', value)}
    {...errorOf('email')}
    />
    </div>
    <div data-field="note">
    <Textarea label={t.note} necessityIndicator="optional" name="note" maxLength={200} showCount value={values.note} onValueChange={(value) => update('note', value)} {...errorOf('note')} />
    </div>
    <div data-field="consent">
    <Checkbox label={t.consent} name="consent" required checked={values.consent} onCheckedChange={(checked) => update('consent', checked)} invalid={Boolean(errors.consent)} />
    {errors.consent ? (
    <Text size="sm" tone="danger" className="mt-xs">
    {errors.consent}
    </Text>
    ) : null}
    </div>
    <div>
    <Button type="submit" loading={status === 'saving'} success={status === 'saved'} successLabel={t.saved}>
    {t.submit}
    </Button>
    </div>
    </Stack>
    </form>
    </LangProvider>
    );
    }
    Kural Değer
    İlk doğrulama Gönder düğmesine basıldığında. Yazarken hata gösterilmez.
    Sonraki doğrulama İlk gönderimden sonra her değişiklikte; hata düzeltildiği anda kalkar.
    Hata yeri Alanın altında (error prop’u) ve formun başındaki özet kutusunda.
    Odak Gönderimde hata varsa özet kutusuna bir kez gider. Özetteki bağlantı ilgili alana odaklanır.
    Zorunlu alan required ile işaretlenir; etiket yanında gizli “zorunlu” metni okunur. İsteğe bağlı alan azsa necessityIndicator="optional" kullanın.
    Gönder düğmesi Hiçbir zaman devre dışı bırakılmaz. Eksikleri kullanıcı gönderince öğrenir.
    Tarayıcı doğrulaması noValidate ile kapatılır; mesajlar ürün dilinde ve tek biçimde kalır.
    E-posta adresini ad@alanadi.com biçiminde girin.
    YapınHata, nasıl düzeltileceğini söyler.
    Geçersiz değer.
    YapmayınGenel hata metni kullanıcıyı tahmine zorlar.
    YapınGönder düğmesi her zaman etkin; eksikler gönderimde söylenir.
    YapmayınDevre dışı düğme nedenini söylemez ve odak alamaz.
    1. Kullanıcı formu doldurur. Bu sırada hata gösterilmez; yalnızca karakter sayacı gibi bilgi metinleri güncellenir.
    2. Gönder düğmesine basılır. Tüm alanlar birlikte denetlenir.
    3. Hata varsa özet kutusu formun başında açılır ve odağı alır. Her hata hem özette hem alanın altında görünür.
    4. Kullanıcı bir alanı düzelttikçe o alanın hatası ve özetteki satırı kalkar; özet başlığındaki sayı güncellenir.
    5. Hata kalmadığında gönderim sürer: düğme yükleniyor durumuna geçer, sonra başarı işaretini gösterir. Ayrıntı için Kaydetme kalıbına bakın.

    Sunucudan dönen alan hataları aynı yolu izler: alan altına yazılır, özet açılır, odak özete gider.

    • Özet kutusu role="alert" taşır ve başlığıyla adlandırılır; açıldığında odak ona gider, böylece ekran okuyucu hata sayısını bir kez okur.
    • Alan hataları aria-describedby ile alana bağlıdır ve aria-invalid="true" ayarlanır (TextInput ve Field bunu kendisi yapar).
    • Özetteki her satır bir bağlantıdır; bağlantı ilgili alanın kontrolüne odaklanır.
    • Zorunluluk yalnızca yıldızla değil, gizli “zorunlu” metniyle de bildirilir.
    • Hata rengi tek başına bilgi taşımaz; metin her zaman vardır (WCAG 1.4.1, 3.3.1, 3.3.3).
    • Hata metni ne olduğunu ve nasıl düzeltileceğini söyler: “E-posta adresini ad@alanadi.com biçiminde girin.”
    • “Geçersiz”, “hatalı giriş” gibi genel ifadeler kullanmayın; kullanıcıyı suçlamayın.
    • Özet başlığı sayıyı verir: “Formda 3 hata var”.
    • Etiketler kısa ad öbeğidir (“E-posta adresi”); yer tutucu metin etiketin yerini tutmaz.
    Öğe Hareket Preset
    Alan çerçevesi ve hata rengi 110 ms renk geçişi motion.field
    Alan hata metni 150 ms solma, 4 px aşağı kayma; sallanma yok motion.fieldMessage
    Gönder düğmesi Basınca 0,97 ölçek; yükleniyor ve başarı işareti motion.press, motion.success

    Hareketi azaltma tercihinde kaymalar kalkar, yalnızca solma kalır.

    TextInput, Textarea, Checkbox, Field, Button, Link. Form durumunu ve şema doğrulamasını üründe React Hook Form ve Zod yönetir; bu kalıp yalnızca görünür davranışı tanımlar.