Podstawowe informacje o tworzeniu łatwo dostępnego komponentu podzielonego przycisku.
W tym poście przedstawię pomysł na stworzenie podzielonego przycisku . Wypróbuj wersję demonstracyjną
Jeśli wolisz film, oto wersja tego posta na YouTube:
Przegląd
Przyciski podziału to przyciski ukrywające przycisk główny i listę przycisków dodatkowych. Przydają się one do ujawniania typowego działania oraz zagnieżdżaniu dodatkowych, rzadziej używanych działań, do momentu, gdy będą potrzebne. Podzielony przycisk może mieć kluczowe znaczenie, aby bardziej pracochłonny projekt był zgrany w minimalnym stopniu. Zaawansowany przycisk podziału może nawet zapamiętać ostatnie działanie użytkownika i przenieść je do pozycji głównej.
Typowy przycisk podziału znajduje się w aplikacji poczty e-mail. Główne działanie jest wysyłane, ale możesz też wysłać je później lub zapisać wersję roboczą:
Wspólny obszar działań jest przyjemny, ponieważ użytkownik nie musi się rozglądać. Wie, że najważniejsze działania e-mail są dostępne dzięki przyciskowi podziału.
Części
Przeanalizujmy najważniejsze elementy podzielonego przycisku, zanim omówimy ogólną administrację i końcowe wrażenia użytkownika. Narzędzie VisBug do sprawdzania ułatwień dostępu pozwala wyświetlić widok komponentu w makrze, pokazując aspekty kodu HTML, stylu i ułatwień dostępu w przypadku każdej większej części.
Kontener przycisku podziału najwyższego poziomu
Komponent najwyższego poziomu to wbudowany flexbox z klasą gui-split-button
, który zawiera działanie główne i element .gui-popup-button
.
Główny przycisk polecenia
Początkowo widoczny i możliwy do zaznaczenia element <button>
mieści się w kontenerze z 2 pasującymi kształtami narożników dla fokusa, hover i aktywnych interakcji, które pojawiają się w elemencie .gui-split-button
.
Przycisk przełączania wyskakującego okienka
Element pomocy „wyskakujący przycisk” służy do aktywowania i odnoszenia się do listy przycisków dodatkowych. Zwróć uwagę, że nie jest to <button>
i nie można go zaznaczyć. Jest to jednak kotwica pozycjonowania dla elementu .gui-popup
i hosta elementu :focus-within
służącego do wyświetlania wyskakującego okienka.
Karta w wyskakującym okienku
To jest pływająca karta podrzędna karty z kotwicą .gui-popup-button
, umiejscowiona bezwzględnie i semantycznie pakująca listę przycisków.
Działania dodatkowe
Możliwy do zaznaczenia element <button>
, w którym rozmiar czcionki jest nieco mniejszy niż przycisk działania głównego, ma ikonę i dodatkowy styl przycisku głównego.
Właściwości niestandardowe
Podane niżej zmienne pomagają uzyskać harmonię kolorów i stwarzają centralne miejsce do modyfikowania wartości używanych w całym komponencie.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);
.gui-split-button {
--theme: hsl(220 75% 50%);
--theme-hover: hsl(220 75% 45%);
--theme-active: hsl(220 75% 40%);
--theme-text: hsl(220 75% 25%);
--theme-border: hsl(220 50% 75%);
--ontheme: hsl(220 90% 98%);
--popupbg: hsl(220 0% 100%);
--border: 1px solid var(--theme-border);
--radius: 6px;
--in-speed: 50ms;
--out-speed: 300ms;
@media (--dark) {
--theme: hsl(220 50% 60%);
--theme-hover: hsl(220 50% 65%);
--theme-active: hsl(220 75% 70%);
--theme-text: hsl(220 10% 85%);
--theme-border: hsl(220 20% 70%);
--ontheme: hsl(220 90% 5%);
--popupbg: hsl(220 10% 30%);
}
}
Układy i kolory
Markup
Na początku elementu znajduje się <div>
z niestandardową nazwą klasy.
<div class="gui-split-button"></div>
Dodaj przycisk główny i elementy .gui-popup-button
.
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>
Zwróć uwagę na atrybuty ARIA aria-haspopup
i aria-expanded
. Dzięki tym sygnałom czytniki ekranu mogą poznać możliwości i stan działania podzielonego przycisku. Atrybut title
jest przydatny dla wszystkich.
Dodaj ikonę <svg>
i element kontenera .gui-popup
.
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup"></ul>
</span>
</div>
Aby ułatwić sobie umieszczenie wyskakującego okienka, .gui-popup
jest elementem podrzędnym przycisku, który go rozwija. Jedynym elementem przechwytywania w przypadku tej strategii jest kontener .gui-split-button
, który nie może używać elementu overflow: hidden
, ponieważ spowoduje to ukrycie wyskakującego okienka z elementu wizualnego.
Element <ul>
wypełniony treścią <li><button>
ogłosi się jako „lista przycisków” dla czytników ekranu, czyli właśnie wyświetlany interfejs.
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup">
<li>
<button>Schedule for later</button>
</li>
<li>
<button>Delete</button>
</li>
<li>
<button>Save draft</button>
</li>
</ul>
</span>
</div>
Aby uatrakcyjnić wygląd przycisków, dodaję ikony do przycisków dodatkowych ze strony https://heroicons.com. Są one opcjonalne zarówno w przypadku przycisków głównych, jak i drugorzędnych.
<div class="gui-split-button">
<button>Send</button>
<span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
<svg aria-hidden="true" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
<ul class="gui-popup">
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Schedule for later
</button></li>
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete
</button></li>
<li><button>
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
Save draft
</button></li>
</ul>
</span>
</div>
Style
Gdy już masz przygotowany kod HTML i treść, style są gotowe do określenia koloru i układu.
Styl kontenera przycisku podziału
Typ wyświetlania inline-flex
sprawdza się w przypadku tego komponentu opakowującego, ponieważ powinien pasować do innych przycisków podziału, działań lub elementów.
.gui-split-button {
display: inline-flex;
border-radius: var(--radius);
background: var(--theme);
color: var(--ontheme);
fill: var(--ontheme);
touch-action: manipulation;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Styl: <button>
Przyciski pozwalają zakryć ilość potrzebnego kodu. Może być konieczne cofnięcie lub zastąpienie domyślnych stylów przeglądarki, ale wymagane jest też wymuszenie dziedziczenia, dodanie stanów interakcji oraz dostosowanie do różnych preferencji użytkownika i typów danych wejściowych. Style przycisków szybko się sumują.
Różnią się one od przycisków standardowych, ponieważ mają wspólne tło z elementem nadrzędnym. Zwykle przycisk ma kolor tła i tekstu. Są one jednak udostępniane i stosują wyłącznie własne tło do interakcji.
.gui-split-button button {
cursor: pointer;
appearance: none;
background: none;
border: none;
display: inline-flex;
align-items: center;
gap: 1ch;
white-space: nowrap;
font-family: inherit;
font-size: inherit;
font-weight: 500;
padding-block: 1.25ch;
padding-inline: 2.5ch;
color: var(--ontheme);
outline-color: var(--theme);
outline-offset: -5px;
}
Dodaj stany interakcji z kilkoma pseudoklasami CSS i użyj dopasowania właściwości niestandardowych dla danego stanu:
.gui-split-button button {
…
&:is(:hover, :focus-visible) {
background: var(--theme-hover);
color: var(--ontheme);
& > svg {
stroke: currentColor;
fill: none;
}
}
&:active {
background: var(--theme-active);
}
}
Przycisk główny wymaga kilku specjalnych stylów, aby zakończyć efekt projektu:
.gui-split-button > button {
border-end-start-radius: var(--radius);
border-start-start-radius: var(--radius);
& > svg {
fill: none;
stroke: var(--ontheme);
}
}
Na koniec, przycisk i ikona jasnego motywu uzyskują cień:
.gui-split-button {
@media (--light) {
& > button,
& button:is(:focus-visible, :hover) {
text-shadow: 0 1px 0 var(--theme-active);
}
& > .gui-popup-button > svg,
& button:is(:focus-visible, :hover) > svg {
filter: drop-shadow(0 1px 0 var(--theme-active));
}
}
}
Świetny przycisk to świetny sposób na zwrócenie uwagi na mikrointerakcje i drobne detale.
Uwaga na temat: :focus-visible
Zwróć uwagę, że style przycisku używają :focus-visible
zamiast :focus
. :focus
to kluczowy element tworzenia dostępnego interfejsu, ale ma on jednak jeden błąd: nie jest on brany pod uwagę przy określaniu, czy użytkownik chce je zobaczyć, czy nie.
Poniższy film stanowi próbę przełamania tej mikrointerakcji, aby pokazać, dlaczego :focus-visible
jest inteligentną alternatywą.
Styl przycisku wyskakującego okienka
Flexbox 4ch
do wyśrodkowania ikony i zakotwiczenia wyskakującego okienka z listą przycisków. Podobnie jak przycisk główny, jest on przezroczysty, dopóki nie najedziesz na niego kursorem lub nie wejdzie z nim w interakcję, i zostanie rozciągnięty, aby wypełnić.
.gui-popup-button {
inline-size: 4ch;
cursor: pointer;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border-inline-start: var(--border);
border-start-end-radius: var(--radius);
border-end-end-radius: var(--radius);
}
Dzięki zagnieżdżeniu CSS i selektorowi funkcjonalnemu :is()
możesz nakładać warstwy w stanach najechania kursorem, zaznaczenia i aktywności:
.gui-popup-button {
…
&:is(:hover,:focus-within) {
background: var(--theme-hover);
}
/* fixes iOS trying to be helpful */
&:focus {
outline: none;
}
&:active {
background: var(--theme-active);
}
}
Te style to główne elementy umożliwiające wyświetlenie i ukrycie wyskakującego okienka. Gdy element podrzędny .gui-popup-button
ma focus
w jednym z elementów podrzędnych, ustaw opacity
, pozycję i pointer-events
na ikonie i wyskakującym okienku.
.gui-popup-button {
…
&:focus-within {
& > svg {
transition-duration: var(--in-speed);
transform: rotateZ(.5turn);
}
& > .gui-popup {
transition-duration: var(--in-speed);
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
}
}
Po ukończeniu stylów wejścia i wyjścia ostatnim elementem jest warunkowe przejście przekształceń w zależności od preferencji ruchu użytkownika.
.gui-popup-button {
…
@media (--motionOK) {
& > svg {
transition: transform var(--out-speed) ease;
}
& > .gui-popup {
transform: translateY(5px);
transition:
opacity var(--out-speed) ease,
transform var(--out-speed) ease;
}
}
}
W przypadku użytkowników, którzy wolą ograniczony ruch, zmienia się przezroczystość kodu.
Styl wyskakującego okienka
Element .gui-popup
to pływający przycisk karty z właściwościami niestandardowymi i jednostkami względnymi, które można nieznacznie zmniejszyć, interaktywnie dopasować do przycisku głównego i użyć koloru marki. Zwróć uwagę, że ikony mają mniejszy kontrast
i są cieńsze, a w cieniu widać błękitną markę. Podobnie jak w przypadku przycisków,
wygodna obsługa i interfejs wynikają z nakładających się na siebie drobnych detali.
.gui-popup {
--shadow: 220 70% 15%;
--shadow-strength: 1%;
opacity: 0;
pointer-events: none;
position: absolute;
bottom: 80%;
left: -1.5ch;
list-style-type: none;
background: var(--popupbg);
color: var(--theme-text);
padding-inline: 0;
padding-block: .5ch;
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
font-size: .9em;
transition: opacity var(--out-speed) ease;
box-shadow:
0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
;
}
Ikony i przyciski są oznaczone kolorami marki, które pasują do każdej karty z ciemnym i jasnym motywem:
.gui-popup {
…
& svg {
fill: var(--popupbg);
stroke: var(--theme);
@media (prefers-color-scheme: dark) {
stroke: var(--theme-border);
}
}
& button {
color: var(--theme-text);
width: 100%;
}
}
Wyskakujące okienko z ciemnym motywem ma dodany cień, tekst i ikony, a także nieco bardziej intensywny cień pola:
.gui-popup {
…
@media (--dark) {
--shadow-strength: 5%;
--shadow: 220 3% 2%;
& button:not(:focus-visible, :hover) {
text-shadow: 0 1px 0 var(--ontheme);
}
& button:not(:focus-visible, :hover) > svg {
filter: drop-shadow(0 1px 0 var(--ontheme));
}
}
}
Ogólne style ikon w usłudze <svg>
Wszystkie ikony mają odpowiedni rozmiar względem przycisku font-size
, w którym się znajdują, dzięki wykorzystaniu jednostki ch
jako elementu inline-size
. Każdy ma też pewne style, dzięki którym kontury są delikatne i gładkie.
.gui-split-button svg {
inline-size: 2ch;
box-sizing: content-box;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2px;
}
Układ od prawej do lewej
Właściwości logiczne wykonują całą złożoną pracę.
Oto lista użytych właściwości logicznych:
– display: inline-flex
tworzy wbudowany element Flex.
– padding-block
i padding-inline
jako pary, zamiast padding
skrótu, korzystaj z zalet dopełniania logicznych boków.
– border-end-start-radius
i znajomi zaokrąglą rogi zgodnie z kierunkiem dokumentu.
Użycie wartości inline-size
zamiast width
zapewnia, że rozmiar nie jest powiązany z wymiarami fizycznymi.
- border-inline-start
dodaje na początku obramowanie, które może znajdować się po prawej lub po lewej stronie w zależności od kierunku skryptu.
JavaScript
Niemal każdy z tych elementów JavaScriptu usprawnia dostępność. Dwie biblioteki pomocnicze są używane, by ułatwić wykonywanie zadań. Język BlingBlingJS służy do obsługi zwięzłych zapytań DOM i łatwej konfiguracji odbiornika zdarzeń, natomiast kod roving-ux ułatwia korzystanie z klawiatury i pada do gier w wyskakującym okienku.
import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'
const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')
Po zaimportowaniu powyższych bibliotek oraz zaznaczeniu elementów i zapisaniu w zmiennych udoskonalenie działania zajmuje kilka funkcji.
Indeks ruchu
Gdy klawiatura lub czytnik ekranu wybierze .gui-popup-button
, chcemy skierować zaznaczenie na pierwszy (lub ostatnio zaznaczony) przycisk w elemencie .gui-popup
. Biblioteka ułatwia nam to za pomocą parametrów element
i target
.
popupButtons.forEach(element =>
rovingIndex({
element,
target: 'button',
}))
Element przechodzi teraz do docelowych elementów podrzędnych <button>
i umożliwia standardową nawigację przy użyciu klawiszy strzałek.
Przełączam aria-expanded
Widać, że wyskakujące okienko się wyświetla i ukrywa, ale czytnik ekranu potrzebuje nie tylko wskazówek wizualnych. Używa się tutaj JavaScriptu, aby uzupełnić interakcję :focus-within
opartą na CSS przez przełączenie odpowiedniego atrybutu czytnika ekranu.
popupButtons.on('focusin', e => {
e.currentTarget.setAttribute('aria-expanded', true)
})
popupButtons.on('focusout', e => {
e.currentTarget.setAttribute('aria-expanded', false)
})
Włączam klucz Escape
Użytkownik celowo trafił w pułapkę, a to oznacza, że musimy mu umożliwić wyjście. Najczęstszym sposobem jest zezwolenie na użycie klucza Escape
.
W tym celu uważaj na naciśnięcia klawiszy na wyskakującym okienku, ponieważ wszystkie zdarzenia z klawiatury w przypadku dzieci są przesyłane do tego rodzica.
popupButtons.on('keyup', e => {
if (e.code === 'Escape')
e.target.blur()
})
Jeśli przycisk wyskakującego okienka rozpozna naciśnięty klawisz Escape
, zniknie zaznaczenie ze swojego elementu za pomocą blur()
.
Kliknięcia przycisku podziału
Gdy użytkownik kliknie lub naciśnie przycisk albo naciśnie go na klawiaturze, aplikacja musi wykonać odpowiednie działanie. Dymki zdarzeń są tu użyte ponownie, ale tym razem w kontenerze .gui-split-button
, aby wychwytywać kliknięcia przycisków w wyskakującym okienku podrzędnym lub w działaniu głównym.
splitButtons.on('click', event => {
if (event.target.nodeName !== 'BUTTON') return
console.info(event.target.innerText)
})
Podsumowanie
Wiesz już, jak to zrobiłem, więc jak to zrobisz 🙂
Stwórzmy różne metody i nauczmy się wszystkiego, jak rozwijać się w internecie. Utwórz demonstrację i udostępnię mi linki na Twitterze, a dodam ją do sekcji remiksów w ramach społeczności poniżej.