Tworzenie komponentu etykietki

Podstawowy opis tworzenia elementu niestandardowego tooltipu, który dostosowuje kolory i zapewnia dostępność.

W tym poście chcę podzielić się z Wami swoimi przemyśleniami na temat tworzenia elementu niestandardowego <tool-tip> dostosowywanego do kolorów i dostępnego dla osób niepełnosprawnych. Wypróbuj wersję demonstracyjną i zobacz źródło.

Etykieta wyświetlana w różnych przykładach i schematach kolorów

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

Omówienie

Opis to niemodalna, nieblokująca i nieinteraktywna nakładka zawierająca dodatkowe informacje dla interfejsów. Domyślnie jest ukryty i staje się widoczny, gdy najedziesz kursorem na powiązany element lub go zaznaczysz. Etykietki nie można wybrać ani z nią bezpośrednio wchodzić w interakcję. Etykietki nie zastępują etykiet ani innych cennych informacji. Użytkownik powinien mieć możliwość pełnego wykonania zadania bez etykietki.

Zaleca się: zawsze etykietuj dane wejściowe.
Nie: zamiast etykiet używaj etykiet pomocniczych.

Etykieta a etykieta przełącznika

Podobnie jak w przypadku wielu innych komponentów, i w tym przypadku istnieją różne opisy tego, czym jest tooltip. Można je znaleźć na przykład w MDN, WAI ARIA, Sarah HigleyUwzględnienie wszystkich w komponentach. Podoba mi się rozróżnienie między podpowiedziami i przełącznikami. Informacje w tooltipie powinny być nieinterakcyjne, a w przypadku toggletipa mogą zawierać interaktywność i ważne informacje. Głównym powodem różnicy jest dostępność, czyli sposób, w jaki użytkownicy mają się dostać do wyskakującego okienka i uzyskać dostęp do informacji oraz przycisków. Wskazówki stają się szybko skomplikowane.

Oto film z poradnikiem ze strony Designcember; nakładka z interakcjami, którą użytkownik może przypiąć i przeglądać, a następnie zamknąć, naciskając jasny przycisk zamykania lub naciskając klawisz Escape:

W tym wyzwaniu dotyczącym interfejsu użytkownika użyliśmy etykietki narzędzia, która prawie wszystko robi za pomocą CSS. Poniżej znajdziesz instrukcje jej tworzenia.

Znacznik

Używam elementu niestandardowego <tool-tip>. Autorzy nie muszą dodawać niestandardowych elementów do komponentów internetowych, jeśli nie chcą tego robić. Przeglądarka będzie traktować <foo-bar> tak samo jak <div>. Element niestandardowy to na przykład nazwa klasy o mniej specyficznym charakterze. Nie używamy JavaScriptu.

<tool-tip>A tooltip</tool-tip>

To jak element div z pewnym tekstem. Możemy połączyć się z drzewem ułatwień dostępu czytników ekranu obsługujących tę funkcję, dodając [role="tooltip"].

<tool-tip role="tooltip">A tooltip</tool-tip>

Teraz czytniki ekranu rozpoznają to jako etykietę. W tym przykładzie pierwszy element linku ma w drzewie rozpoznany element etykiety, a drugi nie. Druga nie ma tej roli. W sekcji stylów ulepszymy ten widok drzewa.

Zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach deweloperskich w Chrome przedstawiające kod HTML. Wyświetla link z tekstem „góra ” oraz etykietką: „Hej, etykietka!”, którą można zaznaczyć. Wewnątrz znajduje się tekst statyczny „top” i element tooltip.

Następnie musimy ustawić, aby nie można było skupić kursora na opisie. Jeśli czytnik ekranu nie rozumie roli tooltipa, użytkownicy będą musieli ustawić fokus na elemencie <tool-tip>, aby przeczytać jego zawartość. Nie jest to jednak konieczne. Czytniki ekranu dołączą zawartość do elementu nadrzędnego, dlatego nie trzeba go zaznaczać, aby był dostępny. Możemy tu użyć inert, aby żaden użytkownik nie znalazł tego tekstu na karcie:

<tool-tip inert role="tooltip">A tooltip</tool-tip>

Kolejny zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach dla programistów w Chrome. Tym razem element Tooltip (Wskazówka) jest niewidoczny.

Następnie wybrałem użycie atrybutów jako interfejsu do określania pozycji etykietki. Domyślnie wszystkie elementy <tool-tip> przyjmują pozycję „u góry”, ale można ją dostosować, dodając element tip-position:

<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>

Zrzut ekranu z linkiem z etykietką po prawej stronie o treści „Podpowiedź”.

W takich przypadkach używam atrybutów zamiast klas, aby element <tool-tip> nie miał przypisanych kilku pozycji jednocześnie. Może być tylko 1 lub brak.

Na koniec umieść elementy <tool-tip> w elemencie, do którego chcesz dodać tekst pomocniczy. Tutaj udostępniam tekst alt użytkownikom ze wzrokiem, umieszczając obraz i element <tool-tip> w elemencie <picture>:

<picture>
  <img alt="The GUI Challenges skull logo" width="100" src="...">
  <tool-tip role="tooltip" tip-position="bottom">
    The <b>GUI Challenges</b> skull logo
  </tool-tip>
</picture>

Zrzut ekranu z obrazem z etykietą „The GUI Challenges skull
logo”.

Tutaj umieszczam element <tool-tip> wewnątrz elementu <abbr>:

<p>
  The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>

Zrzut ekranu z akronimiem HTML podkreślonym kursorem i informacją „Hyper Text Markup Language”.

Ułatwienia dostępu

Wybraliśmy podpowiedzi, a nie przełączam wskazówki, więc ta sekcja jest znacznie prostsza. Najpierw opiszę, jakie wrażenia chcemy zapewnić użytkownikom:

  1. W ograniczonych przestrzeniach lub na zatłoczonych interfejsach ukryj dodatkowe komunikaty.
  2. Wiadomość wyświetli się, gdy użytkownik najedzie kursorem na element, zaznaczy go lub kliknie dotyk w celu interakcji z elementem.
  3. Gdy kończy się najechanie kursorem, skupienie lub dotknięcie, ukryj ponownie wiadomość.
  4. Na koniec upewnij się, że ruch jest ograniczony, jeśli użytkownik preferuje ograniczony ruch.

Naszym celem jest dostarczanie dodatkowych wiadomości na żądanie. Osoba widząca może użyć myszy lub klawiatury, aby wyświetlić wiadomość i ją przeczytać. Użytkownik czytnika ekranu może skupić się na wiadomości, aby ją wyświetlić, i usłyszeć ją za pomocą narzędzia.

Zrzut ekranu z VoiceOver w systemie MacOS czytającym link z opisem

W poprzedniej sekcji omówiliśmy drzewo dostępności, rolę tooltipa i stan nieaktywny. Teraz wystarczy przetestować i sprawdzać, czy użytkownik zobaczy odpowiedni komunikat. Podczas testowania nie było jasne, która część wiadomości audio jest etykietą. Można to też zobaczyć podczas debugowania w drzewie ułatwień dostępu. Tekst linku „top” jest uruchamiany razem, bez wahania, z „Look, tooltips!”. Czytnik ekranu nie działa zepsuć ani nie rozpoznaje tekstu jako treści etykietki.

Zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach deweloperskich w Chrome z tekstem linku o treści „OK, etykietka!”.

Dodaj do elementu <tool-tip> pseudoelement przeznaczony tylko dla czytników ekranu, a my dodamy własny tekst promptu dla niewidomych użytkowników.

&::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

Poniżej możesz zobaczyć zaktualizowane drzewo ułatwień dostępu, które zawiera teraz średnik po tekście linku i prompt dla opisu narzędzia „Ma opis narzędzia:”.

Zaktualizowany zrzut ekranu przedstawiający stronę Accessibility Tree w Narzędziach dla programistów w Chrome, na której tekst linku ma ulepszoną formę, a w polu z poradami wyświetla się komunikat „Hey, a tooltip!”.

Teraz gdy użytkownik czytnika ekranu zaznaczy link, wyświetli się informacja „u góry”, po czym nastąpi drobna pauza, a potem wyświetli się informacja „zawiera etykietkę: wygląd, etykietki”. Dzięki temu czytnik ekranu wyświetli użytkownikowi kilka przydatnych wskazówek dotyczących wygody użytkownika. Zatrzymanie oddziela tekst linku od opisu. Dodatkowo po odczytaniu komunikatu „zawiera etykietkę” użytkownik czytnika ekranu może łatwo anulować tę informację, jeśli już ją słyszał. Przypomina to szybkie najechanie kursorem na reklamę, jak już wiesz z przekazem uzupełniającym. To było bardzo dobre rozwiązanie.

Style

Element <tool-tip> będzie elementem podrzędnym elementu, którego dotyczy, czyli dodatkowej wiadomości, więc zacznijmy od podstaw efektu nakładki. Wyłącz go z obiegu dokumentów za pomocą position absolute:

tool-tip {
  position: absolute;
  z-index: 1;
}

Jeśli element nadrzędny nie jest kontekstem nakładania, tekst podpowiedzi zostanie umieszczony w najbliższym takim kontekście, co nie jest pożądanym działaniem. W bloku jest nowy selektor, który może Ci pomóc: :has().

Obsługa przeglądarek

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Źródło

:has(> tool-tip) {
  position: relative;
}

Nie martw się zbytnio obsługą przeglądarki. Pamiętaj, że te etykiety są tylko dodatkiem. Jeśli nie, to nie powinno być problemu. Po drugie, w sekcji JavaScript wdrożymy skrypt, który spowoduje polyfillowanie funkcji potrzebnych w przypadku przeglądarek bez obsługi :has().

Ustawmy etykietki jako nieinteraktywne, aby nie wykradły zdarzeń wskaźnika z elementu nadrzędnego:

tool-tip {
  
  pointer-events: none;
  user-select: none;
}

Następnie ukryj etykietę za pomocą funkcji ustawiania przezroczystości, aby można było ją przełączyć za pomocą efektu przejścia:

tool-tip {
  opacity: 0;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
}

:is():has() wykonują tu większość pracy, dzięki czemu tool-tip zawierające elementy nadrzędne są świadome interakcji użytkownika, co pozwala włączać i wyłączać widoczność podrzędnego tooltipa. Użytkownicy myszy mogą najechać kursorem, użytkownicy klawiatury i czytników ekranu mogą ustawić fokus, a użytkownicy urządzeń dotykowych mogą kliknąć.

Ponieważ wyświetlanie i ukrywanie nakładki działa już dla widzących użytkowników, nadszedł czas na dodanie stylów do motywu, pozycjonowania i dodania trójkąta do okienka. W następujących stylach zaczynamy używać właściwości niestandardowych, które rozbudowują to, co już mamy, ale też dodają cienie, typografię i kolory, aby wyglądało to jak unosząca się tooltip:

Zrzut ekranu z poradami w ciemnym trybie, który pojawia się nad linkiem „block-start”.

tool-tip {
  --_p-inline: 1.5ch;
  --_p-block: .75ch;
  --_triangle-size: 7px;
  --_bg: hsl(0 0% 20%);
  --_shadow-alpha: 50%;

  --_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
  --_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
  --_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
  --_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;

  pointer-events: none;
  user-select: none;

  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;

  position: absolute;
  z-index: 1;
  inline-size: max-content;
  max-inline-size: 25ch;
  text-align: start;
  font-size: 1rem;
  font-weight: normal;
  line-height: normal;
  line-height: initial;
  padding: var(--_p-block) var(--_p-inline);
  margin: 0;
  border-radius: 5px;
  background: var(--_bg);
  color: CanvasText;
  will-change: filter;
  filter:
    drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
    drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}

/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
  position: relative;
}

/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

/* prepend some prose for screen readers only */
tool-tip::before {
  content: "; Has tooltip: ";
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
  content: "";
  background: var(--_bg);
  position: absolute;
  z-index: -1;
  inset: 0;
  mask: var(--_tip);
}

/* top tooltip styles */
tool-tip:is(
  [tip-position="top"],
  [tip-position="block-start"],
  :not([tip-position]),
  [tip-position="bottom"],
  [tip-position="block-end"]
) {
  text-align: center;
}

Dostosowania motywu

Wskazówka ma tylko kilka kolorów, ponieważ kolor tekstu jest dziedziczony ze strony za pomocą słowa kluczowego systemu CanvasText. Utworzyliśmy też właściwości niestandardowe do przechowywania wartości, więc możemy zaktualizować tylko te właściwości, a motyw zajmie się resztą:

@media (prefers-color-scheme: light) {
  tool-tip {
    --_bg: white;
    --_shadow-alpha: 15%;
  }
}

Zrzut ekranu z wersją jaśniej i ciemnej etykiety.

W przypadku motywu jasnego tło jest białe, a cienie są znacznie słabsze, ponieważ zmieniamy ich przezroczystość.

Od prawej do lewej

Aby obsługiwać tryby czytania od prawej do lewej, właściwość niestandardowa będzie przechowywać wartość kierunku dokumentu jako -1 lub 1.

tool-tip {
  --isRTL: -1;
}

tool-tip:dir(rtl) {
  --isRTL: 1;
}

Możesz go użyć, aby ustawić pozycję etykiety:

tool-tip[tip-position="top"]) {
  --_x: calc(50% * var(--isRTL));
}

Pomożemy Ci też znaleźć trójkąt:

tool-tip[tip-position="right"]::after {
  --_tip: var(--_left-tip);
}

tool-tip[tip-position="right"]:dir(rtl)::after {
  --_tip: var(--_right-tip);
}

Można ich też używać do transformacji logicznych w przypadku translateX():

--_x: calc(var(--isRTL) * -3px * -1);

Umieszczenie etykiety

logicznie ułóż etykietkę z właściwościami inset-block lub inset-inline, aby obsługiwać zarówno fizyczne, jak i logiczne pozycje etykietki. Poniższy kod pokazuje, jak stylizowane są 4 pozycje w przypadku kierunków od lewej do prawej i od prawej do lewej.

Wyrównanie do góry i blokowanie

Zrzut ekranu pokazujący różnicę między pozycją górną od lewej do prawej a pozycją górną od prawej do lewej.

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
  inset-inline-start: 50%;
  inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
  --_tip: var(--_bottom-tip);
  inset-block-end: calc(var(--_triangle-size) * -1);
  border-block-end: var(--_triangle-size) solid transparent;
}

Wyrównanie do prawej i do końca wiersza

Zrzut ekranu pokazujący różnicę między pozycją prawej strony od lewej do prawej a pozycją prawej strony od prawej do lewej.

tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
  inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
  --_tip: var(--_left-tip);
  inset-inline-start: calc(var(--_triangle-size) * -1);
  border-inline-start: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
  --_tip: var(--_right-tip);
}

Wyrównanie do dołu i zakończenia bloku

Zrzut ekranu pokazujący różnicę między umieszczeniem w lewym dolnym rogu a umieszczeniem na końcu bloku po prawej stronie.

tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
  inset-inline-start: 50%;
  inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
  --_x: calc(50% * var(--isRTL));
}

tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
  --_tip: var(--_top-tip);
  inset-block-start: calc(var(--_triangle-size) * -1);
  border-block-start: var(--_triangle-size) solid transparent;
}

wyrównanie do lewej i wyrównanie do początku w wierszu.

Zrzut ekranu pokazujący różnicę między pozycją lewostronną (z lewej do prawej) a pozycją początkową w wierszu (z prawej do lewej).

tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
  inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
  inset-block-end: 50%;
  --_y: 50%;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
  --_tip: var(--_right-tip);
  inset-inline-end: calc(var(--_triangle-size) * -1);
  border-inline-end: var(--_triangle-size) solid transparent;
}

tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
  --_tip: var(--_left-tip);
}

Animacja

Do tej pory zmieniliśmy tylko widoczność opisu. W tej sekcji najpierw pokażemy animację przezroczystości dla wszystkich użytkowników, ponieważ jest to ogólnie bezpieczna animacja z ograniczonymi ruchami. Następnie utworzymy animację zmiany pozycji, aby wskazówka wyglądała na wysuniętą z elementu nadrzędnego.

Bezpieczne i sensowne domyślne przejście

Dostosuj element etykietki do przezroczystości i przekształcenia w ten sposób:

tool-tip {
  opacity: 0;
  transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
  transition: opacity .2s ease, transform .2s ease;
}

:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
  opacity: 1;
  transition-delay: 200ms;
}

Dodawanie ruchu do przejścia

Jeśli użytkownik zgadza się na ruch, dla każdego z boków, na których może pojawić się tooltip, ustaw właściwość translateX, podając niewielką odległość do przebycia z:

@media (prefers-reduced-motion: no-preference) {
  :has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: 3px;
  }

  :has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: -3px;
  }

  :has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_y: -3px;
  }

  :has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
    --_x: 3px;
  }
}

Zwróć uwagę, że jest ustawiony stan „out”, ponieważ stan „w” to translateX(0).

JavaScript

Uważam, że JavaScript jest opcjonalny. Wynika to z tego, że żadne z nich nie powinno być wymagane do wykonania zadania w interfejsie. Jeśli więc tooltipy całkowicie się nie wyświetlają, nie powinno to stanowić problemu. Oznacza to też, że możemy traktować etykietki jako stopniowo ulepszone. Ostatecznie wszystkie przeglądarki będą obsługiwać :has(), a skrypt będzie można całkowicie usunąć.

Skrypt polyfill ma 2 czynności i działa tylko wtedy, gdy przeglądarka nie obsługuje :has(). Najpierw sprawdź, czy :has() obsługuje:

if (!CSS.supports('selector(:has(*))')) {
  // do work
}

Następnie znajdź nadrzędne elementy <tool-tip> i nadaj im nazwę klasy, z którą będą współpracować:

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))
}

Następnie wstrzyknij zestaw stylów, które korzystają z tej nazwy klasy, symulując selektor :has() dokładnie tak samo:

if (!CSS.supports('selector(:has(*))')) {
  document.querySelectorAll('tool-tip').forEach(tooltip =>
    tooltip.parentNode.classList.add('has_tool-tip'))

  let styles = document.createElement('style')
  styles.textContent = `
    .has_tool-tip {
      position: relative;
    }
    .has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
      opacity: 1;
      transition-delay: 200ms;
    }
  `
  document.head.appendChild(styles)
}

To wszystko. Teraz wszystkie przeglądarki będą wyświetlać etykiety, jeśli :has() nie jest obsługiwana.

Podsumowanie

Teraz, gdy już wiesz, jak to zrobić, jak Ty to zrobisz? 🙂 Naprawdę nie mogę się doczekać, aż zobaczę interfejs popup API, który ułatwia tworzenie przełączników, warstwę górną, która eliminuje konieczność korzystania z indeksu z-poziomu, oraz interfejs anchor API, który ułatwia pozycjonowanie elementów w oknie. Do tego czasu będę tworzyć etykietki.

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

Na razie jest tu pusto.

Zasoby