Skip to content

Commit

Permalink
Add possibility to navigate year picker in calendar (#3571)
Browse files Browse the repository at this point in the history
* Add possibility to navigate year picker in calendar

* 🚧 Requested changes

* 🚧 Fix focus on button navigation
  • Loading branch information
arkadiy93 authored Aug 26, 2024
1 parent ca2d8f2 commit 071aa3d
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const Calendar = forwardRef(
ref: RefObject<HTMLDivElement>,
) => {
const [showYearPicker, setShowYearPicker] = useState(false)
const [yearPickerPage, setYearPickerPage] = useState(0)

const { locale } = useLocale()
const calendarState = useCalendarState({
...props,
Expand Down Expand Up @@ -74,6 +76,7 @@ export const Calendar = forwardRef(
nextMonthDisabled={nextButtonProps.isDisabled}
setShowYearPicker={setShowYearPicker}
showYearPicker={showYearPicker}
setYearPickerPage={setYearPickerPage}
/>
)}
</Popover.Header>
Expand All @@ -82,6 +85,8 @@ export const Calendar = forwardRef(
state={calendarState}
setShowYearPicker={setShowYearPicker}
showYearPicker={showYearPicker}
yearPickerPage={yearPickerPage}
setYearPickerPage={setYearPickerPage}
/>
</Popover.Content>
{Footer && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { AriaCalendarGridProps, useCalendarGrid, useLocale } from 'react-aria'
import { getWeeksInMonth } from '@internationalized/date'
import { CalendarCell } from './CalendarCell'
import { YearGrid } from './YearGrid'
import { Dispatch, SetStateAction } from 'react'

/**
* The grid laying out the cells for the calendars in {link Calendar} and {link RangeCalendar}
Expand All @@ -12,11 +13,15 @@ export function CalendarGrid({
state,
showYearPicker,
setShowYearPicker,
yearPickerPage,
setYearPickerPage,
...props
}: {
state: CalendarState | RangeCalendarState
showYearPicker: boolean
setShowYearPicker: (showYearPicker: boolean) => void
yearPickerPage: number
setYearPickerPage: Dispatch<SetStateAction<number>>
} & AriaCalendarGridProps) {
const { locale } = useLocale()
const { gridProps, headerProps, weekDays } = useCalendarGrid(
Expand All @@ -35,6 +40,8 @@ export function CalendarGrid({
state.setFocusedDate(state.focusedDate.set({ year }))
setShowYearPicker(false)
}}
yearPickerPage={yearPickerPage}
setYearPickerPage={setYearPickerPage}
/>
) : (
<table {...gridProps} style={{ borderSpacing: '0px' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@equinor/eds-icons'
import { CalendarDate } from '@internationalized/date'
import { tokens } from '@equinor/eds-tokens'
import { Dispatch, SetStateAction } from 'react'

const HeaderWrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -63,22 +64,28 @@ export function CalendarHeader({
nextMonthDisabled,
showYearPicker,
setShowYearPicker,
setYearPickerPage,
}: {
state: CalendarState | RangeCalendarState
title: string
previousMonthDisabled?: boolean
nextMonthDisabled?: boolean
showYearPicker: boolean
setShowYearPicker: (showYearPicker: boolean) => void
setYearPickerPage?: Dispatch<SetStateAction<number>>
}) {
return (
<HeaderWrapper>
<HeaderActions>
<Button
variant={'ghost_icon'}
aria-label={'Previous month'}
disabled={previousMonthDisabled || showYearPicker}
onClick={() => state.focusPreviousPage()}
disabled={previousMonthDisabled}
onClick={() =>
showYearPicker
? setYearPickerPage((page) => page - 1)
: state.focusPreviousPage()
}
>
<Icon data={chevron_left} />
</Button>
Expand All @@ -103,8 +110,12 @@ export function CalendarHeader({
<span style={{ flex: '1 1 auto' }}></span>
<Button
variant={'ghost_icon'}
onClick={() => state.focusNextPage()}
disabled={nextMonthDisabled || showYearPicker}
onClick={() =>
showYearPicker
? setYearPickerPage((page) => page + 1)
: state.focusNextPage()
}
disabled={nextMonthDisabled}
aria-label={'Next month'}
>
<Icon data={chevron_right} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const RangeCalendar = forwardRef(
ref: RefObject<HTMLDivElement>,
) => {
const [showYearPicker, setShowYearPicker] = useState(false)
const [yearPickerPage, setYearPickerPage] = useState(0)

const { locale } = useLocale()
const state = useRangeCalendarState({
...props,
Expand Down Expand Up @@ -64,6 +66,7 @@ export const RangeCalendar = forwardRef(
title={title}
setShowYearPicker={setShowYearPicker}
showYearPicker={showYearPicker}
setYearPickerPage={setYearPickerPage}
/>
)}
</Popover.Header>
Expand All @@ -72,6 +75,8 @@ export const RangeCalendar = forwardRef(
state={state}
setShowYearPicker={setShowYearPicker}
showYearPicker={showYearPicker}
yearPickerPage={yearPickerPage}
setYearPickerPage={setYearPickerPage}
/>
</Popover.Content>
{Footer && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import styled from 'styled-components'
import { tokens } from '@equinor/eds-tokens'
import { FocusScope, useFocusManager } from 'react-aria'
import { KeyboardEvent } from 'react'
import {
Dispatch,
KeyboardEvent,
SetStateAction,
useEffect,
useRef,
} from 'react'

const Grid = styled.div`
display: grid;
Expand Down Expand Up @@ -38,50 +44,109 @@ const GridColumn = styled.button<{ $active: boolean }>`
}
`

const TOTAL_VISIBLE_YEARS = 36
const RANGE_OFFSET = 30 / 2

const GridFocusManager = ({
year: selectedYear,
setFocusedYear,
yearPickerPage,
setYearPickerPage,
}: {
setFocusedYear: (year: number) => void
year: number
yearPickerPage?: number
setYearPickerPage?: Dispatch<SetStateAction<number>>
}) => {
const focusManager = useFocusManager()
const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {

const prevYear = useRef<number | undefined>()
const navByKeyboard = useRef<boolean>(false)

const page = yearPickerPage * TOTAL_VISIBLE_YEARS

const years = Array.from(
{ length: TOTAL_VISIBLE_YEARS },
(_, i) => i + (selectedYear + page - RANGE_OFFSET),
)

useEffect(() => {
if (prevYear.current === undefined) {
prevYear.current = yearPickerPage
return
}

if (!navByKeyboard.current) {
focusManager.focusFirst()
return
}

navByKeyboard.current = false

yearPickerPage > prevYear.current
? focusManager.focusFirst()
: focusManager.focusLast()

prevYear.current = yearPickerPage
}, [yearPickerPage, focusManager])

const onKeyDown = (year: number) => (e: KeyboardEvent<HTMLButtonElement>) => {
const target = e.currentTarget
const parent = target.parentElement as HTMLDivElement

const isFirstYear = years.at(0) === year
const isLastYear = years.at(-1) === year

switch (e.key) {
case 'ArrowRight':
e.preventDefault()
if (isLastYear) {
navByKeyboard.current = true
setYearPickerPage((page) => page + 1)
break
}
focusManager.focusNext({ wrap: true })
break
case 'ArrowLeft':
e.preventDefault()
if (isFirstYear) {
navByKeyboard.current = true
setYearPickerPage((page) => page - 1)
break
}
focusManager.focusPrevious({ wrap: true })
break
case 'ArrowDown': {
e.preventDefault()
if (isLastYear) {
navByKeyboard.current = true
setYearPickerPage((page) => page + 1)
break
}
const selfIndex = Array.from(parent.children).indexOf(target)
const focusElement = Array.from(parent.children).at(selfIndex + 5)
focusManager.focusNext({ from: focusElement })
break
}
case 'ArrowUp': {
e.preventDefault()
if (isFirstYear) {
navByKeyboard.current = true
setYearPickerPage((page) => page - 1)
break
}
const selfIndex = Array.from(parent.children).indexOf(target)
const focusElement = Array.from(parent.children).at(selfIndex - 5)
focusManager.focusPrevious({ from: focusElement })
break
}
}
}
const years = Array.from(
{ length: 36 },
(_, i) => i + (selectedYear - 30 / 2),
)

return years.map((year) => (
<GridColumn
$active={selectedYear === year}
onKeyDown={onKeyDown}
onKeyDown={onKeyDown(year)}
onClick={() => setFocusedYear(year)}
aria-label={`Set year to ${year}`}
tabIndex={0}
Expand All @@ -95,14 +160,23 @@ const GridFocusManager = ({
export const YearGrid = ({
setFocusedYear,
year: selectedYear,
yearPickerPage,
setYearPickerPage,
}: {
setFocusedYear: (year: number) => void
year: number
yearPickerPage: number
setYearPickerPage?: Dispatch<SetStateAction<number>>
}) => {
return (
<Grid>
<FocusScope contain restoreFocus autoFocus>
<GridFocusManager year={selectedYear} setFocusedYear={setFocusedYear} />
<GridFocusManager
year={selectedYear}
setFocusedYear={setFocusedYear}
yearPickerPage={yearPickerPage}
setYearPickerPage={setYearPickerPage}
/>
</FocusScope>
</Grid>
)
Expand Down

0 comments on commit 071aa3d

Please sign in to comment.