# Teclado, accesibilidad y táctil

> Navegación completa por teclado, modelo de foco y anuncios, alternativas al arrastre y una línea de tiempo adaptada a pantallas táctiles y tecnologías de apoyo.

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

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.

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`) |
|---|---|---|
| Teclado | Siempre activado: las flechas mueven la celda activa, Intro o Espacio la activan | Desactivado por defecto; `keyboardEnabled`, más `keyboardMode: 'Full'` para el modelo completo |
| Eventos | Botones nativos: Tab llega a ellos, Intro o Espacio los pulsan | Se llega a ellos con las flechas dentro de la cuadrícula; Intro ejecuta el flujo de clic |
| Edición con teclado | No (edición de solo lectura) | Mover con Alt+flechas, redimensionar con Alt+Mayús+flecha izquierda/derecha (modo Full) |
| Anuncios | No | Foco, selección y cambios confirmados, en nueve idiomas |
| Nombre de la cuadrícula | Opción `ariaLabel` (por defecto, «Resource schedule») | «Scheduler» integrado («Planificador» para locales en español) |
| Táctil | Scroll y toques nativos | Mantener 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.

```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>
  )
}
```
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ón | Por defecto | Efecto |
|---|---|---|
| `pageRows` | filas visibles menos una | Filas que avanzan RePág y AvPág |
| `contextMenuKey` | `true` en modo Full | La tecla de menú y Mayús+F10 abren el menú del elemento con foco |
| `bubbleOnFocus` | `false` | Muestra la burbuja del evento mientras tiene el foco del teclado |
| `selectAll` | `true` en modo Full | Ctrl/Cmd+A selecciona todos los eventos visibles (necesita `allowMultiSelect`) |
| `zoomKeys` | `false` | Ctrl/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
| Teclas | Modo | Qué ocurre |
|---|---|---|
| Flechas | ambos | Mueven 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. |
| Intro | ambos | Sobre 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 derecha | ambos | Amplía un rango de tiempo desde la celda con foco; al soltar Mayús, se selecciona. |
| Espacio | Full | Sobre una celda: la añade a la selección o la quita. Sobre un evento: igual que Intro. |
| Inicio / Fin | Full | Primera o última celda de la fila; con Ctrl/Cmd, primera o última fila. |
| RePág / AvPág | Full | Mueve el foco `pageRows` filas. |
| Alt+flechas | Full | Empieza a mover el evento con foco; las flechas lo mueven, Intro o Espacio lo sueltan. |
| Alt+Mayús+flecha izquierda / derecha | Full | Empieza 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. |
| Esc | ambos | Cancela 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+F10 | Full | Abre el menú del evento, la cabecera de fila o la celda con foco. |
| Ctrl/Cmd+A | Full | Selecciona todos los eventos visibles. |

> **Behavior:**
> Los movimientos y redimensionados con teclado pasan por las mismas reglas que los del puntero: `onEventMoving` y `onEventResizing` pueden rechazar una posición, `onEventMove` puede cancelar o confirmar de forma asíncrona, y se aplican `allowEventOverlap`, los eventos bloqueados y las celdas deshabilitadas. Las teclas que se escriben en inputs, áreas de texto y elementos editables mantienen su comportamiento normal, y la cuadrícula no gestiona una tecla que tu handler `onKeyDown` cancele con `args.preventDefault()`.

## 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`:

```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]}`
}
```
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.

> **Limitation:**
> En la 0.1.0, los textos de los anuncios vienen integrados: `keyboardOptions` no permite sustituirlos. Las fechas de los anuncios siguen el locale del scheduler.

### La tecla de menú contextual
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`:

```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 })
    },
  }
}
```
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](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) 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](https://superscheduler.org/es/docs/react-render-slots/) 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.

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

## Relacionado
→ https://superscheduler.org/es/examples/clinic-appointments/
- [Árboles de recursos, columnas y selección](https://superscheduler.org/es/docs/trees-columns-selection/) explica qué seleccionan Espacio, Intro y Ctrl/Cmd+A.
- [Arrastrar, redimensionar y reglas de negocio](https://superscheduler.org/es/docs/drag-resize-rules/) explica las reglas por las que pasan los movimientos con teclado.
- [Temas, tokens, Tailwind y modo oscuro](https://superscheduler.org/es/docs/theming/) trata los colores de foco y de selección.
