Choix exclusif entre deux à cinq options courtes, toutes visibles en même temps (pratique, niveau, mode de couleur du profil).
stableÀ valider
// Titre : Par défaut
import { useState } from 'react';
import Segmented from '@pathou/components/Segmented';
export default function Exemple() {
const [pratique, setPratique] = useState<'route' | 'gravel' | 'vtt'>('route');
return (
<Segmented
label="Pratique"
value={pratique}
options={[
['route', 'Route'],
['gravel', 'Gravel'],
['vtt', 'VTT'],
]}
onChange={setPratique}
/>
);
}
Installation
Copie le fichier dans ton projet, ainsi que les tokens qu’il utilise.
import type { KeyboardEvent } from 'react';
/**
* Choix exclusif (pratique, niveau). Clavier comme un groupe de boutons radio : une seule étape de tabulation,
* sur l'option choisie, et les flèches passent à l'option voisine (Début et Fin aux extrémités).
*/
export default function Segmented<T extends string>({ label, value, options, onChange }: { label: string; value: T; options: [T, string][]; onChange: (v: T) => void }) {
const onKey = (e: KeyboardEvent<HTMLDivElement>) => {
const i = options.findIndex(([v]) => v === value);
const n = options.length;
const next = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1, ArrowUp: i - 1, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
const j = (next + n) % n;
onChange(options[j][0]);
(e.currentTarget.children[j] as HTMLElement | undefined)?.focus();
};
return (
<div className="segmented" role="radiogroup" aria-label={label} onKeyDown={onKey}>
{options.map(([v, text]) => (
<button key={v} role="radio" aria-checked={v === value} tabIndex={v === value ? 0 : -1} className={v === value ? 'on' : ''} onClick={() => onChange(v)}>
{text}
</button>
))}
</div>
);
}
Utilisation
import Segmented from './components/Segmented';
const [pratique, setPratique] = useState<'route' | 'gravel' | 'vtt'>('route');
<Segmented
label="Pratique"
value={pratique}
options={[['route', 'Route'], ['gravel', 'Gravel'], ['vtt', 'VTT']]}
onChange={setPratique}
/>Anatomie
| # | Élément | Rôle |
|---|---|---|
| 1 | Rail | Le conteneur radiogroup, qui porte le libellé (aria-label). Fond --chip, translucide. |
| 2 | Option choisie | Un button de rôle radio avec aria-checked="true". Fond --panel, texte --text. |
| 3 | Option libre | Un button de rôle radio avec aria-checked="false", hors de l’ordre de tabulation. Texte --muted. |
États
checkedL’option choisie se détache sur le rail, sur fond de panneau avec une ombre légère. Elle est la seule à recevoir la tabulation.hoverUne option non choisie survolée garde son style ; seul le curseur change.focusVisibleAu clavier, l’option qui a le focus porte l’anneau de focus du navigateur.Guidelines
- MUSTDonner un
labelqui nomme le choix (« Pratique », « Niveau ») ; il est annoncé par les lecteurs d’écran et n’est pas affiché. - SHOULDGarder des options d’un ou deux mots, pour que le groupe tienne sur une ligne en mobile.
- MUST NOTDépasser cinq options ; au-delà, passer à une liste déroulante.
Clavier
| Touche | Effet |
|---|---|
| Tab | Entre dans le groupe sur l’option choisie, puis en sort ; une seule étape de tabulation pour tout le groupe. |
| Flèches | Passent à l’option voisine et la choisissent aussitôt, en boucle. |
| DébutFin | Choisissent la première ou la dernière option. |
Référence de l’API
| Prop | Type | Défaut |
|---|---|---|
label | string | requis |
value | T | requis |
options | [T, string][] | requis |
onChange | (v: T) => void | requis |