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

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 czasu, aż będą potrzebne. Przycisk podzielony może być kluczowy, aby projekt o dużej ilości elementów nie sprawiał wrażenia przytłomionego. Zaawansowany przycisk może nawet zapamiętać ostatnie działanie użytkownika i przesunąć je do pozycji głównej.

W aplikacji poczty e-mail znajdziesz typowy przycisk dzielenia. 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ólne skoordynowanie i wrażenia końcowego 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

Element najwyższego poziomu to wbudowany flexbox z klasą gui-split-button, który zawiera działanie główne.gui-popup-button.

Inspected gui-split-button class and showing the CSS properties used in this class

Główny przycisk działania

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

Inspekcja pokazująca reguły CSS elementu przycisku.

Przycisk przełączania okna

Element pomocy „Przycisk wyskakującego okienka” służy do aktywowania i wskazania listy przycisków dodatkowych. Zwróć uwagę, że nie jest to <button> i nie można go wybrać. 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.

Karta wyskakująca

To jest element potomny karty, który jest elementem kotwicy .gui-popup-button, umieszczony absolutnie i semantycznie otaczający 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>

W przypadku prostego umieszczenia wyskakującego okienka .gui-popup jest elementem podrzędnym przycisku, który je otwiera. Jedynym haczykiem w tej strategii jest to, że kontener .gui-split-button nie może używać overflow: hidden, ponieważ spowoduje to przycięcie wyskakującego okienka.

<ul> wypełniony treściami <li><button> będzie dla czytników ekranu przedstawiany jako „lista przycisków”, ponieważ to właśnie ten interfejs jest wyświetlany.

<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 nadać projektowi charakteru i zastosować ciekawe kolory, dodałem do przycisków dodatkowych ikony z witryny https://heroicons.com. Ikony są opcjonalne zarówno w przypadku przycisków głównych, jak i dodatkowych.

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

Nadawanie stylu kontenerowi przycisku podzielonego

W przypadku tego komponenta otaczającego dobrze sprawdza się typ wyświetlania inline-flex, ponieważ powinien on pasować do innych przycisków, działań lub elementów podzielonych.

.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ę kumulują.

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, przy jasnym motywie przycisk i ikona 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));
    }
  }
}

Przy tworzeniu świetnego przycisku należy zwrócić uwagę na mikrointerakcje i drobny detale.

Uwaga dotycząca :focus-visible

Zwróć uwagę, że style przycisków używają wartości :focus-visible zamiast :focus. :focusjest kluczowym elementem tworzenia dostępnego interfejsu użytkownika, ale ma jedną wadę: nie sprawdza, czy użytkownik musi zobaczyć dany element, czy nie.

Film poniżej pokazuje, jak podzielić mikrointerakcję, aby pokazać, że :focus-visible to inteligentna alternatywa.

Stylowanie przycisku okna

4ch Flexbox do wyśrodkowania ikony i zabezpieczenia listy przycisków w wyskakującym okienku. Podobnie jak przycisk główny, jest przezroczysty, dopóki nie najedziesz na niego kursorem lub nie wejdziesz z nim w interakcję, a potem rozszerza się, aby wypełnić całą powierzchnię.

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

Przyjrzystym użytkownikom uda się zauważyć, że przejścia 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 karty, która jest wyświetlana w układzie ruchomym. Używa właściwości niestandardowych i jednostek względnych, aby była nieco mniejsza i interaktywnie dopasowana do głównego przycisku. Ma też kolorystykę zgodną z elementami marki. 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 z ciemnym i jasnym motywem:

linki i ikony dotyczące płatności, szybkiej płatności i zapisu 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 ikon <svg>

Wszystkie ikony są dopasowane rozmiarem do przycisku font-size, w którym są używane, przy użyciu jednostki ch jako inline-size. Każdy z nich ma też kilka stylów, które nadają ikonom miękkość i gładkość.

.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 wierszu. – 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 na 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. Aby ułatwić wykonywanie zadań, używam 2 bibliotek myhelper. Biblioteka BlingBlingJS służy do krótkich zapytań DOM i łatwego konfigurowania odbiornika zdarzeń, a biblioteka roving-ux ułatwia obsługę klawiatury i kontrolera w przypadku wyskakujących okienek.

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 oraz zapisaniu ich w zmiennych wystarczy kilka funkcji, aby uaktualnić tę funkcję.

indeks przemieszczania się,

Gdy klawiatura lub czytnik ekranu zaznaczy .gui-popup-button, chcemy przekazać fokus do pierwszego (lub ostatnio zaznaczonego) przycisku w .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

Chociaż wizualnie widać, że wyskakujące okienko się wyświetla i chowa, czytnik ekranu potrzebuje więcej niż tylko 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 do pułapki, 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

Jeśli użytkownik kliknie przycisk lub dotknie go, albo jeśli przyciski zostaną użyte za pomocą klawiatury, 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