İçeriğe geç
BigBang

Sık açılanlar

    ↑↓Gezin↵AçEscKapat
    EN

    Kalıplar

    Oturum süresi ve tek kullanımlık kod

    Oturum bitmeden uyarmayı ve tek kullanımlık kod girişini erişilebilir biçimde tasarlamayı anlatır.

    Süreli oturum ve tek kullanımlık kod, kullanıcıya zaman baskısı getirir. Bu kalıp uyarının ne zaman geldiğini, süreyi nasıl duyurduğunu ve kod alanının nasıl davrandığını belirler.

    • Boşta kalınca sona eren her oturumda: bitmeden uyarın.
    • SMS veya e-posta ile gelen tek kullanımlık kod girişinde.
    • Süresi kullanıcıya bağlı olmayan, WCAG 2.2.1 kapsamındaki her zaman sınırında.

    Oturum uyarısı 125 saniyeden başlar: 5 saniye sonra kapatılamayan bir pencere açılır ve 120, 60, 30 saniyede duyurulur. Uzatın: süre tam oturuma döner ve bir Toast görünür.

    Canlı örnek, StorybookStorybook'ta aç
    Oturumunuz açık.
    import { Button, Dialog, ProgressBar, ResultState, Stack, Text, useToast } from '@bigbang/ui';
    import { useEffect, useRef, useState } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    heading: 'Oturumunuz sona ermek üzere',
    remaining: 'Kalan süre',
    body: 'Devam etmek için oturumu uzatın. Girdiğiniz bilgiler korunur.',
    extend: 'Oturumu uzat',
    signOut: 'Çıkış yap',
    active: 'Oturumunuz açık.',
    extended: 'Oturum 15 dakika uzatıldı',
    expiredTitle: 'Oturum süresi doldu',
    expired: 'Devam etmek için tekrar giriş yapın. Gönderilmemiş girdiğiniz bilgiler korunur.',
    signedOut: 'Çıkış yaptınız.',
    announce: (time: string) => `Oturumunuz ${time} sonra sona erecek`,
    format: (m: number, s: number) => (m > 0 ? `${m} dk ${s} sn` : `${s} sn`),
    human: (m: number, s: number) => [m > 0 ? `${m} dakika` : '', s > 0 || m === 0 ? `${s} saniye` : ''].filter(Boolean).join(' '),
    },
    en: {
    heading: 'Your session is about to end',
    remaining: 'Time left',
    body: 'Extend the session to continue. Your input is kept.',
    extend: 'Stay signed in',
    signOut: 'Sign out',
    active: 'Your session is active.',
    extended: 'Session extended by 15 minutes',
    expiredTitle: 'The session has timed out',
    expired: 'Sign in again to continue. Input you have not sent is kept.',
    signedOut: 'You are signed out.',
    announce: (time: string) => `Your session ends in ${time}`,
    format: (m: number, s: number) => (m > 0 ? `${m} min ${s} s` : `${s} s`),
    human: (m: number, s: number) =>
    [m > 0 ? `${m} ${m === 1 ? 'minute' : 'minutes'}` : '', s > 0 || m === 0 ? `${s} ${s === 1 ? 'second' : 'seconds'}` : ''].filter(Boolean).join(' '),
    },
    };
    /** Seconds before the end at which the warning appears (WCAG 2.2.1: at least 20 s to respond; 120 s here). */
    export const WARN_AT = 120;
    /** Remaining times, in seconds, that are announced. The visible countdown is not a live region. */
    export const ANNOUNCE_AT = [120, 60, 30] as const;
    /** Length of a full session in seconds. */
    export const SESSION_SECONDS = 900;
    export const formatRemaining = (seconds: number, lang: Lang) => copy[lang].format(Math.floor(seconds / 60), seconds % 60);
    /** The spoken form of the remaining time, such as "1 dakika 30 saniye". */
    export const humanRemaining = (seconds: number, lang: Lang) => copy[lang].human(Math.floor(seconds / 60), seconds % 60);
    function SessionCountdownDemo({ lang, startAt }: { lang: Lang; startAt: number }) {
    const t = copy[lang];
    const toast = useToast();
    const [remaining, setRemaining] = useState(startAt);
    const [signedOut, setSignedOut] = useState(false);
    const [announcement, setAnnouncement] = useState('');
    const lastBucketRef = useRef<number | null>(null);
    const extendRef = useRef<HTMLButtonElement>(null);
    useEffect(() => {
    const timer = setInterval(() => setRemaining((value) => Math.max(0, value - 1)), 1000);
    return () => clearInterval(timer);
    }, []);
    // Announce when the remaining time enters a new threshold: 120, 60 or 30 s.
    useEffect(() => {
    if (remaining <= 0) return;
    const bucket = [...ANNOUNCE_AT].reverse().find((limit) => remaining <= limit) ?? null;
    if (bucket !== null && bucket !== lastBucketRef.current) {
    lastBucketRef.current = bucket;
    setAnnouncement(t.announce(formatRemaining(remaining, lang)));
    }
    }, [remaining, lang, t]);
    const extend = () => {
    setRemaining(SESSION_SECONDS);
    lastBucketRef.current = null;
    setAnnouncement('');
    toast.show({ title: t.extended, tone: 'success' });
    };
    const expired = remaining <= 0;
    const warning = !expired && !signedOut && remaining <= WARN_AT;
    return (
    <Stack gap="md">
    {/* A blocking dialog: it interrupts, ignores Escape and outside presses, and asks for one of two actions. */}
    <Dialog
    open={warning}
    dismissible={false}
    size="sm"
    title={t.heading}
    description={t.body}
    initialFocus={extendRef}
    footer={
    <>
    <Button variant="secondary" onClick={() => setSignedOut(true)}>
    {t.signOut}
    </Button>
    <Button ref={extendRef} onClick={extend}>
    {t.extend}
    </Button>
    </>
    }
    >
    <div data-part="session-warning">
    <ProgressBar
    label={t.remaining}
    value={remaining}
    max={WARN_AT}
    showValue={false}
    valueText={humanRemaining(remaining, lang)}
    description={formatRemaining(remaining, lang)}
    tone={remaining <= 30 ? 'danger' : 'brand'}
    />
    {/* The visible countdown is not a live region; this one speaks at 120, 60 and 30 s only. */}
    <div role="status" aria-live="polite" className="sr-only">
    {announcement}
    </div>
    </div>
    </Dialog>
    {!warning && !expired && !signedOut ? <Text tone="subtle">{t.active}</Text> : null}
    {expired && !signedOut ? <ResultState tone="warning" headingLevel={3} focusOnMount title={t.expiredTitle} description={t.expired} /> : null}
    {signedOut ? <Text>{t.signedOut}</Text> : null}
    </Stack>
    );
    }
    /**
    * A blocking dialog that appears 120 s before the session ends, with the remaining time in a progress bar.
    * It announces at 120, 60 and 30 s only; "Stay signed in" resets the full session and shows a toast.
    */
    export function SessionCountdown({ lang = 'tr', startAt = 125 }: { lang?: Lang; startAt?: number }) {
    return (
    <LangProvider lang={lang}>
    <div className="w-full max-w-md">
    <SessionCountdownDemo lang={lang} startAt={startAt} />
    </div>
    </LangProvider>
    );
    }

    İkinci örnek 31 saniyeden başlar; son eşik 1 saniye sonra gelir.

    Canlı örnek, StorybookStorybook'ta aç

    Kod girişinde “123 456” yapıştırın, yanlış kod girin ve 30 saniyelik bekleme süresini izleyin. Deneme kodu 123456’dır.

    Canlı örnek, StorybookStorybook'ta aç

    Telefonunuza gönderilen 6 haneli kodu girin

    Deneme kodu: 123456
    import { Button, InlineAlert, Stack, Text, TextInput } from '@bigbang/ui';
    import { useEffect, useState, type ClipboardEvent, type FormEvent } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    label: 'Doğrulama kodu',
    description: 'Telefonunuza gönderilen 6 haneli kodu girin',
    short: 'Kod 6 haneli olmalı',
    wrong: (left: number) => `Kod hatalı. ${left} deneme hakkınız kaldı.`,
    noAttempts: 'Deneme hakkınız bitti. Yeni kod isteyin.',
    expired: 'Kodun süresi doldu. Yeni kod isteyin.',
    verify: 'Doğrula',
    resend: 'Kodu tekrar gönder',
    resendWait: (seconds: number) => `Kodu tekrar gönder (${seconds} sn)`,
    resent: 'Yeni kod gönderildi',
    verified: 'Kod doğrulandı',
    hint: 'Deneme kodu: 123456',
    },
    en: {
    label: 'Verification code',
    description: 'Enter the 6-digit code sent to your phone',
    short: 'The code must be 6 digits',
    wrong: (left: number) => `Wrong code. You have ${left} ${left === 1 ? 'attempt' : 'attempts'} left.`,
    noAttempts: 'You have no attempts left. Request a new code.',
    expired: 'The code has expired. Request a new code.',
    verify: 'Verify',
    resend: 'Resend code',
    resendWait: (seconds: number) => `Resend code (${seconds} s)`,
    resent: 'A new code was sent',
    verified: 'Code verified',
    hint: 'Sample code: 123456',
    },
    };
    export const CODE_LENGTH = 6;
    export const RESEND_COOLDOWN = 30;
    export const CODE_LIFETIME = 180;
    export const MAX_ATTEMPTS = 3;
    /** Keep digits only, so a pasted "123 456" or "123-456" becomes "123456". */
    export const digitsOnly = (value: string) => value.replace(/\D/g, '').slice(0, CODE_LENGTH);
    /**
    * One single-line input for a one-time code. It strips non-digits (also from pasted text),
    * counts wrong attempts, expires after 180 s and allows a resend after a 30 s cooldown.
    */
    export function OneTimeCode({ lang = 'tr', correctCode = '123456' }: { lang?: Lang; correctCode?: string }) {
    const t = copy[lang];
    const [value, setValue] = useState('');
    const [error, setError] = useState('');
    const [attempts, setAttempts] = useState(MAX_ATTEMPTS);
    const [cooldown, setCooldown] = useState(RESEND_COOLDOWN);
    const [lifetime, setLifetime] = useState(CODE_LIFETIME);
    const [notice, setNotice] = useState<'resent' | 'verified' | null>(null);
    const [done, setDone] = useState(false);
    useEffect(() => {
    const timer = setInterval(() => {
    setCooldown((seconds) => Math.max(0, seconds - 1));
    setLifetime((seconds) => Math.max(0, seconds - 1));
    }, 1000);
    return () => clearInterval(timer);
    }, []);
    const expired = lifetime <= 0;
    const shownError = done ? '' : expired ? t.expired : error;
    const onPaste = (event: ClipboardEvent<HTMLInputElement>) => {
    // maxLength would cut "123 456" at 6 characters, spaces included; clean the text first.
    event.preventDefault();
    setValue(digitsOnly(event.clipboardData.getData('text')));
    setError('');
    };
    const onSubmit = (event: FormEvent) => {
    event.preventDefault();
    if (expired) return;
    setNotice(null);
    if (value.length !== CODE_LENGTH) {
    setError(t.short);
    } else if (attempts <= 0) {
    setError(t.noAttempts);
    } else if (value !== correctCode) {
    const left = attempts - 1;
    setAttempts(left);
    setError(left > 0 ? t.wrong(left) : t.noAttempts);
    } else {
    setError('');
    setDone(true);
    setNotice('verified');
    }
    };
    const resend = () => {
    setValue('');
    setError('');
    setAttempts(MAX_ATTEMPTS);
    setCooldown(RESEND_COOLDOWN);
    setLifetime(CODE_LIFETIME);
    setNotice('resent');
    };
    return (
    <LangProvider lang={lang}>
    <form noValidate onSubmit={onSubmit} className="w-full max-w-md">
    <Stack gap="lg">
    <TextInput
    label={t.label}
    description={t.description}
    name="one-time-code"
    inputMode="numeric"
    autoComplete="one-time-code"
    maxLength={CODE_LENGTH}
    pattern="[0-9]*"
    value={value}
    onValueChange={(next) => {
    setValue(digitsOnly(next));
    setError('');
    setNotice(null);
    }}
    onPaste={onPaste}
    invalid={Boolean(shownError)}
    />
    {/* The message sits under the field in an InlineAlert; `invalid` marks the input. It is announced when it appears. */}
    {shownError ? (
    <InlineAlert tone="danger" announce>
    {shownError}
    </InlineAlert>
    ) : null}
    {notice ? (
    <InlineAlert tone={notice === 'verified' ? 'success' : 'info'} announce>
    {notice === 'verified' ? t.verified : t.resent}
    </InlineAlert>
    ) : null}
    <div className="flex flex-wrap gap-sm">
    <Button type="submit" disabled={done}>
    {t.verify}
    </Button>
    <Button variant="tertiary" disabled={cooldown > 0} focusableWhenDisabled onClick={resend}>
    {cooldown > 0 ? t.resendWait(cooldown) : t.resend}
    </Button>
    </div>
    <Text size="sm" tone="subtle">
    {t.hint}
    </Text>
    </Stack>
    </form>
    </LangProvider>
    );
    }
    Kural Değer
    Uyarı zamanı Bitişten 120 sn önce. Kullanıcıya yanıt için en az 20 sn verilir (WCAG 2.2.1).
    Uyarı yüzeyi Kapatılamayan Dialog (dismissible={false}): Escape ve dışarı basma kapatmaz.
    Süre uzatma Tek basit eylemle: birincil “Oturumu uzat”. İkincil “Çıkış yap”. Odak “Oturumu uzat”ta başlar.
    Süre gösterimi ProgressBar kalan süreyi gösterir; görünen metin “1 dk 40 sn”, okunan değer valueText: “1 dakika 40 saniye”. Sayaç canlı bölge değildir.
    Duyuru Yalnızca 120, 60 ve 30 sn’de; her saniye duyurulmaz.
    Süre dolunca ResultState ile “Oturum süresi doldu” gösterilir; odak başlığa gider. Gönderilmemiş girdiyi mümkünse koruyun.
    Kod alanı Tek TextInput: inputMode="numeric", autoComplete="one-time-code", maxLength={6}, pattern="[0-9]*".
    Hata ve bilgi Alan altında InlineAlert: yanlış kod danger, yeniden gönderme info, doğrulama success; hepsi görününce duyurulur.
    Yapıştırma Boşluk ve harfler ayıklanır: “123 456” → “123456”.
    Yeniden gönder 30 sn bekleme; bu sürede focusableWhenDisabled düğme “Kodu tekrar gönder (30 sn)” yazar.
    Kod ömrü 180 sn. Dolunca açık bir mesaj gösterilir.
    Deneme hakkı 3. Yanlış kodda kalan hak yazılır.
    YapınDüğme etiketi eylemi söyler.
    YapmayınGenel etiket sonucu söylemez.
    Kod hatalı. 2 deneme hakkınız kaldı.
    YapınHata kalan hakkı söyler.
    Geçersiz değer.
    YapmayınGenel hata kullanıcıyı tahmine zorlar.
    1. Oturumun bitmesine 120 sn kala kapatılamayan bir Dialog açılır: başlık, ProgressBar ile kalan süre, “Çıkış yap” ve “Oturumu uzat”. Odak “Oturumu uzat” düğmesine gider.
    2. Pencere kalan süreyi saniyede bir günceller. Escape ve dışarı basma pencereyi kapatmaz; kullanıcı iki eylemden birini seçer.
    3. “Oturumu uzat” süreyi tam oturuma (15 dakika) döndürür, pencere kapanır ve “Oturum 15 dakika uzatıldı” Toast’ı görünür.
    4. “Çıkış yap” oturumu kapatır. Süre dolarsa pencere kapanır ve ResultState “Oturum süresi doldu” der; gönderilmemiş girdiyi mümkünse koruyun.
    5. Kod girişinde kullanıcı kodu yazar veya yapıştırır; “Doğrula” ile denetlenir. Eksik kodda “Kod 6 haneli olmalı” görünür.
    6. Yanlış kod kalan hakkı azaltır ve hata InlineAlert ile duyurulur. Bekleme süresi bitince “Kodu tekrar gönder” etkinleşir; yeni kod süreyi ve hakkı sıfırlar, “Yeni kod gönderildi” bilgisi görünür.
    • Uyarı bir modal Dialog’dur: odak pencerede kalır, arkadaki sayfa devre dışıdır. Kapatılamaz; çünkü kullanıcı yanıt vermelidir.
    • ProgressBar kalan süreyi valueText ile okur: “1 dakika 30 saniye”. Görünen sayaç canlı bölge değildir; ayrı bir role="status" bölgesi yalnızca 120, 60 ve 30 sn’de konuşur.
    • Süre uzatma tek düğmedir ve odak ondadır; kullanıcı en az 20 sn içinde yanıt verebilir (WCAG 2.2.1).
    • Süre dolunca pencere kapanır; odak ResultState başlığına taşınır, kullanıcı yerini kaybetmez.
    • Kod alanı tek bir girdidir; parola yöneticisi ve SMS otomatik doldurma autoComplete="one-time-code" ile çalışır.
    • Yeniden gönder düğmesi bekleme sırasında odak alır ve aria-disabled="true" taşır; kalan süre metinde yazar.
    • Yanlış kod alanı aria-invalid ile işaretler; mesaj alanın altındaki InlineAlert’te okunur (“Hata: …”) ve görününce duyurulur.
    • Başlık durumu söyler: “Oturumunuz sona ermek üzere”.
    • Düğmeler eylemi söyler: “Oturumu uzat”, “Çıkış yap”.
    • Süre “1 dk 40 sn” biçimindedir; ondalık veya saat biçimi kullanmayın.
    • Duyuru metni: “Oturumunuz 1 dk 0 sn sonra sona erecek”.
    • Kod açıklaması: “Telefonunuza gönderilen 6 haneli kodu girin”.
    • Hatalar: “Kod 6 haneli olmalı”, “Kod hatalı. 2 deneme hakkınız kaldı.”, “Kodun süresi doldu. Yeni kod isteyin.”
    Öğe Hareket Preset
    Uyarı penceresi Bileşenin kendi açılış ve kapanış hareketi yerleşik
    Süre çubuğu Dolgu bileşenin kendi hareketiyle küçülür yerleşik
    Toast, InlineAlert Bileşenin kendi giriş hareketi yerleşik
    Düğmeler Basınca 0,97 ölçek motion.press

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

    Dialog, ProgressBar, ResultState, Toast, InlineAlert, Button, Stack, Text, TextInput. İlgili kalıp: Formlar ve doğrulama.

    Sıradaki bileşen: OtpInput aynı tek girdinin üzerinde bölümlü kutular çizer.