Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(DatePicker): Enhance with Year Month selection #3956

Merged
merged 1 commit into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ test.describe('DatePicker, jump to today', () => {
await expect(page.locator(selectors.monthHeading)).toHaveText(expectedMonth)

const todayButton = page.getByRole('gridcell', {
name: /^5$/,
name: new RegExp(`^${today.getDate()}$`),
})

await expect(todayButton).toHaveClass(/rdp-day_selected/)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { IconChevronLeft, IconChevronRight } from '@royalnavy/icon-library'
import React, { useCallback, useMemo } from 'react'

import { BUTTON_VARIANT } from '../Button'
import { COMPONENT_SIZE } from '../Forms'
import {
StyledButton,
StyledButtonArrows,
StyledNavigation,
} from './partials/StyledCalendarNavigation'

interface CalendarNavigationProps {
month: Date
onMonthChange: (increment: number) => void
onMonthPickerClick: () => void
onYearPickerClick: () => void
}

export const CalendarNavigation = ({
month,
onMonthChange,
onMonthPickerClick,
onYearPickerClick,
}: CalendarNavigationProps) => {
const formatDate = useCallback(
(options: Intl.DateTimeFormatOptions) => {
return month.toLocaleString('en', options)
},
[month]
)

const monthLabel = useMemo(() => formatDate({ month: 'short' }), [formatDate])
const yearLabel = useMemo(() => formatDate({ year: 'numeric' }), [formatDate])

return (
<StyledNavigation>
<StyledButtonArrows
aria-label="Navigate to previous month"
icon={<IconChevronLeft />}
onClick={() => onMonthChange(-1)}
title="Navigate to previous month"
variant={BUTTON_VARIANT.TERTIARY}
/>
<StyledButton
aria-label="Show month picker"
onClick={onMonthPickerClick}
size={COMPONENT_SIZE.SMALL}
variant={BUTTON_VARIANT.TERTIARY}
>
{monthLabel}
</StyledButton>
<StyledButton
aria-label="Show year picker"
onClick={onYearPickerClick}
size={COMPONENT_SIZE.SMALL}
variant={BUTTON_VARIANT.TERTIARY}
>
{yearLabel}
</StyledButton>
<StyledButtonArrows
aria-label="Navigate to next month"
icon={<IconChevronRight />}
onClick={() => onMonthChange(1)}
title="Navigate to next month"
variant={BUTTON_VARIANT.TERTIARY}
/>
</StyledNavigation>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { IconChevronLeft, IconChevronRight } from '@royalnavy/icon-library'
import React, { useState } from 'react'
import range from 'lodash/range'

import { BUTTON_VARIANT } from '../Button'
import { CALENDAR_TABLE_VARIANT, type CalendarTableVariant } from './constants'
import {
StyledButtonArrows,
StyledCalendarContainer,
StyledCalendarTable,
StyledCalendarTiles,
StyledNavigation,
StyledYearsTitle,
} from './partials/StyledCalendarNavigation'

interface CalendarTableProps {
month: Date
variant: CalendarTableVariant
onMonthClick: (month: number) => void
onYearClick: (year: number) => void
}

const MONTHS = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
]

const getDecadeYears = (year: number) => {
const decadeStart = Math.floor(year / 10) * 10 - 1
return range(decadeStart, decadeStart + 12)
}

const MonthsView = ({
month,
onMonthClick,
}: Pick<CalendarTableProps, 'month' | 'onMonthClick'>) => {
const currentMonth = new Date().getMonth()
const selectedMonth = month.getMonth()

return (
<StyledCalendarTable $isMonths>
{MONTHS.map((label, index) => (
<StyledCalendarTiles
key={label}
aria-label={`Select ${label}`}
$isCurrent={index === currentMonth}
$isSelected={index === selectedMonth}
onClick={() => onMonthClick(index)}
>
{label}
</StyledCalendarTiles>
))}
</StyledCalendarTable>
)
}

const YearsView = ({
month,
onYearClick,
}: Pick<CalendarTableProps, 'month' | 'onYearClick'>) => {
const currentYear = new Date().getFullYear()
const selectedYear = month.getFullYear()
const [years, setYears] = useState(() => getDecadeYears(selectedYear))

return (
<>
<StyledNavigation>
<StyledButtonArrows
aria-label="Navigate to previous decade"
icon={<IconChevronLeft />}
onClick={() => setYears(getDecadeYears(years[0]))}
title="Navigate to previous decade"
variant={BUTTON_VARIANT.TERTIARY}
/>
<StyledYearsTitle>{`${years[1]} - ${years[11]}`}</StyledYearsTitle>
<StyledButtonArrows
aria-label="Navigate to next decade"
icon={<IconChevronRight />}
onClick={() => setYears(getDecadeYears(years[11]))}
title="Navigate to next decade"
variant={BUTTON_VARIANT.TERTIARY}
/>
</StyledNavigation>
<StyledCalendarTable $isYears>
{years.map((year) => (
<StyledCalendarTiles
aria-label={`Select ${year}`}
$isCurrent={year === currentYear}
$isSelected={year === selectedYear}
key={year}
onClick={() => onYearClick(year)}
>
{year}
</StyledCalendarTiles>
))}
</StyledCalendarTable>
</>
)
}

export const CalendarTable = ({
variant,
month,
onMonthClick,
onYearClick,
}: CalendarTableProps) => {
return (
<StyledCalendarContainer>
{variant === CALENDAR_TABLE_VARIANT.MONTHS && (
<MonthsView month={month} onMonthClick={onMonthClick} />
)}
{variant === CALENDAR_TABLE_VARIANT.YEARS && (
<YearsView month={month} onYearClick={onYearClick} />
)}
</StyledCalendarContainer>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,9 @@ JumpToToday.storyName = 'Jump to today'
JumpToToday.args = {
jumpToToday: true,
}

export const NavigateMonthYear = Template.bind({})
NavigateMonthYear.storyName = 'Navigate Months and Years'
NavigateMonthYear.args = {
navigateMonthYear: true,
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { DayPickerProps } from 'react-day-picker'
import { enGB } from 'date-fns/locale'
import FocusTrap from 'focus-trap-react'
import { format } from 'date-fns'
import { IconEvent } from '@royalnavy/icon-library'
import { Placement } from '@popperjs/core'
import {
addMonths,
setMonth as changeMonth,
setYear as changeYear,
} from 'date-fns/fp'
import FocusTrap from 'focus-trap-react'
import React, { useCallback, useRef, useState } from 'react'

import { DATE_VALIDITY } from './constants'
import {
CALENDAR_TABLE_VARIANT,
DATE_VALIDITY,
type CalendarTableVariant,
} from './constants'
import { BUTTON_VARIANT } from '../Button'
import { CalendarNavigation } from './CalendarNavigation'
import { CalendarTable } from './CalendarTable'
import { COMPONENT_SIZE } from '../Forms'
import { DATE_FORMAT } from '../../constants'
import { DATEPICKER_ACTION } from './types'
Expand Down Expand Up @@ -152,6 +163,10 @@ export interface DatePickerProps
* Enable the Jump To Today button that sets the current date to today.
*/
jumpToToday?: boolean
/**
* Enable navigation via the Month and Year grids.
*/
navigateMonthYear?: boolean
}

export const DatePicker = ({
Expand All @@ -171,6 +186,7 @@ export const DatePicker = ({
initialEndDate = null,
initialMonth,
placement = 'bottom-start',
navigateMonthYear = false,
jumpToToday = false,
onBlur,
today = new Date(),
Expand All @@ -189,6 +205,9 @@ export const DatePicker = ({

const [isOpen, setIsOpen] = useState(initialIsOpen)

const [calendarTableVariant, setCalendarTableVariant] =
useState<CalendarTableVariant>(CALENDAR_TABLE_VARIANT.HIDE)

const { hasFocus, onLocalBlur, onLocalFocus } = useFocus()

const close = useCallback(() => setIsOpen(false), [])
Expand Down Expand Up @@ -260,6 +279,29 @@ export const DatePicker = ({
dispatch({ type: DATEPICKER_ACTION.REFRESH_INPUT_VALUE })
}

const handleMonthClick = (monthIndex: number) => {
dispatch({
type: DATEPICKER_ACTION.UPDATE,
data: { currentMonth: changeMonth(monthIndex, currentMonth) },
})
setCalendarTableVariant(CALENDAR_TABLE_VARIANT.HIDE)
}

const handleYearClick = (year: number) => {
dispatch({
type: DATEPICKER_ACTION.UPDATE,
data: { currentMonth: changeYear(year, currentMonth) },
})
setCalendarTableVariant(CALENDAR_TABLE_VARIANT.HIDE)
}

const handleMonthChange = (increment: number) => {
dispatch({
type: DATEPICKER_ACTION.UPDATE,
data: { currentMonth: addMonths(increment, currentMonth) },
})
}

return (
<>
<StyledDatePickerInput
Expand Down Expand Up @@ -334,7 +376,10 @@ export const DatePicker = ({
aria-owns={floatingBoxId}
data-testid="datepicker-input-button"
isDisabled={isDisabled}
onClick={toggleIsOpen}
onClick={() => {
toggleIsOpen()
setCalendarTableVariant(CALENDAR_TABLE_VARIANT.HIDE)
}}
ref={buttonRef}
>
<IconEvent size={18} />
Expand All @@ -354,6 +399,14 @@ export const DatePicker = ({
>
<FocusTrap focusTrapOptions={focusTrapOptions}>
<div>
{calendarTableVariant !== CALENDAR_TABLE_VARIANT.HIDE && (
<CalendarTable
month={currentMonth}
variant={calendarTableVariant}
onMonthClick={handleMonthClick}
onYearClick={handleYearClick}
/>
)}
{jumpToToday && (
<StyledJumpToToday
variant={BUTTON_VARIANT.TERTIARY}
Expand All @@ -378,6 +431,18 @@ export const DatePicker = ({
Jump to today
</StyledJumpToToday>
)}
{navigateMonthYear && (
<CalendarNavigation
month={currentMonth}
onMonthChange={handleMonthChange}
onMonthPickerClick={() =>
setCalendarTableVariant(CALENDAR_TABLE_VARIANT.MONTHS)
}
onYearPickerClick={() =>
setCalendarTableVariant(CALENDAR_TABLE_VARIANT.YEARS)
}
/>
)}
<StyledDayPicker
locale={enGB}
formatters={{
Expand Down
Loading
Loading