Créer un composant de bouton "split-button"

Présentation générale de la création d'un composant accessible split-button.

Dans ce message, je vais vous expliquer comment créer un bouton de division . Tester la fonctionnalité

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> Démonstration

Si vous préférez la vidéo, voici une version YouTube de cet article:

Présentation

Les boutons fractionnés sont des boutons qui dissimulent un bouton principal et une liste de boutons supplémentaires. Ils sont utiles pour exposer une action courante lors de l'imbrication d'éléments secondaires, moins fréquemment utilisé des actions jusqu'à ce que cela soit nécessaire. Un bouton de division peut être crucial pour aider une conception chargée sont minimes. Un bouton de répartition avancée peut même se souvenir de la dernière action de l'utilisateur et le promouvoir en position principale.

Vous trouverez un bouton de répartition courant dans votre application de messagerie. L'action principale est envoyé, mais vous pourrez peut-être l'envoyer plus tard ou enregistrer un brouillon à la place:

Exemple de bouton de division dans une application de messagerie

La zone d'action partagée est agréable, car l'utilisateur n'a pas besoin de regarder autour de lui. Ils sachez que le bouton "Split" (Diviser) contient les actions essentielles sur l'e-mail.

Pièces

Analysons les éléments essentiels d'un bouton fractionné l'orchestration globale et l'expérience utilisateur finale. Accessibilité de VisBug L'outil d'inspection permet d'afficher une vue macro du composant, du code HTML, du style et de l'accessibilité pour chaque aspect important.

Éléments HTML qui composent le bouton de fractionnement.

Conteneur du bouton de fractionnement de niveau supérieur

Le composant de niveau supérieur est un Flexbox intégré, avec une classe de gui-split-button, contenant l'action principale et .gui-popup-button.

Classe gui-split-button inspectée et montrant les propriétés CSS utilisées dans cette classe.

Le bouton d'action principal

Le <button> initialement visible et sélectionnable tient dans le conteneur avec deux formes d'angle correspondantes pour ciblage, pointer et des interactions actives pour apparaissent dans .gui-split-button.

Outil d&#39;inspection affichant les règles CSS pour l&#39;élément de bouton

Bouton d'activation de la fenêtre pop-up

Le "bouton pop-up" sert à activer et à faire allusion à la liste boutons secondaires. Notez qu'il ne s'agit pas d'un élément <button> et qu'il n'est pas sélectionnable. Toutefois, Il s'agit de l'ancre de positionnement pour .gui-popup et de l'hôte pour :focus-within utilisé. pour présenter le pop-up.

Outil d&#39;inspection affichant les règles CSS pour la classe gui-popup-button.

La fiche pop-up

Il s'agit d'une carte flottante enfant à son ancre .gui-popup-button, position absolue et en encapsulant sémantiquement la liste de boutons.

Outil d&#39;inspection affichant les règles CSS pour la classe gui-popup

Les actions secondaires

Un élément <button> sélectionnable avec une taille de police légèrement inférieure à celle de l'élément principal bouton d'action se compose d'une icône et d'un bouton sans frais au bouton principal.

Outil d&#39;inspection affichant les règles CSS pour l&#39;élément de bouton

Propriétés personnalisées

Les variables suivantes contribuent à créer une harmonie des couleurs et un emplacement central pour modifier les valeurs utilisées dans le composant.

@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%);
  }
}

Mises en page et couleur

Majoration

L'élément commence par une <div> avec un nom de classe personnalisé.

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

Ajoutez le bouton principal et les éléments .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>

Notez les attributs ARIA aria-haspopup et aria-expanded. Ces indices sont essentiel pour que les lecteurs d'écran connaissent la fonctionnalité et l'état du fractionnement de l'expérience utilisateur. L'attribut title est utile pour tout le monde.

Ajoutez une icône <svg> et l'élément conteneur .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>

Pour placer directement les pop-up, .gui-popup est un enfant du bouton qui la développe. Le seul inconvénient de cette stratégie est .gui-split-button. Le conteneur ne peut pas utiliser overflow: hidden, car il empêchera le pop-up d'être visuellement présent.

Un élément <ul> rempli avec du contenu <li><button> s'annoncera en tant que "bouton" liste" aux lecteurs d'écran, c'est-à-dire exactement l'interface présentée.

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

Pour plus de style et pour m'amuser avec la couleur, j'ai ajouté des icônes aux boutons secondaires sur https://heroicons.com. Les icônes sont facultatives pour les deux les boutons principal et secondaire.

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

Styles

Une fois le code HTML et le contenu en place, les styles sont prêts à fournir de la couleur et une mise en page.

Appliquer un style au conteneur du bouton de fractionnement

Un type d'affichage inline-flex fonctionne bien pour ce composant d'encapsulation, car il doit s'aligner avec les autres actions, éléments ou boutons fractionnés.

.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;
}

Bouton de division

Style <button>

Les boutons sont très efficaces pour masquer la quantité de code nécessaire. Vous devrez peut-être annuler ou remplacer les styles par défaut du navigateur, mais vous devrez aussi en appliquer l’héritage, ajouter des états d’interaction et s’adapter à différentes préférences et types d'entrée. Les styles de boutons s'additionnent rapidement.

Ces boutons sont différents des boutons standards, car ils partagent un arrière-plan avec un élément parent. Habituellement, un bouton possède son arrière-plan et sa couleur de texte. En revanche, celles-ci le partagent et n'appliquent que leur propre arrière-plan lors de l'interaction.

.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;
}

Ajouter des états d'interaction avec quelques CSS pseudo-classes et utilisation de la mise en correspondance personnalisées pour l'état:

.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);
  }
}

Le bouton principal a besoin de quelques styles spéciaux pour compléter l'effet de conception:

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

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

Enfin, pour apporter une touche d'élégance, le bouton et l'icône du thème clair shadow:

.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 bon bouton a prêté attention aux micro-interactions et aux minuscules détails.

Remarque concernant :focus-visible

Notez que les styles de bouton utilisent :focus-visible au lieu de :focus. :focus est une touche cruciale pour rendre une interface utilisateur accessible, mais elle a une une chute: il n'est pas judicieux de savoir si l'utilisateur a besoin ou non de la voir ou non, il s'appliquera à tous les ciblages.

La vidéo ci-dessous tente de décomposer cette microinteraction pour vous montrer :focus-visible est une alternative intelligente.

Appliquer un style au bouton pop-up

Flexbox 4ch permettant de centrer une icône et d'ancrer une liste de boutons pop-up. J'aime le bouton principal, il reste transparent jusqu'à ce qu'il soit passé la souris ou qu'il n'interagisse pas et étirée pour remplir l'espace.

Flèche du bouton de répartition utilisée pour déclencher le pop-up.

.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);
}

Superposez des éléments à l'état actif, actif ou au passage de la souris avec CSS. L'imbrication Sélecteur de fonctionnalité :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);
  }
}

Ces styles constituent l'accroche principale pour afficher et masquer le pop-up. Lorsque .gui-popup-button comporte focus sur n'importe lequel de ses enfants, définissez opacity, position et pointer-events, sur l'icône et le pop-up.

.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;
    }
  }
}

Une fois les étapes d'entrée et de sortie terminées, la dernière partie consiste à de manière conditionnelle transformations de transition en fonction des préférences de mouvement de l'utilisateur:

.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 œil attentif sur le code remarquera que l'opacité est encore en transition pour les utilisateurs. qui préfèrent les mouvements réduits.

Appliquer un style au pop-up

L'élément .gui-popup est une liste de boutons de carte flottante utilisant des propriétés personnalisées. et les unités relatives sont légèrement plus petites et mises en correspondance de manière interactive avec le bouton et sur la marque avec son utilisation de la couleur. Remarquez que les icônes ont moins de contraste, sont plus fines, et l'ombre présente un soupçon de bleu. Comme pour les boutons, une interface utilisateur et une expérience utilisateur solides découle de l'empilement de ces petits détails.

Élément de carte flottante.

.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%))
  ;
}

Les icônes et les boutons se voient attribuer les couleurs de la marque pour un style harmonieux et thème clair:

Liens et icônes pour le paiement, Quick Pay et Enregistrer pour plus tard.

.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%;
  }
}

Le pop-up du thème sombre propose des ombres de texte et d'icône, ainsi que des éléments intense box shadow:

Pop-up avec le thème sombre.

.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));
    }
  }
}

Styles d'icônes génériques <svg>

Toutes les icônes ont une taille relativement proche du bouton font-size dans lequel elles sont utilisées en utilisant l'unité ch inline-size Chacun dispose également de styles pour aider à définir les icônes douces et fluide.

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

Mise en page de droite à gauche

Les propriétés logiques effectuent les tâches les plus complexes. Voici la liste des propriétés logiques utilisées: - display: inline-flex crée un élément flexible intégré. - padding-block et padding-inline combinés, au lieu de padding obtenir les avantages du remplissage des côtés logiques. - border-end-start-radius et amis va et arrondir les angles selon l'orientation du document. - inline-size au lieu de width garantit que la taille n'est pas liée à des dimensions physiques. - border-inline-start ajoute une bordure au début, qui peut être à droite ou à gauche selon l'orientation du script.

JavaScript

La quasi-totalité du code JavaScript suivant vise à améliorer l'accessibilité. Deux de mes les bibliothèques d'aide sont utilisées pour faciliter un peu les tâches. BlingBlingJS est utilisé pour les scripts concis des requêtes DOM et une configuration aisée de l'écouteur d'événements, roving-ux aide à faciliter l'accès les interactions avec le clavier et la manette de jeu pour le pop-up.

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

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

Avec les bibliothèques ci-dessus importées et les éléments sélectionnés et enregistrés dans et il vous reste quelques fonctions à compléter.

Indice de rotation

Lorsqu'un clavier ou un lecteur d'écran effectue la mise au point sur le .gui-popup-button, nous voulons déplacer le curseur vers le premier bouton (ou le plus récent) de la .gui-popup La bibliothèque nous aide à le faire avec element et target. paramètres.

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

L'élément passe maintenant le curseur aux enfants <button> cibles et active Flèche standard pour parcourir les options

Activation/Désactivation de aria-expanded...

Bien qu'il soit visuellement évident qu'un pop-up s'affiche et se masque, un lecteur d'écran a besoin de plus que de repères visuels. JavaScript est utilisé ici pour compléter l'interaction :focus-within pilotée par CSS en activant un attribut approprié pour le lecteur d'écran.

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

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

Activer la clé Escape

L'attention de l'utilisateur a été délibérément pris dans un piège, ce qui signifie que nous devons proposent un moyen de partir. La méthode la plus courante consiste à autoriser l'utilisation de la clé Escape. Pour ce faire, soyez attentif aux appuis sur le bouton pop-up, car les événements de clavier les enfants remonteront vers ce parent.

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

Si le bouton pop-up détecte des pressions sur la touche Escape, il n'est plus sélectionné. par blur()

Clics sur le bouton de répartition

Enfin, si l'utilisateur clique, appuie ou utilise le clavier, l'application doit effectuer l'action appropriée. L'ébullition de l'événement est utilisée ici, mais cette fois sur le conteneur .gui-split-button, pour intercepter clics à partir d'un pop-up enfant ou de l'action principale.

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

Conclusion

Maintenant que vous savez comment j'ai fait, comment feriez-vous ? 😃

Diversifiez nos approches et découvrons toutes les manières de créer des applications sur le Web. Créer une démonstration, me envoyer des tweets et je l'ajouterai à la section des remix de la communauté ci-dessous.

Remix de la communauté