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 al colore. Prova la demo e visualizza la fonte.

Viene visualizzata una descrizione comando che funziona per diversi esempi e combinazioni di colori

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

Panoramica

Una descrizione comando è un overlay non modale, non di blocco e non interattivo contenente informazioni supplementari sulle interfacce utente. È nascosta per impostazione predefinita e viene mostrata quando si passa il mouse sopra un elemento associato. Non è possibile selezionare o interagire direttamente con una descrizione comando. Le descrizioni comando non sostituiscono le etichette o altre informazioni di alto valore; un utente deve essere in grado di completare l'attività senza una descrizione comando.

Azione: etichetta sempre gli input.
Cosa non fare: usare le descrizioni comando anziché le etichette

Suggerimento di attivazione/disattivazione e descrizione comando

Come per molti componenti, esistono descrizioni diverse di una descrizione comando, ad esempio in MDN, WAI ARIA, Sarah Higley e Inclusive Componenti. Mi piace la separazione tra descrizioni comando e suggerimenti di attivazione/disattivazione. Una descrizione comando dovrebbe contenere informazioni supplementari non interattive, mentre un pulsante di attivazione/disattivazione può contenere interattività e informazioni importanti. La causa principale di questo divario è l'accessibilità, ovvero il modo in cui gli utenti si aspettano di navigare nel popup e avere accesso alle informazioni e ai pulsanti al suo interno. I suggerimenti di attivazione/disattivazione diventano rapidamente complessi.

Ecco un video di un pulsante di attivazione/disattivazione dal sito di Designcember, un overlay con interattività che l'utente può bloccare per aprirlo ed esplorare, quindi chiuderlo con il tasto per la chiusura del pulsante o il tasto Esc:

Questa GUI Challenge ha avuto come esempio una descrizione comando, cercando di fare praticamente tutto con CSS; ecco come crearla.

Markup

Ho scelto di utilizzare un elemento personalizzato <tool-tip>. Gli autori non devono convertire elementi personalizzati in componenti web. Il browser tratterà <foo-bar> come un <div>. Un elemento personalizzato può essere paragonato a un nomeclasse con meno specificità. Non utilizza JavaScript.

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

È come un div con del testo all'interno. Possiamo collegarti all'albero dell'accessibilità degli screen reader supportati aggiungendo [role="tooltip"].

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

Ora per gli screen reader viene riconosciuta come descrizione comando. Nell'esempio che segue, come il primo elemento link presenta un elemento della descrizione comando riconosciuto nella struttura e il secondo no? La seconda non ha questo ruolo. Nella sezione Stili miglioreremo questa visualizzazione ad albero.

Uno screenshot dell&#39;albero di accessibilità Chrome DevTools che rappresenta il codice HTML. Mostra un
link con il testo &quot;in alto ; Con descrizione comando: e una descrizione comando!&quot;, attivabile. All&#39;interno è presente il testo statico &quot;top&quot; e un elemento della descrizione comando.

Abbiamo quindi bisogno che la descrizione comando non sia attivabile. Se uno screen reader non comprende il ruolo della descrizione comando, gli utenti potranno impostare <tool-tip> per leggere i contenuti, senza che sia necessario per l'esperienza utente. Gli screen reader aggiungono i contenuti all'elemento principale e, come tali, non è necessario che lo stato attivo sia reso accessibile. Qui possiamo utilizzare inert per assicurarci che nessun utente trovi accidentalmente questi contenuti della descrizione comando nel flusso delle schede:

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

Un altro screenshot dell&#39;albero dell&#39;accessibilità di Chrome DevTools
in cui manca l&#39;elemento della descrizione comando.

Ho quindi scelto di utilizzare gli attributi come interfaccia per specificare la posizione della descrizione comando. Per impostazione predefinita, tutti gli elementi <tool-tip> assumono la 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>

Uno
screenshot di un link con una descrizione comando a destra che indica &quot;Una descrizione comando&quot;.

Per cose come questo tendo a utilizzare gli attributi invece delle classi, in modo che <tool-tip> non possa avere più posizioni assegnate contemporaneamente. Può esserci un solo elemento o nessuno.

Infine, posiziona gli elementi <tool-tip> all'interno dell'elemento per il quale vuoi fornire una descrizione comando. Qui condivido il testo alt con gli utenti vedenti posizionando 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 la descrizione comando &quot;Logo del teschio della GUI Challenges&quot;.

Qui inserisco un elemento <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 una descrizione comando &quot;Hyper Text Markup Language&quot; sopra.

Accessibilità

Dal momento che ho scelto di creare descrizioni comando e non suggerimenti di attivazione/disattivazione, questa sezione è molto più semplice. Innanzitutto, illustrerò quale sia l'esperienza utente che desideriamo offrire:

  1. In spazi ristretti o nelle interfacce caotiche, nascondi i messaggi supplementari.
  2. Quando un utente passa il mouse sopra un elemento, lo mette a fuoco o lo usa per interagire con un elemento, mostra il messaggio.
  3. Quando passi il mouse, lo stato attivo o il tocco termina, nascondi di nuovo il messaggio.
  4. Infine, assicurati che il movimento sia ridotto se un utente ha specificato una preferenza per il movimento ridotto.

Il nostro obiettivo è la messaggistica supplementare on demand. Una persona vedente il mouse o la tastiera può passare il mouse per visualizzare il messaggio e leggerlo con gli occhi. Una persona non vedente con uno screen reader può concentrarsi sulla rivelazione del messaggio, udindone la ricezione attraverso il proprio strumento.

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

Nella sezione precedente abbiamo parlato dell'albero dell'accessibilità, del ruolo della descrizione comando e dell'inerte. Quello che rimane da fare è testarlo e verificare l'esperienza utente in modo appropriato per mostrare il messaggio della descrizione comando all'utente. Al momento del test, non è chiaro quale parte del messaggio sonoro sia la descrizione comando. Può essere visto anche durante il debug nell'albero dell'accessibilità e il testo del link "top" viene eseguito insieme, senza esitazioni, con "Guarda, descrizioni comando!". Lo screen reader non spezza e non identifica il testo come contenuto della descrizione comando.

Uno screenshot dell&#39;albero di accessibilità di Chrome DevTools in cui il testo del link dice &quot;In alto, una descrizione comando&quot;.

Aggiungi uno pseudo elemento solo per screen reader a <tool-tip> e possiamo aggiungere il testo del nostro 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 una richiesta per la descrizione comando "Ha una descrizione comando: ".

Uno screenshot aggiornato dell&#39;albero dell&#39;accessibilità di Chrome DevTools in cui il testo del link ha migliorato la frase &quot;top ; ha una descrizione comando: &quot;Ehi, una descrizione comando!&quot;.

Ora, quando un utente di screen reader mette a fuoco il link, dice "in alto" e fa una piccola pausa, poi annuncia "con descrizione comando: guarda, descrizioni comando". Ciò offre all'utente di uno screen reader un paio di utili suggerimenti UX. L'esitazione fornisce una bella separazione tra il testo del link e la descrizione comando. Inoltre, quando viene annunciato "presenta la descrizione comando", l'utente di uno screen reader può annullarla facilmente se l'ha già sentita prima. Ricorda il passaggio rapido e l'annullamento del mouse, come hai già visto il messaggio supplementare. Sembrava una bella parità UX.

Stili

L'elemento <tool-tip> sarà un elemento secondario dell'elemento che rappresenta il messaggio supplementare, quindi iniziamo con gli elementi essenziali per l'effetto overlay. Esci dal flusso dei documenti con position absolute:

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

Se l'elemento padre non è un contesto di stack, la descrizione comando si posizionerà a quello più vicino, che non è ciò che vogliamo. È disponibile un nuovo selettore sul blocco che può aiutare, :has():

Supporto dei browser

  • 105
  • 105
  • 121
  • 15,4

Fonte

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

Non preoccuparti troppo del supporto del browser. Innanzitutto, le descrizioni comando sono supplementari. Se non funzionano, dovrebbe andare bene. In secondo luogo, nella sezione JavaScript eseguiremo il deployment di uno script per il polyfill della funzionalità necessaria per i browser che non supportano :has().

Quindi, rendi le descrizioni comando non interattive in modo che non rubano eventi di puntatore dall'elemento principale:

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

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

tool-tip {
  opacity: 0;
}

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

:is() e :has() svolgono il lavoro più impegnativo qui, informando tool-tip, contenenti gli elementi principali, dell'interattività dell'utente per attivare/disattivare la visibilità di una descrizione comando secondaria. Gli utenti di mouse possono passare il mouse, gli utenti di tastiera e screen reader possono impostare lo stato attivo e gli utenti di tocco possono toccare.

Dato che l'overlay "Mostra/Nascondi" è disponibile per gli utenti vedenti, è il momento di aggiungere alcuni stili per la tematizzazione, il posizionamento e l'aggiunta della forma triangolare alla bolla. I seguenti stili iniziano a utilizzare proprietà personalizzate, a partire dalla posizione attuale, ma anche aggiungendo ombre, tipografia e colori in modo che appaia come una descrizione comando mobile:

Uno screenshot della descrizione comando in modalità Buio, che appare 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;
}

Aggiustamenti del 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à e lasciare che sia il tema a occuparsi del resto:

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

Uno screenshot affiancato delle versioni chiare e scure della descrizione comando.

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

Da destra a sinistra

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

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

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

Può essere utilizzato per posizionare la descrizione comando:

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

Oltre a contribuire alla 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 le trasformazioni logiche su translateX():

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

Posizionamento 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 seguente codice mostra lo stile per ciascuna delle quattro posizioni sia per le direzioni da sinistra a destra che da destra a sinistra.

Allineamento in alto e inizio blocco

Uno screenshot che mostra la differenza di posizionamento tra la posizione superiore da sinistra a destra e la posizione superiore 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 in linea alla fine

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 in basso da sinistra a destra e la posizione di fine blocco da destra a sinistra.

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 in linea

Uno screenshot che mostra la differenza di posizionamento tra la posizione di partenza da sinistra a destra e la posizione di inizio 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 attivato solo la visibilità della descrizione comando. In questa sezione, innanzitutto animano l'opacità per tutti gli utenti, poiché si tratta di una transizione a movimento ridotto in genere sicura. Successivamente, annulleremo la posizione della trasformazione in modo che la descrizione comando scorra fuori dall'elemento principale.

Una transizione predefinita sicura e significativa

Applica uno stile all'elemento della descrizione comando in modo da opacità e trasformazione della transizione, in questo modo:

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ò essere visualizzata una descrizione comando. Se l'utente è d'accordo con il movimento, posiziona leggermente la proprietà TraduttoreX dando una piccola distanza da cui percorrere una distanza:

@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 questa opzione è impostata sullo stato "out", poiché lo stato "in" è impostato su translateX(0).

JavaScript

A mio parere il codice JavaScript è facoltativo. Il motivo è che nessuna di queste descrizioni comando dovrebbe essere obbligatoria per svolgere un'attività nella tua UI. Quindi, se le descrizioni comando non funzionano del tutto, non è un problema. Ciò significa anche che possiamo trattare le descrizioni comando come perfezionate progressivamente. Alla fine tutti i browser supporteranno :has() e questo script potrebbe essere eliminato completamente.

Lo script di polyfill ha due funzioni e lo fa solo se il browser non supporta :has(). Innanzitutto, cerca l'assistenza per :has():

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

Quindi, trova gli elementi principali di <tool-tip> e assegna loro un classname con cui lavorare:

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

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

È tutto. Ora tutti i browser mostreranno le descrizioni comando se :has() non è supportato.

Conclusione

Ora che sai come ci sono riuscito, cosa faresti? 🙂 Non vedo l'ora dell'API popup per semplificare i suggerimenti di attivazione/disattivazione, del livello superiore per evitare battaglie z-index e dell'API anchor per posizionare meglio gli elementi nella finestra. Fino ad allora, ti fornirò le descrizioni comando.

Diversifica i nostri approcci e scopriamo tutti i modi per creare sul web.

Crea una demo, inviami un tweet con i link e lo aggiungerò alla sezione Remix della community di seguito.

Remix della community

Ancora niente da visualizzare.

Risorse