Tworzenie komponentu etykietki

Podstawowe omówienie sposobu tworzenia elementu niestandardowego z etykietką, który dostosowuje kolor i jest dostępny.

W tym poście chcę podzielić się moimi przemyśleniami na temat tworzenia <tool-tip>elementu niestandardowego, który dostosowuje się do koloru i jest dostępny. Wypróbuj wersję demonstracyjnąwyświetl kod źródłowy.

Etykietka jest wyświetlana w różnych przykładach i schematach kolorów.

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

Przegląd

Etykietka to niemodalna, nieblokująca i nieinteraktywna nakładka zawierająca dodatkowe informacje o interfejsach użytkownika. Jest domyślnie ukryty i staje się widoczny, gdy użytkownik najedzie kursorem na powiązany element lub go zaznaczy. Etykietki nie można wybrać ani z nią bezpośrednio wejść w interakcję. Etykietki nie zastępują etykiet ani innych ważnych informacji. Użytkownik powinien być w stanie w pełni wykonać zadanie bez etykietki.

Zalecane działanie: zawsze oznaczaj dane wejściowe.
Nie: polegaj na etykietkach zamiast na etykietach.

Etykietka przełączana a etykietka

Podobnie jak w przypadku wielu komponentów, istnieją różne opisy tego, czym jest etykietka narzędzi, np. w MDN, WAI ARIA, Sarah HigleyInclusive Components. Podoba mi się rozdzielenie etykietek i przełączników. Etykietka narzędzi powinna zawierać nieinteraktywne informacje dodatkowe, a etykietka przełączana może zawierać interaktywne i ważne informacje. Głównym powodem podziału jest dostępność. Jak użytkownicy mają nawigować do wyskakującego okienka i uzyskać dostęp do informacji oraz przycisków w nim zawartych? Toggletipy szybko stają się skomplikowane.

Oto film przedstawiający wysuwany panel z witryny Designcember. Jest to nakładka z elementami interaktywnymi, którą użytkownik może przypiąć i przeglądać, a następnie zamknąć, klikając poza nią lub naciskając klawisz Escape:

W tym wyzwaniu dotyczącym interfejsu użytkownika zastosowano podpowiedź, która ma być w większości oparta na CSS. Oto jak ją utworzyć.

Znacznik

Wybrałem(-am) element niestandardowy <tool-tip>. Autorzy nie muszą przekształcać elementów niestandardowych w komponenty internetowe, jeśli nie chcą. Przeglądarka będzie traktować <foo-bar> tak samo jak <div>. Element niestandardowy można traktować jako nazwę klasy o mniejszej precyzyjności. Nie jest wymagany JavaScript.

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

To jest jak element div z tekstem w środku. Możemy powiązać się z drzewem ułatwień dostępu kompatybilnych czytników ekranu, dodając [role="tooltip"].

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

Teraz czytniki ekranu rozpoznają go jako etykietkę. Czy widzisz w przykładzie poniżej, że pierwszy element linku ma w drzewie rozpoznany element etykietki, a drugi nie? Drugi użytkownik 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, które reprezentuje kod HTML. Wyświetla zaznaczalny link z tekstem „top ; Has tooltip: Hey, a tooltip!” (góra; ma etykietkę: Hej, etykietka!). W jego wnętrzu znajduje się statyczny tekst „top” i element etykietki.

Następnie musimy sprawić, aby etykietka nie była elementem, który można zaznaczyć. Jeśli czytnik ekranu nie rozpoznaje roli etykietki, umożliwi użytkownikom skupienie się na elemencie <tool-tip>, aby odczytać jego zawartość, co nie jest potrzebne. Czytniki ekranu dołączą treść do elementu nadrzędnego, więc nie musi on być zaznaczony, aby był dostępny. W tym miejscu możemy użyć inert, aby mieć pewność, że żaden użytkownik nie znajdzie przypadkowo treści tego elementu podpowiedzi w przepływie kart:

<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 brakuje elementu
tooltip.

Następnie wybrałem atrybuty jako interfejs do określania pozycji etykietki. Domyślnie wszystkie <tool-tip> przyjmują pozycję „top”, ale można ją dostosować w przypadku poszczególnych elementów, dodając tip-position:

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

Zrzut ekranu linku z etykietką po prawej stronie z tekstem „Etykietka”.

Zwykle używam atrybutów zamiast klas w przypadku takich rzeczy, aby element <tool-tip> nie mógł mieć jednocześnie przypisanych wielu pozycji. Może być tylko jedna lub żadna.

Na koniec umieść elementy <tool-tip> w elemencie, dla którego chcesz wyświetlać etykietkę. Tekst alt udostępniam użytkownikom widzącym, umieszczając obraz i <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 i etykietką z tekstem „Logo GUI Challenges w kształcie czaszki”.

Tutaj umieszczam element <tool-tip> w elemencie <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 akapitem, w którym skrót HTML jest podkreślony, a nad nim znajduje się etykietka z tekstem „Hyper Text Markup Language”.

Ułatwienia dostępu

Ponieważ wybrałem tworzenie etykietek, a nie etykietek przełączanych, ta sekcja jest znacznie prostsza. Najpierw opiszę, jakie wrażenia użytkownika chcemy osiągnąć:

  1. W przypadku ograniczonej przestrzeni lub przeładowanych interfejsów ukrywaj dodatkowe wiadomości.
  2. Gdy użytkownik najedzie kursorem na element, wybierze go lub dotknie, wyświetl komunikat.
  3. Gdy wskaźnik myszy przestanie wskazywać element, element przestanie być aktywny lub dotyk się zakończy, ponownie ukryj wiadomość.
  4. Na koniec upewnij się, że ruch jest ograniczony, jeśli użytkownik określił preferencję dotyczącą ograniczenia ruchu.

Naszym celem jest wyświetlanie dodatkowych wiadomości na żądanie. Użytkownik korzystający z myszy lub klawiatury może najechać kursorem na wiadomość, aby ją wyświetlić i przeczytać. Użytkownik czytnika ekranu, który nie widzi, może skupić się na wiadomości, aby ją odsłonić i usłyszeć za pomocą swojego narzędzia.

Zrzut ekranu przedstawiający czytnik ekranu VoiceOver w systemie macOS odczytujący link z etykietką

W poprzedniej sekcji omówiliśmy drzewo dostępności, rolę etykietki i atrybut inert. Pozostało nam przetestować to rozwiązanie i sprawdzić, czy użytkownikowi wyświetla się odpowiedni komunikat etykietki. Podczas testowania nie było jasne, która część komunikatu dźwiękowego jest etykietką. Można to też zobaczyć podczas debugowania w drzewie ułatwień dostępu. Tekst linku „top” jest połączony bez wahania z tekstem „Look, tooltips!”. Czytnik ekranu nie dzieli ani nie rozpoznaje tekstu jako treści etykietki.

Zrzut ekranu przedstawiający drzewo dostępności w Narzędziach deweloperskich w Chrome, w którym tekst linku to „top Hey, a tooltip!”.

Dodaj do elementu <tool-tip> pseudoelement dostępny tylko dla czytników ekranu, aby dodać własny tekst podpowiedzi dla osób niewidomych.

&::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 znajdziesz zaktualizowane drzewo ułatwień dostępu, które zawiera teraz średnik po tekście linku i prompt do etykietki „Has tooltip: ” (Zawiera etykietkę:).

Zaktualizowany zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach dla programistów w Chrome, na którym tekst linku ma poprawioną formę: „top ; Has tooltip: Hey, a tooltip!”.

Teraz, gdy użytkownik czytnika ekranu skupi się na linku, usłyszy „góra”, a potem po krótkiej przerwie „ma etykietkę: zobacz etykietki”. Dzięki temu użytkownik czytnika ekranu otrzyma kilka przydatnych wskazówek dotyczących wygody użytkowania. Dzięki temu tekst linku jest wyraźnie oddzielony od etykietki. Dodatkowo, gdy zostanie odczytany komunikat „zawiera etykietkę”, użytkownik czytnika ekranu może łatwo go anulować, jeśli słyszał go już wcześniej. Przypomina to szybkie najeżdżanie kursorem i cofanie go, ponieważ dodatkowy komunikat jest już widoczny. To było dobre wyrównanie UX.

Style

Element <tool-tip> będzie elementem podrzędnym elementu, dla którego ma wyświetlać dodatkowe informacje, więc zacznijmy od podstawowych elementów 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 układania, etykietka zostanie umieszczona w najbliższym kontekście układania, co nie jest pożądane. Na bloku pojawił się nowy selektor, który może Ci pomóc: :has().

Browser Support

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

Source

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

Nie martw się zbytnio obsługą przeglądarek. Pamiętaj, że te podpowiedzi są dodatkowe. Jeśli nie działają, nie powinno być problemu. Po drugie, w sekcji JavaScript wdrożymy skrypt, który uzupełni funkcje potrzebne w przeglądarkach bez obsługi :has().

Następnie sprawimy, że etykietki nie będą interaktywne, aby nie przechwytywały zdarzeń wskaźnika z elementu nadrzędnego:

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

Następnie ukryj etykietkę z przezroczystością, aby można było przejść do niej za pomocą efektu przenikania:

tool-tip {
  opacity: 0;
}

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

:is():has() wykonują tu najważniejszą pracę, informując element tool-tip zawierający elementy nadrzędne o interakcjach użytkownika, aby przełączać widoczność podrzędnego elementu podpowiedzi. Użytkownicy myszy mogą najeżdżać kursorem, użytkownicy klawiatury i czytnika ekranu mogą zaznaczać, a użytkownicy urządzeń dotykowych mogą klikać.

Gdy funkcja wyświetlania i ukrywania nakładki działa już w przypadku użytkowników widzących, czas dodać style do motywów, pozycjonowania i dodawania trójkątnego kształtu do dymku. Poniższe style zaczynają korzystać z właściwości niestandardowych, bazując na tym, co już mamy, ale dodając też cienie, typografię i kolory, aby wyglądało to jak pływająca etykietka:

Zrzut ekranu z etykietką w trybie ciemnym, która pojawia się po najechaniu kursorem na link „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;
}

Dostosowywanie motywu

Etykietka ma tylko kilka kolorów do zarządzania, ponieważ kolor tekstu jest dziedziczony ze strony za pomocą słowa kluczowego systemu CanvasText. Ponieważ utworzyliśmy właściwości niestandardowe do przechowywania wartości, możemy aktualizować tylko te właściwości, a resztę pozostawić motywowi:

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

Zrzut ekranu z jasną i ciemną wersją etykietki narzędziowej obok siebie.

W przypadku jasnego motywu dostosowujemy tło do koloru białego i znacznie zmniejszamy intensywność cieni, dostosowując ich krycie.

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że to pomóc w określeniu położenia etykietki:

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

Pomoże też określić, gdzie znajduje się 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 też używać do przekształceń logicznych w przypadku translateX():

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

Pozycjonowanie etykietki

Ustaw pozycję etykietki w logiczny sposób za pomocą właściwości inset-block lub inset-inline, aby obsługiwać zarówno fizyczne, jak i logiczne pozycje etykietki. Poniższy kod pokazuje, jak każdy z 4 rodzajów pozycji jest stylizowany w przypadku kierunku pisania od lewej do prawej i od prawej do lewej.

Wyrównanie do góry i do początku bloku

Zrzut ekranu pokazujący różnicę w położeniu między pozycją od lewej do prawej u góry a pozycją od prawej do lewej u góry.

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 na końcu wiersza

Zrzut ekranu pokazujący różnicę w umiejscowieniu między pozycją od lewej do prawej a pozycją 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 do końca bloku

Zrzut ekranu pokazujący różnicę w położeniu między pozycją od lewej do prawej na dole a pozycją od prawej do lewej na końcu bloku.

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 na początku wiersza

Zrzut ekranu pokazujący różnicę w położeniu między pozycją od lewej do prawej po lewej stronie a pozycją od prawej do lewej na początku wiersza.

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 przełączaliśmy tylko widoczność etykietki. W tej sekcji najpierw animujemy krycie dla wszystkich użytkowników, ponieważ jest to ogólnie bezpieczne przejście z ograniczonym ruchem. Następnie animujemy pozycję przekształcenia, aby etykietka narzędzi wysuwała się z elementu nadrzędnego.

Bezpieczne i sensowne domyślne przejście

Nadaj elementowi etykietki stylu przejścia nieprzezroczystości i przekształcenia, np. tak:

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

W przypadku każdego z boków, na których może pojawić się etykietka, jeśli użytkownik akceptuje ruch, nieznacznie zmień pozycję właściwości translateX, aby zapewnić niewielką odległość, jaką musi pokonać:

@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 ustawiasz stan „out”, ponieważ stan „in” ma wartość translateX(0).

JavaScript

Moim zdaniem JavaScript jest opcjonalny. Żadna z tych podpowiedzi nie powinna być niezbędna do wykonania zadania w interfejsie. Jeśli więc etykietki całkowicie zawiodą, nie powinno to stanowić większego problemu. Oznacza to również, że możemy traktować etykietki jako stopniowo ulepszane. Ostatecznie wszystkie przeglądarki będą obsługiwać :has() i ten skrypt będzie można całkowicie usunąć.

Skrypt polyfill wykonuje 2 czynności i robi to tylko wtedy, gdy przeglądarka nie obsługuje :has(). Najpierw sprawdź, czy :has() jest obsługiwane:

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

Następnie znajdź elementy nadrzędne elementów <tool-tip> i nadaj im nazwę klasy, aby z nimi pracować:

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

Następnie wstaw zestaw stylów, które używają tej nazwy klasy, symulując :has()selektor, aby uzyskać dokładnie to samo zachowanie:

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ć etykietki, jeśli element :has() nie jest obsługiwany.

Podsumowanie

Teraz, gdy wiesz, jak to zrobiłem, jak Ty byś to zrobił? 🙂 Nie mogę się doczekać interfejsu API, który ułatwi tworzenie podpowiedzi, najwyższej warstwy, która pozwoli uniknąć problemów z indeksem z, oraz interfejsu API, który ułatwi pozycjonowanie elementów w oknie.popupanchor Do tego czasu będę tworzyć etykietki.

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

Na razie jest tu pusto.

Zasoby