Popup'ınız tam ekranda neden kayboluyor
· 5 dk okuma
Test eden kişi, grid tam ekrandayken sağ tık menüsünün açılmadığını söyledi.
Açılıyordu. Düğüm DOM'daydı, gerçek bir sınırlayıcı kutusu vardı, olayları
alıyordu ve tetikleyicideki aria-expanded değişiyordu. Menüde yanlış olan
hiçbir şey yoktu. Sadece görünmüyordu.
O testte bildirilen beş sorunun üçü tek bir sebepten geliyordu, ve düzeltmek altından daha kötü bir hata çıkardı. Mekanizma, düzeltme ve bir DOM düğümünü taşımanın gerçek bedeli aşağıda.
z-index burada işe yaramaz
requestFullscreen() elemanı top layer'a yükseltir — viewport'u kaplayan
ve normal belge akışındaki her şeyin üstünde duran bir çizim katmanı. Bundan
iki sonuç çıkar ve ikisi de kesindir:
- z-index sınırı geçemez. Normal akıştaki bir z-index, elemanları yalnız
o akışın içinde sıralar. Top layer'a karşı hiçbir şey yapmaz.
2147483647, tam ekrandaki bir elemana1ile tamamen aynı şekilde yenilir. - Arada bir backdrop vardır. Her top layer elemanının, kendisiyle belgenin
geri kalanı arasına çizilen bir
::backdropsözde-elemanı olur ve tam ekran için tarayıcının kendi stil dosyası o backdrop'ı opak siyah yapar.
Yani menü hiçbir zaman çizilmemiş değildi. Doğru şekilde çizilmişti — hiçbir yığınlama değerinin üzerinden uzanamayacağı siyah bir örtünün altında.
Bunu yapan tek API tam ekran değil. dialog.showModal() ve popover ile
gösterilen elemanlar da aynı katmana yükseltilir, ve o katmandakiler kendi
aralarında eklenme sırasına göre yığılır. Bu ayrıntı yazının sonunda işimize
yarayacak.
Tekrar üretmek için on satır
const stage = document.getElementById('stage');
const menu = document.createElement('div');
menu.textContent = 'I am open. You cannot see me.';
menu.style.cssText =
'position:fixed;top:20px;left:20px;z-index:2147483647;background:#fff;padding:8px';
stage.addEventListener('click', async () => {
await stage.requestFullscreen();
document.body.appendChild(menu);
console.log(menu.isConnected, menu.getBoundingClientRect().width);
// true, and a real width. Still nothing on screen.
});
Bütün ölçüm araçları elemanın sağlam olduğunu söylüyor; bu hatayı pahalı yapan
şey de bu. isConnected true. Sınırlayıcı kutuda gerçek sayılar var. İsabet
testi çalışıyor, dolayısıyla klavye odağı ve tıklama işleyicileri de çalışıyor.
Bozuk olan tek şey, konsolun raporlayamadığı tek şey.
Düzeltme yığınlama değil, yerleşim
Yığınlama sınırın öbür tarafına uzanamıyorsa, popup'ın kendisi öbür tarafta olmalı — yani tam ekranda ne varsa onun içinde:
const parent = document.fullscreenElement ?? document.body;
parent.appendChild(menu);
Bizdeki hâli üç adımda çözülüyor, çünkü tarayıcı requestFullscreen() çağrısını
reddedebiliyor ve o durum için CSS ile taklit edilen bir tam ekran yolumuz var:
export function portalParent(): HTMLElement {
const d = document as Document & { webkitFullscreenElement?: Element | null };
const native = (d.fullscreenElement ?? d.webkitFullscreenElement ?? null) as HTMLElement | null;
if (native) return native;
return document.querySelector<HTMLElement>('.pivot-is-fullscreen') ?? document.body;
}
Grid'in dışına bir şey mount eden beş yer var: tek seferlik popup katmanı, alan
seçici, içe aktarma penceresine ait iki host, ve lisanssız kullanım rozeti.
Hepsi document.body'ye ekliyordu, dolayısıyla hepsi tam ekranda görünmezdi.
İtiraf etmeye değer: React renderer'ımız bunu zaten biliyordu. Grid tam ekrana geçtiğinde portal hedefini container'a çeviriyor, ve DOM renderer'ının planında açık popup'ların yeniden ebeveynlenmesi gerektiği yazıyordu. Yazılmamıştı. Plandaki bir not, düzeltme değildir.
Toggle'dan uzun yaşayan popup'lar
Popup'ı açılırken doğru yere koymak işin yarısı. Kullanıcı bir pencere zaten açıkken tam ekrana geçebilir, ve bir saniye önce doğru yerde olan pencere artık sınırın yanlış tarafındadır:
document.addEventListener('fullscreenchange', () => {
const parent = document.fullscreenElement ?? document.body;
if (menu.parentElement !== parent) parent.appendChild(menu);
});
Geçici popup'lara yalnız yerleşim kuralı yeter. Uzun ömürlü yüzeyler — açık bırakılmış bir alan seçici, bir durum rozeti — dinleyiciye ihtiyaç duyar, ve iki yönde: tam ekrandan çıkmak da onları aynı kesinlikle ortada bırakır.
Düzeltmenin altından çıkan hata
Yerleşim düzelince tam ekranda sağ tık bir menü üretti. Bir de grid'in tamamının üstünde beyaz bir örtü üretti.
Popup host'umuz, temanın CSS özel özelliklerini miras alsın diye grid kökünün
inline style'ını kopyalıyor. Bu makul bir şey ve aylardır çalışıyordu. CSS ile
taklit edilen tam ekranın aynı köke yazdığı şey ise şu:
position: fixed;
inset: 0;
z-index: 9990;
Yani host tema değişkenlerini ve tam viewport'luk bir yerleşimi kopyalıyordu; kökün sınıfını da taşıdığı için kökün arka planını da boyuyordu. Her popup, köşesinde küçük bir menü olan, sayfa boyutunda opak bir panele dönüştü. Menü görünüyordu. Geri kalan her şey yok olmuştu.
Çıkarılacak ders tek satır: style'ı toptan kopyalamak yalnız değişkenleri
değil, yerleşimi de kopyalar. Host artık kopyadan sonra position, inset,
z-index, width ve height özelliklerini siliyor. Değişkenler kalıyor;
kökün yerleşimi kökün işi olarak kalıyor.
Bu hata ilk düzeltmeden önce de vardı ve görülemezdi — popup'ın sayfayı örttüğünü fark edebilmek için önce popup'ın görünür hâle gelmesi gerekiyordu.
Bir düğümü taşımanın bedeli
Yeniden ebeveynleme bedava değil. Bir elemanı taşımak yaşam döngüsünü yeniden
çalıştırır: içindeki <iframe> yeniden yüklenir, CSS geçişleri ve animasyonları
baştan başlar, odaklanmış bir alt eleman odağını kaybedebilir. Oynayan bir
<video> devam eder, ama yeni oluşturulmuş olan yüklemeye baştan başlar.
Az önce açılmış bir menü için bunların hiçbiri önemli değil. Kendi durumu olan bir yüzey için önemli; bu kalıbı her yere uygulamadan önce neyi taşıdığına bak.
Seçmediğimiz yol
İkinci bir çıkış var ve hiçbir şeyi taşımayı gerektirmiyor. dialog.showModal()
ve popover elemanları aynı top layer'a yükselttiği ve o katmandakiler eklenme
sırasına göre yığıldığı için, bir eleman tam ekrana geçtikten sonra açılan
modal bir pencere onun üstüne çizilir.
Biz bu yolu seçmedik. Popup'larımızın CSS ile taklit edilen yolda da çalışması gerekiyor ve orada top layer hiç devrede değil; iki farklı başarısızlık biçimi olan iki mekanizma yerine, iki yolu da kapsayan tek bir yerleşim kuralı daha basitti. Bu bizim kısıtımız, tavsiye değil — yalnızca native API'ye ihtiyaç duyan bir uygulama için top layer daha küçük bir cevap.
Fullscreen API olmadan test etmek
jsdom Fullscreen API'sini uygulamıyor, yani sürülecek bir şey de gözlenecek bir şey de yok. Tek bir yerleşim kuralı için gerçek tarayıcı kurmak, küçük bir iddia uğruna çok fazla makine olurdu.
Onun yerine document.fullscreenElement'i stub'layıp düğümün nereye düştüğünü
iddia ediyoruz:
it('mounts inside the fullscreen element, so the popup is painted', () => {
fullscreen = host; // stub'lanmış getter'ın döndürdüğü şey
const portal = createPortalLayer(host);
const root = portal.openPopup(() => {});
expect(host.contains(root)).toBe(true);
});
İddianın kendisi zaten yerleşim. Popup tam ekran elemanının içindeyse tarayıcı
onu boyar; <body> üzerinde bir kardeşse boyamaz. Bu düzeltmeyle on yedi test
geldi ve her değişikliği tek tek geri aldığımızda ilgili test kırmızıya döndü —
bir testin gerçekten bir şeyi test ettiğini bilmenin tek yolu bu.
Bu değişiklik 2026-09-06 sürümüyle çıktı. Tam ekran denetiminin kendisi Özellikler sayfasında anlatılıyor.