1
0

dateInput.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /* eslint-disable jsx-a11y/click-events-have-key-events */
  2. /* eslint-disable jsx-a11y/no-static-element-interactions */
  3. import React from 'react';
  4. import cls from 'classnames';
  5. import PropTypes from 'prop-types';
  6. import { get } from 'lodash';
  7. import DateInputFoundation, {
  8. DateInputAdapter,
  9. DateInputFoundationProps,
  10. RangeType,
  11. InsetInputChangeProps,
  12. InsetInputChangeFoundationProps,
  13. InsetInputProps
  14. } from '@douyinfe/semi-foundation/datePicker/inputFoundation';
  15. import { cssClasses, strings } from '@douyinfe/semi-foundation/datePicker/constants';
  16. import { noop } from '@douyinfe/semi-foundation/utils/function';
  17. import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
  18. import { IconCalendar, IconCalendarClock, IconClear } from '@douyinfe/semi-icons';
  19. import { BaseValueType, ValueType } from '@douyinfe/semi-foundation/datePicker/foundation';
  20. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  21. import Input from '../input/index';
  22. import { InsetDateInput, InsetTimeInput } from './insetInput';
  23. export interface DateInputProps extends DateInputFoundationProps, BaseProps {
  24. insetLabel?: React.ReactNode;
  25. prefix?: React.ReactNode;
  26. onClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
  27. onChange?: (value: string, e: React.MouseEvent<HTMLInputElement>) => void;
  28. onEnterPress?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
  29. onBlur?: (e: React.MouseEvent<HTMLInputElement>) => void;
  30. onFocus?: (e: React.MouseEvent<HTMLInputElement>, rangeType?: RangeType) => void;
  31. onClear?: (e: React.MouseEvent<HTMLDivElement>) => void;
  32. onInsetInputChange?: (options: InsetInputChangeProps) => void;
  33. value?: Date[];
  34. inputRef?: React.RefObject<HTMLInputElement>;
  35. rangeInputStartRef?: React.RefObject<HTMLInputElement>;
  36. rangeInputEndRef?: React.RefObject<HTMLInputElement>;
  37. showClearIgnoreDisabled?: boolean
  38. }
  39. // eslint-disable-next-line @typescript-eslint/ban-types
  40. export default class DateInput extends BaseComponent<DateInputProps, {}> {
  41. static propTypes = {
  42. borderless: PropTypes.bool,
  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. borderless: false,
  77. showClear: true,
  78. onClick: noop,
  79. onChange: noop,
  80. onEnterPress: noop,
  81. onBlur: noop,
  82. onClear: noop,
  83. onFocus: noop,
  84. type: 'date',
  85. inputStyle: {},
  86. inputReadOnly: false,
  87. prefixCls: cssClasses.PREFIX,
  88. rangeSeparator: strings.DEFAULT_SEPARATOR_RANGE,
  89. };
  90. foundation: DateInputFoundation;
  91. constructor(props: DateInputProps) {
  92. super(props);
  93. this.foundation = new DateInputFoundation(this.adapter);
  94. }
  95. get adapter(): DateInputAdapter {
  96. return {
  97. ...super.adapter,
  98. updateIsFocusing: isFocusing => this.setState({ isFocusing }),
  99. notifyClick: (...args) => this.props.onClick(...args),
  100. notifyChange: (...args) => this.props.onChange(...args),
  101. notifyEnter: (...args) => this.props.onEnterPress(...args),
  102. notifyBlur: (...args) => this.props.onBlur(...args),
  103. notifyClear: (...args) => this.props.onClear(...args),
  104. notifyFocus: (...args) => this.props.onFocus(...args),
  105. notifyRangeInputClear: (...args) => this.props.onRangeClear(...args),
  106. notifyRangeInputFocus: (...args) => this.props.onFocus(...args),
  107. notifyTabPress: (...args) => this.props.onRangeEndTabPress(...args),
  108. notifyInsetInputChange: options => this.props.onInsetInputChange(options),
  109. };
  110. }
  111. componentDidMount() {
  112. this.foundation.init();
  113. }
  114. componentWillUnmount() {
  115. this.foundation.destroy();
  116. }
  117. formatText(value: ValueType) {
  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, showClearIgnoreDisabled } = this.props;
  178. const isRealDisabled = disabled && !showClearIgnoreDisabled;
  179. const allowClear = (rangeStart || rangeEnd) && showClear && !isRealDisabled;
  180. return allowClear ? (
  181. <div
  182. role="button"
  183. tabIndex={0}
  184. aria-label="Clear range input value"
  185. className={`${prefixCls}-range-input-clearbtn`}
  186. onMouseDown={e => this.handleRangeInputClear(e)}>
  187. {clearIcon ? clearIcon : <IconClear aria-hidden />}
  188. </div>
  189. ) : null;
  190. }
  191. renderRangeSuffix(suffix: React.ReactNode) {
  192. const { prefixCls, disabled, rangeInputFocus } = this.props;
  193. const rangeSuffix = suffix ? (
  194. <div
  195. className={`${prefixCls}-range-input-suffix`}
  196. onClick={e => !disabled && !rangeInputFocus && this.handleRangeStartFocus(e)}
  197. >
  198. {suffix}
  199. </div>
  200. ) : null;
  201. return rangeSuffix;
  202. }
  203. renderRangeInput(rangeProps: DateInputProps) {
  204. const {
  205. // this.props
  206. placeholder,
  207. inputStyle,
  208. disabled,
  209. inputReadOnly,
  210. autofocus,
  211. size,
  212. // compute props
  213. text,
  214. suffix,
  215. inputCls,
  216. // range only props
  217. rangeInputStartRef,
  218. rangeInputEndRef,
  219. rangeInputFocus,
  220. prefixCls,
  221. rangeSeparator,
  222. borderless
  223. } = rangeProps;
  224. const [rangeStart, rangeEnd = ''] = text.split(rangeSeparator) || [];
  225. const rangeSize = size === 'large' ? 'default' : 'small';
  226. const rangePlaceholder = Array.isArray(placeholder) ? placeholder : [placeholder, placeholder];
  227. const [rangeStartPlaceholder, rangeEndPlaceholder] = rangePlaceholder;
  228. const inputLeftWrapperCls = cls(`${prefixCls}-range-input-wrapper-start`, `${prefixCls}-range-input-wrapper`, {
  229. [`${prefixCls}-range-input-wrapper-active`]: rangeInputFocus === 'rangeStart' && !disabled,
  230. [`${prefixCls}-range-input-wrapper-start-with-prefix`]: this.props.prefix || this.props.insetLabel,
  231. [`${prefixCls}-borderless`]: borderless
  232. });
  233. const inputRightWrapperCls = cls(`${prefixCls}-range-input-wrapper-end`, `${prefixCls}-range-input-wrapper`, {
  234. [`${prefixCls}-range-input-wrapper-active`]: rangeInputFocus === 'rangeEnd' && !disabled,
  235. [`${prefixCls}-borderless`]: borderless
  236. });
  237. return (
  238. <>
  239. {this.renderRangePrefix()}
  240. <div
  241. onClick={e => !disabled && this.handleRangeInputFocus(e, 'rangeStart')}
  242. className={`${inputCls} ${inputLeftWrapperCls}`}
  243. >
  244. <Input
  245. borderless={borderless}
  246. size={rangeSize}
  247. style={inputStyle}
  248. disabled={disabled}
  249. readonly={inputReadOnly}
  250. placeholder={rangeStartPlaceholder}
  251. value={rangeStart}
  252. // range input onBlur function is called when panel is closed
  253. // onBlur={noop}
  254. onChange={(rangeStartValue, e) => this.handleRangeInputChange(rangeStartValue, rangeEnd, e)}
  255. onEnterPress={e => this.handleRangeInputEnterPress(e, rangeStart, rangeEnd)}
  256. onFocus={e => this.handleRangeInputFocus(e as any, 'rangeStart')}
  257. autoFocus={autofocus} // autofocus moved to range start
  258. ref={rangeInputStartRef}
  259. />
  260. </div>
  261. {this.renderRangeSeparator(rangeStart, rangeEnd)}
  262. <div
  263. className={`${inputCls} ${inputRightWrapperCls}`}
  264. onClick={e => !disabled && this.handleRangeInputFocus(e, 'rangeEnd')}
  265. >
  266. <Input
  267. borderless={borderless}
  268. size={rangeSize}
  269. style={inputStyle}
  270. disabled={disabled}
  271. readonly={inputReadOnly}
  272. placeholder={rangeEndPlaceholder}
  273. value={rangeEnd}
  274. // range input onBlur function is called when panel is closed
  275. // onBlur={noop}
  276. onChange={(rangeEndValue, e) => this.handleRangeInputChange(rangeStart, rangeEndValue, e)}
  277. onEnterPress={e => this.handleRangeInputEnterPress(e, rangeStart, rangeEnd)}
  278. onFocus={e => this.handleRangeInputFocus(e as any, 'rangeEnd')}
  279. onKeyDown={this.handleRangeInputEndKeyPress} // only monitor tab button on range end
  280. ref={rangeInputEndRef}
  281. />
  282. </div>
  283. {this.renderRangeClearBtn(rangeStart, rangeEnd)}
  284. {this.renderRangeSuffix(suffix)}
  285. </>
  286. );
  287. }
  288. isRenderMultipleInputs() {
  289. const { type } = this.props;
  290. // isRange and not monthRange render multiple inputs
  291. return type.includes('Range') && type !== 'monthRange';
  292. }
  293. renderInputInset() {
  294. const {
  295. type,
  296. handleInsetDateFocus,
  297. handleInsetTimeFocus,
  298. value,
  299. insetInputValue,
  300. prefixCls,
  301. rangeInputStartRef,
  302. rangeInputEndRef,
  303. density,
  304. insetInput,
  305. } = this.props;
  306. const newInsetInputValue = this.foundation.getInsetInputValue({ value, insetInputValue });
  307. const { dateStart, dateEnd, timeStart, timeEnd } = get(insetInput, 'placeholder', {}) as InsetInputProps['placeholder'];
  308. const { datePlaceholder, timePlaceholder } = this.foundation.getInsetInputPlaceholder();
  309. const insetInputWrapperCls = `${prefixCls}-inset-input-wrapper`;
  310. const separatorCls = `${prefixCls}-inset-input-separator`;
  311. return (
  312. <div className={insetInputWrapperCls} x-type={type}>
  313. <InsetDateInput
  314. forwardRef={rangeInputStartRef}
  315. insetInputValue={newInsetInputValue}
  316. placeholder={dateStart ?? datePlaceholder}
  317. valuePath={'monthLeft.dateInput'}
  318. onChange={this.handleInsetInputChange}
  319. onFocus={e => handleInsetDateFocus(e, 'rangeStart')}
  320. />
  321. <InsetTimeInput
  322. disabled={!newInsetInputValue.monthLeft.dateInput}
  323. insetInputValue={newInsetInputValue}
  324. placeholder={timeStart ?? timePlaceholder}
  325. type={type}
  326. valuePath={'monthLeft.timeInput'}
  327. onChange={this.handleInsetInputChange}
  328. onFocus={handleInsetTimeFocus}
  329. />
  330. {this.isRenderMultipleInputs() && (
  331. <>
  332. <div className={separatorCls}>{density === 'compact' ? null : '-'}</div>
  333. <InsetDateInput
  334. forwardRef={rangeInputEndRef}
  335. insetInputValue={newInsetInputValue}
  336. placeholder={dateEnd ?? datePlaceholder}
  337. valuePath={'monthRight.dateInput'}
  338. onChange={this.handleInsetInputChange}
  339. onFocus={e => handleInsetDateFocus(e, 'rangeEnd')}
  340. />
  341. <InsetTimeInput
  342. disabled={!newInsetInputValue.monthRight.dateInput}
  343. insetInputValue={newInsetInputValue}
  344. placeholder={timeEnd ?? timePlaceholder}
  345. type={type}
  346. valuePath={'monthRight.timeInput'}
  347. onChange={this.handleInsetInputChange}
  348. onFocus={handleInsetTimeFocus}
  349. />
  350. </>
  351. )}
  352. </div>
  353. );
  354. }
  355. renderTriggerInput() {
  356. const {
  357. placeholder,
  358. type,
  359. value,
  360. inputValue,
  361. inputStyle,
  362. disabled,
  363. showClear,
  364. inputReadOnly,
  365. insetLabel,
  366. validateStatus,
  367. block,
  368. prefixCls,
  369. multiple, // Whether to allow multiple values for email and file types
  370. dateFnsLocale, // No need to pass to input
  371. onBlur,
  372. onClear,
  373. onFocus,
  374. prefix,
  375. autofocus,
  376. size,
  377. inputRef,
  378. // range input support props, no need passing to not range type
  379. rangeInputStartRef,
  380. rangeInputEndRef,
  381. onRangeClear,
  382. onRangeBlur,
  383. onRangeEndTabPress,
  384. rangeInputFocus,
  385. rangeSeparator,
  386. insetInput,
  387. insetInputValue,
  388. defaultPickerValue,
  389. showClearIgnoreDisabled,
  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. showClearIgnoreDisabled={showClearIgnoreDisabled}
  415. readonly={inputReadOnly}
  416. className={inputCls}
  417. style={inputStyle}
  418. hideSuffix={showClear}
  419. placeholder={type === 'monthRange' && Array.isArray(placeholder) ? placeholder[0] + rangeSeparator + placeholder[1] : placeholder}
  420. onEnterPress={this.handleEnterPress}
  421. onChange={this.handleChange}
  422. onClear={this.handleInputClear}
  423. suffix={suffix}
  424. showClear={showClear}
  425. value={text}
  426. validateStatus={validateStatus}
  427. prefix={prefix}
  428. autoFocus={autofocus}
  429. size={size}
  430. onBlur={onBlur as any}
  431. onFocus={onFocus as any}
  432. />
  433. );
  434. }
  435. render() {
  436. const { insetInput } = this.props;
  437. return insetInput ? this.renderInputInset() : this.renderTriggerInput();
  438. }
  439. }