Açılır menü oluşturma

Açılır menü oluşturma

Bu kılavuzda; açılır liste, bağlam, taşma ve bunları açan menü çubukları gibi tüm pop-up menüler ele alınmaktadır. Örnek olarak, öğelerin arayüzü değiştirdiği (oluşturma, kaydetme, tercihler) bir uygulama komut menüsü verilmiştir.

İki bağımsız seçenek. Buradaki diğer her şey her ikisi için de geçerlidir.

  • Öğe başına öğe. Komut, <button type="button">; hedef ise yeni sekmede açılan, URL olarak kopyalanan ve komut dosyası olmadan çalışan bir <a href>. Açılır listede bunlar serbestçe karıştırılabilir: İki bağlantı ve altı komut veya bir komut ve altı bağlantı, aşağıda hiçbir şeyi değiştirmez.
  • Etkileşim modeli, açılır liste başına. Ok tuşları, Home/End ve Esc, bunu bir menü haline getirir: focusgroup'u ekleyin, düğme veya bağlantı fark etmeksizin her öğede menuitem'yi çıkarmasını sağlayın ve bir menü gibi görünmesini sağlayın (sürekli bir çubuk veya açılır liste göstergesi olan bir tetikleyici; aşağıdaki "Menü çubuğu veya ayrı menü düğmeleri" bölümüne bakın). Kullanıcıların Tab için kullandığı kısa bir bağlantı listesi gezinme menüsüdür: Odak grubu, role="menu", role="menuitem" veya aria-haspopup="menu" yoktur. Popover, Esc ve odak geri yüklemeyi koruyun ve gezinme çubuğu kılavuzuna bakın.

İçerik hiçbir zaman rolleri belirlemez. Bağlantıların yer aldığı "Ürünler" açılır listesi menü olabilir. Komutların yer aldığı "Hesap" açılır listesi gezinme öğesi olamaz.

Menü kalıbını yalnızca tek bir sekme durağı ve ok tuşuyla gezinme özelliğine sahip bileşik widget'lar için kullanın. HTML <menu> öğesini değil, <div focusgroup="menu nomemory"> kullanın. Paylaşılan işaretlemeye role="menu" veya role="menuitem" eklemeyin: Yerel odak grubu bu rolleri tahmin eder ve açık roller bu tahmini engeller. Bunları yalnızca aşağıdaki desteklenmeyen tarayıcı yedeğine ekleyin. nomemory, yeniden açma işleminin ilk öğeden başlamasını sağlar. Focusgroup, odağı taşır. Komut dosyası ise önizleme, etkinleştirme ve odak geri yükleme işlemlerini gerçekleştirir.

Menüyü açma ve konumlandırma

commandfor ve command="toggle-popover" ile her tetikleyiciyi bir komut çağırıcı yapın. Tarayıcı; tıklama, Enter ve boşluk tuşuna basıldığında popover'ı açıp kapatır ve tetikleyiciyi popover'ın hafif kapatma grubunda tutar. Bu nedenle, açık bir tetikleyiciyi tıklamak tetikleyiciyi kapatır. popovertarget veya popovertargetaction kullanmayın. Bu kalıp bunun yerine commandfor kullanır.

Her popover'ı tetikleyicisinin hemen sonrasına yerleştirin. Tarayıcılar, yalnızca popover bir sonraki kardeş öğe olmadığında örtük bir aria-details ilişkisi ekler. Bu nedenle, bu yerleşim ek özellikler olmadan bu durumdan kaçınır.

<!-- Command invoker: the browser toggles the popover on click, Enter, and Space -->
<button id="file-trigger" class="trigger" type="button" aria-haspopup="menu" aria-expanded="false"
  commandfor="file-menu" command="toggle-popover">
  File
</button>
<!-- Immediately after its trigger, so browsers add no implicit aria-details -->
<div id="file-menu" popover="auto">
  <!-- Nested, not on the popover: Chromium otherwise drops focusgroup keyboard behavior -->
  <div focusgroup="menu nomemory" aria-labelledby="file-trigger">
    <button type="button">New file</button>
    <button type="button">Save</button>
    <button id="export-trigger" type="button" aria-haspopup="menu" aria-expanded="false"
      commandfor="export-menu" command="toggle-popover">
      Export <span aria-hidden="true">›</span>
    </button>
    <!-- Submenu: popover="auto" directly after its trigger, inside the parent focusgroup -->
    <div id="export-menu" popover="auto">
      <div focusgroup="menu nomemory" aria-labelledby="export-trigger">
        <button type="button">PDF</button>
        <button type="button">Plain Text</button>
      </div>
    </div>
  </div>
</div>

Her menüyü, tetikleyicisini gösteren aria-labelledby ile adlandırın. Dekoratif alt menü göstergelerini CSS'de tutun veya aria-hidden="true" ekleyin. Kullanıcıların hangi menünün açık olduğunu veya önizlendiğini anlayabilmesi için aria-expanded="true" içeren tetikleyicilere görünür bir etkin stil verin.

Üst düzey tetikleyiciler için bir form seçin. Rol, klavye modeli ve stil eşleşmelidir: Görsel olarak ayrı düğmeler her biri bir Tab duraklamayı işaret eder.

  • Menü çubuğu. role="menubar", tetikleyiciler arasında boşluk olmayan tek bir sürekli çubuk olarak tasarlanır. Bir Tab durak; ArrowLeft/ArrowRight tetikleyiciler arasında geçiş yapar; fareyle üzerine gelindiğinde menüler açılır. Uygulama komut çubuğu için kullanılır.
  • Ayrı menü düğmeleri. Her tetikleyici kendi Tab durağıdır ve bağımsız bir düğme gibi görünür. role="menubar" yok, tetikleyiciler arasında ok tuşları yok, odaklanıldığında önizleme yok, fareyle üzerine gelindiğinde geçiş yok. Gezinme işlemi yapılıyorsa <nav> içine alın, aksi takdirde rol eklemeyin. Araç çubukları ve sayfa başlıkları için kullanılır.

Bu kılavuzun geri kalanında menü çubuğu formu açıklanmaktadır. Ayrı düğmeler için her şeyi her menünün içinde tutun ve menü çubuğu düzeyindeki davranışı bırakın.

CSS Anchor Positioning ile menüyü konumlandırma

JavaScript koordinatları yerine CSS sabitleme konumlandırmasını kullanın. Hem command="toggle-popover" hem de showPopover({ source: trigger }) örtülü bir sabitleyici ayarladığından menü başına anchor-name veya position-anchor kuralları eklemeyin. Menü davranışı, sabitleyici konumlandırmaya bağlı olmamalıdır.

/* Top-level menus sit below their trigger. The implicit anchor comes from the invoker or source. */
[popover] {
  position-area: block-end span-inline-end;
  inset: auto;
}

/* A popover nested in another popover is a submenu: beside its parent item, flipping when it would overflow. */
[popover] [popover] {
  position-area: inline-end span-block-end;
  position-try-fallbacks: flip-inline, flip-block, --below;
}

/* Last resort for narrow viewports: directly below the trigger, indented, so the submenu never
   covers its trigger. */
@position-try --below {
  position-area: block-end span-inline-end;
  margin-inline-start: 1rem;
  /* Fit the area: the submenu's minimum width must not stop this fallback from fitting. */
  min-inline-size: 0;
  max-inline-size: calc(100% - 1rem);
}

--below seçeneğini en sona bırakın. Böylece yalnızca her iki taraf da uymadığında uygulanır. Doğrudan tetikleyicinin altında açılır. Bu nedenle, dar bir ekranda tetikleyicinin altındaki kardeş öğeleri kapsar. Bu, tetikleyicinin yanında kalmak için kabul edilen bir dengelemedir. Ayrıca alt menünün alana (min-inline-size: 0, max-inline-size eksi girinti) sığacak şekilde küçülmesine izin verin. Uymayan bir yedek atlanır ve hiçbiri uymuyorsa tarayıcı temel yerleşimi korur. Bu nedenle, birkaç piksel çok geniş olan bir alt menü, tetikleyicisini sessizce kapsar.

Düz bir popover'da çevirme yedeklerinin işleyiş şekli için Görüntü Alanına Duyarlı Açılır Listeler başlıklı makaleyi inceleyin. Bu kılavuzda bir menü değil, düğme grubu yerleştirilir. Menüler için alt menü yerleşimi ve yukarıdaki --below yedek önceliklidir.

Açma ve kapatma animasyonu

İsteğe bağlıdır. 150 ms'nin altında, opacity ve yalnızca küçük bir translate, alt menüler için de aynı, @media (prefers-reduced-motion: no-preference) içinde. Odak toggle üzerinde hareket eder. Bu nedenle geçiş, klavye kullanıcılarını hiçbir zaman geciktirmez.

@media (prefers-reduced-motion: no-preference) {
  /* Closing: this rule is in effect once :popover-open is removed. */
  [popover] {
    opacity: 0;
    translate: 0 -.25rem;
    transition: opacity .15s, translate .15s, display .15s allow-discrete, overlay .15s allow-discrete;
  }
  /* Opening: faster. */
  [popover]:popover-open {
    opacity: 1;
    translate: 0 0;
    transition-duration: .1s;
    @starting-style { opacity: 0; translate: 0 -.25rem; }
  }
  /* Switching between open menubar menus is instant, as in native menubars. */
  [role="menubar"]:has(> [popover]:popover-open) > [popover] {
    transition: none;
  }
}

@starting-style, [popover]'in tekrarı değildir: Kapalı bir popover display: none'dir ve geçişler oluşturulmamış bir durumdan çalışmaz. Bu nedenle, açık durum için önceki değerleri sağlar. Bu izin verilmezse açma işlemi aniden gerçekleşir ve yalnızca kapatma işlemi animasyonlu olur. display ve overlay, allow-discrete ile birlikte, popover'ın kapatma geçişi sona erene kadar oluşturulmasını ve en üst katmanda kalmasını sağlar.

Klavye ve odak

Bir focusgroup içine ikinci bir roving-tabindex sistemi eklemeyin. Senaryo yalnızca çağırıcıların ve odak grubunun yapmadığı şeyleri kapsar:

  • Menü çubuğu için bir sekme durağı. Tab, ilk üst düzey tetikleyicide girer. ArrowLeft ve ArrowRight, üst düzey tetikleyiciler arasında gezinir ve her iki uçta da kaydırılır. Tetikleyicideki Home ve End, menüsüne ilk veya son öğeden girer.
    • Not: Tab hangi tetikleyiciye döner? APG, ilk tetikleyicinin veya kullanılan son tetikleyicinin kullanılmasına izin verir. Kılavuzda ilk seçenek belirtiliyor ancak demoda sekme durağı ArrowLeft ve ArrowRight ile taşınıyor. Bu nedenle, klavyeyle gezinme işleminden sonra Shift+Tab, ulaşılan son tetikleyiciye geri dönüyor. İşaretçi kullanımı, işaretçiyi hareket ettirmez. Bu nedenle, aynı sıra farklı bir tetikleyiciye denk gelebilir. İki davranış da kabul edilebilir. Birini seçin ve tutarlı bir şekilde kullanın.
  • Önizleme Üst düzey bir tetikleyiciye odaklanıldığında menüsü gösterilir ve aria-expanded="true" ayarlanır. Odak, tetikleyicide kalır ve hiçbir öğeye odaklanılmaz. Tetikleyiciyi bulanıklaştırmak, yalnızca önizleme menüsünü kapatır.
  • Önizlemede Enter ve boşluk tuşuna basma Önizlemeyi kapatır ve tetikleyiciye odaklanmaya devam ederler. Yerel toggle-popover işlemi bunu işlediğinden komut dosyası bu tuşları engellememelidir.
  • Önizlemede aşağı ok tuşuna basma Açık bir menüye dönüştürür ve ilk öğeye odaklanır.
  • Açık veya önizlenen bir tetikleyiciyi işaretçiyle tıklama. Menüye girmeden doğrudan kapatır.
    • Alt menü tetikleyicilerinde bilinen fark. Alt menü tetikleyicisinin işaretçiyle tıklanması da yerel olarak açma/kapatma işlemi yapar. Bu nedenle, fareyle üzerine gelindiğinde önizlenen bir alt menüyü kapatabilir veya henüz görünmeyen bir alt menüyü açabilir. Yerel menüler yalnızca açılır ve hiçbir zaman kapanmaz. Bu zararsız bir durum olduğu için rehber bunu kabul eder.
  • Kapalı bir tetikleyiciyi işaretçiyle tıklama Dosyayı yerel olarak açar. Odak, tetikleyicide kalır. Hiçbir öğeye odaklanılmaz. Menüye yalnızca klavye açma işlemleri girilir.
  • İşaretçi bir öğenin üzerindeyken. Halka olmadan odaklanır. Bu nedenle, fareyle üzerine gelme ve odaklanma tek bir öğedir.
  • Bir menü açıkken işaretçiyi başka bir üst düzey tetikleyicinin üzerine getirme. Kapatır ve fareyle üzerine gelineni önizler, tetikleyicisine halka olmadan odaklanır. Açık bir şey yok: Fareyle üzerine gelindiğinde hiçbir şey olmaz.
  • İşaretçi, alt menü tetikleyicisinin üzerinde. Kısa bir gecikmeden sonra alt menüsünü önizler (işaretçi odağı, aynı odaklanıldığında önizleme işlemini çalıştırır). Kardeş öğe, öğeyi kapatır.
  • Menü içinde. ArrowUp ve ArrowDown, etkin öğeler arasında geçiş yapar. Home ve End, komut dosyasının işlediği ilk ve son etkin öğeye gider.
  • Açık bir üst düzey menüde sol ok veya sağ ok tuşuna basın. Menüyü kapatır ve tetikleyicisine odaklanarak bitişik menüyü önizler. ArrowDown ardından bu kodu girer. Sol ve Sağ tuşları hiçbir zaman bir menüye gitmez.
  • Alt menü tetikleyicisinde sağ ok tuşu. Alt menüyü açar ve ilk öğesine odaklanır.
  • Alt menü öğesi üzerinde sağ ok tuşuna basıldığında alt menü açılmaz. Hiyerarşiyi kapatır ve odak tetikleyicisinde kalırken bir sonraki üst düzey menüyü önizler. ArrowDown, ardından bu kodu girer.
  • Alt menüde sol ok veya Escape tuşu Yalnızca ilgili alt menüyü kapatır ve odağı tetikleyicisine geri yükler. Ebeveyn menüsü açık kalır.
  • Bir komut üzerinde Enter veya Boşluk tuşuna basın. Öğeyi etkinleştirir ve tüm açık menüleri kapatır.
  • Sekme Menü sisteminden çıkar. Odağı yakalamayın veya her komutu adım adım uygulamayın. Kapatma işlemi, odağı gizli içerikte bırakmamalıdır.

command etkinliği, toggle-popover menüyü açmadan önce tetiklenir. Bu nedenle, odak menüye taşınamaz. Bunun yerine odağı toggle etkinliğine taşıyın. HTML, odağı yalnızca bir yığındaki ilk popover="auto" için geri yükler. Bu nedenle, iç içe yerleştirilmiş düzeylerin odağı tetikleyicilerine açıkça geri yüklemesi gerekir.

Etkileşimi uygulama

Kendi işaretlemeniz için yukarıdaki davranışları uygulayın. Ok, Home/End, sarmalama ve çıkış için Tab tuşları normal keydown kodudur. Hata yapılması kolay olan birkaç bölüm, aşağıda giriş türüne göre gruplandırılmıştır.

Klavye ve odak yönetimi

  1. Tek bir açık yol. Klavyeyle açma işlemi trigger.click() yöntemini çağırır. Bu nedenle, menü yalnızca çağırıcının toggle-popover yöntemiyle açılabilir. Menüyü işaretleyerek toggle işleyicisinin menüye girmesini sağlayın. Yalnızca klavye açılır. İşaretçi açıldığında toggle, tetikleyiciye halka olmadan odaklanır. Bunun nedeni, pointerdown odağı engellediği için ok tuşlarının çalışmaya devam etmesi gerekmesidir.
  2. Odaklanıldığında önizleme. Tetikleyiciye odaklanıldığında showPopover({ source: trigger }) çağrılır ve menü bu öğeye sabitlenir. Tetikleyicinin kendi menüsünden veya başka bir pencereden dönen odağı yoksay (blur penceresinde işaretle, focus penceresinden sonra bir onay işaretini temizle veya uygulama değiştirme son menüyü yeniden açar). Odaklanılmadığı sürece blur üzerinde önizlenen menüyü gizle.
  3. Odağı command değil, toggle içinde hareket ettirin. command, menü açılmadan önce tetiklenir.
  4. Odağı tetikleyiciye kendiniz geri yükleyin. Tetikleyiciyi hidePopover() tarihinden önce odaklayın. İşaretçi etkinleştirildikten sonra odak, halka olmadan geri yükleniyor (focus({ focusVisible: false })) veya tetikleyici, fare tıklamasından sonra klavye odağı çerçevesini gösteriyor; klavye etkinleştirmeleri halkayı koruyor. event.detail === 0 ile yapılan bir tıklama klavyeden geldi.
  5. Seviyeleri tek tek kapatın. Bir alt menüde Escape ve ArrowLeft için stopPropagation() yöntemini çağırın. Aksi takdirde üst menünün dinleyicisi de alt menüyü kapatır.

İşaretçi işleme

  1. İşaretçi tıklama açma/kapatma düğmeleri. Tıklama odağı taşımayacak ve tıklama sırasında önizleme başlatmayacak şekilde pointerdown üzerinde preventDefault() yöntemini çağırın. Hangi tetiğe basıldığını hatırlayın. Kapalı veya önizlenen bir tetikleyiciyi tıkladığınızda tetikleyici yerel olarak açılır veya kapanır ve hiçbir zaman menüye girmez. Menüye yalnızca klavye ile açıldığında girilir. Tıklamaya müdahale etmeyin.
  2. Fareyle üzerine gelme, odağı takip eder. :hover ve :focus-visible öğelerini ayrı ayrı stillendirin. Bir öğenin üzerinde pointermove olduğunda focus({ focusVisible: false }) yöntemini çağırın. Seçeneği yok sayan tarayıcılar, işaretçi odağı için halka çizmez.
  3. Fareyle üzerine gelme, açık menüleri değiştirir. Başka bir üst düzey menü açıkken üst düzey bir tetikleyicinin pointerenter: Menüyü gizleyin, üzerine gelinen tetikleyiciye halka olmadan odaklanın ve odaklanıldığında önizleme (yukarıya bakın) önizlemesini gösterin. Açık öğe yok: Hiçbir şey yapmayın.
  4. Güvenli üçgen. Sabit bir CSS köprüsü çok dardır: Yalnızca işaretçi üst öğenin kenarına yakın bir yerde başladığında çalışır. CSS'nin köprüyü çizmesine izin verin ve işaretçiyi bildirmek için yalnızca komut dosyasını kullanın:
    • Bunu yalnızca matchMedia('(hover: hover)').matches olduğunda çalıştırın. Dokunma ve klavye ile alakalı değildir.
    • İşaretçi, alt menüsü açık olan bir alt menü tetikleyicisinden ayrıldığında başlar. Yan kenarı olmayan --below alt menüsünü atlayın.
    • --bridge-x ve --bridge-y (işaretçi, alt menüye göre piksel cinsinden), --bridge-edge (alt menünün yakın kenarı x: işaretçi sol taraftayken 0, çevrildiğinde genişliği) ve alt menüde --bridge-height değerlerini ayarlayın ve köprüyü açmak için data-bridge değerini ayarlayın.
    • İşaretçi alt menüdeki bir öğeye ulaştığında veya alt menü kapandığında yaklaşık 300 ms sonra durun. Dinleyiciyi ve özelliği kaldırın. Bundan sonra hiçbir şey çalıştırılmaz.
    • CSS, alt menüye işaretçiden yakın kenar köşelerine kadar bir üçgen şeklinde kırpılmış bir ::before verir. İşaretçinin yolundaki kardeş öğelerin üzerinde yer alır. Bu nedenle, alt menüyü kapatacak fareyle üzerine gelme işlemi hiçbir zaman gerçekleşmez. clip-path, isabet testini de sınırlandırır. Böylece ebeveyn menüsünün geri kalanı etkileşimli kalır.
    • Alt menü için overflow: visible gerekir (popover varsayılan olarak kırpılır) ve sözde öğe bunun ötesine uzanır. Bu nedenle, şeklin noktalarını bu ölçüde kaydırın.
    • Takip: Eğri bir yol (shape() eğri komutları), gerçek işaretçi yaylarını düz bir üçgenden daha iyi takip eder.
[popover] [popover] {
  overflow: visible;

  &[data-bridge]::before {
    content: "";
    position: absolute;
    inset: -100vh -100vw; /* Room for the triangle, which can extend far outside the submenu. */
    clip-path: shape(
      from calc(100vw + var(--bridge-x)) calc(100vh + var(--bridge-y)),
      line to calc(100vw + var(--bridge-edge)) 100vh,
      line to calc(100vw + var(--bridge-edge)) calc(100vh + var(--bridge-height)),
      close
    );
  }
}
const canHover = matchMedia('(hover: hover)');

// Call on pointerleave of a submenu trigger, with that trigger's submenu.
function startBridge(submenu, event) {
  if (!canHover.matches || !submenu.matches(':popover-open')) return;
  const rect = submenu.getBoundingClientRect();
  if (event.clientX >= rect.left && event.clientX <= rect.right) return; // --below: no side edge.
  const style = submenu.style;
  style.setProperty('--bridge-edge', `${event.clientX < rect.left ? 0 : rect.width}px`);
  style.setProperty('--bridge-height', `${rect.height}px`);
  const update = ({ clientX, clientY, target }) => {
    if (target !== submenu && submenu.contains(target)) return stop(); // Reached an item.
    style.setProperty('--bridge-x', `${clientX - rect.left}px`);
    style.setProperty('--bridge-y', `${clientY - rect.top}px`);
  };
  const timer = setTimeout(() => stop(), 300);
  const stop = () => {
    clearTimeout(timer);
    document.removeEventListener('pointermove', update);
    submenu.removeEventListener('toggle', stop);
    submenu.removeAttribute('data-bridge');
  };
  update(event);
  submenu.dataset.bridge = '';
  document.addEventListener('pointermove', update);
  submenu.addEventListener('toggle', stop, { once: true });
}

"Fareyle üzerine gelme odağı takip eder" (2. öğe) düz bir pointermove işleyici olarak kalır: Üçgen mantığına gerek yoktur.

Ayrıca:

  • Odak dışı. Odak, menü sisteminin dışına çıktığında açık olan tüm menüleri kapatın (document focusin dinleyicisi).
  • Komut etkinleştirme. Duyurun, ardından kök menüyü gizleyin ve odağı tetikleyicisine geri yükleyin. Etkinleştirme bir işaretçiden geldiyse halka olmadan yapın. İşaretlenebilir öğeler (menuitemcheckbox, menuitemradio) için, değişen onay işaretinin görünür olması amacıyla kapatmadan önce yaklaşık 300 ms bekleyin. Kapalı bir tetikleyiciyi yalnızca :focus-visible üzerinde etkin olacak şekilde biçimlendirin. Aksi takdirde, geri yüklenen (halkasız) odak, işaretçi seçiminden sonra tetikleyiciyi vurgulanmış olarak bırakır.
  • aria-expanded. Bu özelliği yalnızca toggle dinleyicisinde ayarlayın. Önizleme ve açık durumları kapsar. Menüsü önizlenirken veya açıkken tetikleyicinin aria-expanded değeri "true" olarak kalır.
  • Yardımcılar: Aşağıdaki yedekler, işaretlemeniz için tanımladığınız yardımcıları varsayar: triggers (tüm [commandfor] düğmeleri), menuFor(trigger), isOpen(menu), closeAndRestore(menu, { fromPointer }) (tetikleyiciye odaklanın, fromPointer olduğunda halka olmadan, ardından menu.hidePopover()), enterOnOpen (açıldıktan sonra girilecek menülerin WeakSet) ve pointerTrigger (bir işaretçi tarafından en son basılan tetikleyici).
  • Alt menüler için popover="auto" kullanın. Bu öğe, üst öğenin focusgroup öğesi içindeki alt menü tetikleyicisinden sonra yerleştirilir. Hiçbir zaman popover="manual".
  • İç içe yerleştirme, tarayıcının üst menüyü açık tutmasına, eş düzey alt menüleri kapatmasına, kök menü kapandığında alt menüleri kapatmasına ve dışarı tıklandığında kapatma işlemini gerçekleştirmesine olanak tanır.
  • Alt menü tetikleyicisi aria-haspopup="menu" ve aria-expanded değerlerini korur; toggle dinleyicisi aria-expanded değerini senkronize tutar.
  • Klavye komut dosyası, iç içe yerleştirilmiş Escape ve ArrowLeft öğelerini yukarıda açıklandığı şekilde işler.
  • Alt menü tetikleyicisi hiçbir zaman bir komut da çalıştırmaz: Bir kontrol hem etkinleştirip hem açamaz ve Enter her ikisi anlamına gelemez. Varsayılan işlemi ilk alt menü öğesi olarak yerleştirin veya tetikleyicinin yanına ayrı bir komut öğesi ekleyin. Birleşik bir kontrol kullanılması gerekiyorsa bunu erişilebilirlik ödünü olarak belgeleyin.

Ayırıcılar ve işaretlenebilir öğeler

  • Odak grubundaki öğe gruplarını <hr> ile ayırın: Yerel bir separator, odaklanılamaz, odak grubu ve yedek tarafından atlanır. Stil uygulanmış <div> öğesi yok.
  • Boole seçeneği, aria-checked ve açık bir role="menuitemcheckbox" içeren bir <button type="button"> değeridir. Focusgroup bu rolleri tahmin etmez. Bu nedenle, paylaşılan işaretlemenin bir öğe rolü ayarladığı tek yer burasıdır.
  • Bir gruptaki öğelerden biri, aynı odak grubunda aria-label içeren bir role="group" içinde role="menuitemradio" ve aria-checked içeren bir <button type="button">.
  • Komut dosyası açma/kapatma düğmeleri aria-checked.
  • Onay kutusunda Enter tuşuna basıldığında menü açılır/kapanır ve kapanır; Space tuşuna basıldığında menü açılır/kapanır ve açık kalır (APG). Radyolar her iki tuşla da kapatılabilir.
  • Onay işaretini ayrı bir simge öğesi olarak değil, [aria-checked="true"]::before kullanarak oluşturun (aşağıdaki CSS'ye bakın).

Burada ele alınmayan konular

  • Etkinleştirme geri bildirimi. Yerel menüler, kapanmadan önce seçilen öğeyi yanıp söndürür. İstenirse hidePopover() öncesinde yaklaşık 100 ms'lik görsel bir onay gösterin. Odak geri yüklemeyi veya duyuruyu geciktirmeyin.
  • İleriye dönük yazma. Focusgroup'ta yok. Gerekirse yazdırılabilir keydown üzerinde mevcut seviyenin etkin öğe etiketlerini eşleştirin ve mevcut öğeden sonraki eşleşmeye odaklanın. Space engellenmez.

İşlemi tamamlamadan önce doğrulama

Talimatlar, davranışları garanti edemez. Bu nedenle sonucu test edin. Sayfayı açın ve gerçek klavye ve fareyle her birini kontrol edin. Başarısız olanları düzeltip tekrar test edin:

  • Klavye. Tab ilk tetikleyiciye bir kez gelir; ArrowDown girer; Escape bir seviyeyi kapatır ve odağı tetikleyicisine döndürür; Tab her menüyü kapatır.
  • İşaretçi açık. Tetikleyiciyi tıkladığınızda tetikleyiciye odaklanmış ve halka içermeyen menüsü açılır. Tekrar tıkladığınızda menü kapanır.
  • İşaretçi komutu. Bir komut tıklandığında tüm menüler kapanır ve tetikleyicide odak halkası gösterilmez.
  • Alt menü. Alt menü tetikleyicisinin ortasından bir alt menü öğesine çapraz olarak hareket etmek alt menüyü açık tutar. Alakasız bir kardeş öğeye geçmek ise alt menüyü kapatır.
  • Dar ekran. 375 pikselde hiçbir alt menü kendi tetikleyicisini kapatmıyor.
  • Onay işaretleri. İşaretli öğelerde onay işareti gösterilir.
  • Yemekler israf edilmez. Konsol hatası yok ve tetikleyicilerde popovertarget, popovertargetaction veya aria-details yok.

Menü öğesi içeriğini hizalama

Bir satırda simge, etiket ve kısayol veya alt menü göstergesi varsa simge ya da onay işareti için sabit boyutlu bir ön alan ayırın ve sondaki içeriği satır sonuna itin. Simgelerin boyutu bellidir. Bu nedenle, sabit bir alan kullanıldığında öğenin simgesi olup olmamasından bağımsız olarak etiketler hizalı kalır. Satırlar için subgrid kullanmayın: Ek satır içi alt öğeler (ör. rozet) kendi ızgara öğesi haline gelir ve düzeni bozar. Yalnızca öğeyi zaten çalıştıran bir tuşu tekrarladığında kısayol metnini yardımcı teknolojiden gizleyin.

[focusgroup~="menu"] {
  --menu-icon-size: 1.25rem;
}

/* Target items only. A closed submenu popover inside the focusgroup must keep display: none. */
[focusgroup~="menu"] > button,
[focusgroup~="menu"] > [role="group"] > button {
  display: flex;
  align-items: center;
  gap: .5rem;
}

/* The leading slot holds the icon or check mark. */
[focusgroup~="menu"] button::before {
  content: "";
  flex: none;
  inline-size: var(--menu-icon-size);
}

/* Check mark for checked items. This selector must be at least as specific as the base
   ::before rule above, or the empty content wins and the check mark never shows. */
[focusgroup~="menu"] button[aria-checked="true"]::before {
  content: "✓";
}

/* Shortcut or submenu indicator at the inline end. */
[focusgroup~="menu"] .hint {
  margin-inline-start: auto;
}

Düğmenin içindeki bir <span class="hint" aria-hidden="true"> öğesine sondaki göstergeyi veya kısayolu yerleştirin. Alt menü tetikleyicisi, pop-up'ını zaten duyurur.

Progresif geliştirme ve yedekler

Focusgroup için yedekler ve tarayıcı desteği

İşaretlemede focusgroup="menu nomemory" değerini koruyun. 'focusGroup' in HTMLElement.prototype yanlışsa Yukarı, Aşağı, Ana Sayfa ve Son tuşlarını yükleyin, her menü kapsayıcısında role="menu" ve rolü olmayan düğmelerde role="menuitem" değerini ayarlayın; işaretlenebilir öğeleri, <hr> ve role="group" değerlerini olduğu gibi bırakın. Yerel focusgroup mevcutsa asla yüklemeyin. Sol, sağ, önizleme, açma ve kapatma, her iki durumda da yazar komut dosyasıdır.

if (!('focusGroup' in HTMLElement.prototype)) {
  document.querySelectorAll('[focusgroup~="menu"]').forEach(group => {
    group.setAttribute('role', 'menu');
    group.querySelectorAll('button:not([role])').forEach(item => {
      item.setAttribute('role', 'menuitem');
    });
    group.addEventListener('keydown', event => {
      if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
      // Own items only: a nested submenu has its own focusgroup, a role="group" wrapper does not.
      const items = [...group.querySelectorAll('button')]
        .filter(item => item.closest('[focusgroup]') === group)
        .filter(item => !item.disabled && item.getAttribute('aria-disabled') !== 'true');
      const current = items.indexOf(document.activeElement);
      if (current < 0) return;
      const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 :
        (current + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
      event.preventDefault();
      event.stopImmediatePropagation();
      items[next].focus({ preventScroll: true });
    }, true);
  });
}

Popover için yedekler ve tarayıcı desteği

Popover API çoğunlukla progresif geliştirme ile ilgilidir ancak tanımlayıcı davranışlarının (üst katman tanıtımı, hafif kapatma ve popovertarget çağrısı) yalnızca CSS'ye dayalı bir eşdeğeri yoktur. Eski tarayıcılar için polyfill veya polyfill kullanmak istemiyorsanız manuel yedek gerekir.

Polyfill: Eski tarayıcılarda popover özelliğini desteklemek için @oddbird/popover-polyfill öğesini koşullu olarak yükleyin. ZORUNLU: HTMLElement.prototype üzerinde popover özelliğini kontrol ederek özelliği tespit edin ve polyfill'i yalnızca yerel destek eksik olduğunda yükleyin. Koşulsuz olarak YÜKLEMEYİN.

Paketleyici veya içe aktarma eşlemesiyle:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

Paketleyici olmadan, <script type="module"> içindeki bir CDN'den içe aktarın:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Stil uyarısı: Polyfill, gerçek :popover-open sözde sınıfını tanımlayamadığı için bunun yerine .\:popover-open sınıfını uygular. ZORUNLU: İkisini :is() veya :where() ile birleştirin. Aksi takdirde, :popover-open içermeyen tarayıcılar kuralın tamamını siler:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

Alternatif olarak, polyfill içermeyen eski bir yedek için position: fixed kullanın ve koordinatları getBoundingClientRect() aracılığıyla manuel olarak hesaplayın veya kullanım alanına uygunsa inset: auto ile varsayılan konumlandırmayı kullanın.

Sabit konumlandırma için yedekler ve tarayıcı desteği

CSS Anchor Positioning olmadan popover, kullanıcı aracısının ortalanmış görüntü alanı yerleşimine geri döner. Menü için bunu asla kullanmayın: Menüyü tetikleyicisinden ayırır ve ortalanmış bir alt menü, üst menüsünü kaplar. Sabitlenmiş kuralları @supports (position-area: block-end) arkasına alın ve başarısız olduğunda menüyü komut dosyasından konumlandırın:

  • Menüyü, tetikleyicisinin altına (üst düzey) veya yanına (alt menü) yerleştirin. Menü, taşma durumunda diğer tarafa geçer ve görüntü alanına sabitlenir.
  • Menü açıldığında toggle işleyicisinde çalıştırın ve sonucu menüde left ve top olarak uygulayın. Koordinatların görüntü alanına göre olması için position: fixed öğesini inset: auto ve margin: 0 ile kullanın.
  • Bu geçersiz kılmaları @supports not (position-area: block-end) içinde tutun. Böylece, sabit konumlandırmaya sahip tarayıcılar etkilenmez.

showPopover({ source }) öğesinin örtük bir bağlantı oluşturmadığı (ToggleEvent.prototype üzerinde source yok) önceki uygulamaları hedefliyorsanız menü başına CSS kuralları yazmak yerine her tetikleyiciye benzersiz bir anchor-name ve her menüye komut dosyasından eşleşen bir position-anchor verin.

Bağlantı noktası konumlandırması olmayan tarayıcılar için polyfill rotası hakkında bilgi edinmek üzere Görüntü alanına duyarlı açılır listeler başlıklı makaleyi inceleyin.

Invoker komutları için yedekler ve tarayıcı desteği

Çağırıcılar olmadan command="toggle-popover" hiçbir şey yapmaz. Bu nedenle komut dosyasının menüyü açıp kapatması gerekir.

  • Örneği değil, arayüzü özellik algılamasıyla kontrol edin: 'commandForElement' in HTMLButtonElement.prototype.
  • Her tetikleyicide click, preventDefault() işlevini çağırın. Böylece, çağırıcıları olan bir tarayıcı iki kez açılıp kapatılamaz.
  • Menü açıksa closeAndRestore(menu) ile kapatın. Aksi takdirde menu.showPopover({ source: trigger }) numarasını arayın.
  • Tıklama, basılan tetikleyiciden (pointerTrigger) gelmediği sürece enterOnOpen içindeki menüyü işaretleyin. Böylece yalnızca klavye açıldığında menüye girilir.
  • Enter ve Space, düğmelerin tıklanmasını sağladığı için çalışmaya devam eder.