fix: show plugin day labels as dots in the phone month view and keep the mini calendar dot visible on today

This commit is contained in:
Linus Rath
2026-10-14 09:42:48 +02:00
parent 173319120a
commit e1c4a721a3
4 changed files with 57 additions and 7 deletions
@@ -194,18 +194,27 @@ describe('calendar-day-header and mini-calendar-marks', () => {
expect(headerOf('2026-09-09').querySelector('[data-plugin-surface="calendar-day-header"]')).toHaveTextContent('Feast day');
});
it('shows the label in the month cell header on desktop only', () => {
act(() => setDayLabels('hol', 'Holidays', [{ date: '2026-09-09', text: 'Feast day' }]));
it('shows the label in the month cell header, as a dot on phones', () => {
act(() => setDayLabels('hol', 'Holidays', [
{ date: '2026-09-09', text: 'Feast day' },
{ date: '2026-09-10', dot: true, title: 'Dot only' },
]));
const month = { ...week, windowKey: 'month:2026-09-09', rangeStart: new Date(2026, 7, 31), rangeEnd: new Date(2026, 9, 4) };
const { unmount } = render(<CalendarMonthView {...month} />);
const labels = document.querySelectorAll('[data-plugin-surface="calendar-day-header"]');
expect(labels).toHaveLength(1);
expect(labels[0]).toHaveTextContent('Feast day');
expect(labels[0].closest('[role="gridcell"]')?.getAttribute('aria-label')).toMatch(/9/);
unmount();
render(<CalendarMonthView {...month} isMobile />);
expect(document.querySelector('[data-plugin-surface="calendar-day-header"]')).toBeNull();
const dots = [...document.querySelectorAll<HTMLElement>('[data-plugin-surface="calendar-day-header"]')];
expect(dots).toHaveLength(2);
for (const dot of dots) expect(dot).toHaveAttribute('data-compact');
expect(dots[0].closest('[role="gridcell"]')?.getAttribute('aria-label')).toMatch(/9/);
expect(dots[0].title).toBe('Feast day (Holidays)');
expect(dots[1].title).toBe('Dot only (Holidays)');
});
it('marks days with a dot in the small month calendar', () => {
@@ -224,4 +233,22 @@ describe('calendar-day-header and mini-calendar-marks', () => {
expect(dots[0].closest('button')).toHaveTextContent('9');
expect(dots[0].closest('button')).toHaveTextContent('A feast');
});
it('keeps the dot visible on today, whose filled circle has white text', () => {
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime(new Date(2026, 8, 9, 12));
try {
act(() => setDayLabels('hol', 'Holidays', [{ date: '2026-09-09', dot: true, title: 'A feast' }]));
render(
<MiniCalendar selectedDate={new Date(2026, 8, 1)} displayMonth={new Date(2026, 8, 1)} onSelectDate={vi.fn()} onChangeMonth={vi.fn()} />,
);
const dot = document.querySelector<HTMLElement>('[data-plugin-surface="mini-calendar-marks"]')!;
expect(dot.closest('button')).toHaveAttribute('aria-current', 'date');
// Its own colour, not the button's text colour, which is white on today.
expect(dot.className).not.toMatch(/bg-current/);
expect(dot.className).toMatch(/ring-background/);
} finally {
vi.useRealTimers();
}
});
});
+2 -1
View File
@@ -380,7 +380,8 @@ export function CalendarMonthView({
>
{formatDayNumber(day)}
</button>
{!isMobile && <PluginDayLabel surface="calendar-day-header" date={key} className="min-w-0 flex-1" />}
{/* Phones have no room for the text: a dot stands in for it. */}
<PluginDayLabel surface="calendar-day-header" date={key} compact={isMobile} className={isMobile ? undefined : "min-w-0 flex-1"} />
</div>
{isMobile && !showChips ? (
<div className="flex items-center justify-center gap-0.5 flex-wrap">
+24 -2
View File
@@ -27,10 +27,16 @@ interface PluginDayLabelProps {
/** YYYY-MM-DD */
date: string;
className?: string;
/**
* calendar-day-header only: no room for text (the phone month view), so a
* dot stands for the day's labels; their text is its tooltip and is read
* out by screen readers.
*/
compact?: boolean;
}
/** What plugins put on a calendar day. Renders nothing without labels. */
export function PluginDayLabel({ surface, date, className }: PluginDayLabelProps) {
export function PluginDayLabel({ surface, date, className, compact }: PluginDayLabelProps) {
const labels = useDayLabels(date);
if (surface === "mini-calendar-marks") {
@@ -42,13 +48,29 @@ export function PluginDayLabel({ surface, date, className }: PluginDayLabelProps
aria-hidden="true"
data-plugin-surface="mini-calendar-marks"
title={tooltip(marks)}
className={cn("pointer-events-none absolute top-0.5 end-0.5 h-1 w-1 rounded-full bg-current opacity-70", className)}
className={cn("pointer-events-none absolute top-0 end-0 h-1.5 w-1.5 rounded-full bg-muted-foreground ring-1 ring-background", className)}
/>
<span className="sr-only">{marks.map((l) => l.title ?? l.text ?? l.pluginName).join(", ")}</span>
</>
);
}
if (compact) {
const shown = labels.filter((l) => l.text || l.dot);
if (shown.length === 0) return null;
return (
<span
data-plugin-surface="calendar-day-header"
data-compact=""
title={tooltip(shown)}
className={cn("inline-flex flex-shrink-0", className)}
>
<span aria-hidden="true" className="block h-1.5 w-1.5 rounded-full bg-muted-foreground" />
<span className="sr-only">{shown.map((l) => l.text ?? l.title ?? l.pluginName).join(", ")}</span>
</span>
);
}
const withText = labels.filter((l) => l.text);
if (withText.length === 0) return null;
return (
+1 -1
View File
@@ -89,7 +89,7 @@ export const SLOT_CATALOG = {
'calendar-event-actions': S('calendar', 'frame', ['ui:calendar-action'], { eventData: 'calendar:read', setVirtualLocation: null }, 'buttons in the event form', true),
'calendar-event-detail': S('calendar', 'frame', ['ui:calendar-action'], { event: 'calendar:read' }, 'a section in an event\'s details'),
'calendar-event-form': S('calendar', 'frame', ['ui:calendar-action'], { event: 'calendar:read' }, 'a section in the form for a new or edited event'),
'calendar-day-header': S('calendar', 'data', ['ui:calendar-action'], { date: null }, 'a short label under a day\'s name in the day and week views (api.ui.setDayLabels)'),
'calendar-day-header': S('calendar', 'data', ['ui:calendar-action'], { date: null }, 'a short label under a day\'s name in the day, week and month views; a dot in the phone month view (api.ui.setDayLabels)'),
'mini-calendar-marks': S('calendar', 'data', ['ui:calendar-action'], { date: null }, 'dots on days in the small month calendar (api.ui.setDayLabels)'),
// ─── Contacts ──────────────────────────────────────────────