diff --git a/packages/datetime/src/common/classes.ts b/packages/datetime/src/common/classes.ts index 0de9a8da501..3862eaef192 100644 --- a/packages/datetime/src/common/classes.ts +++ b/packages/datetime/src/common/classes.ts @@ -36,6 +36,7 @@ export const DATEPICKER_FOOTER = `${DATEPICKER}-footer`; export const DATEPICKER_MONTH_SELECT = `${DATEPICKER}-month-select`; export const DATEPICKER_YEAR_SELECT = `${DATEPICKER}-year-select`; export const DATEPICKER_NAVBAR = `${DATEPICKER}-navbar`; +export const DATEPICKER_NAVBUTTON = `DayPicker-NavButton`; export const DATEPICKER_TIMEPICKER_WRAPPER = `${DATEPICKER}-timepicker-wrapper`; export const DATERANGEPICKER = `${NS}-daterangepicker`; diff --git a/packages/datetime/src/dateInput.tsx b/packages/datetime/src/dateInput.tsx index b7167521e61..72c18c4a973 100644 --- a/packages/datetime/src/dateInput.tsx +++ b/packages/datetime/src/dateInput.tsx @@ -186,7 +186,9 @@ export class DateInput extends AbstractPureComponent2 | null = null; private popoverContentEl: HTMLElement | null = null; - private lastElementInPopover: HTMLElement | null = null; + // Last element in popover that is tabbable, and the one that triggers popover closure + // when the user press TAB on it + private lastTabbableElement: HTMLElement | null = null; private refHandlers = { input: isRefObject(this.props.inputProps?.inputRef) @@ -205,8 +207,20 @@ export class DateInput extends AbstractPureComponent2 { + if ( + e.key === "Tab" && + !e.shiftKey && + this.lastTabbableElement.classList.contains(Classes.DATEPICKER_DAY) + ) { + this.setState({ isOpen: false }); + } + this.props.dayPickerProps.onDayKeyDown?.(day, modifiers, e); + }, // dom elements for the updated month is not available when // onMonthChange is called. setTimeout is necessary to wait // for the updated month to be rendered @@ -406,6 +420,20 @@ export class DateInput extends AbstractPureComponent2 { + // Popover contents are well structured, but the selector will need + // to be updated if more focusable components are added in the future + const tabbableElements = this.popoverContentEl.querySelectorAll("input, [tabindex]:not([tabindex='-1'])"); + const numOfElements = tabbableElements.length; + // Keep track of the last focusable element in popover and add + // a blur handler, so that when: + // * user tabs to the next element, popover closes + // * focus moves to element within popover, popover stays open + const lastTabbableElement = numOfElements > 0 ? tabbableElements[numOfElements - 1] : null; + + return lastTabbableElement as HTMLElement; + }; + // focus DOM event listener (not React event) private handlePopoverBlur = (e: FocusEvent) => { let relatedTarget = e.relatedTarget as HTMLElement; @@ -413,50 +441,40 @@ export class DateInput extends AbstractPureComponent2 { if (this.popoverContentEl != null) { - // If current activeElement exists inside popover content, a month - // change has triggered and this element should be lastTabbableElement - let lastTabbableElement = this.popoverContentEl.contains(document.activeElement) - ? document.activeElement - : undefined; - // Popover contents are well structured, but the selector will need - // to be updated if more focusable components are added in the future - if (lastTabbableElement == null) { - const tabbableElements = this.popoverContentEl.querySelectorAll( - "input, [tabindex]:not([tabindex='-1'])", - ); - const numOfElements = tabbableElements.length; - if (numOfElements > 0) { - // Keep track of the last focusable element in popover and add - // a blur handler, so that when: - // * user tabs to the next element, popover closes - // * focus moves to element within popover, popover stays open - lastTabbableElement = tabbableElements[numOfElements - 1]; - } - } this.unregisterPopoverBlurHandler(); - this.lastElementInPopover = lastTabbableElement as HTMLElement; - this.lastElementInPopover.addEventListener("blur", this.handlePopoverBlur); + this.lastTabbableElement = this.getLastTabbableElement(); + this.lastTabbableElement.addEventListener("blur", this.handlePopoverBlur); } }; private unregisterPopoverBlurHandler = () => { - if (this.lastElementInPopover != null) { - this.lastElementInPopover.removeEventListener("blur", this.handlePopoverBlur); + if (this.lastTabbableElement != null) { + this.lastTabbableElement.removeEventListener("blur", this.handlePopoverBlur); } }; diff --git a/packages/datetime/test/dateInputTests.tsx b/packages/datetime/test/dateInputTests.tsx index 609bb044396..619ee18e96d 100644 --- a/packages/datetime/test/dateInputTests.tsx +++ b/packages/datetime/test/dateInputTests.tsx @@ -87,14 +87,13 @@ describe("", () => { assert.isFalse(wrapper.find(Popover).prop("isOpen")); }); - it("Popover closes when first day of the month is blurred", () => { + it("Popover closes when tabbing on first day of the month", () => { const defaultValue = new Date(2018, Months.FEBRUARY, 6, 15, 0, 0, 0); const wrapper = mount(); wrapper.find("input").simulate("focus").simulate("blur"); // First day of month is the only .DayPicker-Day with tabIndex == 0 const tabbables = wrapper.find(Popover).find(".DayPicker-Day").filter({ tabIndex: 0 }); - const firstDay = tabbables.getDOMNode() as HTMLElement; - firstDay.dispatchEvent(createFocusEvent("blur")); + tabbables.simulate("keydown", { key: "Tab" }); // manually updating wrapper is required with enzyme 3 // ref: https://github.com/airbnb/enzyme/blob/master/docs/guides/migration-from-2-to-3.md#for-mount-updates-are-sometimes-required-when-they-werent-before wrapper.update();