@oslokommune/punkt-react 14.5.3 → 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 +7198 -4674
  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,160 @@
1
+ import type { IDateConstraints, TDateRangeMap } from 'shared-types/calendar'
2
+
3
+ export const DEFAULT_STRINGS: Required<IPktCalendarStrings> = {
4
+ month: 'Måned',
5
+ year: 'År',
6
+ days: ['Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag', 'Søndag'],
7
+ daysShort: ['Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør', 'Søn'],
8
+ months: [
9
+ 'Januar',
10
+ 'Februar',
11
+ 'Mars',
12
+ 'April',
13
+ 'Mai',
14
+ 'Juni',
15
+ 'Juli',
16
+ 'August',
17
+ 'September',
18
+ 'Oktober',
19
+ 'November',
20
+ 'Desember',
21
+ ],
22
+ week: 'Uke',
23
+ prevMonth: 'Forrige måned',
24
+ nextMonth: 'Neste måned',
25
+ }
26
+
27
+ export interface IPktCalendarStrings {
28
+ month?: string
29
+ year?: string
30
+ days?: string[]
31
+ daysShort?: string[]
32
+ months?: string[]
33
+ week?: string
34
+ prevMonth?: string
35
+ nextMonth?: string
36
+ }
37
+
38
+ export interface PktCalendarHandle {
39
+ handleDateSelect: (selectedDate: Date | null) => void
40
+ addToSelected: (selectedDate: Date) => void
41
+ removeFromSelected: (selectedDate: Date) => void
42
+ toggleSelected: (selectedDate: Date) => void
43
+ focusOnCurrentDate: () => void
44
+ close: () => void
45
+ }
46
+
47
+ export interface IPktCalendar {
48
+ // Selection
49
+ selected?: string[]
50
+ multiple?: boolean
51
+ maxMultiple?: number
52
+ range?: boolean
53
+
54
+ // Constraints
55
+ earliest?: string | null
56
+ latest?: string | null
57
+ excludedates?: Date[] | string[]
58
+ excludeweekdays?: string[]
59
+
60
+ // Display
61
+ weeknumbers?: boolean
62
+ withcontrols?: boolean
63
+ currentmonth?: Date | string | null
64
+ today?: string
65
+
66
+ // Localization
67
+ strings?: IPktCalendarStrings
68
+
69
+ // Callbacks
70
+ onDateSelected?: (selected: string[]) => void
71
+ onClose?: () => void
72
+
73
+ // React standard
74
+ id?: string
75
+ className?: string
76
+ }
77
+
78
+ export type TDayViewData = {
79
+ currentDate: Date
80
+ currentDateISO: string
81
+ isToday: boolean
82
+ isSelected: boolean
83
+ isDisabled: boolean
84
+ ariaLabel: string
85
+ tabindex: string
86
+ }
87
+
88
+ export interface ICalendarState {
89
+ componentId: string
90
+ strings: Required<IPktCalendarStrings>
91
+
92
+ // Navigation
93
+ year: number
94
+ month: number
95
+
96
+ // Selection
97
+ activeSelected: string[]
98
+ _selected: Date[]
99
+ inRange: TDateRangeMap
100
+ rangeHovered: Date | null
101
+ focusedDate: string | null
102
+
103
+ // Today override
104
+ todayDate: Date
105
+
106
+ // Props passthrough
107
+ range: boolean
108
+ multiple: boolean
109
+ weeknumbers: boolean
110
+ withcontrols: boolean
111
+ earliest: string | null
112
+ latest: string | null
113
+ excludedates: Date[]
114
+ excludeweekdays: string[]
115
+ className?: string
116
+
117
+ // Constraints
118
+ dateConstraints: IDateConstraints
119
+
120
+ // Refs
121
+ calendarRef: React.RefObject<HTMLDivElement>
122
+ selectableDatesRef: React.MutableRefObject<{ currentDateISO: string; isDisabled: boolean; tabindex: string }[]>
123
+ tabIndexSetRef: React.MutableRefObject<number>
124
+
125
+ // Navigation callbacks
126
+ prevMonth: () => void
127
+ nextMonth: () => void
128
+ changeMonth: (newYear: number, newMonth: number) => void
129
+
130
+ // Selection callbacks
131
+ handleDateSelect: (selectedDate: Date | null) => void
132
+ addToSelected: (selectedDate: Date) => void
133
+ removeFromSelected: (selectedDate: Date) => void
134
+ toggleSelectedDate: (selectedDate: Date) => void
135
+ handleRangeHover: (date: Date) => void
136
+
137
+ // Validation callbacks
138
+ isExcluded: (date: Date) => boolean
139
+ isDayDisabled: (date: Date, isSelected: boolean) => boolean
140
+
141
+ // Focus/keyboard callbacks
142
+ focusOnCurrentDate: () => void
143
+ handleKeydown: (e: React.KeyboardEvent) => void
144
+ handleFocusOut: (e: React.FocusEvent) => void
145
+ close: () => void
146
+ setFocusedDate: (date: string | null) => void
147
+ }
148
+
149
+ export interface ICalendarNavProps {
150
+ componentId: string
151
+ strings: Required<IPktCalendarStrings>
152
+ year: number
153
+ month: number
154
+ earliest: string | null
155
+ latest: string | null
156
+ withcontrols: boolean
157
+ prevMonth: () => void
158
+ nextMonth: () => void
159
+ changeMonth: (newYear: number, newMonth: number) => void
160
+ }
@@ -0,0 +1,426 @@
1
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
2
+ import { formatISODate, newDate, parseISODateString, newDateFromDate, todayInTz } from 'shared-utils/date-utils'
3
+ import type { IDateConstraints, TDateRangeMap } from 'shared-types/calendar'
4
+ import { isDateExcluded, isDayDisabled as checkDayDisabled } from 'shared-utils/calendar/date-validation'
5
+ import {
6
+ convertSelectedToDates,
7
+ updateRangeMap as calculateRangeMap,
8
+ isRangeAllowed as checkRangeAllowed,
9
+ addToSelection,
10
+ removeFromSelection,
11
+ toggleSelection,
12
+ handleRangeSelection,
13
+ } from 'shared-utils/calendar/selection-manager'
14
+ import {
15
+ shouldIgnoreKeyboardEvent,
16
+ findNextSelectableDate as findNextDate,
17
+ getKeyDirection,
18
+ } from 'shared-utils/calendar/keyboard-navigation'
19
+
20
+ import { DEFAULT_STRINGS, type IPktCalendar, type ICalendarState } from './types'
21
+
22
+ export function useCalendarState(props: IPktCalendar): ICalendarState {
23
+ const {
24
+ selected: selectedProp,
25
+ multiple = false,
26
+ maxMultiple = 0,
27
+ range = false,
28
+ earliest = null,
29
+ latest = null,
30
+ excludedates: excludedatesProp,
31
+ excludeweekdays = [],
32
+ weeknumbers = false,
33
+ withcontrols = false,
34
+ currentmonth: currentmonthProp,
35
+ today: todayProp,
36
+ strings: stringsProp,
37
+ onDateSelected,
38
+ onClose,
39
+ id: idProp,
40
+ className,
41
+ } = props
42
+
43
+ const generatedId = useId()
44
+ const componentId = idProp ?? generatedId
45
+
46
+ const strings = useMemo(() => ({ ...DEFAULT_STRINGS, ...stringsProp }), [stringsProp])
47
+
48
+ const todayDate = useMemo(
49
+ () => (todayProp ? parseISODateString(todayProp) : todayInTz()),
50
+ [todayProp],
51
+ )
52
+
53
+ const excludedates = useMemo<Date[]>(() => {
54
+ if (!excludedatesProp) return []
55
+ return excludedatesProp.map((d) => (typeof d === 'string' ? parseISODateString(d) : d))
56
+ }, [excludedatesProp])
57
+
58
+ const isControlled = selectedProp !== undefined
59
+ const [internalSelected, setInternalSelected] = useState<string[]>([])
60
+ const activeSelected = isControlled ? selectedProp : internalSelected
61
+
62
+ const _selected = useMemo(() => convertSelectedToDates(activeSelected), [activeSelected])
63
+
64
+ const [year, setYear] = useState<number>(0)
65
+ const [month, setMonth] = useState<number>(0)
66
+
67
+ const [inRange, setInRange] = useState<TDateRangeMap>({})
68
+ const [rangeHovered, setRangeHovered] = useState<Date | null>(null)
69
+
70
+ const [focusedDate, setFocusedDate] = useState<string | null>(null)
71
+
72
+ const calendarRef = useRef<HTMLDivElement>(null)
73
+ const selectableDatesRef = useRef<{ currentDateISO: string; isDisabled: boolean; tabindex: string }[]>([])
74
+ const tabIndexSetRef = useRef<number>(0)
75
+ const currentmonthtouchedRef = useRef<boolean>(false)
76
+ const initializedRef = useRef<boolean>(false)
77
+
78
+ const dateConstraints = useMemo<IDateConstraints>(
79
+ () => ({ earliest, latest, excludedates, excludeweekdays }),
80
+ [earliest, latest, excludedates, excludeweekdays],
81
+ )
82
+
83
+ useEffect(() => {
84
+ let effectiveMonth: Date | null = null
85
+
86
+ if (currentmonthProp != null) {
87
+ if (currentmonthProp instanceof Date) {
88
+ effectiveMonth = newDateFromDate(currentmonthProp)
89
+ } else if (typeof currentmonthProp === 'string') {
90
+ effectiveMonth = parseISODateString(currentmonthProp)
91
+ }
92
+ }
93
+
94
+ if (!effectiveMonth || isNaN(effectiveMonth.getTime())) {
95
+ if (activeSelected.length > 0 && activeSelected[0] !== '') {
96
+ const d = parseISODateString(activeSelected[activeSelected.length - 1])
97
+ effectiveMonth = isNaN(d.getTime()) ? newDateFromDate(todayDate) : d
98
+ } else {
99
+ effectiveMonth = newDateFromDate(todayDate)
100
+ }
101
+ }
102
+
103
+ if (!effectiveMonth || isNaN(effectiveMonth.getTime())) {
104
+ effectiveMonth = newDateFromDate(todayDate)
105
+ }
106
+
107
+ // Clamp to latest/earliest so the calendar doesn't open on an entirely disabled month
108
+ if (latest) {
109
+ const latestDate = typeof latest === 'string' ? parseISODateString(latest) : latest
110
+ if (!isNaN(latestDate.getTime()) && effectiveMonth > latestDate) {
111
+ effectiveMonth = latestDate
112
+ }
113
+ }
114
+ if (earliest) {
115
+ const earliestDate = typeof earliest === 'string' ? parseISODateString(earliest) : earliest
116
+ if (!isNaN(earliestDate.getTime()) && effectiveMonth < earliestDate) {
117
+ effectiveMonth = earliestDate
118
+ }
119
+ }
120
+
121
+ setYear(effectiveMonth.getFullYear())
122
+ setMonth(effectiveMonth.getMonth())
123
+ initializedRef.current = true
124
+ }, [])
125
+
126
+ // Sync when selected changes externally (controlled mode)
127
+ useEffect(() => {
128
+ if (!initializedRef.current) return
129
+ if (currentmonthtouchedRef.current) return
130
+
131
+ if (activeSelected.length > 0 && activeSelected[0] !== '') {
132
+ const d = parseISODateString(activeSelected[activeSelected.length - 1])
133
+ if (!isNaN(d.getTime())) {
134
+ setYear(d.getFullYear())
135
+ setMonth(d.getMonth())
136
+ }
137
+ }
138
+ }, [activeSelected])
139
+
140
+ // Sync range map when selected changes
141
+ useEffect(() => {
142
+ if (range && _selected.length === 2) {
143
+ setInRange(calculateRangeMap(_selected[0], _selected[1]))
144
+ } else if (!range || _selected.length < 2) {
145
+ setInRange({})
146
+ }
147
+ }, [range, _selected])
148
+
149
+ const updateSelected = useCallback(
150
+ (newSelected: string[]) => {
151
+ if (!isControlled) {
152
+ setInternalSelected(newSelected)
153
+ }
154
+ onDateSelected?.(newSelected)
155
+ },
156
+ [isControlled, onDateSelected],
157
+ )
158
+
159
+ const close = useCallback(() => {
160
+ onClose?.()
161
+ }, [onClose])
162
+
163
+ const changeMonth = useCallback((newYear: number, newMonth: number) => {
164
+ setYear(typeof newYear === 'string' ? parseInt(newYear as unknown as string) : newYear)
165
+ setMonth(typeof newMonth === 'string' ? parseInt(newMonth as unknown as string) : newMonth)
166
+ tabIndexSetRef.current = 0
167
+ setFocusedDate(null)
168
+ selectableDatesRef.current = []
169
+ currentmonthtouchedRef.current = true
170
+ }, [])
171
+
172
+ const prevMonth = useCallback(() => {
173
+ const newMonth = month === 0 ? 11 : month - 1
174
+ const newYear = month === 0 ? year - 1 : year
175
+ changeMonth(newYear, newMonth)
176
+ }, [year, month, changeMonth])
177
+
178
+ const nextMonth = useCallback(() => {
179
+ const newMonth = month === 11 ? 0 : month + 1
180
+ const newYear = month === 11 ? year + 1 : year
181
+ changeMonth(newYear, newMonth)
182
+ }, [year, month, changeMonth])
183
+
184
+ const isExcluded = useCallback((date: Date) => isDateExcluded(date, dateConstraints), [dateConstraints])
185
+
186
+ const isDayDisabled = useCallback(
187
+ (date: Date, isSelected: boolean) =>
188
+ checkDayDisabled(date, isSelected, dateConstraints, {
189
+ multiple,
190
+ maxMultiple,
191
+ selectedCount: activeSelected.length,
192
+ }),
193
+ [dateConstraints, multiple, maxMultiple, activeSelected.length],
194
+ )
195
+
196
+ const normalizeSelected = useCallback((): string[] => {
197
+ if (typeof activeSelected === 'string') {
198
+ return (activeSelected as string).split(',')
199
+ }
200
+ return activeSelected
201
+ }, [activeSelected])
202
+
203
+ const handleRangeHover = useCallback(
204
+ (date: Date) => {
205
+ if (
206
+ !range ||
207
+ _selected.length !== 1 ||
208
+ !checkRangeAllowed(date, _selected, excludedates, excludeweekdays) ||
209
+ _selected[0] >= date
210
+ ) {
211
+ setRangeHovered(null)
212
+ return
213
+ }
214
+ setRangeHovered(date)
215
+ setInRange(calculateRangeMap(_selected[0], date))
216
+ },
217
+ [range, _selected, excludedates, excludeweekdays],
218
+ )
219
+
220
+ const handleDateSelect = useCallback(
221
+ (selectedDate: Date | null) => {
222
+ if (!selectedDate) return
223
+
224
+ let newSelected: string[]
225
+
226
+ if (range) {
227
+ newSelected = handleRangeSelection(selectedDate, normalizeSelected(), {
228
+ excludedates,
229
+ excludeweekdays,
230
+ })
231
+
232
+ if (!isControlled) {
233
+ setInternalSelected(newSelected)
234
+ }
235
+
236
+ if (newSelected.length === 2) {
237
+ onDateSelected?.(newSelected)
238
+ close()
239
+ } else if (newSelected.length === 1) {
240
+ setInRange({})
241
+ onDateSelected?.(newSelected)
242
+ } else {
243
+ onDateSelected?.(newSelected)
244
+ }
245
+ } else if (multiple) {
246
+ newSelected = toggleSelection(selectedDate, normalizeSelected(), maxMultiple)
247
+ updateSelected(newSelected)
248
+ } else {
249
+ const dateISO = formatISODate(selectedDate)
250
+ if (activeSelected.includes(dateISO)) {
251
+ newSelected = []
252
+ } else {
253
+ newSelected = [dateISO]
254
+ }
255
+ updateSelected(newSelected)
256
+ close()
257
+ }
258
+ },
259
+ [
260
+ range,
261
+ multiple,
262
+ maxMultiple,
263
+ normalizeSelected,
264
+ activeSelected,
265
+ excludedates,
266
+ excludeweekdays,
267
+ isControlled,
268
+ updateSelected,
269
+ close,
270
+ onDateSelected,
271
+ ],
272
+ )
273
+
274
+ const addToSelected = useCallback(
275
+ (selectedDate: Date) => {
276
+ const newSelected = addToSelection(selectedDate, normalizeSelected())
277
+ if (!isControlled) {
278
+ setInternalSelected(newSelected)
279
+ }
280
+
281
+ if (range && newSelected.length === 2) {
282
+ onDateSelected?.(newSelected)
283
+ close()
284
+ }
285
+ },
286
+ [normalizeSelected, isControlled, range, onDateSelected, close],
287
+ )
288
+
289
+ const removeFromSelected = useCallback(
290
+ (selectedDate: Date) => {
291
+ const newSelected = removeFromSelection(selectedDate, normalizeSelected())
292
+ if (!isControlled) {
293
+ setInternalSelected(newSelected)
294
+ }
295
+ },
296
+ [normalizeSelected, isControlled],
297
+ )
298
+
299
+ const toggleSelectedDate = useCallback(
300
+ (selectedDate: Date) => {
301
+ const newSelected = toggleSelection(selectedDate, normalizeSelected(), maxMultiple)
302
+ if (!isControlled) {
303
+ setInternalSelected(newSelected)
304
+ }
305
+ },
306
+ [normalizeSelected, maxMultiple, isControlled],
307
+ )
308
+
309
+ const focusOnCurrentDate = useCallback(() => {
310
+ const currentDateISO = formatISODate(newDateFromDate(todayDate))
311
+ const el = calendarRef.current?.querySelector(`button[data-date="${currentDateISO}"]`)
312
+
313
+ if (el instanceof HTMLButtonElement) {
314
+ setFocusedDate(currentDateISO)
315
+ el.focus()
316
+ return
317
+ }
318
+
319
+ const firstSelectable = selectableDatesRef.current.find((x) => !x.isDisabled)
320
+ if (firstSelectable) {
321
+ const firstSelectableEl = calendarRef.current?.querySelector(
322
+ `button[data-date="${firstSelectable.currentDateISO}"]`,
323
+ )
324
+ if (firstSelectableEl instanceof HTMLButtonElement) {
325
+ setFocusedDate(firstSelectable.currentDateISO)
326
+ firstSelectableEl.focus()
327
+ }
328
+ }
329
+ }, [])
330
+
331
+ const handleArrowKey = useCallback(
332
+ (e: KeyboardEvent, direction: number) => {
333
+ const target = e.target as HTMLElement
334
+ if (shouldIgnoreKeyboardEvent(target)) return
335
+
336
+ e.preventDefault()
337
+
338
+ if (!focusedDate) {
339
+ focusOnCurrentDate()
340
+ return
341
+ }
342
+
343
+ const date = newDate(focusedDate)
344
+ const nextDate = findNextDate(date, direction, calendarRef.current!.querySelector.bind(calendarRef.current!))
345
+
346
+ if (nextDate) {
347
+ const el = calendarRef.current!.querySelector(`button[data-date="${formatISODate(nextDate)}"]`)
348
+ if (el instanceof HTMLButtonElement && !el.dataset.disabled) {
349
+ setFocusedDate(formatISODate(nextDate))
350
+ el.focus()
351
+ }
352
+ }
353
+ },
354
+ [focusedDate, focusOnCurrentDate],
355
+ )
356
+
357
+ const handleKeydown = useCallback(
358
+ (e: React.KeyboardEvent) => {
359
+ if (e.key === 'Escape') {
360
+ e.preventDefault()
361
+ close()
362
+ return
363
+ }
364
+
365
+ const direction = getKeyDirection(e.key)
366
+ if (direction !== null) {
367
+ handleArrowKey(e.nativeEvent, direction)
368
+ }
369
+ },
370
+ [close, handleArrowKey],
371
+ )
372
+
373
+ const handleFocusOut = useCallback(
374
+ (e: React.FocusEvent) => {
375
+ if (
376
+ calendarRef.current &&
377
+ !calendarRef.current.contains(e.relatedTarget as Node) &&
378
+ !(e.target as Element).classList.contains('pkt-hide')
379
+ ) {
380
+ close()
381
+ }
382
+ },
383
+ [close],
384
+ )
385
+
386
+ return {
387
+ componentId,
388
+ strings,
389
+ todayDate,
390
+ year,
391
+ month,
392
+ activeSelected,
393
+ _selected,
394
+ inRange,
395
+ rangeHovered,
396
+ focusedDate,
397
+ range,
398
+ multiple,
399
+ weeknumbers,
400
+ withcontrols,
401
+ earliest,
402
+ latest,
403
+ excludedates,
404
+ excludeweekdays,
405
+ className,
406
+ dateConstraints,
407
+ calendarRef,
408
+ selectableDatesRef,
409
+ tabIndexSetRef,
410
+ prevMonth,
411
+ nextMonth,
412
+ changeMonth,
413
+ handleDateSelect,
414
+ addToSelected,
415
+ removeFromSelected,
416
+ toggleSelectedDate,
417
+ handleRangeHover,
418
+ isExcluded,
419
+ isDayDisabled,
420
+ focusOnCurrentDate,
421
+ handleKeydown,
422
+ handleFocusOut,
423
+ close,
424
+ setFocusedDate,
425
+ }
426
+ }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { sortDateStrings, fromISOtoLocal } from 'shared-utils/date-utils'
4
+ import type { IDatepickerStrings } from 'shared-types/datepicker'
5
+ import { PktTag } from '../tag/Tag'
6
+
7
+ interface DateTagsProps {
8
+ dates: string[]
9
+ dateformat: string
10
+ idBase: string
11
+ strings?: IDatepickerStrings
12
+ onDateRemoved: (date: string) => void
13
+ }
14
+
15
+ export const DateTags = ({
16
+ dates,
17
+ dateformat,
18
+ idBase,
19
+ strings,
20
+ onDateRemoved,
21
+ }: DateTagsProps) => {
22
+ const sorted = sortDateStrings(dates)
23
+ const deleteLabel = strings?.calendar?.buttonAltText ?? 'Slett dato'
24
+
25
+ return (
26
+ <div className="pkt-date-tags pkt-datepicker__tags" aria-live="polite">
27
+ {sorted.map((date) => {
28
+ const formatted = fromISOtoLocal(date, dateformat)
29
+ return (
30
+ <PktTag
31
+ key={date}
32
+ id={`${idBase}${date}-tag`}
33
+ closeTag
34
+ ariaLabel={`${deleteLabel} ${formatted}`}
35
+ onClose={() => onDateRemoved(date)}
36
+ >
37
+ <time dateTime={date}>{formatted}</time>
38
+ </PktTag>
39
+ )
40
+ })}
41
+ </div>
42
+ )
43
+ }