Tworzenie komponentu złożonego z przycisku

Podstawowe informacje o tym, jak tworzyć komponenty przycisków podzielonych w taki sposób, aby były dostępne dla wszystkich użytkowników.

W tym poście chcę podzielić się z Wami sposobem na tworzenie przycisków podzielonych. Wypróbuj wersję demonstracyjną.

Demonstracja

Jeśli wolisz film, oto wersja tego posta w YouTube:

Omówienie

Przyciski podzielone to przyciski, które zawierają przycisk główny i listę dodatkowych przycisków. Są one przydatne do wyświetlania często wykonywanych czynności, a także do umieszczania w nich rzadziej używanych czynności, które mogą być ukryte do momentu, aż będą potrzebne. Przycisk podziału może sprawić, że projekt będzie minimalny. Zaawansowany przycisk może nawet zapamiętać ostatnie działanie użytkownika i przesunąć je do pozycji głównej.

Wspólny przycisk podziału można znaleźć w aplikacji do obsługi poczty e-mail. Głównym działaniem jest wysłanie, ale możesz też wysłać później lub zapisać wersję roboczą:

Przykład przycisku podziału w aplikacji poczty e-mail.

Udostępnione pole działania jest przydatne, ponieważ użytkownik nie musi się rozglądać. Wiedzą, że najważniejsze czynności związane z e-mailami są dostępne na przycisku dzielenia.

Części

Zanim omówimy ogólną aranżację i wrażenia użytkownika, przyjrzyjmy się najważniejszym elementom przycisku podzielonego. W tym celu używamy narzędzia do sprawdzania dostępności VisBug, które pozwala wyświetlić widok makro komponentu, pokazując aspekty kodu HTML, styl i dostępność poszczególnych głównych elementów.

Elementy HTML, z których składa się przycisk podzielony.

Kontenery przycisku podziału na najwyższym poziomie

Komponent najwyższego poziomu to wbudowany Flexbox o klasie gui-split-button, zawierający działanie główne i .gui-popup-button.

Klasa gui-split-button została sprawdzona i wyświetla właściwości CSS używane w tej klasie.

Główny przycisk polecenia

Początkowo widoczny i możliwy do skoncentrowania element <button> mieści się w kontenerze z 2 pasującymi do siebie kształtami narożników, aby interakcje fokusowania, najeżdżania kursoremaktywności były widoczne w ramach elementu .gui-split-button.

Inspekcja pokazująca reguły CSS elementu przycisku.

Przycisk przełączania wyskakującego okienka

Element obsługi „przycisk wyskakującego okienka” służy do aktywacji i powiązania z listą przycisków dodatkowych. Zwróć uwagę, że to nie jest obiekt <button> i nie można go zaznaczyć. Jest to jednak element kotwiczący dla .gui-popup i host dla :focus-within, które są używane do wyświetlania wyskakującego okienka.

Inspekcja pokazująca reguły CSS klasy gui-popup-button.

Wyskakująca karta

To jest element potomny karty, który jest umieszczony bezwzględnie względem elementu kotwicy .gui-popup-button i obejmuje semantycznie listę przycisków.

Inspekcja pokazująca reguły CSS klasy gui-popup

dodatkowe działania

Fokusowalny przycisk <button> o nieco mniejszym rozmiarze czcionki niż główny przycisk polecenia zawiera ikonę i styl pasujący do głównego przycisku.

Inspekcja pokazująca reguły CSS elementu przycisku.

Właściwości niestandardowe

Te zmienne pomagają tworzyć harmonię kolorów i stanowią 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

Znacznik

Element zaczyna się jako <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-haspopuparia-expanded. Te wskazówki są kluczowe dla czytników ekranu, aby wiedzieć, jakie funkcje są dostępne i jak działają przyciski. 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 umieścić wyskakujące okienko w prosty sposób, .gui-popup jest elementem podrzędnym przycisku, który je rozwija. Jedyną wadą tej strategii jest kontener .gui-split-button, który nie może użyć pola overflow: hidden, ponieważ wyeliminuje on wyświetlanie wyskakującego okienka.

<ul> wypełniony treściami <li><button> będzie przedstawiany czytnikom ekranu jako „lista przycisków”.

<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 dodać blasku i uatrakcyjnić kolor, dodaliśmy ikony do przycisków dodatkowych na stronie https://heroicons.com. Ikony są opcjonalne w przypadku przycisków głównego i dodatkowego.

<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 kod HTML i treści są już gotowe, style mogą określić kolory i układ.

Styl kontenera przycisku podziału

W przypadku tego komponentu opakowania dobrze sprawdza się typ wyświetlania inline-flex, ponieważ powinien pasować do innych podzielonych przycisków, 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;
}

Przycisk podziału.

Styl <button>

Przyciski bardzo dobrze ukrywają, ile kodu jest wymagane. Może być konieczne cofnięcie lub zastąpienie domyślnych stylów przeglądarki, ale trzeba też zastosować dziedziczenie, dodać stany interakcji i dostosować interfejs do różnych preferencji użytkownika i typów danych wejściowych. Style przycisków szybko się sumują.

Te przyciski różnią się od zwykłych przycisków, ponieważ mają ten sam kolor tła co element nadrzędny. Zazwyczaj przycisk ma swój kolor tła i tekstu. Te jednak udostępniają je i używają tylko własnego tła podczas 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 za pomocą kilku pseudoklas CSS i odpowiednich 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);
  }
}

Aby uzyskać efekt wizualny, przycisk główny musi mieć kilka specjalnych stylów:

.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);

  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}

Na koniec, aby dodać trochę uroku, przycisk i ikona jasnego motywu mają 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 zwraca uwagę na mikrointerakcje i drobne szczegóły.

Uwaga na temat :focus-visible

Zwróć uwagę, że w stylach przycisków pojawia się :focus-visible zamiast :focus. :focus to kluczowy element tworzenia dostępnego interfejsu użytkownika, ale ma jedną wadę: nie sprawdza, czy użytkownik musi zobaczyć dany element, czy nie.

Poniższy film ma na celu omówienie tej mikrointerakcji, aby pokazać, że :focus-visible to inteligentna alternatywa.

Określanie stylu przycisku wyskakującego okienka

4ch flexbox do wyśrodkowania ikony i zakotwiczenia listy przycisków w wyskakującym okienku. Podobnie jak przycisk główny, jest on przezroczysty, dopóki nie najedzie na niego kursorem lub nie wykona na niego działania. Poza tym jest rozciągnięty, aby wypełnić.

Część strzałki przycisku podziału służąca do wywołania wyskakującego okienka.

.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);
}

Warstwy w stanach najechania kursorem, najechania kursorem z aktywną opcją i aktywnej z zagnieżdżeniem CSS:is() selektorem funkcjonalnym:

.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 są głównym elementem umożliwiającym wyświetlanie i ukrywanie wyskakującego okienka. Jeśli element .gui-popup-button ma element podrzędny focus, ustaw pozycję opacity i wartość pointer-events na ikonie oraz w 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 skonfigurowaniu stylów wejścia i wyjścia należy warunkowo przekształcać animacje przejścia w zależności od preferencji użytkownika dotyczących animacji:

.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;
    }
  }
}

Osoba, która dokładnie przyjrzy się kodom, zauważy, że przejścia w przezroczystości nadal występują w przypadku użytkowników, którzy wolą ograniczone ruchy.

Stylizacja wyskakującego okienka

Element .gui-popup to lista przycisków pływających kart, która zawiera właściwości niestandardowe i jednostki względne, aby były subtelnie mniejsze, interaktywnie dopasowywane do przycisku głównego i marka za pomocą koloru. Zwróć uwagę, że ikony mają mniejszy kontrast, są cieńsze, a cień ma odcień niebieskiego charakterystyczny dla marki. Podobnie jak w przypadku przycisków, dobre UI i UX to efekt kumulacji tych drobnych szczegółów.

Element karty unoszącej się na ekranie.

.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 mają kolory marki, aby dobrze wyglądały na kartach w ciemnym i jasnym motywie:

Linki i ikony płatności, szybkiej płatności i Zapisz na później.

.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 w ciemnym motywie zawiera dodatkowy cień tekstu i ikony oraz nieco bardziej intensywny cień pola:

Popup w ciemnym motywie.

.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 ikony <svg>

Wszystkie ikony są dopasowane rozmiarem do przycisku font-size, w którym są używane, przy użyciu jednostki ch jako inline-size. Do każdego z nich przypisano kilka stylów, które pozwalają delikatnie i wygładzić kontury ikon.

.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łą skomplikowaną pracę. Oto lista użytych właściwości logicznych: - display: inline-flex tworzy element flex w tekście. – padding-blockpadding-inline jako parę zamiast padding skrót, który zapewnia korzyści z wypełniania stron logicznych. – border-end-start-radius i znajomi będą mieli zaokrąglone rogi w zależności od orientacji dokumentu. – inline-size zamiast width zapewnia, że rozmiar nie jest powiązany z wymiarami fizycznymi. – border-inline-start dodaje do początku obramowanie, które może znajdować się po prawej lub lewej stronie w zależności od kierunku skryptu.

JavaScript

Prawie cały wymieniony poniżej kod JavaScript służy do ułatwiania dostępu. Dwie biblioteki pomocnicze służą do ułatwienia zadania. BlingBlingJS służy do krótkich zapytań DOM i łatwego konfigurowania detektorów zdarzeń, a roving-ux ułatwia obsługę klawiatury i kontrolera 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 i wybraniu elementów i zapisaniu ich w zmiennych uaktualnienie doświadczenia wymaga tylko kilku funkcji.

Indeks Roving

Gdy klawiatura lub czytnik ekranu skupia się na elemencie .gui-popup-button, chcemy przekazać fokus na pierwszy (lub ostatnio zaznaczony) przycisk w elementach .gui-popup. Biblioteka pomaga nam w tym za pomocą parametrów elementtarget.

popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))

Element przekazuje teraz fokus do elementów podrzędnych <button> i umożliwia standardową nawigację za pomocą klawiszy strzałek, aby przeglądać opcje.

Przełączanie aria-expanded

Choć wyraźnie widać, że wyskakujące okienko wyświetla się i ukrywa, czytnik ekranu potrzebuje więcej niż wskazówek wizualnych. Język JavaScript jest tu używany do uzupełnienia interakcji :focus-within sterowanej przez arkusz CSS poprzez przełączanie atrybutu odpowiedniego dla czytnika ekranu.

popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})

popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})

Włączanie klucza Escape

Użytkownik celowo został skierowany na pułapkę, więc musimy zapewnić mu możliwość wyjścia z niej. Najczęstszym sposobem jest zezwolenie na użycie klucza Escape. Aby to zrobić, obserwuj naciśnięcia klawiszy na przycisku wyskakującego okienka, ponieważ wszystkie zdarzenia klawiatury w elementach potomnych będą przekazywane do tego elementu nadrzędnego.

popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})

Jeśli przycisk wyskakującego okienka wykryje naciśnięcie klawisza Escape, przestaje być aktywny.blur()

Kliknięcia przycisku podziału

I wreszcie, gdy użytkownik kliknie lub kliknie przycisk albo wejdzie z nim w interakcję, aplikacja musi wykonać odpowiednie działanie. Tutaj ponownie używamy przenoszenia zdarzeń, ale tym razem w kontekście kontenera .gui-split-button, aby rejestrować kliknięcia przycisku z wyskakujących okienek podrzędnych lub z głównego działania.

splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})

Podsumowanie

Teraz, gdy już wiesz, jak to zrobić, jak Ty to zrobisz? 🙂

Zróżnicujemy nasze podejścia i poznamy wszystkie sposoby tworzenia stron internetowych. Utwórz wersję demonstracyjną, wyślij mi linki, a ja dodam je do sekcji z remiksami społeczności.

Remiksy społeczności