dateInput.tsx 19 KB

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