Tworzenie komponentu etykietki

Podstawowe informacje o tworzeniu elementu niestandardowego z etykietami z możliwością dostosowania kolorów i z dostępem.

W tym poście chcę podzielić się przemyśleniami na temat tworzenia łatwo dostępnego i dostosowającego się do kolorów elementu niestandardowego <tool-tip>. Wypróbuj wersję demonstracyjną i wyświetl źródło.

Wyświetla się etykietka zgodna z różnymi przykładami i schematami kolorów

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

Przegląd

Etykietka to niemodalna, nieblokująca i nieinteraktywna nakładka z dodatkowymi informacjami o interfejsach. Jest domyślnie ukryte i staje się widoczne, gdy użytkownik najedzie kursorem na powiązany element lub go aktywuje. Nie można wybrać etykietki ani wchodzić z nią w interakcję bezpośrednio. Etykietki nie zastępują etykiet ani innych cennych informacji. Użytkownik powinien mieć możliwość ukończenia zadania bez etykietki.

Tak: zawsze oznacz etykietami dane wejściowe.
Nie korzystaj z etykiet zamiast etykiet.

Zamień wskazówkę i etykietkę

Podobnie jak w przypadku wielu komponentów, etykietki mają różne opisy, na przykład w MDN, WAI ARIA, Sarah Higley i Komponenty inkluzywne. Podoba mi się oddzielenie etykietek od etykietek. Etykietka powinna zawierać nieinteraktywne informacje dodatkowe, a wskazówka może zawierać elementy interaktywne i ważne informacje. Główną przyczyną tego podziału są ułatwienia dostępu, sposób poruszania się użytkowników do wyskakującego okienka i uzyskiwania dostępu do zawartych w nim informacji i przycisków. W przypadku opcji przełączania szpilki szybko się skomplikowanie.

Oto film z wskazówką ze strony Projektcember. Zawiera ona interaktywną nakładkę, którą użytkownik może przypiąć i przejrzeć, a następnie zamknąć za pomocą przycisku zamykania światłem lub klawisza Escape:

To wyzwanie GUI wyłoniło się z podpowiedziami, jak wykorzystać CSS do niemal wszystkich. Tutaj dowiesz się, jak go utworzyć.

Markup

Wybieram element niestandardowy <tool-tip>. Autorzy nie muszą tworzyć niestandardowych elementów w komponentach internetowych. Przeglądarka będzie traktować właściwość <foo-bar> tak samo jak <div>. Taki element to na przykład nazwa klasy o mniejszej szczegółowości. Nie musisz używać JavaScriptu.

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

Jest to element div z tekstem. Możemy dodać do drzewa ułatwień dostępu umiejętne czytniki ekranu, dodając element [role="tooltip"].

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

Czytniki ekranu są teraz rozpoznawane jako etykietki. Poniższy przykład pokazuje, jak pierwszy element link ma w drzewie rozpoznawany element etykietki, a drugi nie. Druga osoba nie ma tej roli. W sekcji Style poprawimy ten widok drzewa.

Zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach deweloperskich w Chrome, które przedstawia kod HTML. Wyświetla link z tekstem „top”; ma etykietkę: Hej, etykietka!”, którą można zaznaczyć. W środku znajduje się tekst statyczny „top” i element etykietki.

Teraz ta etykietka musi być wyłączona z możliwości zaznaczenia. Jeśli czytnik ekranu nie zrozumie roli etykietki, będzie mógł zaznaczyć element <tool-tip>, aby przeczytać treść. Obsługa nie jest potrzebna. Czytniki ekranu dołączają treść do elementu nadrzędnego, dzięki czemu jej dostępność nie wymaga zaznaczenia. Dzięki tagowi inert możemy mieć pewność, że żaden użytkownik nie przypadkowo natknie się na etykietkę w trakcie swojej karty:

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

Kolejny zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach deweloperskich w Chrome, ale tym razem brakuje elementu etykietki.

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

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

Zrzut ekranu z linkiem z etykietką po prawej stronie „Podpowiedź”.

W takich sytuacjach zamiast klas używam atrybutów, a <tool-tip> nie może mieć przypisanych do nich wielu pozycji naraz. Może być tylko jedna lub żadna.

Na koniec umieść elementy <tool-tip> wewnątrz elementu, dla którego chcesz dodać etykietkę. Udostępniam tekst alt osobom widzącym, umieszczając obraz i <tool-tip> wewnątrz elementu <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 etykietką „Logo The GUI Challenges skull”.

umieszczam <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 akronim HTML jest podkreślony, i etykietka z napisem „Hyper Text Markup Language”.

Ułatwienia dostępu

Ponieważ zdecydowałem się tworzyć etykietki, a nie przełączniki, ta sekcja jest znacznie prostsza. Na początek wyjaśnię, co chcemy zrobić dla użytkownika:

  1. Jeśli masz za mało przestrzeni lub zatłoczony interfejs, ukryj dodatkowe komunikaty.
  2. Gdy użytkownik najedzie kursorem na element, aktywuje jego zaznaczenie lub użyje dotyku, aby wejść w interakcję z elementem, wyświetl komunikat.
  3. Po najechaniu kursorem, wskazaniu lub dotknięciu powoduje ponowne ukrycie wiadomości.
  4. Jeśli użytkownik określił preferencję redukcji ruchu, upewnij się też, że ruch jest ograniczony.

Naszym celem jest uzupełnianie przekazu na żądanie. Osoba widząca mysz lub klawiaturę może najechać kursorem na wiadomość, aby ją odczytać. Niedowidzący użytkownik czytnika ekranu może skupić się na ujawnieniu komunikatu, odbierając go za pomocą narzędzia.

Zrzut ekranu przedstawiający funkcję VoiceOver w systemie macOS odczytującym link z etykietką

W poprzedniej sekcji omówiliśmy drzewo ułatwień dostępu, rolę etykietki i bezwłasność. Pozostaje jeszcze przetestowanie i sprawdzenie, czy interfejs użytkownika prawidłowo wyświetla etykietkę. Po przetestowaniu nie wiadomo, która część słyszalnego komunikatu to etykietka. Widać go też podczas debugowania w drzewie ułatwień dostępu. Tekst linku „top” jest cyklicznie tworzony razem, ale za pomocą polecenia „Look, tooltips!”. Czytnik ekranu nie psuje tekstu ani nie identyfikuje tekstu jako treści etykietki.

Zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach deweloperskich w Chrome, z tekstem linku „top Hey, a tooltip!”.

Dodaj do elementu <tool-tip> tylko pseudoelement z czytnikiem ekranu, a my będziemy mogli dodać własny tekst promptu dla 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 znajduje się zaktualizowane drzewo ułatwień dostępu: po tekście linku widać teraz średnik oraz etykietkę „Zawiera etykietkę ”.

Zaktualizowany zrzut ekranu przedstawiający drzewo ułatwień dostępu w Narzędziach deweloperskich w Chrome, z którym tekst linku ulepszono sformułowanie: „top”; zawiera etykietkę: Hey, a tooltip!”.

Teraz, gdy użytkownik czytnika ekranu zaznaczy link, wyświetla się napis „Do góry”, po chwili na chwilę pauza i komunikat „Zawiera etykietkę: spojrzenie, etykietki”. Daje to użytkownikowi czytnika ekranu kilka przydatnych wskazówek dotyczących UX. Dobrze odseparuje on tekst linku od etykietki. Dodatkowo po pojawieniu się komunikatu „ma etykietkę” użytkownik czytnika ekranu może go łatwo anulować, jeśli już go słyszał. Przypomina kojarzący się z szybkim uruchamianiem i usuwaniem najechania, tak jak zaobserwowaliśmy już dodatkowy komunikat. Wydaje się, że wszystko jest w porządku.

Style

Element <tool-tip> będzie elementem podrzędnym elementu, w którym reprezentuje dodatkowy komunikat. Zacznijmy więc od podstawowych informacji o efektu nakładki. Wyjdź z dokumentu za pomocą position absolute:

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

Jeśli element nadrzędny nie jest kontekstem skumulowanym, etykietka umieści się na najbliższym, a nie tego oczekujemy. Dostępny jest nowy selektor, który może Ci pomóc: :has():

Obsługa przeglądarek

  • 105
  • 105
  • 121
  • 15,4

Źródło

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

Nie przejmuj się zbytnio obsługą przeglądarek. Po pierwsze, pamiętaj, że etykietki są uzupełniające. Jeśli funkcja nie działa, wszystko powinno być w porządku. Po drugie, w sekcji JavaScript wdrożymy skrypt do uzupełniania funkcji wymaganych w przeglądarkach bez obsługi standardu :has().

Teraz zmienimy etykietki w nieinteraktywne, aby nie odbierały zdarzeń wskaźnika z elementu nadrzędnego:

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

Następnie ukryj etykietkę z przezroczystością, żebyśmy mogli ją przenieść z przenikaniem:

tool-tip {
  opacity: 0;
}

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

:is() i :has() wykonują najcięższą pracę w tym miejscu, dzięki czemu zawierające elementy nadrzędne tool-tip wiedzą o interakcji użytkownika z użytkownikiem i umożliwiają przełączanie widoczności etykietki podrzędnej. Użytkownicy korzystający z myszy mogą się poruszać po najechaniu kursorem, używać klawiatury i czytnika ekranu, a użytkownicy dotykowi – klikać.

Gdy nakładka Pokaż i ukrycie działa dla osób, które widzą, musisz dodać do bąbelka różne style, pozycjonować je i dodawać do bąbelka kształt trójkąta. Poniższe style zaczynają korzystać z właściwości niestandardowych, bazując na dotychczasowych miejscach, ale też dodając cienie, typografię i kolory, dzięki czemu wygląda ona jak pływająca etykietka:

Zrzut ekranu etykietki w trybie ciemnym 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;
}

Korekty motywu

Etykietka ma tylko kilka kolorów do zarządzania, ponieważ kolor tekstu jest dziedziczony ze strony przez systemowe słowo kluczowe CanvasText. Utworzyliśmy właściwości niestandardowe do przechowywania wartości, więc możemy aktualizować tylko te właściwości, a reszta zajmuje się motywem:

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

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

W przypadku jasnego motywu dostosowujemy tło na białe, a znacznie zmniejszamy ich intensywność, dostosowując ich przezroczystość.

Od prawej do lewej

Do obsługi trybów czytania od prawej do lewej właściwość niestandardowa zapisuje wartość kierunku dokumentu jako wartość odpowiednio -1 lub 1.

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

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

Może to pomóc w pozycjonowaniu etykietki:

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

Zwróć też uwagę na to, gdzie 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);
}

Funkcji tej można też używać do przekształceń logicznych w translateX():

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

Pozycjonowanie etykiet

Umieść etykietkę w logicznym położeniu zgodnie z właściwościami inset-block lub inset-inline, aby obsługiwać zarówno fizyczne, jak i logiczne pozycje etykiet. Poniższy kod przedstawia styl każdego z 4 pozycji w przypadku kierunków „od lewej do prawej” i „od prawej do lewej”.

Wyrównanie do góry i blok

Zrzut ekranu przedstawiający różnicę w miejscu docelowym między górną pozycją od lewej do prawej i 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 środka

Zrzut ekranu przedstawiający różnicę w miejscu docelowym między pozycjami reklamy wbudowanej od lewej do prawej i 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 bloku

Zrzut ekranu przedstawiający różnicę w pozycji między obszarem od lewej do prawej u dołu i od prawej do lewej.

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 w tekście

Zrzut ekranu przedstawiający różnicę w miejscu docelowym między lewą a stroną początkową od 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 przestawiliśmy tylko widoczność etykietki. W tej sekcji najpierw zaczniemy animować przezroczystość u wszystkich użytkowników, ponieważ jest to ogólnie bezpieczne z ograniczonym przejściem ruchu. Następnie przeprowadzimy animację pozycji przekształcenia, tak aby etykietka wysuwała się z elementu nadrzędnego.

Bezpieczne i wymierne przejście na nową wersję.

Określ styl elementu etykietki przezroczystość przejścia i przekształcenie 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;
}

Dodaje ruch do przejścia

Jeśli po każdej ze stron może pojawić się etykietka, jeśli użytkownik nie przeszkadza w ruchu, lekko ustaw właściwość translateX, oddalając ją o niewielką odległość:

@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 stan to „out”, ponieważ stan „w” to translateX(0).

JavaScript

Uważam, że obsługa JavaScript jest opcjonalna. Wynika to z faktu, że do wykonania zadania w interfejsie użytkownika nie powinno się wymagać odczytywania żadnej z tych etykiet. Jeśli więc etykietki całkowicie się nie powiodą, nie powinno to stanowić problemu. Oznacza to również, że etykietki możemy traktować jako stopniowo ulepszone. Ostatecznie wszystkie przeglądarki będą obsługiwać :has(), a ten skrypt będzie dostępny.

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

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

Następnie znajdź elementy nadrzędne elementów <tool-tip> i nadaj im nazwę klasy, z którą będą 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(), aby uzyskać dokładnie takie samo działanie:

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. Jeśli :has() nie będzie obsługiwany, wszystkie przeglądarki będą wyświetlać etykietki.

Podsumowanie

Skoro już wiesz, jak to zrobiłem, to jak to zrobić? 🙂 Czekam na interfejs API popup, który ułatwia korzystanie ze wskazówek, górną warstwę w przypadku braku bitew z indeksem oraz interfejs API anchor do lepszego umieszczania elementów w oknie. Będę używać etykiet.

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

Na razie niczego tu nie ma.

Zasoby