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.
|
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.
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 & 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user