@oslokommune/punkt-react 14.5.4 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/index.d.ts +64 -21
  3. package/dist/punkt-react.es.js +7164 -4654
  4. package/dist/punkt-react.umd.js +573 -569
  5. package/package.json +5 -5
  6. package/src/components/calendar/Calendar.accessibility.test.tsx +75 -0
  7. package/src/components/calendar/Calendar.constraints.test.tsx +84 -0
  8. package/src/components/calendar/Calendar.core.test.tsx +272 -0
  9. package/src/components/calendar/Calendar.interaction.test.tsx +96 -0
  10. package/src/components/calendar/Calendar.selection.test.tsx +227 -0
  11. package/src/components/calendar/Calendar.tsx +54 -0
  12. package/src/components/calendar/CalendarGrid.tsx +192 -0
  13. package/src/components/calendar/CalendarNav.tsx +111 -0
  14. package/src/components/calendar/calendar-utils.ts +90 -0
  15. package/src/components/calendar/types.ts +160 -0
  16. package/src/components/calendar/useCalendarState.ts +426 -0
  17. package/src/components/datepicker/DateTags.tsx +43 -0
  18. package/src/components/datepicker/Datepicker.accessibility.test.tsx +404 -0
  19. package/src/components/datepicker/Datepicker.core.test.tsx +270 -0
  20. package/src/components/datepicker/Datepicker.input.test.tsx +218 -0
  21. package/src/components/datepicker/Datepicker.selection.test.tsx +302 -0
  22. package/src/components/datepicker/Datepicker.tsx +61 -79
  23. package/src/components/datepicker/Datepicker.validation.test.tsx +317 -0
  24. package/src/components/datepicker/DatepickerInputs.tsx +184 -0
  25. package/src/components/datepicker/DatepickerPopup.tsx +90 -0
  26. package/src/components/datepicker/types.ts +139 -0
  27. package/src/components/datepicker/useDatepickerState.ts +502 -0
  28. package/src/components/index.ts +1 -0
  29. package/src/components/datepicker/Datepicker.test.tsx +0 -395
@@ -0,0 +1,227 @@
1
+ import '@testing-library/jest-dom'
2
+ import { render, fireEvent, act } from '@testing-library/react'
3
+ import { createRef } from 'react'
4
+ import { parseISODateString } from 'shared-utils/date-utils'
5
+
6
+ import { PktCalendar, IPktCalendar, PktCalendarHandle } from './Calendar'
7
+
8
+ const createCalendar = (props: Partial<IPktCalendar> = {}) => {
9
+ return render(<PktCalendar {...props} />)
10
+ }
11
+
12
+ describe('PktCalendar', () => {
13
+ describe('Date selection functionality', () => {
14
+ test('selects single date correctly', () => {
15
+ const { container } = createCalendar()
16
+
17
+ // Find and click on a date
18
+ const availableDate = container.querySelector(
19
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
20
+ )
21
+ expect(availableDate).toBeInTheDocument()
22
+
23
+ fireEvent.click(availableDate!)
24
+
25
+ expect(availableDate).toHaveClass('pkt-calendar__date--selected')
26
+ })
27
+
28
+ test('handles multiple date selection', () => {
29
+ const { container } = createCalendar({ multiple: true })
30
+
31
+ // Find and click on multiple dates
32
+ const availableDates = container.querySelectorAll(
33
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
34
+ )
35
+ expect(availableDates.length).toBeGreaterThan(1)
36
+
37
+ fireEvent.click(availableDates[0])
38
+ fireEvent.click(availableDates[1])
39
+
40
+ expect(availableDates[0]).toHaveClass('pkt-calendar__date--selected')
41
+ expect(availableDates[1]).toHaveClass('pkt-calendar__date--selected')
42
+ })
43
+
44
+ test('respects maxMultiple limit', () => {
45
+ const { container } = createCalendar({ multiple: true, maxMultiple: 2 })
46
+
47
+ // Try to select more than maxMultiple dates
48
+ const availableDates = container.querySelectorAll(
49
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
50
+ )
51
+ expect(availableDates.length).toBeGreaterThan(2)
52
+
53
+ // Select 3 dates but only 2 should be selected
54
+ fireEvent.click(availableDates[0])
55
+ fireEvent.click(availableDates[1])
56
+ fireEvent.click(availableDates[2])
57
+
58
+ const selectedDates = container.querySelectorAll('.pkt-calendar__date--selected')
59
+ expect(selectedDates.length).toBeLessThanOrEqual(2)
60
+ })
61
+
62
+ test('handles pre-selected dates', () => {
63
+ const preSelectedDates = ['2024-06-15', '2024-06-20']
64
+ const { container } = createCalendar({
65
+ selected: preSelectedDates,
66
+ currentmonth: '2024-06-01',
67
+ })
68
+
69
+ const selectedDates = container.querySelectorAll('.pkt-calendar__date--selected')
70
+ expect(selectedDates.length).toBe(2)
71
+ })
72
+
73
+ test('toggles date selection when clicking same date twice', () => {
74
+ const { container } = createCalendar()
75
+
76
+ const availableDate = container.querySelector(
77
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
78
+ )
79
+ expect(availableDate).toBeInTheDocument()
80
+
81
+ // First click - select
82
+ fireEvent.click(availableDate!)
83
+ expect(availableDate).toHaveClass('pkt-calendar__date--selected')
84
+
85
+ // Second click - deselect
86
+ fireEvent.click(availableDate!)
87
+ expect(availableDate).not.toHaveClass('pkt-calendar__date--selected')
88
+ })
89
+ })
90
+
91
+ describe('Range selection functionality', () => {
92
+ test('handles range selection correctly', () => {
93
+ const { container } = createCalendar({ range: true })
94
+
95
+ const availableDates = container.querySelectorAll(
96
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
97
+ )
98
+ expect(availableDates.length).toBeGreaterThan(3)
99
+
100
+ // Select start date
101
+ fireEvent.click(availableDates[5])
102
+
103
+ // Select end date
104
+ fireEvent.click(availableDates[10])
105
+
106
+ // Check for range styling
107
+ const rangeStart = container.querySelector('.pkt-calendar__date--range-start')
108
+ const rangeEnd = container.querySelector('.pkt-calendar__date--range-end')
109
+ const rangeInBetween = container.querySelectorAll('.pkt-calendar__date--in-range')
110
+
111
+ expect(rangeStart).toBeInTheDocument()
112
+ expect(rangeEnd).toBeInTheDocument()
113
+ expect(rangeInBetween.length).toBeGreaterThan(0)
114
+ })
115
+
116
+ test('shows range hover preview', () => {
117
+ const { container } = createCalendar({ range: true })
118
+
119
+ const availableDates = container.querySelectorAll(
120
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
121
+ )
122
+
123
+ // Select start date
124
+ fireEvent.click(availableDates[5])
125
+
126
+ // Hover over potential end date
127
+ fireEvent.mouseOver(availableDates[10])
128
+
129
+ // Should show hover preview styling
130
+ const hoveredRanges = container.querySelectorAll('.pkt-calendar__date--in-range-hover')
131
+ expect(hoveredRanges.length).toBeGreaterThan(0)
132
+ })
133
+
134
+ test('clears range when selecting new start date', () => {
135
+ const { container } = createCalendar({ range: true })
136
+
137
+ const availableDates = container.querySelectorAll(
138
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
139
+ )
140
+
141
+ // Create initial range
142
+ fireEvent.click(availableDates[5])
143
+ fireEvent.click(availableDates[10])
144
+
145
+ // Select new start date
146
+ fireEvent.click(availableDates[2])
147
+
148
+ // Old range should be cleared
149
+ const selectedDates = container.querySelectorAll('.pkt-calendar__date--selected')
150
+ expect(selectedDates.length).toBe(1)
151
+ })
152
+ })
153
+
154
+ describe('API methods', () => {
155
+ test('addToSelected method works correctly', () => {
156
+ const ref = createRef<PktCalendarHandle>()
157
+ const { container } = render(
158
+ <PktCalendar ref={ref} multiple currentmonth="2024-06-01" />,
159
+ )
160
+
161
+ const testDate = parseISODateString('2024-06-15')
162
+ act(() => {
163
+ ref.current!.addToSelected(testDate)
164
+ })
165
+
166
+ const dateButton = container.querySelector('button[data-date="2024-06-15"]')
167
+ expect(dateButton).toHaveClass('pkt-calendar__date--selected')
168
+ })
169
+
170
+ test('removeFromSelected method works correctly', () => {
171
+ const ref = createRef<PktCalendarHandle>()
172
+ const { container } = render(
173
+ <PktCalendar ref={ref} multiple currentmonth="2024-06-01" />,
174
+ )
175
+
176
+ const testDate = parseISODateString('2024-06-15')
177
+ act(() => {
178
+ ref.current!.addToSelected(testDate)
179
+ })
180
+
181
+ act(() => {
182
+ ref.current!.removeFromSelected(testDate)
183
+ })
184
+
185
+ const dateButton = container.querySelector('button[data-date="2024-06-15"]')
186
+ expect(dateButton).not.toHaveClass('pkt-calendar__date--selected')
187
+ })
188
+
189
+ test('toggleSelected method works correctly', () => {
190
+ const ref = createRef<PktCalendarHandle>()
191
+ const { container } = render(
192
+ <PktCalendar ref={ref} currentmonth="2024-06-01" />,
193
+ )
194
+
195
+ const testDate = parseISODateString('2024-06-15')
196
+
197
+ // Toggle on
198
+ act(() => {
199
+ ref.current!.toggleSelected(testDate)
200
+ })
201
+
202
+ let dateButton = container.querySelector('button[data-date="2024-06-15"]')
203
+ expect(dateButton).toHaveClass('pkt-calendar__date--selected')
204
+
205
+ // Toggle off
206
+ act(() => {
207
+ ref.current!.toggleSelected(testDate)
208
+ })
209
+
210
+ dateButton = container.querySelector('button[data-date="2024-06-15"]')
211
+ expect(dateButton).not.toHaveClass('pkt-calendar__date--selected')
212
+ })
213
+
214
+ test('focusOnCurrentDate method works correctly', () => {
215
+ const ref = createRef<PktCalendarHandle>()
216
+ render(<PktCalendar ref={ref} />)
217
+
218
+ act(() => {
219
+ ref.current!.focusOnCurrentDate()
220
+ })
221
+
222
+ // Should focus on a date element
223
+ const focusedElement = document.activeElement
224
+ expect(focusedElement).toHaveClass('pkt-calendar__date')
225
+ })
226
+ })
227
+ })
@@ -0,0 +1,54 @@
1
+ 'use client'
2
+
3
+ import { type ForwardedRef, forwardRef, useImperativeHandle } from 'react'
4
+ import { useCalendarState } from './useCalendarState'
5
+ import { CalendarNav } from './CalendarNav'
6
+ import { CalendarGrid } from './CalendarGrid'
7
+ import type { IPktCalendar, PktCalendarHandle } from './types'
8
+
9
+ export type { IPktCalendar, IPktCalendarStrings, PktCalendarHandle } from './types'
10
+
11
+ export const PktCalendar = forwardRef<PktCalendarHandle, IPktCalendar>((
12
+ props,
13
+ ref: ForwardedRef<PktCalendarHandle>,
14
+ ) => {
15
+ const state = useCalendarState(props)
16
+
17
+ useImperativeHandle(ref, () => ({
18
+ handleDateSelect: state.handleDateSelect,
19
+ addToSelected: state.addToSelected,
20
+ removeFromSelected: state.removeFromSelected,
21
+ toggleSelected: state.toggleSelectedDate,
22
+ focusOnCurrentDate: state.focusOnCurrentDate,
23
+ close: state.close,
24
+ }), [state.handleDateSelect, state.addToSelected, state.removeFromSelected, state.toggleSelectedDate, state.focusOnCurrentDate, state.close])
25
+
26
+ return (
27
+ <div
28
+ ref={state.calendarRef}
29
+ className={[
30
+ 'pkt-calendar',
31
+ state.weeknumbers && 'pkt-cal-weeknumbers',
32
+ state.className,
33
+ ].filter(Boolean).join(' ')}
34
+ onBlur={state.handleFocusOut}
35
+ onKeyDown={state.handleKeydown}
36
+ >
37
+ <CalendarNav
38
+ componentId={state.componentId}
39
+ strings={state.strings}
40
+ year={state.year}
41
+ month={state.month}
42
+ earliest={state.earliest}
43
+ latest={state.latest}
44
+ withcontrols={state.withcontrols}
45
+ prevMonth={state.prevMonth}
46
+ nextMonth={state.nextMonth}
47
+ changeMonth={state.changeMonth}
48
+ />
49
+ <CalendarGrid state={state} />
50
+ </div>
51
+ )
52
+ })
53
+
54
+ PktCalendar.displayName = 'PktCalendar'
@@ -0,0 +1,192 @@
1
+ import {
2
+ DAYS_PER_WEEK,
3
+ calculateCalendarDimensions,
4
+ getCellType,
5
+ getDayNumber,
6
+ } from 'shared-utils/calendar/calendar-grid'
7
+ import {
8
+ getDayViewData,
9
+ getDayCellClasses,
10
+ getDayButtonClasses,
11
+ type IDayViewContext,
12
+ type IDayCellClassContext,
13
+ } from './calendar-utils'
14
+ import type { ICalendarState } from './types'
15
+
16
+ export const CalendarGrid = ({ state }: { state: ICalendarState }) => {
17
+ const {
18
+ strings,
19
+ year,
20
+ month,
21
+ activeSelected,
22
+ focusedDate,
23
+ inRange,
24
+ rangeHovered,
25
+ range,
26
+ multiple,
27
+ weeknumbers,
28
+ selectableDatesRef,
29
+ tabIndexSetRef,
30
+ handleDateSelect,
31
+ handleRangeHover,
32
+ isExcluded,
33
+ isDayDisabled,
34
+ setFocusedDate,
35
+ todayDate,
36
+ } = state
37
+
38
+ const dayViewCtx: IDayViewContext = {
39
+ year,
40
+ month,
41
+ activeSelected,
42
+ focusedDate,
43
+ tabIndexSetRef,
44
+ isDayDisabled,
45
+ }
46
+
47
+ const cellClassCtx: IDayCellClassContext = {
48
+ range,
49
+ activeSelected,
50
+ rangeHovered,
51
+ inRange,
52
+ isExcluded,
53
+ }
54
+
55
+ const renderDayNames = () => {
56
+ const headers: React.ReactNode[] = []
57
+
58
+ if (weeknumbers) {
59
+ headers.push(
60
+ <th key="week-header">
61
+ <div className="pkt-calendar__week-number">{strings.week}</div>
62
+ </th>,
63
+ )
64
+ }
65
+
66
+ for (let i = 0; i < strings.daysShort.length; i++) {
67
+ headers.push(
68
+ <th key={`day-${i}`}>
69
+ <div className="pkt-calendar__day-name" aria-label={strings.days[i]}>
70
+ {strings.daysShort[i]}
71
+ </div>
72
+ </th>,
73
+ )
74
+ }
75
+
76
+ return <tr className="pkt-cal-week-row">{headers}</tr>
77
+ }
78
+
79
+ const renderDayView = (dayCounter: number, today: Date) => {
80
+ const data = getDayViewData(dayCounter, today, dayViewCtx)
81
+ const { currentDate, currentDateISO, isSelected, isDisabled: isDisabledVal, ariaLabel, tabindex } = data
82
+
83
+ selectableDatesRef.current.push({ currentDateISO, isDisabled: isDisabledVal, tabindex })
84
+
85
+ const cellClasses = getDayCellClasses(data, cellClassCtx)
86
+ const buttonClasses = getDayButtonClasses(data, cellClassCtx)
87
+
88
+ return (
89
+ <td key={currentDateISO} className={cellClasses}>
90
+ <button
91
+ type="button"
92
+ aria-pressed={isSelected ? 'true' : 'false'}
93
+ disabled={isDisabledVal}
94
+ className={`pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--label-only ${buttonClasses}`}
95
+ onMouseOver={() => range && !isExcluded(currentDate) && handleRangeHover(currentDate)}
96
+ onFocus={() => {
97
+ if (range && !isExcluded(currentDate)) {
98
+ handleRangeHover(currentDate)
99
+ }
100
+ setFocusedDate(currentDateISO)
101
+ }}
102
+ aria-label={ariaLabel}
103
+ tabIndex={parseInt(tabindex)}
104
+ data-disabled={isDisabledVal ? 'disabled' : undefined}
105
+ data-date={currentDateISO}
106
+ onKeyDown={(e) => {
107
+ if (e.key === 'Enter' || e.key === ' ') {
108
+ e.preventDefault()
109
+ handleDateSelect(currentDate)
110
+ }
111
+ }}
112
+ onClick={(e) => {
113
+ if (!isDisabledVal) {
114
+ e.preventDefault()
115
+ handleDateSelect(currentDate)
116
+ }
117
+ }}
118
+ >
119
+ <span className="pkt-btn__text pkt-txt-14-light">{dayCounter}</span>
120
+ </button>
121
+ </td>
122
+ )
123
+ }
124
+
125
+ const renderEmptyDayCell = (day: number, key: string) => (
126
+ <td key={key} className="pkt-cal-other">
127
+ <div className="pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--label-only" data-disabled="disabled">
128
+ <span className="pkt-btn__text pkt-txt-14-light">{day}</span>
129
+ </div>
130
+ </td>
131
+ )
132
+
133
+ const renderCalendarBody = () => {
134
+ const today = todayDate
135
+ const dimensions = calculateCalendarDimensions(year, month)
136
+
137
+ selectableDatesRef.current = []
138
+ tabIndexSetRef.current = 0
139
+
140
+ let dayCounter = 1
141
+ let currentWeek = dimensions.initialWeek
142
+
143
+ const rows: React.ReactNode[] = []
144
+
145
+ for (let i = 0; i < dimensions.numRows; i++) {
146
+ const cells: React.ReactNode[] = []
147
+
148
+ if (weeknumbers) {
149
+ cells.push(
150
+ <td key={`week-${currentWeek}`} className="pkt-cal-week">
151
+ {currentWeek}
152
+ </td>,
153
+ )
154
+ }
155
+ currentWeek++
156
+
157
+ for (let j = 0; j < DAYS_PER_WEEK; j++) {
158
+ const cellType = getCellType(i, j, dayCounter, dimensions)
159
+
160
+ if (cellType === 'current-month') {
161
+ cells.push(renderDayView(dayCounter, today))
162
+ dayCounter++
163
+ } else {
164
+ const dayNumber = getDayNumber(cellType, j, dayCounter, dimensions)
165
+ cells.push(renderEmptyDayCell(dayNumber, `${cellType}-${i}-${j}`))
166
+ if (cellType === 'next-month') {
167
+ dayCounter++
168
+ }
169
+ }
170
+ }
171
+
172
+ rows.push(
173
+ <tr key={`row-${i}`} className="pkt-cal-week-row" role="row">
174
+ {cells}
175
+ </tr>,
176
+ )
177
+ }
178
+
179
+ return rows
180
+ }
181
+
182
+ return (
183
+ <table
184
+ className="pkt-cal-days pkt-txt-12-medium pkt-calendar__body"
185
+ role="grid"
186
+ aria-multiselectable={range || multiple || undefined}
187
+ >
188
+ <thead>{renderDayNames()}</thead>
189
+ <tbody>{renderCalendarBody()}</tbody>
190
+ </table>
191
+ )
192
+ }
@@ -0,0 +1,111 @@
1
+ import {
2
+ isPrevMonthAllowed as checkPrevMonthAllowed,
3
+ isNextMonthAllowed as checkNextMonthAllowed,
4
+ } from 'shared-utils/calendar/date-validation'
5
+ import { PktIcon } from '../icon/Icon'
6
+ import type { ICalendarNavProps } from './types'
7
+
8
+ export const CalendarNav = ({
9
+ componentId,
10
+ strings,
11
+ year,
12
+ month,
13
+ earliest,
14
+ latest,
15
+ withcontrols,
16
+ prevMonth,
17
+ nextMonth,
18
+ changeMonth,
19
+ }: ICalendarNavProps) => {
20
+ const renderMonthNavButton = (direction: 'prev' | 'next') => {
21
+ const isPrev = direction === 'prev'
22
+ const isAllowed = isPrev
23
+ ? checkPrevMonthAllowed(year, month, earliest)
24
+ : checkNextMonthAllowed(year, month, latest)
25
+ const label = isPrev ? strings.prevMonth : strings.nextMonth
26
+ const iconName = isPrev ? 'chevron-thin-left' : 'chevron-thin-right'
27
+ const btnClassName = isPrev ? 'pkt-calendar__prev-month' : 'pkt-calendar__next-month'
28
+ const onClick = isPrev ? prevMonth : nextMonth
29
+
30
+ return (
31
+ <div>
32
+ <button
33
+ type="button"
34
+ aria-label={label}
35
+ onClick={() => isAllowed && onClick()}
36
+ onKeyDown={(e) => {
37
+ if (e.key === 'Enter' || e.key === ' ') {
38
+ e.preventDefault()
39
+ if (isAllowed) onClick()
40
+ }
41
+ }}
42
+ className={[
43
+ 'pkt-btn pkt-btn--tertiary pkt-btn--small pkt-btn--icon-only',
44
+ btnClassName,
45
+ !isAllowed && 'pkt-invisible',
46
+ ].filter(Boolean).join(' ')}
47
+ data-disabled={!isAllowed ? 'disabled' : undefined}
48
+ aria-disabled={!isAllowed || undefined}
49
+ tabIndex={isAllowed ? 0 : -1}
50
+ >
51
+ <PktIcon className="pkt-btn__icon" name={iconName} />
52
+ <span className="pkt-btn__text">{label}</span>
53
+ </button>
54
+ </div>
55
+ )
56
+ }
57
+
58
+ const renderMonthNav = () => {
59
+ if (withcontrols) {
60
+ return (
61
+ <div className="pkt-cal-month-picker">
62
+ <label htmlFor={`${componentId}-monthnav`} className="pkt-hide">{strings.month}</label>
63
+ <select
64
+ aria-label={strings.month}
65
+ className="pkt-input pkt-input-compact"
66
+ id={`${componentId}-monthnav`}
67
+ value={month}
68
+ onChange={(e) => {
69
+ e.stopPropagation()
70
+ changeMonth(year, parseInt(e.target.value))
71
+ }}
72
+ >
73
+ {strings.months.map((monthName, index) => (
74
+ <option key={index} value={index}>
75
+ {monthName}
76
+ </option>
77
+ ))}
78
+ </select>
79
+ <label htmlFor={`${componentId}-yearnav`} className="pkt-hide">{strings.year}</label>
80
+ <input
81
+ aria-label={strings.year}
82
+ className="pkt-input pkt-cal-input-year pkt-input-compact"
83
+ id={`${componentId}-yearnav`}
84
+ type="number"
85
+ size={4}
86
+ placeholder="0000"
87
+ value={year}
88
+ onChange={(e) => {
89
+ e.stopPropagation()
90
+ changeMonth(parseInt(e.target.value), month)
91
+ }}
92
+ />
93
+ </div>
94
+ )
95
+ }
96
+
97
+ return (
98
+ <div className="pkt-txt-16-medium pkt-calendar__month-title" aria-live="polite">
99
+ {strings.months[month]} {year}
100
+ </div>
101
+ )
102
+ }
103
+
104
+ return (
105
+ <nav className="pkt-cal-month-nav">
106
+ {renderMonthNavButton('prev')}
107
+ {renderMonthNav()}
108
+ {renderMonthNavButton('next')}
109
+ </nav>
110
+ )
111
+ }
@@ -0,0 +1,90 @@
1
+ import classNames from 'classnames'
2
+ import { formatISODate, newDateYMD, formatReadableDate } from 'shared-utils/date-utils'
3
+ import type { TDateRangeMap } from 'shared-types/calendar'
4
+ import type { TDayViewData } from './types'
5
+
6
+ export interface IDayViewContext {
7
+ year: number
8
+ month: number
9
+ activeSelected: string[]
10
+ focusedDate: string | null
11
+ // Side-effect ref: tracks which day gets tabindex="0" (first non-disabled day)
12
+ tabIndexSetRef: React.MutableRefObject<number>
13
+ isDayDisabled: (date: Date, isSelected: boolean) => boolean
14
+ }
15
+
16
+ export interface IDayCellClassContext {
17
+ range: boolean
18
+ activeSelected: string[]
19
+ rangeHovered: Date | null
20
+ inRange: TDateRangeMap
21
+ isExcluded: (date: Date) => boolean
22
+ }
23
+
24
+ export function getDayViewData(dayCounter: number, today: Date, ctx: IDayViewContext): TDayViewData {
25
+ const currentDate = newDateYMD(ctx.year, ctx.month, dayCounter)
26
+ const currentDateISO = formatISODate(currentDate)
27
+ const isToday = currentDateISO === formatISODate(today)
28
+ const isSelected = ctx.activeSelected.includes(currentDateISO)
29
+ const isDisabledVal = ctx.isDayDisabled(currentDate, isSelected)
30
+
31
+ let tabindex: string
32
+ if (ctx.focusedDate) {
33
+ tabindex = ctx.focusedDate === currentDateISO && !isDisabledVal ? '0' : '-1'
34
+ } else if (!isDisabledVal && ctx.tabIndexSetRef.current === 0) {
35
+ ctx.tabIndexSetRef.current = dayCounter
36
+ tabindex = '0'
37
+ } else {
38
+ tabindex = ctx.tabIndexSetRef.current === dayCounter ? '0' : '-1'
39
+ }
40
+
41
+ return {
42
+ currentDate,
43
+ currentDateISO,
44
+ isToday,
45
+ isSelected,
46
+ isDisabled: isDisabledVal,
47
+ ariaLabel: formatReadableDate(currentDate),
48
+ tabindex,
49
+ }
50
+ }
51
+
52
+ export function getDayCellClasses(data: TDayViewData, ctx: IDayCellClassContext): string {
53
+ const { currentDateISO, isToday, isSelected } = data
54
+ const isRangeStart =
55
+ ctx.range &&
56
+ (ctx.activeSelected.length === 2 || ctx.rangeHovered !== null) &&
57
+ currentDateISO === ctx.activeSelected[0]
58
+ const isRangeEnd = ctx.range && ctx.activeSelected.length === 2 && currentDateISO === ctx.activeSelected[1]
59
+
60
+ return classNames({
61
+ 'pkt-cal-today': isToday,
62
+ 'pkt-cal-selected': isSelected,
63
+ 'pkt-cal-in-range': ctx.inRange[currentDateISO],
64
+ 'pkt-cal-excluded': ctx.isExcluded(data.currentDate),
65
+ 'pkt-cal-in-range-first': isRangeStart,
66
+ 'pkt-cal-in-range-last': isRangeEnd,
67
+ 'pkt-cal-range-hover': ctx.rangeHovered !== null && currentDateISO === formatISODate(ctx.rangeHovered),
68
+ })
69
+ }
70
+
71
+ export function getDayButtonClasses(data: TDayViewData, ctx: IDayCellClassContext): string {
72
+ const { currentDateISO, isToday, isSelected, isDisabled: isDisabledVal } = data
73
+ const isRangeStart =
74
+ ctx.range &&
75
+ (ctx.activeSelected.length === 2 || ctx.rangeHovered !== null) &&
76
+ currentDateISO === ctx.activeSelected[0]
77
+ const isRangeEnd = ctx.range && ctx.activeSelected.length === 2 && currentDateISO === ctx.activeSelected[1]
78
+
79
+ return classNames({
80
+ 'pkt-calendar__date': true,
81
+ 'pkt-calendar__date--today': isToday,
82
+ 'pkt-calendar__date--selected': isSelected,
83
+ 'pkt-calendar__date--disabled': isDisabledVal,
84
+ 'pkt-calendar__date--in-range': ctx.inRange[currentDateISO],
85
+ 'pkt-calendar__date--in-range-hover':
86
+ ctx.rangeHovered !== null && currentDateISO === formatISODate(ctx.rangeHovered),
87
+ 'pkt-calendar__date--range-start': isRangeStart,
88
+ 'pkt-calendar__date--range-end': isRangeEnd,
89
+ })
90
+ }