Creazione di un componente della descrizione comando

Una panoramica di base su come creare un elemento personalizzato della descrizione comando adattabile al colore e accessibile.

In questo post voglio condividere le mie idee su come creare un elemento personalizzato <tool-tip> accessibile e adattabile ai colori. Prova la demo e visualizza il codice sorgente.

Viene visualizzata una descrizione comando che funziona con una serie di esempi e combinazioni di colori

Se preferisci i video, ecco una versione di questo post su YouTube:

Panoramica

Una descrizione comando è una sovrapposizione non modale, non bloccante e non interattiva che contiene informazioni supplementari per le interfacce utente. È nascosto per impostazione predefinita e viene mostrato quando un elemento associato viene passato con il mouse o selezionato. Non è possibile selezionare o interagire direttamente con una descrizione comando. I suggerimenti non sostituiscono le etichette o altre informazioni di alto valore. Un utente deve essere in grado di completare la propria attività senza un suggerimento.

Azione: etichetta sempre gli input.
Non: fare affidamento sulle descrizioni comando anziché sulle etichette

Toggletip e Descrizione comando

Come per molti componenti, esistono descrizioni diverse di che cos'è una descrizione comando, ad esempio in MDN, WAI ARIA, Sarah Higley e Inclusive Components. Mi piace la separazione tra descrizioni comando e pulsanti di attivazione/disattivazione. Una descrizione comando deve contenere informazioni supplementari non interattive, mentre un toggletip può contenere interattività e informazioni importanti. Il motivo principale della divisione è l'accessibilità: come si prevede che gli utenti accedano al popup e abbiano accesso alle informazioni e ai pulsanti al suo interno. I suggerimenti a comparsa diventano rapidamente complessi.

Ecco un video di un suggerimento a comparsa del sito Designcember: un overlay con interattività che un utente può aprire ed esplorare, poi chiudere con un semplice tocco o il tasto Esci:

Questa sfida della GUI ha seguito il percorso di una descrizione comando, cercando di fare quasi tutto con CSS. Ecco come realizzarla.

Segni e linee

Ho scelto di utilizzare un elemento personalizzato <tool-tip>. Gli autori non devono trasformare gli elementi personalizzati in componenti web se non vogliono. Il browser tratterà <foo-bar> come un <div>. Puoi considerare un elemento personalizzato come una classe con meno specificità. Non è coinvolto JavaScript.

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

È come un div con del testo all'interno. Possiamo integrarlo nell'albero di accessibilità di screen reader compatibili aggiungendo [role="tooltip"].

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

Ora, per gli screen reader, viene riconosciuto come una descrizione comando. Nell'esempio seguente, il primo elemento di collegamento ha un elemento della descrizione comando riconosciuto nel suo albero, mentre il secondo no. Il secondo non ha il ruolo. Nella sezione degli stili miglioreremo questa visualizzazione ad albero.

Uno
screenshot dell&#39;albero di accessibilità di Chrome DevTools che rappresenta l&#39;HTML. Mostra un
link con il testo &quot;top ; Has tooltip: Hey, a tooltip!&quot; (in alto; con descrizione comando: Hey, a tooltip!) che può essere selezionato. All&#39;interno
si trova il testo statico &quot;In alto&quot; e un elemento della descrizione comando.

Poi dobbiamo fare in modo che la descrizione comando non sia selezionabile. Se uno screen reader non comprende il ruolo della descrizione comando, consentirà agli utenti di mettere a fuoco <tool-tip> per leggerne i contenuti, ma l'esperienza utente non ne ha bisogno. Gli screen reader aggiungeranno i contenuti all'elemento principale e, pertanto, non è necessario che venga reso accessibile. Qui possiamo utilizzare inert per assicurarci che nessun utente trovi accidentalmente i contenuti di questo suggerimento nel flusso delle schede:

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

Un altro screenshot dell&#39;albero di accessibilità di Chrome DevTools, questa volta manca l&#39;elemento
tooltip.

Ho quindi scelto di utilizzare gli attributi come interfaccia per specificare la posizione della descrizione comando. Per impostazione predefinita, tutti i <tool-tip> assumono una posizione "in alto", ma la posizione può essere personalizzata su un elemento aggiungendo tip-position:

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

Screenshot
di un link con una descrizione comando a destra che dice &quot;Una descrizione comando&quot;.

Tendo a utilizzare gli attributi anziché le classi per questo tipo di cose, in modo che <tool-tip> non possa avere più posizioni assegnate contemporaneamente. Può essercene uno solo o nessuno.

Infine, inserisci gli elementi <tool-tip> all'interno dell'elemento per cui vuoi fornire una descrizione comando. Qui condivido il testo alt con gli utenti vedenti inserendo un'immagine e un <tool-tip> all'interno di un elemento <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>

Uno
screenshot di un&#39;immagine con una descrizione comando che dice &quot;Il logo del teschio di GUI Challenges&quot;.

Qui inserisco un <tool-tip> all'interno di un elemento <abbr>:

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

Uno
screenshot di un paragrafo con l&#39;acronimo HTML sottolineato e un suggerimento sopra
che dice &quot;Hyper Text Markup Language&quot;.

Accessibilità

Poiché ho scelto di creare descrizioni comando e non descrizioni comando attivate/disattivate, questa sezione è molto più semplice. Innanzitutto, vorrei descrivere l'esperienza utente che vogliamo offrire:

  1. In spazi ristretti o interfacce disordinate, nascondi i messaggi supplementari.
  2. Quando un utente passa il mouse sopra un elemento, lo seleziona o lo tocca, viene visualizzato il messaggio.
  3. Quando il passaggio del mouse, lo stato attivo o il tocco termina, nascondi di nuovo il messaggio.
  4. Infine, assicurati che qualsiasi movimento sia ridotto se un utente ha specificato una preferenza per movimenti ridotti.

Il nostro obiettivo è la messaggistica supplementare on demand. Un utente che utilizza il mouse o la tastiera può passare il cursore sopra il messaggio per visualizzarlo e leggerlo. Un utente di screen reader non vedente può mettere a fuoco per visualizzare il messaggio, ricevendo la notifica tramite il suo strumento.

Screenshot di VoiceOver di macOS che legge un link con una descrizione comando

Nella sezione precedente abbiamo trattato l'albero dell'accessibilità, il ruolo della descrizione comando e inert. Non resta che testare e verificare che l'esperienza utente riveli in modo appropriato il messaggio della descrizione comando all'utente. Durante il test, non è chiaro quale parte del messaggio udibile sia una descrizione comando. Può essere visto anche durante il debug nell'albero dell'accessibilità, il testo del link "top" è unito, senza esitazione, a "Look, tooltips!". Lo screen reader non interrompe né identifica il testo come contenuto della descrizione comando.

Uno
screenshot dell&#39;albero di accessibilità di Chrome DevTools in cui il testo del link indica
&quot;top Hey, a tooltip!&quot;.

Aggiungi uno pseudo-elemento solo per screen reader a <tool-tip> e possiamo aggiungere il nostro testo del prompt per gli utenti non vedenti.

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

Di seguito puoi vedere l'albero dell'accessibilità aggiornato, che ora ha un punto e virgola dopo il testo del link e un prompt per la descrizione comando "Has tooltip: ".

Uno screenshot aggiornato dell&#39;albero dell&#39;accessibilità di Chrome DevTools in cui il
testo del link è stato migliorato con la frase &quot;top ; Has tooltip: Hey, a tooltip!&quot;.

Ora, quando un utente di screen reader mette a fuoco il link, viene pronunciata la parola "in alto", segue una breve pausa e poi viene annunciato "ha tooltip: guarda, tooltip". In questo modo, l'utente di uno screen reader riceve un paio di suggerimenti UX utili. L'esitazione crea una separazione tra il testo del link e la descrizione comando. Inoltre, quando viene annunciato "ha descrizione comando", un utente di screen reader può annullarlo facilmente se lo ha già sentito. Ricorda molto l'hover e l'unhover rapidi, dato che hai già visto il messaggio supplementare. Mi è sembrato un buon modo per raggiungere la parità dell'esperienza utente.

Stili

L'elemento <tool-tip> sarà un elemento secondario dell'elemento per cui rappresenta i messaggi supplementari, quindi iniziamo con gli elementi essenziali per l'effetto di sovrapposizione. Rimuovilo dal flusso di documenti con position absolute:

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

Se l'elemento principale non è un contesto di sovrapposizione, il suggerimento si posizionerà su quello più vicino, il che non è quello che vogliamo. Nel blocco è presente un nuovo selettore che può aiutarti, :has():

Browser Support

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

Source

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

Non preoccuparti troppo del supporto del browser. Innanzitutto, ricorda che questi suggerimenti sono supplementari. Se non funzionano, non dovrebbero esserci problemi. In secondo luogo, nella sezione JavaScript implementeremo uno script per il polyfill della funzionalità di cui abbiamo bisogno per i browser senza supporto :has().

Ora rendiamo le descrizioni comando non interattive in modo che non sottraggano eventi puntatore all'elemento principale:

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

Poi, nascondi la descrizione comando con l'opacità in modo da poterla visualizzare con una dissolvenza incrociata:

tool-tip {
  opacity: 0;
}

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

:is() e :has() fanno il lavoro più pesante, rendendo tool-tip contenente elementi principali consapevole dell'interattività dell'utente per attivare/disattivare la visibilità di una descrizione comando secondaria. Gli utenti del mouse possono passare il mouse sopra, gli utenti della tastiera e dello screen reader possono attivare e gli utenti del tocco possono toccare.

Ora che la visualizzazione e l'occultamento della sovrapposizione funzionano per gli utenti vedenti, è il momento di aggiungere alcuni stili per la definizione dei temi, il posizionamento e l'aggiunta della forma triangolare alla bolla. Gli stili seguenti iniziano a utilizzare proprietà personalizzate, basandosi su quanto fatto finora, ma aggiungendo anche ombre, tipografia e colori in modo che assomigli a una descrizione comando mobile:

Uno
screenshot della descrizione comando in modalità Buio, che fluttua sopra il link &quot;block-start&quot;.

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

Modifiche al tema

La descrizione comando ha solo pochi colori da gestire, poiché il colore del testo viene ereditato dalla pagina tramite la parola chiave di sistema CanvasText. Inoltre, poiché abbiamo creato proprietà personalizzate per archiviare i valori, possiamo aggiornare solo queste proprietà personalizzate e lasciare che il tema gestisca il resto:

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

Uno
screenshot affiancato delle versioni chiara e scura della descrizione comando.

Per il tema chiaro, adattiamo lo sfondo al bianco e rendiamo le ombre molto meno intense regolandone l'opacità.

Da destra a sinistra

Per supportare le modalità di lettura da destra a sinistra, una proprietà personalizzata memorizzerà il valore della direzione del documento in un valore di -1 o 1 rispettivamente.

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

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

Può essere utilizzato per facilitare il posizionamento della descrizione comando:

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

Oltre a indicare la posizione del triangolo:

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

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

Infine, può essere utilizzato anche per trasformazioni logiche su translateX():

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

Posizionamento della descrizione comando

Posiziona la descrizione comando in modo logico con le proprietà inset-block o inset-inline per gestire le posizioni fisiche e logiche della descrizione comando. Il codice seguente mostra lo stile di ciascuna delle quattro posizioni per le direzioni da sinistra a destra e da destra a sinistra.

Allineamento in alto e all'inizio del blocco

Uno
screenshot che mostra la differenza di posizionamento tra la posizione in alto da sinistra a destra
e la posizione in alto da destra a sinistra.

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

Allineamento a destra e alla fine della riga

Uno
screenshot che mostra la differenza di posizionamento tra la posizione da sinistra a destra
e la posizione in linea da destra a sinistra.

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

Allineamento in basso e alla fine del blocco

Uno
screenshot che mostra la differenza di posizionamento tra la posizione da sinistra a destra in basso
e la posizione da destra a sinistra alla fine del blocco.

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

Allineamento a sinistra e all'inizio della riga

Uno
screenshot che mostra la differenza di posizionamento tra la posizione a sinistra da sinistra a destra
e la posizione iniziale in linea da destra a sinistra.

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

Animazione

Finora abbiamo solo attivato/disattivato la visibilità della descrizione comando. In questa sezione, animeremo innanzitutto l'opacità per tutti gli utenti, in quanto si tratta di una transizione con movimento ridotto generalmente sicura. Poi animeremo la posizione di trasformazione in modo che il suggerimento venga visualizzato scorrendo dall'elemento principale.

Una transizione predefinita sicura e significativa

Applica uno stile all'elemento della descrizione comando per la transizione di opacità e trasformazione, come segue:

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

Aggiungere movimento alla transizione

Per ogni lato su cui può apparire una descrizione comando, se l'utente accetta il movimento, posiziona leggermente la proprietà translateX dandole una piccola distanza da percorrere:

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

Tieni presente che questo imposta lo stato "out", poiché lo stato "in" è a translateX(0).

JavaScript

A mio parere, JavaScript è facoltativo. Questo perché nessuno di questi suggerimenti dovrebbe essere una lettura obbligatoria per completare un'attività nell'interfaccia utente. Quindi, se i suggerimenti non funzionano, non dovrebbe essere un problema. Ciò significa anche che possiamo trattare i suggerimenti come migliorati progressivamente. Alla fine tutti i browser supporteranno :has() e questo script potrà essere rimosso completamente.

Lo script polyfill esegue due operazioni e lo fa solo se il browser non supporta :has(). Innanzitutto, verifica il supporto di :has():

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

Successivamente, trova gli elementi principali dei <tool-tip> e assegna loro un nome di classe con cui lavorare:

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

Successivamente, inserisci un insieme di stili che utilizzano quel nome di classe, simulando il selettore :has() per lo stesso comportamento:

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

Ecco fatto. Ora tutti i browser mostreranno i suggerimenti se :has() non è supportato.

Conclusione

Ora che sai come ho fatto, come faresti tu? 🙂 Non vedo l'ora di utilizzare l'API popup per semplificare le descrizioni comando, il livello superiore per evitare problemi con l'indice Z e l'API anchor per posizionare meglio gli elementi nella finestra. Fino ad allora, creerò descrizioni comando.

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web.

Crea una demo, inviami un tweet con i link e la aggiungerò alla sezione dei remix della community qui sotto.

Remix della community

Ancora nessun elemento da visualizzare.

Risorse