#Drag, resize, select
Add the Interaction plugin and set editable and selectable. Drag an event to move it, drag its
edge to resize, and drag across empty cells to select a range. Click an event to open a popover.
Interaction.tsx
Interaction.tsx
import {
Calendar,
DayGrid,
Interaction,
TimeGrid,
useCalendar,
type CalEvent,
type EventInput,
type SelectInfo,
} from 'z-cal/react'
import { useCallback, useState, type JSX } from 'react'
import { useScheme } from '../DemoFrame'
import { today, todayDate } from '../shared/dates'
import { EventCreateDialog } from '../shared/EventCreateDialog'
import { EventPopover, placeUnder, type EventPopoverTarget } from '../shared/EventPopover'
// The Interaction plugin enables dragging, resizing, and range selection.
const PLUGINS = [DayGrid, TimeGrid, Interaction]
const INITIAL_EVENTS: EventInput[] = [
{ id: 'sync', title: 'Team sync', start: today(0, 10), end: today(0, 11) },
{ id: 'workshop', title: 'Workshop', start: today(0, 13), end: today(0, 15) },
{ id: 'review', title: 'Design review', start: today(1, 16), end: today(1, 17) },
{ id: 'offsite', title: 'Offsite', start: todayDate(2), end: todayDate(4) },
]
let counter = 0
export function InteractionDemo(): JSX.Element {
// The app owns the data. Drag and resize report a new range; the app persists it.
const [events, setEvents] = useState(INITIAL_EVENTS)
const [log, setLog] = useState<{ id: number; line: string }[]>([])
const [selection, setSelection] = useState<SelectInfo | null>(null)
const [popover, setPopover] = useState<EventPopoverTarget | null>(null)
const { ref, calendar } = useCalendar()
const closePopover = useCallback(() => setPopover(null), [])
const record = (line: string): void =>
setLog((prev) => [{ id: ++counter, line }, ...prev].slice(0, 5))
const persist = (event: CalEvent): void =>
setEvents((prev) =>
prev.map((input) =>
input.id === event.id
? { ...input, start: event.start, end: event.end, allDay: event.allDay }
: input,
),
)
return (
<>
<div className="card">
<Calendar
ref={ref}
plugins={PLUGINS}
view="timeGridWeek"
colorScheme={useScheme()}
headerToolbar={{
start: 'title',
center: '',
end: 'today prev,next dayGridMonth,timeGridWeek',
}}
events={events}
editable
selectable
nowIndicator
scrollTime="08:00"
height="600px"
eventDrop={(info) => {
persist(info.event)
record(`moved "${info.event.title}" to ${info.event.startStr}`)
}}
eventResize={(info) => {
persist(info.event)
record(`resized "${info.event.title}" to ${info.event.endStr}`)
}}
select={(info) => {
record(`selected ${info.startStr} → ${info.endStr}`)
setPopover(null)
setSelection(info)
}}
eventClick={(info) => {
const target = {
event: info.event,
anchor: info.el,
placement: placeUnder(info.el.getBoundingClientRect()),
}
setPopover((prev) => (prev?.anchor === info.el ? null : target))
}}
datesSet={() => {
setPopover(null)
setSelection(null)
}}
/>
</div>
{selection && (
<EventCreateDialog
selection={selection}
timeZone={new Intl.DateTimeFormat().resolvedOptions().timeZone}
onCancel={() => {
setSelection(null)
calendar?.unselect().focus()
}}
onCreate={(title) => {
const input: EventInput = {
id: `new-${++counter}`,
title,
start: selection.start,
end: selection.end,
allDay: selection.allDay,
}
setEvents((prev) => [...prev, input])
record(`created "${title}"`)
setSelection(null)
calendar?.unselect().focus()
}}
/>
)}
<EventPopover
target={popover}
locale="en-US"
resourceTitle={(id) => id}
onClose={closePopover}
onDelete={(event) => {
setEvents((prev) => prev.filter((input) => input.id !== event.id))
record(`deleted "${event.title}"`)
setPopover(null)
}}
/>
<ul className="log" aria-live="polite">
{log.map((entry) => (
<li key={entry.id}>{entry.line}</li>
))}
</ul>
</>
)
}
#What to look at
- The app owns the event array.
eventDropandeventResizereport the new range, and the demo writes it back into React state so the change survives a refetch. See Events and interaction. selecthands you the range and whether it is all-day. The demo opens a native<dialog>to name the event, then callsunselect()on the instance.- The popover is plain app state driven by
eventClick. The library reports the clicked element; the app positions the popover under it.