UNPKG

wix-style-react

Version:
382 lines (324 loc) • 10.8 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import isUndefined from 'lodash/isUndefined'; import moment from 'moment'; import Text from '../Text'; import Input from '../Input'; import Box from '../Box'; import { st, classes } from './TimeInput.st.css'; import { dataHooks } from './constants'; import { FontUpgradeContext } from '../FontUpgrade/context'; import deprecationLog from '../utils/deprecationLog'; /** * An uncontrolled time input component with a stepper and am/pm support */ export default class TimeInput extends Component { static displayName = 'TimeInput'; static propTypes = { /** Displays "--:--" instead of time when input is in a disabled state */ dashesWhenDisabled: PropTypes.bool, /** Applies a data-hook HTML attribute that can be used in the tests */ dataHook: PropTypes.string, /** Defines the default starting time */ defaultValue: PropTypes.object, /** Enables 24h time format */ disableAmPm: PropTypes.bool, /** Marks field as disabled */ disabled: PropTypes.bool, /** Provides a handler that is called whenever input is updated */ onChange: PropTypes.func, /** Displays content in RTL */ rtl: PropTypes.bool, /** Controls the width of the component. auto will resize the input to match width of its content, while 100% will take up the full parent container width. */ width: PropTypes.oneOf(['auto', '100%']), /** Defines the number of minutes to be changed on arrow click */ minutesStep: PropTypes.number, /** Pass a custom element in the area located before ticker */ customSuffix: PropTypes.node, /** Specify the status of a field */ status: PropTypes.oneOf(['error', 'warning', 'loading']), /** Hides status icon. Field will indicate error or warning with border colour change only. */ hideStatusSuffix: PropTypes.bool, /** Defines the message to display on status icon hover. If not given or empty there will be no tooltip. */ statusMessage: PropTypes.node, /** Displays seconds */ showSeconds: PropTypes.bool, }; static defaultProps = { defaultValue: moment(), onChange: () => {}, disableAmPm: false, disabled: false, dashesWhenDisabled: false, minutesStep: 20, width: 'auto', showSeconds: false, }; constructor(props) { super(props); deprecationLog( '<TimeInput /> is deprecated and will be removed in next major version. Please use <TimeInputNext /> instead. Check out how to migrate here: http://wix.to/q0BaCLQ ', ); this.state = { focus: false, lastCaretIdx: 0, hover: false, ...this._getInitTime( this.props.defaultValue, this.props.showSeconds, this.props.disableAmPm, ), }; } UNSAFE_componentWillReceiveProps(nextProps) { if (this._shouldUpdateState(nextProps)) { this.setState( this._getInitTime( nextProps.defaultValue, nextProps.showSeconds, nextProps.disableAmPm, ), ); } } _shouldUpdateState(nextProps) { return ( nextProps.defaultValue !== this.props.defaultValue || nextProps.showSeconds !== this.props.showSeconds || nextProps.disableAmPm !== this.props.disableAmPm ); } _isAmPmMode(disableAmPm) { return ( !disableAmPm && moment('2016-04-03 13:14:00').format('LT').indexOf('PM') !== -1 ); } _getInitTime(value, showSeconds, disableAmPm) { let time = value || moment(), am = time.hours() < 12; const ampmMode = this._isAmPmMode(disableAmPm); ({ time, am } = this._normalizeTime(am, time, ampmMode)); const text = this._formatTime(time, ampmMode, showSeconds); return { time, am, text, ampmMode }; } _momentizeState(timeSet) { let time, am; const { ampmMode } = this.state; if (timeSet) { ({ time, am } = timeSet); } else { ({ time, am } = this.state); } let hours = time.hours(); if (ampmMode && !am && hours < 12) { hours += 12; } if (ampmMode && am && hours === 12) { hours = 0; } const momentized = moment(); momentized.hours(hours); momentized.minutes(time.minutes()); momentized.seconds(0); return momentized; } _bubbleOnChange(timeSet) { const time = this._momentizeState(timeSet); this.props.onChange(time); } _timeStep(direction) { const time = this._momentizeState(); const timeUnit = this.state.lastFocusedTimeUnit || 'minutes'; const amount = timeUnit === 'hours' ? 1 : this.props.minutesStep; time.add(direction * amount, timeUnit); const am = time.hours() < 12; this._updateDate({ am, time }); } _formatTime( time, ampmMode = this.state.ampmMode, showSeconds = this.props.showSeconds, ) { const withSeconds = showSeconds ? ':ss' : ''; return ampmMode ? time.format(`hh:mm${withSeconds}`) : time.format(`HH:mm${withSeconds}`); } _getFocusedTimeUnit(caretIdx, currentValue) { let colonIdx = currentValue.indexOf(':'); colonIdx = Math.max(0, colonIdx); return caretIdx <= colonIdx ? 'hours' : 'minutes'; } _normalizeTime(am, time, ampmMode = this.state.ampmMode) { const hours = time.hours(); if (ampmMode) { if (hours === 0) { return { time: time.clone().hours(12), am: true }; } if (hours > 12) { return { time: time.clone().hours(hours - 12), am: false }; } } return { time: time.clone().hours(hours), am }; } _updateDate({ time, am }) { am = isUndefined(am) ? this.state.am : am; let newTime = moment(time, 'HH:mm'); newTime = newTime.isValid() ? newTime : this.state.time; const normalizedTime = this._normalizeTime(am, newTime); ({ time, am } = normalizedTime); const text = this._formatTime(time); this.setState({ time, am, text }); this._bubbleOnChange({ time, am }); } _handleAmPmClick = () => !this.props.disabled && this._updateDate({ am: !this.state.am }); _handleFocus = input => this.setState({ focus: true, lastFocus: input }); _handleBlur = () => { this.setState({ focus: false }); this._updateDate({ time: this.state.text }); }; _handleInputChange = e => { // that is why cursor is jumping // https://github.com/facebook/react/issues/955#issuecomment-327069204 const isDisabled = this.props.disabled && this.props.dashesWhenDisabled; const isInvalid = /[^0-9 :]/.test(e.target.value); if (isDisabled || isInvalid) { e.preventDefault(); return; } return this.setState({ text: e.target.value, }); }; _handleHover = hover => this.setState({ hover }); _handleMinus = () => this._timeStep(-1); _handlePlus = () => this._timeStep(1); _handleInputBlur = ({ target }) => { if (this.props.disabled && this.props.dashesWhenDisabled) { return; } const caretIdx = target.selectionEnd || 0; let lastFocusedTimeUnit; if (caretIdx >= 0) { lastFocusedTimeUnit = this._getFocusedTimeUnit(caretIdx, target.value); } this.setState({ lastCaretIdx: caretIdx, lastFocusedTimeUnit }); this._updateDate({ time: target.value }); }; _renderTimeTextbox() { const { customSuffix, disabled, dashesWhenDisabled, width, rtl, status, hideStatusSuffix, statusMessage, border, } = this.props; const text = disabled && dashesWhenDisabled ? '-- : --' : this.state.text; const suffix = ( <Input.Group> <FontUpgradeContext.Consumer> {({ active: isMadefor }) => ( <Box alignItems="center" justifyContent="space-between"> <Box verticalAlign="middle" flexGrow={0} marginRight="6px"> {this.state.ampmMode && ( <Text weight={isMadefor ? 'thin' : 'normal'} skin={disabled ? 'disabled' : 'standard'} className={classes.ampm} onClick={this._handleAmPmClick} dataHook={dataHooks.amPmIndicator} > {this.state.am ? 'am' : 'pm'} </Text> )} </Box> <Box align="right" verticalAlign="middle" className={classes.suffixEndWrapper} > {customSuffix && ( <Box marginRight="6px" width="max-content"> {typeof customSuffix === 'string' ? ( <Text weight={isMadefor ? 'thin' : 'normal'} light secondary dataHook={dataHooks.customSuffix} > {customSuffix} </Text> ) : ( <span data-hook={dataHooks.customSuffix}> {customSuffix} </span> )} </Box> )} <Input.Ticker upDisabled={disabled} downDisabled={disabled} onUp={this._handlePlus} onDown={this._handleMinus} dataHook={dataHooks.ticker} /> </Box> </Box> )} </FontUpgradeContext.Consumer> </Input.Group> ); return ( <Input ref="input" rtl={rtl} value={text} className={width === 'auto' ? classes.input : undefined} onFocus={this._handleFocus} onChange={this._handleInputChange} onBlur={this._handleInputBlur} dataHook={dataHooks.input} disabled={disabled} suffix={suffix} border={border} status={status} hideStatusSuffix={hideStatusSuffix} statusMessage={statusMessage} /> ); } render() { const { className, style, dataHook, rtl, disabled, width, showSeconds } = this.props; const { focus, hover } = this.state; return ( <div className={st(classes.root, { disabled, rtl, showSeconds }, className)} style={style} data-hook={dataHook} > <div onMouseOver={() => this._handleHover(true)} onMouseOut={() => this._handleHover(false)} className={st( classes.time, { focus, hover: hover && !focus, stretch: width === '100%', }, className, )} > {this._renderTimeTextbox()} </div> </div> ); } }