From 6c641cfc2a951812bd59dec3b37f60092567de7b Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Mon, 11 Mar 2019 16:28:49 -0600 Subject: [PATCH] Fix time selection in EuiSuperDatePicker (#1704) * Update react-datepicker time selector to not _always_ scroll to preSelection time * Update react-datepicker time selection scroll-into-view onMount logic * revert props default changes I made for testing * fix scroll issue * fix ie issue * A few more dark mode fixes (#1700) * 9.2.0 * Updated documentation. * Make EuiPopover's repositionOnScroll prop optional in TS (#1705) * Make EuiPopover's repositionOnScroll prop optional in TS * changelog * fix range coloring * Fix scrollTop target * changelog --- CHANGELOG.md | 3 +- packages/react-datepicker.js | 131 +++++++++++++----- packages/react-datepicker/docs-site/bundle.js | 87 ++++++++---- packages/react-datepicker/src/time.jsx | 103 +++++++++----- src/components/date_picker/_date_picker.scss | 22 ++- 5 files changed, 237 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9700d78aeef..4dbd05c0431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -**Note: this release is a backport containing changes originally made in `9.0.0`, `9.1.0`, and `9.4.0`** +**Note: this release is a backport containing changes originally made in `9.0.0`, `9.1.0`, `9.3.0`, and `9.4.0`** - Adjusted the dark theme palette to have a slight blue tint ([#1691](https://github.com/elastic/eui/pull/1691)) - Added button to `EuiSuperDatePicker`'s “Now” tab to trigger the "now" time selection ([#1620](https://github.com/elastic/eui/pull/1620)) @@ -10,6 +10,7 @@ - Fixed keyboard navigation and UI of `EuiComboBox` items in single selection mode ([#1619](https://github.com/elastic/eui/pull/1619)) - Fixed `EuiComboBox` `activeOptonIndex` error with empty search results ([#1695](https://github.com/elastic/eui/pull/1695)) - Prevent `EuiComboBox` from creating a custom option value when user clicks on a value in the dropdown ([#1728](https://github.com/elastic/eui/pull/1728)) +- Fixed `EuiSuperDatePicker` time selection jumping on focus ([#1704](https://github.com/elastic/eui/pull/1704)) ## [`6.10.4`](https://github.com/elastic/eui/tree/v6.10.4) diff --git a/packages/react-datepicker.js b/packages/react-datepicker.js index 10dd96d0a25..78564f191bd 100644 --- a/packages/react-datepicker.js +++ b/packages/react-datepicker.js @@ -2634,9 +2634,24 @@ var Time = function (_React$Component) { } }, null); + if (preSelection == null) { + // there is no exact pre-selection, find the element closest to the selected time and preselect it + var currH = _this.props.selected ? getHour(_this.props.selected) : getHour(newDate()); + var currM = _this.props.selected ? getMinute(_this.props.selected) : getMinute(newDate()); + var closestTimeIndex = Math.floor((60 * currH + currM) / _this.props.intervals); + var closestMinutes = closestTimeIndex * _this.props.intervals; + preSelection = setTime(newDate(), { + hour: Math.floor(closestMinutes / 60), + minute: closestMinutes % 60, + second: 0, + millisecond: 0 + }); + } + _this.timeFormat = "hh:mm A"; _this.state = { preSelection: preSelection, + needsScrollToPreSelection: false, readInstructions: false, isFocused: false }; @@ -2645,36 +2660,45 @@ var Time = function (_React$Component) { Time.prototype.componentDidMount = function componentDidMount() { // code to ensure selected time will always be in focus within time window when it first appears - this.list.scrollTop = Time.calcCenterPosition(this.props.monthRef ? this.props.monthRef.clientHeight - this.header.clientHeight : this.list.clientHeight, this.centerLi); - - if (this.state.preSelection == null) { - // there is no pre-selection, find the element closest to the selected time and preselect it - var currH = this.props.selected ? getHour(this.props.selected) : getHour(newDate()); - var currM = this.props.selected ? getMinute(this.props.selected) : getMinute(newDate()); - var closestTimeIndex = Math.floor((60 * currH + currM) / this.props.intervals); - var closestMinutes = closestTimeIndex * this.props.intervals; - var closestTime = setTime(newDate(), { - hour: Math.floor(closestMinutes / 60), - minute: closestMinutes % 60, - second: 0, - millisecond: 0 - }); - this.setState({ preSelection: closestTime }); - } + var scrollParent = this.list; + + scrollParent.scrollTop = Time.calcCenterPosition(this.props.monthRef ? this.props.monthRef.clientHeight - this.header.clientHeight : this.list.clientHeight, this.selectedLi || this.preselectedLi); }; - Time.prototype.componentDidUpdate = function componentDidUpdate() { - // scroll to the preSelected time - var scrollToElement = this.preselectedLi; + Time.prototype.componentDidUpdate = function componentDidUpdate(prevProps) { + // if selection changed, scroll to the selected item + if (this.props.selected && this.props.selected.isSame(prevProps.selected) === false) { + var scrollToElement = this.selectedLi; + + if (scrollToElement) { + // an element matches the selected time, scroll to it + scrollToElement.scrollIntoView({ + behavior: "instant", + block: "nearest", + inline: "nearest" + }); + } - if (scrollToElement) { - // an element matches the selected time, scroll to it - scrollToElement.scrollIntoView({ - behavior: "instant", - block: "nearest", - inline: "nearest" + // update preSelection to the selection + this.setState({ + preSelection: this.props.selected }); } + + if (this.state.needsScrollToPreSelection) { + var _scrollToElement = this.preselectedLi; + + if (_scrollToElement) { + // an element matches the selected time, scroll to it + _scrollToElement.scrollIntoView({ + behavior: "instant", + block: "nearest", + inline: "nearest" + }); + } + + this.setState({ needsScrollToPreSelection: false }); + } }; Time.prototype.render = function render() { @@ -2817,7 +2841,10 @@ var _initialiseProps = function _initialiseProps() { } if (!newSelection) return; // Let the input component handle this keydown event.preventDefault(); - _this3.setState({ preSelection: newSelection }); + _this3.setState({ + preSelection: newSelection, + needsScrollToPreSelection: true + }); }; this.handleClick = function (time) { @@ -2880,13 +2907,13 @@ var _initialiseProps = function _initialiseProps() { onClick: _this3.handleClick.bind(_this3, time), className: _this3.liClasses(time, activeTime), ref: function ref(li) { - if (currH === getHour(time) && currM === getMinute(time) || currH === getHour(time) && !_this3.centerLi) { - _this3.centerLi = li; - } - if (li && li.classList.contains("react-datepicker__time-list-item--preselected")) { _this3.preselectedLi = li; } + + if (li && li.classList.contains("react-datepicker__time-list-item--selected")) { + _this3.selectedLi = li; + } }, role: "option", id: i @@ -3597,10 +3624,17 @@ var _aFunction = function (it) { return it; }; +var _aFunction$1 = /*#__PURE__*/Object.freeze({ + default: _aFunction, + __moduleExports: _aFunction +}); + +var aFunction = ( _aFunction$1 && _aFunction ) || _aFunction$1; + // optional / simple context binding var _ctx = function (fn, that, length) { - _aFunction(fn); + aFunction(fn); if (that === undefined) return fn; switch (length) { case 1: return function (a) { @@ -3938,11 +3972,18 @@ _export(_export.S + _export.F, 'Object', { assign: _objectAssign }); var assign = _core.Object.assign; -var assign$1 = createCommonjsModule(function (module) { -module.exports = { "default": assign, __esModule: true }; +var assign$1 = /*#__PURE__*/Object.freeze({ + default: assign, + __moduleExports: assign }); -unwrapExports(assign$1); +var require$$0 = ( assign$1 && assign ) || assign$1; + +var assign$2 = createCommonjsModule(function (module) { +module.exports = { "default": require$$0, __esModule: true }; +}); + +unwrapExports(assign$2); var _extends$1 = createCommonjsModule(function (module, exports) { @@ -3950,7 +3991,7 @@ exports.__esModule = true; -var _assign2 = _interopRequireDefault(assign$1); +var _assign2 = _interopRequireDefault(assign$2); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } @@ -4699,7 +4740,12 @@ var setPrototypeOf$1 = createCommonjsModule(function (module) { module.exports = { "default": setPrototypeOf, __esModule: true }; }); -unwrapExports(setPrototypeOf$1); +var setPrototypeOf$2 = unwrapExports(setPrototypeOf$1); + +var setPrototypeOf$3 = /*#__PURE__*/Object.freeze({ + default: setPrototypeOf$2, + __moduleExports: setPrototypeOf$1 +}); // 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties]) _export(_export.S, 'Object', { create: _objectCreate }); @@ -4715,13 +4761,15 @@ module.exports = { "default": create, __esModule: true }; unwrapExports(create$1); +var _setPrototypeOf = ( setPrototypeOf$3 && setPrototypeOf$2 ) || setPrototypeOf$3; + var inherits$1 = createCommonjsModule(function (module, exports) { exports.__esModule = true; -var _setPrototypeOf2 = _interopRequireDefault(setPrototypeOf$1); +var _setPrototypeOf2 = _interopRequireDefault(_setPrototypeOf); @@ -7302,6 +7350,13 @@ emptyFunction.thatReturnsArgument = function (arg) { var emptyFunction_1 = emptyFunction; +var emptyFunction$1 = /*#__PURE__*/Object.freeze({ + default: emptyFunction_1, + __moduleExports: emptyFunction_1 +}); + +var emptyFunction$2 = ( emptyFunction$1 && emptyFunction_1 ) || emptyFunction$1; + /** * Similar to invariant but only logs a warning if the condition is not met. * This can be used to log issues in development environments in critical @@ -7309,7 +7364,7 @@ var emptyFunction_1 = emptyFunction; * same logic and follow the same code paths. */ -var warning = emptyFunction_1; +var warning = emptyFunction$2; if (process.env.NODE_ENV !== 'production') { var printWarning = function printWarning(format) { diff --git a/packages/react-datepicker/docs-site/bundle.js b/packages/react-datepicker/docs-site/bundle.js index 181d3e7da23..1a480f40da7 100644 --- a/packages/react-datepicker/docs-site/bundle.js +++ b/packages/react-datepicker/docs-site/bundle.js @@ -48710,9 +48710,24 @@ } }, null); + if (preSelection == null) { + // there is no exact pre-selection, find the element closest to the selected time and preselect it + var currH = _this.props.selected ? (0, _date_utils.getHour)(_this.props.selected) : (0, _date_utils.getHour)((0, _date_utils.newDate)()); + var currM = _this.props.selected ? (0, _date_utils.getMinute)(_this.props.selected) : (0, _date_utils.getMinute)((0, _date_utils.newDate)()); + var closestTimeIndex = Math.floor((60 * currH + currM) / _this.props.intervals); + var closestMinutes = closestTimeIndex * _this.props.intervals; + preSelection = (0, _date_utils.setTime)((0, _date_utils.newDate)(), { + hour: Math.floor(closestMinutes / 60), + minute: closestMinutes % 60, + second: 0, + millisecond: 0 + }); + } + _this.timeFormat = "hh:mm A"; _this.state = { preSelection: preSelection, + needsScrollToPreSelection: false, readInstructions: false, isFocused: false }; @@ -48721,36 +48736,45 @@ Time.prototype.componentDidMount = function componentDidMount() { // code to ensure selected time will always be in focus within time window when it first appears - this.list.scrollTop = Time.calcCenterPosition(this.props.monthRef ? this.props.monthRef.clientHeight - this.header.clientHeight : this.list.clientHeight, this.centerLi); - - if (this.state.preSelection == null) { - // there is no pre-selection, find the element closest to the selected time and preselect it - var currH = this.props.selected ? (0, _date_utils.getHour)(this.props.selected) : (0, _date_utils.getHour)((0, _date_utils.newDate)()); - var currM = this.props.selected ? (0, _date_utils.getMinute)(this.props.selected) : (0, _date_utils.getMinute)((0, _date_utils.newDate)()); - var closestTimeIndex = Math.floor((60 * currH + currM) / this.props.intervals); - var closestMinutes = closestTimeIndex * this.props.intervals; - var closestTime = (0, _date_utils.setTime)((0, _date_utils.newDate)(), { - hour: Math.floor(closestMinutes / 60), - minute: closestMinutes % 60, - second: 0, - millisecond: 0 - }); - this.setState({ preSelection: closestTime }); - } + var scrollParent = this.list; + + scrollParent.scrollTop = Time.calcCenterPosition(this.props.monthRef ? this.props.monthRef.clientHeight - this.header.clientHeight : this.list.clientHeight, this.selectedLi || this.preselectedLi); }; - Time.prototype.componentDidUpdate = function componentDidUpdate() { - // scroll to the preSelected time - var scrollToElement = this.preselectedLi; + Time.prototype.componentDidUpdate = function componentDidUpdate(prevProps) { + // if selection changed, scroll to the selected item + if (this.props.selected && this.props.selected.isSame(prevProps.selected) === false) { + var scrollToElement = this.selectedLi; - if (scrollToElement) { - // an element matches the selected time, scroll to it - scrollToElement.scrollIntoView({ - behavior: "instant", - block: "nearest", - inline: "nearest" + if (scrollToElement) { + // an element matches the selected time, scroll to it + scrollToElement.scrollIntoView({ + behavior: "instant", + block: "nearest", + inline: "nearest" + }); + } + + // update preSelection to the selection + this.setState({ + preSelection: this.props.selected }); } + + if (this.state.needsScrollToPreSelection) { + var _scrollToElement = this.preselectedLi; + + if (_scrollToElement) { + // an element matches the selected time, scroll to it + _scrollToElement.scrollIntoView({ + behavior: "instant", + block: "nearest", + inline: "nearest" + }); + } + + this.setState({ needsScrollToPreSelection: false }); + } }; Time.prototype.render = function render() { @@ -48893,7 +48917,10 @@ } if (!newSelection) return; // Let the input component handle this keydown event.preventDefault(); - _this3.setState({ preSelection: newSelection }); + _this3.setState({ + preSelection: newSelection, + needsScrollToPreSelection: true + }); }; this.handleClick = function (time) { @@ -48956,13 +48983,13 @@ onClick: _this3.handleClick.bind(_this3, time), className: _this3.liClasses(time, activeTime), ref: function ref(li) { - if (currH === (0, _date_utils.getHour)(time) && currM === (0, _date_utils.getMinute)(time) || currH === (0, _date_utils.getHour)(time) && !_this3.centerLi) { - _this3.centerLi = li; - } - if (li && li.classList.contains("react-datepicker__time-list-item--preselected")) { _this3.preselectedLi = li; } + + if (li && li.classList.contains("react-datepicker__time-list-item--selected")) { + _this3.selectedLi = li; + } }, role: "option", id: i diff --git a/packages/react-datepicker/src/time.jsx b/packages/react-datepicker/src/time.jsx index c151c16f2cc..68157330640 100644 --- a/packages/react-datepicker/src/time.jsx +++ b/packages/react-datepicker/src/time.jsx @@ -59,32 +59,15 @@ export default class Time extends React.Component { super(...args); const times = this.generateTimes(); - const preSelection = times.reduce((preSelection, time) => { + let preSelection = times.reduce((preSelection, time) => { if (preSelection) return preSelection; if (doHoursAndMinutesAlign(time, this.props.selected)) { return time; } }, null); - this.timeFormat = "hh:mm A"; - this.state = { - preSelection, - readInstructions: false, - isFocused: false - }; - } - - componentDidMount() { - // code to ensure selected time will always be in focus within time window when it first appears - this.list.scrollTop = Time.calcCenterPosition( - this.props.monthRef - ? this.props.monthRef.clientHeight - this.header.clientHeight - : this.list.clientHeight, - this.centerLi - ); - - if (this.state.preSelection == null) { - // there is no pre-selection, find the element closest to the selected time and preselect it + if (preSelection == null) { + // there is no exact pre-selection, find the element closest to the selected time and preselect it const currH = this.props.selected ? getHour(this.props.selected) : getHour(newDate()); @@ -95,28 +78,69 @@ export default class Time extends React.Component { (60 * currH + currM) / this.props.intervals ); const closestMinutes = closestTimeIndex * this.props.intervals; - const closestTime = setTime(newDate(), { + preSelection = setTime(newDate(), { hour: Math.floor(closestMinutes / 60), minute: closestMinutes % 60, second: 0, millisecond: 0, }); - this.setState({ preSelection: closestTime }); } + + this.timeFormat = "hh:mm A"; + this.state = { + preSelection, + needsScrollToPreSelection: false, + readInstructions: false, + isFocused: false + }; + } + + componentDidMount() { + // code to ensure selected time will always be in focus within time window when it first appears + const scrollParent = this.list; + + scrollParent.scrollTop = Time.calcCenterPosition( + this.props.monthRef + ? this.props.monthRef.clientHeight - this.header.clientHeight + : this.list.clientHeight, + this.selectedLi || this.preselectedLi + ); } - componentDidUpdate() { - // scroll to the preSelected time - const scrollToElement = this.preselectedLi; + componentDidUpdate(prevProps) { + // if selection changed, scroll to the selected item + if (this.props.selected && this.props.selected.isSame(prevProps.selected) === false) { + const scrollToElement = this.selectedLi; + + if (scrollToElement) { + // an element matches the selected time, scroll to it + scrollToElement.scrollIntoView({ + behavior: "instant", + block: "nearest", + inline: "nearest" + }); + } - if (scrollToElement) { - // an element matches the selected time, scroll to it - scrollToElement.scrollIntoView({ - behavior: "instant", - block: "nearest", - inline: "nearest" + // update preSelection to the selection + this.setState({ + preSelection: this.props.selected, }); } + + if (this.state.needsScrollToPreSelection) { + const scrollToElement = this.preselectedLi; + + if (scrollToElement) { + // an element matches the selected time, scroll to it + scrollToElement.scrollIntoView({ + behavior: "instant", + block: "nearest", + inline: "nearest" + }); + } + + this.setState({ needsScrollToPreSelection: false }); + } } onFocus = () => { @@ -150,7 +174,10 @@ export default class Time extends React.Component { } if (!newSelection) return; // Let the input component handle this keydown event.preventDefault(); - this.setState({ preSelection: newSelection }); + this.setState({ + preSelection: newSelection, + needsScrollToPreSelection: true, + }); }; handleClick = time => { @@ -242,19 +269,21 @@ export default class Time extends React.Component { className={this.liClasses(time, activeTime)} ref={li => { if ( - (currH === getHour(time) && currM === getMinute(time)) || - (currH === getHour(time) && !this.centerLi) + li && + li.classList.contains( + "react-datepicker__time-list-item--preselected" + ) ) { - this.centerLi = li; + this.preselectedLi = li; } if ( li && li.classList.contains( - "react-datepicker__time-list-item--preselected" + "react-datepicker__time-list-item--selected" ) ) { - this.preselectedLi = li; + this.selectedLi = li; } }} role="option" diff --git a/src/components/date_picker/_date_picker.scss b/src/components/date_picker/_date_picker.scss index ee687ca9a3b..814c7c1c975 100644 --- a/src/components/date_picker/_date_picker.scss +++ b/src/components/date_picker/_date_picker.scss @@ -302,13 +302,22 @@ flex-grow: 1; display: flex; padding-left: $euiSizeXS; - overflow-y: scroll; - @include euiScrollBar; + flex-direction: column; .react-datepicker__time-box { width: auto; + display: flex; + flex-direction: column; + flex-grow: 1; + ul.react-datepicker__time-list { + @include euiScrollBar; height: 204px !important; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-y: auto; + align-items: center; li.react-datepicker__time-list-item { padding: $euiSizeXS $euiSizeS; @@ -316,6 +325,8 @@ text-align: right; color: $euiColorDarkShade; white-space: nowrap; + // IE needs this to fix collapsing flex + line-height: $euiSizeM; &:hover, &:focus { @@ -617,12 +628,17 @@ } .react-datepicker__month-read-view:focus, -.react-datepicker__year-read-view:focus, { +.react-datepicker__year-read-view:focus { text-decoration: underline; } .react-datepicker__month--accessible:focus { background: $euiFocusBackgroundColor; + + .react-datepicker__day--in-range { + border-top-color: $euiFocusBackgroundColor; + border-bottom-color: $euiFocusBackgroundColor; + } } .react-datepicker__navigation:focus { background-color: $euiFocusBackgroundColor;