@planningcenter/tapestry-react 2.6.0-rc.9 → 2.6.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.
Files changed (76) hide show
  1. package/dist/cjs/Button/Button.js +8 -1
  2. package/dist/cjs/Button/Button.test.js +51 -8
  3. package/dist/cjs/Collapse/Collapse.js +3 -1
  4. package/dist/cjs/DataTable/components/BodyRow.js +2 -2
  5. package/dist/cjs/DataTable/hooks/useCollapsibleRows.js +2 -2
  6. package/dist/cjs/Dropdown/Dropdown.js +23 -4
  7. package/dist/cjs/Dropdown/Dropdown.test.js +3 -3
  8. package/dist/cjs/Dropdown/Link.js +2 -4
  9. package/dist/cjs/Input/InputLabel.js +40 -63
  10. package/dist/cjs/Modal/Modal.js +18 -8
  11. package/dist/cjs/Modal/Modal.test.js +2 -2
  12. package/dist/cjs/Popover/Popover.js +10 -2
  13. package/dist/cjs/Scrim/Scrim.js +16 -4
  14. package/dist/cjs/Table/Table.js +5 -3
  15. package/dist/cjs/ThemeProvider/ThemeProvider.js +2 -2
  16. package/dist/cjs/TimeField/TimeField.js +38 -3
  17. package/dist/cjs/TimeField/TimeField.test.js +179 -10
  18. package/dist/cjs/Tooltip/Tooltip.js +27 -23
  19. package/dist/cjs/Tooltip/Tooltip.test.js +178 -0
  20. package/dist/cjs/system/utils.js +2 -2
  21. package/dist/cjs/utils.js +3 -3
  22. package/dist/esm/Button/Button.js +8 -1
  23. package/dist/esm/Button/Button.test.js +67 -9
  24. package/dist/esm/Collapse/Collapse.js +3 -1
  25. package/dist/esm/DataTable/components/BodyRow.js +2 -2
  26. package/dist/esm/DataTable/hooks/useCollapsibleRows.js +1 -1
  27. package/dist/esm/Dropdown/Dropdown.js +24 -5
  28. package/dist/esm/Dropdown/Dropdown.test.js +1 -1
  29. package/dist/esm/Dropdown/Link.js +1 -2
  30. package/dist/esm/Input/InputLabel.js +40 -63
  31. package/dist/esm/Modal/Modal.js +16 -8
  32. package/dist/esm/Modal/Modal.test.js +1 -1
  33. package/dist/esm/Popover/Popover.js +8 -2
  34. package/dist/esm/Scrim/Scrim.js +15 -4
  35. package/dist/esm/Table/Table.js +2 -1
  36. package/dist/esm/ThemeProvider/ThemeProvider.js +1 -1
  37. package/dist/esm/TimeField/TimeField.js +37 -3
  38. package/dist/esm/TimeField/TimeField.test.js +153 -10
  39. package/dist/esm/Tooltip/Tooltip.js +29 -24
  40. package/dist/esm/Tooltip/Tooltip.test.js +160 -0
  41. package/dist/esm/system/utils.js +1 -1
  42. package/dist/esm/utils.js +1 -1
  43. package/dist/types/Button/Button.d.ts +4 -0
  44. package/dist/types/ThemeProvider/ThemeProvider.d.ts +3 -3
  45. package/dist/types/Tooltip/Tooltip.test.d.ts +1 -0
  46. package/dist/types/index.d.ts +1 -1
  47. package/package.json +4 -4
  48. package/src/Button/Button.test.tsx +30 -0
  49. package/src/Button/Button.tsx +14 -1
  50. package/src/Collapse/Collapse.js +1 -1
  51. package/src/DataTable/DataTable.js +1 -1
  52. package/src/DataTable/components/BodyRow.js +1 -1
  53. package/src/DataTable/hooks/useCollapsibleRows.js +1 -1
  54. package/src/Dropdown/Dropdown.js +19 -5
  55. package/src/Dropdown/Dropdown.mdx +3 -3
  56. package/src/Dropdown/Dropdown.test.tsx +1 -1
  57. package/src/Dropdown/Link.js +1 -7
  58. package/src/Input/InputLabel.js +39 -36
  59. package/src/Input/InputLabel.mdx +1 -0
  60. package/src/Modal/Modal.js +16 -6
  61. package/src/Modal/Modal.mdx +2 -1
  62. package/src/Modal/Modal.test.tsx +1 -1
  63. package/src/Popover/Popover.mdx +1 -0
  64. package/src/Popover/Popover.tsx +8 -2
  65. package/src/Scrim/Scrim.mdx +1 -0
  66. package/src/Scrim/Scrim.tsx +11 -6
  67. package/src/Sidebar/Sidebar.mdx +0 -1
  68. package/src/Table/Table.js +2 -1
  69. package/src/ThemeProvider/ThemeProvider.tsx +7 -6
  70. package/src/TimeField/TimeField.js +36 -3
  71. package/src/TimeField/TimeField.test.tsx +105 -1
  72. package/src/Tooltip/Tooltip.js +19 -21
  73. package/src/Tooltip/Tooltip.test.tsx +136 -0
  74. package/src/index.d.ts +1 -1
  75. package/src/system/utils.js +1 -1
  76. package/src/utils.js +1 -1
@@ -9,7 +9,7 @@ import Popover from '../Popover'
9
9
  import { cloneChildren, generateId } from '../utils'
10
10
 
11
11
  import Item, { ITEM_DISPLAY_NAME } from './Item'
12
- import Link, { LINK_DISPLAY_NAME, LINK_DATA } from './Link'
12
+ import Link, { LINK_DISPLAY_NAME } from './Link'
13
13
 
14
14
  type Props = {
15
15
  children?: React.ReactNode,
@@ -91,6 +91,7 @@ class Dropdown extends Component<Props> {
91
91
  super(props)
92
92
  this.state = {
93
93
  isPopoverOpen: props.defaultOpen || props.forceOpen,
94
+ preventBlur: false,
94
95
  }
95
96
  this.id = generateId('dropdown')
96
97
  }
@@ -112,6 +113,7 @@ class Dropdown extends Component<Props> {
112
113
  }
113
114
 
114
115
  togglePopover = (event) => {
116
+ this.setState({ preventBlur: false })
115
117
  if (this.state.isPopoverOpen) {
116
118
  this.closePopover(event)
117
119
  } else {
@@ -125,7 +127,7 @@ class Dropdown extends Component<Props> {
125
127
  this.closePopover()
126
128
  this.popover.focusAnchor()
127
129
  }
128
- if (data === LINK_DATA) {
130
+ if (node.tagName === 'A' && event.type !== 'click') {
129
131
  node.click()
130
132
  } else if (this.props.onSelect) {
131
133
  this.props.onSelect(data)
@@ -215,18 +217,27 @@ class Dropdown extends Component<Props> {
215
217
  'aria-haspopup': true,
216
218
  'aria-expanded': isPopoverOpen,
217
219
  [arrowIconOnly ? 'icon' : 'iconRight']: {
218
- name: isPopoverOpen ? 'general.upCaret' : 'general.downCaret',
220
+ name: isPopoverOpen
221
+ ? 'general.upCaret'
222
+ : 'general.downCaret',
219
223
  size: 'sm',
220
224
  },
221
225
  title: arrowIconOnly ? 'arrow down' : restProps.title,
222
226
  tabIndex: 0,
223
227
  cursor: 'pointer',
224
- onBlur: requestBlur,
225
- onClick: () => {
228
+ onBlur: (event) => {
229
+ if (this.state.preventBlur) {
230
+ this.setState({ preventBlur: false })
231
+ } else {
232
+ requestBlur(event)
233
+ }
234
+ },
235
+ onClick: (event) => {
226
236
  this.togglePopover()
227
237
  if (!isPopoverOpen) {
228
238
  this.popover.focusAnchor()
229
239
  }
240
+ onClick && onClick(event)
230
241
  },
231
242
  onKeyDown: (event) => {
232
243
  anchorProps.onKeyDown(event)
@@ -274,6 +285,9 @@ class Dropdown extends Component<Props> {
274
285
  })
275
286
  }
276
287
  },
288
+ onMouseDown: () => {
289
+ this.setState({ preventBlur: true })
290
+ },
277
291
  ...restProps,
278
292
  }
279
293
  if (React.isValidElement(triggerElement)) {
@@ -26,10 +26,10 @@ render(
26
26
  render(
27
27
  <Dropdown title="Links" size="sm" variant="outline">
28
28
  <Dropdown.Link to="http://planning.center" external>
29
- Planning Center
29
+ Planning Center (external)
30
30
  </Dropdown.Link>
31
- <Dropdown.Link to="https://reactjs.org" external>
32
- React Docs
31
+ <Dropdown.Link to="/button">
32
+ Tapestry React Button
33
33
  </Dropdown.Link>
34
34
  </Dropdown>
35
35
  )
@@ -2,7 +2,7 @@ import React, { useState } from 'react'
2
2
  import { fireEvent, render, screen } from '@testing-library/react'
3
3
  import userEvent from '@testing-library/user-event'
4
4
  import '@testing-library/jest-dom/extend-expect'
5
- import { noop } from 'lodash'
5
+ import noop from 'lodash/noop'
6
6
  import { Box, Button, Text, ThemeProvider } from '..'
7
7
  import Dropdown from './Dropdown'
8
8
 
@@ -4,7 +4,6 @@ import { ItemListItem } from '../ItemList'
4
4
  import Menu from '../Menu'
5
5
 
6
6
  export const LINK_DISPLAY_NAME = 'Dropdown.Link'
7
- export const LINK_DATA = 'link'
8
7
 
9
8
  class Link extends Component {
10
9
  // Graphql wasn't picking up the correct displayName when this was
@@ -17,12 +16,7 @@ class Link extends Component {
17
16
  restProps.target = '_blank'
18
17
  }
19
18
  return (
20
- <ItemListItem
21
- data={LINK_DATA}
22
- text={text}
23
- disabled={disabled}
24
- index={index}
25
- >
19
+ <ItemListItem data="link" text={text} disabled={disabled} index={index}>
26
20
  {({ id, highlight, highlighted, clearHighlight, select }) => (
27
21
  <Menu.Item
28
22
  as="a"
@@ -1,8 +1,9 @@
1
1
  // @flow
2
- import React, { Component } from 'react'
2
+ import React, { useCallback, useRef, useEffect } from 'react'
3
3
 
4
4
  import { getColor } from '../system'
5
5
  import Text from '../Text'
6
+ import { useThemeProps } from '../system'
6
7
 
7
8
  import { inputs, inputLabels } from './utils'
8
9
 
@@ -10,54 +11,56 @@ export type InputLabelProps = {
10
11
  /**
11
12
  * The `id` of the input to control. Compatible with all Tapestry-React form components.
12
13
  */
13
- controls: string,
14
+ controls?: string,
14
15
 
15
16
  /**
16
17
  * The current state of the label. Should match corresponding `Input`'s state prop.
17
18
  */
18
- state: 'warning' | 'error' | 'success',
19
+ state?: 'warning' | 'error' | 'success',
19
20
  }
20
21
 
21
- class InputLabel extends Component<InputLabelProps> {
22
- componentDidMount() {
23
- this.input = inputs[this.props.controls]
24
- inputLabels[this.props.controls] = true
25
- }
22
+ function InputLabel({ controls, state, ...restProps }: InputLabelProps) {
23
+ const { ...themeProps } = useThemeProps('inputLabel', restProps)
26
24
 
27
- componentWillUnmount() {
28
- delete inputLabels[this.props.controls]
29
- }
25
+ const input = useRef(null)
30
26
 
31
- focusInput = () => {
32
- this.input && this.input.focus()
27
+ if (controls) {
28
+ themeProps.id = `${controls}-label`
33
29
  }
34
-
35
- handleMouseOver = () => {
36
- this.input && this.input.setState({ isHovered: true })
30
+ if (state) {
31
+ themeProps.color = getColor(state)
37
32
  }
38
33
 
39
- handleMouseOut = () => {
40
- this.input && this.input.setState({ isHovered: false })
41
- }
34
+ useEffect(() => {
35
+ input.current = inputs[controls]
36
+ inputLabels[controls] = true
42
37
 
43
- render() {
44
- const { controls, state, ...restProps } = this.props
45
- if (controls) {
46
- restProps.id = `${controls}-label`
47
- }
48
- if (state) {
49
- restProps.color = getColor(state)
38
+ return () => {
39
+ delete inputLabels[controls]
50
40
  }
51
- return (
52
- <Text
53
- as="label"
54
- onMouseOver={this.handleMouseOver}
55
- onMouseOut={this.handleMouseOut}
56
- onClick={this.focusInput}
57
- {...restProps}
58
- />
59
- )
60
- }
41
+ }, [])
42
+
43
+ const focusInput = useCallback(() => {
44
+ input.current && input.current.focus()
45
+ }, [input])
46
+
47
+ const handleMouseOver = useCallback(() => {
48
+ input.current && input.current.setState({ isHovered: true })
49
+ }, [input])
50
+
51
+ const handleMouseOut = useCallback(() => {
52
+ input.current && input.current.setState({ isHovered: false })
53
+ }, [input])
54
+
55
+ return (
56
+ <Text
57
+ as="label"
58
+ onMouseOver={handleMouseOver}
59
+ onMouseOut={handleMouseOut}
60
+ onClick={focusInput}
61
+ {...themeProps}
62
+ />
63
+ )
61
64
  }
62
65
 
63
66
  InputLabel.displayName = 'Input.InputLabel'
@@ -4,6 +4,7 @@ category: Forms
4
4
  summary: Provides accessibility as well as usability improvements for mouse users by allowing the user to click the <InputLabel/> component to focus the respective control. This mimics the browsers native label tag, but allows use with custom components like <Select/>.
5
5
  propsSummary: Accepts [Text](/text) props.
6
6
  parent: Input
7
+ themeKey: inputLabel
7
8
  ---
8
9
 
9
10
  ```jsx live
@@ -5,6 +5,7 @@ import Box from '../Box'
5
5
  import Scrim from '../Scrim'
6
6
  import { useDocumentEvent } from '../hooks'
7
7
  import { trapFocus } from '../utils'
8
+ import { useThemeProps } from '../system'
8
9
 
9
10
  export type ModalProps = {
10
11
  children?: any,
@@ -23,6 +24,11 @@ export type ModalProps = {
23
24
  * Determines whether the modal is open or not.
24
25
  */
25
26
  open: boolean,
27
+
28
+ /**
29
+ * Props passed to the internal [`<Scrim/>`](/scrim) component.
30
+ */
31
+ scrimProps?: object,
26
32
  }
27
33
 
28
34
  function Modal({
@@ -32,6 +38,7 @@ function Modal({
32
38
  open,
33
39
  ...restProps
34
40
  }: ModalProps) {
41
+ const { scrimProps = {}, ...themeProps } = useThemeProps('modal', restProps)
35
42
  const modalRef = useRef(null)
36
43
 
37
44
  useLayoutEffect(() => {
@@ -58,16 +65,19 @@ function Modal({
58
65
  onRequestClose()
59
66
  }
60
67
  }}
68
+ {...scrimProps}
61
69
  >
62
70
  <Box
71
+ backgroundColor="surface"
72
+ boxShadow="0 0 20px rgba(0, 0, 0, 0.15)"
63
73
  innerRef={modalRef}
64
- width="100%"
65
- maxWidth={60}
66
- padding={2}
67
74
  margin={4}
68
- backgroundColor="surface"
69
- radius={3}
70
- {...restProps}
75
+ maxWidth={60}
76
+ padding={4}
77
+ paddingBottom={3}
78
+ radius={8}
79
+ width="100%"
80
+ {...themeProps}
71
81
  >
72
82
  {children}
73
83
  </Box>
@@ -2,6 +2,7 @@
2
2
  title: Modal
3
3
  category: Overlays
4
4
  propsSummary: Accepts [Box](/box) props.
5
+ themeKey: modal
5
6
  ---
6
7
 
7
8
  ```jsx live
@@ -16,7 +17,7 @@ function MyModal(props) {
16
17
  return () => setLazyComponent(null)
17
18
  }, [props.open])
18
19
  return (
19
- <Modal id="modal" closeOnOutsideClick height={200} {...props}>
20
+ <Modal id="modal" closeOnOutsideClick height={25} {...props}>
20
21
  <StackView spacing={2}>
21
22
  {lazyComponent || <Text>Loading lazy component...</Text>}
22
23
  <Select
@@ -5,7 +5,7 @@ import '@testing-library/jest-dom/extend-expect'
5
5
  import Dropdown from '../Dropdown'
6
6
  import Select from '../Select'
7
7
  import { Button, Heading, Modal, ThemeProvider } from '../'
8
- import { noop } from 'lodash'
8
+ import noop from 'lodash/noop'
9
9
 
10
10
  describe('Modal', () => {
11
11
  const TestModal = () => {
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Popover
3
3
  category: Overlays
4
+ themeKey: popover
4
5
  ---
5
6
 
6
7
  ```jsx live
@@ -7,6 +7,8 @@ import { lockScroll } from '../utils'
7
7
 
8
8
  import rewireTabOrder from './rewireTabOrder'
9
9
  import { getFixedParent, getModifiers } from './utils'
10
+ import { useThemeProps } from '../system'
11
+ import { useTheme } from '@emotion/react'
10
12
 
11
13
  type anchorCallbackProps = {
12
14
  innerRef(node: HTMLElement): void
@@ -116,6 +118,10 @@ export const Popover = React.forwardRef(
116
118
  }: PopoverProps,
117
119
  ref
118
120
  ) => {
121
+ const { zIndex = 10000, ...themeProps } = useThemeProps(
122
+ 'popover',
123
+ restProps
124
+ )
119
125
  const anchorRef = React.useRef(null)
120
126
  const popperRef = React.useRef(null)
121
127
  const unlockScroll = React.useRef(null)
@@ -253,8 +259,8 @@ export const Popover = React.forwardRef(
253
259
  }
254
260
  },
255
261
  // ideally this should be pulled out to something like ThemeProvider for predictable z indices
256
- style: { zIndex: 10000 },
257
- ...restProps,
262
+ style: { zIndex },
263
+ ...themeProps,
258
264
  })
259
265
  : null}
260
266
  </Portal>
@@ -3,6 +3,7 @@ title: Scrim
3
3
  category: Overlays
4
4
  summary: Scrims are used to display focused content like alerts, modals, or media previews.
5
5
  propsSummary: Accepts [StackView](/stackview) props.
6
+ themeKey: scrim
6
7
  ---
7
8
 
8
9
  ```jsx live
@@ -1,33 +1,38 @@
1
1
  import * as React from 'react'
2
2
 
3
3
  import StackView from '../StackView'
4
+ import { useThemeProps } from '../system'
4
5
 
5
6
  type ScrimProps = {
6
7
  /** Gain access to the internal ref. */
7
- ref?: any,
8
- children?: any,
8
+ ref?: any
9
+ children?: any
9
10
  }
10
11
 
11
- const Scrim = React.forwardRef(function (props: ScrimProps, _ref: any) {
12
+ const Scrim = React.forwardRef(function (
13
+ { ref, ...restProps }: ScrimProps,
14
+ _ref: any
15
+ ) {
12
16
  React.useLayoutEffect(() => {
13
17
  document.body.style.overflow = 'hidden'
14
18
  return () => {
15
19
  document.body.style.overflow = ''
16
20
  }
17
21
  }, [])
22
+ const { zIndex = 10000, ...themeProps } = useThemeProps('scrim', restProps)
18
23
  return (
19
24
  <StackView
20
- innerRef={props.ref}
25
+ innerRef={ref}
21
26
  position="fixed"
22
27
  top={0}
23
28
  right={0}
24
29
  bottom={0}
25
30
  left={0}
26
- zIndex={10000}
31
+ zIndex={zIndex}
27
32
  overflow="auto"
28
33
  // @ts-ignore
29
34
  backgroundColor="hsla(0,0%,0%,0.4)"
30
- {...props}
35
+ {...themeProps}
31
36
  />
32
37
  )
33
38
  })
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  title: Sidebar
3
3
  category: General
4
- propsSummary: Accepts [StackView](/stackview) props.
5
4
  ---
6
5
 
7
6
  ```jsx live
@@ -1,7 +1,8 @@
1
1
  // @flow
2
2
  import React, { PureComponent, Children, Fragment } from 'react'
3
3
  import { Global } from '@emotion/react'
4
- import { camelCase, snakeCase } from 'lodash'
4
+ import camelCase from 'lodash/camelCase'
5
+ import snakeCase from 'lodash/snakeCase'
5
6
 
6
7
  import type { PaginationProps } from '../Pagination/Pagination'
7
8
 
@@ -2,7 +2,7 @@ import React, { useLayoutEffect, useState } from 'react'
2
2
  import { ThemeProvider as EmotionThemeProvider } from '@emotion/react'
3
3
  import { CacheProvider } from '@emotion/react'
4
4
  import createCache from '@emotion/cache'
5
- import { merge } from 'lodash'
5
+ import merge from 'lodash/merge'
6
6
  import { Box, BoxProps } from '../Box'
7
7
 
8
8
  import defaultTheme from '../system/default-theme'
@@ -58,15 +58,16 @@ function mergeThemes(a: Theme = {}, b: Theme = {}) {
58
58
  }
59
59
  }
60
60
 
61
+ type Props = {
62
+ theme?: Theme
63
+ children: React.ReactNode
64
+ } & BoxProps
65
+
61
66
  export function ThemeProvider({
62
67
  theme = emptyTheme,
63
68
  children,
64
69
  ...boxProps
65
- }: {
66
- theme?: Theme
67
- children: React.ReactNode
68
- boxProps?: BoxProps
69
- }) {
70
+ }: Props) {
70
71
  const [mergedTheme, setMergedTheme] = useState(() =>
71
72
  mergeThemes(defaultTheme, theme)
72
73
  )
@@ -18,6 +18,9 @@ import {
18
18
  getTimeFromDate,
19
19
  } from './utils'
20
20
 
21
+ const MIN_MINUTE = 0
22
+ const MAX_MINUTE = 59
23
+
21
24
  type Props = {
22
25
  /**
23
26
  * An array of keys to ignore when pushed.
@@ -119,9 +122,31 @@ class TimeField extends Component<Props> {
119
122
  }
120
123
 
121
124
  handleHoursKeyDown = (event) => {
125
+ const hour = Number(event.target.value)
126
+ const isTwelveHourClock = this.props.twelveHourClock
127
+ const isHourEleven = HOURS_TO_NOON - 1
128
+ const maxHour = isTwelveHourClock ? HOURS_TO_NOON : HOURS_IN_DAY - 1
129
+ const minHour = isTwelveHourClock ? 1 : 0
130
+
122
131
  if (event.key === 'ArrowRight') {
123
132
  this.inputBox.focus(1)
124
133
  }
134
+
135
+ if (event.key === 'ArrowUp' && isTwelveHourClock && hour === isHourEleven) {
136
+ this.toggleMeridiem()
137
+ }
138
+
139
+ if (event.key === 'ArrowUp' && hour === maxHour) {
140
+ this.updateHours(1)
141
+ }
142
+
143
+ if (event.key === 'ArrowDown' && isTwelveHourClock && hour === minHour) {
144
+ this.toggleMeridiem()
145
+ }
146
+
147
+ if (event.key === 'ArrowDown' && hour === minHour) {
148
+ this.updateHours(-1)
149
+ }
125
150
  }
126
151
 
127
152
  handleMinutesChange = (minutes) => {
@@ -129,6 +154,8 @@ class TimeField extends Component<Props> {
129
154
  }
130
155
 
131
156
  handleMinutesKeyDown = (event) => {
157
+ const minute = Number(event.target.value)
158
+
132
159
  if (this.props.ignoredKeys.includes(event.key)) {
133
160
  return
134
161
  }
@@ -138,6 +165,12 @@ class TimeField extends Component<Props> {
138
165
  if (event.key === 'ArrowRight') {
139
166
  this.inputBox.focus(2)
140
167
  }
168
+ if (event.key === 'ArrowUp' && minute >= MAX_MINUTE) {
169
+ this.updateMinutes(1)
170
+ }
171
+ if (event.key === 'ArrowDown' && minute <= MIN_MINUTE) {
172
+ this.updateMinutes(-1)
173
+ }
141
174
  }
142
175
 
143
176
  handleAMPMKeyDown = (event) => {
@@ -214,8 +247,8 @@ class TimeField extends Component<Props> {
214
247
  fontVariantNumeric="tabular-nums"
215
248
  moveFocusOnMax
216
249
  value={minutes}
217
- min={0}
218
- max={59}
250
+ min={MIN_MINUTE}
251
+ max={MAX_MINUTE}
219
252
  pad={2}
220
253
  grow={0}
221
254
  width="2ch"
@@ -230,7 +263,7 @@ class TimeField extends Component<Props> {
230
263
  highlightOnInteraction
231
264
  value={this.state.meridiem}
232
265
  grow={0}
233
- width={3}
266
+ width="2em"
234
267
  textAlign="center"
235
268
  aria-label="AM/PM"
236
269
  onChange={noop} // prevent React warnings
@@ -121,7 +121,7 @@ it('does not change the meridiem on arrow key if arrow keys are ignored', () =>
121
121
  expect(meridiemInput).toHaveValue('PM')
122
122
  })
123
123
 
124
- it('changes the value of hours on arrow up or arrow down', () => {
124
+ it('increment hour on arrow up', () => {
125
125
  const { hourInput } = setup( { hours: 1, minutes: 42 })
126
126
 
127
127
  expect(hourInput).toHaveValue("01")
@@ -129,6 +129,110 @@ it('changes the value of hours on arrow up or arrow down', () => {
129
129
  expect(hourInput).toHaveValue("02")
130
130
  })
131
131
 
132
+ it('increment hour if arrow up on minutes exceeds max value', () => {
133
+ const { hourInput, minuteInput } = setup( { hours: 1, minutes: 59 })
134
+
135
+ expect(hourInput).toHaveValue("01")
136
+ userEvent.type(minuteInput, '{arrowup}')
137
+ expect(hourInput).toHaveValue("02")
138
+ })
139
+
140
+ it('decrement hour on arrow down', () => {
141
+ const { hourInput } = setup( { hours: 2, minutes: 42 })
142
+
143
+ expect(hourInput).toHaveValue("02")
144
+ userEvent.type(hourInput, '{arrowdown}')
145
+ expect(hourInput).toHaveValue("01")
146
+ })
147
+
148
+ it('decrement hour if arrow down on minutes exceeds min value', () => {
149
+ const { hourInput, minuteInput } = setup( { hours: 2, minutes: 0 })
150
+
151
+ expect(hourInput).toHaveValue("02")
152
+ userEvent.type(minuteInput, '{arrowdown}')
153
+ expect(hourInput).toHaveValue("01")
154
+ })
155
+
156
+ it('increment minute on arrow up', () => {
157
+ const { minuteInput } = setup( { hours: 1, minutes: 42 })
158
+
159
+ expect(minuteInput).toHaveValue("42")
160
+ userEvent.type(minuteInput, '{arrowup}')
161
+ expect(minuteInput).toHaveValue("43")
162
+ })
163
+
164
+ it('decrement minute on arrow down', () => {
165
+ const { minuteInput } = setup( { hours: 2, minutes: 43 })
166
+
167
+ expect(minuteInput).toHaveValue("43")
168
+ userEvent.type(minuteInput, '{arrowdown}')
169
+ expect(minuteInput).toHaveValue("42")
170
+ })
171
+
172
+ it('set minute to min value if arrow up on minutes exceeds max value', () => {
173
+ const { minuteInput } = setup( { hours: 1, minutes: 59 })
174
+
175
+ expect(minuteInput).toHaveValue("59")
176
+ userEvent.type(minuteInput, '{arrowup}')
177
+ expect(minuteInput).toHaveValue("00")
178
+ })
179
+
180
+ it('set minute to max value if arrow down on minutes exceeds min value', () => {
181
+ const { minuteInput } = setup( { hours: 1, minutes: 0 })
182
+
183
+ expect(minuteInput).toHaveValue("00")
184
+ userEvent.type(minuteInput, '{arrowdown}')
185
+ expect(minuteInput).toHaveValue("59")
186
+ })
187
+
188
+ it('toggle meridiem if arrow up on hour exceeds eleven', () => {
189
+ const { hourInput, meridiemInput } = setup( { hours: 11 })
190
+
191
+ expect(meridiemInput).toHaveValue('AM')
192
+ userEvent.type(hourInput, '{arrowup}')
193
+ expect(meridiemInput).toHaveValue('PM')
194
+ })
195
+
196
+ it('toggle meridiem if arrow down on hour exceeds min value', () => {
197
+ const { hourInput, meridiemInput } = setup( { hours: 1 })
198
+
199
+ expect(meridiemInput).toHaveValue('AM')
200
+ userEvent.type(hourInput, '{arrowdown}')
201
+ expect(meridiemInput).toHaveValue('PM')
202
+ })
203
+
204
+ it('set 12 hour clock to min value if arrow up on hour exceeds max value', () => {
205
+ const { hourInput } = setup( { hours: 12 })
206
+
207
+ expect(hourInput).toHaveValue("12")
208
+ userEvent.type(hourInput, '{arrowup}')
209
+ expect(hourInput).toHaveValue("01")
210
+ })
211
+
212
+ it('set 24 hour clock to min value if arrow up on hour exceeds max value', () => {
213
+ const { hourInput } = setup( { hours: 23, twelveHourClock: false })
214
+
215
+ expect(hourInput).toHaveValue("23")
216
+ userEvent.type(hourInput, '{arrowup}')
217
+ expect(hourInput).toHaveValue("00")
218
+ })
219
+
220
+ it('set 12 hour clock to max value if arrow down on hour exceeds min value', () => {
221
+ const { hourInput } = setup( { hours: 1 })
222
+
223
+ expect(hourInput).toHaveValue("01")
224
+ userEvent.type(hourInput, '{arrowdown}')
225
+ expect(hourInput).toHaveValue("12")
226
+ })
227
+
228
+ it('set 24 hour clock to max value if arrow down on hour exceeds min value', () => {
229
+ const { hourInput } = setup( { hours: 0, twelveHourClock: false })
230
+
231
+ expect(hourInput).toHaveValue("00")
232
+ userEvent.type(hourInput, '{arrowdown}')
233
+ expect(hourInput).toHaveValue("23")
234
+ })
235
+
132
236
  it('does not change the value of hours on arrow up or arrow down if ignored', () => {
133
237
  const { hourInput } = setup( { hours: 1, minutes: 42, ignoredKeys: ['ArrowUp', 'ArrowDown'] })
134
238