Skip to content

Commit

Permalink
fix(date-picker): prevent range inputs from clearing prematurely (car…
Browse files Browse the repository at this point in the history
…bon-design-system#10603)

### Related Ticket(s)

carbon-design-system/carbon-for-ibm-dotcom#10501
https://jsw.ibm.com/browse/ADCMS-3429

Upstream flatpickr tickets / PRs

- flatpickr/flatpickr#2801
- flatpickr/flatpickr#2918

### Description

Fixes bugs with the date picker. Specifically we fix the reported bug where bluring from the input after a range selection would clear the first input. We then end up fixing a couple of other related bugs to get us back to a usable range input. Notably the usability issue reported wherin there ends up being a lot of month paging for far-past or far-future ranges to get a complete range is not addressed here, as that behavior is by design.

Note that, as dicussed with @IgnacioBecerra, @kennylam and @andy-blum  at office hours, the underlying [flatpickr](https://github.com/flatpickr/flatpickr) library is not seeing a concerted effort at maintenance. There are many bug reports piled up in their issue queue ([over 600 at time of writing](https://github.com/flatpickr/flatpickr/issues)) and not much commit activity or late. As a result, here I've updated to the latest tagged version of flatpickr (4.6.13), and then used [`yarn patch`](https://yarnpkg.com/cli/patch) to include necessary bug fixes. Upstream PR's and issues are linked above.

### Changelog

**Changed**

- date picker with range: fix a bug with date picker where bluring away from the first input after having populated a range value, would clear the first input
- date picker with range: fix a bug where loosing focus from the calendar picker after having populated a range value, would clear the first input
- date picker with range: fix a bug where clicking either input after having set a range would not show the selected range set in the calendar
  • Loading branch information
m4olivei authored Jul 11, 2023
1 parent 1920d7c commit 1fb4911
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 22 deletions.
2 changes: 1 addition & 1 deletion web-components/packages/carbon-web-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"dependencies": {
"@babel/runtime": "^7.16.3",
"carbon-components": "10.58.3",
"flatpickr": "4.6.9",
"flatpickr": "4.6.13",
"lit-element": "^2.5.1",
"lit-html": "^1.4.1",
"lodash-es": "^4.17.21"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @license
*
* Copyright IBM Corp. 2019, 2022
* Copyright IBM Corp. 2019, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
Expand Down Expand Up @@ -87,13 +87,16 @@ export default (config: DatePickerFocusPluginConfig): Plugin =>
if (
isOpen &&
calendarContainer.contains(target as Node) &&
!calendarContainer.contains(relatedTarget as Node)
!calendarContainer.contains(relatedTarget as Node) &&
relatedTarget !== config.inputFrom &&
relatedTarget !== config.inputTo
) {
Promise.resolve().then(() => {
const rootNode = (target as Node).getRootNode();
// This `blur` event handler can be called from Flatpickr's code cleaning up calenar dropdown's DOM,
// and changing focus in such condition causes removing an orphaned DOM node,
// because Flatpickr redraws the calendar dropdown when the `<input>` gets focus.
// This `blur` event handler can be called from Flatpickr's code,
// cleaning up the calendar dropdowns DOM. Changing focus in such
// condition causes removing an orphaned DOM node, because Flatpickr
// redraws the calendar dropdown when the `<input>` gets focus.
if (
rootNode.nodeType === Node.DOCUMENT_NODE ||
rootNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @license
*
* Copyright IBM Corp. 2019, 2022
* Copyright IBM Corp. 2019, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
Expand All @@ -10,6 +10,24 @@
import rangePlugin, { Config } from 'flatpickr/dist/plugins/rangePlugin';
import { Instance as FlatpickrInstance } from 'flatpickr/dist/types/instance';
import { Plugin } from 'flatpickr/dist/types/options';
import on from 'carbon-components/es/globals/js/misc/on';
import Handle from '../../globals/internal/handle';

/**
* `FlatpickrInstance` with additional properties used for `range` plugin.
*/
export interface ExtendedFlatpickrInstanceRangePlugin
extends FlatpickrInstance {
/**
* The handle for `blur` event handler for from date range input.
*/
_hBXCEDatePickerRangePluginOnBlurFrom?: Handle | null;

/**
* The handle for `blur` event handler for to date range input.
*/
_hBXCEDatePickerRangePluginOnBlurTo?: Handle | null;
}

/**
* @param config Plugin configuration.
Expand All @@ -23,31 +41,71 @@ import { Plugin } from 'flatpickr/dist/types/options';
* We'd like to reset the selection when user re-opens calendar dropdown and re-selects a date.
* Workaround for: https://github.com/flatpickr/flatpickr/issues/1958
* * Disables the logic in Flatpickr `rangePlugin` that closes the calendar dropdown
* when it's lauched from the `<input>` for the end date and user selects a date.
* when it's launched from the `<input>` for the end date and user selects a date.
* Workaround for: https://github.com/flatpickr/flatpickr/issues/1958
* * Ensures that the `<input>` in shadow DOM is treated as part of Flatpickr UI,
* by ensuring such `<input>` hits `.contains()` search from `fp.config.ignoredFocusElements`.
* Without that, Flatpickr clears the `<input>` when end date hasn't been selected yet (which we don't want).
*/
export default (config: Config): Plugin => {
const factory = rangePlugin({ position: 'left', ...config });
return (fp: FlatpickrInstance) => {
return (fp: ExtendedFlatpickrInstanceRangePlugin) => {
const origRangePlugin = factory(fp);
const { onReady: origOnReady } = origRangePlugin;
return Object.assign(origRangePlugin, {
onChange() {},
onPreCalendarPosition() {},
onValueUpdate(selectedDates) {
const [startDateFormatted, endDateFormatted] = selectedDates.map(
(item) => fp.formatDate(item, fp.config.dateFormat)

const getDateStrFromInputs = (dates: Array<string>) => {
return dates
.filter((value) => value)
.filter(
(d, i, arr) =>
fp.config.mode !== 'range' ||
fp.config.enableTime ||
arr.indexOf(d) === i
)
.join(
fp.config.mode !== 'range'
? fp.config.conjunction
: fp.l10n.rangeSeparator
);
if (startDateFormatted) {
fp._input.value = startDateFormatted;
}
if (endDateFormatted) {
(config.input as HTMLInputElement).value = endDateFormatted;
}
},
};

const handleBlur = (event: FocusEvent) => {
event.stopPropagation();
const firstInput = fp._input;
const secondInput = config.input as HTMLInputElement;
const isInput =
event.target === firstInput || event.target === secondInput;
const valueChanged =
getDateStrFromInputs([firstInput.value, secondInput.value]) !==
fp.getDateStr();
const relatedTargetIsCalendar =
event.relatedTarget &&
event.relatedTarget instanceof Node &&
fp.calendarContainer.contains(event.relatedTarget);

if (isInput && valueChanged && !relatedTargetIsCalendar) {
fp.setDate(
[firstInput.value, secondInput.value],
true,
firstInput === fp.altInput
? fp.config.altFormat
: fp.config.dateFormat
);
}
};

const release = () => {
if (fp._hBXCEDatePickerRangePluginOnBlurFrom) {
fp._hBXCEDatePickerRangePluginOnBlurFrom =
fp._hBXCEDatePickerRangePluginOnBlurFrom.release();
}
if (fp._hBXCEDatePickerRangePluginOnBlurTo) {
fp._hBXCEDatePickerRangePluginOnBlurTo =
fp._hBXCEDatePickerRangePluginOnBlurTo.release();
}
};

return Object.assign(origRangePlugin, {
onReady() {
(origOnReady as Function).call(this);
const { ignoredFocusElements } = fp.config;
Expand All @@ -56,6 +114,28 @@ export default (config: Config): Plugin => {
.map((elem) => elem.shadowRoot as any)
.filter(Boolean)
);

// Setup event listeners for the blur even on both inputs. In the case
// of the first input, we're overriding the blur event handler from
// the library to fix it by setting it on the capture phase and then
// stopping propagation. This is necessary b/c the library does not take
// the range plugin into consideration when it calls setDate.
// Workaround for: https://github.com/flatpickr/flatpickr/issues/2918
release();
if (fp.config.allowInput) {
fp._hBXCEDatePickerRangePluginOnBlurFrom = on(
fp._input,
'blur',
handleBlur,
{ capture: true }
);
fp._hBXCEDatePickerRangePluginOnBlurTo = on(
config.input as HTMLInputElement,
'blur',
handleBlur,
{ capture: true }
);
}
},
});
};
Expand Down

0 comments on commit 1fb4911

Please sign in to comment.