diff --git a/packages/datetime/src/components/date-range-input/dateRangeInput.tsx b/packages/datetime/src/components/date-range-input/dateRangeInput.tsx index 0b8dd45ace..bbb771591d 100644 --- a/packages/datetime/src/components/date-range-input/dateRangeInput.tsx +++ b/packages/datetime/src/components/date-range-input/dateRangeInput.tsx @@ -630,10 +630,18 @@ export class DateRangeInput extends AbstractPureComponent) => { const isTabPressed = e.key === "Tab"; const isEnterPressed = e.key === "Enter"; + const isEscapeKeyPressed = e.key === "Escape"; const isShiftPressed = e.shiftKey; const { selectedStart, selectedEnd } = this.state; + if (isEscapeKeyPressed) { + this.startInputElement?.blur(); + this.endInputElement?.blur(); + this.setState({ isOpen: false, isStartInputFocused: false, isEndInputFocused: false }); + return; + } + // order of JS events is our enemy here. when tabbing between fields, // this handler will fire in the middle of a focus exchange when no // field is currently focused. we work around this by referring to the diff --git a/packages/datetime/test/components/dateRangeInputTests.tsx b/packages/datetime/test/components/dateRangeInputTests.tsx index deb4c72109..018d4aecab 100644 --- a/packages/datetime/test/components/dateRangeInputTests.tsx +++ b/packages/datetime/test/components/dateRangeInputTests.tsx @@ -602,6 +602,21 @@ describe("", () => { expect(root.state("isOpen"), "popover closed at end").to.be.false; }); + it("pressing Escape closes the popover", () => { + const { root } = wrap(); + root.setState({ isOpen: true }); + + const startInput = getStartInput(root); + startInput.simulate("focus"); + + expect(root.state("isOpen")).to.be.true; + + startInput.simulate("keydown", { key: "Escape" }); + + expect(root.state("isOpen")).to.be.false; + expect(isStartInputFocused(root)).to.be.false; + }); + it("Clicking a date invokes onChange with the new date range and updates the input fields", () => { const defaultValue = [START_DATE, null] as DateRange; diff --git a/packages/datetime2/src/components/date-range-input3/dateRangeInput3.tsx b/packages/datetime2/src/components/date-range-input3/dateRangeInput3.tsx index 58bd965c29..655112a80b 100644 --- a/packages/datetime2/src/components/date-range-input3/dateRangeInput3.tsx +++ b/packages/datetime2/src/components/date-range-input3/dateRangeInput3.tsx @@ -484,10 +484,18 @@ export class DateRangeInput3 extends DateFnsLocalizedComponent) => { const isTabPressed = e.key === "Tab"; const isEnterPressed = e.key === "Enter"; + const isEscapeKeyPressed = e.key === "Escape"; const isShiftPressed = e.shiftKey; const { selectedStart, selectedEnd } = this.state; + if (isEscapeKeyPressed) { + this.startInputElement?.blur(); + this.endInputElement?.blur(); + this.setState({ isOpen: false, isStartInputFocused: false, isEndInputFocused: false }); + return; + } + // order of JS events is our enemy here. when tabbing between fields, // this handler will fire in the middle of a focus exchange when no // field is currently focused. we work around this by referring to the diff --git a/packages/datetime2/test/components/dateRangeInput3Tests.tsx b/packages/datetime2/test/components/dateRangeInput3Tests.tsx index 81760e54e6..c28e1c03ef 100644 --- a/packages/datetime2/test/components/dateRangeInput3Tests.tsx +++ b/packages/datetime2/test/components/dateRangeInput3Tests.tsx @@ -2343,6 +2343,21 @@ describe("", () => { assertDateRangesEqual(onChange.args[1][0], [START_STR, null]); }); + it("pressing Escape closes the popover", () => { + const { root } = wrap(); + root.setState({ isOpen: true }); + + const startInput = getStartInput(root); + startInput.simulate("focus"); + + expect(root.state("isOpen")).to.be.true; + + startInput.simulate("keydown", { key: "Escape" }); + + expect(root.state("isOpen")).to.be.false; + expect(isStartInputFocused(root)).to.be.false; + }); + it("Clicking a date invokes onChange with the new date range and updates the input field text", () => { const onChange = sinon.spy(); const { root, getDayElement } = wrap(