Make mobile filters a keyboard-accessible modal drawer
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"escape": {
|
||||
"active": "Filtreler & Hızlı Erişim",
|
||||
"openDialogs": 0
|
||||
},
|
||||
"desktop": {
|
||||
"openDialogs": 0
|
||||
},
|
||||
"beforeResize": 1,
|
||||
"afterResize": 0
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 & 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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user