Skip to content

Commit 6bd5f0d

Browse files
[LENS-626] InputTime Component (#737)
1 parent 348d7d2 commit 6bd5f0d

File tree

17 files changed

+1244
-51
lines changed

17 files changed

+1244
-51
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [0.7.26] - 2020-04-14
99

10+
- `InputTime` component
11+
1012
### Added
1113

1214
- `ActionListManager` component

packages/components/src/Form/Inputs/InputChips/InputChips.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ test('values are removed by clicking remove on the chip', () => {
139139

140140
test('new values are validated', () => {
141141
const onChangeMock = jest.fn()
142-
const onInvalidMock = jest.fn()
142+
const onValidationFailMock = jest.fn()
143143
const onDuplicateMock = jest.fn()
144144

145145
const validate = jest.fn((value) => value === 'tag1')
@@ -149,7 +149,7 @@ test('new values are validated', () => {
149149
values={[]}
150150
placeholder="type here"
151151
validate={validate}
152-
onInvalid={onInvalidMock}
152+
onValidationFail={onValidationFailMock}
153153
onDuplicate={onDuplicateMock}
154154
/>
155155
)
@@ -159,7 +159,7 @@ test('new values are validated', () => {
159159
expect(onChangeMock).not.toHaveBeenCalled()
160160
// invalid value remains in the input
161161
expect(input).toHaveValue('tag2')
162-
expect(onInvalidMock).toHaveBeenCalledWith(['tag2'])
162+
expect(onValidationFailMock).toHaveBeenCalledWith(['tag2'])
163163

164164
// value should be trimmed before validation
165165
fireEvent.change(input, { target: { value: ' tag1,' } })

packages/components/src/Form/Inputs/InputChips/InputChips.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
*/
4141

4242
export interface InputChipsProps
43-
extends Omit<InputChipsCommonProps, 'onInvalid'>,
43+
extends Omit<InputChipsCommonProps, 'onValidationFail'>,
4444
InputChipsControlProps,
4545
Partial<InputChipsInputControlProps> {
4646
/**
@@ -50,7 +50,7 @@ export interface InputChipsProps
5050
/**
5151
* callback when values fail validation
5252
*/
53-
onInvalid?: (values: string[]) => void
53+
onValidationFail?: (values: string[]) => void
5454
/**
5555
* callback when values are duplicates
5656
*/
@@ -96,7 +96,7 @@ export const InputChipsInternal = forwardRef(
9696
inputValue: controlledInputValue,
9797
onInputChange,
9898
validate,
99-
onInvalid,
99+
onValidationFail,
100100
onDuplicate,
101101
...props
102102
}: InputChipsProps,
@@ -142,7 +142,7 @@ export const InputChipsInternal = forwardRef(
142142
}
143143

144144
if (invalidValues.length > 0) {
145-
onInvalid && onInvalid(invalidValues)
145+
onValidationFail && onValidationFail(invalidValues)
146146
}
147147
if (duplicateValues.length > 0) {
148148
onDuplicate && onDuplicate(duplicateValues)
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2020 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import React from 'react'
28+
import { fireEvent } from '@testing-library/react'
29+
import { renderWithTheme } from '@looker/components-test-utils'
30+
import { InputTime, InputTimeProps } from './InputTime'
31+
32+
const globalConsole = global.console
33+
34+
beforeEach(() => {
35+
global.console = ({
36+
error: jest.fn(),
37+
warn: jest.fn(),
38+
} as unknown) as Console
39+
})
40+
41+
afterEach(() => {
42+
jest.resetAllMocks()
43+
global.console = globalConsole
44+
})
45+
46+
const selectSubInputs = (mockProps: any) => {
47+
const { getByTestId } = renderWithTheme(<InputTime {...mockProps} />)
48+
const inputHour = getByTestId('input-hour')
49+
const inputMinute = getByTestId('input-minute')
50+
const inputPeriod = getByTestId('input-period')
51+
52+
return {
53+
inputHour,
54+
inputMinute,
55+
inputPeriod,
56+
}
57+
}
58+
59+
test('fires onChange ONLY when all fields are filled in', () => {
60+
const mockProps: InputTimeProps = {
61+
onChange: jest.fn(),
62+
}
63+
64+
const { inputHour, inputMinute, inputPeriod } = selectSubInputs(mockProps)
65+
66+
fireEvent.keyDown(inputHour, {
67+
key: '1',
68+
keyCode: 49,
69+
})
70+
71+
expect(mockProps.onChange).not.toHaveBeenCalled()
72+
73+
fireEvent.keyDown(inputMinute, {
74+
key: '1',
75+
keyCode: 49,
76+
})
77+
78+
expect(mockProps.onChange).not.toHaveBeenCalled()
79+
80+
fireEvent.keyDown(inputPeriod, {
81+
key: 'p',
82+
})
83+
84+
// convert '01:01 PM' to 24-hour time ('13:01') and call onChange
85+
expect(mockProps.onChange).toHaveBeenCalledWith('13:01')
86+
})
87+
88+
test('fires onChange when any sub-input is cleared', () => {
89+
const mockProps: InputTimeProps = {
90+
onChange: jest.fn(),
91+
value: '14:52',
92+
}
93+
94+
const { inputHour } = selectSubInputs(mockProps)
95+
96+
expect(mockProps.onChange).not.toHaveBeenCalled()
97+
expect((inputHour as HTMLInputElement).value).toEqual('02')
98+
99+
// reset sub-input value
100+
fireEvent.keyDown(inputHour, {
101+
key: 'Backspace',
102+
})
103+
104+
expect((inputHour as HTMLInputElement).value).toEqual('')
105+
expect(mockProps.onChange).toHaveBeenCalledWith(undefined)
106+
})
107+
108+
test('accepts a 24-hour time value', () => {
109+
const mockProps: InputTimeProps = {
110+
value: '14:52',
111+
}
112+
113+
const { inputHour, inputMinute, inputPeriod } = selectSubInputs(mockProps)
114+
115+
expect((inputHour as HTMLInputElement).value).toEqual('02')
116+
expect((inputMinute as HTMLInputElement).value).toEqual('52')
117+
expect((inputPeriod as HTMLInputElement).value).toEqual('PM')
118+
})
119+
120+
test('accepts a 24-hour time defautValue', () => {
121+
const mockProps: InputTimeProps = {
122+
defaultValue: '14:52',
123+
}
124+
125+
const { inputHour, inputMinute, inputPeriod } = selectSubInputs(mockProps)
126+
127+
expect((inputHour as HTMLInputElement).value).toEqual('02')
128+
expect((inputMinute as HTMLInputElement).value).toEqual('52')
129+
expect((inputPeriod as HTMLInputElement).value).toEqual('PM')
130+
})
131+
132+
test('ignores invalid time value string', () => {
133+
const mockProps: InputTimeProps = {
134+
value: 'cheesecake',
135+
}
136+
137+
const { inputHour, inputMinute, inputPeriod } = selectSubInputs(mockProps)
138+
139+
expect((inputHour as HTMLInputElement).value).toEqual('')
140+
expect((inputMinute as HTMLInputElement).value).toEqual('')
141+
expect((inputPeriod as HTMLInputElement).value).toEqual('')
142+
143+
// eslint-disable-next-line no-console
144+
expect(console.error).toHaveBeenCalledWith(
145+
'Invalid time ("cheesecake") passed to <InputTime />. Value should be formatted as a 24-hour string (e.g. value="02:00" or value="23:15").'
146+
)
147+
})
148+
149+
test('clears child input if an invalid number is entered', () => {
150+
const mockProps: InputTimeProps = {
151+
onChange: jest.fn(),
152+
}
153+
154+
const { inputMinute } = selectSubInputs(mockProps)
155+
156+
fireEvent.keyDown(inputMinute, {
157+
key: '7',
158+
keyCode: 55,
159+
})
160+
161+
expect((inputMinute as HTMLInputElement).value).toEqual('07')
162+
163+
// pressing 7 a second time would be an attempt to enter "77" into the minute field
164+
fireEvent.keyDown(inputMinute, {
165+
key: '7',
166+
keyCode: 55,
167+
})
168+
169+
// invalid input causes field to clear
170+
expect((inputMinute as HTMLInputElement).value).toEqual('')
171+
})
172+
173+
test('renders 24 hour formatted time', () => {
174+
const mockProps: any = {
175+
format: '24h',
176+
value: '23:32',
177+
}
178+
179+
const { getByTestId, queryByTestId } = renderWithTheme(
180+
<InputTime {...mockProps} />
181+
)
182+
const inputHour = getByTestId('input-hour')
183+
const inputMinute = getByTestId('input-minute')
184+
const inputPeriod = queryByTestId('input-period')
185+
186+
expect((inputHour as HTMLInputElement).value).toEqual('23')
187+
expect((inputMinute as HTMLInputElement).value).toEqual('32')
188+
expect(inputPeriod).not.toBeInTheDocument()
189+
})
190+
191+
test('up/down arrow keys cycle through possible values', () => {
192+
const mockProps: InputTimeProps = {}
193+
194+
const { inputHour, inputMinute, inputPeriod } = selectSubInputs(mockProps)
195+
196+
// HOUR
197+
// --------------------------------------
198+
fireEvent.keyDown(inputHour, {
199+
key: 'ArrowUp',
200+
keyCode: 38,
201+
})
202+
expect((inputHour as HTMLInputElement).value).toEqual('01')
203+
204+
fireEvent.keyDown(inputHour, {
205+
key: 'ArrowDown',
206+
keyCode: 40,
207+
})
208+
expect((inputHour as HTMLInputElement).value).toEqual('12')
209+
210+
// MINUTE
211+
// --------------------------------------
212+
fireEvent.keyDown(inputMinute, {
213+
key: 'ArrowUp',
214+
keyCode: 38,
215+
})
216+
expect((inputMinute as HTMLInputElement).value).toEqual('01')
217+
218+
fireEvent.keyDown(inputMinute, {
219+
key: 'ArrowDown',
220+
keyCode: 40,
221+
})
222+
fireEvent.keyDown(inputMinute, {
223+
key: 'ArrowDown',
224+
keyCode: 40,
225+
})
226+
expect((inputMinute as HTMLInputElement).value).toEqual('59')
227+
228+
// PERIOD
229+
// --------------------------------------
230+
fireEvent.keyDown(inputPeriod, {
231+
key: 'ArrowUp',
232+
keyCode: 38,
233+
})
234+
expect((inputPeriod as HTMLInputElement).value).toEqual('PM')
235+
236+
fireEvent.keyDown(inputPeriod, {
237+
key: 'ArrowDown',
238+
keyCode: 40,
239+
})
240+
expect((inputPeriod as HTMLInputElement).value).toEqual('AM')
241+
})
242+
243+
test('fires onValidationFail callback when an invalid time value prop is passed in', () => {
244+
const mockProps: InputTimeProps = {
245+
onValidationFail: jest.fn(),
246+
value: 'Stardate 2004.14',
247+
}
248+
249+
expect(mockProps.onValidationFail).not.toHaveBeenCalled()
250+
251+
const { inputHour, inputMinute, inputPeriod } = selectSubInputs(mockProps)
252+
253+
expect(mockProps.onValidationFail).toHaveBeenCalledTimes(1)
254+
expect((inputHour as HTMLInputElement).value).toEqual('')
255+
expect((inputMinute as HTMLInputElement).value).toEqual('')
256+
expect((inputPeriod as HTMLInputElement).value).toEqual('')
257+
})

0 commit comments

Comments
 (0)