dateInput.tsx 19 KB

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