@planningcenter/tapestry-react 2.7.0 → 2.8.1
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/dist/cjs/Button/Button.js +10 -13
- package/dist/cjs/Button/Button.test.js +53 -21
- package/dist/cjs/Calendar/Calendar.js +30 -25
- package/dist/cjs/Combobox/ComboboxInput.js +41 -37
- package/dist/cjs/DateField/DateField.js +78 -47
- package/dist/cjs/DateField/parse.js +106 -0
- package/dist/cjs/DateField/parse.test.js +46 -0
- package/dist/cjs/DateField/useArrowKeysToNavigateCalendar.js +44 -0
- package/dist/cjs/DateField/useEditableDate.js +72 -0
- package/dist/cjs/Select/Select.test.js +74 -0
- package/dist/esm/Button/Button.js +10 -13
- package/dist/esm/Button/Button.test.js +58 -26
- package/dist/esm/Calendar/Calendar.js +30 -25
- package/dist/esm/Combobox/ComboboxInput.js +40 -37
- package/dist/esm/DateField/DateField.js +79 -48
- package/dist/esm/DateField/parse.js +93 -0
- package/dist/esm/DateField/parse.test.js +42 -0
- package/dist/esm/DateField/useArrowKeysToNavigateCalendar.js +36 -0
- package/dist/esm/DateField/useEditableDate.js +62 -0
- package/dist/esm/Select/Select.test.js +59 -0
- package/dist/types/Button/Button.d.ts +1 -1
- package/dist/types/DateField/DateField.d.ts +48 -0
- package/dist/types/DateField/parse.d.ts +17 -0
- package/dist/types/DateField/parse.test.d.ts +1 -0
- package/dist/types/DateField/useArrowKeysToNavigateCalendar.d.ts +8 -0
- package/dist/types/DateField/useEditableDate.d.ts +25 -0
- package/dist/types/Select/Select.test.d.ts +1 -0
- package/package.json +3 -3
- package/src/Button/Button.test.tsx +32 -8
- package/src/Button/Button.tsx +8 -9
- package/src/Calendar/Calendar.js +22 -17
- package/src/Combobox/ComboboxInput.js +76 -62
- package/src/DateField/DateField.mdx +15 -0
- package/src/DateField/{DateField.js → DateField.tsx} +104 -52
- package/src/DateField/parse.test.ts +76 -0
- package/src/DateField/parse.ts +92 -0
- package/src/DateField/useArrowKeysToNavigateCalendar.ts +54 -0
- package/src/DateField/useEditableDate.ts +81 -0
- package/src/Select/Select.test.tsx +58 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const monthDayYearFormat = /^(0?[1-9]|1[0-2]|(?:(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*))(?:-|\/|\s)(0?[1-9]|[1-2][0-9]|3[0-1])(?:-|\/|,\s)(\d{4})$/i
|
|
2
|
+
const dayMonthYearFormat = /^(0?[1-9]|[1-2][0-9]|3[0-1])(?:-|\/|\s)(0?[1-9]|1[0-2]|(?:(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*))(?:-|\/|,\s)(\d{4})$/i
|
|
3
|
+
const yearMonthDayFormat = /^(\d{4})[-/](0?[1-9]|1[0-2])[-/](0?[1-9]|[1-2][0-9]|3[0-1])$/
|
|
4
|
+
|
|
5
|
+
interface Params {
|
|
6
|
+
/**
|
|
7
|
+
* The string we want to parse into a date object
|
|
8
|
+
*/
|
|
9
|
+
date: string
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format hint for parser
|
|
13
|
+
* Helps us know if we should parse day/month/year or month/day/year
|
|
14
|
+
*
|
|
15
|
+
* Should adhere to [date-fns spec](https://date-fns.org/v2.0.0-alpha.9/docs/format).
|
|
16
|
+
*/
|
|
17
|
+
format?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const parseDate = ({ date, format }: Params): Date | null => {
|
|
21
|
+
try {
|
|
22
|
+
const { year, month, day } = parseDateIntoObject(date, format)
|
|
23
|
+
return new Date(year, month - 1, day)
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const isValidDate = (date: string) => {
|
|
30
|
+
return monthDayYearFormat.test(date) || yearMonthDayFormat.test(date)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const parseMonth = (monthString: string): number => {
|
|
34
|
+
const months = {
|
|
35
|
+
jan: 1,
|
|
36
|
+
feb: 2,
|
|
37
|
+
mar: 3,
|
|
38
|
+
apr: 4,
|
|
39
|
+
may: 5,
|
|
40
|
+
jun: 6,
|
|
41
|
+
jul: 7,
|
|
42
|
+
aug: 8,
|
|
43
|
+
sep: 9,
|
|
44
|
+
oct: 10,
|
|
45
|
+
nov: 11,
|
|
46
|
+
dec: 12,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const normalizedString = monthString.toLowerCase().replace(/^0/, '').slice(0, 3)
|
|
50
|
+
|
|
51
|
+
if (/^\d+$/.test(normalizedString)) {
|
|
52
|
+
const month = parseInt(normalizedString, 10)
|
|
53
|
+
if (month >= 1 && month <= 12) {
|
|
54
|
+
return month
|
|
55
|
+
}
|
|
56
|
+
} else if (normalizedString in months) {
|
|
57
|
+
return months[normalizedString]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(`Invalid month string: ${monthString}`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const parseDateIntoObject = (date: string, format?: string) => {
|
|
64
|
+
if (format?.match(/d.*M/) && dayMonthYearFormat.test(date)) {
|
|
65
|
+
const { 1: day, 2: month, 3: year } = dayMonthYearFormat.exec(date)
|
|
66
|
+
return {
|
|
67
|
+
year: parseInt(year, 10),
|
|
68
|
+
month: parseMonth(month),
|
|
69
|
+
day: parseInt(day, 10),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (monthDayYearFormat.test(date)) {
|
|
74
|
+
const { 1: month, 2: day, 3: year } = monthDayYearFormat.exec(date)
|
|
75
|
+
return {
|
|
76
|
+
year: parseInt(year, 10),
|
|
77
|
+
month: parseMonth(month),
|
|
78
|
+
day: parseInt(day, 10),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (yearMonthDayFormat.test(date)) {
|
|
83
|
+
const { 1: year, 2: month, 3: day } = yearMonthDayFormat.exec(date)
|
|
84
|
+
return {
|
|
85
|
+
year: parseInt(year, 10),
|
|
86
|
+
month: parseMonth(month),
|
|
87
|
+
day: parseInt(day, 10),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new Error(`Invalid date: ${date}`)
|
|
92
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useCallback } from "react"
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
date: Date
|
|
5
|
+
calendarIsOpen: boolean
|
|
6
|
+
openCalendar: () => void
|
|
7
|
+
onChange: (date: Date) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useArrowKeysToNavigateCalendar = ({
|
|
11
|
+
date,
|
|
12
|
+
calendarIsOpen,
|
|
13
|
+
openCalendar,
|
|
14
|
+
onChange,
|
|
15
|
+
}: Props) => {
|
|
16
|
+
const incrementDate = useCallback(
|
|
17
|
+
(by: number): void => {
|
|
18
|
+
if (!date) return
|
|
19
|
+
|
|
20
|
+
const newDate = new Date(date)
|
|
21
|
+
newDate.setDate(newDate.getDate() + by)
|
|
22
|
+
onChange(newDate)
|
|
23
|
+
},
|
|
24
|
+
[date, onChange]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const handleInputKeyDown = useCallback(
|
|
28
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
29
|
+
if (calendarIsOpen) {
|
|
30
|
+
if (event.key === 'ArrowUp') {
|
|
31
|
+
event.preventDefault()
|
|
32
|
+
incrementDate(-7)
|
|
33
|
+
} else if (event.key === 'ArrowDown') {
|
|
34
|
+
event.preventDefault()
|
|
35
|
+
incrementDate(7)
|
|
36
|
+
} else if (event.key === 'ArrowLeft') {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
incrementDate(-1)
|
|
39
|
+
} else if (event.key === 'ArrowRight') {
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
incrementDate(1)
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
if (event.key === 'ArrowDown') {
|
|
45
|
+
event.preventDefault()
|
|
46
|
+
openCalendar()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
[calendarIsOpen, openCalendar, incrementDate]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return handleInputKeyDown
|
|
54
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react"
|
|
2
|
+
import { format } from 'date-fns'
|
|
3
|
+
import { parseDate } from "./parse"
|
|
4
|
+
|
|
5
|
+
interface Params {
|
|
6
|
+
/**
|
|
7
|
+
* The currently selected date
|
|
8
|
+
*/
|
|
9
|
+
date: Date
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format the displayed date using date-fns [format](https://date-fns.org/v2.0.0-alpha.9/docs/format) function.
|
|
13
|
+
*/
|
|
14
|
+
dateFormat: string
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Custom function that validates date
|
|
18
|
+
*/
|
|
19
|
+
dateValidator: (date: Date) => boolean
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Called when a valid date is entered
|
|
23
|
+
*/
|
|
24
|
+
onChange: (date: Date) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const useEditableDate = ({ date, dateFormat, dateValidator, onChange }: Params) => {
|
|
28
|
+
const [keyBuffer, setKeyBuffer] = useState<string>()
|
|
29
|
+
const [invalidKeyBuffer, setInvalidKeyBuffer] = useState(false)
|
|
30
|
+
|
|
31
|
+
const formattedDate = useMemo(() => {
|
|
32
|
+
if (keyBuffer !== undefined) {
|
|
33
|
+
return keyBuffer
|
|
34
|
+
} else {
|
|
35
|
+
return date ? format(date, dateFormat) : ''
|
|
36
|
+
}
|
|
37
|
+
}, [date, dateFormat, keyBuffer])
|
|
38
|
+
|
|
39
|
+
const setKeyBufferAndValidate = useCallback(
|
|
40
|
+
(value: string) => {
|
|
41
|
+
setKeyBuffer(value)
|
|
42
|
+
if (
|
|
43
|
+
value &&
|
|
44
|
+
!dateValidator(parseDate({ date: value, format: dateFormat }))
|
|
45
|
+
) {
|
|
46
|
+
setInvalidKeyBuffer(true)
|
|
47
|
+
} else {
|
|
48
|
+
setInvalidKeyBuffer(false)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[dateFormat, dateValidator]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const setDate = useCallback(
|
|
55
|
+
(date: Date | string) => {
|
|
56
|
+
let newDate: Date
|
|
57
|
+
|
|
58
|
+
if (typeof date === 'string') {
|
|
59
|
+
setKeyBufferAndValidate(date)
|
|
60
|
+
newDate = parseDate({ date, format: dateFormat })
|
|
61
|
+
} else {
|
|
62
|
+
setKeyBufferAndValidate(undefined)
|
|
63
|
+
newDate = date
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (dateValidator(newDate) && onChange) onChange(newDate)
|
|
67
|
+
},
|
|
68
|
+
[onChange, setKeyBufferAndValidate, dateValidator, dateFormat]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const clearKeyBuffer = useCallback(() => {
|
|
72
|
+
setKeyBufferAndValidate(undefined)
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
formattedDate,
|
|
77
|
+
setDate,
|
|
78
|
+
clearKeyBuffer,
|
|
79
|
+
invalidKeyBuffer,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import userEvent from '@testing-library/user-event'
|
|
4
|
+
import Select from '.'
|
|
5
|
+
import { ThemeProvider } from '../ThemeProvider/ThemeProvider'
|
|
6
|
+
|
|
7
|
+
const people = [
|
|
8
|
+
{
|
|
9
|
+
first: 'Charlie',
|
|
10
|
+
last: 'Brown',
|
|
11
|
+
twitter: 'dancounsell',
|
|
12
|
+
active: true,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
first: 'Charlotte',
|
|
16
|
+
last: 'White',
|
|
17
|
+
twitter: 'mtnmissy',
|
|
18
|
+
active: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
first: 'John',
|
|
22
|
+
last: 'James',
|
|
23
|
+
twitter: 'miller',
|
|
24
|
+
active: false,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
first: 'Travis',
|
|
28
|
+
last: 'Arnold',
|
|
29
|
+
twitter: 'souporserious',
|
|
30
|
+
active: true,
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const selectMock = jest.fn()
|
|
35
|
+
|
|
36
|
+
test('can click to select item from list', async () => {
|
|
37
|
+
jest.useFakeTimers()
|
|
38
|
+
|
|
39
|
+
render(
|
|
40
|
+
<ThemeProvider>
|
|
41
|
+
<Select onChange={selectMock}>
|
|
42
|
+
{people.map(p => (
|
|
43
|
+
<Select.Option value={p.twitter} key={p.twitter}>{p.first} {p.last}</Select.Option>
|
|
44
|
+
))}
|
|
45
|
+
</Select>
|
|
46
|
+
</ThemeProvider>
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
userEvent.click(screen.getByRole('button'))
|
|
50
|
+
jest.runAllTimers()
|
|
51
|
+
userEvent.click(screen.getByText('Travis Arnold'))
|
|
52
|
+
|
|
53
|
+
expect(selectMock).toHaveBeenCalledWith(
|
|
54
|
+
{ selectedValue: 'souporserious', value: 'souporserious' }
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
jest.useRealTimers()
|
|
58
|
+
})
|