@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.
- package/CHANGELOG.md +33 -0
- package/dist/index.d.ts +64 -21
- package/dist/punkt-react.es.js +7198 -4674
- package/dist/punkt-react.umd.js +573 -569
- package/package.json +5 -5
- package/src/components/calendar/Calendar.accessibility.test.tsx +75 -0
- package/src/components/calendar/Calendar.constraints.test.tsx +84 -0
- package/src/components/calendar/Calendar.core.test.tsx +272 -0
- package/src/components/calendar/Calendar.interaction.test.tsx +96 -0
- package/src/components/calendar/Calendar.selection.test.tsx +227 -0
- package/src/components/calendar/Calendar.tsx +54 -0
- package/src/components/calendar/CalendarGrid.tsx +192 -0
- package/src/components/calendar/CalendarNav.tsx +111 -0
- package/src/components/calendar/calendar-utils.ts +90 -0
- package/src/components/calendar/types.ts +160 -0
- package/src/components/calendar/useCalendarState.ts +426 -0
- package/src/components/datepicker/DateTags.tsx +43 -0
- package/src/components/datepicker/Datepicker.accessibility.test.tsx +404 -0
- package/src/components/datepicker/Datepicker.core.test.tsx +270 -0
- package/src/components/datepicker/Datepicker.input.test.tsx +218 -0
- package/src/components/datepicker/Datepicker.selection.test.tsx +302 -0
- package/src/components/datepicker/Datepicker.tsx +61 -79
- package/src/components/datepicker/Datepicker.validation.test.tsx +317 -0
- package/src/components/datepicker/DatepickerInputs.tsx +184 -0
- package/src/components/datepicker/DatepickerPopup.tsx +90 -0
- package/src/components/datepicker/types.ts +139 -0
- package/src/components/datepicker/useDatepickerState.ts +502 -0
- package/src/components/index.ts +1 -0
- package/src/components/datepicker/Datepicker.test.tsx +0 -395
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
import { IPktDatepicker, PktDatepicker } from './Datepicker'
|
|
6
|
+
|
|
7
|
+
const datePickerId = 'datepickerId'
|
|
8
|
+
const label = 'Date Picker Label'
|
|
9
|
+
|
|
10
|
+
const createDatepickerTest = (props: Partial<IPktDatepicker> = {}) => {
|
|
11
|
+
const defaultProps: IPktDatepicker = {
|
|
12
|
+
label,
|
|
13
|
+
id: datePickerId,
|
|
14
|
+
...props,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return render(<PktDatepicker {...defaultProps} />)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('PktDatepicker', () => {
|
|
21
|
+
describe('Date boundary testing', () => {
|
|
22
|
+
test('handles minimum possible dates', () => {
|
|
23
|
+
const { container } = createDatepickerTest({
|
|
24
|
+
value: '1900-01-01',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
28
|
+
expect(input.value).toBe('1900-01-01')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('handles maximum possible dates', () => {
|
|
32
|
+
const { container } = createDatepickerTest({
|
|
33
|
+
value: '2100-12-31',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
37
|
+
expect(input.value).toBe('2100-12-31')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('handles year boundaries correctly', () => {
|
|
41
|
+
const { container } = createDatepickerTest({
|
|
42
|
+
value: '2024-12-31',
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
46
|
+
expect(input.value).toBe('2024-12-31')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('handles month boundaries correctly', () => {
|
|
50
|
+
const { container } = createDatepickerTest({
|
|
51
|
+
value: '2024-01-31',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
55
|
+
expect(input.value).toBe('2024-01-31')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('handles leap year dates correctly', () => {
|
|
59
|
+
const { container } = createDatepickerTest({
|
|
60
|
+
value: '2024-02-29',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
64
|
+
expect(input.value).toBe('2024-02-29')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test.each([
|
|
68
|
+
{ desc: 'date before min', min: '2024-06-01', max: '2024-06-30', value: '2024-05-15' },
|
|
69
|
+
{ desc: 'date after max', min: '2024-06-01', max: '2024-06-30', value: '2024-07-15' },
|
|
70
|
+
{ desc: 'valid date within range', min: '2024-06-01', max: '2024-06-30', value: '2024-06-15' },
|
|
71
|
+
])('renders with $desc ($value) without crashing', ({ min, max, value }) => {
|
|
72
|
+
const { container } = createDatepickerTest({
|
|
73
|
+
min,
|
|
74
|
+
max,
|
|
75
|
+
value,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
79
|
+
expect(input.value).toBe(value)
|
|
80
|
+
expect(input).toHaveAttribute('min', min)
|
|
81
|
+
expect(input).toHaveAttribute('max', max)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('Error handling and edge cases', () => {
|
|
86
|
+
test('handles empty values correctly', () => {
|
|
87
|
+
const { container } = createDatepickerTest({
|
|
88
|
+
value: '',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
92
|
+
expect(input.value).toBe('')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('handles undefined value correctly', () => {
|
|
96
|
+
const { container } = createDatepickerTest({
|
|
97
|
+
value: undefined,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
101
|
+
expect(input.value).toBe('')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('handles empty array value for multiple', () => {
|
|
105
|
+
const { container } = createDatepickerTest({
|
|
106
|
+
multiple: true,
|
|
107
|
+
value: [],
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const tags = container.querySelectorAll('.pkt-date-tags .pkt-tag')
|
|
111
|
+
expect(tags.length).toBe(0)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('handles single-value array for range mode', () => {
|
|
115
|
+
const { container } = createDatepickerTest({
|
|
116
|
+
range: true,
|
|
117
|
+
value: ['2024-06-15'],
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const inputs = container.querySelectorAll('input[type="date"]') as NodeListOf<HTMLInputElement>
|
|
121
|
+
expect(inputs[0].value).toBe('2024-06-15')
|
|
122
|
+
expect(inputs[1].value).toBe('')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('handles many dates with maxlength', () => {
|
|
126
|
+
// Generate 20 unique dates
|
|
127
|
+
const dates = Array.from({ length: 20 }, (_, i) => {
|
|
128
|
+
const day = String(i + 1).padStart(2, '0')
|
|
129
|
+
return `2024-06-${day}`
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const { container } = createDatepickerTest({
|
|
133
|
+
multiple: true,
|
|
134
|
+
maxlength: 10,
|
|
135
|
+
value: dates,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Should render all dates as tags (maxlength only restricts new additions)
|
|
139
|
+
const tags = container.querySelectorAll('.pkt-date-tags .pkt-tag')
|
|
140
|
+
expect(tags.length).toBe(dates.length)
|
|
141
|
+
|
|
142
|
+
// Input should be disabled since we're over maxlength
|
|
143
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
144
|
+
expect(input).toBeDisabled()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('handles conflicting properties gracefully (both multiple and range)', () => {
|
|
148
|
+
// When both multiple and range are set, multiple takes precedence
|
|
149
|
+
const { container } = createDatepickerTest({
|
|
150
|
+
multiple: true,
|
|
151
|
+
range: true,
|
|
152
|
+
value: ['2024-06-15', '2024-06-20'],
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Multiple mode should render tags
|
|
156
|
+
const tags = container.querySelectorAll('.pkt-date-tags .pkt-tag')
|
|
157
|
+
expect(tags.length).toBe(2)
|
|
158
|
+
|
|
159
|
+
// Should have only one input (multiple mode), not two (range mode)
|
|
160
|
+
const inputs = container.querySelectorAll('input[type="date"]')
|
|
161
|
+
expect(inputs.length).toBe(1)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('handles string value passed as array', () => {
|
|
165
|
+
const { container } = createDatepickerTest({
|
|
166
|
+
value: '2024-06-15',
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
170
|
+
expect(input.value).toBe('2024-06-15')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('handles array value passed for single mode', () => {
|
|
174
|
+
const { container } = createDatepickerTest({
|
|
175
|
+
value: ['2024-06-15'],
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
179
|
+
expect(input.value).toBe('2024-06-15')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('Controlled vs uncontrolled', () => {
|
|
184
|
+
test('works as controlled component', () => {
|
|
185
|
+
const { container, rerender } = render(
|
|
186
|
+
<PktDatepicker id={datePickerId} label={label} value="2024-06-15" />,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
let input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
190
|
+
expect(input.value).toBe('2024-06-15')
|
|
191
|
+
|
|
192
|
+
rerender(<PktDatepicker id={datePickerId} label={label} value="2024-07-20" />)
|
|
193
|
+
|
|
194
|
+
input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
195
|
+
expect(input.value).toBe('2024-07-20')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('works as uncontrolled component', () => {
|
|
199
|
+
const { container } = createDatepickerTest()
|
|
200
|
+
|
|
201
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
202
|
+
expect(input.value).toBe('')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('controlled range updates both inputs on rerender', () => {
|
|
206
|
+
const { container, rerender } = render(
|
|
207
|
+
<PktDatepicker id={datePickerId} label={label} range value={['2024-06-15', '2024-06-25']} />,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const inputs = container.querySelectorAll('input[type="date"]') as NodeListOf<HTMLInputElement>
|
|
211
|
+
expect(inputs[0].value).toBe('2024-06-15')
|
|
212
|
+
expect(inputs[1].value).toBe('2024-06-25')
|
|
213
|
+
|
|
214
|
+
rerender(
|
|
215
|
+
<PktDatepicker id={datePickerId} label={label} range value={['2024-07-01', '2024-07-31']} />,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
const updatedInputs = container.querySelectorAll('input[type="date"]') as NodeListOf<HTMLInputElement>
|
|
219
|
+
expect(updatedInputs[0].value).toBe('2024-07-01')
|
|
220
|
+
expect(updatedInputs[1].value).toBe('2024-07-31')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('controlled multiple updates tags on rerender', () => {
|
|
224
|
+
const { container, rerender } = render(
|
|
225
|
+
<PktDatepicker id={datePickerId} label={label} multiple value={['2024-06-15']} />,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
let tags = container.querySelectorAll('.pkt-date-tags .pkt-tag')
|
|
229
|
+
expect(tags.length).toBe(1)
|
|
230
|
+
|
|
231
|
+
rerender(
|
|
232
|
+
<PktDatepicker
|
|
233
|
+
id={datePickerId}
|
|
234
|
+
label={label}
|
|
235
|
+
multiple
|
|
236
|
+
value={['2024-06-15', '2024-06-20', '2024-06-25']}
|
|
237
|
+
/>,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
tags = container.querySelectorAll('.pkt-date-tags .pkt-tag')
|
|
241
|
+
expect(tags.length).toBe(3)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('calendarOpen prop controls calendar state', () => {
|
|
245
|
+
const { container, rerender } = render(
|
|
246
|
+
<PktDatepicker id={datePickerId} label={label} calendarOpen={false} />,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
let popup = container.querySelector('.pkt-calendar-popup')
|
|
250
|
+
expect(popup).toHaveAttribute('hidden')
|
|
251
|
+
|
|
252
|
+
rerender(<PktDatepicker id={datePickerId} label={label} calendarOpen={true} />)
|
|
253
|
+
|
|
254
|
+
popup = container.querySelector('.pkt-calendar-popup')
|
|
255
|
+
expect(popup).not.toHaveAttribute('hidden')
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe('Form integration', () => {
|
|
260
|
+
test('renders hidden input for form submission when name is set', () => {
|
|
261
|
+
const { container } = createDatepickerTest({
|
|
262
|
+
name: 'dateField',
|
|
263
|
+
value: '2024-06-15',
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const hiddenInput = container.querySelector('input[type="hidden"]') as HTMLInputElement
|
|
267
|
+
expect(hiddenInput).toBeInTheDocument()
|
|
268
|
+
expect(hiddenInput).toHaveAttribute('name', 'dateField')
|
|
269
|
+
expect(hiddenInput.value).toBe('2024-06-15')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('does not render hidden input when name is not set', () => {
|
|
273
|
+
const { container } = createDatepickerTest({
|
|
274
|
+
value: '2024-06-15',
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const hiddenInput = container.querySelector('input[type="hidden"]')
|
|
278
|
+
expect(hiddenInput).not.toBeInTheDocument()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('hidden input has comma-separated value for multiple', () => {
|
|
282
|
+
const { container } = createDatepickerTest({
|
|
283
|
+
name: 'dates',
|
|
284
|
+
multiple: true,
|
|
285
|
+
value: ['2024-06-15', '2024-06-20'],
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const hiddenInput = container.querySelector('input[type="hidden"]') as HTMLInputElement
|
|
289
|
+
expect(hiddenInput.value).toBe('2024-06-15,2024-06-20')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('hidden input has comma-separated value for range', () => {
|
|
293
|
+
const { container } = createDatepickerTest({
|
|
294
|
+
name: 'dateRange',
|
|
295
|
+
range: true,
|
|
296
|
+
value: ['2024-06-15', '2024-06-25'],
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
const hiddenInput = container.querySelector('input[type="hidden"]') as HTMLInputElement
|
|
300
|
+
expect(hiddenInput.value).toBe('2024-06-15,2024-06-25')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('submits form on Enter key press', () => {
|
|
304
|
+
const submitHandler = jest.fn((e: Event) => e.preventDefault())
|
|
305
|
+
const { container } = render(
|
|
306
|
+
<form onSubmit={submitHandler as unknown as React.FormEventHandler}>
|
|
307
|
+
<PktDatepicker id={datePickerId} label={label} value="2024-06-15" />
|
|
308
|
+
</form>,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
const input = container.querySelector('input[type="date"]') as HTMLInputElement
|
|
312
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
313
|
+
|
|
314
|
+
expect(submitHandler).toHaveBeenCalled()
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { PktIcon } from '../icon/Icon'
|
|
2
|
+
import { DateTags } from './DateTags'
|
|
3
|
+
import type { IDatepickerState } from './types'
|
|
4
|
+
|
|
5
|
+
export const DatepickerInputs = ({ state }: { state: IDatepickerState }) => {
|
|
6
|
+
const {
|
|
7
|
+
id,
|
|
8
|
+
inputId,
|
|
9
|
+
values,
|
|
10
|
+
dateformat,
|
|
11
|
+
multiple,
|
|
12
|
+
range,
|
|
13
|
+
showRangeLabels,
|
|
14
|
+
disabled,
|
|
15
|
+
readOnly,
|
|
16
|
+
required,
|
|
17
|
+
label,
|
|
18
|
+
name,
|
|
19
|
+
placeholder,
|
|
20
|
+
hasError,
|
|
21
|
+
helptext,
|
|
22
|
+
minStr,
|
|
23
|
+
maxStr,
|
|
24
|
+
inputType,
|
|
25
|
+
isIOSDevice,
|
|
26
|
+
strings,
|
|
27
|
+
inputClasses,
|
|
28
|
+
buttonClasses,
|
|
29
|
+
rangeLabelClasses,
|
|
30
|
+
datepickerInputsClasses,
|
|
31
|
+
isInputDisabled,
|
|
32
|
+
inputRef,
|
|
33
|
+
inputRefTo,
|
|
34
|
+
btnRef,
|
|
35
|
+
toggleCalendar,
|
|
36
|
+
handleFocus,
|
|
37
|
+
handleSingleInputChange,
|
|
38
|
+
handleRangeFromChange,
|
|
39
|
+
handleRangeToChange,
|
|
40
|
+
handleRangeBlur,
|
|
41
|
+
handleMultipleBlur,
|
|
42
|
+
handleTagRemoved,
|
|
43
|
+
handleSingleKeydown,
|
|
44
|
+
handleRangeFromKeydown,
|
|
45
|
+
handleRangeToKeydown,
|
|
46
|
+
handleMultipleKeydown,
|
|
47
|
+
restProps,
|
|
48
|
+
} = state
|
|
49
|
+
|
|
50
|
+
const renderCalendarButton = () => (
|
|
51
|
+
<button
|
|
52
|
+
ref={btnRef}
|
|
53
|
+
className={buttonClasses}
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={toggleCalendar}
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
aria-label={strings.calendar?.buttonAltText ?? 'Åpne kalender'}
|
|
58
|
+
>
|
|
59
|
+
<PktIcon name="calendar" />
|
|
60
|
+
<span className="pkt-btn__text">{strings.calendar?.buttonAltText ?? 'Åpne kalender'}</span>
|
|
61
|
+
</button>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (multiple) {
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<DateTags dates={values} dateformat={dateformat} idBase={id} strings={strings} onDateRemoved={handleTagRemoved} />
|
|
68
|
+
<div className="pkt-datepicker__inputs">
|
|
69
|
+
<div className="pkt-input__container">
|
|
70
|
+
<input
|
|
71
|
+
{...restProps}
|
|
72
|
+
ref={inputRef}
|
|
73
|
+
className={inputClasses}
|
|
74
|
+
type={inputType}
|
|
75
|
+
id={inputId}
|
|
76
|
+
name={name ?? id}
|
|
77
|
+
placeholder={placeholder}
|
|
78
|
+
readOnly={readOnly || (isIOSDevice && !readOnly)}
|
|
79
|
+
disabled={isInputDisabled}
|
|
80
|
+
aria-describedby={helptext ? `${id}-helptext` : undefined}
|
|
81
|
+
aria-invalid={hasError}
|
|
82
|
+
aria-errormessage={hasError ? `${id}-error` : undefined}
|
|
83
|
+
onClick={toggleCalendar}
|
|
84
|
+
onKeyDown={handleMultipleKeydown}
|
|
85
|
+
onFocus={handleFocus}
|
|
86
|
+
onBlur={handleMultipleBlur}
|
|
87
|
+
/>
|
|
88
|
+
{renderCalendarButton()}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (range) {
|
|
96
|
+
return (
|
|
97
|
+
<div className={datepickerInputsClasses}>
|
|
98
|
+
<div className="pkt-input__container">
|
|
99
|
+
{showRangeLabels && <div className="pkt-input-prefix">{strings.generic?.from ?? 'Fra'}</div>}
|
|
100
|
+
<input
|
|
101
|
+
{...restProps}
|
|
102
|
+
ref={inputRef}
|
|
103
|
+
className={inputClasses}
|
|
104
|
+
type={inputType}
|
|
105
|
+
id={inputId}
|
|
106
|
+
name={name ? `${name}-from` : `${id}-from`}
|
|
107
|
+
value={values[0] ?? ''}
|
|
108
|
+
min={minStr}
|
|
109
|
+
max={maxStr}
|
|
110
|
+
placeholder={placeholder}
|
|
111
|
+
readOnly={readOnly || (isIOSDevice && !readOnly)}
|
|
112
|
+
disabled={disabled}
|
|
113
|
+
required={required}
|
|
114
|
+
aria-label={`${label} ${strings.generic?.from ?? 'Fra'}`}
|
|
115
|
+
aria-describedby={helptext ? `${id}-helptext` : undefined}
|
|
116
|
+
aria-invalid={hasError}
|
|
117
|
+
aria-errormessage={hasError ? `${id}-error` : undefined}
|
|
118
|
+
onClick={toggleCalendar}
|
|
119
|
+
onKeyDown={handleRangeFromKeydown}
|
|
120
|
+
onFocus={handleFocus}
|
|
121
|
+
onBlur={handleRangeBlur}
|
|
122
|
+
onChange={handleRangeFromChange}
|
|
123
|
+
/>
|
|
124
|
+
<div className={rangeLabelClasses} id={`${id}-to-label`}>
|
|
125
|
+
{strings.generic?.to ?? 'Til'}
|
|
126
|
+
</div>
|
|
127
|
+
{!showRangeLabels && <div className="pkt-input-separator">–</div>}
|
|
128
|
+
<input
|
|
129
|
+
ref={inputRefTo}
|
|
130
|
+
className={inputClasses}
|
|
131
|
+
type={inputType}
|
|
132
|
+
id={`${id}-to`}
|
|
133
|
+
name={name ? `${name}-to` : `${id}-to`}
|
|
134
|
+
value={values[1] ?? ''}
|
|
135
|
+
min={minStr}
|
|
136
|
+
max={maxStr}
|
|
137
|
+
readOnly={readOnly || (isIOSDevice && !readOnly)}
|
|
138
|
+
disabled={disabled}
|
|
139
|
+
aria-label={`${label} ${strings.generic?.to ?? 'Til'}`}
|
|
140
|
+
aria-invalid={hasError}
|
|
141
|
+
onClick={toggleCalendar}
|
|
142
|
+
onKeyDown={handleRangeToKeydown}
|
|
143
|
+
onFocus={handleFocus}
|
|
144
|
+
onBlur={handleRangeBlur}
|
|
145
|
+
onChange={handleRangeToChange}
|
|
146
|
+
/>
|
|
147
|
+
{renderCalendarButton()}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Single mode
|
|
154
|
+
return (
|
|
155
|
+
<div className="pkt-datepicker__inputs">
|
|
156
|
+
<div className="pkt-input__container">
|
|
157
|
+
<input
|
|
158
|
+
{...restProps}
|
|
159
|
+
ref={inputRef}
|
|
160
|
+
className={inputClasses}
|
|
161
|
+
type={inputType}
|
|
162
|
+
id={inputId}
|
|
163
|
+
name={name ?? id}
|
|
164
|
+
value={values[0] ?? ''}
|
|
165
|
+
min={minStr}
|
|
166
|
+
max={maxStr}
|
|
167
|
+
placeholder={placeholder}
|
|
168
|
+
readOnly={readOnly || (isIOSDevice && !readOnly)}
|
|
169
|
+
disabled={disabled}
|
|
170
|
+
required={required}
|
|
171
|
+
aria-describedby={helptext ? `${id}-helptext` : undefined}
|
|
172
|
+
aria-invalid={hasError}
|
|
173
|
+
aria-errormessage={hasError ? `${id}-error` : undefined}
|
|
174
|
+
onClick={toggleCalendar}
|
|
175
|
+
onKeyDown={handleSingleKeydown}
|
|
176
|
+
onFocus={handleFocus}
|
|
177
|
+
onBlur={undefined}
|
|
178
|
+
onChange={handleSingleInputChange}
|
|
179
|
+
/>
|
|
180
|
+
{renderCalendarButton()}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
import { handleCalendarPosition } from 'shared-utils/datepicker-utils'
|
|
5
|
+
|
|
6
|
+
import { PktCalendar } from '../calendar/Calendar'
|
|
7
|
+
|
|
8
|
+
interface DatepickerPopupProps {
|
|
9
|
+
open: boolean
|
|
10
|
+
multiple?: boolean
|
|
11
|
+
range?: boolean
|
|
12
|
+
weeknumbers?: boolean
|
|
13
|
+
withcontrols?: boolean
|
|
14
|
+
selected?: string[]
|
|
15
|
+
earliest?: string | null
|
|
16
|
+
latest?: string | null
|
|
17
|
+
excludedates?: string[]
|
|
18
|
+
excludeweekdays?: string[]
|
|
19
|
+
maxMultiple?: number
|
|
20
|
+
currentmonth?: string | null
|
|
21
|
+
today?: string
|
|
22
|
+
inputRef?: React.RefObject<HTMLInputElement | null>
|
|
23
|
+
hasCounter?: boolean
|
|
24
|
+
onDateSelected: (selected: string[]) => void
|
|
25
|
+
onClose: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DatepickerPopup = ({
|
|
29
|
+
open,
|
|
30
|
+
multiple = false,
|
|
31
|
+
range = false,
|
|
32
|
+
weeknumbers = false,
|
|
33
|
+
withcontrols = false,
|
|
34
|
+
selected = [],
|
|
35
|
+
earliest,
|
|
36
|
+
latest,
|
|
37
|
+
excludedates,
|
|
38
|
+
excludeweekdays,
|
|
39
|
+
maxMultiple,
|
|
40
|
+
currentmonth,
|
|
41
|
+
today,
|
|
42
|
+
inputRef,
|
|
43
|
+
hasCounter = false,
|
|
44
|
+
onDateSelected,
|
|
45
|
+
onClose,
|
|
46
|
+
}: DatepickerPopupProps) => {
|
|
47
|
+
const popupRef = useRef<HTMLDivElement>(null)
|
|
48
|
+
const hasBeenOpenedRef = useRef(false)
|
|
49
|
+
|
|
50
|
+
if (open) {
|
|
51
|
+
hasBeenOpenedRef.current = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (open) {
|
|
56
|
+
handleCalendarPosition(popupRef.current, inputRef?.current ?? null, hasCounter)
|
|
57
|
+
}
|
|
58
|
+
}, [open, inputRef, hasCounter])
|
|
59
|
+
|
|
60
|
+
const popupClasses = ['pkt-calendar-popup', open ? 'show' : 'hide'].join(' ')
|
|
61
|
+
const shouldRenderCalendar = open || hasBeenOpenedRef.current
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
ref={popupRef}
|
|
66
|
+
className={popupClasses}
|
|
67
|
+
hidden={!open}
|
|
68
|
+
aria-hidden={!open}
|
|
69
|
+
>
|
|
70
|
+
{shouldRenderCalendar && (
|
|
71
|
+
<PktCalendar
|
|
72
|
+
multiple={multiple}
|
|
73
|
+
range={range}
|
|
74
|
+
weeknumbers={weeknumbers}
|
|
75
|
+
withcontrols={withcontrols}
|
|
76
|
+
selected={selected}
|
|
77
|
+
earliest={earliest}
|
|
78
|
+
latest={latest}
|
|
79
|
+
excludedates={excludedates}
|
|
80
|
+
excludeweekdays={excludeweekdays}
|
|
81
|
+
maxMultiple={maxMultiple}
|
|
82
|
+
currentmonth={currentmonth}
|
|
83
|
+
today={today}
|
|
84
|
+
onDateSelected={onDateSelected}
|
|
85
|
+
onClose={onClose}
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|