Tworzenie komponentu złożonego z przycisku

Podstawowe informacje o tworzeniu łatwo dostępnego komponentu podzielonego przycisku.

W tym poście przedstawię pomysł na stworzenie podzielonego przycisku . Wypróbuj wersję demonstracyjną

Demonstracja

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ą:

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

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.

Elementy HTML tworzące przycisk podziału.

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.

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

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.

Inspektor pokazujący reguły CSS dla elementu przycisku.

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.

Inspektor pokazujący reguły CSS dla klasy gui-popup-button.

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.

Inspektor pokazujący reguły CSS dla wyskakującego okienka gui klasy

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.

Inspektor pokazujący reguły CSS dla elementu przycisku.

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

Przycisk podziału.

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

Strzałka na przycisku podziału używana do wyświetlenia 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);
}

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.

Pływający element karty.

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

Linki i ikony płatności, Quick Pay 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 z ciemnym motywem ma dodany cień, tekst i ikony, a także nieco bardziej intensywny cień pola:

Wyskakujące okienko z ciemnym motywem.

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

Remiksy społeczności