1
0

dateInput.tsx 19 KB

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