@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.
- package/dist/cjs/Button/Button.js +12 -3
- package/dist/cjs/Button/Button.test.js +67 -14
- package/dist/cjs/Calendar/Calendar.js +30 -25
- package/dist/cjs/Combobox/ComboboxInput.js +41 -37
- package/dist/cjs/DataTable/DataTable.js +3 -2
- package/dist/cjs/DateField/DateField.js +74 -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/cjs/system/colors/colors.js +18 -15
- package/dist/esm/Button/Button.js +12 -3
- package/dist/esm/Button/Button.test.js +78 -19
- package/dist/esm/Calendar/Calendar.js +30 -25
- package/dist/esm/Combobox/ComboboxInput.js +40 -37
- package/dist/esm/DataTable/DataTable.js +3 -2
- package/dist/esm/DateField/DateField.js +75 -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/esm/system/colors/colors.js +18 -15
- 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 +5 -6
- package/src/Button/Button.test.tsx +39 -1
- package/src/Button/Button.tsx +8 -3
- package/src/Calendar/Calendar.js +22 -17
- package/src/Combobox/ComboboxInput.js +76 -62
- package/src/DataTable/DataTable.js +2 -1
- package/src/DateField/DateField.mdx +15 -0
- package/src/DateField/{DateField.js → DateField.tsx} +96 -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/Icon/Icon.mdx +12 -11
- package/src/Select/Select.test.tsx +58 -0
- package/src/system/colors/colors.js +18 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/tapestry-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "A collection of flexible React components to help you build resilient, accessible user interfaces quickly and effectively.",
|
|
5
5
|
"author": "Front End Systems Engineering <frontend@pco.bz>",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@emotion/cache": "10.x",
|
|
40
40
|
"@emotion/react": "^11.10.5",
|
|
41
|
-
"react": "
|
|
42
|
-
"react-dom": "
|
|
41
|
+
"react": ">=16.8.0 <19",
|
|
42
|
+
"react-dom": ">=16.8.0 <19"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@babel/cli": "7.12.17",
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
"@types/react-dom": "^18.0.8",
|
|
64
64
|
"babel-eslint": "10.1.0",
|
|
65
65
|
"chokidar-cli": "^2.1.0",
|
|
66
|
-
"cz-conventional-changelog": "^3.3.0",
|
|
67
66
|
"dotenv": "^8.2.0",
|
|
68
67
|
"eslint": "7.20.0",
|
|
69
68
|
"eslint-config-react-app": "6.0.0",
|
|
@@ -88,8 +87,8 @@
|
|
|
88
87
|
"typescript": "^4.1.5"
|
|
89
88
|
},
|
|
90
89
|
"dependencies": {
|
|
91
|
-
"@planningcenter/icons": "^14.
|
|
92
|
-
"@planningcenter/react-beautiful-dnd": "^13.
|
|
90
|
+
"@planningcenter/icons": "^14.18.1",
|
|
91
|
+
"@planningcenter/react-beautiful-dnd": "^13.3.0",
|
|
93
92
|
"@popmotion/popcorn": "^0.4.4",
|
|
94
93
|
"@popperjs/core": "^2.11.6",
|
|
95
94
|
"@react-hook/window-size": "^3.1.1",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render, fireEvent } from '@testing-library/react'
|
|
2
|
+
import { render, fireEvent, createEvent } from '@testing-library/react'
|
|
3
3
|
import { Button } from './Button'
|
|
4
4
|
|
|
5
5
|
it(`should render as <button> with type="button" by default`, () => {
|
|
@@ -14,6 +14,44 @@ it(`should render as <button> with type="submit"`, () => {
|
|
|
14
14
|
expect(button.getAttribute('type')).toEqual('submit')
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
+
it(`if "disabled" prop is provided, set "aria-disabled" attribute`, () => {
|
|
18
|
+
const { container } = render(<Button disabled />)
|
|
19
|
+
const button = container.querySelector('button')
|
|
20
|
+
|
|
21
|
+
expect(button.getAttribute("aria-disabled")).toEqual("true")
|
|
22
|
+
expect(button.disabled).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it(`if "disabled" prop is provided, prevent "Click" event from firing`, () => {
|
|
26
|
+
const { container } = render(<Button disabled />)
|
|
27
|
+
const button = container.querySelector('button')
|
|
28
|
+
|
|
29
|
+
const clickEvent = createEvent.click(button)
|
|
30
|
+
fireEvent(button, clickEvent)
|
|
31
|
+
|
|
32
|
+
expect(clickEvent.defaultPrevented).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it(`if "disabled" prop is provided, prevent keyDown "Enter" event from firing`, () => {
|
|
36
|
+
const { container } = render(<Button disabled />)
|
|
37
|
+
const button = container.querySelector('button')
|
|
38
|
+
|
|
39
|
+
const keyDownEvent = createEvent.keyDown(button, { key: 'Enter' })
|
|
40
|
+
fireEvent(button, keyDownEvent)
|
|
41
|
+
|
|
42
|
+
expect(keyDownEvent.defaultPrevented).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it(`if "disabled" prop is provided, prevent keyDown "Space" event from firing`, () => {
|
|
46
|
+
const { container } = render(<Button disabled />)
|
|
47
|
+
const button = container.querySelector('button')
|
|
48
|
+
|
|
49
|
+
const keyDownEvent = createEvent.keyDown(button, { key: ' ' })
|
|
50
|
+
fireEvent(button, keyDownEvent)
|
|
51
|
+
|
|
52
|
+
expect(keyDownEvent.defaultPrevented).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
17
55
|
it(`should render title`, () => {
|
|
18
56
|
const title = 'Hello'
|
|
19
57
|
const { getByText } = render(<Button title={title} />)
|
package/src/Button/Button.tsx
CHANGED
|
@@ -16,7 +16,7 @@ type ButtonProps = {
|
|
|
16
16
|
children?: any
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* "Soft disables" button by adding an `aria-disabled` attribute and preventing `onClick` and `keyDown` events for "space" / "enter". This approach allows composing components (such as `Tooltip`) to still bubble up their events, while ensuring that clicking the button or submitting a form is prevented.
|
|
20
20
|
*/
|
|
21
21
|
disabled?: boolean
|
|
22
22
|
|
|
@@ -254,11 +254,16 @@ export function Button({
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
// don't allow interacting with button when disabled
|
|
258
257
|
if (disabled) {
|
|
258
|
+
buttonProps.opacity = 0.65
|
|
259
259
|
buttonProps['aria-disabled'] = true
|
|
260
260
|
buttonProps.cursor = 'not-allowed'
|
|
261
|
-
buttonProps.
|
|
261
|
+
buttonProps.onClick = (event) => event.preventDefault()
|
|
262
|
+
buttonProps.onKeyDown = (event) => {
|
|
263
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
264
|
+
event.preventDefault()
|
|
265
|
+
}
|
|
266
|
+
}
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
// don't apply hover/active styles or events when disabled or spinner is present
|
package/src/Calendar/Calendar.js
CHANGED
|
@@ -220,34 +220,36 @@ const Calendar = forwardRef(
|
|
|
220
220
|
>
|
|
221
221
|
<StackView axis="horizontal" spacing={0.5}>
|
|
222
222
|
<Box
|
|
223
|
-
as="button"
|
|
224
223
|
aria-label={isMonthsView ? 'Choose dates' : 'Choose month'}
|
|
225
|
-
|
|
226
|
-
fontSize={headerFontSize}
|
|
227
|
-
lineHeight={3}
|
|
228
|
-
radius={3}
|
|
229
|
-
cursor="pointer"
|
|
224
|
+
as="button"
|
|
230
225
|
backgroundColor={hover ? 'highlight' : 'transparent'}
|
|
226
|
+
cursor="pointer"
|
|
227
|
+
fontSize={headerFontSize}
|
|
231
228
|
hover={{ backgroundColor: 'highlightSecondary' }}
|
|
229
|
+
lineHeight={3}
|
|
232
230
|
onClick={() => {
|
|
233
231
|
setCurrentView(isMonthsView ? 'calendar' : 'months')
|
|
234
232
|
}}
|
|
233
|
+
padding={0.25}
|
|
234
|
+
radius={3}
|
|
235
|
+
tabIndex={-1}
|
|
235
236
|
>
|
|
236
237
|
{format(currentDate, 'MMMM')}
|
|
237
238
|
</Box>
|
|
238
239
|
<Box
|
|
239
|
-
as="button"
|
|
240
240
|
aria-label={isYearsView ? 'Choose dates' : 'Choose year'}
|
|
241
|
-
|
|
242
|
-
fontSize={headerFontSize}
|
|
243
|
-
lineHeight={3}
|
|
244
|
-
radius={3}
|
|
245
|
-
cursor="pointer"
|
|
241
|
+
as="button"
|
|
246
242
|
backgroundColor={hover ? 'highlight' : 'transparent'}
|
|
243
|
+
cursor="pointer"
|
|
244
|
+
fontSize={headerFontSize}
|
|
247
245
|
hover={{ backgroundColor: 'highlightSecondary' }}
|
|
246
|
+
lineHeight={3}
|
|
248
247
|
onClick={() => {
|
|
249
248
|
setCurrentView(isYearsView ? 'calendar' : 'years')
|
|
250
249
|
}}
|
|
250
|
+
padding={0.25}
|
|
251
|
+
radius={3}
|
|
252
|
+
tabIndex={-1}
|
|
251
253
|
>
|
|
252
254
|
{currentYear}
|
|
253
255
|
</Box>
|
|
@@ -260,29 +262,32 @@ const Calendar = forwardRef(
|
|
|
260
262
|
}}
|
|
261
263
|
>
|
|
262
264
|
<Button
|
|
263
|
-
title="Previous month"
|
|
264
|
-
icon={{ name: 'general.leftChevron', size: "xxs" }}
|
|
265
265
|
disabled={
|
|
266
266
|
isYearsView
|
|
267
267
|
? currentYear <= minYear
|
|
268
268
|
: isSameMonth(currentDate, minDate)
|
|
269
269
|
}
|
|
270
|
+
icon={{ name: 'general.leftChevron', size: 'xxs' }}
|
|
270
271
|
onClick={() => navigateCalendar(-1)}
|
|
272
|
+
tabIndex={-1}
|
|
273
|
+
title="Previous month"
|
|
271
274
|
/>
|
|
272
275
|
<Button
|
|
273
|
-
tooltip={{ title: 'Today' }}
|
|
274
276
|
icon={{ name: 'tapestry.radio1' }}
|
|
275
277
|
onClick={() => setDate(TODAY, true)}
|
|
278
|
+
tabIndex={-1}
|
|
279
|
+
tooltip={{ title: 'Today' }}
|
|
276
280
|
/>
|
|
277
281
|
<Button
|
|
278
|
-
title="Next month"
|
|
279
|
-
icon={{ name: 'general.rightChevron', size: "xxs"}}
|
|
280
282
|
disabled={
|
|
281
283
|
isYearsView
|
|
282
284
|
? currentYear >= maxYear
|
|
283
285
|
: isSameMonth(currentDate, maxDate)
|
|
284
286
|
}
|
|
287
|
+
icon={{ name: 'general.rightChevron', size: 'xxs' }}
|
|
285
288
|
onClick={() => navigateCalendar(1)}
|
|
289
|
+
tabIndex={-1}
|
|
290
|
+
title="Next month"
|
|
286
291
|
/>
|
|
287
292
|
</Group>
|
|
288
293
|
</StackView>
|
|
@@ -10,6 +10,7 @@ import React, {
|
|
|
10
10
|
|
|
11
11
|
import Input from '../Input'
|
|
12
12
|
import ItemListContext from '../ItemList/ItemListContext'
|
|
13
|
+
import { ItemListController } from '../ItemList'
|
|
13
14
|
import { useThemeProps } from '../system'
|
|
14
15
|
|
|
15
16
|
type Props = {
|
|
@@ -64,77 +65,90 @@ function ComboboxInput(props: Props, ref) {
|
|
|
64
65
|
}))
|
|
65
66
|
|
|
66
67
|
const itemList = useContext(ItemListContext)
|
|
67
|
-
const { highlightedItemId, id } = itemList
|
|
68
68
|
|
|
69
69
|
useEffect(() => {
|
|
70
70
|
setTimeout(() => {
|
|
71
71
|
itemList.setHighlightedIndex(0)
|
|
72
72
|
})
|
|
73
|
-
}, [itemList
|
|
73
|
+
}, [itemList])
|
|
74
74
|
|
|
75
|
-
const handleKeyDown = useCallback(
|
|
76
|
-
|
|
77
|
-
e.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
e.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
e.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
onKeyDown
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
onClose
|
|
93
|
-
|
|
94
|
-
|
|
75
|
+
const handleKeyDown = useCallback(
|
|
76
|
+
(e) => {
|
|
77
|
+
if (e.key === 'ArrowUp') {
|
|
78
|
+
e.preventDefault()
|
|
79
|
+
itemList.moveHighlightedIndex(-1, { contain: false })
|
|
80
|
+
}
|
|
81
|
+
if (e.key === 'ArrowDown') {
|
|
82
|
+
e.preventDefault()
|
|
83
|
+
itemList.moveHighlightedIndex(1, { contain: false })
|
|
84
|
+
}
|
|
85
|
+
if (e.key === 'Enter') {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
itemList.selectHighlightedItem()
|
|
88
|
+
}
|
|
89
|
+
if (onKeyDown) {
|
|
90
|
+
onKeyDown(e)
|
|
91
|
+
}
|
|
92
|
+
if (onClose && e.key === 'Tab') {
|
|
93
|
+
onClose()
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
[itemList, onClose, onKeyDown]
|
|
97
|
+
)
|
|
95
98
|
|
|
96
|
-
const handleDocumentKeyDown = useCallback(
|
|
97
|
-
|
|
98
|
-
onClear
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
const handleDocumentKeyDown = useCallback(
|
|
100
|
+
(e) => {
|
|
101
|
+
if (onClear && e.key === 'Escape') {
|
|
102
|
+
onClear()
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[onClear]
|
|
106
|
+
)
|
|
101
107
|
|
|
102
108
|
return (
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
109
|
+
<ItemListController>
|
|
110
|
+
{(itemList) => {
|
|
111
|
+
const { highlightedItemId, id } = itemList
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Input.InputBox
|
|
115
|
+
innerRef={innerRef}
|
|
116
|
+
size={size}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
isLoading={isLoading}
|
|
119
|
+
onClear={onClear}
|
|
120
|
+
{...restProps}
|
|
121
|
+
grow={1}
|
|
122
|
+
>
|
|
123
|
+
<Input.InputField
|
|
124
|
+
innerRef={(node) => {
|
|
125
|
+
inputNode.current = node
|
|
126
|
+
}}
|
|
127
|
+
grow={1}
|
|
128
|
+
role="combobox"
|
|
129
|
+
autoComplete="off"
|
|
130
|
+
aria-autocomplete="list"
|
|
131
|
+
aria-controls={id}
|
|
132
|
+
aria-expanded={isOpen}
|
|
133
|
+
aria-activedescendant={highlightedItemId}
|
|
134
|
+
autoFocus={autoFocus}
|
|
135
|
+
type={type}
|
|
136
|
+
name={name}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
readOnly={readOnly}
|
|
139
|
+
placeholder={placeholder}
|
|
140
|
+
defaultValue={defaultValue}
|
|
141
|
+
value={value}
|
|
142
|
+
onChange={onChange}
|
|
143
|
+
onFocus={onFocus}
|
|
144
|
+
onBlur={onBlur}
|
|
145
|
+
onKeyDown={handleKeyDown}
|
|
146
|
+
onKeyUp={onKeyUp}
|
|
147
|
+
/>
|
|
148
|
+
</Input.InputBox>
|
|
149
|
+
)
|
|
150
|
+
}}
|
|
151
|
+
</ItemListController>
|
|
138
152
|
)
|
|
139
153
|
}
|
|
140
154
|
ComboboxInput = forwardRef(ComboboxInput)
|
|
@@ -436,12 +436,13 @@ const DataTable = (props: Props) => {
|
|
|
436
436
|
>
|
|
437
437
|
{(provided) => (
|
|
438
438
|
<div
|
|
439
|
+
className={css(getVariant('body'))}
|
|
440
|
+
data-dnd-ignore-scrollable
|
|
439
441
|
ref={(ref) => {
|
|
440
442
|
bodyRef.current = ref
|
|
441
443
|
provided.innerRef(ref)
|
|
442
444
|
}}
|
|
443
445
|
role="rowgroup"
|
|
444
|
-
className={css(getVariant('body'))}
|
|
445
446
|
{...provided.droppableProps}
|
|
446
447
|
>
|
|
447
448
|
<BodyRows {...bodyRowsProps} />
|
|
@@ -17,3 +17,18 @@ render(() => {
|
|
|
17
17
|
)
|
|
18
18
|
})
|
|
19
19
|
```
|
|
20
|
+
|
|
21
|
+
### Example with day/month/year format
|
|
22
|
+
|
|
23
|
+
```jsx live
|
|
24
|
+
render(() => {
|
|
25
|
+
const [date, setDate] = React.useState(Calendar.TODAY)
|
|
26
|
+
return (
|
|
27
|
+
<DateField
|
|
28
|
+
value={date}
|
|
29
|
+
formatValue="dd/MM/yyyy"
|
|
30
|
+
onChange={(date) => setDate(date)}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
```
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// @flow
|
|
2
|
-
import React, { useState, useCallback, useRef } from 'react'
|
|
3
|
-
import { format } from 'date-fns'
|
|
2
|
+
import React, { useState, useCallback, useMemo, useRef } from 'react'
|
|
4
3
|
|
|
5
4
|
import Card from '../Card'
|
|
6
5
|
import Calendar from '../Calendar'
|
|
@@ -10,62 +9,64 @@ import Input from '../Input/Input'
|
|
|
10
9
|
import Popover from '../Popover'
|
|
11
10
|
import { generateId } from '../utils'
|
|
12
11
|
import { useThemeProps } from '../system'
|
|
12
|
+
import { useArrowKeysToNavigateCalendar } from './useArrowKeysToNavigateCalendar'
|
|
13
|
+
import { useEditableDate } from './useEditableDate'
|
|
13
14
|
|
|
14
15
|
export type DateFieldProps = {
|
|
15
16
|
/**
|
|
16
17
|
* Format the displayed date using date-fns [format](https://date-fns.org/v2.0.0-alpha.9/docs/format) function.
|
|
17
18
|
*/
|
|
18
|
-
formatValue: string
|
|
19
|
+
formatValue: string
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Controls the initial Popover state: open or closed (default).
|
|
22
23
|
*/
|
|
23
|
-
defaultOpen: boolean
|
|
24
|
+
defaultOpen: boolean
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* The minimum date that can be chosen.
|
|
27
28
|
*/
|
|
28
|
-
minDate: Date
|
|
29
|
+
minDate: Date
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The maximum date that can be chosen.
|
|
32
33
|
*/
|
|
33
|
-
maxDate: Date
|
|
34
|
+
maxDate: Date
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Called when a date has been selected.
|
|
37
38
|
*/
|
|
38
|
-
onChange: (date: Date) => null
|
|
39
|
+
onChange: (date: Date) => null
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Determines where the popover is placed.
|
|
42
43
|
*/
|
|
43
|
-
placement: string
|
|
44
|
+
placement: string
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* The date that will be selected.
|
|
47
48
|
*/
|
|
48
|
-
value: Date
|
|
49
|
+
value: Date
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Locks external scrollbars when open.
|
|
52
53
|
*/
|
|
53
|
-
lockScrollWhileOpen?: boolean
|
|
54
|
+
lockScrollWhileOpen?: boolean
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Attempts to keep popover in view clipping edges if too large.
|
|
57
58
|
*/
|
|
58
|
-
keepInView?: boolean
|
|
59
|
+
keepInView?: boolean
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* Accepts any valid [Calendar](/calendar) props.
|
|
62
63
|
*/
|
|
63
|
-
calendarProps?: object
|
|
64
|
+
calendarProps?: object
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Accepts any valid [Popover](/popover) props.
|
|
67
68
|
*/
|
|
68
|
-
popoverProps?: object
|
|
69
|
+
popoverProps?: object
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
function DateField({
|
|
@@ -89,81 +90,124 @@ function DateField({
|
|
|
89
90
|
trackColor,
|
|
90
91
|
...restProps
|
|
91
92
|
} = useThemeProps('dateField', props)
|
|
92
|
-
let canClosePopover = true
|
|
93
93
|
const [isPopoverOpen, setIsPopoverOpen] = useState(defaultOpen)
|
|
94
94
|
const id = generateId('datefield')
|
|
95
95
|
const popover = useRef(null)
|
|
96
96
|
const inputWrapper = useRef(null)
|
|
97
97
|
|
|
98
|
+
const focusInput = useCallback(() => {
|
|
99
|
+
const input = inputWrapper.current.querySelector('input')
|
|
100
|
+
if (input.focus) {
|
|
101
|
+
input.focus()
|
|
102
|
+
}
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
98
105
|
const openPopover = useCallback(() => {
|
|
99
106
|
setIsPopoverOpen(true)
|
|
100
|
-
})
|
|
107
|
+
}, [])
|
|
101
108
|
|
|
102
109
|
const closePopover = useCallback(() => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
setIsPopoverOpen(false)
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
113
|
+
const dateValidator = useCallback(
|
|
114
|
+
(date: Date) => {
|
|
115
|
+
if (!date) return false
|
|
116
|
+
if (minDate && date < minDate) return false
|
|
117
|
+
if (maxDate && date > maxDate) return false
|
|
118
|
+
return true
|
|
119
|
+
},
|
|
120
|
+
[minDate, maxDate]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const {
|
|
124
|
+
formattedDate,
|
|
125
|
+
setDate,
|
|
126
|
+
clearKeyBuffer,
|
|
127
|
+
invalidKeyBuffer,
|
|
128
|
+
} = useEditableDate({
|
|
129
|
+
date: value,
|
|
130
|
+
dateFormat: formatValue,
|
|
131
|
+
dateValidator,
|
|
132
|
+
onChange,
|
|
106
133
|
})
|
|
107
134
|
|
|
108
|
-
const
|
|
109
|
-
|
|
135
|
+
const handleDateSelectedFromCalendar = useCallback(
|
|
136
|
+
(date) => {
|
|
137
|
+
focusInput()
|
|
138
|
+
setDate(date)
|
|
110
139
|
closePopover()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
140
|
+
},
|
|
141
|
+
[focusInput, setDate, closePopover]
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const navigateCalendarWithArrowKeys = useArrowKeysToNavigateCalendar({
|
|
145
|
+
date: value,
|
|
146
|
+
calendarIsOpen: isPopoverOpen,
|
|
147
|
+
openCalendar: openPopover,
|
|
148
|
+
onChange: setDate,
|
|
114
149
|
})
|
|
115
150
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
151
|
+
const handleInputOnChange = useCallback(
|
|
152
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
153
|
+
setDate(event.currentTarget.value)
|
|
154
|
+
},
|
|
155
|
+
[setDate]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const handleOnBlur = useCallback(() => {
|
|
159
|
+
clearKeyBuffer()
|
|
124
160
|
closePopover()
|
|
125
|
-
})
|
|
161
|
+
}, [clearKeyBuffer, closePopover])
|
|
162
|
+
|
|
163
|
+
const inputColors = useMemo(() => {
|
|
164
|
+
if (invalidKeyBuffer) {
|
|
165
|
+
return { color: 'error-darker', backgroundColor: 'error-lighter' }
|
|
166
|
+
} else {
|
|
167
|
+
return {}
|
|
168
|
+
}
|
|
169
|
+
}, [invalidKeyBuffer])
|
|
126
170
|
|
|
127
171
|
return (
|
|
128
|
-
<FocusGroup onBlur={
|
|
172
|
+
<FocusGroup onBlur={handleOnBlur}>
|
|
129
173
|
{({ requestBlur, setRef }) => (
|
|
130
174
|
<Popover
|
|
131
175
|
{...popoverProps}
|
|
132
176
|
ref={(component) => {
|
|
133
177
|
popover.current = component
|
|
134
178
|
}}
|
|
179
|
+
as={Card}
|
|
180
|
+
elevation={2}
|
|
135
181
|
innerRef={(node) => {
|
|
136
182
|
popover.current = node
|
|
137
183
|
setRef(`${id}-popover`)(node)
|
|
138
184
|
}}
|
|
139
|
-
as={Card}
|
|
140
|
-
tabIndex={-1}
|
|
141
|
-
elevation={2}
|
|
142
|
-
onBlur={requestBlur}
|
|
143
185
|
keepInView={keepInView}
|
|
144
186
|
lockScrollWhileOpen={lockScrollWhileOpen}
|
|
145
|
-
|
|
146
|
-
open={isPopoverOpen}
|
|
187
|
+
onBlur={requestBlur}
|
|
147
188
|
onRequestClose={closePopover}
|
|
189
|
+
open={isPopoverOpen}
|
|
190
|
+
placement={placement}
|
|
191
|
+
tabIndex={-1}
|
|
148
192
|
anchorElement={
|
|
149
193
|
<Input
|
|
150
194
|
innerRef={(node) => {
|
|
151
195
|
inputWrapper.current = node
|
|
152
196
|
setRef(`${id}-input`)(node)
|
|
153
197
|
}}
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
onBlur={requestBlur}
|
|
199
|
+
onFocus={openPopover}
|
|
200
|
+
onChange={handleInputOnChange}
|
|
201
|
+
onKeyDown={navigateCalendarWithArrowKeys}
|
|
202
|
+
value={formattedDate}
|
|
156
203
|
renderRight={
|
|
157
|
-
<Icon
|
|
204
|
+
<Icon
|
|
205
|
+
name="general.calendar"
|
|
206
|
+
color="foregroundTertiary"
|
|
207
|
+
onClick={openPopover}
|
|
208
|
+
/>
|
|
158
209
|
}
|
|
159
|
-
|
|
160
|
-
onBlur={requestBlur}
|
|
161
|
-
onKeyDown={(event) => {
|
|
162
|
-
if (event.key === ' ') {
|
|
163
|
-
event.preventDefault()
|
|
164
|
-
togglePopover()
|
|
165
|
-
}
|
|
166
|
-
}}
|
|
210
|
+
{...inputColors}
|
|
167
211
|
{...restProps}
|
|
168
212
|
/>
|
|
169
213
|
}
|
|
@@ -171,11 +215,11 @@ function DateField({
|
|
|
171
215
|
<Calendar
|
|
172
216
|
size="sm"
|
|
173
217
|
{...calendarProps}
|
|
174
|
-
|
|
218
|
+
date={value}
|
|
175
219
|
selected={value}
|
|
176
220
|
minDate={minDate}
|
|
177
221
|
maxDate={maxDate}
|
|
178
|
-
onDateSelect={
|
|
222
|
+
onDateSelect={handleDateSelectedFromCalendar}
|
|
179
223
|
/>
|
|
180
224
|
</Popover>
|
|
181
225
|
)}
|