Skip to content

Commit 9b012ef

Browse files
[DataGrid] GridEditDateCell does not handle timezone correctly (#2918)
1 parent cca95ce commit 9b012ef

File tree

2 files changed

+216
-34
lines changed

2 files changed

+216
-34
lines changed

packages/grid/_modules_/grid/components/cell/GridEditDateCell.tsx

+55-34
Original file line numberDiff line numberDiff line change
@@ -37,66 +37,87 @@ export function GridEditDateCell(props: GridRenderEditCellParams & InputBaseProp
3737
...other
3838
} = props;
3939

40+
const isDateTime = colDef.type === 'dateTime';
4041
const inputRef = React.useRef<HTMLInputElement>();
41-
const [valueState, setValueState] = React.useState(value);
42+
43+
const valueProp = React.useMemo(() => {
44+
let parsedDate: Date | null;
45+
46+
if (value == null) {
47+
parsedDate = null;
48+
} else if (value instanceof Date) {
49+
parsedDate = value;
50+
} else {
51+
parsedDate = new Date((value ?? '').toString());
52+
}
53+
54+
let formattedDate: string;
55+
if (parsedDate == null || Number.isNaN(parsedDate.getTime())) {
56+
formattedDate = '';
57+
} else {
58+
const localDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60 * 1000);
59+
formattedDate = localDate.toISOString().substr(0, isDateTime ? 16 : 10);
60+
}
61+
62+
return {
63+
parsed: parsedDate,
64+
formatted: formattedDate,
65+
};
66+
}, [value, isDateTime]);
67+
68+
const [valueState, setValueState] = React.useState(valueProp);
4269
const rootProps = useGridRootProps();
4370
const ownerState = { classes: rootProps.classes };
4471
const classes = useUtilityClasses(ownerState);
4572

4673
const handleChange = React.useCallback(
4774
(event) => {
48-
const newValue = event.target.value;
49-
setValueState(newValue);
50-
51-
if (newValue === '') {
52-
api.setEditCellValue({ id, field, value: null }, event);
53-
return;
54-
}
55-
56-
const [date, time] = newValue.split('T');
57-
const [year, month, day] = date.split('-');
58-
const dateObj = new Date();
59-
dateObj.setFullYear(Number(year));
60-
dateObj.setMonth(Number(month) - 1);
61-
dateObj.setDate(Number(day));
62-
dateObj.setHours(0, 0, 0, 0);
63-
64-
if (time) {
65-
const [hours, minutes] = time.split(':');
66-
dateObj.setHours(Number(hours), Number(minutes), 0, 0);
75+
const newFormattedDate = event.target.value;
76+
let newParsedDate: Date | null;
77+
78+
if (newFormattedDate === '') {
79+
newParsedDate = null;
80+
} else {
81+
const [date, time] = newFormattedDate.split('T');
82+
const [year, month, day] = date.split('-');
83+
newParsedDate = new Date();
84+
newParsedDate.setFullYear(Number(year));
85+
newParsedDate.setMonth(Number(month) - 1);
86+
newParsedDate.setDate(Number(day));
87+
newParsedDate.setHours(0, 0, 0, 0);
88+
89+
if (time) {
90+
const [hours, minutes] = time.split(':');
91+
newParsedDate.setHours(Number(hours), Number(minutes), 0, 0);
92+
}
6793
}
6894

69-
api.setEditCellValue({ id, field, value: dateObj }, event);
95+
setValueState({ parsed: newParsedDate, formatted: newFormattedDate });
96+
api.setEditCellValue({ id, field, value: newParsedDate }, event);
7097
},
7198
[api, field, id],
7299
);
73100

74-
const isDateTime = colDef.type === 'dateTime';
75-
76-
React.useEffect(() => {
77-
setValueState(value);
78-
}, [value]);
101+
if (
102+
valueProp.parsed !== valueState.parsed &&
103+
valueProp.parsed?.getTime() !== valueState.parsed?.getTime()
104+
) {
105+
setValueState(valueProp);
106+
}
79107

80108
useEnhancedEffect(() => {
81109
if (hasFocus) {
82110
inputRef.current!.focus();
83111
}
84112
}, [hasFocus]);
85113

86-
let valueToDisplay = valueState || '';
87-
if (valueState instanceof Date) {
88-
const offset = valueState.getTimezoneOffset();
89-
const localDate = new Date(valueState.getTime() - offset * 60 * 1000);
90-
valueToDisplay = localDate.toISOString().substr(0, isDateTime ? 16 : 10);
91-
}
92-
93114
return (
94115
<InputBase
95116
inputRef={inputRef}
96117
fullWidth
97118
className={classes.root}
98119
type={isDateTime ? 'datetime-local' : 'date'}
99-
value={valueToDisplay}
120+
value={valueState.formatted}
100121
onChange={handleChange}
101122
{...other}
102123
/>

packages/grid/x-grid/src/tests/editRows.DataGridPro.test.tsx

+161
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,35 @@ import {
1717
fireEvent,
1818
// @ts-expect-error need to migrate helpers to TypeScript
1919
screen,
20+
// @ts-expect-error need to migrate helpers to TypeScript
21+
waitFor,
2022
} from 'test/utils';
2123

2224
const isJSDOM = /jsdom/.test(window.navigator.userAgent);
2325

26+
/**
27+
* Creates a date that is compatible with years before 1901
28+
* `new Date(0001)` creates a date for 1901, not 0001
29+
*/
30+
const generateDate = (
31+
year: number,
32+
month: number,
33+
date?: number,
34+
hours?: number,
35+
minutes?: number,
36+
) => {
37+
const rawDate = new Date();
38+
rawDate.setFullYear(year);
39+
rawDate.setMonth(month);
40+
rawDate.setDate(date ?? 0);
41+
rawDate.setHours(hours ?? 0);
42+
rawDate.setMinutes(minutes ?? 0);
43+
rawDate.setSeconds(0);
44+
rawDate.setMilliseconds(0);
45+
46+
return rawDate.getTime();
47+
};
48+
2449
describe('<DataGridPro /> - Edit Rows', () => {
2550
let clock;
2651
let baselineProps;
@@ -811,6 +836,68 @@ describe('<DataGridPro /> - Edit Rows', () => {
811836
// @ts-expect-error need to migrate helpers to TypeScript
812837
expect(screen.getByRole('cell').querySelector('input')).toHaveFocus();
813838
});
839+
840+
it('should allow external value updates as date', async () => {
841+
const onEditCellPropsChange = spy();
842+
render(
843+
<TestCase
844+
rows={[{ id: 0, date: new Date(2021, 6, 5) }]}
845+
columns={[{ field: 'date', type: 'date', editable: true }]}
846+
onEditCellPropsChange={onEditCellPropsChange}
847+
/>,
848+
);
849+
const cell = getCell(0, 0);
850+
cell.focus();
851+
fireEvent.doubleClick(cell);
852+
const newValue = new Date(2021, 6, 4);
853+
apiRef.current.setEditCellValue({ id: 0, field: 'date', value: newValue });
854+
const input = cell.querySelector('input')!;
855+
await waitFor(() => {
856+
expect(input.value).to.equal('2021-07-04');
857+
});
858+
});
859+
860+
it('should handle all the intermediate dates while editing the value', () => {
861+
const onEditCellPropsChange = spy();
862+
render(
863+
<TestCase
864+
rows={[{ id: 0, date: new Date(2021, 6, 5) }]}
865+
columns={[{ field: 'date', type: 'date', editable: true }]}
866+
onEditCellPropsChange={onEditCellPropsChange}
867+
/>,
868+
);
869+
870+
const cell = getCell(0, 0);
871+
cell.focus();
872+
fireEvent.doubleClick(cell);
873+
const input = cell.querySelector('input')!;
874+
fireEvent.change(input, { target: { value: '' } });
875+
expect(onEditCellPropsChange.lastCall.args[0].props.value).to.equal(null);
876+
fireEvent.change(input, { target: { value: '2021-01-05' } });
877+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
878+
generateDate(2021, 0, 5),
879+
);
880+
fireEvent.change(input, { target: { value: '2021-01-01' } });
881+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
882+
generateDate(2021, 0, 1),
883+
);
884+
fireEvent.change(input, { target: { value: '0001-01-01' } });
885+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
886+
generateDate(1, 0, 1),
887+
);
888+
fireEvent.change(input, { target: { value: '0019-01-01' } });
889+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
890+
generateDate(19, 0, 1),
891+
);
892+
fireEvent.change(input, { target: { value: '0199-01-01' } });
893+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
894+
generateDate(199, 0, 1),
895+
);
896+
fireEvent.change(input, { target: { value: '1999-01-01' } });
897+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
898+
generateDate(1999, 0, 1),
899+
);
900+
});
814901
});
815902

816903
describe('column type: dateTime', () => {
@@ -862,6 +949,80 @@ describe('<DataGridPro /> - Edit Rows', () => {
862949
// @ts-expect-error need to migrate helpers to TypeScript
863950
expect(screen.getByRole('cell').querySelector('input')).toHaveFocus();
864951
});
952+
953+
it('should allow external value updates as date', async () => {
954+
const onEditCellPropsChange = spy();
955+
render(
956+
<TestCase
957+
rows={[{ id: 0, date: new Date(2021, 6, 5, 14, 30) }]}
958+
columns={[{ field: 'date', type: 'dateTime', editable: true }]}
959+
onEditCellPropsChange={onEditCellPropsChange}
960+
/>,
961+
);
962+
const cell = getCell(0, 0);
963+
cell.focus();
964+
fireEvent.doubleClick(cell);
965+
const newValue = new Date(2021, 6, 4, 17, 30);
966+
apiRef.current.setEditCellValue({ id: 0, field: 'date', value: newValue });
967+
const input = cell.querySelector('input')!;
968+
await waitFor(() => {
969+
expect(input.value).to.equal('2021-07-04T17:30');
970+
});
971+
});
972+
973+
it('should handle all the intermediate dates while editing the value', () => {
974+
const onEditCellPropsChange = spy();
975+
render(
976+
<TestCase
977+
rows={[{ id: 0, date: new Date(2021, 6, 5, 14, 30) }]}
978+
columns={[{ field: 'date', type: 'dateTime', editable: true }]}
979+
onEditCellPropsChange={onEditCellPropsChange}
980+
/>,
981+
);
982+
983+
const cell = getCell(0, 0);
984+
cell.focus();
985+
fireEvent.doubleClick(cell);
986+
const input = cell.querySelector('input')!;
987+
fireEvent.change(input, { target: { value: '' } });
988+
expect(onEditCellPropsChange.lastCall.args[0].props.value).to.equal(null);
989+
fireEvent.change(input, { target: { value: '2021-01-05T14:30' } });
990+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
991+
generateDate(2021, 0, 5, 14, 30),
992+
);
993+
fireEvent.change(input, { target: { value: '2021-01-01T14:30' } });
994+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
995+
generateDate(2021, 0, 1, 14, 30),
996+
);
997+
fireEvent.change(input, { target: { value: '0001-01-01T14:30' } });
998+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
999+
generateDate(1, 0, 1, 14, 30),
1000+
);
1001+
fireEvent.change(input, { target: { value: '0019-01-01T14:30' } });
1002+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
1003+
generateDate(19, 0, 1, 14, 30),
1004+
);
1005+
fireEvent.change(input, { target: { value: '0199-01-01T14:30' } });
1006+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
1007+
generateDate(199, 0, 1, 14, 30),
1008+
);
1009+
fireEvent.change(input, { target: { value: '1999-01-01T14:30' } });
1010+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
1011+
generateDate(1999, 0, 1, 14, 30),
1012+
);
1013+
fireEvent.change(input, { target: { value: '1999-01-01T20:30' } });
1014+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
1015+
generateDate(1999, 0, 1, 20, 30),
1016+
);
1017+
fireEvent.change(input, { target: { value: '1999-01-01T20:02' } });
1018+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
1019+
generateDate(1999, 0, 1, 20, 2),
1020+
);
1021+
fireEvent.change(input, { target: { value: '1999-01-01T20:25' } });
1022+
expect(onEditCellPropsChange.lastCall.args[0].props.value.getTime()).to.equal(
1023+
generateDate(1999, 0, 1, 20, 25),
1024+
);
1025+
});
8651026
});
8661027

8671028
it('should call onCellEditCommit with the correct params', () => {

0 commit comments

Comments
 (0)