Skip to content
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

[EuiDatePicker] Allow user text entry by preventing focus trap #4243

Merged
merged 26 commits into from
Dec 17, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d7c863
conditional focus trap activation
thompsongl Nov 9, 2020
f4314c9
Merge branch 'master' into 3367-datepicker
thompsongl Nov 10, 2020
a076530
a11y; escape
thompsongl Nov 11, 2020
1f8a25a
snapshots
thompsongl Nov 11, 2020
6a97b63
Merge branch 'master' into 3367-datepicker
thompsongl Nov 11, 2020
a30b25d
fix month navigation closing popover
thompsongl Nov 11, 2020
475f230
Merge branch 'master' into 3367-datepicker
thompsongl Nov 16, 2020
de9b970
better return focus
thompsongl Nov 16, 2020
0f2435b
open popover on focus
thompsongl Nov 16, 2020
82b722b
Merge branch 'master' into 3367-datepicker
thompsongl Nov 17, 2020
2dbe24d
prevent year dropdown from closing
thompsongl Nov 17, 2020
f97e377
Merge branch 'master' into 3367-datepicker
thompsongl Nov 18, 2020
af17f27
onChange call for time selection
thompsongl Nov 18, 2020
18ef10c
clean up
thompsongl Nov 18, 2020
6eda8bf
better isSameTime
thompsongl Nov 20, 2020
9c0a2f8
retain focus trap on year and month change
thompsongl Nov 20, 2020
e8c5d1d
use refs
thompsongl Nov 23, 2020
b0b1738
clean up
thompsongl Nov 23, 2020
919765f
Merge branch 'master' into 3367-datepicker
thompsongl Dec 7, 2020
bdd4808
Merge branch 'master' into 3367-datepicker
thompsongl Dec 14, 2020
2ead918
strictParsing option
thompsongl Dec 14, 2020
ff8c184
snapshots
thompsongl Dec 14, 2020
8ecc7c2
Merge branch 'master' into 3367-datepicker
thompsongl Dec 17, 2020
5b28f1b
close popover on enter
thompsongl Dec 17, 2020
b809b97
CL
thompsongl Dec 17, 2020
6b55d28
Merge branch 'master' into 3367-datepicker
thompsongl Dec 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 236 additions & 113 deletions packages/react-datepicker.js

Large diffs are not rendered by default.

188 changes: 128 additions & 60 deletions packages/react-datepicker/docs-site/bundle.js

Large diffs are not rendered by default.

58 changes: 37 additions & 21 deletions packages/react-datepicker/src/calendar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ export default class Calendar extends React.Component {
renderCustomHeader: PropTypes.func,
renderDayContents: PropTypes.func,
updateSelection: PropTypes.func.isRequired,
accessibleMode: PropTypes.bool
accessibleMode: PropTypes.bool,
enableFocusTrap: PropTypes.bool
};

static get defaultProps() {
Expand All @@ -131,16 +132,8 @@ export default class Calendar extends React.Component {
forceShowMonthNavigation: false,
timeCaption: "Time",
previousMonthButtonLabel: "Previous Month",
nextMonthButtonLabel: "Next Month"
};
}

static get defaultProps() {
return {
onDropdownFocus: () => {},
monthsShown: 1,
forceShowMonthNavigation: false,
timeCaption: "Time"
nextMonthButtonLabel: "Next Month",
enableFocusTrap: true
};
}

Expand All @@ -149,8 +142,11 @@ export default class Calendar extends React.Component {
this.state = {
date: this.localizeDate(this.getDateInView()),
selectingDate: null,
monthContainer: null
monthContainer: null,
pauseFocusTrap: false
};
this.monthRef = React.createRef();
this.yearRef = React.createRef();
}

componentDidMount() {
Expand Down Expand Up @@ -183,6 +179,28 @@ export default class Calendar extends React.Component {
}
}

setMonthRef = (node) => {
this.monthRef = node;
}

setYearRef = (node) => {
this.yearRef = node;
}

handleOnDropdownToggle = (isOpen, dropdown) => {
this.setState({pauseFocusTrap: isOpen});
if (!isOpen) {
const element = dropdown === 'month' ? this.monthRef : this.yearRef;
if (element) {
// The focus trap has been unpaused and will reinitialize focus
// but does so on the wrong element (calendar)
// This refocuses the previous element (dropdown button).
// Duration arrived at by trial-and-error.
setTimeout(() => element.focus(), 25);
}
}
}

handleClickOutside = event => {
this.props.onClickOutside(event);
};
Expand Down Expand Up @@ -250,14 +268,6 @@ export default class Calendar extends React.Component {
if (this.props.onMonthChange) {
this.props.onMonthChange(date);
}
if (this.props.adjustDateOnChange) {
if (this.props.onSelect) {
this.props.onSelect(date);
}
if (this.props.setOpen) {
this.props.setOpen(true);
}
}
if (this.props.accessibleMode) {
this.handleSelectionChange(date);
}
Expand Down Expand Up @@ -473,6 +483,8 @@ export default class Calendar extends React.Component {
scrollableYearDropdown={this.props.scrollableYearDropdown}
yearDropdownItemNumber={this.props.yearDropdownItemNumber}
accessibleMode={this.props.accessibleMode}
onDropdownToggle={this.handleOnDropdownToggle}
buttonRef={this.setYearRef}
/>
);
};
Expand All @@ -490,6 +502,8 @@ export default class Calendar extends React.Component {
month={getMonth(this.state.date)}
useShortMonthInDropdown={this.props.useShortMonthInDropdown}
accessibleMode={this.props.accessibleMode}
onDropdownToggle={this.handleOnDropdownToggle}
buttonRef={this.setMonthRef}
/>
);
};
Expand Down Expand Up @@ -690,9 +704,11 @@ export default class Calendar extends React.Component {
})}
>
<FocusTrap
paused={this.state.pauseFocusTrap}
active={this.props.enableFocusTrap}
tag={FocusTrapContainer}
focusTrapOptions={{
onDeactivate: () => this.props.setOpen(false),
onDeactivate: () => this.props.setOpen(false, true),
initialFocus: initialFocusTarget
}}
>
Expand Down
12 changes: 10 additions & 2 deletions packages/react-datepicker/src/date_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export function cloneDate(date) {
return date.clone();
}

export function parseDate(value, { dateFormat, locale }) {
const m = moment(value, dateFormat, locale || moment.locale(), true);
export function parseDate(value, { dateFormat, locale, strictParsing}) {
const m = moment(value, dateFormat, locale || moment.locale(), strictParsing);
return m.isValid() ? m : null;
}

Expand Down Expand Up @@ -272,6 +272,14 @@ export function isSameDay(moment1, moment2) {
}
}

export function isSameTime(moment1, moment2) {
if (moment1 && moment2) {
return moment1.isSame(moment2, "second");
} else {
return !moment1 && !moment2;
}
}

export function isSameUtcOffset(moment1, moment2) {
if (moment1 && moment2) {
return moment1.utcOffset() === moment2.utcOffset();
Expand Down
99 changes: 64 additions & 35 deletions packages/react-datepicker/src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
subtractWeeks,
subtractYears,
isSameDay,
isSameTime,
isDayDisabled,
isOutOfBounds,
isDayInRange,
Expand Down Expand Up @@ -168,7 +169,8 @@ export default class DatePicker extends React.Component {
renderCustomHeader: PropTypes.func,
renderDayContents: PropTypes.func,
accessibleMode: PropTypes.bool,
accessibleModeButton: PropTypes.element
accessibleModeButton: PropTypes.element,
strictParsing: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
};

static get defaultProps() {
Expand Down Expand Up @@ -201,7 +203,8 @@ export default class DatePicker extends React.Component {
nextMonthButtonLabel: "Next month",
renderDayContents(date) {
return date;
}
},
strictParsing: false
};
}

Expand Down Expand Up @@ -262,7 +265,12 @@ export default class DatePicker extends React.Component {
// transforming highlighted days (perhaps nested array)
// to flat Map for faster access in day.jsx
highlightDates: getHightLightDaysMap(this.props.highlightDates),
focused: false
focused: false,
// We attempt to handle focus trap activation manually,
// but that is not possible with custom inputs like buttons.
// Err on the side of a11y and trap focus when we can't be certain
// that the trigger comoponent will work with our keyDown logic.
enableFocusTrap: this.props.customInput && this.props.customInput.type !== 'input' ? true : false,
};
};

Expand Down Expand Up @@ -291,20 +299,20 @@ export default class DatePicker extends React.Component {
};

setOpen = (open, skipSetBlur = false) => {
this.setState(
{
open: open,
preSelection:
open && this.state.open
? this.state.preSelection
: this.calcInitialState().preSelection,
lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE
},
this.setState({
open: open,
preSelection:
open && this.state.open
? this.state.preSelection
: this.calcInitialState().preSelection,
lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE,
},
() => {
if (!open) {
this.setState(
prev => ({
focused: skipSetBlur ? prev.focused : false
focused: skipSetBlur ? prev.focused : false,
enableFocusTrap: skipSetBlur ? false : prev.enableFocusTrap
}),
() => {
!skipSetBlur && this.setBlur();
Expand All @@ -329,7 +337,7 @@ export default class DatePicker extends React.Component {
this.props.onFocus(event);
if (
!this.props.preventOpenOnFocus &&
!this.props.readOnly && !this.props.accessibleMode
!this.props.readOnly
) {
this.setOpen(true);
}
Expand Down Expand Up @@ -410,7 +418,7 @@ export default class DatePicker extends React.Component {
if (!this.props.shouldCloseOnSelect || this.props.showTimeSelect) {
this.setPreSelection(date);
} else if (!this.props.inline) {
this.setOpen(false);
this.setOpen(false, true);
}
};

Expand All @@ -433,21 +441,22 @@ export default class DatePicker extends React.Component {
return;
}

if (changedDate !== null && this.props.selected) {
let selected = this.props.selected;
if (keepInput) selected = newDate(changedDate);
changedDate = setTime(newDate(changedDate), {
hour: getHour(selected),
minute: getMinute(selected),
second: getSecond(selected),
millisecond: getMillisecond(selected),
});
}

if (
!isSameDay(this.props.selected, changedDate) ||
!isSameTime(this.props.selected, changedDate) ||
this.props.allowSameDay
) {
if (changedDate !== null) {
if (this.props.selected) {
let selected = this.props.selected;
if (keepInput) selected = newDate(changedDate);
changedDate = setTime(newDate(changedDate), {
hour: getHour(selected),
minute: getMinute(selected),
second: getSecond(selected),
millisecond: getMillisecond(selected),
});
}
if (!this.props.inline) {
this.setState({
preSelection: changedDate
Expand Down Expand Up @@ -490,13 +499,18 @@ export default class DatePicker extends React.Component {
millisecond: 0,
});

this.setState({
preSelection: changedDate
});
if (!isSameTime(selected, changedDate)) {
this.setState({
preSelection: changedDate
});

this.props.onChange(changedDate);
}

this.props.onChange(changedDate);
this.props.onSelect(changedDate);

if (this.props.shouldCloseOnSelect) {
this.setOpen(false);
this.setOpen(false, true);
}
this.setState({ inputValue: null });
};
Expand Down Expand Up @@ -528,7 +542,21 @@ export default class DatePicker extends React.Component {
) {
if (eventKey === "ArrowDown" || eventKey === "ArrowUp") {
event.preventDefault();
this.onInputClick();
this.setState({enableFocusTrap: true}, () => {
this.onInputClick();
});
}
return;
}
if (this.state.open && !this.state.enableFocusTrap) {
if (eventKey === "ArrowDown" || eventKey === "Tab") {
event.preventDefault();
this.setState({enableFocusTrap: true}, () => {
this.onInputClick();
});
} else if (eventKey === "Escape") {
event.preventDefault();
this.setOpen(false, true);
}
return;
}
Expand All @@ -542,12 +570,12 @@ export default class DatePicker extends React.Component {
this.handleSelect(copy, event);
!this.props.shouldCloseOnSelect && this.setPreSelection(copy);
} else {
this.setOpen(false);
this.setOpen(false, true);
}
} else if (eventKey === "Escape") {
event.preventDefault();

this.setOpen(false);
this.setOpen(false, true);
if (!this.inputOk()) {
this.props.onInputError({ code: 1, msg: INPUT_ERR_1 });
}
Expand Down Expand Up @@ -686,6 +714,7 @@ export default class DatePicker extends React.Component {
renderDayContents={this.props.renderDayContents}
updateSelection={this.updateSelection}
accessibleMode={this.props.accessibleMode}
enableFocusTrap={this.state.enableFocusTrap}
>
{this.props.children}
</WrappedCalendar>
Expand Down Expand Up @@ -729,7 +758,7 @@ export default class DatePicker extends React.Component {
readOnly: this.props.readOnly,
required: this.props.required,
tabIndex: this.props.tabIndex,
"aria-label": inputValue
"aria-label": this.state.open ? 'Press the down key to enter a popover containing a calendar. Press the escape key to close the popover.' : 'Press the down key to open a popover containing a calendar.'
});
};

Expand Down
12 changes: 9 additions & 3 deletions packages/react-datepicker/src/month_dropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export default class MonthDropdown extends React.Component {
month: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
useShortMonthInDropdown: PropTypes.bool,
accessibleMode: PropTypes.bool
accessibleMode: PropTypes.bool,
onDropdownToggle: PropTypes.func,
buttonRef: PropTypes.func
};

constructor(props) {
Expand Down Expand Up @@ -65,6 +67,7 @@ export default class MonthDropdown extends React.Component {

setReadViewRef = ref => {
this.readViewref = ref;
this.props.buttonRef(ref);
};

onReadViewKeyDown = event => {
Expand Down Expand Up @@ -156,10 +159,13 @@ export default class MonthDropdown extends React.Component {
}
};

toggleDropdown = () =>
toggleDropdown = () => {
const isOpen = !this.state.dropdownVisible
this.setState({
dropdownVisible: !this.state.dropdownVisible
dropdownVisible: isOpen
});
this.props.onDropdownToggle(isOpen, 'month');
}

render() {
let renderedDropdown;
Expand Down
Loading