@planningcenter/tapestry-react 2.7.0 → 2.8.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 (39) hide show
  1. package/dist/cjs/Button/Button.js +10 -13
  2. package/dist/cjs/Button/Button.test.js +53 -21
  3. package/dist/cjs/Calendar/Calendar.js +30 -25
  4. package/dist/cjs/Combobox/ComboboxInput.js +41 -37
  5. package/dist/cjs/DateField/DateField.js +74 -47
  6. package/dist/cjs/DateField/parse.js +106 -0
  7. package/dist/cjs/DateField/parse.test.js +46 -0
  8. package/dist/cjs/DateField/useArrowKeysToNavigateCalendar.js +44 -0
  9. package/dist/cjs/DateField/useEditableDate.js +72 -0
  10. package/dist/cjs/Select/Select.test.js +74 -0
  11. package/dist/esm/Button/Button.js +10 -13
  12. package/dist/esm/Button/Button.test.js +58 -26
  13. package/dist/esm/Calendar/Calendar.js +30 -25
  14. package/dist/esm/Combobox/ComboboxInput.js +40 -37
  15. package/dist/esm/DateField/DateField.js +75 -48
  16. package/dist/esm/DateField/parse.js +93 -0
  17. package/dist/esm/DateField/parse.test.js +42 -0
  18. package/dist/esm/DateField/useArrowKeysToNavigateCalendar.js +36 -0
  19. package/dist/esm/DateField/useEditableDate.js +62 -0
  20. package/dist/esm/Select/Select.test.js +59 -0
  21. package/dist/types/Button/Button.d.ts +1 -1
  22. package/dist/types/DateField/DateField.d.ts +48 -0
  23. package/dist/types/DateField/parse.d.ts +17 -0
  24. package/dist/types/DateField/parse.test.d.ts +1 -0
  25. package/dist/types/DateField/useArrowKeysToNavigateCalendar.d.ts +8 -0
  26. package/dist/types/DateField/useEditableDate.d.ts +25 -0
  27. package/dist/types/Select/Select.test.d.ts +1 -0
  28. package/package.json +3 -3
  29. package/src/Button/Button.test.tsx +32 -8
  30. package/src/Button/Button.tsx +8 -9
  31. package/src/Calendar/Calendar.js +22 -17
  32. package/src/Combobox/ComboboxInput.js +76 -62
  33. package/src/DateField/DateField.mdx +15 -0
  34. package/src/DateField/{DateField.js → DateField.tsx} +96 -52
  35. package/src/DateField/parse.test.ts +76 -0
  36. package/src/DateField/parse.ts +92 -0
  37. package/src/DateField/useArrowKeysToNavigateCalendar.ts +54 -0
  38. package/src/DateField/useEditableDate.ts +81 -0
  39. 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
+ })