mirror of
https://github.com/layer-systems/website.git
synced 2026-09-13 14:14:07 +02:00
fix: address Copilot review — keyboard trap, midnight formatting, nprofile hint
- MonthGrid: exactly one gridcell must stay tab-focusable. When no day is selected and the displayed month doesn't contain today (e.g. after a PageUp/PageDown jump), every cell previously got tabIndex=-1, trapping keyboard users out of the grid. Falls back to the 1st of the month. - calendarEvents: formatEventTimeRange treated `end` as inclusive when checking same-day, so a time-based event ending exactly at local midnight formatted as a cross-day range even though eventDateKeys() attributes it to the start day only. Now compares against end-1ms, consistent with eventDateKeys(). - CalendarFilters: the author placeholder/error text only mentioned npub/hex even though nprofile is accepted (resolveAuthorInput handles it) — updated both to mention all three. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
This commit is contained in:
@@ -80,13 +80,13 @@ export function CalendarFilters({ filters, onChange, authorError, resolvedAuthor
|
||||
</Label>
|
||||
<Input
|
||||
id="calendar-filter-author"
|
||||
placeholder="npub or hex pubkey"
|
||||
placeholder="npub, nprofile, or hex pubkey"
|
||||
value={filters.authorInput}
|
||||
onChange={(event) => set('authorInput', event.target.value)}
|
||||
aria-invalid={authorError || undefined}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
{authorError && <p className="text-[11px] text-destructive">Not a valid npub or hex pubkey.</p>}
|
||||
{authorError && <p className="text-[11px] text-destructive">Not a valid npub, nprofile, or hex pubkey.</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
|
||||
@@ -48,6 +48,13 @@ export function MonthGrid({
|
||||
const todayKey = localDateKey(today);
|
||||
const cellRefs = useRef(new Map<string, HTMLButtonElement>());
|
||||
|
||||
// Exactly one cell must be tab-focusable, or a keyboard user who tabs away
|
||||
// and back can never re-enter the grid. Prefer the selected day, then
|
||||
// today if it's in the displayed month, and only otherwise fall back to
|
||||
// the 1st — e.g. after PageUp/PageDown lands on a month with neither.
|
||||
const isTodayInMonth = today.getFullYear() === monthAnchor.getFullYear() && today.getMonth() === monthIndex;
|
||||
const focusKey = selectedDate ?? (isTodayInMonth ? todayKey : localDateKey(new Date(monthAnchor.getFullYear(), monthIndex, 1)));
|
||||
|
||||
const focusDate = (date: Date) => {
|
||||
const key = localDateKey(date);
|
||||
onSelectDate(key, { focus: true });
|
||||
@@ -121,7 +128,7 @@ export function MonthGrid({
|
||||
const isCurrentMonth = date.getMonth() === monthIndex;
|
||||
const isToday = key === todayKey;
|
||||
const isSelected = key === selectedDate;
|
||||
const isFocusable = selectedDate ? isSelected : isToday && isCurrentMonth;
|
||||
const isFocusable = key === focusKey;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -199,4 +199,25 @@ describe('formatEventTimeRange', () => {
|
||||
)!;
|
||||
expect(formatEventTimeRange(parsed)).toContain('–');
|
||||
});
|
||||
|
||||
it('treats a time-based event ending exactly at local midnight as ending on the start day, not the next', () => {
|
||||
// `end` is exclusive, so this instant belongs to the start day, same as eventDateKeys() would attribute it.
|
||||
const start = new Date(2026, 5, 1, 23, 0, 0);
|
||||
const end = new Date(2026, 5, 2, 0, 0, 0);
|
||||
const parsed = parseCalendarEvent(
|
||||
makeEvent({
|
||||
kind: TIME_BASED_KIND,
|
||||
tags: [
|
||||
['d', 'a'],
|
||||
['title', 'T'],
|
||||
['start', String(Math.floor(start.getTime() / 1000))],
|
||||
['end', String(Math.floor(end.getTime() / 1000))],
|
||||
],
|
||||
}),
|
||||
)!;
|
||||
|
||||
const startDate = start.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const expected = `${startDate}, ${start.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })} – ${end.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}`;
|
||||
expect(formatEventTimeRange(parsed)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -225,14 +225,18 @@ export function formatEventTimeRange(parsed: ParsedCalendarEvent): string {
|
||||
return `${formatAllDayDate(parsed.start)} – ${formatAllDayDate(lastDay)}`;
|
||||
}
|
||||
|
||||
const sameDay = localDateKey(parsed.start) === localDateKey(parsed.end);
|
||||
const startDate = parsed.start.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
if (parsed.start.getTime() === parsed.end.getTime()) {
|
||||
return `${startDate} at ${formatTime(parsed.start)}`;
|
||||
}
|
||||
// `end` is exclusive, so an event ending exactly at midnight belongs to
|
||||
// the *previous* instant's day — the same day `eventDateKeys()` puts it
|
||||
// on — not the day `end` technically ticks over into.
|
||||
const lastInstant = new Date(parsed.end.getTime() - 1);
|
||||
const sameDay = localDateKey(parsed.start) === localDateKey(lastInstant);
|
||||
if (sameDay) {
|
||||
return `${startDate}, ${formatTime(parsed.start)} – ${formatTime(parsed.end)}`;
|
||||
}
|
||||
const endDate = parsed.end.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const endDate = lastInstant.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
return `${startDate} ${formatTime(parsed.start)} – ${endDate} ${formatTime(parsed.end)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user