# Slots de rendu React et cartes de survol

> Rendez événements, lignes, en-têtes, cellules et zones avec React via super-scheduler/react-render, ajoutez des cartes de survol et gardez le défilement fluide.

Source: https://superscheduler.org/fr/docs/react-render-slots/
Reviewed: 2026-10-07

Importez SuperSchedulerComponent depuis super-scheduler/react-render au lieu de la racine du paquet, puis passez renderEvent, renderRowHeader, renderTimeHeader, renderCorner, renderCell ou renderArea ; chacun renvoie le contenu React d’un type de slot. Le contenu de repli en HTML ou en texte s’affiche d’abord, puis React le remplace par petits lots pendant les temps morts : le défilement n’attend donc jamais React. Ajoutez eventHover pour des cartes de survol que les utilisateurs peuvent épingler.

SuperScheduler dessine sa grille avec son propre code DOM, et c’est ce qui garde le défilement fluide avec des milliers de lignes et d’événements. Quand le contenu d’un événement ou d’un en-tête doit venir de vos composants React (votre design system, des icônes, des avatars, des valeurs formatées), le point d’entrée `super-scheduler/react-render` monte du contenu React dans les slots du planificateur sans confier à React le contrôle de la grille.

Les slots de rendu React et les cartes de survol nécessitent SuperScheduler Pro.

## Passer au composant React-render
`super-scheduler/react-render` exporte son propre `SuperSchedulerComponent`. Il accepte toutes les props du composant principal, expose les mêmes `ref.current.control` et `controlRef`, et ajoute les props `render*`, `eventHover`, `renderOptions` et les handlers `onBefore*DomAdd` / `onBefore*DomRemove`.

Le composant de la racine du paquet accepte aussi ces props, mais se contente d’un avertissement unique (`needs the component from "super-scheduler/react-render"`) et n’en affiche rien. Garder la machinerie React dans son propre point d’entrée évite aux pages qui ne s’en servent pas de la charger.

## Les slots
| Prop | Arguments | Remplace |
|---|---|---|
| `renderEvent` | `control`, `e`, `data`, `row`, `width`, `lod` | Le contenu de la boîte d’un événement |
| `renderRowHeader` | `control`, `row`, `column` | Le contenu d’une cellule d’en-tête de ligne (`column` est l’index de colonne, 0 sans colonnes) |
| `renderTimeHeader` | `control`, `header` (`start`, `end`, `level`) | Le contenu d’une cellule d’en-tête de temps |
| `renderCorner` | `control` | Le coin supérieur gauche |
| `renderCell` | `control`, `cell` | Le contenu d’une cellule de la grille |
| `renderArea` | `control`, `area`, `source` | Une zone déclarée avec `render: true` |

Le moteur garde les parties qui lui appartiennent : la boîte de l’événement et sa position, la barre de durée, les poignées de redimensionnement, les zones ordinaires, le bouton de dépliage de l’arborescence et les lignes de grille. Un slot, c’est le contenu à l’intérieur.

## Rendre le contenu des événements
```tsx
// src/CampaignBoard.tsx
import { memo, useMemo } from 'react'
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'
import 'super-scheduler/styles.css'

type Campaign = { client: string; channel: 'social' | 'print' | 'video'; progress: number }

const CHANNEL_LABEL: Record<Campaign['channel'], string> = {
  social: 'Social',
  print: 'Print',
  video: 'Video',
}

const CampaignContent = memo(function CampaignContent(props: {
  title: string
  campaign: Campaign
  compact: boolean
}) {
  const { title, campaign, compact } = props
  if (compact) return <strong className="campaign__title">{title}</strong>
  return (
    <span className="campaign">
      <strong className="campaign__title">{title}</strong>
      <span className="campaign__meta">
        {campaign.client} · {CHANNEL_LABEL[campaign.channel]} ·{' '}
        {Math.round(campaign.progress * 100)}%
      </span>
    </span>
  )
})

// Module-level functions keep their identity: a new function re-renders every slot.
const renderEvent: NonNullable<SchedulerProps['renderEvent']> = ({ e, data, width, lod }) => {
  // `data` is the event after onBeforeEventRender; custom fields need a cast.
  const campaign = data as SuperScheduler.EventRenderData<Campaign>
  // `width` comes in 8 px steps and changes only when a gesture ends.
  return (
    <CampaignContent title={e.text()} campaign={campaign} compact={width < 160 || lod !== 'full'} />
  )
}

// The HTML fallback paints first and stays if the React content fails.
const onBeforeEventRender: NonNullable<SchedulerProps['onBeforeEventRender']> = (args) => {
  args.data.html = SuperScheduler.Util.escapeHtml(args.data.text)
}

export function CampaignBoard(props: {
  resources: SuperScheduler.ResourceData[]
  campaigns: SuperScheduler.EventData<Campaign>[]
}) {
  const events = useMemo(() => props.campaigns.slice(), [props.campaigns])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={61}
      scale="Day"
      cellWidth={36}
      eventHeight={44}
      resources={props.resources}
      events={events}
      onBeforeEventRender={onBeforeEventRender}
      renderEvent={renderEvent}
    />
  )
}
```
Chaque campagne devrait afficher son client, son canal et son avancement, et seulement le titre quand l’événement fait moins de 160 px de large ou que la vue est dézoomée.

Les arguments en détail :

- `e` est l’objet enveloppe de l’événement : `e.id()`, `e.text()`, `e.start()`, `e.end()`, et `e.data` pour l’objet stocké.
- `data` est l’événement tel que l’a laissé `onBeforeEventRender`, avec `start` et `end` sous forme de valeurs `SuperScheduler.Date`. Les champs personnalisés nécessitent un cast, comme dans le snippet.
- `width` est la largeur rendue par paliers de 8 px, mise à jour à la fin d’un geste plutôt qu’à chaque frame.
- `lod` est le niveau de détail (`'full'`, `'compact'` ou `'overview'`) au moment du rendu du contenu.

> **Behavior:**
> Le contenu React est purement visuel. Le nom accessible de l’événement vient toujours de `ariaLabel`, `text` ou `html` (voir [clavier et accessibilité](https://superscheduler.org/fr/docs/keyboard-accessibility-touch/#focus-model)) : gardez donc un `text` explicite. Un appui du pointeur à l’intérieur d’un événement déclenche la gestion du clic et du glisser propre à l’événement : n’y placez ni boutons ni liens, et mettez les actions dans une carte de survol, un menu contextuel ou un panneau de détail.

## En-têtes, coin, cellules et zones
```tsx
// src/TeamBoard.tsx
import { useMemo } from 'react'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'

const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [{ groupBy: 'Month' }, { groupBy: 'Day' }]

// Scheduler dates are civil values: format their native Date in UTC to keep the wall clock.
const WEEKDAY = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' })

// Every slot of a kind gets the function's result: return content for each case
// (a null result leaves that slot empty rather than showing the fallback).
const SLOTS: SchedulerProps = {
  renderRowHeader: ({ row }) => {
    const role = typeof row.data.role === 'string' ? row.data.role : ''
    return (
      <span className="person">
        <span className="person__initials" aria-hidden="true">
          {row.name.slice(0, 1)}
        </span>
        <span className="person__name">{row.name}</span>
        {role !== '' && <span className="person__role">{role}</span>}
      </span>
    )
  },
  renderTimeHeader: ({ header }) =>
    header.level === 0 ? (
      <span>{header.start.toString('MMMM yyyy')}</span>
    ) : (
      <span className="day">
        <small>{WEEKDAY.format(header.start.toDate())}</small> {header.start.toString('d')}
      </span>
    ),
  renderCorner: () => <span className="corner">Team</span>,
  // Only areas declared with `render: true` reach renderArea.
  renderArea: ({ area }) =>
    area.id === 'approval' ? <span className="badge">Needs approval</span> : null,
  onBeforeEventRender: (args) => {
    if (args.data.status === 'draft') {
      args.data.areas = [
        { id: 'approval', render: true, right: 4, top: 4, width: 96, height: 16, action: 'None' },
      ]
    }
  },
}

export function TeamBoard(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const events = useMemo(() => props.events.slice(), [props.events])
  return (
    <SuperSchedulerComponent
      {...SLOTS}
      startDate="2026-10-01"
      days={31}
      scale="Day"
      timeHeaders={TIME_HEADERS}
      rowHeaderWidth={200}
      resources={props.resources}
      events={events}
      // Keeps React work bounded on large boards (defaults shown).
      renderOptions={{ sliceMs: 8 }}
    />
  )
}
```
Une fonction de rendu possède tous les slots de son type. Renvoyez du contenu pour chaque cas : un résultat `null` laisse ce slot vide au lieu d’afficher le contenu de repli. En pratique, `renderArea` fait exception, car seules les zones déclarées avec `render: true` l’atteignent.

Remarques par slot :

- **En-têtes de ligne.** Le bouton de dépliage de l’arborescence reste en place. Avec `rowHeaderColumns`, la fonction s’exécute une fois par colonne et reçoit son index dans `column`.
- **En-têtes de temps.** `header.level` est l’index dans `timeHeaders` (0 correspond à la ligne du haut). Les dates du planificateur sont des valeurs civiles : pour les formater avec `Intl`, passez `date.toDate()` et `timeZone: 'UTC'`, comme le fait le snippet.
- **Cellules.** `renderCell` monte une racine React par cellule montée, et aucune tant que les cellules font moins de 24 px de large. Une vue de 40 lignes sur 30 jours en monte déjà 1 200 : pour la disponibilité, les prix ou le grisé, définissez plutôt `html`, `cssClass` ou `backColor` dans `onBeforeCellRender`.
- **Zones.** Déclarez la zone sur l’événement (ou la ligne, la cellule, l’en-tête) avec `render: true` et sa position ; `source` vous indique à quel élément la zone appartient.

## Contenus de repli, lots et cycle de vie
Le contenu React ne bloque jamais l’affichage :

1. Le planificateur dessine d’abord le contenu de repli en HTML ou en texte : le `html` ou le `text` que produisent vos données et vos hooks `onBefore*Render`.
2. Quand le navigateur est inactif, le contenu React est appliqué par lots visant `renderOptions.sliceMs` (8 ms par défaut). Chaque slot masque son contenu de repli dès que son contenu est prêt.
3. Pendant le défilement, le zoom et le glisser, le contenu existant se déplace avec la grille. Les nouveaux slots et les changements de fonctions de rendu attendent la fin du geste.
4. Si une fonction de rendu lève une erreur, ce slot garde son contenu de repli et l’erreur est signalée une fois par slot via `reportError` du navigateur (un événement global `error` que votre outil de suivi des erreurs peut intercepter).

Le contenu qui sort de la zone visible est conservé détaché pour pouvoir revenir sans nouveau rendu : jusqu’à `renderOptions.retain` éléments, par défaut deux fois le nombre d’éléments montés, avec un maximum de 2 000. L’état local d’un élément conservé survit ; un élément évincé repart de zéro. `retain: 0` désactive la conservation.

Le contenu des slots est rendu via des portails : il voit donc vos providers (thème, traductions, routeur, clients de données). Le CSS peut cibler `[data-super-scheduler-slot]`, `[data-super-scheduler-slot-ready]` et `[data-super-scheduler-fallback]`.

Côté serveur, le composant affiche une `<div>` vide ; les slots apparaissent une fois le planificateur monté côté client. Voir [rendu serveur et prérendu](https://superscheduler.org/fr/docs/ssr-prerender/).

> **Tip:**
> Le code porté depuis des planificateurs à base de callbacks peut utiliser `onBeforeEventDomAdd` et ses équivalents (cellule, en-tête de ligne, en-tête de temps, coin) : affectez à `args.element` un nœud DOM ou un élément React, et le handler `DomRemove` correspondant reçoit le même élément. Quand les deux existent, la prop `render*` l’emporte et émet un avertissement.

## Cartes de survol
`eventHover` affiche une carte React à côté d’un événement une fois que le pointeur s’y est posé. Sans `eventHover`, aucune carte n’apparaît.

| Option | Défaut | Effet |
|---|---|---|
| `render(args)` | obligatoire | Contenu de la carte ; `args` contient `control`, `e`, `row`, `anchor` (la boîte de l’événement), `pinned` et `close()` |
| `delay` | `350` | Millisecondes pendant lesquelles le pointeur reste posé avant l’ouverture de la carte |
| `leaveGrace` | `180` | Millisecondes avant la fermeture une fois que le pointeur a quitté l’événement ou la carte |
| `placement` | `'auto'` | `'auto'`, `'above'`, `'below'`, `'start'` ou `'end'` |
| `pin` | `false` | `'click'` ou `'dblclick'` épingle la carte pour que les utilisateurs puissent interagir avec |
| `glide` | `true` | Passer à un autre événement déplace la carte ouverte au lieu de la rouvrir |

```tsx
// src/BookingsWithCards.tsx
import { useMemo } from 'react'
import type { SchedulerEventHoverOptions, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'

// Module-level: the options object keeps its identity across renders.
const BOOKING_CARD: SchedulerEventHoverOptions = {
  delay: 350,
  leaveGrace: 180,
  placement: 'auto',
  // A click pins the card as a non-modal dialog; on touch screens a tap does it.
  pin: 'click',
  render: ({ e, row, pinned, close }) => (
    <article className="booking-card">
      <h3>{e.text()}</h3>
      <p>{row.name}</p>
      <p>
        {e.start().toString('d MMM, HH:mm')} to {e.end().toString('d MMM, HH:mm')}
      </p>
      {pinned && (
        <button type="button" onClick={close}>
          Close
        </button>
      )}
    </article>
  ),
}

export function BookingsWithCards(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const events = useMemo(() => props.events.slice(), [props.events])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={14}
      scale="Day"
      resources={props.resources}
      events={events}
      eventHover={BOOKING_CARD}
    />
  )
}
```
Comportement de la carte :

- Une carte non épinglée a `role="tooltip"` ; une carte épinglée est un `role="dialog"` non modal qui prend le focus. Échap ou un clic à l’extérieur ferme une carte épinglée et rend le focus à l’événement.
- Amener le pointeur dans la carte la garde ouverte. Le défilement, le zoom, le glisser et la sélection la masquent immédiatement.
- La carte est positionnée à l’ouverture, se retourne ou se réduit pour tenir dans la fenêtre, et respecte la préférence d’animations réduites.
- Elle vit dans `document.body` et porte le thème du planificateur. Stylez-la avec `--super-scheduler-hover-padding`, `-hover-border`, `-hover-radius`, `-hover-bg`, `-hover-color`, `-hover-shadow` et `--super-scheduler-z-hover`.
- Les écrans tactiles n’ont pas de survol : avec `pin: 'click'`, un appui ouvre une carte épinglée.

Les cartes de survol sont indépendantes des bulles HTML (`bubble`, `bubbleHtml`). Les bulles ne peuvent pas accueillir de contenu React ; utilisez `eventHover` pour cela.

## Performances
- **Fonctions stables.** Définissez les fonctions de rendu et les objets d’options au niveau du module, ou mémoïsez-les. Une nouvelle identité de fonction provoque un nouveau rendu de tous les slots de ce type.
- **Rendus légers.** `sliceMs` est une cible pour les lots, pas une limite imposée à votre code : une fonction de rendu lente retarde son lot. Ne lisez pas la mise en page et ne mesurez pas le DOM dans les fonctions de rendu ; utilisez `width` et `lod`.
- **Composants mémoïsés.** Enveloppez les composants de slot dans `memo` et passez des props primitives, comme dans le snippet des événements.
- **Contexte.** Une valeur de contexte qui change souvent provoque un nouveau rendu de chaque slot qui la lit. Gardez l’état qui change vite (position du pointeur, minuteurs) hors des contextes consommés par les slots.
- **Cellules.** Sur les grandes grilles, préférez les chaînes de `onBeforeCellRender` à `renderCell`.
- **Pas de state par frame.** Ne mettez pas à jour le state React depuis `onScroll` ou les handlers de glisser ; la bibliothèque effectue son travail par frame sans rendu React.

Mesurez votre propre contenu avec le React Profiler : la bibliothèque ne peut pas rendre léger un composant coûteux.

## Voir aussi
→ https://superscheduler.org/fr/examples/agency-campaigns/
→ https://superscheduler.org/fr/examples/lab-instruments/
- [Thèmes, tokens, Tailwind et mode sombre](https://superscheduler.org/fr/docs/theming/) pour styler le contenu des slots avec les tokens du planificateur.
- [Performances et virtualisation](https://superscheduler.org/fr/docs/performance-virtualization/) pour le modèle de rendu sur lequel reposent les slots.
