dateInput.tsx 14 KB

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