Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Event editor

Event editor is the create-and-edit form for a single scheduler event, covering a title, description, a date (or a start and end date range in all-day mode), start and end time, location, notes, priority, color, and guests to share the event with, each rendered with an existing library field component. It's an independent piece of the Scheduler feature, which embeds it inside its own desktop drawer and mobile modal, and it's just as usable dropped directly on any page on its own.

Dependencies

eun-input · renders the title, location, and guest email fields eun-textarea · renders the description and notes fields eun-date-picker · renders the date field(s) eun-time-picker · renders the start/end time fields, hidden in all-day mode eun-checkbox · renders the "All day" toggle eun-color-picker · renders the color field, its presets shown inline eun-button · renders the guest add button and the built-in Save/Cancel/Delete footer, unless hide-actions is set eun-toggle · eun-toggle-group and eun-toggle-item render the priority picker eun-icon · renders the guest add button's icon and, inside each guest chip, its status icon and delete glyph eun-chip · renders each invited guest as a removable chip with its status icon
Overview API Examples Accessibility

When to use

Reach for event editor whenever you need the exact create-and-edit form scheduler itself uses for a single event, embedded in your own dialog instead of a drawer or modal, on a dedicated "new event" page, or anywhere else a standalone event form makes sense outside a full calendar.

Leaving the event unset (or null) puts the form in create mode, where it seeds from an initial start and end time, defaulting to the next hour and one hour after that. Passing an existing event switches it to edit mode instead, so every field seeds from it and a Delete button appears in the built-in footer. It never mutates the event itself, and reads or writes nothing beyond dispatching its events (see below), so the consumer decides what a save or delete actually does to their own data.

See Scheduler: Building your own scheduler for a complete example wiring this form, with its built-in footer left on inside a plain modal, into a calendar UI of your own.

Install & usage

npm install @eunomia/elements
import "@eunomia/elements/scheduler-event-editor.js";
<eun-scheduler-event-editor id="editor"></eun-scheduler-event-editor>

<script>
  const editor = document.querySelector("#editor");
  editor.addEventListener("euneventcreate", (event) => {
    console.log(event.event); // a full EunomiaSchedulerEvent, id included
  });
  editor.addEventListener("euneventupdate", (event) => {
    console.log(event.event);
  });
  editor.addEventListener("euneventdelete", (event) => {
    console.log(event.eventId);
  });
</script>

Set hide-actions to suppress the built-in Save/Cancel/Delete footer and drive the form from your own UI instead, via the public requestSave()/ requestDelete() methods and the dirty/valid/isEditing getters, exactly how eun-scheduler unifies the footer with its own drawer/modal chrome rather than rendering two stacked footers:

const saved = editor.requestSave(); // false if invalid, turning on inline errors
if (saved) {
  /* editor already dispatched euneventcreate/euneventupdate */
}
editor.requestDelete(); // dispatches euneventdelete, edit mode only

Live testing

Properties

SchedulerEventEditor <eun-scheduler-event-editor>

Attributes

NameTypeDefaultDescription
localestringThe locale used by the nested date and time fields. Defaults to the runtime's own
hide-actionsbooleanfalseHides the built-in save, cancel, and delete footer, for a host that drives the form itself
title-labelstring'Title'The label of the title field
description-labelstring'Description'The label of the description field
all-day-labelstring'All day'The label of the all day checkbox
date-labelstring'Date'The label of the date field
end-date-labelstring'End date'The label of the end date field, shown in place of the start and end time fields while all day is checked
start-time-labelstring'Start time'The label of the start time field
end-time-labelstring'End time'The label of the end time field
location-labelstring'Location'The label of the location field
notes-labelstring'Notes'The label of the notes field
priority-labelstring'Priority'The label of the priority control
color-labelstring'Color'The label of the color swatch picker
required-labelstring"is required"Accessible name appended after titleLabel when the title field is left empty on submit (e.g. "Title is required").
guests-labelstring'Guests'The label of the guest-sharing field
add-guest-labelstring'Add guest'The accessible label of the guest email input's add button
remove-guest-labelstring'Remove'A prefix combined with the guest's email to build each guest chip's delete button accessible name, such as "Remove alice@example.com"
save-labelstring'Save'The label of the built-in save button
cancel-labelstring'Cancel'The label of the built-in cancel button. Resets the form to its last-seeded values
delete-labelstring'Delete'The label of the built-in delete button. Only shown while editing an existing event

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
eventEunomiaSchedulerEvent | nullThe event being edited. Unset, or null, puts the form in create mode instead, seeded from initialStart and initialEnd
initialStartDateThe start time to seed a new event with, in create mode. Defaults to the next hour
initialEndDateThe end time to seed a new event with, in create mode. Defaults to one hour after initialStart
colorLabelsRecord<EunomiaSchedulerEventColor, string>{ red: "Red", purple: "Purple", yellow: "Yellow", blue: "Blue", green: "Green", pink: "Pink", }Accessible name for each color swatch button, keyed by EunomiaSchedulerEventColor.
dirtybooleanWhether any field has changed since the last seed/save — wire into a host `eun-drawer`'s own `dirty` for its built-in unsaved-changes guard.
isEditingbooleanWhether the form is currently editing an existing event (`event` is set) rather than creating a new one.
validbooleanWhether the form currently passes validation (non-empty title, a complete date/start/end, and an end strictly after start).

Slots

NameDescription
(default)Unused. Present only so a wrapping drawer or modal can slot this element into their own default body slot

Events

NameTypeDescription
inputEvent
euneventdeleteEventDeleteEventFired when the built-in delete button is activated
eunguestinviteGuestInviteEventFired whenever a guest email is added, so a consumer can actually send the share invite
euneventcreateEventCreateEventFired on a valid save, in create mode
euneventupdateEventUpdateEventFired on a valid save, in edit mode

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--scheduler-event-editor-gapSets the gap between fields

Create mode

Leaving event unset renders a blank form (seeded from initialStart/ initialEnd, if set) with no Delete button.

<eun-scheduler-event-editor></eun-scheduler-event-editor>

Seeding a new event

initialStart and initialEnd seed a blank, create-mode form's date and time fields, useful for prefilling from wherever the user triggered "new event," such as a clicked time slot on a calendar. Both are Date properties (attribute: false, like every date value elsewhere in this library), so set them from JavaScript rather than as HTML attributes.

editor.initialStart = new Date(2026, 2, 16, 15, 30);
editor.initialEnd = new Date(2026, 2, 16, 16, 30);

Edit mode

Setting event seeds every field from it, and shows a Delete button.

editor.event = {
  id: "sample-1",
  title: "Design review",
  description: "Walk through the new onboarding flow.",
  location: "Room A",
  color: "purple",
  priority: "high",
  start: new Date(2026, 2, 16, 14, 0),
  end: new Date(2026, 2, 16, 15, 0),
};

Sharing with guests

Guests are entered as emails through the "Guests" field (Enter or the add button), each starting out pending in event.guests, since the editor never sends anything itself and only tracks the resulting pending/confirmed/declined status. Adding one fires eunguestinvite with the email (and the event's id, once editing an existing event): the signal a consumer listens for to actually send the invite. Each guest chip shows its status as an icon: check / hourglass / cross for confirmed/pending/declined, in both the editor and the read-only event popover.

editor.event = {
  id: "sample-guests-1",
  title: "Design review",
  color: "purple",
  start: new Date(2026, 2, 16, 14, 0),
  end: new Date(2026, 2, 16, 15, 0),
  guests: [
    { email: "amelie@example.com", status: "confirmed" },
    { email: "noah@example.com", status: "pending" },
    { email: "sofia@example.com", status: "declined" },
  ],
};

// Fired whenever a guest email is added from the field — send the actual
// invite from here (this library only tracks the resulting status).
editor.addEventListener("eunguestinvite", (event) => {
  sendInvite(event.email, event.eventId); // your own API call
});

All-day mode

Checking "All day" swaps the start/end time fields for a start/end date field instead, with no time of day involved. Saving sets allDay: true and start/end at the two dates' own day boundaries.

editor.event = {
  id: "trip-1",
  title: "Company offsite",
  color: "purple",
  allDay: true,
  start: new Date(2026, 2, 17),
  end: new Date(2026, 2, 19), // spans 3 days
};

Without the built-in footer

For a host (like eun-scheduler itself) that drives Save/Cancel/Delete from its own surrounding chrome instead.

<eun-scheduler-event-editor hide-actions></eun-scheduler-event-editor>

Localizing labels

Every visible label, down to the guest chip's delete button, is overridable, which is what lets the whole form read naturally in a language other than English without touching a single word of markup.

<eun-scheduler-event-editor
  title-label="Titre"
  description-label="Description"
  all-day-label="Toute la journée"
  date-label="Date"
  end-date-label="Date de fin"
  start-time-label="Heure de début"
  end-time-label="Heure de fin"
  location-label="Lieu"
  notes-label="Notes"
  priority-label="Priorité"
  color-label="Couleur"
  guests-label="Invités"
  add-guest-label="Ajouter un invité"
  remove-guest-label="Retirer"
  save-label="Enregistrer"
  cancel-label="Annuler"
  delete-label="Supprimer"
></eun-scheduler-event-editor>

Custom event editor

You're not limited to eun-scheduler-event-editor's own fields or layout. It never mutates event and only ever dispatches plain euneventcreate/ euneventupdate/euneventdelete events carrying an event/eventId property: the exact same contract eun-calendar's own wiring in Scheduler: Building your own scheduler listens for. Reimplementing that contract on your own fields is enough to swap it in: the editor below is a deliberately minimal one built straight from eun-input/eun-date-picker/ eun-time-picker (just title, date, start/end time and a color, no description/notes/priority/all-day support), and drops straight into that same eun-calendar wiring in place of <eun-scheduler-event-editor>, no other changes needed.

Delete Cancel Save
<div id="custom-editor">
  <eun-input id="ce-title" label="Title" required hide-error></eun-input>
  <span id="ce-title-error" role="alert" hidden>Title is required</span>
  <eun-date-picker id="ce-date" label="Date"></eun-date-picker>
  <div class="row">
    <eun-time-picker id="ce-start" label="Start time"></eun-time-picker>
    <eun-time-picker id="ce-end" label="End time"></eun-time-picker>
  </div>
  <label for="ce-color">Color</label>
  <select id="ce-color">
    <option value="blue">Blue</option>
    <option value="red">Red</option>
    <option value="green">Green</option>
    <option value="purple">Purple</option>
    <option value="yellow">Yellow</option>
    <option value="pink">Pink</option>
  </select>
  <div class="actions">
    <eun-button id="ce-delete" variant="critical">Delete</eun-button>
    <eun-button id="ce-cancel" variant="secondary">Cancel</eun-button>
    <eun-button id="ce-save" variant="primary">Save</eun-button>
  </div>
</div>

<script type="module">
  import "@eunomia/elements/input.js";
  import "@eunomia/elements/date-picker.js";
  import "@eunomia/elements/time-picker.js";
  import "@eunomia/elements/button.js";

  const root = document.querySelector("#custom-editor");
  const titleEl = root.querySelector("#ce-title");
  const titleErrorEl = root.querySelector("#ce-title-error");
  const dateEl = root.querySelector("#ce-date");
  const startEl = root.querySelector("#ce-start");
  const endEl = root.querySelector("#ce-end");
  const colorEl = root.querySelector("#ce-color");
  const deleteBtn = root.querySelector("#ce-delete");

  let currentEvent = null; // null => create mode

  function startOfDay(date) {
    const start = new Date(date);
    start.setHours(0, 0, 0, 0);
    return start;
  }

  function nextHour() {
    const date = new Date();
    date.setMinutes(0, 0, 0);
    date.setHours(date.getHours() + 1);
    return date;
  }

  // Call this from your own click/select handlers, the same way
  // `eun-scheduler-event-editor.event = ...` seeds it — `event` here is a
  // `EunomiaSchedulerEvent`, `null` for create mode.
  function seed(event) {
    currentEvent = event;
    titleEl.value = event?.title ?? "";
    dateEl.value = event ? startOfDay(event.start) : startOfDay(new Date());
    startEl.value = event?.start ?? nextHour();
    endEl.value = event?.end ?? new Date(nextHour().getTime() + 60 * 60 * 1000);
    colorEl.value = event?.color ?? "blue";
    titleErrorEl.hidden = true;
    deleteBtn.hidden = !event;
  }

  function requestSave() {
    if (!titleEl.value.trim()) {
      titleErrorEl.hidden = false;
      return false;
    }
    const start = new Date(dateEl.value);
    start.setHours(startEl.value.getHours(), startEl.value.getMinutes(), 0, 0);
    const end = new Date(dateEl.value);
    end.setHours(endEl.value.getHours(), endEl.value.getMinutes(), 0, 0);

    const event = {
      id: currentEvent?.id ?? crypto.randomUUID(),
      title: titleEl.value.trim(),
      color: colorEl.value,
      start,
      end,
    };
    // Matches `EventCreateEvent`/`EventUpdateEvent`'s own shape (an
    // `event` property, not `detail`) so this drops into the exact same
    // `euneventcreate`/`euneventupdate`/`euneventdelete` listeners
    // "Building your own scheduler" wires up for `eun-scheduler-event-editor`.
    const saveEvent = new Event(
      currentEvent ? "euneventupdate" : "euneventcreate",
      { bubbles: true },
    );
    saveEvent.event = event;
    root.dispatchEvent(saveEvent);
    seed(event);
    return true;
  }

  root.querySelector("#ce-save").addEventListener("click", requestSave);
  root
    .querySelector("#ce-cancel")
    .addEventListener("click", () => seed(currentEvent));
  deleteBtn.addEventListener("click", () => {
    if (!currentEvent) {
      return;
    }
    const deleteEvent = new Event("euneventdelete", { bubbles: true });
    deleteEvent.eventId = currentEvent.id;
    root.dispatchEvent(deleteEvent);
  });
</script>

<style>
  #custom-editor {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  #custom-editor .row {
    display: flex;
    gap: 12px;
  }
  #custom-editor .actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
  }
  #custom-editor .actions #ce-delete {
    margin-right: auto;
  }
</style>

Keyboard interactions

Key Action
Tab / Shift+Tab Moves focus through the fields and footer buttons in document order
ArrowLeft/ArrowRight Priority: moves and selects between options, wrapping. Color: only moves focus among swatches, browsing without picking
Home / End Jumps to the first/last option in the focused group
Enter / Space Activates the focused field/button/swatch/option, and is how a color actually gets picked

Every text/date/time field, including the "All day" checkbox (eun-checkbox) and the date/time fields it toggles between, is an existing library component, so their keyboard behavior applies unchanged, and each one's own Accessibility tab documents the full contract. The color field is a real eun-color-picker in presets-inline mode, showing its declared presets directly as the field with no trigger button or panel, while the priority picker is a real eun-toggle-group in single mode, so their exact keyboard/ARIA contracts are documented on their own Accessibility tabs instead of duplicated here, and this page only covers how each is configured.

Aria attributes and rules

  • The color field is eun-color-picker's own preset swatch radiogroup: a role="radiogroup" of role="radio" swatch buttons, each with aria-checked reflecting the current selection and an aria-label taken from its option's label text (e.g. "Red"), and each its own real Tab stop (tabindex="0"), reachable one at a time by Tab just like by the arrow keys. Browsing with the arrow keys only moves focus, it never changes aria-checked on its own, picking a color always takes an explicit Enter/Space or click.
  • The priority picker is a eun-toggle-group with type="stroke" and single set: exclusive selection (exactly one of the four always checked, defaulting to medium) rendered as standalone bordered buttons instead of the color picker's swatches, each a eun-toggle-item labelled "Low"/"Medium"/"High"/"Critical". type="stroke" is purely visual: the underlying role/state/keyboard model is identical to eun-toggle-group's default presentation, see its own Accessibility tab. Each item's variant maps low to success, medium to the unset primary default, high to warning, and critical to critical, a narrower, different mapping than the semantic-state token scale the read-only event chip/popover use elsewhere (that one also has info, which eun-button/eun-toggle-group's own variant doesn't).
  • A required title left empty surfaces an inline role="alert" message once requestSave()/the built-in Save button is attempted, not proactively while still typing, so a screen reader isn't interrupted before the user has even finished the field.