-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[release/1.x] Close dateinput popover #2093
Changes from 3 commits
7f20c79
85bb3d5
031cfdf
4f49526
e6b3f41
9fd606d
76253fe
dafca2c
8745263
69dd5ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import * as classNames from "classnames"; | |
import * as moment from "moment"; | ||
import * as React from "react"; | ||
import * as ReactDayPicker from "react-day-picker"; | ||
import * as ReactDOM from "react-dom"; | ||
|
||
import { | ||
AbstractComponent, | ||
|
@@ -184,6 +185,7 @@ export class DateInput extends AbstractComponent<IDateInputProps, IDateInputStat | |
public static displayName = "Blueprint.DateInput"; | ||
|
||
private inputRef: HTMLElement = null; | ||
private lastPopoverElement: HTMLElement = null; | ||
|
||
public constructor(props?: IDateInputProps, context?: any) { | ||
super(props, context); | ||
|
@@ -198,15 +200,29 @@ export class DateInput extends AbstractComponent<IDateInputProps, IDateInputStat | |
}; | ||
} | ||
|
||
public componentWillUnmount() { | ||
this.unregisterPopoverBlurHandler(); | ||
super.componentWillUnmount(); | ||
} | ||
|
||
public render() { | ||
const { value, valueString } = this.state; | ||
const dateString = this.state.isInputFocused ? valueString : this.getDateString(value); | ||
const date = this.state.isInputFocused ? this.createMoment(valueString) : value; | ||
const dateValue = this.isMomentValidAndInRange(value) ? fromMomentToDate(value) : null; | ||
const dayPickerProps = { | ||
...this.props.dayPickerProps, | ||
onMonthChange: () => setTimeout(this.registerPopoverBlurHandler, 0), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the timeout here? leave a code comment explaining why it's necessary There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also, should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
}; | ||
|
||
const popoverContent = | ||
this.props.timePrecision === undefined ? ( | ||
<DatePicker {...this.props} onChange={this.handleDateChange} value={dateValue} /> | ||
<DatePicker | ||
{...this.props} | ||
dayPickerProps={dayPickerProps} | ||
onChange={this.handleDateChange} | ||
value={dateValue} | ||
/> | ||
) : ( | ||
<DateTimePicker | ||
canClearSelection={this.props.canClearSelection} | ||
|
@@ -307,7 +323,7 @@ export class DateInput extends AbstractComponent<IDateInputProps, IDateInputStat | |
return isMomentInRange(value, this.props.minDate, this.props.maxDate); | ||
} | ||
|
||
private handleClosePopover = (e: React.SyntheticEvent<HTMLElement>) => { | ||
private handleClosePopover = (e?: React.SyntheticEvent<HTMLElement>) => { | ||
const { popoverProps = {} } = this.props; | ||
Utils.safeInvoke(popoverProps.onClose, e); | ||
this.setState({ isOpen: false }); | ||
|
@@ -434,6 +450,7 @@ export class DateInput extends AbstractComponent<IDateInputProps, IDateInputStat | |
this.setState({ isInputFocused: false }); | ||
} | ||
} | ||
this.registerPopoverBlurHandler(); | ||
this.safeInvokeInputProp("onBlur", e); | ||
}; | ||
|
||
|
@@ -447,10 +464,37 @@ export class DateInput extends AbstractComponent<IDateInputProps, IDateInputStat | |
// the page. tabbing forward should *not* close the popover, because | ||
// focus will be moving into the popover itself. | ||
this.setState({ isOpen: false }); | ||
} else if (e.which === Keys.ESCAPE) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. popovers have a feature to try adding an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like without additional work, pressing ESC while focus is within the popover, the popover will close. However, when the focus is on the input we still need to handle ESC and set the popover to close. |
||
this.setState({ isOpen: false }); | ||
this.inputRef.blur(); | ||
} | ||
this.safeInvokeInputProp("onKeyDown", e); | ||
}; | ||
|
||
private registerPopoverBlurHandler = () => { | ||
const node = ReactDOM.findDOMNode(this); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm not a huge fan of using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just wrapped the popover content since |
||
const tabbableElements = node.querySelectorAll("[tabindex]:not([tabindex='-1'])"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. any sense in supporting other focusable things like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, we will need to if we also want to support datetime. |
||
const numOfElements = tabbableElements.length; | ||
if (numOfElements) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. avoid boolean coercion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const lastPopoverElement = tabbableElements[numOfElements - 1] as HTMLElement; | ||
if (this.lastPopoverElement !== lastPopoverElement) { | ||
this.unregisterPopoverBlurHandler(); | ||
this.lastPopoverElement = lastPopoverElement; | ||
this.lastPopoverElement.addEventListener("blur", this.handlePopoverBlur); | ||
} | ||
} | ||
}; | ||
|
||
private unregisterPopoverBlurHandler = () => { | ||
if (this.lastPopoverElement) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
this.lastPopoverElement.removeEventListener("blur", this.handlePopoverBlur); | ||
} | ||
}; | ||
|
||
private handlePopoverBlur = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need for this method with optional arg in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to do something like: |
||
this.handleClosePopover(); | ||
}; | ||
|
||
private setInputRef = (el: HTMLElement) => { | ||
this.inputRef = el; | ||
const { inputProps = {} } = this.props; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,6 +63,31 @@ describe("<DateInput>", () => { | |
assert.isFalse(wrapper.find(Popover).prop("isOpen")); | ||
}); | ||
|
||
it("Popover closes when ESC key pressed", () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned up top, we still need the functionality for hitting ESC while the input is in focus. So I still think we need this. |
||
const wrapper = mount(<DateInput openOnFocus={true} />); | ||
const input = wrapper.find("input"); | ||
input.simulate("focus"); | ||
const popover = wrapper.find(Popover); | ||
assert.isTrue(popover.prop("isOpen")); | ||
input.simulate("keydown", { which: Keys.ESCAPE }); | ||
assert.isFalse(popover.prop("isOpen")); | ||
}); | ||
|
||
it("Popover closes when last tabbable component is blurred", () => { | ||
const wrapper = mount(<DateInput openOnFocus={true} />); | ||
const input = wrapper.find("input"); | ||
input.simulate("focus"); | ||
const popover = wrapper.find(Popover); | ||
assert.isTrue(popover.prop("isOpen")); | ||
input.simulate("blur"); | ||
const lastTabbable = popover | ||
.find(".DayPicker-Day--outside") | ||
.last() | ||
.getDOMNode() as HTMLElement; | ||
lastTabbable.dispatchEvent(new Event("blur")); | ||
assert.isFalse(popover.prop("isOpen")); | ||
}); | ||
|
||
it("setting timePrecision renders a TimePicker", () => { | ||
const wrapper = mount(<DateInput timePrecision={TimePickerPrecision.SECOND} />).setState({ isOpen: true }); | ||
// assert TimePicker appears | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
call
super
firstThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.