fix: support keyboard navigation in tender details

This commit is contained in:
2026-09-06 09:12:07 +03:00
parent 2aaf56ce8b
commit c3264149ea
4 changed files with 48 additions and 6 deletions
@@ -0,0 +1,8 @@
Yerel sentetik QA: başlık düğmesinde Enter ile açma, dialog adı, Escape ile kapatma ve odağın geri dönmesi
{
"dialogCount": 0,
"focusTag": "BUTTON",
"focusText": "QA — Giresun laboratuvar",
"hasPopup": "dialog"
}
Ek gözlemler: İlk odak Listeye Geri Dön; Shift+Tab son Google/Outlook düğmesine; son düğmede Tab ilk düğmeye dönüyor.
+7
View File
@@ -177,3 +177,10 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont
- Parçalı analizde şema doğrulaması birleştirmeden önce yapılıyor. Null kalem/alıntı gibi hatalı bir parça tüm birleştirmeyi çökertmiyor; başarısız kaynak aralığı olarak korunuyor. Hiçbir geçerli parça yoksa analiz başarısız oluyor.
- Kanıt: 54/54 test geçti. Yeni regresyonlar hatalı nested alanlar, metin olarak false, eksik miktar/zorunluluk, bozuk parça ve tamamen başarısız belgeyi kapsıyor. Mevcut test örneklerinden iki olumlu fixture kaynak miktarı/zorunluluk bilgisi içerecek şekilde tamamlandı.
- Sınır: Bu şema doğrulaması anlam veya mevzuat doğruluğunu kanıtlamaz. Gerçek sağlayıcı yanıtları, kaynak belgeyle semantik çelişki kontrolü ve uzun işlerin kalıcı devam ettirilmesi hâlâ açık.
### D09 — İhale detayında klavye erişimi
- Ana listedeki ihale başlığı gerçek düğme oldu; Enter/Space ile detay açılabiliyor, görünür klavye odağı ve dialog işareti var.
- Detay penceresine dialog rolü, başlığıyla erişilebilir ad, kapatma düğmesine ad, başlangıç odağı, Tab/Shift+Tab döngüsü ve Escape ile kapanış eklendi. Kapanışta hâlâ DOM’da bulunan açan kontrole odak dönüyor. İç içe başka dialogdan gelen olaylar alt pencereyi kapatmıyor.
- Tarayıcı kanıtı: `evidence/qa-detail-keyboard.txt`. Yerel sentetik Giresun kaydı başlık düğmesinden Enter ile açıldı; ilk/son kontrol arasında ileri-geri döngü ve Escape sonrası başlık düğmesine dönüş doğrulandı. Sağdaki yaklaşan ihale düğmesinden açılış/dönüş de geçti.
- Üretim derlemesi geçti; mevcut paket boyutu uyarıları sürüyor. Bu kontrol bütün modallerin erişilebilirliğini, ekran okuyucu deneyimini veya gerçek mobil genişlikleri doğrulamaz. Arka planın inert yönetimi ve üst üste farklı modallerin tüm kombinasyonları ayrıca ele alınmalı.
+3 -3
View File
@@ -45,9 +45,9 @@ export const TenderCard = ({ tender }) => {
{/* Top Header Row */}
<div>
<div className="flex items-start justify-between gap-3">
<h3
<h3><button type="button" aria-haspopup="dialog"
onClick={() => setActiveTenderDetail(tender)}
className="font-bold text-sm sm:text-base text-slate-900 hover:text-blue-600 cursor-pointer leading-snug tracking-tight"
className="text-left focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2 rounded font-bold text-sm sm:text-base text-slate-900 hover:text-blue-600 cursor-pointer leading-snug tracking-tight"
>
{tender.baslik}
{tender.isYeni && (
@@ -55,7 +55,7 @@ export const TenderCard = ({ tender }) => {
YENİ
</span>
)}
</h3>
</button></h3>
{/* Bookmark Star Button */}
<button
+30 -3
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef, useId } from 'react';
import { useApp } from '../context/AppContext';
import { BidPackageBuilder } from './BidPackageBuilder';
import { TenderDocuments } from './TenderDocuments';
@@ -14,6 +14,32 @@ import { createTenderICS } from '../services/tenderCalendar';
export const TenderDetailModal = () => {
const { activeTenderDetail, setActiveTenderDetail, toggleTrackTender, userProfile, setIsAiSpecAnalyzerOpen, gorevAc, gorevKontrol, aiAnalysisResults, tenders, mutateTender, savingCount, saveError, authUser } = useApp();
const dialogRef = useRef(null);
const dialogTitleId = useId();
const isOpen = Boolean(activeTenderDetail);
useEffect(() => {
if (!isOpen) return;
const opener = document.activeElement;
const root = dialogRef.current;
root?.querySelector('button')?.focus();
return () => { if (opener?.isConnected) opener.focus(); };
}, [isOpen]);
const handleDialogKey = e => {
if (e.defaultPrevented || e.target.closest('[role="dialog"]') !== dialogRef.current) return;
if (e.key === 'Escape') {
e.preventDefault(); e.stopPropagation(); setActiveTenderDetail(null); return;
}
if (e.key !== 'Tab') return;
const focusable = [...dialogRef.current.querySelectorAll('button, a[href], input, select, textarea, [tabindex]')]
.filter(el => el.tabIndex >= 0 && !el.disabled && el.getClientRects().length && !el.closest('[inert]'));
const first = focusable[0], last = focusable.at(-1);
if (!first) { e.preventDefault(); dialogRef.current.focus(); }
else if (e.shiftKey && (document.activeElement === first || document.activeElement === dialogRef.current)) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
};
const [activeTab, setActiveTab] = useState('details'); // 'details', 'pdf_stamp', 'ai_assistant', 'checklist', 'kurum_tenders'
const noteOwner = authUser?.email || '';
const [noteDrafts, setNoteDrafts] = useState(() => ({ owner: noteOwner, drafts: {} }));
@@ -76,7 +102,7 @@ export const TenderDetailModal = () => {
return (
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-xs flex justify-center p-2 sm:p-4 animate-fade-in">
<div className="bg-white w-full max-w-6xl rounded-2xl shadow-2xl border border-slate-200 overflow-hidden my-auto flex flex-col max-h-[92vh]">
<div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby={dialogTitleId} tabIndex={-1} onKeyDown={handleDialogKey} className="bg-white w-full max-w-6xl rounded-2xl shadow-2xl border border-slate-200 overflow-hidden my-auto flex flex-col max-h-[92vh]">
{(actionError || saveError) && <p role="alert" className="p-3 bg-red-50 text-red-900">{actionError || saveError}</p>}
{savingCount > 0 && <p role="status" className="p-2 bg-blue-50">Kaydediliyor…</p>}
@@ -123,6 +149,7 @@ export const TenderDetailModal = () => {
)}
<button
onClick={() => setActiveTenderDetail(null)}
aria-label="İhale detayını kapat"
className="text-slate-400 hover:text-white p-1"
>
<X className="w-6 h-6" />
@@ -150,7 +177,7 @@ export const TenderDetailModal = () => {
<span className="text-xs text-slate-500 ml-auto">Ekleme: {tender.eklenmeZamani}</span>
</div>
<h1 className="text-xl sm:text-2xl font-black text-slate-900 leading-snug">
<h1 id={dialogTitleId} className="text-xl sm:text-2xl font-black text-slate-900 leading-snug">
{tender.baslik}
</h1>
</div>