From 5895f60b281e33bd380ce100e006bf105ec63e6a Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 09:56:34 +0300 Subject: [PATCH] Make mobile filters a keyboard-accessible modal drawer --- .../evidence/qa-mobile-filter-keyboard.json | 11 ++++ docs/quality/uygulama-durumu.md | 6 ++ src/components/LeftSidebar.jsx | 3 +- src/components/MobilFiltreCekmecesi.jsx | 59 +++++++++++-------- 4 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 docs/quality/evidence/qa-mobile-filter-keyboard.json diff --git a/docs/quality/evidence/qa-mobile-filter-keyboard.json b/docs/quality/evidence/qa-mobile-filter-keyboard.json new file mode 100644 index 0000000..8e7f0ac --- /dev/null +++ b/docs/quality/evidence/qa-mobile-filter-keyboard.json @@ -0,0 +1,11 @@ +{ + "escape": { + "active": "Filtreler & Hızlı Erişim", + "openDialogs": 0 + }, + "desktop": { + "openDialogs": 0 + }, + "beforeResize": 1, + "afterResize": 0 +} \ No newline at end of file diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index b7cc040..0dcde4a 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -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. diff --git a/src/components/LeftSidebar.jsx b/src/components/LeftSidebar.jsx index c8d20c2..b25ef13 100644 --- a/src/components/LeftSidebar.jsx +++ b/src/components/LeftSidebar.jsx @@ -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); }; diff --git a/src/components/MobilFiltreCekmecesi.jsx b/src/components/MobilFiltreCekmecesi.jsx index ca422be..730e237 100644 --- a/src/components/MobilFiltreCekmecesi.jsx +++ b/src/components/MobilFiltreCekmecesi.jsx @@ -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 (
- - {acik && ( -
setAcik(false)}> -
-
e.stopPropagation()} className="relative h-full w-[88%] max-w-sm bg-slate-100 shadow-2xl overflow-y-auto p-3 animate-fade-in"> -
- Filtreler - -
- -
+ {acik && 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"> +
+

Filtreler

+
- )} + setAcik(false)} /> +
}
); };