dateInput.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /* eslint-disable jsx-a11y/click-events-have-key-events */
  2. /* eslint-disable jsx-a11y/no-static-element-interactions */
  3. /* eslint-disable max-lines-per-function */
  4. /* eslint-disable no-unused-vars */
  5. import React from 'react';
  6. import cls from 'classnames';
  7. import PropTypes from 'prop-types';
  8. import { get } from 'lodash';
  9. import DateInputFoundation, {
  10. DateInputAdapter,
  11. DateInputFoundationProps,
  12. RangeType,
  13. InsetInputChangeProps,
  14. InsetInputChangeFoundationProps,
  15. InsetInputProps
  16. } from '@douyinfe/semi-foundation/datePicker/inputFoundation';
  17. import { cssClasses, strings } from '@douyinfe/semi-foundation/datePicker/constants';
  18. import { noop } from '@douyinfe/semi-foundation/utils/function';
  19. import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
  20. import { IconCalendar, IconCalendarClock, IconClear } from '@douyinfe/semi-icons';
  21. import { BaseValueType, ValueType } from '@douyinfe/semi-foundation/datePicker/foundation';
  22. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  23. import Input from '../input/index';
  24. import { InsetDateInput, InsetTimeInput } from './insetInput';
  25. export interface DateInputProps extends DateInputFoundationProps, BaseProps {
  26. insetLabel?: React.ReactNode;
  27. prefix?: React.ReactNode;
  28. onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
  29. onChange?: (value: string, e: React.MouseEvent<HTMLInputElement>) => void;
  30. onEnterPress?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
  31. onBlur?: (e: React.MouseEvent<HTMLInputElement>) => void;
  32. onFocus?: (e: React.MouseEvent<HTMLInputElement>, rangeType?: RangeType) => void;
  33. onClear?: (e: React.MouseEvent<HTMLDivElement>) => void;
  34. onInsetInputChange?: (options: InsetInputChangeProps) => void;
  35. value?: Date[]
  36. }
  37. // eslint-disable-next-line @typescript-eslint/ban-types
  38. export default class DateInput extends BaseComponent<DateInputProps, {}> {
  39. static propTypes = {
  40. onClick: PropTypes.func,
  41. onChange: PropTypes.func,
  42. onEnterPress: PropTypes.func,
  43. onBlur: PropTypes.func,
  44. onClear: PropTypes.func,
  45. onFocus: PropTypes.func,
  46. value: PropTypes.array,
  47. disabled: PropTypes.bool,
  48. type: PropTypes.oneOf(strings.TYPE_SET),
  49. showClear: PropTypes.bool,
  50. format: PropTypes.string, // Attributes not used
  51. inputStyle: PropTypes.object,
  52. inputReadOnly: PropTypes.bool, // Text box can be entered
  53. insetLabel: PropTypes.node,
  54. validateStatus: PropTypes.string,
  55. prefix: PropTypes.node,
  56. prefixCls: PropTypes.string,
  57. dateFnsLocale: PropTypes.object.isRequired, // Foundation useful to
  58. placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  59. rangeInputFocus: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
  60. rangeInputStartRef: PropTypes.object,
  61. rangeInputEndRef: PropTypes.object,
  62. rangeSeparator: PropTypes.string,
  63. insetInput: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
  64. insetInputValue: PropTypes.object,
  65. defaultPickerValue: PropTypes.oneOfType([
  66. PropTypes.string,
  67. PropTypes.number,
  68. PropTypes.object,
  69. PropTypes.array,
  70. ]),
  71. };
  72. static defaultProps = {
  73. showClear: true,
  74. onClick: noop,
  75. onChange: noop,
  76. onEnterPress: noop,
  77. onBlur: noop,
  78. onClear: noop,
  79. onFocus: noop,
  80. type: 'date',
  81. inputStyle: {},
  82. inputReadOnly: false,
  83. prefixCls: cssClasses.PREFIX,
  84. rangeSeparator: strings.DEFAULT_SEPARATOR_RANGE,
  85. };
  86. foundation: DateInputFoundation;
  87. constructor(props: DateInputProps) {
  88. super(props);
  89. this.foundation = new DateInputFoundation(this.adapter);
  90. }
  91. get adapter(): DateInputAdapter {
  92. return {
  93. ...super.adapter,
  94. updateIsFocusing: isFocusing => this.setState({ isFocusing }),
  95. notifyClick: (...args) => this.props.onClick(...args),
  96. notifyChange: (...args) => this.props.onChange(...args),
  97. notifyEnter: (...args) => this.props.onEnterPress(...args),
  98. notifyBlur: (...args) => this.props.onBlur(...args),
  99. notifyClear: (...args) => this.props.onClear(...args),
  100. notifyFocus: (...args) => this.props.onFocus(...args),
  101. notifyRangeInputClear: (...args) => this.props.onRangeClear(...args),
  102. notifyRangeInputFocus: (...args) => this.props.onFocus(...args),
  103. notifyTabPress: (...args) => this.props.onRangeEndTabPress(...args),
  104. notifyInsetInputChange: options => this.props.onInsetInputChange(options),
  105. };
  106. }
  107. componentDidMount() {
  108. this.foundation.init();
  109. }
  110. componentWillUnmount() {
  111. this.foundation.destroy();
  112. }
  113. formatText(value: ValueType) {
  114. // eslint-disable-next-line max-len
  115. return value && (value as BaseValueType[]).length ? this.foundation.formatShowText(value as BaseValueType[]) : '';
  116. }
  117. handleChange = (value: string, e: React.ChangeEvent<HTMLInputElement>) => this.foundation.handleChange(value, e);
  118. handleEnterPress = (e: React.KeyboardEvent<HTMLInputElement>) => this.foundation.handleInputComplete(e);
  119. handleInputClear = (e: React.MouseEvent<HTMLDivElement>) => this.foundation.handleInputClear(e);
  120. handleRangeInputChange = (rangeStart: string, rangeEnd: string, e: React.ChangeEvent) => {
  121. const rangeInputValue = this.getRangeInputValue(rangeStart, rangeEnd);
  122. this.foundation.handleChange(rangeInputValue, e);
  123. };
  124. handleRangeInputClear: React.MouseEventHandler<HTMLDivElement> = e => {
  125. this.foundation.handleRangeInputClear(e);
  126. };
  127. handleRangeInputEnterPress = (e: React.KeyboardEvent<HTMLInputElement>, rangeStart: string, rangeEnd: string) => {
  128. const rangeInputValue = this.getRangeInputValue(rangeStart, rangeEnd);
  129. this.foundation.handleRangeInputEnterPress(e, rangeInputValue);
  130. };
  131. handleRangeInputEndKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
  132. this.foundation.handleRangeInputEndKeyPress(e);
  133. };
  134. handleRangeInputFocus = (e: React.MouseEvent<HTMLElement>, rangeType: RangeType) => {
  135. this.foundation.handleRangeInputFocus(e, rangeType);
  136. };
  137. handleRangeStartFocus: React.MouseEventHandler<HTMLElement> = e => {
  138. this.handleRangeInputFocus(e, 'rangeStart');
  139. };
  140. handleInsetInputChange = (options: InsetInputChangeFoundationProps) => {
  141. this.foundation.handleInsetInputChange(options);
  142. };
  143. getRangeInputValue = (rangeStart: string, rangeEnd: string) => {
  144. const { rangeSeparator } = this.props;
  145. const rangeInputValue = `${rangeStart}${rangeSeparator}${rangeEnd}`;
  146. return rangeInputValue;
  147. };
  148. renderRangePrefix() {
  149. const { prefix, insetLabel, prefixCls, disabled, rangeInputFocus } = this.props;
  150. const labelNode = prefix || insetLabel;
  151. return labelNode ? (
  152. <div
  153. className={`${prefixCls}-range-input-prefix`}
  154. onClick={e => !disabled && !rangeInputFocus && this.handleRangeStartFocus(e)}
  155. x-semi-prop="prefix,insetLabel"
  156. >
  157. {labelNode}
  158. </div>
  159. ) : null;
  160. }
  161. renderRangeSeparator(rangeStart: string, rangeEnd: string) {
  162. const { disabled, rangeSeparator } = this.props;
  163. const separatorCls = cls({
  164. [`${cssClasses.PREFIX}-range-input-separator`]: true,
  165. [`${cssClasses.PREFIX}-range-input-separator-active`]: (rangeStart || rangeEnd) && !disabled,
  166. });
  167. return (
  168. <span onClick={e => !disabled && this.handleRangeStartFocus(e)} className={separatorCls}>
  169. {rangeSeparator}
  170. </span>
  171. );
  172. }
  173. renderRangeClearBtn(rangeStart: string, rangeEnd: string) {
  174. const { showClear, prefixCls, disabled, clearIcon } = this.props;
  175. const allowClear = (rangeStart || rangeEnd) && showClear;
  176. return allowClear && !disabled ? (
  177. <div
  178. role="button"
  179. tabIndex={0}
  180. aria-label="Clear range input value"
  181. className={`${prefixCls}-range-input-clearbtn`}
  182. onMouseDown={e => !disabled && this.handleRangeInputClear(e)}>
  183. {clearIcon ? clearIcon : <IconClear aria-hidden />}
  184. </div>
  185. ) : null;
  186. }
  187. renderRangeSuffix(suffix: React.ReactNode) {
  188. const { prefixCls, disabled, rangeInputFocus } = this.props;
  189. const rangeSuffix = suffix ? (
  190. <div
  191. className={`${prefixCls}-range-input-suffix`}
  192. onClick={e => !disabled && !rangeInputFocus && this.handleRangeStartFocus(e)}
  193. >
  194. {suffix}
  195. </div>
  196. ) : null;
  197. return rangeSuffix;
  198. }
  199. renderRangeInput(rangeProps: DateInputProps) {
  200. const {
  201. // this.props
  202. placeholder,
  203. inputStyle,
  204. disabled,
  205. inputReadOnly,
  206. autofocus,
  207. size,
  208. // compute props
  209. text,
  210. suffix,
  211. inputCls,
  212. // range only props
  213. rangeInputStartRef,
  214. rangeInputEndRef,
  215. rangeInputFocus,
  216. prefixCls,
  217. rangeSeparator,
  218. } = rangeProps;
  219. const [rangeStart, rangeEnd = ''] = text.split(rangeSeparator) || [];
  220. const rangeSize = size === 'large' ? 'default' : 'small';
  221. const rangePlaceholder = Array.isArray(placeholder) ? placeholder : [placeholder, placeholder];
  222. const [rangeStartPlaceholder, rangeEndPlaceholder] = rangePlaceholder;
  223. const inputLeftWrapperCls = cls(`${prefixCls}-range-input-wrapper-start`, `${prefixCls}-range-input-wrapper`, {
  224. [`${prefixCls}-range-input-wrapper-active`]: rangeInputFocus === 'rangeStart' && !disabled,
  225. [`${prefixCls}-range-input-wrapper-start-with-prefix`]: this.props.prefix || this.props.insetLabel,
  226. });
  227. const inputRightWrapperCls = cls(`${prefixCls}-range-input-wrapper-end`, `${prefixCls}-range-input-wrapper`, {
  228. [`${prefixCls}-range-input-wrapper-active`]: rangeInputFocus === 'rangeEnd' && !disabled,
  229. });
  230. return (
  231. <>
  232. {this.renderRangePrefix()}
  233. <div
  234. onClick={e => !disabled && this.handleRangeInputFocus(e, 'rangeStart')}
  235. className={`${inputCls} ${inputLeftWrapperCls}`}
  236. >
  237. <Input
  238. size={rangeSize}
  239. style={inputStyle}
  240. disabled={disabled}
  241. readonly={inputReadOnly}
  242. placeholder={rangeStartPlaceholder}
  243. value={rangeStart}
  244. // range input onBlur function is called when panel is closed
  245. // onBlur={noop}
  246. onChange={(rangeStartValue, e) => this.handleRangeInputChange(rangeStartValue, rangeEnd, e)}
  247. onEnterPress={e => this.handleRangeInputEnterPress(e, rangeStart, rangeEnd)}
  248. onFocus={e => this.handleRangeInputFocus(e as any, 'rangeStart')}
  249. autofocus={autofocus} // autofocus moved to range start
  250. ref={rangeInputStartRef}
  251. />
  252. </div>
  253. {this.renderRangeSeparator(rangeStart, rangeEnd)}
  254. <div
  255. className={`${inputCls} ${inputRightWrapperCls}`}
  256. onClick={e => !disabled && this.handleRangeInputFocus(e, 'rangeEnd')}
  257. >
  258. <Input
  259. size={rangeSize}
  260. style={inputStyle}
  261. disabled={disabled}
  262. readonly={inputReadOnly}
  263. placeholder={rangeEndPlaceholder}
  264. value={rangeEnd}
  265. // range input onBlur function is called when panel is closed
  266. // onBlur={noop}
  267. onChange={(rangeEndValue, e) => this.handleRangeInputChange(rangeStart, rangeEndValue, e)}
  268. onEnterPress={e => this.handleRangeInputEnterPress(e, rangeStart, rangeEnd)}
  269. onFocus={e => this.handleRangeInputFocus(e as any, 'rangeEnd')}
  270. onKeyDown={this.handleRangeInputEndKeyPress} // only monitor tab button on range end
  271. ref={rangeInputEndRef}
  272. />
  273. </div>
  274. {this.renderRangeClearBtn(rangeStart, rangeEnd)}
  275. {this.renderRangeSuffix(suffix)}
  276. </>
  277. );
  278. }
  279. renderInputInset() {
  280. const {
  281. type,
  282. handleInsetDateFocus,
  283. handleInsetTimeFocus,
  284. value,
  285. insetInputValue,
  286. prefixCls,
  287. rangeInputStartRef,
  288. rangeInputEndRef,
  289. density,
  290. insetInput,
  291. } = this.props;
  292. const _isRangeType = type.includes('Range');
  293. const newInsetInputValue = this.foundation.getInsetInputValue({ value, insetInputValue });
  294. const { dateStart, dateEnd, timeStart, timeEnd } = get(insetInput, 'placeholder', {}) as InsetInputProps['placeholder'];
  295. const { datePlaceholder, timePlaceholder } = this.foundation.getInsetInputPlaceholder();
  296. const insetInputWrapperCls = `${prefixCls}-inset-input-wrapper`;
  297. const separatorCls = `${prefixCls}-inset-input-separator`;
  298. return (
  299. <div className={insetInputWrapperCls} x-type={type}>
  300. <InsetDateInput
  301. forwardRef={rangeInputStartRef}
  302. insetInputValue={newInsetInputValue}
  303. placeholder={dateStart ?? datePlaceholder}
  304. valuePath={'monthLeft.dateInput'}
  305. onChange={this.handleInsetInputChange}
  306. onFocus={e => handleInsetDateFocus(e, 'rangeStart')}
  307. />
  308. <InsetTimeInput
  309. disabled={!newInsetInputValue.monthLeft.dateInput}
  310. insetInputValue={newInsetInputValue}
  311. placeholder={timeStart ?? timePlaceholder}
  312. type={type}
  313. valuePath={'monthLeft.timeInput'}
  314. onChange={this.handleInsetInputChange}
  315. onFocus={handleInsetTimeFocus}
  316. />
  317. {_isRangeType && (
  318. <>
  319. <div className={separatorCls}>{density === 'compact' ? null : '-'}</div>
  320. <InsetDateInput
  321. forwardRef={rangeInputEndRef}
  322. insetInputValue={newInsetInputValue}
  323. placeholder={dateEnd ?? datePlaceholder}
  324. valuePath={'monthRight.dateInput'}
  325. onChange={this.handleInsetInputChange}
  326. onFocus={e => handleInsetDateFocus(e, 'rangeEnd')}
  327. />
  328. <InsetTimeInput
  329. disabled={!newInsetInputValue.monthRight.dateInput}
  330. insetInputValue={newInsetInputValue}
  331. placeholder={timeEnd ?? timePlaceholder}
  332. type={type}
  333. valuePath={'monthRight.timeInput'}
  334. onChange={this.handleInsetInputChange}
  335. onFocus={handleInsetTimeFocus}
  336. />
  337. </>
  338. )}
  339. </div>
  340. );
  341. }
  342. renderTriggerInput() {
  343. const {
  344. placeholder,
  345. type,
  346. value,
  347. inputValue,
  348. inputStyle,
  349. disabled,
  350. showClear,
  351. inputReadOnly,
  352. insetLabel,
  353. validateStatus,
  354. block,
  355. prefixCls,
  356. multiple, // Whether to allow multiple values for email and file types
  357. dateFnsLocale, // No need to pass to input
  358. onBlur,
  359. onClear,
  360. onFocus,
  361. prefix,
  362. autofocus,
  363. size,
  364. // range input support props, no need passing to not range type
  365. rangeInputStartRef,
  366. rangeInputEndRef,
  367. onRangeClear,
  368. onRangeBlur,
  369. onRangeEndTabPress,
  370. rangeInputFocus,
  371. rangeSeparator,
  372. insetInput,
  373. insetInputValue,
  374. defaultPickerValue,
  375. ...rest
  376. } = this.props;
  377. const dateIcon = <IconCalendar aria-hidden />;
  378. const dateTimeIcon = <IconCalendarClock aria-hidden />;
  379. const suffix = type.includes('Time') ? dateTimeIcon : dateIcon;
  380. let text = '';
  381. if (!isNullOrUndefined(inputValue)) {
  382. text = inputValue;
  383. } else if (value) {
  384. text = this.formatText(value);
  385. }
  386. const inputCls = cls({
  387. [`${prefixCls}-input-readonly`]: inputReadOnly,
  388. });
  389. const isRangeType = /range/i.test(type);
  390. const rangeProps = { ...this.props, text, suffix, inputCls };
  391. return isRangeType ? (
  392. this.renderRangeInput(rangeProps)
  393. ) : (
  394. <Input
  395. {...rest}
  396. insetLabel={insetLabel}
  397. disabled={disabled}
  398. readonly={inputReadOnly}
  399. className={inputCls}
  400. style={inputStyle}
  401. hideSuffix={showClear}
  402. placeholder={placeholder}
  403. onEnterPress={this.handleEnterPress}
  404. onChange={this.handleChange}
  405. onClear={this.handleInputClear}
  406. suffix={suffix}
  407. showClear={showClear}
  408. value={text}
  409. validateStatus={validateStatus}
  410. prefix={prefix}
  411. autofocus={autofocus}
  412. size={size}
  413. onBlur={onBlur as any}
  414. onFocus={onFocus as any}
  415. />
  416. );
  417. }
  418. render() {
  419. const { insetInput } = this.props;
  420. return insetInput ? this.renderInputInset() : this.renderTriggerInput();
  421. }
  422. }