From 1fb49110a70d5a426692966461321b9a251ae8e9 Mon Sep 17 00:00:00 2001 From: Matthew Oliveira Date: Tue, 11 Jul 2023 11:38:58 -0400 Subject: [PATCH] fix(date-picker): prevent range inputs from clearing prematurely (#10603) ### Related Ticket(s) https://github.com/carbon-design-system/carbon-for-ibm-dotcom/issues/10501 https://jsw.ibm.com/browse/ADCMS-3429 Upstream flatpickr tickets / PRs - https://github.com/flatpickr/flatpickr/pull/2801 - https://github.com/flatpickr/flatpickr/issues/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 --- .../carbon-web-components/package.json | 2 +- .../components/date-picker/focus-plugin.ts | 13 +- .../components/date-picker/range-plugin.ts | 112 +++++++++++++++--- 3 files changed, 105 insertions(+), 22 deletions(-) diff --git a/web-components/packages/carbon-web-components/package.json b/web-components/packages/carbon-web-components/package.json index 0a9329451da3..1a77747f43f4 100644 --- a/web-components/packages/carbon-web-components/package.json +++ b/web-components/packages/carbon-web-components/package.json @@ -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" diff --git a/web-components/packages/carbon-web-components/src/components/date-picker/focus-plugin.ts b/web-components/packages/carbon-web-components/src/components/date-picker/focus-plugin.ts index f5554e81ef68..061f1c58ecca 100644 --- a/web-components/packages/carbon-web-components/src/components/date-picker/focus-plugin.ts +++ b/web-components/packages/carbon-web-components/src/components/date-picker/focus-plugin.ts @@ -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. @@ -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 `` 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 `` gets focus. if ( rootNode.nodeType === Node.DOCUMENT_NODE || rootNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE diff --git a/web-components/packages/carbon-web-components/src/components/date-picker/range-plugin.ts b/web-components/packages/carbon-web-components/src/components/date-picker/range-plugin.ts index c0f95a440340..4bf1fa73cb35 100644 --- a/web-components/packages/carbon-web-components/src/components/date-picker/range-plugin.ts +++ b/web-components/packages/carbon-web-components/src/components/date-picker/range-plugin.ts @@ -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. @@ -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. @@ -23,7 +41,7 @@ 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 `` for the end date and user selects a date. + * when it's launched from the `` for the end date and user selects a date. * Workaround for: https://github.com/flatpickr/flatpickr/issues/1958 * * Ensures that the `` in shadow DOM is treated as part of Flatpickr UI, * by ensuring such `` hits `.contains()` search from `fp.config.ignoredFocusElements`. @@ -31,23 +49,63 @@ import { Plugin } from 'flatpickr/dist/types/options'; */ 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) => { + 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; @@ -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 } + ); + } }, }); };