@planningcenter/tapestry-react 2.6.2 → 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 (46) hide show
  1. package/dist/cjs/Button/Button.js +12 -3
  2. package/dist/cjs/Button/Button.test.js +67 -14
  3. package/dist/cjs/Calendar/Calendar.js +30 -25
  4. package/dist/cjs/Combobox/ComboboxInput.js +41 -37
  5. package/dist/cjs/DataTable/DataTable.js +3 -2
  6. package/dist/cjs/DateField/DateField.js +74 -47
  7. package/dist/cjs/DateField/parse.js +106 -0
  8. package/dist/cjs/DateField/parse.test.js +46 -0
  9. package/dist/cjs/DateField/useArrowKeysToNavigateCalendar.js +44 -0
  10. package/dist/cjs/DateField/useEditableDate.js +72 -0
  11. package/dist/cjs/Select/Select.test.js +74 -0
  12. package/dist/cjs/system/colors/colors.js +18 -15
  13. package/dist/esm/Button/Button.js +12 -3
  14. package/dist/esm/Button/Button.test.js +78 -19
  15. package/dist/esm/Calendar/Calendar.js +30 -25
  16. package/dist/esm/Combobox/ComboboxInput.js +40 -37
  17. package/dist/esm/DataTable/DataTable.js +3 -2
  18. package/dist/esm/DateField/DateField.js +75 -48
  19. package/dist/esm/DateField/parse.js +93 -0
  20. package/dist/esm/DateField/parse.test.js +42 -0
  21. package/dist/esm/DateField/useArrowKeysToNavigateCalendar.js +36 -0
  22. package/dist/esm/DateField/useEditableDate.js +62 -0
  23. package/dist/esm/Select/Select.test.js +59 -0
  24. package/dist/esm/system/colors/colors.js +18 -15
  25. package/dist/types/Button/Button.d.ts +1 -1
  26. package/dist/types/DateField/DateField.d.ts +48 -0
  27. package/dist/types/DateField/parse.d.ts +17 -0
  28. package/dist/types/DateField/parse.test.d.ts +1 -0
  29. package/dist/types/DateField/useArrowKeysToNavigateCalendar.d.ts +8 -0
  30. package/dist/types/DateField/useEditableDate.d.ts +25 -0
  31. package/dist/types/Select/Select.test.d.ts +1 -0
  32. package/package.json +5 -6
  33. package/src/Button/Button.test.tsx +39 -1
  34. package/src/Button/Button.tsx +8 -3
  35. package/src/Calendar/Calendar.js +22 -17
  36. package/src/Combobox/ComboboxInput.js +76 -62
  37. package/src/DataTable/DataTable.js +2 -1
  38. package/src/DateField/DateField.mdx +15 -0
  39. package/src/DateField/{DateField.js → DateField.tsx} +96 -52
  40. package/src/DateField/parse.test.ts +76 -0
  41. package/src/DateField/parse.ts +92 -0
  42. package/src/DateField/useArrowKeysToNavigateCalendar.ts +54 -0
  43. package/src/DateField/useEditableDate.ts +81 -0
  44. package/src/Icon/Icon.mdx +12 -11
  45. package/src/Select/Select.test.tsx +58 -0
  46. package/src/system/colors/colors.js +18 -15
@@ -0,0 +1,76 @@
1
+ import { format } from 'date-fns'
2
+ import { parseDate, isValidDate, parseMonth } from './parse'
3
+
4
+ describe('isValidDate', () => {
5
+ const validDates = [
6
+ 'jun 6, 2022',
7
+ 'June-06-2022',
8
+ '6-6-2022',
9
+ '6/6/2022',
10
+ '06-06-2022',
11
+ '2022-06-26', // year, month, day
12
+ '2022/6/1', // year, month, day
13
+ ]
14
+
15
+ validDates.forEach((date) => {
16
+ it(`returns true for "${date}"`, () => {
17
+ expect(isValidDate(date)).toBe(true)
18
+ })
19
+ })
20
+
21
+ const invalidDates = [
22
+ 'June-06-22',
23
+ '13-6-2022',
24
+ 'June 6 2022',
25
+ '2022/6/1/1',
26
+ '2022-15-06',
27
+ ]
28
+
29
+ invalidDates.forEach((date) => {
30
+ it(`returns false for "${date}"`, () => {
31
+ expect(isValidDate(date)).toBe(false)
32
+ })
33
+ })
34
+ })
35
+
36
+ describe('parseDate', () => {
37
+ const dates = [
38
+ ['January 31, 2022', 'January 31, 2022'],
39
+ ['jun 9, 2022', 'June 09, 2022'],
40
+ ['June-09-2022', 'June 09, 2022'],
41
+ ['6-9-2022', 'June 09, 2022'],
42
+ ['6/9/2022', 'June 09, 2022'],
43
+ ['06-09-2022', 'June 09, 2022'],
44
+ ['2022-06-26', 'June 26, 2022'],
45
+ ['2022/6/1', 'June 01, 2022', 'MMMM dd, yyyy'],
46
+ ['15/6/2022', 'June 15, 2022', 'dd/MM/YYYY'],
47
+ ['1/6/2022', 'June 01, 2022', 'dd MMMM, yyyy'],
48
+ ['15-aug-2023', 'August 15, 2023', 'dd MMMM, yyyy'],
49
+ ['15 August, 2023', 'August 15, 2023', 'dd MMMM, yyyy'],
50
+ ]
51
+
52
+ dates.forEach(([date, expected, dateFormat]) => {
53
+ it(`returns ${expected} for "${date}"`, () => {
54
+ expect(
55
+ format(parseDate({ date, format: dateFormat }), 'MMMM dd, yyyy')
56
+ ).toEqual(expected)
57
+ })
58
+ })
59
+ })
60
+
61
+ describe('parseMonth', () => {
62
+ const months: [string, number][] = [
63
+ ['jan', 1],
64
+ ['feb', 2],
65
+ ['December', 12],
66
+ ['2', 2],
67
+ ['12', 12],
68
+ ['05', 5],
69
+ ]
70
+
71
+ months.forEach(([month, expected]) => {
72
+ it(`returns ${expected} for "${month}"`, () => {
73
+ expect(parseMonth(month)).toBe(expected)
74
+ })
75
+ })
76
+ })
@@ -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
+ }
package/src/Icon/Icon.mdx CHANGED
@@ -68,8 +68,9 @@ Preview all of the available icon sets and their icon's names from `@planningcen
68
68
 
69
69
  ```jsx live
70
70
  render(() => {
71
- const [value, setValue] = React.useState('')
72
- const [appName, setAppName] = React.useState('general')
71
+ const iconSets = Object.keys(icons).filter(icons => icons !== "tapestry")
72
+ const [searchValue, setSearchValue] = React.useState('')
73
+ const [selectedIconSet, setSelectedIconSet] = React.useState('general')
73
74
 
74
75
  return (
75
76
  <StackView grow={1}>
@@ -77,12 +78,12 @@ render(() => {
77
78
  <Select
78
79
  basis={26}
79
80
  emptyValue="Choose icon set"
80
- onChange={(event) => setAppName(event.value)}
81
- defaultValue={appName}
81
+ onChange={(event) => setSelectedIconSet(event.value)}
82
+ defaultValue={selectedIconSet}
82
83
  >
83
- {appNames.map((appName) => (
84
- <Select.Option key={appName} value={appName}>
85
- {appName}
84
+ {iconSets.map((icons) => (
85
+ <Select.Option key={icons} value={icons}>
86
+ {icons}
86
87
  </Select.Option>
87
88
  ))}
88
89
  </Select>
@@ -91,14 +92,14 @@ render(() => {
91
92
  autoFocus
92
93
  renderLeft={<Icon name="general.search" />}
93
94
  placeholder="Search by icon name"
94
- value={value}
95
- onChange={(e) => setValue(e.target.value)}
95
+ value={searchValue}
96
+ onChange={(e) => setSearchValue(e.target.value)}
96
97
  />
97
98
  </StackView>
98
99
  <TileView minCellWidth={16} spacing={4} margin={4}>
99
- {matchSorter(Object.keys(icons[appName]), value).map((iconName) => (
100
+ {matchSorter(Object.keys(icons[selectedIconSet]), searchValue).map((iconName) => (
100
101
  <StackView key={iconName} alignment="center" spacing={1}>
101
- <Icon key={iconName} name={`${appName}.${iconName}`} size="xl" />
102
+ <Icon key={iconName} name={`${selectedIconSet}.${iconName}`} size="xl" />
102
103
  <Text fontSize={5} color="foregroundSecondary">
103
104
  {iconName}
104
105
  </Text>
@@ -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
+ })
@@ -12,27 +12,30 @@ export const palette = {
12
12
  },
13
13
 
14
14
  warning: {
15
- lighter: 'hsl(60, 99%, 90%)',
16
- light: 'hsl(56, 97%, 74%)',
17
- base: 'hsl(50, 94%, 56%)',
18
- dark: 'hsl(48, 93%, 48%)',
19
- darker: 'hsl(42, 90%, 30%)',
15
+ lightest: 'hsl(42, 87%, 97%)',
16
+ lighter: 'hsl(42, 87%, 94%)',
17
+ light: 'hsl(42, 87%, 90%)',
18
+ base: 'hsl(42, 84%, 63%)',
19
+ dark: 'hsl(42, 84%, 55%)',
20
+ darker: 'hsl(42, 84%, 49%)',
20
21
  },
21
22
 
22
23
  error: {
23
- lighter: 'hsl(7, 60%, 90%)',
24
- light: 'hsl(7, 72%, 76%)',
25
- base: 'hsl(7, 78%, 64%)',
26
- dark: 'hsl(7, 80%, 52%)',
27
- darker: 'hsl(7, 86%, 30%)',
24
+ lightest: 'hsl(7, 60%, 97%)',
25
+ lighter: 'hsl(9, 59%, 93%)',
26
+ light: 'hsl(8, 60%, 85%)',
27
+ base: 'hsl(8, 60%, 47%)',
28
+ dark: 'hsl(8, 60%, 45%)',
29
+ darker: 'hsl(9, 61%, 43%)',
28
30
  },
29
31
 
30
32
  success: {
31
- lighter: 'hsl(96, 24%, 92%)',
32
- light: 'hsl(96, 32%, 76%)',
33
- base: 'hsl(96, 40%, 58%)',
34
- dark: 'hsl(96, 44%, 50%)',
35
- darker: 'hsl(96, 60%, 20%)',
33
+ lightest: 'hsl(93, 53%, 97%)',
34
+ lighter: 'hsl(95, 50%, 91%)',
35
+ light: 'hsl(86, 91%, 35%)',
36
+ base: 'hsl(86, 91%, 27%)',
37
+ dark: 'hsl(86, 91%, 25%)',
38
+ darker: 'hsl(86, 91%, 23%)',
36
39
  },
37
40
 
38
41
  red: [