Tworzenie menu
Ten przewodnik dotyczy wszystkich wyskakujących menu: menu rozwijanych, menu kontekstowych, rozszerzonych menu i pasków menu, które je otwierają. Przykładowy przypadek to menu poleceń aplikacji, w którym elementy zmieniają interfejs (tworzenie, zapisywanie, ustawienia).
Polecenia, linki i elementy menu
Dwa niezależne wybory. Pozostałe informacje dotyczą obu tych opcji.
- Element, za sztukę. Polecenie to
<button type="button">, a miejsce docelowe to<a href>, który nadal otwiera się w nowej karcie, kopiuje się jako adres URL i działa bez skryptu. Menu może zawierać dowolną kombinację: 2 linki i 6 poleceń lub 1 polecenie i 6 linków – nie ma to wpływu na poniższe informacje. - Model interakcji, według menu. Klawisze strzałek,
Home/EndiEscsprawiają, że jest to menu: dodaj grupę zaznaczenia, pozwól jej wywnioskowaćmenuitemdla każdego elementu, czy jest to przycisk, czy link, i spraw, aby wyglądał jak jeden (ciągły pasek lub aktywator ze wskaźnikiem menu; patrz „Pasek menu lub oddzielne przyciski menu” poniżej). Krótka lista linków, które użytkownicyTab, to nawigacja: brak grupy zaznaczenia, brakrole="menu",role="menuitem"lubaria-haspopup="menu"; zachowaj wyskakujące okienko,Esci przywracanie zaznaczenia oraz zapoznaj się z przewodnikiem po pasku nawigacyjnym.
Treści nigdy nie decydują o rolach. Menu może być menu rozwijanym „Produkty” z linkami, ale menu rozwijane „Konto” z poleceniami nie może być menu nawigacyjnym.
Wzorca menu używaj tylko w przypadku widżetu złożonego z 1 punktem tabulacji i nawigacją za pomocą klawiszy strzałek. Użyj <div focusgroup="menu nomemory">, a nie elementu HTML <menu>. Nie dodawaj role="menu" ani role="menuitem" w udostępnionych znacznikach: natywna grupa zaznaczenia wnioskuje te role, a jawne role blokują to wnioskowanie. Dodaj je tylko w elemencie zastępczym dla nieobsługiwanej przeglądarki poniżej. nomemory powoduje, że ponowne otwarcie rozpoczyna się od pierwszego elementu. Grupa zaznaczenia przenosi zaznaczenie, a skrypt obsługuje podgląd, aktywację i przywracanie zaznaczenia.
Otwieranie i ustawianie menu
Ustaw każdy aktywator jako wywołujący polecenie za pomocą commandfor i command="toggle-popover". Przeglądarka otwiera i zamyka wyskakujące okienko po kliknięciu, naciśnięciu klawisza Enter i spacji oraz utrzymuje aktywator w grupie wyskakujących okienek, które można zamknąć, klikając poza nimi. Dzięki temu kliknięcie otwartego aktywatora powoduje jego zamknięcie. Nie używaj popovertarget ani popovertargetaction. Ten wzorzec używa commandfor.
Umieść każdy element wyskakujący bezpośrednio po jego aktywatorze. Przeglądarki dodają domyślną relację aria-details tylko wtedy, gdy element popover nie jest następnym elementem nadrzędnym, więc takie umieszczenie pozwala uniknąć tego problemu bez dodatkowych atrybutów.
<!-- 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>
Nazwij każde menu za pomocą aria-labelledby wskazującego jego regułę. Zachowaj dekoracyjne wskaźniki podmenu w kodzie CSS lub dodaj aria-hidden="true". Nadaj aktywatorom z atrybutem aria-expanded="true" widoczny styl aktywny, aby użytkownicy mogli rozpoznać, które menu jest otwarte lub wyświetlane w podglądzie.
Pasek menu lub oddzielne przyciski menu
Wybierz jedną formę dla reguł najwyższego poziomu. Rola, model klawiatury i styl muszą być zgodne: wizualnie oddzielne przyciski oznaczają po jednym przystanku Tab.
- Pasek menu.
role="menubar"na kontenerze (nigdy na wyskakującym okienku), stylizowany na jeden ciągły pasek bez przerw między elementami wyzwalającymi.Tab– 1 przystanek;ArrowLeft/ArrowRight– przechodzenie między elementami aktywującymi; najechanie kursorem powoduje otwarcie menu. Użyj do paska poleceń aplikacji. - Oddzielne przyciski menu. Każdy aktywator jest osobnym punktem zatrzymania
Tabi wygląda jak samodzielny przycisk. Brakrole="menubar", brak klawiszy strzałek między aktywatorami, brak podglądu po zaznaczeniu, brak przełączania po najechaniu kursorem. Jeśli element służy do nawigacji, umieść go w elemencie<nav>. W przeciwnym razie nie przypisuj mu roli. Używaj w paskach narzędzi i nagłówkach stron.
W dalszej części tego przewodnika opisujemy pasek menu. W przypadku oddzielnych przycisków zachowaj wszystkie elementy w każdym menu i usuń zachowanie na poziomie paska menu.
Pozycjonowanie menu za pomocą pozycjonowania kotwicy CSS
Używaj pozycjonowania zakotwiczonego CSS, a nie współrzędnych JavaScriptu. Zarówno command="toggle-popover", jak i showPopover({ source: trigger }) ustawiają niejawne zakotwiczenie, więc nie dodawaj reguł anchor-name ani position-anchor dla poszczególnych menu. Działanie menu nie może zależeć od pozycjonowania kotwicy.
/* 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);
}
Pozostaw --below na końcu, aby ta reguła była stosowana tylko wtedy, gdy żadna ze stron nie pasuje. Otwiera się bezpośrednio pod elementem aktywującym, więc na wąskim ekranie zakrywa elementy podrzędne pod elementem aktywującym. Jest to akceptowalny kompromis, który pozwala na pozostanie obok elementu aktywującego. Zezwól też na zmniejszenie podmenu do obszaru (min-inline-size: 0, max-inline-size minus wcięcie). Kreacja zastępcza, która nie pasuje, jest pomijana, a jeśli żadna nie pasuje, przeglądarka zachowuje podstawowe miejsce docelowe, więc podmenu, które jest o kilka pikseli za szerokie, po cichu zakrywa swój aktywator.
Informacje o tym, jak działają zastępcze wyświetlane ścieżki w przypadku zwykłego wyskakującego okienka, znajdziesz w artykule Menu rozwijane dostosowane do widocznego obszaru. Ten przewodnik określa pozycję grupy przycisków, a nie menu. W przypadku menu pierwszeństwo mają umiejscowienie menu podrzędnego i powyższa kreacja zastępcza --below.
Animacja otwierania i zamykania
Opcjonalnie: Under 150 ms, opacity and a small translate only, same for submenus, inside @media (prefers-reduced-motion: no-preference). Zaznaczenie zostanie przeniesione na toggle, więc przejście nigdy nie opóźnia użytkowników klawiatury.
@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 nie jest powtórzeniem [popover]: zamknięty popover to display: none, a przejścia nie są uruchamiane ze stanu niewyrenderowanego, więc dostarcza on wartości przed otwarciem. Bez niego otwieranie jest gwałtowne, a animowane jest tylko zamykanie. display i overlay z allow-discrete utrzymują renderowanie wyskakującego okienka i górną warstwę do momentu zakończenia przejścia zamykania.
Klawiatura i fokus
Nie dodawaj drugiego systemu roving-tabindex w focusgroup. Skrypt obejmuje tylko to, czego nie robią wywołujący i grupa dyskusyjna:
- Jeden punkt tabulacji dla paska menu.
Tabwchodzi do pierwszego aktywatora najwyższego poziomu.ArrowLeftiArrowRightumożliwiają przechodzenie między aktywatorami najwyższego poziomu i zawijanie na obu końcach.HomeiEndna aktywatorze powodują wejście do menu na pierwszym lub ostatnim elemencie.- Uwaga: który aktywator zwraca wartość
Tab. W ramach APG można użyć pierwszego lub ostatniego wywołania. Przewodnik określa pierwszą opcję, ale w prezentacji tabulator jest przesuwany za pomocą klawiszyArrowLeftiArrowRight, więc po nawigacji za pomocą klawiaturyShift+Tabwraca do ostatniego osiągniętego aktywatora. Użycie wskaźnika nie powoduje jego przesunięcia, więc ta sama sekwencja może trafić na inny aktywator. Oba zachowania są akceptowalne. Wybierz jedno i stosuj je konsekwentnie.
- Uwaga: który aktywator zwraca wartość
- Podgląd. Ustawienie fokusu na aktywatorze najwyższego poziomu powoduje wyświetlenie jego menu i ustawienie
aria-expanded="true". Zaznaczenie pozostaje na aktywatorze i żaden element nie jest zaznaczony. Rozmycie aktywatora powoduje zamknięcie menu tylko do podglądu. - Enter i spacja w podglądzie. Zamykają podgląd i skupiają się na regule. Działanie natywne
toggle-popoverobsługuje to, więc skrypt nie może przechwytywać tych klawiszy. - Naciśnij strzałkę w dół na podglądzie. Przekształca go w otwarte menu i ustawia fokus na pierwszym elemencie.
- Kliknięcie otwartej lub wyświetlanej w podglądzie reguły. Zamyka go natywnie, nie wchodzi do menu.
- Znana różnica w wyzwalaczach menu podrzędnego. Kliknięcie wskaźnikiem myszy elementu aktywującego podmenu również powoduje przełączenie, więc może zamknąć podmenu, które zostało już wyświetlone po najechaniu kursorem, lub otworzyć podmenu, które jeszcze się nie pojawiło. Menu natywne tylko je otwierają i nigdy nie zamykają. Jest to nieszkodliwe, więc przewodnik to akceptuje.
- Kliknięcie zamkniętego aktywatora. Otwiera go natywnie. Fokus pozostaje na aktywatorze; żaden element nie jest zaznaczony. Otwiera się tylko klawiatura, a nie menu.
- Najedź kursorem na element. Zaznacza element bez pierścienia, więc najechanie kursorem i zaznaczenie to jeden element.
- Wskaźnik nad innym aktywatorem najwyższego poziomu, gdy menu jest otwarte. Zamyka go i wyświetla podgląd elementu, na który najechano kursorem, zaznaczając jego aktywator bez pierścienia. Nic nie jest otwarte: najechanie kursorem nic nie daje.
- Wskaźnik nad aktywatorem menu podrzędnego. wyświetla podgląd podmenu po krótkim opóźnieniu (w przypadku wskaźnika podgląd po zaznaczeniu działa tak samo). Zamyka go element równorzędny.
- W menu.
ArrowUpiArrowDownprzechodzą między włączonymi elementami.HomeiEndpowodują przejście do pierwszego i ostatniego włączonego elementu, który jest obsługiwany przez skrypt. - Strzałka w lewo lub Strzałka w prawo w otwartym menu najwyższego poziomu. Zamyka je i wyświetla podgląd sąsiedniego menu z zaznaczeniem jego aktywatora, zawijając na obu końcach.
ArrowDown, a następnie wpisuje go. Strzałki w lewo i w prawo nigdy nie powodują przejścia do menu. - Strzałka w prawo na aktywatorze menu podrzędnego. Otwiera podmenu i ustawia kursor na jego pierwszym elemencie.
- Naciśnięcie klawisza strzałki w prawo na elemencie menu podrzędnego, który nie ma podrzędnego menu podrzędnego. Zamyka hierarchię i wyświetla podgląd następnego menu najwyższego poziomu, podczas gdy fokus pozostaje na jego aktywatorze.
ArrowDown, a następnie wpisuje go. - Strzałka w lewo lub Escape w menu podrzędnym. Zamyka tylko to menu podrzędne i przywraca zaznaczenie jego aktywatora. Menu Rodzic pozostanie otwarte.
- Enter lub spacja na poleceniu. Aktywuje go i zamyka wszystkie otwarte menu.
- Tab. Opuszcza system menu. Nie blokuj fokusu ani nie przechodź przez każde polecenie. Zamknięcie nie może pozostawiać fokusu w ukrytych treściach.
Zdarzenie command jest wywoływane, zanim toggle-popover otworzy menu, więc nie można tam przenieść zaznaczenia. Zamiast tego przenieś zaznaczenie w zdarzeniu toggle. HTML przywraca fokus tylko dla pierwszego elementu popover="auto" w stosie, więc zagnieżdżone poziomy muszą jawnie przywracać fokus do elementu wyzwalającego.
Wdrażanie interakcji
Zaimplementuj powyższe zachowania we własnych znacznikach. Obsługa strzałek, Home/End, zawijania i Tab do wyjścia to zwykły kod keydown. Poniżej znajdziesz kilka elementów, które łatwo pomylić, pogrupowanych według typu danych wejściowych.
Obsługa klawiatury i fokusa
- Jedna otwarta ścieżka. Otwarcie za pomocą klawiatury wywołuje
trigger.click(), więctoggle-popoverelementu wywołującego to jedyny sposób na otwarcie menu. Oznacz menu, aby moduł obsługitogglemógł je otworzyć. Otworzy się tylko klawiatura. Po otwarciu wskaźnikatoggleustawia zaznaczenie na aktywatorze bez pierścienia, ponieważpointerdownuniemożliwiło zaznaczenie, a klawisze strzałek muszą nadal działać. - Podgląd po zaznaczeniu. Ustawienie fokusu na aktywatorze wywołuje funkcję
showPopover({ source: trigger }), dzięki czemu menu jest do niego zakotwiczone. Ignoruj fokus powracający z menu aktywatora lub z innego okna (flaga w oknieblur, odznaczona po okniefocuslub przełączenie aplikacji powoduje ponowne otwarcie ostatniego menu). Ukryj podgląd menu na urządzeniublur, chyba że zostanie ono zaznaczone. - Przenieś zaznaczenie do
toggle, a nie docommand.commanduruchamia się przed otwarciem menu. - Samodzielnie przywróć zaznaczenie elementu aktywującego. Ustaw ostrość na aktywatorze przed
hidePopover(). Po aktywacji wskaźnika przywróć zaznaczenie bez pierścienia (focus({ focusVisible: false })) lub po kliknięciu myszą aktywator wyświetli obrys zaznaczenia klawiaturą. Aktywacje klawiaturą zachowują pierścień. Kliknięcie zevent.detail === 0pochodzi z klawiatury. - Zamykaj po jednym poziomie naraz. Wywołaj
stopPropagation()wEscapeiArrowLeftw podmenu, bo inaczej detektor elementu nadrzędnego też je zamknie.
Obsługa wskaźnika
- Kliknięcie wskaźnika powoduje przełączenie. Wywołaj
preventDefault()napointerdown, aby kliknięcie nie przenosiło zaznaczenia i nie rozpoczynało podglądu w trakcie kliknięcia. Zapamiętaj, który przycisk został naciśnięty. Kliknięcie zamkniętego lub wyświetlanego w podglądzie aktywatora powoduje jego otwarcie lub zamknięcie i nigdy nie otwiera menu. Menu można otworzyć tylko za pomocą klawiatury. Nie przechwytuj kliknięcia. - Najechanie kursorem podąża za fokusem. Stylizuj
:hoveri:focus-visibleosobno. W przypadkupointermovenad elementem wywołajfocus({ focusVisible: false }). Przeglądarki, które ignorują tę opcję, i tak nie wyświetlają pierścienia dla fokusu wskaźnika. - Najedź kursorem, aby otworzyć menu. Po
pointerenteraktywatora najwyższego poziomu, gdy otwarte jest inne menu najwyższego poziomu: ukryj je, ustaw zaznaczenie na aktywatorze, na który najechano kursorem, bez pierścienia i wyświetl podgląd po zaznaczeniu (patrz wyżej). Nic nie jest otwarte: nie podejmuj żadnych działań. - Bezpieczny trójkąt. Stały mostek CSS jest zbyt wąski: działa tylko wtedy, gdy wskaźnik zaczyna się w pobliżu krawędzi elementu nadrzędnego. Pozwól CSS narysować most i użyj skryptu tylko do zgłaszania wskaźnika:
- Uruchamiaj tylko wtedy, gdy
matchMedia('(hover: hover)').matches; nie ma znaczenia w przypadku dotyku i klawiatury. - Rozpocznij, gdy wskaźnik opuści aktywator podmenu, którego podmenu jest otwarte. Pomiń podmenu
--below, które nie ma bocznej krawędzi. - Ustaw
--bridge-xi--bridge-y(wskaźnik, w pikselach względem podmenu),--bridge-edge(współrzędna X podmenu przy krawędzi:0, gdy wskaźnik znajduje się po jego lewej stronie, szerokość po odwróceniu) i--bridge-heightw podmenu orazdata-bridge, aby włączyć mostek. - Zatrzymaj po około 300 ms, gdy wskaźnik dotrze do elementu w podmenu lub gdy podmenu zostanie zamknięte. Usuń detektor i atrybut. Po tym czasie nic nie będzie działać.
- CSS nadaje menu podrzędnemu
::beforeprzycięte do trójkąta od wskaźnika do rogów przy krawędzi. Znajduje się nad elementami równorzędnymi na ścieżce wskaźnika, więc nigdy nie otrzymują one najechania, które zamknęłoby podmenu.clip-pathogranicza też test pozycji wskaźnika, dzięki czemu pozostała część menu Rodzic pozostaje interaktywna. - Podmenu wymaga właściwości
overflow: visible(element popover jest domyślnie przycinany), a pseudoelement wykracza poza nią, więc przesuń punkty kształtu o tę wartość. - Dalsze działania: zakrzywiona ścieżka (
shape()polecenia krzywej) lepiej odzwierciedla rzeczywiste łuki wskaźnika niż prosty trójkąt.
- Uruchamiaj tylko wtedy, gdy
[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 });
}
Obsługa najechania kursorem (element 2) pozostaje zwykłym modułem obsługi pointermove: nie wymaga logiki trójkąta.
Również:
- Poza obszarem zaznaczenia. Zamknij wszystkie otwarte menu, gdy zaznaczenie znajdzie się poza systemem menu (detektor
documentfocusin). - Aktywacja polecenia. Ogłoś to, a następnie ukryj menu główne i przywróć zaznaczenie na jego aktywatorze, bez pierścienia, jeśli aktywacja nastąpiła za pomocą wskaźnika. W przypadku elementów, które można zaznaczyć (
menuitemcheckbox,menuitemradio), odczekaj około 300 ms przed zamknięciem, aby zmieniony znacznik wyboru był widoczny. Stylizuj zamknięty aktywator jako aktywny tylko w przypadku:focus-visible. W przeciwnym razie przywrócone zaznaczenie (bez pierścienia) pozostawi go podświetlonym po wybraniu wskaźnikiem. aria-expanded. Ustaw go tylko w detektorze zdarzeńtoggle. Obejmuje stany podglądu i otwarcia, aaria-expandedaktywatora pozostaje"true", gdy menu jest wyświetlane w podglądzie lub otwarte.- Pomocnicy. Poniższe opcje zastępcze zakładają, że definiujesz pomocników dla znaczników:
triggers(wszystkie przyciski[commandfor]),menuFor(trigger),isOpen(menu),closeAndRestore(menu, { fromPointer })(skupienie się na aktywatorze, bez pierścienia, gdyfromPointer, a następniemenu.hidePopover()),enterOnOpen(WeakSetmenu, które można otworzyć po otwarciu) ipointerTrigger(aktywator ostatnio naciśnięty przez wskaźnik).
Menu podrzędne
- Użyj
popover="auto"w przypadku menu podrzędnych, umieszczonych po aktywatorze menu podrzędnego w elemencie nadrzędnymfocusgroup. Nigdypopover="manual". - Zagnieżdżanie pozwala przeglądarce zachować otwarte menu nadrzędne, zamknąć podmenu na tym samym poziomie, zamknąć podmenu podrzędne po zamknięciu menu głównego i obsługiwać zamykanie po kliknięciu poza menu.
- Aktywator menu podrzędnego zachowuje
aria-haspopup="menu"iaria-expanded, a detektortogglesynchronizujearia-expanded. - Skrypt klawiatury obsługuje zagnieżdżone elementy
EscapeiArrowLeftw sposób opisany powyżej. - Element aktywujący podmenu nigdy nie uruchamia polecenia: jeden element sterujący nie może jednocześnie aktywować i otwierać, a
Enternie może oznaczać obu tych czynności. Ustaw domyślne działanie jako pierwszy element podmenu lub dodaj osobny element polecenia obok aktywatora. Jeśli wymuszona jest kontrolka łączona, udokumentuj to jako kompromis w zakresie ułatwień dostępu.
Separatory i elementy do zaznaczenia
- Oddziel grupy elementów za pomocą
<hr>w grupie zaznaczenia: natywny elementseparator, którego nie można zaznaczyć, pomijany przez grupę zaznaczenia i element zastępczy. Nigdy nie stylizuj elementu<div>. - Opcja logiczna to
<button type="button">zaria-checkedi wyraźnymrole="menuitemcheckbox". Focusgroup nie wnioskuje tych ról, więc jest to jedyne miejsce, w którym udostępnione znaczniki ustawiają rolę elementu. - Jednym z elementów zbioru jest
<button type="button">zrole="menuitemradio"iaria-checkedwewnątrzrole="group"zaria-labelw tej samej grupie zaznaczenia. - Skrypt przełącza
aria-checked. Enterna polu wyboru przełącza i zamyka menu;Spaceprzełącza i pozostawia otwarte (APG). Radio można wyłączyć dowolnym przyciskiem.- Wyrenderuj znacznik wyboru z
[aria-checked="true"]::before, a nie osobny element ikony (patrz CSS poniżej).
Nie dotyczy
- Opinie dotyczące aktywacji. Natywne menu migają wybranym elementem przed zamknięciem. Jeśli chcesz, przed
hidePopover()możesz wyświetlić potwierdzenie wizualne trwające ok. 100 ms. Nie opóźniaj przywrócenia fokusu ani komunikatu. - Podpowiedzi. Focusgroup nie ma żadnych. W razie potrzeby dopasuj etykiety elementów włączonych na bieżącym poziomie na drukowanym arkuszu
keydowni ustaw fokus na następnym dopasowaniu po bieżącym elemencie, zawijając wiersze. Nie przechwytujSpace.
Weryfikacja przed zakończeniem
Instrukcja nie gwarantuje zachowania, więc przetestuj wynik. Otwórz stronę i sprawdź każdy z tych elementów za pomocą fizycznej klawiatury i myszy. Popraw i ponownie przetestuj wszystkie elementy, które nie działają:
- Klawiatura
Tab– przejście do pierwszego aktywatora;ArrowDown– wejście;Escape– zamknięcie jednego poziomu i powrót do aktywatora;Tab– zamknięcie wszystkich menu. - Wskaźnik otwarty. Kliknięcie aktywatora otwiera jego menu z fokusem na aktywatorze i bez pierścienia. Ponowne kliknięcie zamyka menu.
- Polecenie wskaźnika. Kliknięcie polecenia powoduje zamknięcie wszystkich menu i nie powoduje wyświetlenia pierścienia zaznaczenia na aktywatorze.
- Menu podrzędne. Przesunięcie po przekątnej od środka aktywatora menu podrzędnego do elementu menu podrzędnego powoduje, że menu pozostaje otwarte. Przesunięcie do niepowiązanego elementu nadrzędnego powoduje zamknięcie menu.
- Wąski ekran. Przy szerokości 375 pikseli żadne menu podrzędne nie zasłania swojego aktywatora.
- Znaczniki wyboru. Zaznaczone elementy są oznaczone znacznikiem wyboru.
- Nie zostawiaj resztek. Brak błędów konsoli i brak
popovertarget,popovertargetactionlubaria-detailsw regułach.
Wyrównaj zawartość pozycji menu
Jeśli wiersz zawiera ikonę, etykietę i skrót lub wskaźnik podmenu, zarezerwuj miejsce o stałym rozmiarze na początku na ikonę lub znacznik wyboru i przesuń treść na końcu do końca wiersza. Ikony mają znany rozmiar, więc stałe miejsce pozwala zachować wyrównanie etykiet niezależnie od tego, czy element ma ikonę. Nie używaj subgrid w przypadku wierszy: każdy dodatkowy element podrzędny (np. plakietka) staje się osobnym elementem siatki i psuje układ. Ukryj tekst skrótu przed technologią wspomagającą osoby z niepełnosprawnością, gdy powtarza on tylko klawisz, który już obsługuje dany element.
[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;
}
Umieść wskaźnik końcowy lub skrót w elemencie <span class="hint" aria-hidden="true"> wewnątrz przycisku. Aktywator menu podrzędnego już ogłasza wyskakujące okienko.
Progresywne ulepszanie i kreacje zastępcze
Zastępcze rozwiązania i obsługa focusgroup w przeglądarkach
Zachowaj focusgroup="menu nomemory" w znacznikach. Jeśli 'focusGroup' in HTMLElement.prototype ma wartość false, zainstaluj klawisze strzałek w górę i w dół oraz Home i End, ustaw role="menu" w każdym kontenerze menu i role="menuitem" na przyciskach bez roli. Nie zmieniaj elementów do zaznaczenia, <hr> ani role="group". Nigdy nie instaluj go, gdy obecna jest natywna grupa fokusowa. Left, Right, preview, open, and close are author script either way.
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);
});
}
Kreacje zastępcze i obsługa wyskakujących okienek w przeglądarkach
Interfejs Popover API to głównie progresywne ulepszanie, ale jego definiujące zachowania – promowanie warstwy górnej, lekkie odrzucanie i wywoływanie popovertarget – nie mają odpowiednika tylko w CSS. Starsze przeglądarki wymagają polyfilla lub ręcznego rozwiązania zastępczego, jeśli nie chcesz go dostarczać.
Polyfill: aby obsługiwać atrybut popover w starszych przeglądarkach, warunkowo wczytuj @oddbird/popover-polyfill. OBOWIĄZKOWO: wykryj funkcję, sprawdzając właściwość popover w HTMLElement.prototype, i wczytaj polyfill tylko wtedy, gdy brakuje obsługi natywnej – NIE wczytuj go bezwarunkowo.
Za pomocą bundlera lub mapy importu:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
Bez bundlera, importuj z sieci CDN w tagu <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
Uwaga dotycząca stylów: polyfill nie może zdefiniować prawdziwej pseudoklasy :popover-open, więc zamiast tego stosuje klasę .\:popover-open. OBOWIĄZKOWO: połącz te 2 właściwości za pomocą :is() lub :where(), w przeciwnym razie przeglądarki, które nie obsługują :popover-open, odrzucą całą regułę:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
Możesz też użyć position: fixed i ręcznie obliczyć współrzędne za pomocą getBoundingClientRect() lub polegać na domyślnym pozycjonowaniu za pomocą inset: auto, jeśli jest to akceptowalne w danym przypadku użycia.
Kreacje zastępcze i obsługa pozycjonowania reklam zakotwiczonych w przeglądarkach
Bez pozycjonowania zakotwiczonego CSS element popover wraca do wyśrodkowanego miejsca docelowego widocznego obszaru UA. Nigdy nie używaj tego w przypadku menu: odłącza ono menu od aktywatora, a wyśrodkowane menu podrzędne zasłania menu nadrzędne. Zablokuj reguły zakotwiczone za pomocą @supports (position-area: block-end) i ustaw pozycję menu ze skryptu, gdy się nie powiedzie:
- Umieść menu pod elementem aktywującym (górny poziom) lub obok niego (podmenu), odwracając je na drugą stronę i przypinając do widocznego obszaru, gdyby miało się nie zmieścić.
- Uruchom go w uchwycie
togglepo otwarciu menu i zastosuj wynik jakoleftitopw menu. Użyjposition: fixedzinset: autoimargin: 0, aby współrzędne były względne względem widocznego obszaru. - Zachowaj te zastąpienia w regule
@supports not (position-area: block-end), aby nie miały wpływu na przeglądarki z pozycjonowaniem zakotwiczonym.
Jeśli kierujesz reklamy na wcześniejsze implementacje, w których showPopover({ source }) nie ustanawia niejawnej kotwicy (brak source w ToggleEvent.prototype), nadaj każdemu aktywatorowi unikalny anchor-name, a każdemu menu pasujący position-anchor ze skryptu, zamiast pisać reguły CSS dla poszczególnych menu.
W przypadku przeglądarek, które w ogóle nie obsługują pozycjonowania zakotwiczonego, zapoznaj się z artykułem Menu rozwijane uwzględniające widoczny obszar, aby dowiedzieć się, jak używać kodu polyfill.
Obsługa poleceń wywołujących w przeglądarkach i reagowanie na błędy
Bez wywołujących command="toggle-popover" nic nie robi, więc skrypt musi przełączać menu.
- Wykrywaj funkcje interfejsu, a nie instancji:
'commandForElement' in HTMLButtonElement.prototype. - Przy każdym aktywatorze
clickwywołajpreventDefault(), aby przeglądarka z wywołującymi nie mogła przełączyć się dwukrotnie. - Jeśli menu jest otwarte, zamknij je, naciskając
closeAndRestore(menu). W przeciwnym razie zadzwoń pod numermenu.showPopover({ source: trigger }). - Oznacz menu w języku
enterOnOpen, chyba że kliknięcie pochodziło z naciśniętego aktywatora (pointerTrigger), aby tylko klawiatura otwierała menu. EnteriSpacenadal działają, ponieważ generują kliknięcie przycisków.