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.
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
} from 'lucide-react';
export const LeftSidebar = () => {
export const LeftSidebar = ({onOpenProductSearch} = {}) => {
const {
favoriteKurumlar,
quickFilter,
@@ -49,6 +49,7 @@ export const LeftSidebar = () => {
return () => { iptal = true; clearTimeout(z); };
}, [okasQ]);
const okasIleAra = (k) => {
onOpenProductSearch?.();
setUrunAramaBaslangic({ okasKod: k.kod, okasAd: k.ad });
setIsUrunAramaOpen(true);
};
+36 -23
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 { 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 = () => {
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 (
<div className="lg:hidden">
<button
type="button"
onClick={() => setAcik(true)}
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 ref={trigger} type="button" onClick={() => setAcik(true)} aria-haspopup="dialog" aria-expanded={acik}
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 &amp; Hızlı Erişim
</button>
{acik && (
<div className="fixed inset-0 z-50 flex" onClick={() => setAcik(false)}>
<div className="absolute inset-0 bg-slate-900/50" />
<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">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-black text-slate-800 uppercase tracking-wider">Filtreler</span>
<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>
</div>
<LeftSidebar />
</div>
{acik && <dialog ref={dialog} aria-labelledby={titleId} onCancel={() => setAcik(false)} onClose={() => setAcik(false)}
onClick={event => {
if (event.target !== event.currentTarget) return;
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">
<h2 id={titleId} className="text-xs font-black text-slate-800 uppercase tracking-wider">Filtreler</h2>
<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>
)}
<LeftSidebar onOpenProductSearch={() => setAcik(false)} />
</dialog>}
</div>
);
};