calendar-tsx

External drops and transfers

Create events from a palette and transfer events between calendars.

External drops create events from outside the calendar. Transfers move an existing event between two calendars. Both use the hit testing, previews and constraints of event editing.

External event sources

On the receiving calendar, register interactionFeature(), pointerFeature() and externalDropFeature(); movement and selection are optional. onExternalDrop receives { kind: 'create', after, metadata }, so onExternalDrop: ({ after }) => addEvent(after) inserts the accepted event into your state.

Attach a palette item after its element mounts:

export function attachPalette(
  element: HTMLElement,
  makeEvent: () => Calendar_EventInput<Booking>,
  dragScope: Calendar_DragScope,
) {
  return createExternalDraggable(
    element,
    () => ({
      event: makeEvent(),
      metadata: { source: 'booking-palette' },
    }),
    { dragScope, pointer: {} },
  );
}

Create a scope with createCalendarDragScope() and pass it to both the receiver, useCalendarInteractionBindings(calendar, { dragScope }), and the palette. The palette owns its pointer and optional touch activation settings; the receiver owns its scrolling. Call the returned cleanup on unmount. makeEvent runs when a gesture begins, so give each new event a unique ID. The event keeps your data type, and the optional metadata arrives on the proposal.

externalDrop.accept(event, metadata) can reject a source. externalDrop.relocationPolicy overrides fields of the calendar's eventRelocationPolicy, independently of movement. By default conversion is off and the destination resource replaces the assignment list. Drops whose ID already exists in the destination are rejected.

For keyboard users, call api.externalDrop.begin(event, metadata?), update the active session with a date target and commit. Your application still inserts after.

Cross-calendar transfers

Register interactionFeature(), pointerFeature() and eventTransferFeature() on both calendars, and give each a distinct calendar-level id, such as 'left' and 'right'. Neither calendar references the other. The source also needs eventMoveFeature(); the receiver needs neither movement nor selection. Settings live under transfer: enabled, accept and relocationPolicy.

Pass the same { dragScope } to both calendars' bindings; a host without one is isolated. To drag both ways, register movement and transfer on both, and either calendar can be source or receiver on each gesture.

Dragging an event onto the other calendar holds the source's movement session while the receiver validates the proposal against its own constraints, its eventRelocationPolicy and transfer.relocationPolicy. The source calendar's settings don't apply. Duplicate IDs in the destination are rejected. transfer.accept(sourceId) on the receiver restricts senders.

On commit, the source calls onEventTransfer once with:

({ kind: 'transfer', sourceId, destinationId, before, after });

Remove before.id from the source and add after to the destination in one transaction. The library changes neither array, so keep event state shared between the calendars to avoid applying half a transfer. One callback can serve both calendars, since each proposal names its arrays:

export function applyTransfer(events: TransferEvents, proposal: Calendar_TransferProposal<Booking>): TransferEvents {
  return {
    ...events,
    [proposal.sourceId]: events[proposal.sourceId].filter(event => event.id !== proposal.before.id),
    [proposal.destinationId]: [...events[proposal.destinationId], proposal.after],
  };
}

Programmatic transfers

Programmatic creation and transfer need no pointerFeature. Call api.transfer.prepare(eventId, resourceId?) on the source, then the receiver's transfer.begin or transfer.evaluate with the handle. Always call the handle's release() when abandoning a transfer; the DOM driver does this for pointer gestures.

This helper assumes the default internal owner. A controlled owner must acknowledge pending snapshots before the coordinator continues or releases the prepared source.

export function transferEvent(
  source: { api: { transfer: Calendar_TransferApi<Booking> } },
  destination: { api: { transfer: Calendar_TransferApi<Booking> } },
  eventId: string,
  target: Calendar_DateTarget,
  sourceResourceId?: string,
) {
  const prepared = source.api.transfer?.prepare(eventId, sourceResourceId);
  if (!prepared) return 'unavailable';
  try {
    const session = destination.api.transfer?.begin(prepared);
    if (session?.status !== 'active') return session?.status ?? 'unavailable';
    session.session.update(target);
    const result = session.session.commit();
    if (result.status !== 'committed') session.session.cancel();
    return result;
  } finally {
    prepared.release();
  }
}

See editing policies for conversion and resource rules.

Bidirectional React setup

The shared features register interaction, movement, transfer and pointer input. This hook owns both event arrays and one drag scope. Render each calendar with its own provider, root, surface and event source refs as described in selection and editing.

export function useTransferCalendars() {
  const [dragScope] = useState(createCalendarDragScope);
  const [events, setEvents] = useState<TransferEvents>({ left: initialEvents, right: [] });
  const [leftDate, setLeftDate] = useState(initialDate);
  const [rightDate, setRightDate] = useState(initialDate);
  const onEventTransfer = (proposal: Calendar_TransferProposal<Booking>) =>
    setEvents(current => applyTransfer(current, proposal));

  const left = useCalendar({
    id: 'left',
    features,
    views,
    view: 'day',
    date: leftDate,
    timeZone: 'UTC',
    events: events.left,
    onEventTransfer,
    onNavigate: next => setLeftDate(next.date),
  });
  const right = useCalendar({
    id: 'right',
    features,
    views,
    view: 'day',
    date: rightDate,
    timeZone: 'UTC',
    events: events.right,
    onEventTransfer,
    onNavigate: next => setRightDate(next.date),
  });
  const leftBindings = useCalendarInteractionBindings(left, { dragScope });
  const rightBindings = useCalendarInteractionBindings(right, { dragScope });
  return { left, right, leftBindings, rightBindings };
}