From 6bc42a10dc77e7ffefc238ac4b104023fdf4668b Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Thu, 7 Apr 2016 12:15:04 -0700 Subject: [PATCH] feat(scheduler): better time picker fix(scheduler): time picker fixed --- internal_packages/N1-Scheduler/package.json | 3 - .../N1-Scheduler/stylesheets/scheduler.less | 2 + src/components/time-picker.jsx | 121 +++++++++++++++--- static/components/time-picker.less | 40 ++++++ 4 files changed, 148 insertions(+), 18 deletions(-) diff --git a/internal_packages/N1-Scheduler/package.json b/internal_packages/N1-Scheduler/package.json index 0b4cebc21..dd42bdf45 100644 --- a/internal_packages/N1-Scheduler/package.json +++ b/internal_packages/N1-Scheduler/package.json @@ -17,9 +17,6 @@ }, "icon": "./icon.png", "isOptional": true, - "dependencies": { - "moment-round": "^1.0" - }, "windowTypes": { "default": true, "composer": true, diff --git a/internal_packages/N1-Scheduler/stylesheets/scheduler.less b/internal_packages/N1-Scheduler/stylesheets/scheduler.less index 8e6ab14d6..3cb97f0e7 100644 --- a/internal_packages/N1-Scheduler/stylesheets/scheduler.less +++ b/internal_packages/N1-Scheduler/stylesheets/scheduler.less @@ -33,6 +33,7 @@ color: @text-color; } .row { + position: relative; display: flex; align-items: baseline; padding: 10px 0; @@ -60,6 +61,7 @@ } } .row.time { + z-index: 10; // So the time pickers show over .time-picker { text-align: center; } diff --git a/src/components/time-picker.jsx b/src/components/time-picker.jsx index cb95c2594..40534329c 100644 --- a/src/components/time-picker.jsx +++ b/src/components/time-picker.jsx @@ -1,8 +1,11 @@ import React from 'react' import ReactDOM from 'react-dom' import moment from 'moment' +require('moment-round') // overrides moment import classnames from 'classnames' +const INTERVAL = [30, 'minutes'] + export default class TimePicker extends React.Component { static displayName = "TimePicker"; @@ -29,26 +32,54 @@ export default class TimePicker extends React.Component { } } + componentDidMount() { + this._fixTimeOptionScroll() + } + componentWillReceiveProps(newProps) { this.setState({rawText: this._valToTimeString(newProps.value)}) } + componentDidUpdate() { + if (this._gotoScrollStartOnUpdate) { + this._fixTimeOptionScroll() + } + } + _valToTimeString(value) { return moment(value).format("LT") } _onKeyDown = (event) => { if (event.key === "ArrowUp") { - // TODO: When `renderTimeOptions` is implemented + event.preventDefault() + this._onArrow(event.key) } else if (event.key === "ArrowDown") { - // TODO: When `renderTimeOptions` is implemented + event.preventDefault() + this._onArrow(event.key) } else if (event.key === "Enter") { this.context.parentTabGroup.shiftFocus(1); } } + _onArrow(key) { + let newT = moment(this.props.value); + newT = newT.round.apply(newT, INTERVAL); + if (key === "ArrowUp") { + newT = newT.subtract.apply(newT, INTERVAL); + } else if (key === "ArrowDown") { + newT = newT.add.apply(newT, INTERVAL); + } + if (moment(this.props.value).day() !== newT.day()) { + return + } + this._gotoScrollStartOnUpdate = true + this.props.onChange(newT); + } + _onFocus = () => { this.setState({focused: true}); + this._gotoScrollStartOnUpdate = true const el = ReactDOM.findDOMNode(this.refs.input); el.setSelectionRange(0, el.value.length) } @@ -83,26 +114,86 @@ export default class TimePicker extends React.Component { if (simpleDigitMatch && simpleDigitMatch.length > 0) { const hr = parseInt(simpleDigitMatch[1], 10); if (hr <= 7) { - // If you're going to punch in "2" into the time field, you - // probably mean 2pm, not 2am. return true } } return false } - // TODO - _renderTimeOptions() { - // TODO: When you select a time a dropdown will additionally show - // letting you pick from preset times. The `relativeTo` prop will give - // you relative times - const opts = [] - if (this.state.focused) { - return ( -
{opts}
- ) + _fixTimeOptionScroll() { + this._gotoScrollStartOnUpdate = false + const el = ReactDOM.findDOMNode(this); + const scrollTo = el.querySelector(".scroll-start"); + const scrollWrap = el.querySelector(".time-options"); + if (scrollTo && scrollWrap) { + scrollWrap.scrollTop = scrollTo.offsetTop } - return false + } + + _onSelectOption(val) { + this.props.onChange(val) + } + + _renderTimeOptions() { + if (!this.state.focused) { + return false + } + + const enteredMoment = moment(this.props.value); + + const roundedMoment = moment(enteredMoment); + roundedMoment.ceil.apply(roundedMoment, INTERVAL); + + const firstVisibleMoment = moment(roundedMoment); + firstVisibleMoment.add.apply(firstVisibleMoment, INTERVAL); + + let startVal = moment(this.props.value).startOf('day').valueOf(); + startVal = Math.max(startVal, (this.props.relativeTo || 0)); + + const startMoment = moment(startVal) + if (this.props.relativeTo) { + startMoment.ceil.apply(startMoment, INTERVAL).add.apply(startMoment, INTERVAL) + } + const endMoment = moment(startVal).endOf('day'); + const opts = [] + + const relStart = moment(this.props.relativeTo); + const timeIter = moment(startMoment) + while (timeIter.isSameOrBefore(endMoment)) { + const val = timeIter.valueOf(); + const className = classnames({ + option: true, + selected: timeIter.isSame(enteredMoment), + "scroll-start": timeIter.isSame(firstVisibleMoment), + }) + + let relTxt = false + if (this.props.relativeTo) { + relTxt = ( + + {`(${timeIter.diff(relStart, 'hours', true)}hr)`} + + ) + } + + opts.push( +
this._onSelectOption(val)} + > + {timeIter.format("LT")}{relTxt} +
+ ) + timeIter.add.apply(timeIter, INTERVAL) + } + + const className = classnames({ + "time-options": true, + "relative-to": this.props.relativeTo, + }) + + return ( +
{opts}
+ ) } render() { diff --git a/static/components/time-picker.less b/static/components/time-picker.less index 2b4247f6e..4cec8e6be 100644 --- a/static/components/time-picker.less +++ b/static/components/time-picker.less @@ -3,10 +3,50 @@ .time-picker-wrap { display: inline-block; width: 5.5em; + position: relative; input { width: 5.5em; &.invalid { background: fade(@color-error, 10%); } } + + .time-options { + max-height: 158px; + overflow: auto; + border: 1px solid #eee; + position: absolute; + width: 100%; + text-align: center; + background: @background-primary; + border-top: 0; + border-radius: 0 0 @border-radius-base @border-radius-base; + box-shadow: 0 0 3px rgba(0,0,0,0.1); + + &.relative-to { + width: 120px; + text-align: left; + + .option { + padding-left: 12px; + } + } + + .option { + line-height: 1.75; + &.selected { + color: @text-color; + &:hover { + background: transparent; + } + } + &.focused, &:hover { + background: rgba(0,0,0,0.05); + } + .rel-text { + font-size: 0.8em; + margin-left: 4px; + } + } + } }