Skip to content

Commit af726ba

Browse files
authored
chore(datepicker): fix infinite re-render (#295)
2 parents 403ed3c + 7e03c66 commit af726ba

File tree

8 files changed

+104
-48
lines changed

8 files changed

+104
-48
lines changed

cypress/apps/react19/package-lock.json

Lines changed: 17 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cypress/apps/react19/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"dependencies": {
1313
"@govtechsg/sgds": "^2.3.6",
1414
"@govtechsg/sgds-react": "file:../../../dist",
15+
"bootstrap-icons": "^1.12.1",
1516
"react": "^19.0.0",
1617
"react-bootstrap": "^2.10.7",
1718
"react-dom": "^19.0.0"

cypress/apps/react19/src/App.jsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1-
import './App.css'
2-
import Modal from "./components/ModalCom"
3-
import Accordion from "./components/AccordionCom"
4-
import "@govtechsg/sgds/css/sgds.css"
1+
import './App.css';
2+
import 'bootstrap-icons/font/bootstrap-icons.css';
3+
import Modal from './components/ModalCom';
4+
import Accordion from './components/AccordionCom';
5+
import DatepickerCom from './components/DatepickerCom';
6+
import '@govtechsg/sgds/css/sgds.css';
7+
import ButtonCom from './components/ButtonCom';
58

69
function App() {
710
return (
811
<>
9-
<Modal></Modal>
10-
<Accordion/>
12+
<Modal></Modal>
13+
<Accordion />
14+
<DatepickerCom />
15+
<ButtonCom />
1116
</>
12-
)
17+
);
1318
}
1419

15-
export default App
20+
export default App;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Button } from '@govtechsg/sgds-react';
2+
3+
const ButtonCom = () => {
4+
return (
5+
<>
6+
<Button>Click Me</Button>
7+
</>
8+
);
9+
};
10+
11+
export default ButtonCom;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { DatePicker } from '@govtechsg/sgds-react';
2+
3+
const DatepickerCom = () => {
4+
return (
5+
<>
6+
<DatePicker />
7+
<DatePicker mode="range" />
8+
</>
9+
);
10+
};
11+
12+
export default DatepickerCom;

src/DatePicker/DatePicker.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,10 @@ export const DatePicker: BsPrefixRefForwardingComponent<
567567
const enterDateRange = (event: React.ChangeEvent<HTMLInputElement>) => {
568568
const enteredDate = event.target.value;
569569

570-
if (enteredDate === `${dateFormat.toLowerCase()} - ${dateFormat.toLowerCase()}`) {
570+
if (
571+
enteredDate ===
572+
`${dateFormat.toLowerCase()} - ${dateFormat.toLowerCase()}`
573+
) {
571574
return clear();
572575
}
573576

@@ -586,7 +589,7 @@ export const DatePicker: BsPrefixRefForwardingComponent<
586589
const dateEndBeforeMaxDate = props.maxDate
587590
? setTimeToNoon(dateEnd) <= setTimeToNoon(new Date(props.maxDate))
588591
: true;
589-
592+
590593
if (
591594
isValidDate(start, dateFormat) &&
592595
isValidDate(end, dateFormat) &&
@@ -810,7 +813,7 @@ export const DatePicker: BsPrefixRefForwardingComponent<
810813
);
811814
}
812815
}
813-
}, [props.initialValue, isRange, displayDate, dateFormat]);
816+
}, [props.initialValue, isRange, dateFormat]);
814817

815818
React.useEffect(() => {
816819
setDatepickerMenuId(generateId('datepicker', 'ul'));
@@ -840,7 +843,7 @@ export const DatePicker: BsPrefixRefForwardingComponent<
840843
const resetFocusedDate = getFocusedDate();
841844
updateFocusedDate(resetFocusedDate);
842845
}
843-
}, [showCalendar, displayDate]);
846+
}, [showCalendar]);
844847

845848
const ariaLabelsForMenu = {
846849
day: 'Choose date',

tests/DatePicker/DatePicker.test.tsx

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,14 @@ describe('DatePicker', () => {
232232
});
233233
it('onChangeDate fn fires when input is made empty', async () => {
234234
const onChangeDate = jest.fn();
235-
const onClear = jest.fn()
236-
const { container } = render(<DatePicker onChangeDate={onChangeDate} onClear={onClear} initialValue={new Date(2024, 9, 3)} />);
235+
const onClear = jest.fn();
236+
const { container } = render(
237+
<DatePicker
238+
onChangeDate={onChangeDate}
239+
onClear={onClear}
240+
initialValue={new Date(2024, 9, 3)}
241+
/>
242+
);
237243
const input = container.querySelector('input') as HTMLInputElement;
238244
input?.focus();
239245
fireEvent.change(input, { target: { value: '' } });
@@ -245,15 +251,27 @@ describe('DatePicker', () => {
245251
});
246252
it('when mode=range onChangeDate fn fires when input is made empty', async () => {
247253
const onChangeDate = jest.fn();
248-
const onClear = jest.fn()
249-
const { container } = render(<DatePicker mode="range" onChangeDate={onChangeDate} onClear={onClear} initialValue={{start: new Date(2024, 9, 3), end: new Date(2024, 9, 4)}} />);
254+
const onClear = jest.fn();
255+
const { container } = render(
256+
<DatePicker
257+
mode="range"
258+
onChangeDate={onChangeDate}
259+
onClear={onClear}
260+
initialValue={{
261+
start: new Date(2024, 9, 3),
262+
end: new Date(2024, 9, 4),
263+
}}
264+
/>
265+
);
250266
const input = container.querySelector('input') as HTMLInputElement;
251267
input?.focus();
252268
fireEvent.change(input, { target: { value: '' } });
253269
await waitFor(() => {
254270
expect(onChangeDate).toHaveBeenCalledTimes(1);
255271
expect(onClear).toHaveBeenCalledTimes(1);
256-
expect(container.querySelector('input')?.value).toEqual('dd/mm/yyyy - dd/mm/yyyy');
272+
expect(container.querySelector('input')?.value).toEqual(
273+
'dd/mm/yyyy - dd/mm/yyyy'
274+
);
257275
});
258276
});
259277
it('when mode=range, onChangeDate fn fires when an start and end valid dates are typed in the Datepicker Input', async () => {
@@ -2082,34 +2100,18 @@ describe('Datepicker Range mode', () => {
20822100
});
20832101

20842102
it('when initialValue passed in, either start or end should have same value as displayDate, else a console.error should appear', async () => {
2085-
const initialValue = { start: new Date('2020-01-01'), end: new Date() };
2086-
const consoleSpy = jest
2087-
.spyOn(console, 'error')
2088-
.mockImplementation(() => {});
2089-
2090-
const { rerender } = render(
2091-
<DatePicker mode="range" initialValue={initialValue} />
2092-
);
2093-
expect(consoleSpy).not.toHaveBeenCalled();
2094-
2095-
const newInitialValue = {
2103+
const initialValue = {
20962104
start: new Date('2022-12-12'),
20972105
end: new Date('2022-12-14'),
20982106
};
2099-
rerender(
2100-
<DatePicker
2101-
mode="range"
2102-
initialValue={newInitialValue}
2103-
displayDate={new Date('2022-12-12')}
2104-
/>
2105-
);
2106-
2107-
expect(consoleSpy).not.toHaveBeenCalled();
2107+
const consoleSpy = jest
2108+
.spyOn(console, 'error')
2109+
.mockImplementation(() => {});
21082110

2109-
rerender(
2111+
render(
21102112
<DatePicker
21112113
mode="range"
2112-
initialValue={newInitialValue}
2114+
initialValue={initialValue}
21132115
displayDate={new Date('2022-12-20')}
21142116
/>
21152117
);

tests/DatePicker/MonthView.test.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,22 @@ describe('MonthView a11y', () => {
6565
const { getByText } = render(
6666
<MonthView
6767
onClickMonth={mockFn}
68-
selectedDate={new Date(2024, 3, 11)}
68+
selectedDate={selectedDate}
6969
displayDate={displayDate}
7070
show={true}
7171
onChangeMonth={mockOnChangeMonth}
7272
handleTabPressOnCalendarBody={mockhandleTabPressOnCalendarBody}
7373
monthRefs={monthRefs}
7474
/>
7575
);
76-
expect(getByText('Jan').getAttribute('aria-label')).toEqual(
77-
`Current month, January ${selectedDate.getFullYear()}`
76+
expect(
77+
getByText(
78+
selectedDate.toLocaleString('default', { month: 'long' })
79+
).getAttribute('aria-label')
80+
).toEqual(
81+
`Current month, ${selectedDate.toLocaleString('default', {
82+
month: 'long',
83+
})} ${selectedDate.getFullYear()}`
7884
);
7985
});
8086
it('selected month should have aria-selected=true, mode=single', () => {
@@ -119,7 +125,7 @@ describe('MonthView a11y', () => {
119125
);
120126
}
121127
});
122-
it("current month should be indicated in aria-label" , () => {
128+
it('current month should be indicated in aria-label', () => {
123129
const { getByText } = render(
124130
<MonthView
125131
onClickMonth={mockFn}
@@ -131,10 +137,10 @@ describe('MonthView a11y', () => {
131137
monthRefs={monthRefs}
132138
/>
133139
);
134-
const currentMonth = new Date().getMonth()
135-
const currentMonthName = MONTH_LABELS[currentMonth].slice(0,3)
140+
const currentMonth = new Date().getMonth();
141+
const currentMonthName = MONTH_LABELS[currentMonth].slice(0, 3);
136142
expect(getByText(currentMonthName).getAttribute('aria-label')).toContain(
137-
"Current month"
143+
'Current month'
138144
);
139-
})
145+
});
140146
});

0 commit comments

Comments
 (0)