From ad4a0e1456f17434a6c37308992dff912c91b8a5 Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Thu, 25 Jun 2026 12:32:37 -0300 Subject: [PATCH] =?UTF-8?q?fix(ui):=20date=20picker=20=E2=80=94=20constant?= =?UTF-8?q?=20calendar=20height=20+=20year-skip=20arrows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The calendar grid rendered a variable number of weeks (5 or 6) depending on the month, so its height jumped when navigating months. Pin it to a constant 6 weeks (42 cells) regardless of month length. Also add year-skip double-chevron arrows on both sides of the month nav, alongside the existing month arrows. Applied to both web and mobile; adds common.previousYear / common.nextYear (en + pt-BR). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/mobile/components/ui/app-date-picker.tsx | 82 +++++++++++++------ .../components/ui/app-date-picker.test.tsx | 22 +++++ apps/web/components/ui/app-date-picker.tsx | 73 +++++++++++------ packages/shared/src/i18n/en.json | 2 + packages/shared/src/i18n/pt-BR.json | 2 + 5 files changed, 132 insertions(+), 49 deletions(-) diff --git a/apps/mobile/components/ui/app-date-picker.tsx b/apps/mobile/components/ui/app-date-picker.tsx index 4ac1347dd7..16bf562ec1 100644 --- a/apps/mobile/components/ui/app-date-picker.tsx +++ b/apps/mobile/components/ui/app-date-picker.tsx @@ -9,17 +9,17 @@ import { import { addMonths, subMonths, + addYears, + subYears, + addDays, startOfMonth, - endOfMonth, startOfWeek, - endOfWeek, - eachDayOfInterval, format, isSameMonth, isSameDay, parseISO, } from 'date-fns' -import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react-native' +import { Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react-native' import { useTranslation } from 'react-i18next' import { formatLocaleDate } from '@orbit/shared/utils' import { useProfile } from '@/hooks/use-profile' @@ -83,11 +83,8 @@ export function AppDatePicker({ }, [weekStartsOn, t]) const calendarDays = useMemo(() => { - const monthStart = startOfMonth(viewDate) - const monthEnd = endOfMonth(viewDate) - const calStart = startOfWeek(monthStart, { weekStartsOn }) - const calEnd = endOfWeek(monthEnd, { weekStartsOn }) - return eachDayOfInterval({ start: calStart, end: calEnd }) + const calStart = startOfWeek(startOfMonth(viewDate), { weekStartsOn }) + return Array.from({ length: 42 }, (_, index) => addDays(calStart, index)) }, [viewDate, weekStartsOn]) const calendarWeeks = useMemo(() => { @@ -106,6 +103,14 @@ export function AppDatePicker({ setViewDate((d) => addMonths(d, 1)) }, []) + const prevYear = useCallback(() => { + setViewDate((d) => subYears(d, 1)) + }, []) + + const nextYear = useCallback(() => { + setViewDate((d) => addYears(d, 1)) + }, []) + function selectDay(day: Date) { onChange(format(day, 'yyyy-MM-dd')) setIsOpen(false) @@ -156,25 +161,47 @@ export function AppDatePicker({ onStartShouldSetResponder={() => true} > - - - + + + + + + + + + {monthLabel} - - - + + + + + + + + + @@ -282,6 +309,11 @@ function createStyles(tokens: AppTokens) { marginBottom: 8, paddingHorizontal: 4, }, + monthNavGroup: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, monthLabel: { color: tokens.fg1, fontFamily: 'Rubik_500Medium', diff --git a/apps/web/__tests__/components/ui/app-date-picker.test.tsx b/apps/web/__tests__/components/ui/app-date-picker.test.tsx index b50408d6aa..14e22f7725 100644 --- a/apps/web/__tests__/components/ui/app-date-picker.test.tsx +++ b/apps/web/__tests__/components/ui/app-date-picker.test.tsx @@ -77,4 +77,26 @@ describe('AppDatePicker', () => { const selected = document.querySelector('[aria-pressed="true"]') expect(selected).toBeInTheDocument() }) + + it('always renders six weeks (42 day cells) regardless of month length', () => { + render() + fireEvent.click(screen.getByRole('button')) + expect(document.querySelectorAll('tbody tr')).toHaveLength(6) + expect(document.querySelectorAll('td button')).toHaveLength(42) + }) + + it('has previous and next year navigation', () => { + render() + fireEvent.click(screen.getByRole('button')) + expect(screen.getByLabelText('common.previousYear')).toBeInTheDocument() + expect(screen.getByLabelText('common.nextYear')).toBeInTheDocument() + }) + + it('advances the year when the next-year arrow is clicked', () => { + render() + fireEvent.click(screen.getByRole('button')) + expect(screen.getByText('June 2025')).toBeInTheDocument() + fireEvent.click(screen.getByLabelText('common.nextYear')) + expect(screen.getByText('June 2026')).toBeInTheDocument() + }) }) diff --git a/apps/web/components/ui/app-date-picker.tsx b/apps/web/components/ui/app-date-picker.tsx index 5c6918c3f3..e360ba6630 100644 --- a/apps/web/components/ui/app-date-picker.tsx +++ b/apps/web/components/ui/app-date-picker.tsx @@ -4,18 +4,18 @@ import { useState, useId, useRef, useCallback, useMemo, useEffect } from 'react' import { addMonths, subMonths, + addYears, + subYears, startOfMonth, - endOfMonth, startOfWeek, endOfWeek, - eachDayOfInterval, addDays, format, isSameMonth, isSameDay, parseISO, } from 'date-fns' -import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react' +import { Calendar, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react' import { useTranslations, useLocale } from 'next-intl' import { formatLocaleDate } from '@orbit/shared/utils' import { useProfile } from '@/hooks/use-profile' @@ -67,11 +67,8 @@ export function AppDatePicker({ }, [weekStartsOn, t]) const calendarDays = useMemo(() => { - const monthStart = startOfMonth(viewDate) - const monthEnd = endOfMonth(viewDate) - const calStart = startOfWeek(monthStart, { weekStartsOn }) - const calEnd = endOfWeek(monthEnd, { weekStartsOn }) - return eachDayOfInterval({ start: calStart, end: calEnd }) + const calStart = startOfWeek(startOfMonth(viewDate), { weekStartsOn }) + return Array.from({ length: 42 }, (_, index) => addDays(calStart, index)) }, [viewDate, weekStartsOn]) const calendarWeeks = (() => { @@ -98,6 +95,14 @@ export function AppDatePicker({ setViewDate((d) => addMonths(d, 1)) } + function prevYear() { + setViewDate((d) => subYears(d, 1)) + } + + function nextYear() { + setViewDate((d) => addYears(d, 1)) + } + const closePicker = useCallback(() => { setIsOpen(false) setFocusedDate(null) @@ -193,14 +198,24 @@ export function AppDatePicker({ className="fixed z-50 left-1/2 top-1/2 m-0 w-[min(90vw,320px)] -translate-x-1/2 -translate-y-1/2 rounded-[16px] border-0 bg-[var(--bg-sheet)] p-2.5 text-[var(--fg-1)] shadow-[var(--shadow-2),inset_0_0_0_1px_var(--hairline)]" >
- +
+ + +
{monthLabel} - +
+ + +