@open-webapp/calendar 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/react/CalendarPane.js +10 -4
- package/dist/react/CalendarSelector.d.ts +6 -0
- package/dist/react/CalendarSelector.js +22 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,10 +174,16 @@ Built-in copy (overridable only through the slots above):
|
|
|
174
174
|
- Loading: "Loading calendar…".
|
|
175
175
|
- Empty: "No matching events in the next 30 days".
|
|
176
176
|
|
|
177
|
+
#### Footer calendar selection
|
|
178
|
+
|
|
179
|
+
One footer combobox opens a shared searchable dropdown, with calendars grouped under account headings in multi-source use. Each account has its own Clear (primary, else first calendar) and filtered Select all, which adds matching calendars while retaining existing selections. Actions affect only their account; its last selected calendar cannot be deselected, including a non-primary selection.
|
|
180
|
+
|
|
177
181
|
### CalendarSelector props
|
|
178
182
|
|
|
179
183
|
`calendars: CalendarInfo[]`, `selectedIds: string[]`, `onChange(ids)`, `heading?: string` (account heading in multi-source use). It has no slot props or `popupName`. At least one calendar stays selected (deselecting the last one is ignored). "Clear" resets to the primary calendar. Built-in copy: "Search calendars", "Select all", "Clear", "Done", "No calendars found", "No calendars available".
|
|
180
184
|
|
|
185
|
+
The exported standalone selector keeps its single-list props and behavior; Select all adds only search matches while retaining existing selections.
|
|
186
|
+
|
|
181
187
|
### Host CSS
|
|
182
188
|
|
|
183
189
|
`styles.css` is self-contained (plain colors, dark mode via `prefers-color-scheme`). It requires no host CSS variables. Variables it reads, all with defaults:
|
|
@@ -188,6 +194,10 @@ Built-in copy (overridable only through the slots above):
|
|
|
188
194
|
|
|
189
195
|
The Unaccepted count badge uses `.calendar-rsvp-chip-badge`, with light and dark styling included in `styles.css`.
|
|
190
196
|
|
|
197
|
+
## 0.4.0 (BREAKING pre-1.0 minor)
|
|
198
|
+
|
|
199
|
+
- CalendarPane replaces per-account footer triggers with one shared dropdown grouped by account. Hosts relying on multiple footer triggers must adapt. Account-local selection rules and the exported standalone CalendarSelector remain compatible.
|
|
200
|
+
|
|
191
201
|
## 0.3.0 (minor, additive)
|
|
192
202
|
|
|
193
203
|
- Adds the Unaccepted exclusive shortcut after Accepted, Maybe, and Declined, with local selection restoration and default fallback as described above.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { filterByRsvp, groupByDay, getOrganizerLabel, formatDayHeader, isEventCurrent, findNextUpcomingEvent, getCalendarEventKey, DEFAULT_RSVP_STATUSES, } from '../core/events.js';
|
|
4
|
-
import {
|
|
4
|
+
import { GroupedCalendarSelector } from './CalendarSelector.js';
|
|
5
5
|
function toggleRsvpStatus(selection, status) {
|
|
6
6
|
const next = new Set(selection);
|
|
7
7
|
if (next.has(status)) {
|
|
@@ -343,7 +343,13 @@ export function CalendarPane(props) {
|
|
|
343
343
|
openEventPopup(event.htmlLink, popupRef, popupName);
|
|
344
344
|
}
|
|
345
345
|
}, children: _jsx("span", { className: "calendar-event-title", children: event.summary }) })) : (_jsx("span", { className: "calendar-event-title", children: event.summary })), props.renderEventAction?.(event)] }), showCalendarLabel && (_jsxs("span", { className: "calendar-event-calendar", children: [_jsx("span", { className: "calendar-event-calendar-dot", style: { backgroundColor: calendar.backgroundColor } }), calendar.summary] })), showOrganizerLabel && (_jsxs("span", { className: "calendar-event-organizer", children: [multi && (_jsx("span", { className: "calendar-event-source-dot", style: { backgroundColor: sourceColor(event.sourceId) } })), organizerText] }))] }, eventKey));
|
|
346
|
-
})] }, group.dateKey))), loading && _jsx("p", { className: "calendar-empty-state-text", children: "Loading calendar\u2026" }), filteredEvents.length === 0 && !loading && !error && needsScopeSources.length === 0 && _jsx("p", { className: "calendar-empty-state-text", children: emptyStateText })] }))] }), showCalendarSelector && (_jsx("footer", { className: "calendar-pane-footer", children:
|
|
347
|
-
|
|
348
|
-
|
|
346
|
+
})] }, group.dateKey))), loading && _jsx("p", { className: "calendar-empty-state-text", children: "Loading calendar\u2026" }), filteredEvents.length === 0 && !loading && !error && needsScopeSources.length === 0 && _jsx("p", { className: "calendar-empty-state-text", children: emptyStateText })] }))] }), showCalendarSelector && (_jsx("footer", { className: "calendar-pane-footer", children: _jsx(GroupedCalendarSelector, { groups: calendarSources.map((s) => ({
|
|
347
|
+
id: s.id,
|
|
348
|
+
calendars: controller.getCalendars(s.id),
|
|
349
|
+
selectedIds: controller.getSelectedIds(s.id),
|
|
350
|
+
onChange: (ids) => {
|
|
351
|
+
void controller.setSelectedIds(s.id, ids);
|
|
352
|
+
},
|
|
353
|
+
heading: multi ? s.email : undefined,
|
|
354
|
+
})) }) }))] }));
|
|
349
355
|
}
|
|
@@ -7,4 +7,10 @@ interface CalendarSelectorProps {
|
|
|
7
7
|
heading?: string;
|
|
8
8
|
}
|
|
9
9
|
export declare function CalendarSelector({ calendars, selectedIds, onChange, heading }: CalendarSelectorProps): import("react").JSX.Element;
|
|
10
|
+
/** Internal pane renderer; intentionally not re-exported from the package barrel. */
|
|
11
|
+
export declare function GroupedCalendarSelector({ groups }: {
|
|
12
|
+
groups: (CalendarSelectorProps & {
|
|
13
|
+
id: string;
|
|
14
|
+
})[];
|
|
15
|
+
}): import("react").JSX.Element;
|
|
10
16
|
export {};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
3
3
|
import { filterCalendarsByName, summarizeCalendarSelection } from '../core/events.js';
|
|
4
4
|
export function CalendarSelector({ calendars, selectedIds, onChange, heading }) {
|
|
5
|
+
return _jsx(GroupedCalendarSelector, { groups: [{ id: '', calendars, selectedIds, onChange, heading }] });
|
|
6
|
+
}
|
|
7
|
+
/** Internal pane renderer; intentionally not re-exported from the package barrel. */
|
|
8
|
+
export function GroupedCalendarSelector({ groups }) {
|
|
5
9
|
const [open, setOpen] = useState(false);
|
|
6
10
|
const [query, setQuery] = useState('');
|
|
7
11
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
@@ -9,8 +13,9 @@ export function CalendarSelector({ calendars, selectedIds, onChange, heading })
|
|
|
9
13
|
const searchRef = useRef(null);
|
|
10
14
|
const rootRef = useRef(null);
|
|
11
15
|
const listboxId = useId();
|
|
12
|
-
const
|
|
13
|
-
const
|
|
16
|
+
const visibleGroups = groups.map((group) => ({ ...group, visible: filterCalendarsByName(group.calendars, query) }));
|
|
17
|
+
const visibleCalendars = visibleGroups.flatMap((group) => group.visible.map((calendar) => ({ calendar, group })));
|
|
18
|
+
const optionId = (sourceId, calendarId) => `${listboxId}-${encodeURIComponent(JSON.stringify([sourceId, calendarId]))}`;
|
|
14
19
|
useEffect(() => {
|
|
15
20
|
if (open)
|
|
16
21
|
searchRef.current?.focus();
|
|
@@ -37,7 +42,8 @@ export function CalendarSelector({ calendars, selectedIds, onChange, heading })
|
|
|
37
42
|
setOpen(false);
|
|
38
43
|
buttonRef.current?.focus();
|
|
39
44
|
}
|
|
40
|
-
function toggle(id) {
|
|
45
|
+
function toggle(group, id) {
|
|
46
|
+
const { selectedIds, onChange } = group;
|
|
41
47
|
if (selectedIds.includes(id)) {
|
|
42
48
|
if (selectedIds.length === 1)
|
|
43
49
|
return;
|
|
@@ -46,11 +52,13 @@ export function CalendarSelector({ calendars, selectedIds, onChange, heading })
|
|
|
46
52
|
}
|
|
47
53
|
onChange([...selectedIds, id]);
|
|
48
54
|
}
|
|
49
|
-
function selectAll() {
|
|
50
|
-
const
|
|
55
|
+
function selectAll(group) {
|
|
56
|
+
const { selectedIds, onChange } = group;
|
|
57
|
+
const visibleIds = group.visible.map((calendar) => calendar.id);
|
|
51
58
|
onChange([...selectedIds, ...visibleIds.filter((id) => !selectedIds.includes(id))]);
|
|
52
59
|
}
|
|
53
|
-
function clear() {
|
|
60
|
+
function clear({ calendars, onChange }) {
|
|
61
|
+
const primaryId = calendars.find((calendar) => calendar.primary)?.id ?? calendars[0]?.id;
|
|
54
62
|
onChange(primaryId ? [primaryId] : []);
|
|
55
63
|
}
|
|
56
64
|
function handleListboxKeyDown(event) {
|
|
@@ -75,16 +83,19 @@ export function CalendarSelector({ calendars, selectedIds, onChange, heading })
|
|
|
75
83
|
event.preventDefault();
|
|
76
84
|
const activeCalendar = visibleCalendars[activeIndex];
|
|
77
85
|
if (activeCalendar)
|
|
78
|
-
toggle(activeCalendar.id);
|
|
86
|
+
toggle(activeCalendar.group, activeCalendar.calendar.id);
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
|
-
const label =
|
|
89
|
+
const label = groups.length === 1
|
|
90
|
+
? summarizeCalendarSelection(groups[0].selectedIds, groups[0].calendars)
|
|
91
|
+
: `${groups.reduce((count, group) => count + group.selectedIds.length, 0)} calendars selected`;
|
|
92
|
+
const activeCalendar = visibleCalendars[activeIndex];
|
|
82
93
|
return (_jsxs("div", { className: "calendar-selector", ref: rootRef, children: [_jsx("button", { ref: buttonRef, type: "button", role: "combobox", "aria-label": label, "aria-expanded": open, "aria-controls": listboxId, "aria-haspopup": "listbox", onClick: () => {
|
|
83
94
|
setOpen((isOpen) => !isOpen);
|
|
84
95
|
setQuery('');
|
|
85
96
|
setActiveIndex(0);
|
|
86
|
-
}, children: label }), open && (_jsxs("div", { className: "calendar-selector-dropdown calendar-selector-dropdown--upward", onKeyDown: handleListboxKeyDown, children: [heading && (_jsx("div", { role: "heading", "aria-level": 3, className: "calendar-selector-heading", children: heading })), _jsx("input", { ref: searchRef, type: "search", role: "searchbox", "aria-label": "Search calendars", value: query, onChange: (event) => {
|
|
97
|
+
}, children: label }), open && (_jsxs("div", { className: "calendar-selector-dropdown calendar-selector-dropdown--upward", onKeyDown: handleListboxKeyDown, children: [groups.length === 1 && groups[0].heading && (_jsx("div", { role: "heading", "aria-level": 3, className: "calendar-selector-heading", children: groups[0].heading })), _jsx("input", { ref: searchRef, type: "search", role: "searchbox", "aria-label": "Search calendars", value: query, onChange: (event) => {
|
|
87
98
|
setQuery(event.target.value);
|
|
88
99
|
setActiveIndex(0);
|
|
89
|
-
} }), _jsxs("div", { className: "calendar-selector-actions", children: [_jsx("button", { type: "button", onClick: selectAll, children: "Select all" }), _jsx("button", { type: "button", onClick: clear, children: "Clear" }), _jsx("button", { type: "button", onClick: close, children: "Done" })] }), _jsx("div", { id: listboxId, role: "listbox", "aria-multiselectable": "true", "aria-activedescendant":
|
|
100
|
+
} }), _jsxs("div", { className: "calendar-selector-actions", children: [groups.length === 1 && _jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => selectAll(visibleGroups[0]), children: "Select all" }), _jsx("button", { type: "button", onClick: () => clear(groups[0]), children: "Clear" })] }), _jsx("button", { type: "button", onClick: close, children: "Done" })] }), _jsx("div", { id: listboxId, role: "listbox", "aria-multiselectable": "true", "aria-activedescendant": activeCalendar ? optionId(activeCalendar.group.id, activeCalendar.calendar.id) : undefined, tabIndex: -1, children: visibleGroups.map((group) => (_jsxs("div", { role: groups.length > 1 ? 'group' : undefined, "aria-label": group.heading, children: [groups.length > 1 && _jsxs(_Fragment, { children: [_jsx("div", { role: "heading", "aria-level": 3, className: "calendar-selector-heading", children: group.heading }), _jsxs("div", { className: "calendar-selector-actions", children: [_jsx("button", { type: "button", onClick: () => selectAll(group), children: "Select all" }), _jsx("button", { type: "button", onClick: () => clear(group), children: "Clear" })] })] }), group.calendars.length === 0 ? (_jsx("p", { children: "No calendars available" })) : group.visible.length === 0 ? (_jsx("p", { children: "No calendars found" })) : (group.visible.map((calendar) => (_jsxs("button", { type: "button", id: optionId(group.id, calendar.id), role: "option", title: calendar.summary, "aria-selected": group.selectedIds.includes(calendar.id), "data-active": (activeCalendar?.group.id === group.id && activeCalendar.calendar.id === calendar.id) || undefined, onClick: () => toggle(group, calendar.id), children: [_jsx("input", { type: "checkbox", className: "calendar-selector-checkbox", checked: group.selectedIds.includes(calendar.id), readOnly: true, tabIndex: -1, "aria-hidden": "true" }), _jsx("span", { "aria-hidden": "true", className: "calendar-selector-dot", "data-testid": `calendar-color-${calendar.id}`, style: { backgroundColor: calendar.backgroundColor } }), _jsx("span", { className: "calendar-selector-name", title: calendar.summary, children: calendar.summary })] }, optionId(group.id, calendar.id)))))] }, group.id))) })] }))] }));
|
|
90
101
|
}
|