Creazione di un componente Pulsante di suddivisione

Una panoramica di base su come creare un componente Split-button accessibile.

In questo post voglio condividere le vostre riflessioni su un modo per creare un pulsante di suddivisione . Prova la demo.

Demo

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

Panoramica

I pulsanti suddivisi sono pulsanti che nascondono un pulsante principale e un elenco di pulsanti aggiuntivi. Sono utili per esporre un'azione comune e nidificare le azioni secondarie, utilizzate meno frequentemente, fino a quando non è necessario. Un pulsante diviso può essere fondamentale per ridurre al minimo il design. Un pulsante di suddivisione avanzato potrebbe persino ricordare l'ultima azione dell'utente e promuoverla nella posizione principale.

Puoi trovare un pulsante di suddivisione comune nella tua applicazione email. L'azione principale è l'invio, ma puoi decidere di inviare in un secondo momento oppure salvare una bozza:

Un esempio di pulsante di suddivisione visto in un'applicazione email.

L'area di azioni condivisa è utile, dato che l'utente non deve guardarsi intorno. Sapendo che le azioni email essenziali sono contenute nel pulsante di suddivisione.

Ricambi

Prima di esaminare l'orchestrazione complessiva e l'esperienza utente finale, analizziamo le parti essenziali di un pulsante di suddivisione. Lo strumento di controllo dell'accessibilità di VisBug viene utilizzato qui per mostrare una visualizzazione macro del componente, mettendo in evidenza gli aspetti di HTML, stile e accessibilità per ogni parte principale.

Gli elementi HTML che compongono il pulsante Suddividi.

Contenitore del pulsante Suddividi di primo livello

Il componente di livello più elevato è una flexbox in linea, con una classe gui-split-button, che contiene l'azione principale e l'.gui-popup-button.

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

Pulsante di azione principale

L'elemento <button> inizialmente visibile e attivabile si adatta all'interno del contenitore con due forme d'angolo corrispondenti per l'elemento attivo, hover e le interazioni attive in modo che sembrino all'interno di .gui-split-button.

La finestra di controllo che mostra le regole CSS per l&#39;elemento pulsante.

Il pulsante di attivazione/disattivazione popup

L'elemento di supporto "pulsante popup" consente di attivare e fare riferimento all'elenco di pulsanti secondari. Nota che non si tratta di un <button> e non è attivabile. Tuttavia, è l'ancoraggio 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 il pulsante gui-popup-button della classe.

La scheda popup

Si tratta di una scheda mobile secondaria al relativo ancoraggio .gui-popup-button, posizionata in modo assoluto e avvolgere semanticamente l'elenco di pulsanti.

La finestra di controllo che mostra le regole CSS per il popup gui della classe

Le azioni secondarie

Un elemento <button> attivabile con dimensioni del carattere leggermente più piccole rispetto al pulsante di azione principale presenta un'icona e uno stile complementare al pulsante principale.

La finestra di controllo che mostra le regole CSS per l&#39;elemento pulsante.

Proprietà personalizzate

Le seguenti variabili contribuiscono a creare un'armonia cromatica e una posizione centrale per modificare i valori utilizzati nel 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

Markup

L'elemento inizia come <div> con un nome della 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>

Osserva gli attributi aria aria-haspopup e aria-expanded. Questi segnali sono fondamentali per gli screen reader che devono conoscere la funzionalità e lo stato dell'esperienza con i pulsanti divisi. 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 popup, .gui-popup è un elemento secondario del pulsante che lo espande. L'unico problema di questa strategia è che il container .gui-split-button non può utilizzare overflow: hidden, in quanto tratterà il popup dalla visualizzazione visiva.

Un <ul> riempito di contenuti <li><button> si annuncia 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 fantasia e divertimento con i colori, ho aggiunto delle icone ai pulsanti secondari di 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 HTML e contenuti attivi, gli stili sono pronti per fornire colore e layout.

Stili del contenitore del pulsante Suddividi

Un tipo di visualizzazione inline-flex funziona bene per questo componente di wrapping, perché deve adattarsi ad altri pulsanti, azioni o elementi di suddivisione.

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

Lo stile <button>

I pulsanti sono molto efficaci per nascondere la quantità di codice necessario. Potrebbe essere necessario annullare o sostituire gli stili predefiniti del browser, ma dovrai anche forzare l'ereditarietà, aggiungere stati di interazione e adattarsi a vari tipi di preferenze utente e di input. Gli stili dei pulsanti vengono aggiunti rapidamente.

Questi pulsanti sono diversi dai normali pulsanti perché condividono uno sfondo con un elemento principale. In genere, il colore dello sfondo e del testo di un pulsante è lo stesso. Tuttavia, le 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);
  }
}

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 eleganza, 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 ottimo pulsante ha prestato attenzione alle microinterazioni e ai piccoli dettagli.

Una nota su :focus-visible

Osserva come gli stili dei pulsanti utilizzano :focus-visible anziché :focus. :focus è un tocco fondamentale per rendere accessibile un'interfaccia utente, ma ha uno svantaggio: non è intelligente se l'utente deve vederla o meno, si applica a qualsiasi obiettivo.

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

Applicare uno stile al pulsante popup

Una flexbox 4ch per centrare un'icona e ancorare un elenco di pulsanti popup. Come il pulsante principale, è trasparente fino a quando non viene passato il mouse o si interagisce con e viene allungato fino a riempire.

La parte della freccia del pulsante Suddividi 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);
}

Livello in stato di passaggio del mouse, stato attivo e stato attivo con Nesting CSS e il selettore di funzioni :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'hook principale per mostrare e nascondere i popup. Quando .gui-popup-button ha focus su una delle sue istanze secondarie, imposta opacity, posizione e pointer-events nell'icona e nel 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;
    }
  }
}

Con gli stili di entrata e uscita completati, l'ultimo passaggio consiste nel eseguire la transizione condizionale a seconda della 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 attento controllo del codice noterebbe che l'opacità è ancora in fase di transizione per gli utenti che preferiscono un movimento ridotto.

Applicare uno stile al popup

L'elemento .gui-popup è un elenco di pulsanti di una scheda mobile che utilizza proprietà personalizzate e unità relative da essere leggermente più piccole, abbinate in modo interattivo al pulsante principale e in base al brand con l'uso del colore. Le icone hanno un contrasto minore, sono più sottili e all'ombra c'è una sfumatura di blu brand. Come per i pulsanti, UI e UX sono il risultato della sovrapposizione di questi piccoli dettagli.

Un elemento di carta 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%))
  ;
}

Alle icone e ai pulsanti vengono assegnati i colori del brand per creare uno stile accattivante all'interno di ogni scheda con tema chiaro e scuro:

Link e icone per il pagamento, il pagamento rapido e l&#39;opzione 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 il tema scuro presenta aggiunte di ombre del testo e delle 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 generici delle icone <svg>

Tutte le icone hanno le dimensioni corrispondenti al pulsante font-size al quale vengono utilizzate utilizzando l'unità ch come inline-size. Ognuno di essi ha anche uno stile che aiuta a delineare le icone morbide e fluide.

.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 tutte le operazioni più complesse. Ecco l'elenco di proprietà logiche utilizzate: - display: inline-flex crea un elemento flessibile in linea. - padding-block e padding-inline in coppia, anziché padding in forma abbreviata, ottieni i vantaggi di aggiungere una spaziatura interna ai lati logici. - border-end-start-radius e amici arrotondano gli angoli in base alla direzione del documento. - inline-size anziché width garantisce che la dimensione non sia collegata alle dimensioni fisiche. - border-inline-start aggiunge un bordo all'inizio, che potrebbe essere a destra o a sinistra, a seconda della direzione dello script.

JavaScript

Quasi tutti i seguenti elementi JavaScript sono pensati per migliorare l'accessibilità. Due delle mie librerie helper sono usate per semplificare un po' le attività. BlingBlingJS viene utilizzato per query DOM brevi e per una facile configurazione del listener di eventi, mentre roving-ux facilita le interazioni accessibili da 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 l'importazione delle librerie sopra riportate e gli elementi selezionati e salvati in variabili, l'upgrade dell'esperienza manca alcune funzioni.

Indice mobile

Quando una tastiera o uno screen reader mette a fuoco .gui-popup-button, vogliamo inoltrarlo al primo pulsante (o più di recente) nella .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 lo stato attivo agli elementi secondari <button> di destinazione e consente la navigazione standard con i tasti Freccia per sfogliare le opzioni.

Attivazione/disattivazione di aria-expanded in corso...

Sebbene sia visivamente evidente che un popup viene visualizzato e nascosto, uno screen reader ha bisogno di più indicazioni visive. Qui viene utilizzato JavaScript per integrare l'interazione :focus-within guidata da CSS mediante l'attivazione/disattivazione di 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 inviata a una trappola, il che significa che dobbiamo fornire un modo per uscirne. Il modo più comune è consentire l'utilizzo della chiave Escape. A questo scopo, tieni d'occhio la pressione dei tasti sul pulsante popup, poiché gli eventi della tastiera sui dispositivi secondari verranno visualizzati come fumetto con l'elemento padre.

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

Se il pulsante popup rileva la pressione dei tasti Escape, rimuove lo stato attivo da se stesso blur().

Dividi clic sul pulsante

Infine, se l'utente fa clic, tocca o la tastiera interagisce con i pulsanti, l'applicazione deve eseguire l'azione appropriata. Il bubbling degli eventi viene nuovamente utilizzato qui, ma questa volta nel container .gui-split-button, per catturare i clic sui pulsanti 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 ci sono riuscito, come faresti? 🙂

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