Tworzenie komponentu złożonego z przycisku

Podstawowe informacje o tym, jak utworzyć komponent dostępnego przycisku dzielonego.

W tym poście chcę podzielić się przemyśleniami na temat sposobu tworzenia przycisku dzielonego . Wypróbuj wersję demonstracyjną

Wersja demonstracyjna

Jeśli wolisz film, obejrzyj tę wersję posta w YouTube:

Przegląd

Przyciski dzielone to przyciski, które ukrywają przycisk główny i listę dodatkowych przycisków. Są przydatne do udostępniania typowych działań, a jednocześnie do ukrywania działań dodatkowych, rzadziej używanych, do czasu, aż będą potrzebne. Przycisk dzielony może być kluczowy, aby uprościć złożony projekt. Zaawansowany przycisk dzielony może nawet zapamiętać ostatnie działanie użytkownika i przenieść je na pozycję główną.

Typowy przycisk dzielony można znaleźć w aplikacji poczty e-mail. Głównym działaniem jest wysłanie, ale możesz też wysłać wiadomość później lub zapisać ją jako wersję roboczą:

Przykładowy przycisk podziału widoczny w aplikacji poczty e-mail.

Wspólny obszar działania jest przydatny, ponieważ użytkownik nie musi się rozglądać. Wiedzą, że najważniejsze działania związane z e-mailami znajdują się w podzielonym przycisku.

Części

Zanim omówimy ogólną koordynację i wrażenia użytkownika, przyjrzyjmy się najważniejszym częściom przycisku dzielonego. Narzędzie do sprawdzania dostępności VisBug zostało użyte w tym przypadku, aby pokazać widok makro komponentu, uwidaczniając aspekty kodu HTML, stylu i dostępności każdej głównej części.

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

Kontener przycisku podziału najwyższego poziomu

Komponent najwyższego poziomu to elastyczne pole wbudowane z klasą gui-split-button, które zawiera główne działanie.gui-popup-button.

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

Główny przycisk działania

Początkowo widoczny i możliwy do zaznaczenia element <button> mieści się w kontenerze i ma 2 pasujące kształty narożników, które w przypadku interakcji focus, hoveractive sprawiają, że element wydaje się być zawarty w elemencie .gui-split-button.

Inspektor wyświetlający reguły CSS elementu przycisku.

Przycisk przełączania wyskakującego okienka

Element „popup button” służy do aktywowania i wskazywania listy przycisków dodatkowych. Zwróć uwagę, że nie jest to <button> i nie można na nim ustawić fokusu. Jest to jednak punkt zakotwiczenia pozycji dla .gui-popup i host dla :focus-within używanego do wyświetlania wyskakującego okienka.

Inspektor wyświetlający reguły CSS dla klasy gui-popup-button.

Wyskakująca karta

Jest to karta pływająca podrzędna względem elementu zakotwiczonego .gui-popup-button, pozycjonowana bezwzględnie i semantycznie otaczająca listę przycisków.

Inspektor wyświetlający reguły CSS dla klasy gui-popup

działania dodatkowe,

Skupiony <button> z nieco mniejszym rozmiarem czcionki niż główny przycisk działania zawiera ikonę i styl uzupełniający styl głównego przycisku.

Inspektor wyświetlający reguły CSS elementu przycisku.

Właściwości niestandardowe

Poniższe 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 kolor

Znacznik

Element zaczyna się od tagu <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, aby czytniki ekranu mogły rozpoznawać funkcje i stan przycisku dzielonego. 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 element .gui-popup jest podrzędny względem przycisku, który je rozwija. Jedynym minusem tej strategii jest to, że kontener .gui-split-button nie może używać overflow: hidden, ponieważ spowoduje to odcięcie wyskakującego okienka od widoczności.

Element <ul> wypełniony treścią <li><button> będzie odczytywany przez czytniki ekranu jako „lista przycisków”, co dokładnie odpowiada wyświetlanemu interfejsowi.

<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ć stylu i koloru, dodałem ikony do przycisków dodatkowych ze strony 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 masz już kod HTML i treści, możesz zastosować style, aby określić kolor i układ.

Stylizowanie kontenera przycisku dzielonego

Typ wyświetlania inline-flex dobrze sprawdza się w przypadku tego komponentu opakowującego, ponieważ powinien pasować do innych przycisków dzielonych, 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 dzielony.

Styl <button>

Przyciski bardzo dobrze maskują ilość wymaganego kodu. Może być konieczne cofnięcie lub zastąpienie domyślnych stylów przeglądarki, ale trzeba też wymusić pewne dziedziczenie, dodać stany interakcji i dostosować się do różnych preferencji użytkowników oraz typów danych wejściowych. Style przycisków szybko się sumują.

Te przyciski różnią się od zwykłych przycisków, ponieważ mają wspólne tło z elementem nadrzędnym. Zwykle przycisk ma własny kolor tła i tekstu. Te jednak udostępniają je i stosują własne tło tylko w przypadku 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 użyj pasujących właściwości niestandardowych dla 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 uzyskać efekt projektowy:

.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ć nieco stylu, przycisk i ikona jasnego motywu otrzymują 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));
    }
  }
}

Dobry przycisk uwzględnia mikrointerakcje i drobne szczegóły.

Uwaga dotycząca :focus-visible

Zwróć uwagę, że style przycisków używają :focus-visible zamiast :focus. :focus to kluczowy element interfejsu użytkownika, który ułatwia korzystanie z niego osobom z niepełnosprawnościami. Ma jednak jedną wadę: nie jest inteligentny i nie wie, czy użytkownik musi go widzieć, czy nie. Zostanie zastosowany w przypadku każdego fokusu.

Film poniżej przedstawia tę mikrointerakcję i pokazuje, jak :focus-visible jest inteligentną alternatywą.

Stylizowanie przycisku wyskakującego okienka

4chFlexbox do wyśrodkowania ikony i zakotwiczenia listy przycisków wyskakującego okienka. Podobnie jak przycisk główny jest przezroczysty, dopóki nie zostanie najechany lub nie wejdzie w interakcję z użytkownikiem, i rozciąga się na całą szerokość.

Część strzałki przycisku podziału używana do wywoływania 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);
}

Nakładaj stany najechania kursorem, zaznaczenia i aktywności za pomocą zagnieżdżania CSS i selektora funkcyjnego :is():

.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 focus w dowolnym z elementów podrzędnych, ustaw opacity, position 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 zakończeniu stylów wejścia i wyjścia ostatnim elementem jest warunkowe przejście transformacji w zależności od preferencji użytkownika dotyczących ruchu:

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

Uważny obserwator kodu zauważy, że przezroczystość nadal jest zmieniana w przypadku użytkowników, którzy wolą ograniczone animacje.

Stylizowanie wyskakującego okienka

Element .gui-popup to lista przycisków w formie pływających kart, która wykorzystuje właściwości niestandardowe i jednostki względne, aby była nieco mniejsza, interaktywnie dopasowana do przycisku głównego i zgodna z marką dzięki zastosowaniu odpowiedniego koloru. Zwróć uwagę, że ikony mają mniejszy kontrast, są cieńsze, a cień ma odcień niebieskiego charakterystycznego dla marki. Podobnie jak w przypadku przycisków, silny interfejs użytkownika i UX to efekt kumulacji tych drobnych szczegółów.

Element karty pływającej.

.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, dzięki czemu dobrze wyglądają na kartach z jasnym i ciemnym motywem:

Linki i ikony do płatności, szybkiej płatności i zapisywania 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 ciemnego motywu ma dodatki w postaci cienia tekstu i ikony oraz nieco bardziej intensywny cień pola:

Wyskakujące okienko w trybie ciemnym.

.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 mają rozmiar względny w stosunku do przycisku font-size, w którym są używane. Rozmiar jest określany za pomocą jednostki ch jako inline-size. Każda ikona ma też przypisane style, które pomagają uzyskać miękkie i gładkie kontury.

.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żywanych właściwości logicznych: - display: inline-flex tworzy element elastyczny w linii. – padding-blockpadding-inline jako para zamiast skrótu padding – zyskaj korzyści z dopełnienia po stronach logicznych. – border-end-start-radiusznajomi będą zaokrąglać rogi w zależności od kierunku dokumentu. – inline-size zamiast width zapewnia, że rozmiar nie jest powiązany z wymiarami fizycznymi. – border-inline-start dodaje obramowanie na początku, które może znajdować się po prawej lub lewej stronie w zależności od kierunku pisania.

JavaScript

Prawie cały poniższy kod JavaScript służy do zwiększania dostępności. Do ułatwienia sobie pracy używam 2 bibliotek pomocniczych. BlingBlingJS służy do zwięzłych zapytań DOM i łatwego konfigurowania odbiorników zdarzeń, a roving-ux ułatwia dostępność interakcji z klawiaturą i gamepadem 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 wybraniu i zapisaniu elementów w zmiennych ulepszenie interfejsu będzie wymagać tylko kilku funkcji.

Indeks ruchomy

Gdy klawiatura lub czytnik ekranu zaznaczy element .gui-popup-button, chcemy przekierować fokus na pierwszy (lub ostatnio zaznaczony) przycisk w elemencie .gui-popup. Biblioteka pomaga nam to zrobić 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 przeglądanie opcji za pomocą standardowych klawiszy strzałek.

Przełączanie aria-expanded

Chociaż wizualnie widać, że wyskakujące okienko jest wyświetlane i ukrywane, czytnik ekranu potrzebuje więcej niż tylko wskazówek wizualnych. W tym przypadku język JavaScript uzupełnia interakcję :focus-within opartą na CSS, przełączając atrybut odpowiedni 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 klawisza Escape

Fokus użytkownika został celowo przeniesiony do pułapki, co oznacza, że musimy zapewnić sposób na jej opuszczenie. Najczęstszym sposobem jest zezwolenie na używanie klawisza Escape. W tym celu obserwuj naciśnięcia klawiszy na przycisku wyskakującego okienka, ponieważ wszystkie zdarzenia klawiatury dotyczące elementów podrzędnych 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 dowolnego klawisza Escape, usunie z siebie fokus za pomocą metody blur().

Kliknięcia przycisku podziału

Jeśli użytkownik kliknie, naciśnie lub użyje klawiatury do interakcji z przyciskami, aplikacja musi wykonać odpowiednie działanie. Ponownie używamy tu propagacji zdarzeń, ale tym razem w przypadku kontenera .gui-split-button, aby przechwytywać kliknięcia przycisków w podrzędnym wyskakującym okienku lub w głównym działaniu.

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

Podsumowanie

Teraz, gdy wiesz, jak to zrobiłem, jak Ty byś to zrobił? 🙂

Urozmaićmy nasze podejście i poznajmy wszystkie sposoby tworzenia treści w internecie. Utwórz demo, wyślij mi na Twitterze linki, a ja dodam je do sekcji remiksów społeczności poniżej.

Remiksy społeczności