Make mobile filters a keyboard-accessible modal drawer

This commit is contained in:
2026-09-06 09:56:34 +03:00
parent 3550f1a9bc
commit 5895f60b28
4 changed files with 55 additions and 24 deletions
@@ -0,0 +1,11 @@
{
"escape": {
"active": "Filtreler & Hızlı Erişim",
"openDialogs": 0
},
"desktop": {
"openDialogs": 0
},
"beforeResize": 1,
"afterResize": 0
}
+6
View File
@@ -395,3 +395,9 @@ In-app tarayıcı viewport özelliğiyle 390×844, 768×1024 ve 1440×1000 denen
Son ölçüm: mobil 384/384, tablet 762/762, masaüstü 1434/1434 (içerik/kullanılabilir genişlik; scrollbar 6 px). Mobil belge inceleme bölgesi 244/244, iki metin alanı ve kaydet/analiz düğmeleri viewport içinde. Normal viewport geri yüklendi. Kanıt: evidence/qa-responsive-widths.json. Son ölçüm: mobil 384/384, tablet 762/762, masaüstü 1434/1434 (içerik/kullanılabilir genişlik; scrollbar 6 px). Mobil belge inceleme bölgesi 244/244, iki metin alanı ve kaydet/analiz düğmeleri viewport içinde. Normal viewport geri yüklendi. Kanıt: evidence/qa-responsive-widths.json.
80 test ve üretim derlemesi geçti. Bu test ana liste, üst çubuk ve belge inceleme formundaki taşmayı kapsar; tüm modüller, gerçek dokunmatik cihaz, ekran okuyucu ve bütün klavye akışları kabul edilmiş sayılmaz. 80 test ve üretim derlemesi geçti. Bu test ana liste, üst çubuk ve belge inceleme formundaki taşmayı kapsar; tüm modüller, gerçek dokunmatik cihaz, ekran okuyucu ve bütün klavye akışları kabul edilmiş sayılmaz.
### Mobil filtre dialog ve odak davranışı
Mobil filtre çekmecesi native dialog/showModal kullanıyor: adlandırılmış dialog, açılışta kapat düğmesine odak, Escape/kapat sonrası tetikleyiciye dönüş, arka sayfa kaydırma kilidi ve masaüstüne geçince otomatik kapanma eklendi. OKAS ürün arama penceresi açılırken drawer kapanıyor; native dialog üst katmanının diğer pencereyi örtmesi önleniyor.
Gerçek 390 px tarayıcı testinde dialog 337/337 px genişlikle taşmadı, ilk odak “Filtreleri kapat” oldu. Escape sonrası açık dialog sayısı 0 ve odak “Filtreler & Hızlı Erişim” düğmesiydi. Açık çekmecede 1440 px'e geçiş 1 açık dialogdan 0'a geçti. Kanıt: evidence/qa-mobile-filter-keyboard.json. 80 test ve üretim derlemesi geçti. OKAS pencere geçişinin gerçek sonuçla tarayıcı kabulü, tüm filtre kombinasyonları ve ekran okuyucu testi açık.
+2 -1
View File
@@ -6,7 +6,7 @@ import {
Bot, PlusCircle, Trash2, ChevronDown, ChevronUp, Filter, MapPin, Building2, Tag, Star, Hash Bot, PlusCircle, Trash2, ChevronDown, ChevronUp, Filter, MapPin, Building2, Tag, Star, Hash
} from 'lucide-react'; } from 'lucide-react';
export const LeftSidebar = () => { export const LeftSidebar = ({onOpenProductSearch} = {}) => {
const { const {
favoriteKurumlar, favoriteKurumlar,
quickFilter, quickFilter,
@@ -49,6 +49,7 @@ export const LeftSidebar = () => {
return () => { iptal = true; clearTimeout(z); }; return () => { iptal = true; clearTimeout(z); };
}, [okasQ]); }, [okasQ]);
const okasIleAra = (k) => { const okasIleAra = (k) => {
onOpenProductSearch?.();
setUrunAramaBaslangic({ okasKod: k.kod, okasAd: k.ad }); setUrunAramaBaslangic({ okasKod: k.kod, okasAd: k.ad });
setIsUrunAramaOpen(true); setIsUrunAramaOpen(true);
}; };
+35 -22
View File
@@ -1,35 +1,48 @@
import React, { useState } from 'react'; import React, { useEffect, useId, useRef, useState } from 'react';
import { SlidersHorizontal, X } from 'lucide-react'; import { SlidersHorizontal, X } from 'lucide-react';
import { LeftSidebar } from './LeftSidebar'; import { LeftSidebar } from './LeftSidebar';
/**
* Mobil filtre çekmecesi: küçük ekranda sol menü listeyi aşağı itiyordu
* (72 satırlık panel). Artık lg altında gizli; "Filtreler" düğmesiyle
* soldan açılan çekmecede aynı bileşen çalışır.
*/
export const MobilFiltreCekmecesi = () => { export const MobilFiltreCekmecesi = () => {
const [acik, setAcik] = useState(false); const [acik, setAcik] = useState(false);
const dialog = useRef(null);
const trigger = useRef(null);
const titleId = useId();
useEffect(() => {
if (!acik) return;
const element = dialog.current;
element.showModal();
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const desktop = window.matchMedia('(min-width: 1024px)');
const closeOnDesktop = () => { if (desktop.matches) setAcik(false); };
desktop.addEventListener('change', closeOnDesktop);
closeOnDesktop();
return () => {
desktop.removeEventListener('change', closeOnDesktop);
element.close();
document.body.style.overflow = previousOverflow;
if (trigger.current?.getClientRects().length) trigger.current.focus();
};
}, [acik]);
return ( return (
<div className="lg:hidden"> <div className="lg:hidden">
<button <button ref={trigger} type="button" onClick={() => setAcik(true)} aria-haspopup="dialog" aria-expanded={acik}
type="button" className="w-full mb-3 bg-white border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold text-slate-800 flex items-center justify-center gap-2 shadow-xs">
onClick={() => setAcik(true)} <SlidersHorizontal className="w-4 h-4 text-blue-600" /> Filtreler &amp; Hızlı Erişim
className="w-full mb-3 bg-white border border-slate-200 rounded-xl px-4 py-2.5 text-xs font-bold text-slate-800 flex items-center justify-center gap-2 shadow-xs"
>
<SlidersHorizontal className="w-4 h-4 text-blue-600" /> Filtreler & Hızlı Erişim
</button> </button>
{acik && ( {acik && <dialog ref={dialog} aria-labelledby={titleId} onCancel={() => setAcik(false)} onClose={() => setAcik(false)}
<div className="fixed inset-0 z-50 flex" onClick={() => setAcik(false)}> onClick={event => {
<div className="absolute inset-0 bg-slate-900/50" /> if (event.target !== event.currentTarget) return;
<div onClick={(e) => e.stopPropagation()} className="relative h-full w-[88%] max-w-sm bg-slate-100 shadow-2xl overflow-y-auto p-3 animate-fade-in"> const box = event.currentTarget.getBoundingClientRect();
if (event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom) setAcik(false);
}}
className="fixed inset-y-0 left-0 right-auto m-0 h-dvh max-h-none w-[88%] max-w-sm border-0 bg-slate-100 shadow-2xl overflow-y-auto p-3 backdrop:bg-slate-900/50">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-xs font-black text-slate-800 uppercase tracking-wider">Filtreler</span> <h2 id={titleId} className="text-xs font-black text-slate-800 uppercase tracking-wider">Filtreler</h2>
<button onClick={() => setAcik(false)} className="p-1.5 rounded-lg bg-white border border-slate-200" aria-label="Kapat"><X className="w-4 h-4" /></button> <button autoFocus onClick={() => setAcik(false)} className="p-2.5 rounded-lg bg-white border border-slate-200" aria-label="Filtreleri kapat"><X className="w-4 h-4" /></button>
</div> </div>
<LeftSidebar /> <LeftSidebar onOpenProductSearch={() => setAcik(false)} />
</div> </dialog>}
</div>
)}
</div> </div>
); );
}; };