Aller au contenu
SuperScheduler

PersonnalisationS’applique àSuperScheduler Pro

Slots de rendu React et cartes de survol

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.

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

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

PropArgumentsRemplace
renderEventcontrol, e, data, row, width, lodLe contenu de la boîte d’un événement
renderRowHeadercontrol, row, columnLe contenu d’une cellule d’en-tête de ligne (column est l’index de colonne, 0 sans colonnes)
renderTimeHeadercontrol, header (start, end, level)Le contenu d’une cellule d’en-tête de temps
renderCornercontrolLe coin supérieur gauche
renderCellcontrol, cellLe contenu d’une cellule de la grille
renderAreacontrol, area, sourceUne 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

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

En-têtes, coin, cellules et zones

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

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.

OptionDéfautEffet
render(args)obligatoireContenu de la carte ; args contient control, e, row, anchor (la boîte de l’événement), pinned et close()
delay350Millisecondes pendant lesquelles le pointeur reste posé avant l’ouverture de la carte
leaveGrace180Millisecondes avant la fermeture une fois que le pointeur a quitté l’événement ou la carte
placement'auto''auto', 'above', 'below', 'start' ou 'end'
pinfalse'click' ou 'dblclick' épingle la carte pour que les utilisateurs puissent interagir avec
glidetruePasser à un autre événement déplace la carte ouverte au lieu de la rouvrir
src/BookingsWithCards.tsxtsx
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.

Planification des ressources d’une agence créativeUne graphiste est surréservée. Confiez deux tâches à un collègue d’un seul glisser, vérifiez ce qu’elles débloquent, puis annulez. Réservation d’instruments de laboratoireRéservez un instrument, l’étalonnage est compris. Sortez une séance d’une visite de maintenance, puis allongez votre mesure.