Aller au contenu
SuperScheduler

InteractionS’applique àLite et Pro

Clavier, accessibilité et tactile

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.

Vérifié avec la v0.1.0 · relu le 7 octobre 2026.md

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)
ClavierToujours actif : les flèches déplacent la cellule active, Entrée ou Espace l’activeDésactivé par défaut ; keyboardEnabled, plus keyboardMode: 'Full' pour le modèle complet
ÉvénementsBoutons natifs : Tab les atteint, Entrée ou Espace les cliqueAtteints avec les flèches à l’intérieur de la grille ; Entrée lance le flux de clic
Édition au clavierNon (édition en lecture seule)Déplacement avec Alt+flèches, redimensionnement avec Alt+Maj+Flèche gauche/droite (mode Full)
AnnoncesNonFocus, sélection et modifications validées, en neuf langues
Nom de la grilleOption ariaLabel (« Resource schedule » par défaut)« Scheduler » intégré (« Planificador » pour les locales espagnoles)
TactileDéfilement et appuis natifsAppui 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.

src/AccessiblePlanner.tsxtsx
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 :

OptionDéfautEffet
pageRowslignes visibles moins uneNombre de lignes parcourues par Page précédente et Page suivante
contextMenuKeytrue en mode FullLa touche Menu et Maj+F10 ouvrent le menu de l’élément qui a le focus
bubbleOnFocusfalseAffiche la bulle de l’événement tant qu’il a le focus clavier
selectAlltrue en mode FullCtrl/Cmd+A sélectionne tous les événements visibles (nécessite allowMultiSelect)
zoomKeysfalseCtrl/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

TouchesModeEffet
Flèchesles deuxDé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éeles deuxSur 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 droiteles deuxÉtend une plage de temps à partir de la cellule qui a le focus ; relâcher Maj la sélectionne.
EspaceFullSur une cellule : l’ajoute à la sélection ou l’en retire. Sur un événement : comme Entrée.
Début / FinFullPremière ou dernière cellule de la ligne ; avec Ctrl/Cmd, première ou dernière ligne.
Page précédente / Page suivanteFullDéplace le focus de pageRows lignes.
Alt+flèchesFullCommence à 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 / droiteFullCommence à 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.
Échaples deuxAnnule 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+F10FullOuvre le menu de l’événement, de l’en-tête de ligne ou de la cellule qui a le focus.
Ctrl/Cmd+AFullSélectionne tous les événements visibles.

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 :

src/labelVisit.tstsx
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.

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 :

src/menuHandlers.tstsx
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 (↗) 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 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.

src/OccupancyBoard.tsxtsx
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.

Rendez-vous d’un cabinet de kinésithérapieUn patient ne peut pas venir à 10 h. Trouvez le prochain créneau qui respecte pauses et nettoyages.