Skip to content

Commit 9aef2c2

Browse files
authored
Merge pull request #814 from rvsia/updateCarbonTime
feat(carbon): add AM/PM, timezones into carbon timepicker
2 parents ecc8af3 + 21a6552 commit 9aef2c2

File tree

5 files changed

+313
-24
lines changed

5 files changed

+313
-24
lines changed

packages/carbon-component-mapper/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
3131
"@babel/preset-env": "^7.1.6",
3232
"@babel/preset-react": "^7.0.0",
33-
"@carbon/icons-react": "^10.17.0",
33+
"@carbon/icons-react": "^10.18.0",
3434
"@semantic-release/git": "^7.0.5",
3535
"@semantic-release/npm": "^5.1.1",
3636
"@types/carbon-components-react": "^7.10.9",
@@ -39,8 +39,8 @@
3939
"babel-jest": "^23.6.0",
4040
"babel-loader": "^8.0.4",
4141
"babel-plugin-lodash": "^3.3.4",
42-
"carbon-components": "^10.19.0",
43-
"carbon-components-react": "^7.19.0",
42+
"carbon-components": "^10.20.0",
43+
"carbon-components-react": "^7.20.0",
4444
"carbon-icons": "^7.0.7",
4545
"clsx": "^1.1.1",
4646
"css-loader": "^1.0.1",

packages/carbon-component-mapper/src/files/time-picker.js

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import PropTypes from 'prop-types';
33
import { useFieldApi } from '@data-driven-forms/react-form-renderer';
44

@@ -9,19 +9,79 @@ import prepareProps from '../common/prepare-props';
99
const TimePicker = (props) => {
1010
const { input, meta, twelveHoursFormat, timezones, validateOnMount, ...rest } = useFieldApi(prepareProps(props));
1111

12+
const [timezone, selectTimezone] = useState(timezones ? timezones[0]?.value : '');
13+
const [format, selectFormat] = useState('AM');
14+
const isMounted = useRef(false);
15+
1216
const invalid = (meta.touched || validateOnMount) && meta.error;
1317

18+
let finalValue = input.value;
19+
if (input.value instanceof Date) {
20+
let [hours = '00', minutes = '00'] = input.value
21+
.toLocaleTimeString('en-us', {
22+
hour12: !!twelveHoursFormat,
23+
timeZone: timezones?.find(({ value }) => value === timezone)?.showAs
24+
})
25+
.split(':');
26+
27+
finalValue = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
28+
}
29+
30+
const enhnancedOnBlur = () => {
31+
let [hours = '00', minutes = '00'] = finalValue?.split(':') || [];
32+
33+
if (!hours || isNaN(hours)) {
34+
hours = '00';
35+
}
36+
37+
if (!minutes || isNaN(minutes)) {
38+
minutes = '00';
39+
}
40+
41+
if (twelveHoursFormat) {
42+
hours = hours % 12;
43+
if (format === 'PM') {
44+
hours = hours + 12;
45+
}
46+
} else {
47+
hours = hours % 24;
48+
}
49+
50+
minutes = minutes % 59;
51+
const enhancedValue = new Date(`Jan 1 2000 ${hours}:${minutes}:00 ${timezone}`);
52+
53+
input.onChange(enhancedValue);
54+
input.onBlur();
55+
};
56+
57+
useEffect(() => {
58+
if (isMounted.current === true) {
59+
enhnancedOnBlur();
60+
} else {
61+
isMounted.current = true;
62+
}
63+
}, [timezone, format]);
64+
1465
return (
15-
<CarbonTimePicker {...input} key={input.name} id={input.name} invalid={Boolean(invalid)} invalidText={invalid || ''} {...rest}>
66+
<CarbonTimePicker
67+
{...input}
68+
value={finalValue}
69+
onBlur={enhnancedOnBlur}
70+
key={input.name}
71+
id={input.name}
72+
invalid={Boolean(invalid)}
73+
invalidText={invalid || ''}
74+
{...rest}
75+
>
1676
{twelveHoursFormat && (
17-
<TimePickerSelect id={`${rest.id || input.name}-12h`}>
77+
<TimePickerSelect labelText="Period" id={`${rest.id || input.name}-12h`} onChange={({ target: { value } }) => selectFormat(value)}>
1878
<SelectItem value="AM" text="AM" />
1979
<SelectItem value="PM" text="PM" />
2080
</TimePickerSelect>
2181
)}
2282
{timezones && (
23-
<TimePickerSelect id={`${rest.id || input.name}-timezones`}>
24-
{timezones.map((tz) => (
83+
<TimePickerSelect labelText="Timezone" id={`${rest.id || input.name}-timezones`} onChange={({ target: { value } }) => selectTimezone(value)}>
84+
{timezones.map(({ showAs, ...tz }) => (
2585
<SelectItem key={tz.value} text={tz.label} {...tz} />
2686
))}
2787
</TimePickerSelect>
@@ -40,8 +100,9 @@ TimePicker.propTypes = {
40100
twelveHoursFormat: PropTypes.bool,
41101
timezones: PropTypes.arrayOf(
42102
PropTypes.shape({
43-
value: PropTypes.string,
44-
label: PropTypes.node
103+
value: PropTypes.string.isRequired,
104+
label: PropTypes.node.isRequired,
105+
showAs: PropTypes.string.isRequired
45106
})
46107
)
47108
};
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import React from 'react';
2+
import { mount } from 'enzyme';
3+
4+
import FormRenderer, { componentTypes } from '@data-driven-forms/react-form-renderer';
5+
6+
import FormTemplate from '../files/form-template';
7+
import componentMapper from '../files/component-mapper';
8+
import { act } from 'react-dom/test-utils';
9+
10+
describe('TimePicker', () => {
11+
let initialProps;
12+
let onSubmit;
13+
let wrapper;
14+
let schema;
15+
16+
beforeEach(() => {
17+
onSubmit = jest.fn();
18+
initialProps = {
19+
onSubmit: (values) => onSubmit(values),
20+
componentMapper,
21+
FormTemplate
22+
};
23+
});
24+
25+
it('change AM/PM', async () => {
26+
schema = {
27+
fields: [
28+
{
29+
component: componentTypes.TIME_PICKER,
30+
name: 'time-picker',
31+
twelveHoursFormat: true
32+
}
33+
]
34+
};
35+
36+
wrapper = mount(<FormRenderer schema={schema} {...initialProps} />);
37+
38+
await act(async () => {
39+
wrapper.find('input').simulate('change', { target: { value: '00:35' } });
40+
});
41+
wrapper.update();
42+
43+
await act(async () => {
44+
wrapper.find('select#time-picker-12h').simulate('change', { target: { value: 'PM' } });
45+
});
46+
wrapper.update();
47+
48+
await act(async () => {
49+
wrapper.find('form').simulate('submit');
50+
});
51+
wrapper.update();
52+
53+
expect(wrapper.find('input').props().value).toEqual('12:35');
54+
expect(onSubmit.mock.calls[0][0]['time-picker'].getHours()).toEqual(12);
55+
expect(onSubmit.mock.calls[0][0]['time-picker'].getMinutes()).toEqual(35);
56+
57+
onSubmit.mockReset();
58+
59+
await act(async () => {
60+
wrapper.find('select#time-picker-12h').simulate('change', { target: { value: 'AM' } });
61+
});
62+
wrapper.update();
63+
64+
await act(async () => {
65+
wrapper.find('form').simulate('submit');
66+
});
67+
wrapper.update();
68+
69+
expect(wrapper.find('input').props().value).toEqual('12:35');
70+
expect(onSubmit.mock.calls[0][0]['time-picker'].getHours()).toEqual(0);
71+
expect(onSubmit.mock.calls[0][0]['time-picker'].getMinutes()).toEqual(35);
72+
});
73+
74+
it('handle invalid date', async () => {
75+
schema = {
76+
fields: [
77+
{
78+
component: componentTypes.TIME_PICKER,
79+
name: 'time-picker'
80+
}
81+
]
82+
};
83+
84+
wrapper = mount(<FormRenderer schema={schema} {...initialProps} />);
85+
86+
await act(async () => {
87+
wrapper.find('input').simulate('change', { target: { value: 'aa:BB' } });
88+
});
89+
wrapper.update();
90+
91+
await act(async () => {
92+
wrapper.find('input').simulate('blur');
93+
});
94+
wrapper.update();
95+
96+
await act(async () => {
97+
wrapper.find('form').simulate('submit');
98+
});
99+
wrapper.update();
100+
101+
expect(wrapper.find('input').props().value).toEqual('00:00');
102+
expect(onSubmit.mock.calls[0][0]['time-picker'].getHours()).toEqual(0);
103+
expect(onSubmit.mock.calls[0][0]['time-picker'].getMinutes()).toEqual(0);
104+
});
105+
106+
it('handle change', async () => {
107+
schema = {
108+
fields: [
109+
{
110+
component: componentTypes.TIME_PICKER,
111+
name: 'time-picker'
112+
}
113+
]
114+
};
115+
116+
wrapper = mount(<FormRenderer schema={schema} {...initialProps} />);
117+
118+
await act(async () => {
119+
wrapper.find('input').simulate('change', { target: { value: '13:87' } });
120+
});
121+
wrapper.update();
122+
123+
await act(async () => {
124+
wrapper.find('input').simulate('blur');
125+
});
126+
wrapper.update();
127+
128+
await act(async () => {
129+
wrapper.find('form').simulate('submit');
130+
});
131+
wrapper.update();
132+
133+
expect(wrapper.find('input').props().value).toEqual('13:28');
134+
expect(onSubmit.mock.calls[0][0]['time-picker'].getHours()).toEqual(13);
135+
expect(onSubmit.mock.calls[0][0]['time-picker'].getMinutes()).toEqual(28);
136+
onSubmit.mockReset();
137+
138+
await act(async () => {
139+
wrapper.find('input').simulate('change', { target: { value: '25:16' } });
140+
});
141+
wrapper.update();
142+
143+
await act(async () => {
144+
wrapper.find('input').simulate('blur');
145+
});
146+
wrapper.update();
147+
148+
await act(async () => {
149+
wrapper.find('form').simulate('submit');
150+
});
151+
wrapper.update();
152+
153+
expect(wrapper.find('input').props().value).toEqual('01:16');
154+
expect(onSubmit.mock.calls[0][0]['time-picker'].getHours()).toEqual(1);
155+
expect(onSubmit.mock.calls[0][0]['time-picker'].getMinutes()).toEqual(16);
156+
});
157+
158+
it('change timezone', async () => {
159+
schema = {
160+
fields: [
161+
{
162+
component: componentTypes.TIME_PICKER,
163+
name: 'time-picker',
164+
twelveHoursFormat: true,
165+
timezones: [
166+
{ label: 'UTC', value: 'UTC', showAs: 'UTC' },
167+
{ label: 'EST', value: 'EAST', showAs: 'Pacific/Easter' }
168+
]
169+
}
170+
]
171+
};
172+
173+
wrapper = mount(<FormRenderer schema={schema} {...initialProps} />);
174+
175+
await act(async () => {
176+
wrapper.find('input').simulate('change', { target: { value: '00:35' } });
177+
});
178+
wrapper.update();
179+
180+
await act(async () => {
181+
wrapper.find('select#time-picker-timezones').simulate('change', { target: { value: 'EST' } });
182+
});
183+
wrapper.update();
184+
185+
await act(async () => {
186+
wrapper.find('form').simulate('submit');
187+
});
188+
wrapper.update();
189+
190+
expect(wrapper.find('input').props().value).toEqual('05:35');
191+
expect(onSubmit.mock.calls[0][0]['time-picker'].getHours()).toEqual(5);
192+
expect(onSubmit.mock.calls[0][0]['time-picker'].getMinutes()).toEqual(35);
193+
194+
onSubmit.mockReset();
195+
196+
await act(async () => {
197+
wrapper.find('select#time-picker-12h').simulate('change', { target: { value: 'UTC' } });
198+
});
199+
wrapper.update();
200+
201+
await act(async () => {
202+
wrapper.find('form').simulate('submit');
203+
});
204+
wrapper.update();
205+
206+
expect(wrapper.find('input').props().value).toEqual('10:35');
207+
expect(onSubmit.mock.calls[0][0]['time-picker'].getHours()).toEqual(10);
208+
expect(onSubmit.mock.calls[0][0]['time-picker'].getMinutes()).toEqual(35);
209+
});
210+
});

packages/react-renderer-demo/src/doc-components/examples-texts/carbon/carbon-time-picker.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,22 @@ This component also accepts all other original props, please see [here](https://
99

1010
### Timezone
1111

12-
Object of `value`, `label`. Extends [SelectItem component](https://react.carbondesignsystem.com/?path=/story/select--default).
12+
Extends [SelectItem component](https://react.carbondesignsystem.com/?path=/story/select--default).
13+
14+
|Option|Description|
15+
|-----|-----------|
16+
|label|A label of the timezone|
17+
|value|A value of the timezone used in `new Date('... ${value}')`|
18+
|showAs|Timezone that will be used to convert the value `value.toLocaleTimeString(..., { ..., timeZone: showsAs })`. Supported timezones can be found [here](https://cloud.google.com/dataprep/docs/html/Supported-Time-Zone-Values_66194188).|
19+
20+
#### value and showAs relationship
21+
22+
To make this component work, please provide corresponding `showAs` for each timezone.
23+
24+
```jsx
25+
{
26+
label: 'PST',
27+
value: 'PST',
28+
showsAs: 'US/Eastern'
29+
}
30+
```

0 commit comments

Comments
 (0)