Components · Date, time and scheduling
WeeklyHoursEditor
Edits weekly availability: days on or off, several intervals a day, copy and overlap checks.
Controlled with validation
37.5 hours a week
import { useState } from 'react';
import { WeeklyHoursEditor, validateWeeklyHours, type WeeklyHoursInterval } from '@rookcom/monarch-react/scheduling';
const WEEKDAYS: WeeklyHoursInterval[] = [1, 2, 3, 4, 5].flatMap((day) => [
{ day, start: 540, end: 750 },
{ day, start: 810, end: 1050 },
]);
function ControlledDemo() {
const [value, setValue] = useState<WeeklyHoursInterval[]>(WEEKDAYS);
const invalid = Object.keys(validateWeeklyHours(value)).length > 0;
const hours = value.reduce((sum, r) => sum + Math.max(0, r.end - r.start), 0) / 60;
return (
<div style={{ display: 'grid', gap: 8, width: '100%', maxWidth: 640 }}>
<WeeklyHoursEditor
locale="en-GB"
value={value}
onValueChange={setValue}
timeZoneLabel="Times in Europe/London"
/>
<p style={{ margin: 0, fontSize: 13 }} aria-live="polite">
{invalid ? 'Fix the highlighted days to save.' : `${hours} hours a week`}
</p>
</div>
);
}
<ControlledDemo />Usage examples
Uncontrolled, 12-hour clock, week from Sunday
import { WeeklyHoursEditor } from '@rookcom/monarch-react/scheduling';
<div style={{ width: '100%', maxWidth: 640 }}>
<WeeklyHoursEditor
locale="en-US"
hourCycle="12"
weekStartsOn={0}
legend="Support hours"
defaultValue={[{ day: 1, start: 480, end: 1440 }]}
step={30}
name="supportHours"
/>
</div>Read-only
import { WeeklyHoursEditor, type WeeklyHoursInterval } from '@rookcom/monarch-react/scheduling';
const WEEKDAYS: WeeklyHoursInterval[] = [1, 2, 3, 4, 5].flatMap((day) => [
{ day, start: 540, end: 750 },
{ day, start: 810, end: 1050 },
]);
<div style={{ width: '100%', maxWidth: 480 }}>
<WeeklyHoursEditor
locale="en-GB"
legend="Business hours"
readOnly
defaultValue={WEEKDAYS}
timeZoneLabel="Times in Asia/Kolkata"
/>
</div>Purpose
Lets people set the hours they (or a team, or a support desk) are available each week. Needed by: calendar, tickets.
Use when
- Booking availability, business hours, support or on-call hours.
- Any setting that is "these times on these weekdays".
Avoid when
- One-off dates (holidays, time off): use a date list next to it.
- Shift rotas that differ week to week: that is a scheduling grid, not a weekly pattern.
Design principles
- The value is plain data,
{ day, start, end }[]in minutes, a plain, serialisable shape, so it goes to the API and to TimeGridworkingHoursunchanged. - Each day is switched on or off; switching off and on again restores the day’s intervals.
- Times are picked from a list (
stepminutes), never typed free-hand, so there are no parse errors. - Validation is shown under the day and exported as
validateWeeklyHours; the product decides whether to block saving. - The time zone is shown, not assumed:
timeZoneLabelholds text or the product’s timezone picker.
API reference
import { WeeklyHoursEditor, validateWeeklyHours } from '@rookcom/monarch-react/scheduling';| Export | Kind | Rendering | Root class |
|---|---|---|---|
WeeklyHoursEditor | component | client | .mn-weekly-hours-editor |
validateWeeklyHours | utility | client | – |
Renders a <fieldset> (ref) with the legend, the zone slot and one row per weekday in weekStartsOn order (0 = Sunday … 6 = Saturday, as in the date pickers). Interval day values are ISO weekdays (1 = Monday … 7 = Sunday), Rook Calendar’s stored shape.
Controlled with value + onValueChange(value, errors), or uncontrolled with defaultValue. onValueChange emits intervals ordered by weekday, each day’s intervals in the order the user arranged them.
name adds a hidden input holding the value as JSON, for plain form posts.
validateWeeklyHours(value) returns { [weekday]: "range" | "order" | "overlap" } for invalid days.
Times use Monarch NativeSelect (the platform list works well on phones and with 97 options); a separate TimeInput/TimeSelect component is not part of this wave.
Generated from the TypeScript declarations of packages/react/src/components/scheduling/weekly-hours-editor.
Properties
WeeklyHoursEditor
Edits weekly availability: each day switched on or off, one or more time intervals per day, copying a day to the whole week and overlap checks. The value is a plain list of `{ day, start, end }` intervals in minutes.
| Prop | Type | Description |
|---|---|---|
children | ReactNode | |
className | string | |
defaultInterval | { start: number; end: number; } | The interval a day gets when switched on for the first time. Default 09:00–17:00. |
defaultValue | WeeklyHoursValue | Uncontrolled starting value. Default: no hours on any day. |
disabled | boolean | |
hourCycle | HourCycle | Force a 12- or 24-hour clock (`'12'` or `'24'`, as in the date pickers); by default the locale decides. |
id | string | |
labels | WeeklyHoursEditorLabels | |
legend | ReactNode | Visible group title. Default "Weekly hours". |
locale | string | BCP 47 locale for day names and times. |
maxIntervals | number | Most intervals per day. Default 6. |
name | string | Submits the value as JSON in a hidden input with this name. |
onValueChange | ((value: WeeklyHoursInterval[], errors: Partial<Record<number, WeeklyHoursError>>) => void) | Called with every change, intervals ordered by ISO weekday, and the current validation result (see `validateWeeklyHours`) so the product can block saving while it is not empty. |
readOnly | boolean | Shows the hours as text, without controls. |
step | number | Minutes between time options. Default 15. |
timeZoneLabel | ReactNode | Slot beside the legend for the zone the hours are in: text such as "Times in Asia/Kolkata", or a timezone picker. |
value | WeeklyHoursValue | Controlled value. |
weekStartsOn | Weekday | First row: 0 = Sunday … 6 = Saturday, as in MiniMonth and the date pickers. Default 1 (Monday). (Interval `day` values stay ISO 1–7.) |
Variants
| Prop | Values | Description |
|---|---|---|
readOnly | 'true''false' | Shows each day’s hours as text ("09:00 – 17:00"), without controls. |
disabled | 'true''false' | Disables the whole fieldset. |
step | '5''10''15''30''60' | Minutes between time options. Default 15. |
hourCycle | '12''24' | Force a 12- or 24-hour clock; 24-hour shows "24:00" for the end of the day, 12-hour "Midnight". |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Day on | data-on (row) | Switch on, intervals shown, copy button available. |
| Day off | – | "Unavailable" in muted text. |
| Invalid | data-invalid (row), aria-invalid (selects) | Danger-coloured message under the day (role="alert"), linked to its selects. |
| Read-only | data-readonly | Text only. |
| Disabled | – | Native fieldset disabling of every control. |
Interaction behaviour
- Switching a day on adds
defaultInterval(09:00–17:00), or restores the intervals it had when switched off. - "+" adds an interval an hour after the last one (an hour long); "×" removes one. At most
maxIntervalsper day. - "Copy to all" copies that day’s intervals to every day of the week.
- Errors appear as soon as a day is invalid: times outside the day, an interval ending before it starts, or overlapping intervals.
Keyboard support
| Keys | Action |
|---|---|
| Tab / Shift+Tab | Through each day’s switch, selects, remove/add buttons and copy button, in reading order. |
| Space | Toggles a day’s switch. |
| Arrow keys | Change a time in a focused select (platform behaviour). |
| Enter / Space | Activate the add, remove and copy buttons. |
Accessibility
- Role
- group (fieldset with legend); one group per day
- Accessible name
- The fieldset is named by
legend; each switch and day group by the weekday; selects "Monday interval 1 start".
- Buttons have full names: "Add Monday interval", "Remove Monday interval 2", "Copy to all days from Monday" (the visible "Copy to all" starts the name).
- Errors use
role="alert"and are referenced by the day’s selects througharia-describedby. - Day and time names come from Intl in
locale; all fixed strings are inlabels. - Verified with axe in jsdom and in Chromium, Firefox and WebKit on /components/weekly-hours-editor.
Responsive behaviour
- A container query stacks each day below 34rem: the name and copy button on one line, intervals underneath.
- Interval rows wrap, so two selects and their buttons fit at 320px.
Theme support
- Built from Monarch Switch, NativeSelect and Button, so every theme and the product accent apply.
- Errors use
--mn-color-danger-strong.
Do and don't
Do
- Show the time zone the hours are in.
- Block saving while
validateWeeklyHoursreturns errors, and validate again on the server. - Pass the workspace’s week start so rows match the calendar.
Don't
- Do not store times as display strings; keep minutes from midnight.
- Do not put holidays or one-off closures in weekly hours.
Related components
- TimeGrid: Shades time outside the same value (
workingHours). - TimezoneSelect: Goes in the
timeZoneLabelslot. - Switch: The per-day on/off control.
- Field: NativeSelect provides the time lists.
Dependencies
Testing coverage
validateWeeklyHours: range, order and overlap per day.- Switching a day on adds the default interval (uncontrolled) and reports it.
- Add, edit and remove intervals.
- Overlap shows an alert linked to the selects with
aria-invalid. - Copy to all days.
- Switching a day off and on restores its intervals.
- Controlled mode and the hidden form input.
- Week start order, 12-hour labels and Tab order.
- Read-only text, zone label and disabled fieldset; no axe violations; renders on the server.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/weekly-hours-editor; Cross-browser axe: /components/weekly-hours-editor |
Tests: packages/react/src/components/scheduling/weekly-hours-editor