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 far sembrare minimo un design complesso. Un pulsante di suddivisione avanzato potrebbe persino ricordare l'ultima azione dell'utente e promuoverla nella posizione principale.

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

Un esempio di pulsante di suddivisione, come si vede 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 di suddivisione prima di analizzare l'orchestrazione complessiva e l'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 di suddivisione.

Contenitore del pulsante di suddivisione di primo livello

Il componente di livello più elevato è un flexbox in linea con una classe gui-split-button contenente l'azione principale e l'.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&#39;elemento di supporto &quot;pulsante popup&quot; serve per attivare e fare riferimento all&#39;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.

La finestra di controllo 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 semplice posizionamento dei 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à visualizzato.

Un elemento <ul> pieno di contenuti <li><button> si annuncerà come "elenco di pulsanti" per gli screen reader, ovvero 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.

Definire lo stile del contenitore del pulsante diviso

Un tipo di visualizzazione inline-flex è adatto a questo componente di a capo perché 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. Potrebbe essere necessario annullare o sostituire gli stili predefiniti del browser, ma dovrai anche applicare un'ereditarietà, aggiungere stati di interazione e adattarti a vari tipi di input e preferenze dell'utente. Gli stili dei pulsanti si accumulano rapidamente.

Questi pulsanti sono diversi dai normali pulsanti perché condividono lo sfondo con un elemento principale. In genere, un pulsante ha il proprio colore di sfondo e di testo. Queste persone, tuttavia, lo condividono e applicano solo il proprio sfondo 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 pseudo-classi CSS e utilizza le 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);
  }
}

Per completare l'effetto del design, il pulsante principale richiede alcuni stili speciali:

.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 visualizzarlo o meno, ma si applica a qualsiasi opzione attivata.

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

Definizione dello stile del 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 un livello agli 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 transizioni delle trasformazioni 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 mobili di schede che utilizzano proprietà personalizzate e unità relative che devono essere leggermente più piccole, abbinate in modo interattivo al pulsante principale e in base al brand con 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 un'estetica gradevole 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 uniforme.

.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 arrotondano gli angoli 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 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 imposta lo stato attivo su .gui-popup-button, vogliamo inoltrare lo stato attivo sul primo pulsante (o sull'ultimo pulsante selezionato) in .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.

Attivazione/disattivazione di 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)
})

Abilitazione della chiave Escape in corso...

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 dei 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 con i link e io la aggiungerò alla sezione dei remix della community qui sotto.

Remix della community