fix: manage guide and profile modal keyboard focus

This commit is contained in:
2026-09-06 15:22:36 +03:00
parent 1d00f4fa91
commit abbf24078d
3 changed files with 23 additions and 17 deletions
+6 -12
View File
@@ -1,5 +1,6 @@
import {HealthPortfolioEditor} from './HealthPortfolioEditor';
import React,{useState,useEffect,useRef,useId} from 'react';
import {useModalFocus} from '../hooks/useModalFocus';
import {useApp} from '../context/AppContext';
import {getToken} from '../services/auth';
import {X,Loader2} from 'lucide-react';
@@ -13,10 +14,11 @@ function ProfileEditor(){
const [url,setUrl]=useState(userProfile.website || ''),[caps,setCaps]=useState(userProfile.scrapedCapabilities || {});
const [threshold,setThreshold]=useState(uyumEsigi),[busy,setBusy]=useState(''),[error,setError]=useState(null),[message,setMessage]=useState('');
const [entries,setEntries]=useState({});
const alive=useRef(true),controller=useRef(null),locked=useRef(false),root=useRef(null),titleId=useId();
const alive=useRef(true),controller=useRef(null),locked=useRef(false),titleId=useId();
const modalFocus=useModalFocus(true,()=>setIsCompanyScannerOpen(false));
useEffect(()=>{
alive.current=true;const opener=document.activeElement;root.current?.querySelector('button')?.focus();
return()=>{alive.current=false;controller.current?.abort();if(opener?.isConnected)opener.focus();};
alive.current=true;
return()=>{alive.current=false;controller.current?.abort();};
},[]);
const run=async(kind,operation)=>{
if(locked.current)return;locked.current=true;setBusy(kind);setError(null);setMessage('');
@@ -32,16 +34,8 @@ function ProfileEditor(){
if(alive.current)setMessage('Firma profili sunucuya kaydedildi. Eşleşmeler kayıtlı profilinizle güncellenir.');
});
const edit=(key,values)=>{setCaps(previous=>({...previous,[key]:values}));setMessage('Kaydedilmemiş değişiklikler var.');};
const keyDown=e=>{
if(e.key==='Escape'){e.preventDefault();e.stopPropagation();setIsCompanyScannerOpen(false);}
if(e.key!=='Tab')return;
const controls=[...root.current.querySelectorAll('button,input,select,textarea,a[href]')].filter(el=>!el.disabled&&el.getClientRects().length);
const first=controls[0],last=controls.at(-1);
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last?.focus();}
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first?.focus();}
};
return <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/70">
<section ref={root} role="dialog" aria-modal="true" aria-labelledby={titleId} onKeyDown={keyDown} className="bg-white rounded-2xl w-full max-w-3xl max-h-[92vh] overflow-y-auto p-5 space-y-4">
<section {...modalFocus} role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1} className="bg-white rounded-2xl w-full max-w-3xl max-h-[92vh] overflow-y-auto p-5 space-y-4">
<div className="flex justify-between gap-3"><h2 id={titleId} className="font-bold text-lg">Firma ürün ve faaliyet profili</h2><button aria-label="Firma profilini kapat" onClick={()=>setIsCompanyScannerOpen(false)}><X/></button></div>
<p className="text-sm text-slate-600">Ürünlerinizi elle ekleyebilir veya web sitenizden öneri alabilirsiniz. Tarama, ürün uygunluğunu ya da ÜTS/sertifika geçerliliğini doğrulamaz. Kaydetmeden kapatırsanız bu ekrandaki değişiklikler uygulanmaz.</p>
<form onSubmit={scan} className="flex gap-2 items-end flex-wrap">
+9 -5
View File
@@ -1,5 +1,6 @@
import { createPortal } from 'react-dom';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useId } from 'react';
import {useModalFocus} from '../hooks/useModalFocus';
import { useApp } from '../context/AppContext';
import { HelpCircle, X, BookOpen, Lightbulb, ListChecks } from 'lucide-react';
@@ -79,10 +80,12 @@ const GENEL = {
export const KullanimKilavuzu = () => {
const { activeTab } = useApp();
const [acik, setAcik] = useState(false);
const titleId=useId();
const modalFocus=useModalFocus(acik,()=>setAcik(false));
useEffect(() => {
const h = (e) => {
if (e.key === '?' && !/input|textarea|select/i.test(e.target?.tagName || '')) setAcik((o) => !o);
if (e.key === 'Escape') setAcik(false);
if(e.defaultPrevented || e.ctrlKey || e.altKey || e.metaKey || e.target?.isContentEditable)return;
if (e.key === '?' && !/input|textarea|select/i.test(e.target?.tagName || '')) {e.preventDefault();setAcik((o) => !o);}
};
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
@@ -113,6 +116,7 @@ export const KullanimKilavuzu = () => {
<div className="fixed inset-0 z-50 flex justify-end" onClick={() => setAcik(false)}>
<div className="absolute inset-0 bg-slate-900/40" />
<aside
{...modalFocus} role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1}
onClick={(e) => e.stopPropagation()}
className="relative h-full w-full max-w-md bg-white shadow-2xl flex flex-col animate-fade-in"
>
@@ -120,11 +124,11 @@ export const KullanimKilavuzu = () => {
<div className="flex items-center gap-2">
<BookOpen className="w-5 h-5 text-amber-400" />
<div>
<p className="text-[10px] uppercase tracking-wider text-slate-400 font-bold">Kullanım kılavuzu</p>
<p id={titleId} className="text-[10px] uppercase tracking-wider text-slate-400 font-bold">Kullanım kılavuzu</p>
<h3 className="font-extrabold text-sm">{k.baslik}</h3>
</div>
</div>
<button onClick={() => setAcik(false)} className="text-slate-400 hover:text-white p-1" aria-label="Kapat"><X className="w-5 h-5" /></button>
<button onClick={() => setAcik(false)} className="text-slate-400 hover:text-white p-1" aria-label="Kullanım kılavuzunu kapat"><X className="w-5 h-5" /></button>
</div>
<div className="flex-1 overflow-y-auto p-5 space-y-5">
<p className="text-xs text-slate-600">Üst menüde Keşfet ilanları ve takvimi, Hazırlan dosyaları ve hesap araçlarını, Pazar sonuçları ve analizleri, Yönetim hesap ve bağlantı ayarlarını açar. AI model ayarları yalnız yöneticilere görünür.</p>