sanity-plugin-recurring-dates 1.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/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.esm.js +9173 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +9197 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
- package/sanity.json +8 -0
- package/src/components/CustomRule/CustomRule.tsx +230 -0
- package/src/components/CustomRule/Monthly.tsx +67 -0
- package/src/components/CustomRule/Weekly.tsx +62 -0
- package/src/components/CustomRule/index.tsx +1 -0
- package/src/components/DateInputs/CommonDateTimeInput.tsx +111 -0
- package/src/components/DateInputs/DateInput.tsx +81 -0
- package/src/components/DateInputs/DateTimeInput.tsx +122 -0
- package/src/components/DateInputs/base/DatePicker.tsx +34 -0
- package/src/components/DateInputs/base/DateTimeInput.tsx +104 -0
- package/src/components/DateInputs/base/LazyTextInput.tsx +77 -0
- package/src/components/DateInputs/base/calendar/Calendar.tsx +381 -0
- package/src/components/DateInputs/base/calendar/CalendarDay.tsx +47 -0
- package/src/components/DateInputs/base/calendar/CalendarMonth.tsx +52 -0
- package/src/components/DateInputs/base/calendar/YearInput.tsx +22 -0
- package/src/components/DateInputs/base/calendar/constants.ts +33 -0
- package/src/components/DateInputs/base/calendar/features.ts +4 -0
- package/src/components/DateInputs/base/calendar/utils.ts +34 -0
- package/src/components/DateInputs/index.ts +2 -0
- package/src/components/DateInputs/types.ts +4 -0
- package/src/components/DateInputs/utils.ts +4 -0
- package/src/components/RecurringDate.tsx +139 -0
- package/src/constants.ts +36 -0
- package/src/index.ts +2 -0
- package/src/plugin.tsx +19 -0
- package/src/schema/recurringDates.tsx +44 -0
- package/src/types.ts +27 -0
- package/src/utils.ts +16 -0
- package/v2-incompatible.js +11 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import {Box, Button, Flex, Grid, Select, Text, useForwardedRef} from '@sanity/ui'
|
|
2
|
+
import {ChevronLeftIcon, ChevronRightIcon} from '@sanity/icons'
|
|
3
|
+
import {addDays, addMonths, setDate, setHours, setMinutes, setMonth, setYear} from 'date-fns'
|
|
4
|
+
import {range} from 'lodash'
|
|
5
|
+
import React, {forwardRef, useCallback, useEffect} from 'react'
|
|
6
|
+
import {CalendarMonth} from './CalendarMonth'
|
|
7
|
+
import {ARROW_KEYS, HOURS_24, MONTH_NAMES, DEFAULT_TIME_PRESETS} from './constants'
|
|
8
|
+
import {features} from './features'
|
|
9
|
+
import {formatTime} from './utils'
|
|
10
|
+
import {YearInput} from './YearInput'
|
|
11
|
+
|
|
12
|
+
type CalendarProps = Omit<React.ComponentProps<'div'>, 'onSelect'> & {
|
|
13
|
+
selectTime?: boolean
|
|
14
|
+
selectedDate?: Date
|
|
15
|
+
timeStep?: number
|
|
16
|
+
onSelect: (date: Date) => void
|
|
17
|
+
focusedDate: Date
|
|
18
|
+
onFocusedDateChange: (index: Date) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// This is used to maintain focus on a child element of the calendar-grid between re-renders
|
|
22
|
+
// When using arrow keys to move focus from a day in one month to another we are setting focus at the button for the day
|
|
23
|
+
// after it has changed but *only* if we *already* had focus inside the calendar grid (e.g not if focus was on the "next
|
|
24
|
+
// year" button, or any of the other controls)
|
|
25
|
+
// When moving from the last day of a month that displays 6 weeks in the grid to a month that displays 5 weeks, current
|
|
26
|
+
// focus gets lost on render, so this provides us with a stable element to help us preserve focus on a child element of
|
|
27
|
+
// the calendar grid between re-renders
|
|
28
|
+
const PRESERVE_FOCUS_ELEMENT = (
|
|
29
|
+
<span
|
|
30
|
+
data-preserve-focus
|
|
31
|
+
style={{overflow: 'hidden', position: 'absolute', outline: 'none'}}
|
|
32
|
+
tabIndex={-1}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export const Calendar = forwardRef(function Calendar(
|
|
37
|
+
props: CalendarProps,
|
|
38
|
+
forwardedRef: React.ForwardedRef<HTMLDivElement>
|
|
39
|
+
) {
|
|
40
|
+
const {
|
|
41
|
+
selectTime,
|
|
42
|
+
onFocusedDateChange,
|
|
43
|
+
selectedDate = new Date(),
|
|
44
|
+
focusedDate = selectedDate,
|
|
45
|
+
timeStep = 1,
|
|
46
|
+
onSelect,
|
|
47
|
+
...restProps
|
|
48
|
+
} = props
|
|
49
|
+
|
|
50
|
+
const setFocusedDate = useCallback(
|
|
51
|
+
(date: Date) => onFocusedDateChange(date),
|
|
52
|
+
[onFocusedDateChange]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const setFocusedDateMonth = useCallback(
|
|
56
|
+
(month: number) => setFocusedDate(setDate(setMonth(focusedDate, month), 1)),
|
|
57
|
+
[focusedDate, setFocusedDate]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const handleFocusedMonthChange = useCallback(
|
|
61
|
+
(e: React.FormEvent<HTMLSelectElement>) => setFocusedDateMonth(Number(e.currentTarget.value)),
|
|
62
|
+
[setFocusedDateMonth]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const moveFocusedDate = useCallback(
|
|
66
|
+
(by: number) => setFocusedDate(addMonths(focusedDate, by)),
|
|
67
|
+
[focusedDate, setFocusedDate]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const setFocusedDateYear = useCallback(
|
|
71
|
+
(year: number) => setFocusedDate(setYear(focusedDate, year)),
|
|
72
|
+
[focusedDate, setFocusedDate]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const handleDateChange = useCallback(
|
|
76
|
+
(date: Date) => {
|
|
77
|
+
onSelect(setMinutes(setHours(date, selectedDate.getHours()), selectedDate.getMinutes()))
|
|
78
|
+
},
|
|
79
|
+
[onSelect, selectedDate]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const handleMinutesChange = useCallback(
|
|
83
|
+
(event: React.FormEvent<HTMLSelectElement>) => {
|
|
84
|
+
const m = Number(event.currentTarget.value)
|
|
85
|
+
onSelect(setMinutes(selectedDate, m))
|
|
86
|
+
},
|
|
87
|
+
[onSelect, selectedDate]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const handleHoursChange = useCallback(
|
|
91
|
+
(event: React.FormEvent<HTMLSelectElement>) => {
|
|
92
|
+
const m = Number(event.currentTarget.value)
|
|
93
|
+
onSelect(setHours(selectedDate, m))
|
|
94
|
+
},
|
|
95
|
+
[onSelect, selectedDate]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const handleTimeChange = useCallback(
|
|
99
|
+
(hours: number, mins: number) => {
|
|
100
|
+
onSelect(setHours(setMinutes(selectedDate, mins), hours))
|
|
101
|
+
},
|
|
102
|
+
[onSelect, selectedDate]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const ref = useForwardedRef(forwardedRef)
|
|
106
|
+
|
|
107
|
+
const focusCurrentWeekDay = useCallback(() => {
|
|
108
|
+
ref.current?.querySelector<HTMLElement>(`[data-focused="true"]`)?.focus()
|
|
109
|
+
}, [ref])
|
|
110
|
+
|
|
111
|
+
const handleKeyDown = useCallback(
|
|
112
|
+
(event: any) => {
|
|
113
|
+
if (!ARROW_KEYS.includes(event.key)) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
event.preventDefault()
|
|
117
|
+
if (event.target.hasAttribute('data-calendar-grid')) {
|
|
118
|
+
focusCurrentWeekDay()
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if (event.key === 'ArrowUp') {
|
|
122
|
+
onFocusedDateChange(addDays(focusedDate, -7))
|
|
123
|
+
}
|
|
124
|
+
if (event.key === 'ArrowDown') {
|
|
125
|
+
onFocusedDateChange(addDays(focusedDate, 7))
|
|
126
|
+
}
|
|
127
|
+
if (event.key === 'ArrowLeft') {
|
|
128
|
+
onFocusedDateChange(addDays(focusedDate, -1))
|
|
129
|
+
}
|
|
130
|
+
if (event.key === 'ArrowRight') {
|
|
131
|
+
onFocusedDateChange(addDays(focusedDate, 1))
|
|
132
|
+
}
|
|
133
|
+
// set focus temporarily on this element to make sure focus is still inside the calendar-grid after re-render
|
|
134
|
+
ref.current?.querySelector<HTMLElement>('[data-preserve-focus]')?.focus()
|
|
135
|
+
},
|
|
136
|
+
[ref, focusCurrentWeekDay, onFocusedDateChange, focusedDate]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
focusCurrentWeekDay()
|
|
141
|
+
}, [focusCurrentWeekDay])
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const currentFocusInCalendarGrid = document.activeElement?.matches(
|
|
145
|
+
'[data-calendar-grid], [data-calendar-grid] [data-preserve-focus]'
|
|
146
|
+
)
|
|
147
|
+
if (
|
|
148
|
+
// Only move focus if it's currently in the calendar grid
|
|
149
|
+
currentFocusInCalendarGrid
|
|
150
|
+
) {
|
|
151
|
+
focusCurrentWeekDay()
|
|
152
|
+
}
|
|
153
|
+
}, [ref, focusCurrentWeekDay, focusedDate])
|
|
154
|
+
|
|
155
|
+
const handleYesterdayClick = useCallback(
|
|
156
|
+
() => handleDateChange(addDays(new Date(), -1)),
|
|
157
|
+
[handleDateChange]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const handleTodayClick = useCallback(() => handleDateChange(new Date()), [handleDateChange])
|
|
161
|
+
|
|
162
|
+
const handleTomorrowClick = useCallback(
|
|
163
|
+
() => handleDateChange(addDays(new Date(), 1)),
|
|
164
|
+
[handleDateChange]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
const handleNowClick = useCallback(() => onSelect(new Date()), [onSelect])
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Box data-ui="Calendar" {...restProps} ref={ref}>
|
|
171
|
+
{/* Select date */}
|
|
172
|
+
<Box padding={2}>
|
|
173
|
+
{/* Day presets */}
|
|
174
|
+
{features.dayPresets && (
|
|
175
|
+
<Grid columns={3} data-ui="CalendaryDayPresets" gap={1}>
|
|
176
|
+
<Button text="Yesterday" mode="bleed" fontSize={1} onClick={handleYesterdayClick} />
|
|
177
|
+
<Button text="Today" mode="bleed" fontSize={1} onClick={handleTodayClick} />
|
|
178
|
+
<Button text="Tomorrow" mode="bleed" fontSize={1} onClick={handleTomorrowClick} />
|
|
179
|
+
</Grid>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Select month and year */}
|
|
183
|
+
<Flex>
|
|
184
|
+
<Box flex={1}>
|
|
185
|
+
<CalendarMonthSelect
|
|
186
|
+
moveFocusedDate={moveFocusedDate}
|
|
187
|
+
onChange={handleFocusedMonthChange}
|
|
188
|
+
value={focusedDate?.getMonth()}
|
|
189
|
+
/>
|
|
190
|
+
</Box>
|
|
191
|
+
<Box marginLeft={2}>
|
|
192
|
+
<CalendarYearSelect
|
|
193
|
+
moveFocusedDate={moveFocusedDate}
|
|
194
|
+
onChange={setFocusedDateYear}
|
|
195
|
+
value={focusedDate.getFullYear()}
|
|
196
|
+
/>
|
|
197
|
+
</Box>
|
|
198
|
+
</Flex>
|
|
199
|
+
|
|
200
|
+
{/* Selected month (grid of days) */}
|
|
201
|
+
<Box
|
|
202
|
+
data-calendar-grid
|
|
203
|
+
onKeyDown={handleKeyDown}
|
|
204
|
+
marginTop={2}
|
|
205
|
+
overflow="hidden"
|
|
206
|
+
tabIndex={0}
|
|
207
|
+
>
|
|
208
|
+
<CalendarMonth
|
|
209
|
+
date={focusedDate}
|
|
210
|
+
focused={focusedDate}
|
|
211
|
+
onSelect={handleDateChange}
|
|
212
|
+
selected={selectedDate}
|
|
213
|
+
/>
|
|
214
|
+
{PRESERVE_FOCUS_ELEMENT}
|
|
215
|
+
</Box>
|
|
216
|
+
</Box>
|
|
217
|
+
|
|
218
|
+
{/* Select time */}
|
|
219
|
+
{selectTime && (
|
|
220
|
+
<Box padding={2} style={{borderTop: '1px solid var(--card-border-color)'}}>
|
|
221
|
+
<Flex align="center">
|
|
222
|
+
<Flex align="center" flex={1}>
|
|
223
|
+
<Box>
|
|
224
|
+
<Select
|
|
225
|
+
aria-label="Select hour"
|
|
226
|
+
value={selectedDate?.getHours()}
|
|
227
|
+
onChange={handleHoursChange}
|
|
228
|
+
>
|
|
229
|
+
{HOURS_24.map((h) => (
|
|
230
|
+
<option key={h} value={h}>
|
|
231
|
+
{`${h}`.padStart(2, '0')}
|
|
232
|
+
</option>
|
|
233
|
+
))}
|
|
234
|
+
</Select>
|
|
235
|
+
</Box>
|
|
236
|
+
|
|
237
|
+
<Box paddingX={1}>
|
|
238
|
+
<Text>:</Text>
|
|
239
|
+
</Box>
|
|
240
|
+
|
|
241
|
+
<Box>
|
|
242
|
+
<Select
|
|
243
|
+
aria-label="Select minutes"
|
|
244
|
+
value={selectedDate?.getMinutes()}
|
|
245
|
+
onChange={handleMinutesChange}
|
|
246
|
+
>
|
|
247
|
+
{range(0, 60, timeStep).map((m) => (
|
|
248
|
+
<option key={m} value={m}>
|
|
249
|
+
{`${m}`.padStart(2, '0')}
|
|
250
|
+
</option>
|
|
251
|
+
))}
|
|
252
|
+
</Select>
|
|
253
|
+
</Box>
|
|
254
|
+
</Flex>
|
|
255
|
+
|
|
256
|
+
<Box marginLeft={2}>
|
|
257
|
+
<Button text="Set to current time" mode="bleed" onClick={handleNowClick} />
|
|
258
|
+
</Box>
|
|
259
|
+
</Flex>
|
|
260
|
+
|
|
261
|
+
{features.timePresets && (
|
|
262
|
+
<Flex direction="row" justify="center" align="center" style={{marginTop: 5}}>
|
|
263
|
+
{DEFAULT_TIME_PRESETS.map(([hours, minutes]) => {
|
|
264
|
+
return (
|
|
265
|
+
<CalendarTimePresetButton
|
|
266
|
+
key={`${hours}-${minutes}`}
|
|
267
|
+
hours={hours}
|
|
268
|
+
minutes={minutes}
|
|
269
|
+
onTimeChange={handleTimeChange}
|
|
270
|
+
selectedDate={selectedDate}
|
|
271
|
+
/>
|
|
272
|
+
)
|
|
273
|
+
})}
|
|
274
|
+
</Flex>
|
|
275
|
+
)}
|
|
276
|
+
</Box>
|
|
277
|
+
)}
|
|
278
|
+
</Box>
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
function CalendarTimePresetButton(props: {
|
|
283
|
+
hours: number
|
|
284
|
+
minutes: number
|
|
285
|
+
onTimeChange: (hours: number, minutes: number) => void
|
|
286
|
+
selectedDate: Date
|
|
287
|
+
}) {
|
|
288
|
+
const {hours, minutes, onTimeChange, selectedDate} = props
|
|
289
|
+
const formatted = formatTime(hours, minutes)
|
|
290
|
+
|
|
291
|
+
const handleClick = useCallback(() => {
|
|
292
|
+
onTimeChange(hours, minutes)
|
|
293
|
+
}, [hours, minutes, onTimeChange])
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<Button
|
|
297
|
+
text={formatted}
|
|
298
|
+
aria-label={`${formatted} on ${selectedDate.toDateString()}`}
|
|
299
|
+
mode="bleed"
|
|
300
|
+
fontSize={1}
|
|
301
|
+
onClick={handleClick}
|
|
302
|
+
/>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function CalendarMonthSelect(props: {
|
|
307
|
+
moveFocusedDate: (by: number) => void
|
|
308
|
+
onChange: (e: React.FormEvent<HTMLSelectElement>) => void
|
|
309
|
+
value?: number
|
|
310
|
+
}) {
|
|
311
|
+
const {moveFocusedDate, onChange, value} = props
|
|
312
|
+
|
|
313
|
+
const handlePrevMonthClick = useCallback(() => moveFocusedDate(-1), [moveFocusedDate])
|
|
314
|
+
|
|
315
|
+
const handleNextMonthClick = useCallback(() => moveFocusedDate(1), [moveFocusedDate])
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<Flex flex={1}>
|
|
319
|
+
<Button
|
|
320
|
+
aria-label="Go to previous month"
|
|
321
|
+
onClick={handlePrevMonthClick}
|
|
322
|
+
mode="bleed"
|
|
323
|
+
icon={ChevronLeftIcon}
|
|
324
|
+
paddingX={2}
|
|
325
|
+
radius={0}
|
|
326
|
+
/>
|
|
327
|
+
<Box flex={1}>
|
|
328
|
+
<Select radius={0} value={value} onChange={onChange}>
|
|
329
|
+
{MONTH_NAMES.map((m, i) => (
|
|
330
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
331
|
+
<option key={i} value={i}>
|
|
332
|
+
{m}
|
|
333
|
+
</option>
|
|
334
|
+
))}
|
|
335
|
+
</Select>
|
|
336
|
+
</Box>
|
|
337
|
+
<Button
|
|
338
|
+
aria-label="Go to next month"
|
|
339
|
+
mode="bleed"
|
|
340
|
+
icon={ChevronRightIcon}
|
|
341
|
+
onClick={handleNextMonthClick}
|
|
342
|
+
paddingX={2}
|
|
343
|
+
radius={0}
|
|
344
|
+
/>
|
|
345
|
+
</Flex>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function CalendarYearSelect(props: {
|
|
350
|
+
moveFocusedDate: (by: number) => void
|
|
351
|
+
onChange: (year: number) => void
|
|
352
|
+
value?: number
|
|
353
|
+
}) {
|
|
354
|
+
const {moveFocusedDate, onChange, value} = props
|
|
355
|
+
|
|
356
|
+
const handlePrevYearClick = useCallback(() => moveFocusedDate(-12), [moveFocusedDate])
|
|
357
|
+
|
|
358
|
+
const handleNextYearClick = useCallback(() => moveFocusedDate(12), [moveFocusedDate])
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<Flex>
|
|
362
|
+
<Button
|
|
363
|
+
aria-label="Previous year"
|
|
364
|
+
onClick={handlePrevYearClick}
|
|
365
|
+
mode="bleed"
|
|
366
|
+
icon={ChevronLeftIcon}
|
|
367
|
+
paddingX={2}
|
|
368
|
+
radius={0}
|
|
369
|
+
/>
|
|
370
|
+
<YearInput value={value} onChange={onChange} radius={0} style={{width: 65}} />
|
|
371
|
+
<Button
|
|
372
|
+
aria-label="Next year"
|
|
373
|
+
onClick={handleNextYearClick}
|
|
374
|
+
mode="bleed"
|
|
375
|
+
icon={ChevronRightIcon}
|
|
376
|
+
paddingX={2}
|
|
377
|
+
radius={0}
|
|
378
|
+
/>
|
|
379
|
+
</Flex>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {Card, Text} from '@sanity/ui'
|
|
2
|
+
import React, {useCallback} from 'react'
|
|
3
|
+
|
|
4
|
+
interface CalendarDayProps {
|
|
5
|
+
date: Date
|
|
6
|
+
focused?: boolean
|
|
7
|
+
onSelect: (date: Date) => void
|
|
8
|
+
isCurrentMonth?: boolean
|
|
9
|
+
isToday: boolean
|
|
10
|
+
selected?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CalendarDay(props: CalendarDayProps) {
|
|
14
|
+
const {date, focused, isCurrentMonth, isToday, onSelect, selected} = props
|
|
15
|
+
|
|
16
|
+
const handleClick = useCallback(() => {
|
|
17
|
+
onSelect(date)
|
|
18
|
+
}, [date, onSelect])
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div aria-selected={selected} data-ui="CalendarDay">
|
|
22
|
+
<Card
|
|
23
|
+
aria-label={date.toDateString()}
|
|
24
|
+
aria-pressed={selected}
|
|
25
|
+
as="button"
|
|
26
|
+
__unstable_focusRing
|
|
27
|
+
data-weekday
|
|
28
|
+
data-focused={focused ? 'true' : ''}
|
|
29
|
+
role="button"
|
|
30
|
+
tabIndex={-1}
|
|
31
|
+
onClick={handleClick}
|
|
32
|
+
padding={3}
|
|
33
|
+
radius={2}
|
|
34
|
+
selected={selected}
|
|
35
|
+
tone={isToday || selected ? 'primary' : 'default'}
|
|
36
|
+
>
|
|
37
|
+
<Text
|
|
38
|
+
muted={!selected && !isCurrentMonth}
|
|
39
|
+
style={{textAlign: 'center'}}
|
|
40
|
+
weight={isCurrentMonth ? 'medium' : 'regular'}
|
|
41
|
+
>
|
|
42
|
+
{date.getDate()}
|
|
43
|
+
</Text>
|
|
44
|
+
</Card>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {Box, Grid, Text} from '@sanity/ui'
|
|
2
|
+
import {isSameDay, isSameMonth} from 'date-fns'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import {CalendarDay} from './CalendarDay'
|
|
5
|
+
import {WEEK_DAY_NAMES} from './constants'
|
|
6
|
+
import {getWeeksOfMonth} from './utils'
|
|
7
|
+
|
|
8
|
+
interface CalendarMonthProps {
|
|
9
|
+
date: Date
|
|
10
|
+
focused?: Date
|
|
11
|
+
selected?: Date
|
|
12
|
+
onSelect: (date: Date) => void
|
|
13
|
+
hidden?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function CalendarMonth(props: CalendarMonthProps) {
|
|
17
|
+
return (
|
|
18
|
+
<Box aria-hidden={props.hidden || false} data-ui="CalendarMonth">
|
|
19
|
+
<Grid gap={1} style={{gridTemplateColumns: 'repeat(7, minmax(44px, 46px))'}}>
|
|
20
|
+
{WEEK_DAY_NAMES.map((weekday) => (
|
|
21
|
+
<Box key={weekday} paddingY={2}>
|
|
22
|
+
<Text size={1} weight="medium" style={{textAlign: 'center'}}>
|
|
23
|
+
{weekday}
|
|
24
|
+
</Text>
|
|
25
|
+
</Box>
|
|
26
|
+
))}
|
|
27
|
+
|
|
28
|
+
{getWeeksOfMonth(props.date).map((week, weekIdx) =>
|
|
29
|
+
week.days.map((date, dayIdx) => {
|
|
30
|
+
const focused = props.focused && isSameDay(date, props.focused)
|
|
31
|
+
const selected = props.selected && isSameDay(date, props.selected)
|
|
32
|
+
const isToday = isSameDay(date, new Date())
|
|
33
|
+
const isCurrentMonth = props.focused && isSameMonth(date, props.focused)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<CalendarDay
|
|
37
|
+
date={date}
|
|
38
|
+
focused={focused}
|
|
39
|
+
isCurrentMonth={isCurrentMonth}
|
|
40
|
+
isToday={isToday}
|
|
41
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
42
|
+
key={`${weekIdx}-${dayIdx}`}
|
|
43
|
+
onSelect={props.onSelect}
|
|
44
|
+
selected={selected}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
)}
|
|
49
|
+
</Grid>
|
|
50
|
+
</Box>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {TextInput} from '@sanity/ui'
|
|
3
|
+
import {LazyTextInput} from '../LazyTextInput'
|
|
4
|
+
|
|
5
|
+
type Props = Omit<React.ComponentProps<typeof TextInput>, 'onChange' | 'value'> & {
|
|
6
|
+
value?: number
|
|
7
|
+
onChange: (year: number) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const YearInput = ({onChange, ...props}: Props) => {
|
|
11
|
+
const handleChange = React.useCallback(
|
|
12
|
+
(event: React.FocusEvent<HTMLInputElement> | React.ChangeEvent<HTMLInputElement>) => {
|
|
13
|
+
const numericValue = parseInt(event.currentTarget.value, 10)
|
|
14
|
+
if (!isNaN(numericValue)) {
|
|
15
|
+
onChange(numericValue)
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
[onChange]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return <LazyTextInput {...props} onChange={handleChange} inputMode="numeric" />
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {range} from 'lodash'
|
|
2
|
+
|
|
3
|
+
export const MONTH_NAMES = [
|
|
4
|
+
'January',
|
|
5
|
+
'February',
|
|
6
|
+
'March',
|
|
7
|
+
'April',
|
|
8
|
+
'May',
|
|
9
|
+
'June',
|
|
10
|
+
'July',
|
|
11
|
+
'August',
|
|
12
|
+
'September',
|
|
13
|
+
'October',
|
|
14
|
+
'November',
|
|
15
|
+
'December',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export const WEEK_DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
19
|
+
|
|
20
|
+
export const HOURS_24 = range(0, 24)
|
|
21
|
+
|
|
22
|
+
export const ARROW_KEYS = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_TIME_PRESETS = [
|
|
25
|
+
[0, 0],
|
|
26
|
+
[6, 0],
|
|
27
|
+
[12, 0],
|
|
28
|
+
[18, 0],
|
|
29
|
+
[23, 59],
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
// all weekdays except first
|
|
33
|
+
export const TAIL_WEEKDAYS = [1, 2, 3, 4, 5, 6]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {addDays, eachWeekOfInterval, getWeek, lastDayOfMonth, startOfMonth} from 'date-fns'
|
|
2
|
+
|
|
3
|
+
import {TAIL_WEEKDAYS} from './constants'
|
|
4
|
+
|
|
5
|
+
export const getWeekStartsOfMonth = (date: Date): Date[] => {
|
|
6
|
+
const firstDay = startOfMonth(date)
|
|
7
|
+
return eachWeekOfInterval({
|
|
8
|
+
start: firstDay,
|
|
9
|
+
end: lastDayOfMonth(firstDay),
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const getWeekDaysFromWeekStarts = (weekStarts: Date[]): Date[][] => {
|
|
14
|
+
return weekStarts.map((weekStart) => [
|
|
15
|
+
weekStart,
|
|
16
|
+
...TAIL_WEEKDAYS.map((d) => addDays(weekStart, d)),
|
|
17
|
+
])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Week = {
|
|
21
|
+
number: number
|
|
22
|
+
days: Date[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const getWeeksOfMonth = (date: Date): Week[] =>
|
|
26
|
+
getWeekDaysFromWeekStarts(getWeekStartsOfMonth(date)).map(
|
|
27
|
+
(days): Week => ({
|
|
28
|
+
number: getWeek(days[0]),
|
|
29
|
+
days,
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
export const formatTime = (hours: number, minutes: number): string =>
|
|
34
|
+
`${`${hours}`.padStart(2, '0')}:${`${minutes}`.padStart(2, '0')}`
|