Saltar al contenido
SuperScheduler

InteracciónSe aplica aLite y Pro

Teclado, accesibilidad y táctil

En Pro, activa keyboardEnabled (desactivado por defecto), añade keyboardMode: 'Full' para el conjunto completo de teclas y keyboardTarget: 'component' para que las teclas solo actúen mientras la cuadrícula tiene el foco. La cuadrícula es una única parada de tabulación con role="grid": el foco se mueve mediante aria-activedescendant y los cambios se anuncian en nueve idiomas. Lite trae de serie la navegación con las flechas. En pantallas táctiles, mantén pulsado un evento para moverlo y arrastra sus tiradores para redimensionarlo.

Verificado con v0.1.0 · revisado el 7 de octubre de 2026.md

Un planificador de recursos es una cuadrícula bidimensional grande, y por eso dar soporte al teclado y a los lectores de pantalla es más difícil que en una lista o un formulario. SuperScheduler da a la cuadrícula una única parada de tabulación, un foco itinerante que sobrevive a la virtualización, anuncios hablados para el foco y los cambios, y equivalentes de teclado para mover y redimensionar eventos. Esta guía explica qué hace cada edición, cómo activarlo y qué tiene que aportar todavía tu aplicación.

Lite y Pro de un vistazo

Lite (super-scheduler-lite)Pro (super-scheduler)
TecladoSiempre activado: las flechas mueven la celda activa, Intro o Espacio la activanDesactivado por defecto; keyboardEnabled, más keyboardMode: 'Full' para el modelo completo
EventosBotones nativos: Tab llega a ellos, Intro o Espacio los pulsanSe llega a ellos con las flechas dentro de la cuadrícula; Intro ejecuta el flujo de clic
Edición con tecladoNo (edición de solo lectura)Mover con Alt+flechas, redimensionar con Alt+Mayús+flecha izquierda/derecha (modo Full)
AnunciosNoFoco, selección y cambios confirmados, en nueve idiomas
Nombre de la cuadrículaOpción ariaLabel (por defecto, «Resource schedule»)«Scheduler» integrado («Planificador» para locales en español)
TáctilScroll y toques nativosMantener pulsado para mover, tiradores para redimensionar, pellizcar para hacer zoom

Activar el teclado en Pro

Pro mantiene el teclado desactivado hasta que pones keyboardEnabled: true. El valor por defecto keyboardMode: 'SuperScheduler' gestiona las flechas, Intro y Mayús+flecha izquierda/derecha. keyboardMode: 'Full' añade el resto del modelo: Inicio/Fin, RePág/AvPág, Espacio, mover y redimensionar eventos, la tecla de menú contextual y el anuncio de textos de ayuda. El modo Full sin keyboardEnabled no hace nada y avisa en desarrollo.

keyboardTarget decide dónde se escuchan las teclas. El valor por defecto, 'document', reacciona a las teclas pulsadas en cualquier parte de la página fuera de los campos de texto, lo que quita las flechas al scroll de la página. Usa 'component' para que las teclas solo actúen mientras la cuadrícula tiene el foco; también es la opción adecuada cuando hay varios schedulers en una misma página.

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

Deberías poder entrar en la cuadrícula con Tab, moverte con las flechas, pulsar Intro sobre un evento para abrirlo y pulsar Alt+flecha abajo sobre un evento para empezar a moverlo. Si lo llevas a la habitación 104 y pulsas Intro, se anuncia «Not allowed here» y el evento se queda donde estaba.

keyboardOptions ajusta el modo Full:

OpciónPor defectoEfecto
pageRowsfilas visibles menos unaFilas que avanzan RePág y AvPág
contextMenuKeytrue en modo FullLa tecla de menú y Mayús+F10 abren el menú del elemento con foco
bubbleOnFocusfalseMuestra la burbuja del evento mientras tiene el foco del teclado
selectAlltrue en modo FullCtrl/Cmd+A selecciona todos los eventos visibles (necesita allowMultiSelect)
zoomKeysfalseCtrl/Cmd con = o +, - y 0 acercan, alejan y restablecen el zoom

zoomKeys está desactivado por defecto para que los atajos de zoom de página del propio navegador sigan funcionando.

Teclas

TeclasModoQué ocurre
FlechasambosMueven el foco. Izquierda y derecha se detienen en cada evento y en cada celda vacía de la fila; arriba y abajo cambian de fila.
IntroambosSobre un evento: el flujo de clic (onEventClick y después eventClickHandling). Sobre una celda: la selecciona como rango de tiempo.
Mayús+flecha izquierda / Mayús+flecha derechaambosAmplía un rango de tiempo desde la celda con foco; al soltar Mayús, se selecciona.
EspacioFullSobre una celda: la añade a la selección o la quita. Sobre un evento: igual que Intro.
Inicio / FinFullPrimera o última celda de la fila; con Ctrl/Cmd, primera o última fila.
RePág / AvPágFullMueve el foco pageRows filas.
Alt+flechasFullEmpieza a mover el evento con foco; las flechas lo mueven, Intro o Espacio lo sueltan.
Alt+Mayús+flecha izquierda / derechaFullEmpieza a redimensionar el final del evento con foco; izquierda y derecha lo cambian, Intro confirma. Sobre el título de una columna, mueve la columna.
EscambosCancela un movimiento, un redimensionado o un rango hecho con el teclado. Durante un movimiento o un redimensionado con el teclado, Tab también lo cancela y sale de la cuadrícula.
Tecla de menú, Mayús+F10FullAbre el menú del evento, la cabecera de fila o la celda con foco.
Ctrl/Cmd+AFullSelecciona todos los eventos visibles.

Modelo de foco y lectores de pantalla

La cuadrícula de Pro tiene role="grid" con aria-rowcount y aria-colcount; las cabeceras de fila son rowheader, las celdas de la cabecera de tiempo columnheader, y las celdas y los eventos gridcell. Las filas y celdas fuera del área visible no están en el DOM, así que el foco no pasa de un elemento a otro. En su lugar:

  • la raíz de la cuadrícula es la única parada de tabulación (tabindex="0") mientras el teclado está activado;
  • la celda o el evento con foco es un nodo de foco al que la raíz apunta con aria-activedescendant, y sobrevive al scroll y a la virtualización;
  • la etiqueta del nodo de foco se lee como «Room 101, Oct 1» para una celda y «Ana, Room 101, Oct 2 – 4» para un evento.

El nombre de un evento sale de su ariaLabel; si no lo hay, de su text; después, de su html como texto plano y, por último, de su id. El nombre de una fila es el name de su recurso. Cuando html muestra algo distinto de text, fija ariaLabel en 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]}`
}

Como las etiquetas de foco y los anuncios añaden la fila y las fechas, ariaLabel solo debe contener lo que identifica al evento. SuperScheduler.ColorUtil.contrasting(color) devuelve texto oscuro para rellenos claros y texto claro para los oscuros.

El nombre accesible de la propia cuadrícula es «Scheduler» («Planificador» cuando el locale empieza por es), y la 0.1.0 no tiene ninguna opción para cambiarlo. Coloca el scheduler dentro de una región etiquetada con un encabezado visible, como hace el snippet de configuración.

Desde código, control.keyboard ofrece focusEvent(e or id), focusCell(date, resource), getFocus(), move(direction), clearFocus() y resetFocus(). onKeyboardFocusChange (cancelable) y onKeyboardFocusChanged comunican los cambios de foco con previous y focus ({ e } o { cell }); úsalos para sincronizar un panel de detalle con el teclado.

Anuncios

Una región live discreta (polite) dentro de la cuadrícula anuncia:

  • en modo Full, un breve texto de ayuda la primera vez que la cuadrícula recibe el foco;
  • las selecciones («Selected: Room 101, Oct 4»), las deselecciones y «N events selected»;
  • el inicio de un movimiento o un redimensionado con teclado, con instrucciones;
  • los movimientos y redimensionados confirmados («Event moved to Room 101, Oct 3 – 5»), tanto si vienen del teclado como del puntero;
  • «Cancelled», «Not allowed here» y los movimientos de columnas.

Los textos siguen el primer segmento del locale del scheduler: inglés, español, catalán, euskera, gallego, alemán, francés, italiano y portugués. Los demás idiomas usan el inglés. Los paquetes de idioma distintos del inglés y el español se cargan bajo demanda.

En modo Full, la tecla de menú y Mayús+F10 abren el menú del elemento con foco cuando es un SuperScheduler.Menu: el contextMenu del evento (o el contextMenu del control), contextMenuResource en una cabecera de fila y contextMenuSelection en una celda. Si tu aplicación dibuja su propio menú desde onEventRightClick, gestiona tú la tecla en 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 })
    },
  }
}

Incorpora menuHandlers(open) a las props del scheduler con un spread, y memorízalo. A partir de ahí, tu menú es responsable de su propio foco: mueve el foco a su interior cuando se abra y devuélvelo a la cuadrícula cuando se cierre.

Alternativas accesibles al arrastre

El criterio de conformidad 2.5.7 de WCAG 2.2 (↗) pide una forma de hacer con acciones de un solo puntero lo mismo que se hace arrastrando. El modo de teclado Full cubre a quienes usan el teclado, pero no a quien usa un solo puntero, un conmutador o el control por voz. Da a cada evento un camino que no requiera arrastrar, por ejemplo un panel de detalle o una entrada de menú contextual con campos de inicio, fin y recurso que actualice tu estado (o llame a control.events.update con un objeto nuevo). Antes de guardar, ejecuta la misma validación que usas en onEventMoving, para que ambos caminos apliquen las mismas reglas.

Táctil

En pantallas táctiles, con un dedo se hace scroll por la línea de tiempo. El resto del modelo táctil:

  • Mover un evento. Mantén el evento pulsado y quieto durante tapAndHoldTimeout (300 ms) y después arrastra. Si el dedo se mueve más de unos 8 px antes de ese tiempo, se hace scroll. eventTapAndHoldHandling decide qué hace una pulsación larga: 'Move' (por defecto), 'ContextMenu' (abre el SuperScheduler.Menu del evento) o 'Disabled'. Mantener pulsada una cabecera de fila abre contextMenuResource.
  • Redimensionar. Un toque en un evento muestra sus tiradores, con áreas táctiles de 44 px (--super-scheduler-handle-target); arrastra un tirador para redimensionar. Si apoyas el dedo en el borde de un evento, lo mueves en lugar de redimensionarlo.
  • Seleccionar tiempo. Un toque en una celda vacía la selecciona (origin: 'click'); mantener pulsado y arrastrar selecciona un rango.
  • Zoom. Pellizca con dos dedos para hacer zoom (zoomGesture.pinch, activado por defecto). Un segundo dedo cancela cualquier arrastre en curso.
  • Hover. En táctil no existe el hover. Las tarjetas emergentes de eventHover se pueden fijar con un toque (pin: 'click'); las áreas con visibility: 'TouchVisible' siguen visibles en dispositivos táctiles, mientras que las áreas 'Hover' no aparecen.

Lite es de solo lectura: hace scroll de forma nativa y comunica los toques mediante onEventClick y onTimeRangeClick.

Movimiento reducido, contraste y colores forzados

Pro lee las preferencias del usuario mediante CSS y media queries, sin ninguna opción que configurar:

  • prefers-reduced-motion: reduce pone --super-scheduler-duration a 0s, hace instantáneo control.zoom.animateTo() y elimina las transiciones de las tarjetas emergentes;
  • prefers-contrast: more refuerza los bordes, las líneas de la cuadrícula, las líneas de fila y el contorno de selección;
  • forced-colors: active cambia el tema a los colores del sistema (Highlight, CanvasText, GrayText) y quita los sombreados decorativos, como los de los fines de semana y el día de hoy.

Lite también adapta sus bordes y su contorno de foco a los colores forzados. Si sustituyes colores con tus propios tokens o tu CSS, vuelve a probar estos modos: tus sobrescrituras pueden anularlos.

Soporte de teclado en Lite

Lite no necesita configuración. La cuadrícula puede recibir el foco, es aria-readonly y su nombre viene de la opción ariaLabel. Las flechas mueven la celda activa (anunciada mediante aria-activedescendant), Intro o Espacio llaman a onTimeRangeClick para ella y cada evento es un botón nativo.

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'))
      }
    />
  )
}

Lo que tu aplicación todavía debe garantizar

La librería se ocupa de la cuadrícula. Estas partes corresponden a tu aplicación:

  • Contraste. Los colores de evento que fijas con backColor, fontColor, CSS o contenido personalizado deben cumplir los requisitos de contraste en modo claro y oscuro.
  • Nombres en el contenido personalizado. El HTML de onBeforeEventRender, los slots de React y el marcado de las cabeceras de fila son tuyos: mantén un text con sentido o fija ariaLabel, da alternativas de texto a los iconos y evita controles interactivos dentro del contenido de los eventos.
  • Menús, diálogos y paneles. La gestión del foco, las etiquetas y el manejo de Esc de todo lo que abras desde la cuadrícula.
  • Un camino sin arrastre para cada acción de arrastre, como se ha descrito arriba.
  • Estructura de la página. Un encabezado o una etiqueta para la región que rodea la cuadrícula, y un lugar razonable para ella en el orden de tabulación.
  • Pruebas. Comprueba tu configuración con un lector de pantalla y un verificador automático; las sobrescrituras y el contenido personalizado pueden cambiar lo que oyen los usuarios.

Citas de clínica de fisioterapiaUn paciente no puede venir a las 10:00. Encuentra el siguiente hueco que respete pausas y limpiezas.