Creazione di un componente Pulsante di suddivisione

Una panoramica di base su come creare un componente pulsante diviso accessibile.

In questo post voglio condividere il mio pensiero su un modo per creare un pulsante diviso . Prova la demo.

Demo

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

Panoramica

I pulsanti divisi sono pulsanti che nascondono un pulsante principale e un elenco di pulsanti aggiuntivi. Sono utili per esporre un'azione comune nidificando azioni secondarie, usate meno frequentemente, fino a quando non sono necessarie. Un pulsante diviso può essere fondamentale per dare un aspetto minimal a un design molto elaborato. Un pulsante diviso avanzato può persino ricordare l'ultima azione dell'utente e promuoverla nella posizione principale.

Un pulsante diviso comune si trova nell'applicazione email. L'azione principale è l'invio, ma forse puoi inviare il messaggio in un secondo momento o salvare una bozza:

Un esempio di pulsante di suddivisione visualizzato in un'applicazione di posta elettronica.

L'area delle azioni condivise è utile perché l'utente non deve cercare. Sanno che le azioni email essenziali sono contenute nel pulsante diviso.

Componenti

Analizziamo le parti essenziali di un pulsante diviso prima di discutere la loro orchestrazione complessiva e l'esperienza utente finale. Lo strumento di ispezione dell'accessibilità di VisBug viene utilizzato qui per mostrare una visione macro del componente, mettendo in evidenza aspetti di HTML, stile e accessibilità per ogni parte principale.

Gli elementi HTML che compongono il pulsante diviso.

Contenitore del pulsante di suddivisione di primo livello

Il componente di livello più alto è un flexbox incorporato, con una classe di gui-split-button, contenente l'azione principale e il .gui-popup-button.

La classe gui-split-button ispezionata e che mostra le proprietà CSS utilizzate in questa classe.

Il pulsante di azione principale

L'elemento <button> inizialmente visibile e selezionabile rientra nel contenitore con due forme angolari corrispondenti per le interazioni focus, hover e attiva che appaiono contenute all'interno di .gui-split-button.

L&#39;inspector che mostra le regole CSS per l&#39;elemento pulsante.

Pulsante di attivazione/disattivazione del popup

L'elemento di supporto "pulsante popup" serve per attivare e fare riferimento all'elenco di pulsanti secondari. Nota che non è un <button> e non è selezionabile. Tuttavia, è l'ancora di posizionamento per .gui-popup e l'host per :focus-within utilizzato per presentare il popup.

L&#39;inspector che mostra le regole CSS per la classe gui-popup-button.

La scheda popup

Questa è una scheda mobile secondaria del relativo ancoraggio .gui-popup-button, posizionata in modo assoluto e che racchiude semanticamente l'elenco dei pulsanti.

L&#39;inspector che mostra le regole CSS per la classe gui-popup

L'azione o le azioni secondarie

Un <button> selezionabile con una dimensione del carattere leggermente inferiore rispetto al pulsante di azione principale presenta un'icona e uno stile complementare al pulsante principale.

L&#39;inspector che mostra le regole CSS per l&#39;elemento pulsante.

Proprietà personalizzate

Le seguenti variabili aiutano a creare un'armonia di colori e un punto centrale per modificare i valori utilizzati in tutto il componente.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);

.gui-split-button {
  --theme:             hsl(220 75% 50%);
  --theme-hover:  hsl(220 75% 45%);
  --theme-active:  hsl(220 75% 40%);
  --theme-text:      hsl(220 75% 25%);
  --theme-border: hsl(220 50% 75%);
  --ontheme:         hsl(220 90% 98%);
  --popupbg:         hsl(220 0% 100%);

  --border: 1px solid var(--theme-border);
  --radius: 6px;
  --in-speed: 50ms;
  --out-speed: 300ms;

  @media (--dark) {
    --theme:             hsl(220 50% 60%);
    --theme-hover:  hsl(220 50% 65%);
    --theme-active:  hsl(220 75% 70%);
    --theme-text:      hsl(220 10% 85%);
    --theme-border: hsl(220 20% 70%);
    --ontheme:         hsl(220 90% 5%);
    --popupbg:         hsl(220 10% 30%);
  }
}

Layout e colore

Segni e linee

L'elemento inizia come <div> con un nome di classe personalizzato.

<div class="gui-split-button"></div>

Aggiungi il pulsante principale e gli elementi .gui-popup-button.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>

Nota gli attributi aria aria-haspopup e aria-expanded. Questi segnali sono fondamentali per consentire agli screen reader di rilevare la funzionalità e lo stato dell'esperienza del pulsante diviso. L'attributo title è utile per tutti.

Aggiungi un'icona <svg> e l'elemento contenitore .gui-popup.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup"></ul>
  </span>
</div>

Per un posizionamento semplice del popup, .gui-popup è un elemento secondario del pulsante che lo espande. L'unico problema di questa strategia è che il contenitore .gui-split-button non può utilizzare overflow: hidden, in quanto il popup non sarà visibile.

Un <ul> pieno di contenuti <li><button> si annuncerà come "elenco di pulsanti" agli screen reader, che è esattamente l'interfaccia presentata.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li>
        <button>Schedule for later</button>
      </li>
      <li>
        <button>Delete</button>
      </li>
      <li>
        <button>Save draft</button>
      </li>
    </ul>
  </span>
</div>

Per aggiungere un tocco di stile e divertirmi con i colori, ho aggiunto icone ai pulsanti secondari da https://heroicons.com. Le icone sono facoltative sia per i pulsanti principali che per quelli secondari.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
        </svg>
        Schedule for later
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
        Delete
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
        Save draft
      </button></li>
    </ul>
  </span>
</div>

Stili

Con l'HTML e i contenuti a posto, gli stili sono pronti a fornire colore e layout.

Applicare uno stile al contenitore del pulsante diviso

Un tipo di visualizzazione inline-flex è ideale per questo componente di wrapping perché deve essere in linea con altri pulsanti di divisione, azioni o elementi.

.gui-split-button {
  display: inline-flex;
  border-radius: var(--radius);
  background: var(--theme);
  color: var(--ontheme);
  fill: var(--ontheme);

  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

Il pulsante di divisione.

Stile di <button>

I pulsanti sono molto bravi a nascondere la quantità di codice necessaria. Potresti dover annullare o sostituire gli stili predefiniti del browser, ma dovrai anche applicare alcune ereditarietà, aggiungere stati di interazione e adattarti a varie preferenze dell'utente e tipi di input. Gli stili dei pulsanti si accumulano rapidamente.

Questi pulsanti sono diversi dai pulsanti normali perché condividono uno sfondo con un elemento principale. In genere, un pulsante ha il proprio colore di sfondo e del testo. Questi, invece, lo condividono e applicano solo il proprio background sull'interazione.

.gui-split-button button {
  cursor: pointer;
  appearance: none;
  background: none;
  border: none;

  display: inline-flex;
  align-items: center;
  gap: 1ch;
  white-space: nowrap;

  font-family: inherit;
  font-size: inherit;
  font-weight: 500;

  padding-block: 1.25ch;
  padding-inline: 2.5ch;

  color: var(--ontheme);
  outline-color: var(--theme);
  outline-offset: -5px;
}

Aggiungi stati di interazione con alcune pseudo-classi CSS e utilizza proprietà personalizzate corrispondenti per lo stato:

.gui-split-button button {
  

  &:is(:hover, :focus-visible) {
    background: var(--theme-hover);
    color: var(--ontheme);

    & > svg {
      stroke: currentColor;
      fill: none;
    }
  }

  &:active {
    background: var(--theme-active);
  }
}

Il pulsante principale richiede alcuni stili speciali per completare l'effetto di progettazione:

.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);

  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}

Infine, per un tocco di stile, il pulsante e l'icona del tema chiaro hanno una sfumatura:

.gui-split-button {
  @media (--light) {
    & > button,
    & button:is(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--theme-active);
    }
    & > .gui-popup-button > svg,
    & button:is(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--theme-active));
    }
  }
}

Un ottimo pulsante è stato progettato prestando attenzione alle microinterazioni e ai piccoli dettagli.

Una nota su :focus-visible

Nota come gli stili dei pulsanti utilizzano :focus-visible anziché :focus. :focus è un tocco fondamentale per creare un'interfaccia utente accessibile, ma ha un inconveniente: non è intelligente e non capisce se l'utente ha bisogno di vederlo o meno, ma lo applica a qualsiasi messa a fuoco.

Il video di seguito tenta di analizzare questa microinterazione per mostrare come :focus-visible sia un'alternativa intelligente.

Applicare uno stile al pulsante popup

Un 4ch flexbox per centrare un'icona e ancorare un elenco di pulsanti popup. Come il pulsante principale, è trasparente finché non viene passato sopra o interagito con esso e viene allungato per riempire lo spazio.

La parte a forma di freccia del pulsante diviso utilizzata per attivare il popup.

.gui-popup-button {
  inline-size: 4ch;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-inline-start: var(--border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}

Sovrapponi gli stati al passaggio del mouse, di messa a fuoco e attivo con CSS Nesting e il selettore funzionale :is():

.gui-popup-button {
  

  &:is(:hover,:focus-within) {
    background: var(--theme-hover);
  }

  /* fixes iOS trying to be helpful */
  &:focus {
    outline: none;
  }

  &:active {
    background: var(--theme-active);
  }
}

Questi stili sono il principale punto di aggancio per mostrare e nascondere il popup. Quando .gui-popup-button ha focus su uno dei suoi elementi secondari, imposta opacity, posizione e pointer-events sull'icona e sul popup.

.gui-popup-button {
  

  &:focus-within {
    & > svg {
      transition-duration: var(--in-speed);
      transform: rotateZ(.5turn);
    }
    & > .gui-popup {
      transition-duration: var(--in-speed);
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
  }
}

Una volta completati gli stili di entrata e uscita, l'ultimo passaggio consiste nel trasformare condizionatamente le transizioni in base alla preferenza di movimento dell'utente:

.gui-popup-button {
  

  @media (--motionOK) {
    & > svg {
      transition: transform var(--out-speed) ease;
    }
    & > .gui-popup {
      transform: translateY(5px);

      transition:
        opacity var(--out-speed) ease,
        transform var(--out-speed) ease;
    }
  }
}

Un occhio attento al codice noterà che la transizione dell'opacità è ancora attiva per gli utenti che preferiscono un movimento ridotto.

Applicare uno stile al popup

L'elemento .gui-popup è un elenco di pulsanti di schede mobili che utilizza proprietà personalizzate e unità relative per essere leggermente più piccolo, abbinato in modo interattivo al pulsante principale e in linea con il brand grazie all'uso del colore. Nota che le icone hanno meno contrasto, sono più sottili e l'ombra ha una sfumatura di blu del brand. Come per i pulsanti, un'interfaccia utente e un'esperienza utente efficaci sono il risultato di questi piccoli dettagli.

Un elemento scheda mobile.

.gui-popup {
  --shadow: 220 70% 15%;
  --shadow-strength: 1%;

  opacity: 0;
  pointer-events: none;

  position: absolute;
  bottom: 80%;
  left: -1.5ch;

  list-style-type: none;
  background: var(--popupbg);
  color: var(--theme-text);
  padding-inline: 0;
  padding-block: .5ch;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: .9em;
  transition: opacity var(--out-speed) ease;

  box-shadow:
    0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
    0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
    0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
    0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
    0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
    0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
  ;
}

Le icone e i pulsanti hanno i colori del brand per uno stile piacevole in ogni scheda con tema scuro e chiaro:

Link e icone per il pagamento, Quick Pay e Salva per dopo.

.gui-popup {
  

  & svg {
    fill: var(--popupbg);
    stroke: var(--theme);

    @media (prefers-color-scheme: dark) {
      stroke: var(--theme-border);
    }
  }

  & button {
    color: var(--theme-text);
    width: 100%;
  }
}

Il popup del tema scuro presenta testo e ombre delle icone aggiuntive, oltre a un'ombra della casella leggermente più intensa:

Il popup nel tema scuro.

.gui-popup {
  

  @media (--dark) {
    --shadow-strength: 5%;
    --shadow: 220 3% 2%;

    & button:not(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--ontheme);
    }

    & button:not(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--ontheme));
    }
  }
}

Stili di icone <svg> generici

Tutte le icone sono dimensionate in modo relativo al pulsante font-size in cui vengono utilizzate utilizzando l'unità ch come inline-size. A ciascuna viene inoltre assegnato uno stile per rendere i contorni delle icone morbidi e uniformi.

.gui-split-button svg {
  inline-size: 2ch;
  box-sizing: content-box;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2px;
}

Layout da destra a sinistra

Le proprietà logiche svolgono tutto il lavoro complesso. Ecco l'elenco delle proprietà logiche utilizzate: - display: inline-flex crea un elemento flessibile in linea. - padding-block e padding-inline come coppia, anziché padding abbreviato, ottieni i vantaggi del padding dei lati logici. - border-end-start-radius e amici avranno angoli arrotondati in base alla direzione del documento. - inline-size anziché width garantisce che le dimensioni non siano legate alle dimensioni fisiche. - border-inline-start aggiunge un bordo all'inizio, che potrebbe trovarsi a destra o a sinistra a seconda della direzione di scrittura.

JavaScript

Quasi tutto il seguente codice JavaScript serve a migliorare l'accessibilità. Due delle mie librerie di utilità vengono utilizzate per semplificare un po' le attività. BlingBlingJS viene utilizzato per query DOM concise e per una facile configurazione del listener di eventi, mentre roving-ux facilita le interazioni accessibili con tastiera e gamepad per il popup.

import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'

const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')

Con le librerie sopra importate e gli elementi selezionati e salvati nelle variabili, l'upgrade dell'esperienza è a poche funzioni dal completamento.

Indice mobile

Quando una tastiera o uno screen reader mette a fuoco .gui-popup-button, vogliamo spostare lo stato attivo sul primo pulsante (o su quello più recente) in .gui-popup. La libreria ci aiuta a farlo con i parametri element e target.

popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))

L'elemento ora passa il focus agli elementi secondari di destinazione <button> e consente la navigazione standard con i tasti freccia per sfogliare le opzioni.

Attivazione/disattivazione di aria-expanded

Mentre è visivamente evidente che un popup viene mostrato e nascosto, uno screen reader ha bisogno di più di semplici segnali visivi. JavaScript viene utilizzato qui per completare l'interazione :focus-within basata su CSS attivando/disattivando un attributo appropriato per lo screen reader.

popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})

popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})

Attivazione del tasto Escape

Il focus dell'utente è stato intenzionalmente inviato a una trappola, il che significa che dobbiamo fornire un modo per uscire. Il modo più comune è consentire l'utilizzo del tasto Escape. A questo scopo, monitora le pressioni dei tasti sul pulsante popup, poiché tutti gli eventi della tastiera sui figli verranno propagati a questo elemento principale.

popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})

Se il pulsante popup rileva la pressione di tasti Escape, rimuove lo stato attivo con blur().

Clic sui pulsanti suddivisi

Infine, se l'utente fa clic, tocca o interagisce con i pulsanti tramite la tastiera, l'applicazione deve eseguire l'azione appropriata. Il bubbling degli eventi viene utilizzato di nuovo qui, ma questa volta sul contenitore .gui-split-button, per intercettare i clic sui pulsanti di un popup secondario o dell'azione principale.

splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})

Conclusione

Ora che sai come ho fatto, come faresti tu?‽ 🙂

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