fix: isolate background interaction behind stacked modals
This commit is contained in:
@@ -1216,3 +1216,11 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
|
|||||||
- Global Escape dinleyicisi kaldırıldı; kapanışı odaktaki pencere yönetiyor. Soru işareti kısayolu başka işlemin engellediği olayları, komut kombinasyonlarını ve düzenlenebilir metin alanlarını açma/kapatma komutu olarak kullanmıyor. Firma profili mevcut ayrı odak döngüsü yerine aynı ortak kancaya geçti; istek iptali ve bileşen yaşam döngüsü korunuyor.
|
- Global Escape dinleyicisi kaldırıldı; kapanışı odaktaki pencere yönetiyor. Soru işareti kısayolu başka işlemin engellediği olayları, komut kombinasyonlarını ve düzenlenebilir metin alanlarını açma/kapatma komutu olarak kullanmıyor. Firma profili mevcut ayrı odak döngüsü yerine aynı ortak kancaya geçti; istek iptali ve bileşen yaşam döngüsü korunuyor.
|
||||||
- On gerçek tarayıcı kontrolü geçti: kılavuz ilk odağı, ileri/geri Tab ve açan düğmeye dönüş; profilde ilk odak/ileri/geri döngü; profilin üstünde ? ile kılavuz açma; ilk Escape ile yalnız kılavuzu kapatıp profile odak dönüşü; ikinci Escape ile profili kapatıp başlıktaki açan düğmeye dönüş. Kanıt `rapor/qa-guide-profile-focus.json`.
|
- On gerçek tarayıcı kontrolü geçti: kılavuz ilk odağı, ileri/geri Tab ve açan düğmeye dönüş; profilde ilk odak/ileri/geri döngü; profilin üstünde ? ile kılavuz açma; ilk Escape ile yalnız kılavuzu kapatıp profile odak dönüşü; ikinci Escape ile profili kapatıp başlıktaki açan düğmeye dönüş. Kanıt `rapor/qa-guide-profile-focus.json`.
|
||||||
- Build başarılı. Önceki 275 birim/entegrasyon testi sonucu geçerli; JSX odak bağlamaları için tarayıcı kabulü yapıldı, tüm paket tekrarlanmadı. Bu kabul ekran okuyucunun sanal imleci/arka planın inert olması veya bütün mobil kontrolleri kapsamaz. Genel plan açık.
|
- Build başarılı. Önceki 275 birim/entegrasyon testi sonucu geçerli; JSX odak bağlamaları için tarayıcı kabulü yapıldı, tüm paket tekrarlanmadı. Bu kabul ekran okuyucunun sanal imleci/arka planın inert olması veya bütün mobil kontrolleri kapsamaz. Genel plan açık.
|
||||||
|
|
||||||
|
|
||||||
|
### 2026-09-06 — Modal arka planını etkileşime kapatma
|
||||||
|
|
||||||
|
- Ortak pencere kancası yalnız Tab döngüsünü yönetiyordu; pencere dışındaki alanlar DOM üzerinde etkileşime açıktı. Açık pencereler sırasıyla izleniyor ve yalnız en üst pencereye giden DOM yolu dışında kalan kardeş alanlar inert yapılıyor. Önceki inert değerleri saklanıyor; üst pencere kapanınca alttaki pencerenin, son pencere kapanınca sayfanın durumu geri getiriliyor.
|
||||||
|
- Portal ile body altına açılan kılavuz da aynı sıraya katılıyor. Yeni DOM alanları eklendiğinde izolasyon yeniden uygulanıyor; son pencere kapanınca gözlemci kapatılıyor. Alttaki pencere kaldırılırken üst pencerenin odağı geri alınmıyor.
|
||||||
|
- Yedi tarayıcı kabulü geçti: ihale açıkken başlığın inert olması, analiz üstteyken ihale detayının inert olması, kılavuz üstteyken iki pencerenin inert olması, sırayla kapanışta doğru pencere/odağın geri gelmesi, tümü kapalıyken hiçbir artık inert alanın kalmaması ve ardından firma profilinin normal açılması. Kanıt `rapor/qa-modal-background-isolation.json`.
|
||||||
|
- 275 test ve build başarılı. Bu doğrulama tarayıcı DOM inert durumu ve gerçek klavye etkileşimini kapsar; belirli bir ekran okuyucuyla dinleme testi yapılmış değildir. Henüz ortak kancayı kullanmayan diğer pencerelerin kabulü ayrıca gereklidir. Genel plan açık.
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
import {useEffect,useRef} from 'react';
|
import {useEffect,useRef} from 'react';
|
||||||
|
|
||||||
|
// Only the top modal remains interactive, including when it is portalled to body.
|
||||||
|
const modalStack=[];
|
||||||
|
const previousInert=new Map();
|
||||||
|
let isolationObserver=null;
|
||||||
|
function isolateTopModal() {
|
||||||
|
for(const [element,inert] of previousInert)element.inert=inert;
|
||||||
|
previousInert.clear();
|
||||||
|
const top=modalStack.at(-1);
|
||||||
|
if(!top?.isConnected)return;
|
||||||
|
for(let branch=top;branch.parentElement;branch=branch.parentElement){
|
||||||
|
const parent=branch.parentElement;
|
||||||
|
for(const sibling of parent.children){
|
||||||
|
if(sibling===branch)continue;
|
||||||
|
previousInert.set(sibling,sibling.inert);
|
||||||
|
sibling.inert=true;
|
||||||
|
}
|
||||||
|
if(parent===document.body)break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function focusableElements(root) {
|
function focusableElements(root) {
|
||||||
return [...root.querySelectorAll('button, a[href], input, select, textarea, summary, [tabindex]')].filter(el=>{
|
return [...root.querySelectorAll('button, a[href], input, select, textarea, summary, [tabindex]')].filter(el=>{
|
||||||
if(el.tabIndex<0 || el.matches(':disabled') || !el.getClientRects().length || el.closest('[inert]'))return false;
|
if(el.tabIndex<0 || el.matches(':disabled') || !el.getClientRects().length || el.closest('[inert]'))return false;
|
||||||
@@ -18,8 +38,21 @@ export function useModalFocus(open,onClose) {
|
|||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(!open || !ref.current)return;
|
if(!open || !ref.current)return;
|
||||||
const opener=document.activeElement,root=ref.current;
|
const opener=document.activeElement,root=ref.current;
|
||||||
|
modalStack.push(root);
|
||||||
|
isolateTopModal();
|
||||||
|
if(!isolationObserver){
|
||||||
|
isolationObserver=new MutationObserver(isolateTopModal);
|
||||||
|
isolationObserver.observe(document.body,{childList:true,subtree:true});
|
||||||
|
}
|
||||||
(focusableElements(root)[0] || root).focus();
|
(focusableElements(root)[0] || root).focus();
|
||||||
return ()=>{if(opener?.isConnected)opener.focus();};
|
return ()=>{
|
||||||
|
const wasTop=modalStack.at(-1)===root;
|
||||||
|
const index=modalStack.indexOf(root);
|
||||||
|
if(index>=0)modalStack.splice(index,1);
|
||||||
|
isolateTopModal();
|
||||||
|
if(!modalStack.length){isolationObserver?.disconnect();isolationObserver=null;}
|
||||||
|
if(wasTop && opener?.isConnected && !opener.closest('[inert]'))opener.focus();
|
||||||
|
};
|
||||||
},[open]);
|
},[open]);
|
||||||
const onKeyDown=e=>{
|
const onKeyDown=e=>{
|
||||||
const root=ref.current;
|
const root=ref.current;
|
||||||
|
|||||||
Reference in New Issue
Block a user