Skip to content

Commit eedcd9a

Browse files
authored
refactor: rewrite rangePlugin in typescript (#20401)
1 parent f390a33 commit eedcd9a

File tree

4 files changed

+146
-68
lines changed

4 files changed

+146
-68
lines changed

packages/react/src/components/DatePicker/DatePicker-test.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright IBM Corp. 2016, 2023
2+
* Copyright IBM Corp. 2016, 2025
33
*
44
* This source code is licensed under the Apache-2.0 license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -866,6 +866,88 @@ describe('Range date picker', () => {
866866
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
867867
consoleWarnSpy.mockRestore();
868868
});
869+
870+
describe('rangePlugin', () => {
871+
it('should set start and end input values correctly when calling setDate with triggerChange=false', async () => {
872+
const ref = React.createRef();
873+
874+
render(
875+
<DatePicker ref={ref} datePickerType="range" value={undefined}>
876+
<DatePickerInput
877+
id="start"
878+
placeholder="mm/dd/yyyy"
879+
labelText="Start date"
880+
data-testid="start-input"
881+
/>
882+
<DatePickerInput
883+
id="end"
884+
placeholder="mm/dd/yyyy"
885+
labelText="End date"
886+
data-testid="end-input"
887+
/>
888+
</DatePicker>
889+
);
890+
891+
const fp = ref.current.calendar;
892+
const start = await screen.findByTestId('start-input');
893+
const end = await screen.findByTestId('end-input');
894+
895+
// Call `setDate` with two dates and `triggerChange` equal to `false`,
896+
// which is where the `rangePlugin` logic should be triggered to set
897+
// values on both inputs.
898+
fp.setDate(['01/05/2025', '01/10/2025'], false, 'm/d/Y');
899+
900+
expect(start.value).toBe('01/05/2025');
901+
expect(end.value).toBe('01/10/2025');
902+
903+
// Verify clearing the end date keeps the start date while clearing the
904+
// end date.
905+
fp.setDate(['01/15/2025', null], false, 'm/d/Y');
906+
907+
expect(start.value).toBe('01/15/2025');
908+
expect(end.value).toBe('');
909+
910+
// Verify that calling `setDate` again with both dates updates both
911+
// fields.
912+
fp.setDate(['02/01/2025', '02/14/2025'], false, 'm/d/Y');
913+
914+
expect(start.value).toBe('02/01/2025');
915+
expect(end.value).toBe('02/14/2025');
916+
});
917+
918+
it('should not write both dates into the first input', async () => {
919+
const ref = React.createRef();
920+
921+
render(
922+
<DatePicker ref={ref} datePickerType="range" value={undefined}>
923+
<DatePickerInput
924+
id="start-2"
925+
placeholder="mm/dd/yyyy"
926+
labelText="Start date"
927+
data-testid="start-input-2"
928+
/>
929+
<DatePickerInput
930+
id="end-2"
931+
placeholder="mm/dd/yyyy"
932+
labelText="End date"
933+
data-testid="end-input-2"
934+
/>
935+
</DatePicker>
936+
);
937+
938+
const fp = ref.current.calendar;
939+
const start = await screen.findByTestId('start-input-2');
940+
const end = await screen.findByTestId('end-input-2');
941+
942+
// When `triggerChange` is `false`, flatpickr's default behavior could
943+
// leave both dates reflected only in the first input. The plugin should
944+
// ensure each input is updated correctly.
945+
fp.setDate(['03/03/2025', '03/09/2025'], false, 'm/d/Y');
946+
947+
expect(start.value).toBe('03/03/2025');
948+
expect(end.value).toBe('03/09/2025');
949+
});
950+
});
869951
});
870952

871953
describe('Date picker with locale', () => {

packages/react/src/components/DatePicker/DatePicker.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import l10n from 'flatpickr/dist/l10n/index';
2222
import DatePickerInput from '../DatePickerInput';
2323
import { appendToPlugin } from './plugins/appendToPlugin';
2424
import carbonFlatpickrFixEventsPlugin from './plugins/fixEventsPlugin';
25-
import carbonFlatpickrRangePlugin from './plugins/rangePlugin';
25+
import { rangePlugin } from './plugins/rangePlugin';
2626
import { deprecate } from '../../prop-types/deprecate';
2727
import { match, keys } from '../../internal/keyboard';
2828
import { usePrefix } from '../../internal/usePrefix';
@@ -194,19 +194,7 @@ function updateClassNames(calendar, prefix) {
194194
}
195195

196196
export type DatePickerTypes = 'simple' | 'single' | 'range';
197-
export type CalRef = {
198-
inline: boolean;
199-
disableMobile: boolean;
200-
defaultDate: Date;
201-
closeOnSelect: (evt: React.ChangeEvent<HTMLTextAreaElement>) => void;
202-
mode: 'simple' | 'single' | 'range';
203-
allowInput: boolean;
204-
dateFormat: string;
205-
locale: string;
206-
plugins: [];
207-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- https://github.com/carbon-design-system/carbon/issues/20071
208-
clickOpens: any;
209-
};
197+
210198
export interface DatePickerProps {
211199
/**
212200
* Flatpickr prop passthrough enables direct date input, and when set to false,
@@ -498,8 +486,8 @@ const DatePicker = React.forwardRef(function DatePicker(
498486
}
499487
}, [calendarCloseEvent, handleCalendarClose]);
500488

501-
const endInputField = useRef<HTMLTextAreaElement>(null);
502-
const lastFocusedField = useRef<HTMLTextAreaElement>(null);
489+
const endInputField = useRef<HTMLInputElement>(null);
490+
const lastFocusedField = useRef<HTMLInputElement>(null);
503491
const savedOnChange = useSavedCallback(onChange);
504492

505493
const savedOnOpen = useSavedCallback(onOpen);
@@ -657,8 +645,8 @@ const DatePicker = React.forwardRef(function DatePicker(
657645
parseDate: parseDate,
658646
plugins: [
659647
datePickerType === 'range'
660-
? carbonFlatpickrRangePlugin({
661-
input: endInputField.current,
648+
? rangePlugin({
649+
input: endInputField.current ?? undefined,
662650
})
663651
: () => {},
664652
appendTo

packages/react/src/components/DatePicker/plugins/rangePlugin.js

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright IBM Corp. 2019, 2025
3+
*
4+
* This source code is licensed under the Apache-2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import baseRangePlugin, {
9+
type Config,
10+
} from 'flatpickr/dist/plugins/rangePlugin';
11+
import { Instance } from 'flatpickr/dist/types/instance';
12+
13+
/**
14+
* @param config Plugin configuration.
15+
* @returns An extension of Flatpickr `rangePlugin` that does the following:
16+
* * Better ensures the calendar dropdown is always aligned to the `<input>` for the starting date.
17+
* Workaround for: https://github.com/flatpickr/flatpickr/issues/1944
18+
* * A logic to ensure `fp.setDate()` call won't end up with "startDate to endDate" set to the first `<input>`
19+
*/
20+
export const rangePlugin = (config: Config = {}) => {
21+
const factory = baseRangePlugin(Object.assign({ position: 'left' }, config));
22+
return (fp: Instance) => {
23+
const { setDate: origSetDate } = fp;
24+
25+
const init = () => {
26+
fp.setDate = (dates, triggerChange, format) => {
27+
origSetDate(dates, triggerChange, format);
28+
// If `triggerChange` is `true`, `onValueUpdate` Flatpickr event is fired
29+
// where Flatpickr's range plugin takes care of fixing the first `<input>`
30+
if (!triggerChange && Array.isArray(dates) && dates.length === 2) {
31+
const { formatDate, _input: inputFrom } = fp;
32+
const { input: inputTo } = config;
33+
const inputToElement =
34+
typeof inputTo === 'string'
35+
? document.querySelector(inputTo)
36+
: inputTo;
37+
38+
[inputFrom, inputToElement].forEach((input, i) => {
39+
if (input && input instanceof HTMLInputElement) {
40+
input.value = !dates[i]
41+
? ''
42+
: formatDate(new Date(dates[i]), fp.config.dateFormat);
43+
}
44+
});
45+
}
46+
};
47+
};
48+
49+
const origRangePlugin = factory(fp);
50+
const { onReady: origOnReady } = origRangePlugin;
51+
52+
return Object.assign(origRangePlugin, {
53+
onReady: [init, origOnReady],
54+
onPreCalendarPosition: () => {},
55+
});
56+
};
57+
};

0 commit comments

Comments
 (0)