Creazione di un componente Pulsante di suddivisione

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

In questo post voglio condividere alcune idee su come creare un pulsante diviso . Prova la demo.

Demo

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

Panoramica

I pulsanti suddivisi sono pulsanti che nascondono un pulsante principale e un elenco di pulsanti aggiuntivi. Sono utili per mostrare un'azione comune e nidificare azioni secondarie meno utilizzate fino a quando non sono necessarie. Un pulsante diviso può essere fondamentale per rendere un design elaborato più minimalista. Un pulsante di suddivisione avanzato potrebbe persino ricordare l'ultima azione dell'utente e promuoverla nella posizione principale.

Un pulsante di separazione comune è disponibile nell'applicazione email. L'azione principale è Invia, ma potresti inviare il messaggio in un secondo momento o salvare una bozza:

Un esempio di pulsante diviso in un'applicazione email.

L'area di azioni condivisa è utile, perché l'utente non deve guardarsi intorno. Lo fanno sapendo che le azioni email essenziali sono contenute nel pulsante diviso.

Parti

Analizziamo le parti essenziali di un pulsante diviso prima di discutere della loro orchestrazione complessiva e dell'esperienza utente finale. Lo strumento di controllo dell'accessibilità di VisBug viene utilizzato qui per mostrare una visualizzazione macro del componente, evidenziando aspetti dell'HTML, dello stile e dell'accessibilità per ogni parte principale.

Gli elementi HTML che compongono il pulsante diviso.

Contenitore del pulsante di suddivisione di primo livello

Il componente di primo livello è un flexbox in linea, con una classe gui-split-button, contenente l'azione principale e .gui-popup-button.

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

Il pulsante di azione principale

<button>, inizialmente visibile e attivabile, si inserisce nel contenitore con due forme angolari corrispondenti per le interazioni di attivazione, tasto Maiusc e attiva in modo che appaiano contenute in .gui-split-button.

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

Pulsante di attivazione/disattivazione del popup

L'elemento di supporto "pulsante popup" serve per attivare e fare riferimento all'elenco dei pulsanti secondari. Tieni presente che non è un <button> e non è possibile acquisire il relativo focus. 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

Si tratta di una scheda secondaria mobile rispetto all'elemento di ancoraggio .gui-popup-button, posizionata in modo assoluto e che avvolge semanticamente l'elenco di pulsanti.

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

Le azioni secondarie

Un <button> attivabile 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 del pulsante.

Proprietà personalizzate

Le seguenti variabili consentono di creare armonia di colori e un punto di riferimento 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>

Tieni presente gli attributi aria aria-haspopup e aria-expanded. Questi indicatori sono fondamentali per consentire agli screen reader di conoscere la funzionalità e lo stato dell'esperienza con i pulsanti suddivisi. 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 popup semplice, .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à visualizzato.

Un <ul> compilato con contenuti <li><button> verrà annunciato 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 un tocco di stile e per divertirti con i colori, ho aggiunto icone ai pulsanti secondari provenienti da https://heroicons.com. Le icone sono facoltative sia per i pulsanti principali sia 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

Una volta impostati HTML e contenuti, gli stili sono pronti per fornire colore e layout.

Aggiungere stili al contenitore del pulsante diviso

Un tipo di visualizzazione inline-flex è adatto a questo componente di a capo, in quanto deve essere in linea con altri pulsanti, azioni o elementi suddivisi.

.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 suddivisione.

Lo stile <button>

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

Questi pulsanti sono diversi dai pulsanti normali perché condividono un sfondo con un elemento principale. In genere, un pulsante ha il proprio colore di sfondo e di testo. Tuttavia, questi lo condividono e applicano il proprio sfondo solo all'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 pseudoclassi 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 design:

.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 un'ombra:

.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 pulsante eccellente è stato realizzato 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 svantaggio: non è intelligente nel capire se l'utente deve visualizzarla o meno, ma si applica a qualsiasi opzione di messa a fuoco.

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

Aggiungere 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 si passa il mouse sopra o non si interagisce con esso e si estende 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);
}

Applica gli stati di passaggio del mouse, di attivazione e attivo con il nidificazione CSS 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 l'elemento principale per mostrare e nascondere il popup. Quando .gui-popup-button ha focus su uno dei suoi elementi secondari, imposta opacity, la 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 in e out, l'ultimo passaggio consiste nel applicare le trasformazioni di transizione in modo condizionale in base alle preferenze 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 l'opacità è ancora in transizione per gli utenti che preferiscono ridurre il movimento.

Stilizzazione del popup

L'elemento .gui-popup è un elenco di pulsanti di schede fluttuanti 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 per l'uso del colore. Notare che le icone hanno meno contrasto, sono più sottili e l'ombra ha un tocco di blu del brand. Come per i pulsanti, un'interfaccia utente e un'esperienza utente efficaci sono il risultato dell'accumulo 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 sono assegnati ai colori del brand per creare uno stile accattivante in ogni scheda con tema scuro e chiaro:

Link e icone per il pagamento, il pagamento rapido 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 con tema scuro ha aggiunte di ombre di testo e icone, 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 generiche <svg>

Tutte le icone hanno dimensioni relative al pulsante font-size in cui vengono utilizzate utilizzando l'unità ch come inline-size. A ciascuna è stato anche assegnato uno stile per aiutare a delineare le icone in modo morbido e scorrevole.

.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 eseguono tutto il lavoro complesso. Di seguito è riportato l'elenco delle proprietà logiche utilizzate: - display: inline-flex crea un elemento flex in linea. - padding-block e padding-inline come coppia, anziché la rappresentazione abbreviata padding, per usufruire dei vantaggi dell'aggiunta di spazi ai lati logici. - border-end-start-radius e amici arrotonderanno i bordi in base all'orientamento 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 può essere a destra o a sinistra a seconda della direzione dello script.

JavaScript

Quasi tutto il codice JavaScript seguente serve a migliorare l'accessibilità. Due delle mie librerie di supporto vengono utilizzate per semplificare le attività. BlingBlingJS viene utilizzato per query DOM concise e una facile configurazione degli ascoltatori di eventi, mentre roving-ux contribuisce a facilitare 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')

Dopo aver importato le librerie precedenti e aver selezionato e salvato gli elementi nelle variabili, l'upgrade dell'esperienza manca solo di alcune funzioni.

Indice mobile

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

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

Ora l'elemento passa il focus agli elementi secondari <button> target e consente di navigare tra le opzioni con i tasti freccia standard.

Attiva/disattiva aria-expanded

Sebbene sia visivamente evidente che un popup viene visualizzato e nascosto, uno screen reader ha bisogno di più di semplici indicatori visivi. JavaScript viene utilizzato qui per integrare 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 della chiave Escape

L'attenzione dell'utente è stata intenzionalmente indirizzata a una trappola, il che significa che dobbiamo fornire un modo per uscire. Il modo più comune è consentire l'utilizzo della chiave Escape. A tal fine, controlla le pressioni dei tasti sul pulsante popup, poiché tutti gli eventi relativi alla tastiera sugli elementi figli verranno visualizzati in questo elemento principale.

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

Se il pulsante popup rileva una pressione del tasto Escape, rimuove il proprio stato attivo con blur().

Clic sul pulsante di suddivisione

Infine, se l'utente fa clic, tocca o la tastiera interagisce con i pulsanti, l'applicazione deve eseguire l'azione appropriata. Qui viene utilizzato nuovamente il bubbling degli eventi, ma questa volta sul contenitore .gui-split-button per rilevare i clic sul pulsante da un popup secondario o dall'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? 🙂

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, twittami i link e io la aggiungerò alla sezione dei remix della community di seguito.

Remix della community