Skip to content

Commit 67e6bdb

Browse files
committed
fix(material): Improve usability of date renderers
Previously, data wasn't cleared when the input field was emptied. This commit will set the value to 'undefined' when the input is empty or invalid upon blurring. During editing, the data is only updated when the current input is valid. Closes #2183
1 parent da3321b commit 67e6bdb

File tree

7 files changed

+135
-26
lines changed

7 files changed

+135
-26
lines changed

packages/material-renderers/src/controls/MaterialDateControl.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
THE SOFTWARE.
2424
*/
2525
import merge from 'lodash/merge';
26-
import React, { useMemo } from 'react';
26+
import React, { useCallback, useMemo, useState } from 'react';
2727
import {
2828
ControlProps,
2929
isDateControl,
@@ -35,7 +35,12 @@ import { withJsonFormsControlProps } from '@jsonforms/react';
3535
import { FormHelperText, Hidden } from '@mui/material';
3636
import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
3737
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
38-
import { createOnChangeHandler, getData, useFocus } from '../util';
38+
import {
39+
createOnBlurHandler,
40+
createOnChangeHandler,
41+
getData,
42+
useFocus,
43+
} from '../util';
3944

4045
export const MaterialDateControl = (props: ControlProps) => {
4146
const [focused, onFocus, onBlur] = useFocus();
@@ -62,6 +67,8 @@ export const MaterialDateControl = (props: ControlProps) => {
6267
appliedUiSchemaOptions.showUnfocusedDescription
6368
);
6469

70+
const [key, setKey] = useState<number>(0);
71+
6572
const format = appliedUiSchemaOptions.dateFormat ?? 'YYYY-MM-DD';
6673
const saveFormat = appliedUiSchemaOptions.dateSaveFormat ?? 'YYYY-MM-DD';
6774

@@ -73,20 +80,36 @@ export const MaterialDateControl = (props: ControlProps) => {
7380
? errors
7481
: null;
7582
const secondFormHelperText = showDescription && !isValid ? errors : null;
83+
84+
const updateChild = useCallback(() => setKey((key) => key + 1), []);
85+
7686
const onChange = useMemo(
7787
() => createOnChangeHandler(path, handleChange, saveFormat),
7888
[path, handleChange, saveFormat]
7989
);
8090

91+
const onBlurHandler = useMemo(
92+
() =>
93+
createOnBlurHandler(
94+
path,
95+
handleChange,
96+
format,
97+
saveFormat,
98+
updateChild,
99+
onBlur
100+
),
101+
[path, handleChange, format, saveFormat, updateChild]
102+
);
81103
const value = getData(data, saveFormat);
82104

83105
return (
84106
<Hidden xsUp={!visible}>
85107
<LocalizationProvider dateAdapter={AdapterDayjs}>
86108
<DatePicker
109+
key={key}
87110
label={label}
88111
value={value}
89-
onChange={onChange}
112+
onAccept={onChange}
90113
format={format}
91114
views={views}
92115
disabled={!enabled}
@@ -109,7 +132,7 @@ export const MaterialDateControl = (props: ControlProps) => {
109132
},
110133
InputLabelProps: data ? { shrink: true } : undefined,
111134
onFocus: onFocus,
112-
onBlur: onBlur,
135+
onBlur: onBlurHandler,
113136
},
114137
}}
115138
/>

packages/material-renderers/src/controls/MaterialDateTimeControl.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import React, { useMemo } from 'react';
25+
import React, { useCallback, useMemo, useState } from 'react';
2626
import merge from 'lodash/merge';
2727
import {
2828
ControlProps,
@@ -35,7 +35,12 @@ import { withJsonFormsControlProps } from '@jsonforms/react';
3535
import { FormHelperText, Hidden } from '@mui/material';
3636
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
3737
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
38-
import { createOnChangeHandler, getData, useFocus } from '../util';
38+
import {
39+
createOnBlurHandler,
40+
createOnChangeHandler,
41+
getData,
42+
useFocus,
43+
} from '../util';
3944

4045
export const MaterialDateTimeControl = (props: ControlProps) => {
4146
const [focused, onFocus, onBlur] = useFocus();
@@ -64,7 +69,9 @@ export const MaterialDateTimeControl = (props: ControlProps) => {
6469
);
6570

6671
const format = appliedUiSchemaOptions.dateTimeFormat ?? 'YYYY-MM-DD HH:mm';
67-
const saveFormat = appliedUiSchemaOptions.dateTimeSaveFormat ?? undefined;
72+
const saveFormat = appliedUiSchemaOptions.dateTimeSaveFormat ?? undefined; //'YYYY-MM-DDTHH:mm:Z';
73+
74+
const [key, setKey] = useState<number>(0);
6875

6976
const views = appliedUiSchemaOptions.views ?? [
7077
'year',
@@ -80,20 +87,35 @@ export const MaterialDateTimeControl = (props: ControlProps) => {
8087
: null;
8188
const secondFormHelperText = showDescription && !isValid ? errors : null;
8289

90+
const updateChild = useCallback(() => setKey((key) => key + 1), []);
91+
8392
const onChange = useMemo(
8493
() => createOnChangeHandler(path, handleChange, saveFormat),
8594
[path, handleChange, saveFormat]
8695
);
8796

97+
const onBlurHandler = useMemo(
98+
() =>
99+
createOnBlurHandler(
100+
path,
101+
handleChange,
102+
format,
103+
saveFormat,
104+
updateChild,
105+
onBlur
106+
),
107+
[path, handleChange, format, saveFormat, updateChild]
108+
);
88109
const value = getData(data, saveFormat);
89110

90111
return (
91112
<Hidden xsUp={!visible}>
92113
<LocalizationProvider dateAdapter={AdapterDayjs}>
93114
<DateTimePicker
115+
key={key}
94116
label={label}
95117
value={value}
96-
onChange={onChange}
118+
onAccept={onChange}
97119
format={format}
98120
ampm={!!appliedUiSchemaOptions.ampm}
99121
views={views}
@@ -117,7 +139,7 @@ export const MaterialDateTimeControl = (props: ControlProps) => {
117139
},
118140
InputLabelProps: data ? { shrink: true } : undefined,
119141
onFocus: onFocus,
120-
onBlur: onBlur,
142+
onBlur: onBlurHandler,
121143
},
122144
}}
123145
/>

packages/material-renderers/src/controls/MaterialTimeControl.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import React, { useMemo } from 'react';
25+
import React, { useCallback, useMemo, useState } from 'react';
2626
import merge from 'lodash/merge';
2727
import {
2828
ControlProps,
@@ -35,7 +35,12 @@ import { withJsonFormsControlProps } from '@jsonforms/react';
3535
import { FormHelperText, Hidden } from '@mui/material';
3636
import { TimePicker, LocalizationProvider } from '@mui/x-date-pickers';
3737
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
38-
import { createOnChangeHandler, getData, useFocus } from '../util';
38+
import {
39+
createOnBlurHandler,
40+
createOnChangeHandler,
41+
getData,
42+
useFocus,
43+
} from '../util';
3944

4045
export const MaterialTimeControl = (props: ControlProps) => {
4146
const [focused, onFocus, onBlur] = useFocus();
@@ -56,6 +61,8 @@ export const MaterialTimeControl = (props: ControlProps) => {
5661
const appliedUiSchemaOptions = merge({}, config, uischema.options);
5762
const isValid = errors.length === 0;
5863

64+
const [key, setKey] = useState<number>(0);
65+
5966
const showDescription = !isDescriptionHidden(
6067
visible,
6168
description,
@@ -75,19 +82,35 @@ export const MaterialTimeControl = (props: ControlProps) => {
7582
: null;
7683
const secondFormHelperText = showDescription && !isValid ? errors : null;
7784

85+
const updateChild = useCallback(() => setKey((key) => key + 1), []);
86+
7887
const onChange = useMemo(
7988
() => createOnChangeHandler(path, handleChange, saveFormat),
8089
[path, handleChange, saveFormat]
8190
);
8291

92+
const onBlurHandler = useMemo(
93+
() =>
94+
createOnBlurHandler(
95+
path,
96+
handleChange,
97+
format,
98+
saveFormat,
99+
updateChild,
100+
onBlur
101+
),
102+
[path, handleChange, format, saveFormat, updateChild]
103+
);
83104
const value = getData(data, saveFormat);
105+
84106
return (
85107
<Hidden xsUp={!visible}>
86108
<LocalizationProvider dateAdapter={AdapterDayjs}>
87109
<TimePicker
110+
key={key}
88111
label={label}
89112
value={value}
90-
onChange={onChange}
113+
onAccept={onChange}
91114
format={format}
92115
ampm={!!appliedUiSchemaOptions.ampm}
93116
views={views}
@@ -111,7 +134,7 @@ export const MaterialTimeControl = (props: ControlProps) => {
111134
},
112135
InputLabelProps: data ? { shrink: true } : undefined,
113136
onFocus: onFocus,
114-
onBlur: onBlur,
137+
onBlur: onBlurHandler,
115138
},
116139
}}
117140
/>

packages/material-renderers/src/util/datejs.tsx

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,58 @@ export const createOnChangeHandler =
88
(
99
path: string,
1010
handleChange: (path: string, value: any) => void,
11-
saveFormat: string | undefined
11+
saveFormat: string
1212
) =>
13-
(time: dayjs.Dayjs) => {
14-
if (!time) {
13+
(value: dayjs.Dayjs) => {
14+
if (!value) {
1515
handleChange(path, undefined);
16-
return;
16+
} else if (value.toString() !== 'Invalid Date') {
17+
const formatedDate = formatDate(value, saveFormat);
18+
handleChange(path, formatedDate);
1719
}
18-
const result = dayjs(time).format(saveFormat);
19-
handleChange(path, result);
2020
};
2121

22+
export const createOnBlurHandler =
23+
(
24+
path: string,
25+
handleChange: (path: string, value: any) => void,
26+
format: string,
27+
saveFormat: string,
28+
rerenderChild: () => void,
29+
onBlur: () => void
30+
) =>
31+
(e: React.FocusEvent<HTMLTextAreaElement | HTMLInputElement, Element>) => {
32+
const date = dayjs(e.target.value, format);
33+
const formatedDate = formatDate(date, saveFormat);
34+
if (formatedDate.toString() === 'Invalid Date') {
35+
handleChange(path, undefined);
36+
rerenderChild();
37+
} else {
38+
handleChange(path, formatedDate);
39+
}
40+
onBlur();
41+
};
42+
43+
const formatDate = (date: dayjs.Dayjs, saveFormat: string) => {
44+
let formatedDate = date.format(saveFormat);
45+
// Workaround to address a bug in Dayjs (https://github.com/iamkun/dayjs/issues/1849)
46+
if (
47+
date.year() < 1000 &&
48+
date.year() > 100 &&
49+
saveFormat.length > formatedDate.length
50+
) {
51+
const indexOfYear = saveFormat.indexOf('YYYY');
52+
if (indexOfYear !== -1) {
53+
formatedDate = [
54+
formatedDate.slice(0, indexOfYear),
55+
0,
56+
formatedDate.slice(indexOfYear),
57+
].join('');
58+
}
59+
}
60+
return formatedDate;
61+
};
62+
2263
export const getData = (
2364
data: any,
2465
saveFormat: string | undefined

packages/material-renderers/test/renderers/MaterialDateControl.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ describe('Material date control', () => {
225225
);
226226
const input = wrapper.find('input').first();
227227
(input.getDOMNode() as HTMLInputElement).value = '1961-04-12';
228-
input.simulate('change', input);
228+
input.simulate('blur', input);
229229
expect(onChangeData.data.foo).toBe('1961-04-12');
230230
});
231231

@@ -421,7 +421,7 @@ describe('Material date control', () => {
421421
expect(input.props().value).toBe('1980/06');
422422

423423
(input.getDOMNode() as HTMLInputElement).value = '1961/04';
424-
input.simulate('change', input);
424+
input.simulate('blur', input);
425425
expect(onChangeData.data.foo).toBe('04---1961');
426426
});
427427
});

packages/material-renderers/test/renderers/MaterialDateTimeControl.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ describe('Material date time control', () => {
228228
);
229229
const input = wrapper.find('input').first();
230230
(input.getDOMNode() as HTMLInputElement).value = '1961-12-12 20:15';
231-
input.simulate('change', input);
231+
input.simulate('blur', input);
232232
expect(onChangeData.data.foo).toBe(dayjs('1961-12-12 20:15').format());
233233
});
234234

@@ -427,7 +427,7 @@ describe('Material date time control', () => {
427427
expect(input.props().value).toBe('23-04-80 01:37:pm');
428428

429429
(input.getDOMNode() as HTMLInputElement).value = '10-12-05 11:22:am';
430-
input.simulate('change', input);
430+
input.simulate('blur', input);
431431
expect(onChangeData.data.foo).toBe('2005/12/10 11:22 am');
432432
});
433433
});

packages/material-renderers/test/renderers/MaterialTimeControl.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ describe('Material time control', () => {
225225
);
226226
const input = wrapper.find('input').first();
227227
(input.getDOMNode() as HTMLInputElement).value = '08:40';
228-
input.simulate('change', input);
229-
expect(onChangeData.data.foo).toBe('08:40:05');
228+
input.simulate('blur', input);
229+
expect(onChangeData.data.foo).toBe('08:40:00');
230230
});
231231

232232
it('should update via action', () => {
@@ -421,7 +421,7 @@ describe('Material time control', () => {
421421
expect(input.props().value).toBe('02-13');
422422

423423
(input.getDOMNode() as HTMLInputElement).value = '12-01';
424-
input.simulate('change', input);
424+
input.simulate('blur', input);
425425
expect(onChangeData.data.foo).toBe('1//12 am');
426426
});
427427
});

0 commit comments

Comments
 (0)