# Clavier, accessibilité et tactile

> Activez la navigation clavier complète, comprenez focus et annonces, proposez des alternatives au glisser et adaptez la frise au tactile et aux aides techniques.

Source: https://superscheduler.org/fr/docs/keyboard-accessibility-touch/
Reviewed: 2026-10-07

Dans Pro, activez keyboardEnabled (désactivé par défaut), ajoutez keyboardMode: 'Full' pour le jeu de touches complet et keyboardTarget: 'component' pour que les touches n’agissent que lorsque la grille a le focus. La grille est un arrêt de tabulation unique avec role="grid" : le focus se déplace via aria-activedescendant et les modifications sont annoncées en neuf langues. Lite intègre la navigation aux flèches. Sur écran tactile, maintenez le doigt sur un événement pour le déplacer et faites glisser ses poignées pour le redimensionner.

Un planificateur de ressources est une grande grille à deux dimensions, ce qui rend la prise en charge du clavier et des lecteurs d’écran plus difficile que pour une liste ou un formulaire. SuperScheduler donne à la grille un arrêt de tabulation unique, un focus itinérant qui résiste à la virtualisation, des annonces vocales pour le focus et les modifications, et des équivalents clavier pour déplacer et redimensionner les événements. Ce guide explique ce que fait chaque édition, comment l’activer et ce que votre application doit encore fournir.

## Lite et Pro en un coup d’œil
| | Lite (`super-scheduler-lite`) | Pro (`super-scheduler`) |
|---|---|---|
| Clavier | Toujours actif : les flèches déplacent la cellule active, Entrée ou Espace l’active | Désactivé par défaut ; `keyboardEnabled`, plus `keyboardMode: 'Full'` pour le modèle complet |
| Événements | Boutons natifs : Tab les atteint, Entrée ou Espace les clique | Atteints avec les flèches à l’intérieur de la grille ; Entrée lance le flux de clic |
| Édition au clavier | Non (édition en lecture seule) | Déplacement avec Alt+flèches, redimensionnement avec Alt+Maj+Flèche gauche/droite (mode Full) |
| Annonces | Non | Focus, sélection et modifications validées, en neuf langues |
| Nom de la grille | Option `ariaLabel` (« Resource schedule » par défaut) | « Scheduler » intégré (« Planificador » pour les locales espagnoles) |
| Tactile | Défilement et appuis natifs | Appui long pour déplacer, poignées pour redimensionner, pincement pour zoomer |

## Activer le clavier dans Pro
Pro garde le clavier désactivé tant que vous ne définissez pas `keyboardEnabled: true`. Le mode par défaut, `keyboardMode: 'SuperScheduler'`, gère les flèches, Entrée et Maj+Flèche gauche/droite. `keyboardMode: 'Full'` ajoute le reste du modèle : Début/Fin, Page précédente/Page suivante, Espace, le déplacement et le redimensionnement des événements, la touche Menu et l’annonce de textes d’aide. Le mode Full sans `keyboardEnabled` ne fait rien et émet un avertissement en développement.

`keyboardTarget` détermine où les touches sont écoutées. La valeur par défaut, `'document'`, réagit aux touches pressées n’importe où dans la page en dehors des champs de texte, ce qui prive le défilement de la page des flèches. Utilisez `'component'` pour que les touches n’agissent que lorsque la grille a le focus ; c’est aussi le bon choix quand une page contient plusieurs planificateurs.

```tsx
// src/AccessiblePlanner.tsx
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'

// Module-level: the same object on every render.
const KEYBOARD: SchedulerProps = {
  keyboardEnabled: true,
  // Keys act only while the grid has focus; the page keeps its own arrow-key scrolling.
  keyboardTarget: 'component',
  keyboardMode: 'Full',
  keyboardOptions: { pageRows: 10, zoomKeys: true },
}

const UNDER_MAINTENANCE = new Set<SuperScheduler.ResourceId>(['room-104'])

export function AccessiblePlanner(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onOpen: (id: SuperScheduler.EventId) => void
}) {
  const { onOpen } = props
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      ...KEYBOARD,
      // Enter on a focused event runs the same click flow as the pointer.
      onEventClick: (args) => onOpen(args.e.id()),
      // Alt + arrow moves go through the same rules as drags.
      onEventMoving: (args) => {
        if (UNDER_MAINTENANCE.has(args.resource)) {
          args.allowed = false
          args.message = 'Room under maintenance'
        }
      },
    }),
    [onOpen],
  )

  return (
    // The grid's own accessible name is generic: label the region around it.
    <section aria-labelledby="room-plan-title">
      <h2 id="room-plan-title">Room plan, October 2026</h2>
      <SuperSchedulerComponent
        {...config}
        startDate="2026-10-01"
        days={31}
        scale="Day"
        locale="en-us"
        resources={props.resources}
        events={events}
      />
    </section>
  )
}
```
Vous devriez pouvoir entrer dans la grille avec Tab, vous déplacer avec les flèches, appuyer sur Entrée sur un événement pour l’ouvrir et sur Alt+Flèche bas sur un événement pour commencer à le déplacer. Le déplacer sur la chambre 104 puis appuyer sur Entrée annonce « Not allowed here », et l’événement reste à sa place.

`keyboardOptions` règle le mode Full :

| Option | Défaut | Effet |
|---|---|---|
| `pageRows` | lignes visibles moins une | Nombre de lignes parcourues par Page précédente et Page suivante |
| `contextMenuKey` | `true` en mode Full | La touche Menu et Maj+F10 ouvrent le menu de l’élément qui a le focus |
| `bubbleOnFocus` | `false` | Affiche la bulle de l’événement tant qu’il a le focus clavier |
| `selectAll` | `true` en mode Full | Ctrl/Cmd+A sélectionne tous les événements visibles (nécessite `allowMultiSelect`) |
| `zoomKeys` | `false` | Ctrl/Cmd avec `=` ou `+`, `-` et `0` zooment vers l’avant, vers l’arrière et reviennent au niveau initial |

`zoomKeys` est désactivé par défaut pour que les raccourcis de zoom de page du navigateur continuent de fonctionner.

## Touches
| Touches | Mode | Effet |
|---|---|---|
| Flèches | les deux | Déplacent le focus. Gauche et Droite s’arrêtent sur chaque événement et chaque cellule vide de la ligne ; Haut et Bas changent de ligne. |
| Entrée | les deux | Sur un événement : le flux de clic (`onEventClick`, puis `eventClickHandling`). Sur une cellule : la sélectionne comme plage de temps. |
| Maj+Flèche gauche / Maj+Flèche droite | les deux | Étend une plage de temps à partir de la cellule qui a le focus ; relâcher Maj la sélectionne. |
| Espace | Full | Sur une cellule : l’ajoute à la sélection ou l’en retire. Sur un événement : comme Entrée. |
| Début / Fin | Full | Première ou dernière cellule de la ligne ; avec Ctrl/Cmd, première ou dernière ligne. |
| Page précédente / Page suivante | Full | Déplace le focus de `pageRows` lignes. |
| Alt+flèches | Full | Commence à déplacer l’événement qui a le focus ; les flèches le déplacent, Entrée ou Espace le dépose. |
| Alt+Maj+Flèche gauche / droite | Full | Commence à redimensionner la fin de l’événement qui a le focus ; Gauche et Droite la modifient, Entrée confirme. Sur un titre de colonne, déplace la colonne. |
| Échap | les deux | Annule un déplacement, un redimensionnement ou une plage au clavier. Pendant un déplacement ou un redimensionnement au clavier, Tab l’annule aussi et quitte la grille. |
| Touche Menu, Maj+F10 | Full | Ouvre le menu de l’événement, de l’en-tête de ligne ou de la cellule qui a le focus. |
| Ctrl/Cmd+A | Full | Sélectionne tous les événements visibles. |

> **Behavior:**
> Les déplacements et redimensionnements au clavier passent par les mêmes règles qu’au pointeur : `onEventMoving` et `onEventResizing` peuvent refuser une position, `onEventMove` peut annuler ou confirmer de façon asynchrone, et `allowEventOverlap`, les événements verrouillés et les cellules désactivées s’appliquent. Les touches saisies dans les champs, les zones de texte et les éléments modifiables gardent leur comportement normal, et une touche que votre handler `onKeyDown` annule avec `args.preventDefault()` n’est pas traitée par la grille.

## Modèle de focus et lecteurs d’écran
La grille Pro a `role="grid"` avec `aria-rowcount` et `aria-colcount` ; les en-têtes de ligne sont des `rowheader`, les cellules d’en-tête de temps des `columnheader`, et les cellules et événements des `gridcell`. Les lignes et cellules hors de la zone visible ne sont pas dans le DOM : le focus ne passe donc pas d’un élément à l’autre. À la place :

- la racine de la grille est l’unique arrêt de tabulation (`tabindex="0"`) tant que le clavier est activé ;
- la cellule ou l’événement qui a le focus est un nœud de focus vers lequel la racine pointe avec `aria-activedescendant`, et ce nœud résiste au défilement et à la virtualisation ;
- le libellé du nœud de focus se lit comme « Room 101, Oct 1 » pour une cellule et « Ana, Room 101, Oct 2 – 4 » pour un événement.

Le nom d’un événement vient de son `ariaLabel`, puis de son `text`, puis de son `html` en texte brut, puis de son id. Le nom d’une ligne est le `name` de sa ressource. Quand `html` affiche autre chose que `text`, définissez `ariaLabel` dans `onBeforeEventRender` :

```tsx
// src/labelVisit.ts
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'

type Visit = { patient: string; kind: 'checkup' | 'surgery'; color: string }

const KIND_LABEL: Record<Visit['kind'], string> = { checkup: 'check-up', surgery: 'surgery' }

/** Pass as `onBeforeEventRender` (module-level, so its identity never changes). */
export const labelVisit: NonNullable<SchedulerProps['onBeforeEventRender']> = (args) => {
  const visit = args.data as SuperScheduler.EventRenderData<Visit>
  args.data.backColor = visit.color
  // Dark text on light fills and light text on dark ones (WCAG contrast).
  args.data.fontColor = SuperScheduler.ColorUtil.contrasting(visit.color)
  // `html` is trusted markup: escape what users typed.
  args.data.html = `<strong>${SuperScheduler.Util.escapeHtml(visit.patient)}</strong>`
  // The accessible name of the event. Focus labels and announcements append
  // the row name and the dates, so they are not repeated here.
  args.data.ariaLabel = `${visit.patient}, ${KIND_LABEL[visit.kind]}`
}
```
Comme les libellés de focus et les annonces ajoutent la ligne et les dates, `ariaLabel` ne doit contenir que ce qui identifie l’événement. `SuperScheduler.ColorUtil.contrasting(color)` renvoie un texte foncé pour les fonds clairs et un texte clair pour les fonds foncés.

Le nom accessible de la grille elle-même est « Scheduler » (« Planificador » quand la locale commence par `es`), et la version 0.1.0 ne propose aucune option pour le changer. Placez le planificateur dans une région étiquetée par un titre visible, comme le fait le snippet de configuration.

Depuis le code, `control.keyboard` propose `focusEvent(e or id)`, `focusCell(date, resource)`, `getFocus()`, `move(direction)`, `clearFocus()` et `resetFocus()`. `onKeyboardFocusChange` (annulable) et `onKeyboardFocusChanged` signalent les changements de focus avec `previous` et `focus` (`{ e }` ou `{ cell }`) ; utilisez-les pour synchroniser un panneau de détail avec le clavier.

### Annonces
Une région live non intrusive (polite), placée dans la grille, annonce :

- en mode Full, un court texte d’aide la première fois que la grille reçoit le focus ;
- les sélections (« Selected: Room 101, Oct 4 »), les désélections et « N events selected » ;
- le début d’un déplacement ou d’un redimensionnement au clavier, avec des instructions ;
- les déplacements et redimensionnements validés (« Event moved to Room 101, Oct 3 – 5 »), qu’ils viennent du clavier ou du pointeur ;
- « Cancelled », « Not allowed here » et les déplacements de colonnes.

Les textes suivent le premier segment de la `locale` du planificateur : anglais, espagnol, catalan, basque, galicien, allemand, français, italien et portugais. Les autres langues se rabattent sur l’anglais. Les packs de langue autres que l’anglais et l’espagnol sont chargés à la demande.

> **Limitation:**
> Dans la version 0.1.0, les textes des annonces sont intégrés : `keyboardOptions` ne permet pas de les remplacer. Les dates des annonces suivent la locale du planificateur.

### La touche Menu
En mode Full, la touche Menu et Maj+F10 ouvrent le menu de l’élément qui a le focus lorsqu’il s’agit d’un `SuperScheduler.Menu` : le `contextMenu` de l’événement (ou le `contextMenu` du contrôle), `contextMenuResource` sur un en-tête de ligne, et `contextMenuSelection` sur une cellule. Si votre application dessine son propre menu depuis `onEventRightClick`, gérez vous-même la touche dans `onKeyDown` :

```tsx
// src/menuHandlers.ts
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

export interface MenuRequest {
  readonly eventId: SuperScheduler.EventId
  /** Viewport coordinates where the application opens its own menu. */
  readonly x: number
  readonly y: number
}

/**
 * Opens the application's menu from the pointer (right click) and from the keyboard
 * (Menu key, Shift+F10). The library's Menu key support covers `SuperScheduler.Menu`
 * objects only, so a custom menu handles the key in `onKeyDown`.
 */
export function menuHandlers(open: (request: MenuRequest) => void): SchedulerProps {
  return {
    onEventRightClick: (args) => {
      args.preventDefault()
      open({ eventId: args.e.id(), x: args.originalEvent.clientX, y: args.originalEvent.clientY })
    },
    onKeyDown(args) {
      const key = args.originalEvent
      if (key.key !== 'ContextMenu' && !(key.key === 'F10' && key.shiftKey)) return
      const focused = this.keyboard.getFocus().e
      const root = key.target
      if (focused === undefined || !(root instanceof HTMLElement)) return
      // Skips the library's own handling of the key.
      args.preventDefault()
      // The grid points at the focused item with aria-activedescendant.
      const ring = root.ownerDocument.getElementById(
        root.getAttribute('aria-activedescendant') ?? '',
      )
      const box = (ring ?? root).getBoundingClientRect()
      open({ eventId: focused.id(), x: box.left, y: box.bottom })
    },
  }
}
```
Étalez `menuHandlers(open)` dans les props du planificateur, en le mémoïsant. Votre menu est alors responsable de son propre focus : déplacez le focus dans le menu à son ouverture et rendez-le à la grille à sa fermeture.

## Alternatives accessibles au glisser
Le [critère de succès 2.5.7 des WCAG 2.2](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) exige un moyen d’accomplir avec de simples actions de pointeur ce que permet le glisser. Le mode clavier Full couvre les utilisateurs du clavier, mais pas une personne qui utilise un pointeur simple, un contacteur ou la commande vocale. Donnez à chaque événement un chemin sans glisser, par exemple un panneau de détail ou une entrée de menu contextuel avec des champs début, fin et ressource qui met à jour votre state (ou appelle `control.events.update` avec un nouvel objet). Exécutez la même validation que dans `onEventMoving` avant d’enregistrer, pour que les deux chemins appliquent les mêmes règles.

## Tactile
Sur écran tactile, un doigt fait défiler la frise. Le reste du modèle tactile :

- **Déplacer un événement.** Maintenez le doigt immobile sur l’événement pendant `tapAndHoldTimeout` (300 ms), puis faites glisser. Un doigt qui bouge de plus de 8 px environ avant ce délai fait défiler à la place. `eventTapAndHoldHandling` détermine l’effet d’un appui long : `'Move'` (par défaut), `'ContextMenu'` (ouvre le `SuperScheduler.Menu` de l’événement) ou `'Disabled'`. Un appui long sur un en-tête de ligne ouvre `contextMenuResource`.
- **Redimensionner.** Un appui sur un événement affiche ses poignées, avec des cibles tactiles de 44 px (`--super-scheduler-handle-target`) ; faites glisser une poignée pour redimensionner. Le bord d’un événement sous un doigt le déplace au lieu de le redimensionner.
- **Sélectionner du temps.** Un appui sur une cellule vide la sélectionne (`origin: 'click'`) ; un appui long suivi d’un glisser sélectionne une plage.
- **Zoom.** Un pincement à deux doigts zoome (`zoomGesture.pinch`, activé par défaut). Un deuxième doigt annule tout glisser en cours.
- **Survol.** Le tactile n’a pas de survol. Les cartes de survol de `eventHover` peuvent être épinglées d’un appui (`pin: 'click'`) ; les [zones](https://superscheduler.org/fr/docs/react-render-slots/) avec `visibility: 'TouchVisible'` restent visibles sur les appareils tactiles, alors que les zones `'Hover'` n’apparaissent pas.

Lite est en lecture seule : il défile nativement et signale les appuis via `onEventClick` et `onTimeRangeClick`.

## Animations réduites, contraste et couleurs forcées
Pro lit les préférences de l’utilisateur via le CSS et les media queries, sans option à définir :

- `prefers-reduced-motion: reduce` règle `--super-scheduler-duration` sur `0s`, rend `control.zoom.animateTo()` instantané et supprime les transitions des cartes de survol ;
- `prefers-contrast: more` renforce les bordures, les lignes de grille, les séparateurs de lignes et le contour de sélection ;
- `forced-colors: active` fait passer le thème aux couleurs système (`Highlight`, `CanvasText`, `GrayText`) et supprime les grisés décoratifs, comme ceux des week-ends et du jour courant.

Lite adapte aussi ses bordures et son contour de focus aux couleurs forcées. Si vous remplacez les couleurs par vos propres tokens ou votre propre CSS, testez de nouveau ces modes : vos surcharges peuvent les annuler.

## Clavier dans Lite
Lite ne demande aucune configuration. La grille peut recevoir le focus, porte `aria-readonly` et tient son nom de l’option `ariaLabel`. Les flèches déplacent la cellule active (annoncée via `aria-activedescendant`), Entrée ou Espace appelle `onTimeRangeClick` pour cette cellule, et chaque événement est un bouton natif.

```tsx
// src/OccupancyBoard.tsx
import { SuperSchedulerComponent } from 'super-scheduler-lite'
import type { SuperScheduler } from 'super-scheduler-lite'
import 'super-scheduler-lite/styles.css'

const ROOMS: SuperScheduler.ResourceData[] = [
  { id: 'r101', name: 'Room 101' },
  { id: 'r102', name: 'Room 102' },
]

const STAYS: SuperScheduler.EventData[] = [
  { id: 'b1', resource: 'r101', start: '2026-10-02', end: '2026-10-05', text: 'Lena Fischer' },
]

export function OccupancyBoard(props: {
  onOpenBooking: (id: SuperScheduler.EventData['id']) => void
  onOpenDay: (resource: SuperScheduler.ResourceData['id'], day: string) => void
}) {
  return (
    <SuperSchedulerComponent
      // The grid's accessible name (default "Resource schedule").
      ariaLabel="Room occupancy, October 2026"
      startDate="2026-10-01"
      days={31}
      scale="Day"
      resources={ROOMS}
      events={STAYS}
      // Events are native buttons: Tab reaches them, Enter and Space click them.
      onEventClick={({ e }) => props.onOpenBooking(e.data.id)}
      // Arrow keys move the active cell; Enter or Space activates it.
      onTimeRangeClick={({ start, resource }) =>
        props.onOpenDay(resource, start.toString('yyyy-MM-dd'))
      }
    />
  )
}
```
## Ce que votre application doit encore garantir
La bibliothèque gère la grille. Les points suivants relèvent de votre application :

- **Contraste.** Les couleurs d’événement que vous définissez avec `backColor`, `fontColor`, du CSS ou du contenu personnalisé doivent respecter les exigences de contraste en mode clair comme en mode sombre.
- **Noms dans le contenu personnalisé.** Le HTML de `onBeforeEventRender`, les slots React et le balisage des en-têtes de ligne sont sous votre responsabilité : gardez un `text` explicite ou définissez `ariaLabel`, donnez des alternatives textuelles aux icônes et évitez les contrôles interactifs dans le contenu des événements.
- **Menus, boîtes de dialogue et panneaux.** Gestion du focus, libellés et prise en charge d’Échap pour tout ce que vous ouvrez depuis la grille.
- **Un chemin sans glisser** pour chaque action de glisser, comme décrit plus haut.
- **Structure de la page.** Un titre ou un libellé pour la région qui entoure la grille, et une place cohérente dans l’ordre de tabulation.
- **Tests.** Vérifiez votre configuration avec un lecteur d’écran et un outil de contrôle automatique ; les surcharges et le contenu personnalisé peuvent changer ce qu’entendent les utilisateurs.

## Voir aussi
→ https://superscheduler.org/fr/examples/clinic-appointments/
- [Arborescences de ressources, colonnes et sélection](https://superscheduler.org/fr/docs/trees-columns-selection/) précise ce que sélectionnent Espace, Entrée et Ctrl/Cmd+A.
- [Glisser, redimensionner et règles métier](https://superscheduler.org/fr/docs/drag-resize-rules/) explique les règles que suivent les déplacements au clavier.
- [Thèmes, tokens, Tailwind et mode sombre](https://superscheduler.org/fr/docs/theming/) traite des couleurs de focus et de sélection.
