datePicker.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. /* eslint-disable max-len */
  2. import React from 'react';
  3. import classnames from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import { noop, stubFalse, isDate, get, isFunction } from 'lodash';
  6. import ConfigContext from '../configProvider/context';
  7. import DatePickerFoundation, { DatePickerAdapter, DatePickerFoundationProps, DatePickerFoundationState, DayStatusType, PresetType, Type } from '@douyinfe/semi-foundation/datePicker/foundation';
  8. import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/datePicker/constants';
  9. import { strings as popoverStrings, numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  10. import BaseComponent from '../_base/baseComponent';
  11. import Popover from '../popover/index';
  12. import DateInput, { DateInputProps } from './dateInput';
  13. import MonthsGrid, { MonthsGridProps } from './monthsGrid';
  14. import QuickControl from './quickControl';
  15. import Footer from './footer';
  16. import Trigger from '../trigger';
  17. import YearAndMonth, { YearAndMonthProps } from './yearAndMonth';
  18. import '@douyinfe/semi-foundation/datePicker/datePicker.scss';
  19. import { Locale } from '../locale/interface';
  20. import { RangeType } from '@douyinfe/semi-foundation/datePicker/inputFoundation';
  21. import { TimePickerProps } from '../timePicker/TimePicker';
  22. export interface DatePickerProps extends DatePickerFoundationProps {
  23. 'aria-describedby'?: React.AriaAttributes['aria-describedby'];
  24. 'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
  25. 'aria-invalid'?: React.AriaAttributes['aria-invalid'];
  26. 'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
  27. 'aria-required'?: React.AriaAttributes['aria-required'];
  28. timePickerOpts?: TimePickerProps;
  29. bottomSlot?: React.ReactNode;
  30. insetLabel?: React.ReactNode;
  31. insetLabelId?: string;
  32. prefix?: React.ReactNode;
  33. topSlot?: React.ReactNode;
  34. renderDate?: (dayNumber?: number, fullDate?: string) => React.ReactNode;
  35. renderFullDate?: (dayNumber?: number, fullDate?: string, dayStatus?: DayStatusType) => React.ReactNode;
  36. triggerRender?: (props: DatePickerProps) => React.ReactNode;
  37. onBlur?: React.MouseEventHandler<HTMLInputElement>;
  38. onClear?: React.MouseEventHandler<HTMLDivElement>;
  39. onFocus?: (e: React.MouseEvent, rangeType: RangeType) => void;
  40. onPresetClick?: (item: PresetType, e: React.MouseEvent<HTMLDivElement>) => void;
  41. locale?: Locale['DatePicker'];
  42. dateFnsLocale?: Locale['dateFnsLocale'];
  43. }
  44. export type DatePickerState = DatePickerFoundationState;
  45. export default class DatePicker extends BaseComponent<DatePickerProps, DatePickerState> {
  46. static contextType = ConfigContext;
  47. static propTypes = {
  48. 'aria-describedby': PropTypes.string,
  49. 'aria-errormessage': PropTypes.string,
  50. 'aria-invalid': PropTypes.bool,
  51. 'aria-labelledby': PropTypes.string,
  52. 'aria-required': PropTypes.bool,
  53. type: PropTypes.oneOf(strings.TYPE_SET),
  54. size: PropTypes.oneOf(strings.SIZE_SET),
  55. density: PropTypes.oneOf(strings.DENSITY_SET),
  56. defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object, PropTypes.array]),
  57. value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object, PropTypes.array]),
  58. defaultPickerValue: PropTypes.oneOfType([
  59. PropTypes.string,
  60. PropTypes.number,
  61. PropTypes.object,
  62. PropTypes.array,
  63. ]),
  64. disabledTime: PropTypes.func,
  65. disabledTimePicker: PropTypes.bool,
  66. hideDisabledOptions: PropTypes.bool,
  67. format: PropTypes.string,
  68. disabled: PropTypes.bool,
  69. multiple: PropTypes.bool,
  70. max: PropTypes.number, // only work when multiple is true
  71. placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  72. presets: PropTypes.array,
  73. onChange: PropTypes.func,
  74. onChangeWithDateFirst: PropTypes.bool,
  75. weekStartsOn: PropTypes.number,
  76. disabledDate: PropTypes.func,
  77. timePickerOpts: PropTypes.object, // When dateTime, dateTimeRange, pass through the props to timePicker
  78. showClear: PropTypes.bool, // Whether to show the clear button
  79. onOpenChange: PropTypes.func,
  80. open: PropTypes.bool,
  81. defaultOpen: PropTypes.bool,
  82. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  83. className: PropTypes.string,
  84. prefixCls: PropTypes.string,
  85. prefix: PropTypes.node,
  86. insetLabel: PropTypes.node,
  87. insetLabelId: PropTypes.string,
  88. zIndex: PropTypes.number,
  89. position: PropTypes.oneOf(popoverStrings.POSITION_SET),
  90. getPopupContainer: PropTypes.func,
  91. onCancel: PropTypes.func,
  92. onConfirm: PropTypes.func,
  93. needConfirm: PropTypes.bool,
  94. inputStyle: PropTypes.object,
  95. timeZone: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  96. triggerRender: PropTypes.func,
  97. stopPropagation: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
  98. autoAdjustOverflow: PropTypes.bool,
  99. onBlur: PropTypes.func,
  100. onFocus: PropTypes.func,
  101. onClear: PropTypes.func,
  102. style: PropTypes.object,
  103. autoFocus: PropTypes.bool,
  104. inputReadOnly: PropTypes.bool, // Text box can be entered
  105. validateStatus: PropTypes.oneOf(strings.STATUS),
  106. renderDate: PropTypes.func,
  107. renderFullDate: PropTypes.func,
  108. spacing: PropTypes.number,
  109. startDateOffset: PropTypes.func,
  110. endDateOffset: PropTypes.func,
  111. autoSwitchDate: PropTypes.bool,
  112. dropdownClassName: PropTypes.string,
  113. dropdownStyle: PropTypes.object,
  114. topSlot: PropTypes.node,
  115. bottomSlot: PropTypes.node,
  116. dateFnsLocale: PropTypes.object, // isRequired, but no need to add isRequired key. ForwardStatics function pass static properties to index.jsx, so there is no need for user to pass the prop.
  117. // Support synchronous switching of months
  118. syncSwitchMonth: PropTypes.bool,
  119. // Callback function for panel date switching
  120. onPanelChange: PropTypes.func,
  121. rangeSeparator: PropTypes.string,
  122. };
  123. static defaultProps = {
  124. onChangeWithDateFirst: true,
  125. autoAdjustOverflow: true,
  126. stopPropagation: true,
  127. motion: true,
  128. prefixCls: cssClasses.PREFIX,
  129. // position: 'bottomLeft',
  130. zIndex: popoverNumbers.DEFAULT_Z_INDEX,
  131. type: 'date',
  132. size: 'default',
  133. density: 'default',
  134. disabled: false,
  135. multiple: false,
  136. defaultOpen: false,
  137. disabledHours: noop,
  138. disabledMinutes: noop,
  139. disabledSeconds: noop,
  140. hideDisabledOptions: false,
  141. onBlur: noop,
  142. onFocus: noop,
  143. onClear: noop,
  144. onCancel: noop,
  145. onConfirm: noop,
  146. onChange: noop,
  147. onOpenChange: noop,
  148. onPanelChange: noop,
  149. onPresetClick: noop,
  150. weekStartsOn: numbers.WEEK_START_ON,
  151. disabledDate: stubFalse,
  152. disabledTime: stubFalse,
  153. inputReadOnly: false,
  154. spacing: numbers.SPACING,
  155. autoSwitchDate: true,
  156. syncSwitchMonth: false,
  157. rangeSeparator: strings.DEFAULT_SEPARATOR_RANGE,
  158. };
  159. triggerElRef: React.MutableRefObject<HTMLElement>;
  160. panelRef: React.RefObject<HTMLDivElement>;
  161. monthGrid: React.RefObject<MonthsGrid>;
  162. rangeInputStartRef: React.RefObject<HTMLElement>;
  163. rangeInputEndRef: React.RefObject<HTMLElement>;
  164. focusRecordsRef: React.RefObject<{ rangeStart: boolean; rangeEnd: boolean }>;
  165. clickOutSideHandler: (e: MouseEvent) => void;
  166. _mounted: boolean;
  167. foundation: DatePickerFoundation;
  168. constructor(props: DatePickerProps) {
  169. super(props);
  170. this.state = {
  171. panelShow: props.open || props.defaultOpen,
  172. isRange: false,
  173. inputValue: null, // Staging input values
  174. value: [], // The currently selected date, each date is a Date object
  175. cachedSelectedValue: null, // Save last selected date
  176. prevTimeZone: null,
  177. motionEnd: false, // Monitor if popover animation ends
  178. rangeInputFocus: undefined, // Optional'rangeStart ',' rangeEnd ', false
  179. autofocus: props.autoFocus || (this.isRangeType(props.type, props.triggerRender) && (props.open || props.defaultOpen))
  180. };
  181. this.adapter.setCache('cachedSelectedValue', null);
  182. this.triggerElRef = React.createRef();
  183. this.panelRef = React.createRef();
  184. this.monthGrid = React.createRef();
  185. this.rangeInputStartRef = React.createRef();
  186. this.rangeInputEndRef = React.createRef();
  187. this.focusRecordsRef = React.createRef();
  188. // @ts-ignore ignore readonly
  189. this.focusRecordsRef.current = {
  190. rangeStart: false,
  191. rangeEnd: false
  192. };
  193. this.foundation = new DatePickerFoundation(this.adapter);
  194. }
  195. get adapter(): DatePickerAdapter {
  196. return {
  197. ...super.adapter,
  198. togglePanel: panelShow => {
  199. this.setState({ panelShow });
  200. if (!panelShow) {
  201. this.focusRecordsRef.current.rangeEnd = false;
  202. this.focusRecordsRef.current.rangeStart = false;
  203. }
  204. },
  205. registerClickOutSide: () => {
  206. if (this.clickOutSideHandler) {
  207. this.adapter.unregisterClickOutSide();
  208. this.clickOutSideHandler = null;
  209. }
  210. this.clickOutSideHandler = e => {
  211. if (this.adapter.needConfirm()) {
  212. return;
  213. }
  214. const triggerEl = this.triggerElRef && this.triggerElRef.current;
  215. const panelEl = this.panelRef && this.panelRef.current;
  216. const isInTrigger = triggerEl && triggerEl.contains(e.target as Node);
  217. const isInPanel = panelEl && panelEl.contains(e.target as Node);
  218. if (!isInTrigger && !isInPanel && this._mounted) {
  219. this.foundation.closePanel(e);
  220. }
  221. };
  222. document.addEventListener('mousedown', this.clickOutSideHandler);
  223. },
  224. unregisterClickOutSide: () => {
  225. document.removeEventListener('mousedown', this.clickOutSideHandler);
  226. },
  227. notifyBlur: (...args) => this.props.onBlur(...args),
  228. notifyFocus: (...args) => this.props.onFocus(...args),
  229. notifyClear: (...args) => this.props.onClear(...args),
  230. notifyChange: (...args) => this.props.onChange(...args),
  231. notifyCancel: (...args) => this.props.onCancel(...args),
  232. notifyConfirm: (...args) => this.props.onConfirm(...args),
  233. notifyOpenChange: (...args) => this.props.onOpenChange(...args),
  234. notifyPresetsClick: (...args) => this.props.onPresetClick(...args),
  235. updateValue: value => this.setState({ value }),
  236. updatePrevTimezone: prevTimeZone => this.setState({ prevTimeZone }),
  237. updateCachedSelectedValue: cachedSelectedValue => {
  238. let _cachedSelectedValue = cachedSelectedValue;
  239. if (cachedSelectedValue && !Array.isArray(cachedSelectedValue)) {
  240. _cachedSelectedValue = [...cachedSelectedValue as any];
  241. }
  242. this.setState({ cachedSelectedValue: _cachedSelectedValue });
  243. },
  244. updateInputValue: inputValue => {
  245. this.setState({ inputValue });
  246. },
  247. needConfirm: () =>
  248. ['dateTime', 'dateTimeRange'].includes(this.props.type) && this.props.needConfirm === true,
  249. typeIsYearOrMonth: () => ['month', 'year'].includes(this.props.type),
  250. setMotionEnd: motionEnd => this.setState({ motionEnd }),
  251. setRangeInputFocus: rangeInputFocus => {
  252. if (rangeInputFocus !== this.state.rangeInputFocus) {
  253. this.setState({ rangeInputFocus });
  254. }
  255. switch (rangeInputFocus) {
  256. case 'rangeStart':
  257. const inputStartNode = get(this, 'rangeInputStartRef.current');
  258. inputStartNode && inputStartNode.focus();
  259. /**
  260. * 解决选择完startDate,切换到endDate后panel被立马关闭的问题。
  261. * 用户打开panel,选了startDate后,会执行setRangeInputFocus('rangeEnd'),focus到endDateInput,
  262. * 同时会走到datePicker/foundation.js中的handleSelectedChange方法,在这个方法里会根据focusRecordsRef来判断是否可以关闭panel。
  263. * 如果在setRangeInputFocus里同步修改了focusRecordsRef的状态为true,那在handleSelectedChange里会误判startDate和endDate都已经完成选择,
  264. * 导致endDate还没选就关闭了panel
  265. *
  266. * Fix the problem that the panel is closed immediately after switching to endDate after starting Date is selected.
  267. * The user opens the panel and after starting Date is selected, setRangeInputFocus ('rangeEnd') will be executed, focus to endDateInput,
  268. * At the same time, it will go to the handleSelectedChange method in datePicker/foundation.js, where it will be determined whether the panel can be closed according to focusRecordsRef.
  269. * If the status of focusRecordsRef is modified synchronously in setRangeInputFocus to true, then in handleSelectedChange it will be misjudged that both begDate and endDate have completed the selection,
  270. * resulting in the panel being closed before endDate is selected
  271. */
  272. setTimeout(() => {
  273. this.focusRecordsRef.current.rangeStart = true;
  274. }, 0);
  275. break;
  276. case 'rangeEnd':
  277. const inputEndNode = get(this, 'rangeInputEndRef.current');
  278. inputEndNode && inputEndNode.focus();
  279. /**
  280. * 解决选择完startDate,切换到endDate后panel被立马关闭的问题。
  281. * 用户打开panel,选了startDate后,会执行setRangeInputFocus('rangeEnd'),focus到endDateInput,
  282. * 同时会走到datePicker/foundation.js中的handleSelectedChange方法,在这个方法里会根据focusRecordsRef来判断是否可以关闭panel。
  283. * 如果在setRangeInputFocus里同步修改了focusRecordsRef的状态为true,那在handleSelectedChange里会误判startDate和endDate都已经完成选择,
  284. * 导致endDate还没选就关闭了panel
  285. *
  286. * Fix the problem that the panel is closed immediately after switching to endDate after starting Date is selected.
  287. * The user opens the panel and after starting Date is selected, setRangeInputFocus ('rangeEnd') will be executed, focus to endDateInput,
  288. * At the same time, it will go to the handleSelectedChange method in datePicker/foundation.js, where it will be determined whether the panel can be closed according to focusRecordsRef.
  289. * If the status of focusRecordsRef is modified synchronously in setRangeInputFocus to true, then in handleSelectedChange it will be misjudged that both begDate and endDate have completed the selection,
  290. * resulting in the panel being closed before endDate is selected
  291. */
  292. setTimeout(() => {
  293. this.focusRecordsRef.current.rangeEnd = true;
  294. }, 0);
  295. break;
  296. default:
  297. return;
  298. }
  299. },
  300. couldPanelClosed: () => this.focusRecordsRef.current.rangeStart && this.focusRecordsRef.current.rangeEnd,
  301. isEventTarget: e => e && e.target === e.currentTarget,
  302. };
  303. }
  304. isRangeType(type: Type, triggerRender: DatePickerProps['triggerRender']) {
  305. return /range/i.test(type) && !isFunction(triggerRender);
  306. }
  307. componentDidUpdate(prevProps: DatePickerProps) {
  308. if (prevProps.value !== this.props.value) {
  309. this.foundation.initFromProps({
  310. ...this.props,
  311. });
  312. } else if (this.props.timeZone !== prevProps.timeZone) {
  313. this.foundation.initFromProps({
  314. value: this.state.value,
  315. timeZone: this.props.timeZone,
  316. prevTimeZone: prevProps.timeZone,
  317. });
  318. }
  319. if (prevProps.open !== this.props.open) {
  320. this.foundation.initPanelOpenStatus();
  321. if (!this.props.open) {
  322. this.foundation.clearRangeInputFocus();
  323. }
  324. }
  325. }
  326. componentDidMount() {
  327. this._mounted = true;
  328. super.componentDidMount();
  329. }
  330. componentWillUnmount() {
  331. this._mounted = false;
  332. super.componentWillUnmount();
  333. }
  334. setTriggerRef = (node: HTMLDivElement) => (this.triggerElRef.current = node);
  335. // Called when changes are selected by clicking on the selected date
  336. handleSelectedChange: MonthsGridProps['onChange'] = (v, options) => this.foundation.handleSelectedChange(v, options);
  337. // Called when the year and month change
  338. handleYMSelectedChange: YearAndMonthProps['onSelect'] = item => this.foundation.handleYMSelectedChange(item);
  339. disabledDisposeDate: MonthsGridProps['disabledDate'] = (date, ...rest) => this.foundation.disabledDisposeDate(date, ...rest);
  340. disabledDisposeTime: MonthsGridProps['disabledTime'] = (date, ...rest) => this.foundation.disabledDisposeTime(date, ...rest);
  341. renderMonthGrid(locale: Locale['DatePicker'], localeCode: string, dateFnsLocale: Locale['dateFnsLocale']) {
  342. const {
  343. type,
  344. multiple,
  345. max,
  346. weekStartsOn,
  347. timePickerOpts,
  348. defaultPickerValue,
  349. format,
  350. hideDisabledOptions,
  351. disabledTimePicker,
  352. renderDate,
  353. renderFullDate,
  354. startDateOffset,
  355. endDateOffset,
  356. autoSwitchDate,
  357. density,
  358. syncSwitchMonth,
  359. onPanelChange,
  360. timeZone,
  361. triggerRender
  362. } = this.props;
  363. const { value, cachedSelectedValue, motionEnd, rangeInputFocus } = this.state;
  364. // const cachedSelectedValue = this.adapter.getCache('cachedSelectedValue');
  365. let defaultValue = value;
  366. if (this.adapter.needConfirm()) {
  367. defaultValue = cachedSelectedValue;
  368. }
  369. return (
  370. <MonthsGrid
  371. ref={this.monthGrid}
  372. locale={locale}
  373. localeCode={localeCode}
  374. dateFnsLocale={dateFnsLocale}
  375. weekStartsOn={weekStartsOn}
  376. type={type}
  377. multiple={multiple}
  378. max={max}
  379. format={format}
  380. disabledDate={this.disabledDisposeDate}
  381. hideDisabledOptions={hideDisabledOptions}
  382. disabledTimePicker={disabledTimePicker}
  383. disabledTime={this.disabledDisposeTime}
  384. defaultValue={defaultValue}
  385. defaultPickerValue={defaultPickerValue}
  386. timePickerOpts={timePickerOpts}
  387. isControlledComponent={!this.adapter.needConfirm() && this.isControlled('value')}
  388. onChange={this.handleSelectedChange}
  389. renderDate={renderDate}
  390. renderFullDate={renderFullDate}
  391. startDateOffset={startDateOffset}
  392. endDateOffset={endDateOffset}
  393. autoSwitchDate={autoSwitchDate}
  394. motionEnd={motionEnd}
  395. density={density}
  396. rangeInputFocus={rangeInputFocus}
  397. setRangeInputFocus={this.handleSetRangeFocus}
  398. isAnotherPanelHasOpened={this.isAnotherPanelHasOpened}
  399. syncSwitchMonth={syncSwitchMonth}
  400. onPanelChange={onPanelChange}
  401. timeZone={timeZone}
  402. focusRecordsRef={this.focusRecordsRef}
  403. triggerRender={triggerRender}
  404. />
  405. );
  406. }
  407. renderQuickControls() {
  408. const { presets, type } = this.props;
  409. return (
  410. <QuickControl
  411. type={type}
  412. presets={presets}
  413. onPresetClick={(item, e) => this.foundation.handlePresetClick(item, e)}
  414. />
  415. );
  416. }
  417. handleOpenPanel = () => this.foundation.openPanel();
  418. handleInputChange: DatePickerFoundation['handleInputChange'] = (...args) => this.foundation.handleInputChange(...args);
  419. handleInputComplete: DatePickerFoundation['handleInputComplete'] = v => this.foundation.handleInputComplete(v);
  420. handleInputBlur: DateInputProps['onBlur'] = e => this.foundation.handleInputBlur(get(e, 'nativeEvent.target.value'), e);
  421. handleInputFocus: DatePickerFoundation['handleInputFocus'] = (...args) => this.foundation.handleInputFocus(...args);
  422. handleInputClear: DatePickerFoundation['handleInputClear'] = e => this.foundation.handleInputClear(e);
  423. handleTriggerWrapperClick: DatePickerFoundation['handleTriggerWrapperClick'] = e => this.foundation.handleTriggerWrapperClick(e);
  424. handleSetRangeFocus: DatePickerFoundation['handleSetRangeFocus'] = rangeInputFocus => this.foundation.handleSetRangeFocus(rangeInputFocus);
  425. handleRangeInputBlur = (value: any, e: any) => this.foundation.handleRangeInputBlur(value, e);
  426. handleRangeInputClear: DatePickerFoundation['handleRangeInputClear'] = e => this.foundation.handleRangeInputClear(e);
  427. handleRangeEndTabPress: DatePickerFoundation['handleRangeEndTabPress'] = e => this.foundation.handleRangeEndTabPress(e);
  428. isAnotherPanelHasOpened = (currentRangeInput: RangeType) => {
  429. if (currentRangeInput === 'rangeStart') {
  430. return this.focusRecordsRef.current.rangeEnd;
  431. } else {
  432. return this.focusRecordsRef.current.rangeStart;
  433. }
  434. };
  435. renderInner(extraProps?: Partial<DatePickerProps>) {
  436. const {
  437. type,
  438. format,
  439. multiple,
  440. disabled,
  441. showClear,
  442. insetLabel,
  443. insetLabelId,
  444. placeholder,
  445. validateStatus,
  446. inputStyle,
  447. prefix,
  448. locale,
  449. dateFnsLocale,
  450. triggerRender,
  451. size,
  452. inputReadOnly,
  453. rangeSeparator
  454. } = this.props;
  455. const { value, inputValue, rangeInputFocus } = this.state;
  456. // This class is not needed when triggerRender is function
  457. const isRangeType = this.isRangeType(type, triggerRender);
  458. const inputCls = classnames(`${cssClasses.PREFIX}-input`, {
  459. [`${cssClasses.PREFIX}-range-input`]: isRangeType,
  460. [`${cssClasses.PREFIX}-range-input-${size}`]: isRangeType && size,
  461. [`${cssClasses.PREFIX}-range-input-active`]: isRangeType && rangeInputFocus && !disabled,
  462. [`${cssClasses.PREFIX}-range-input-disabled`]: isRangeType && disabled,
  463. [`${cssClasses.PREFIX}-range-input-${validateStatus}`]: isRangeType && validateStatus,
  464. });
  465. const phText = placeholder || locale.placeholder[type]; // i18n
  466. // These values should be passed to triggerRender, do not delete any key if it is not necessary
  467. const props = {
  468. ...extraProps,
  469. placeholder: phText,
  470. disabled,
  471. inputValue,
  472. value,
  473. onChange: this.handleInputChange,
  474. onEnterPress: this.handleInputComplete,
  475. // TODO: remove in next major version
  476. block: true,
  477. inputStyle,
  478. showClear,
  479. insetLabel,
  480. insetLabelId,
  481. type,
  482. format,
  483. multiple,
  484. validateStatus,
  485. inputReadOnly,
  486. // onClick: this.handleOpenPanel,
  487. onBlur: this.handleInputBlur,
  488. onFocus: this.handleInputFocus,
  489. onClear: this.handleInputClear,
  490. prefix,
  491. size,
  492. autofocus: this.state.autofocus,
  493. dateFnsLocale,
  494. rangeInputStartRef: this.rangeInputStartRef,
  495. rangeInputEndRef: this.rangeInputEndRef,
  496. rangeInputFocus,
  497. rangeSeparator,
  498. onRangeBlur: this.handleRangeInputBlur,
  499. onRangeClear: this.handleRangeInputClear,
  500. onRangeEndTabPress: this.handleRangeEndTabPress,
  501. };
  502. return (
  503. // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
  504. <div
  505. onClick={this.handleTriggerWrapperClick}
  506. className={inputCls}>
  507. {typeof triggerRender === 'function' ? (
  508. <Trigger
  509. {...props}
  510. triggerRender={triggerRender}
  511. componentName="DatePicker"
  512. componentProps={{ ...this.props }}
  513. />
  514. ) : (
  515. <DateInput {...props} />
  516. )}
  517. </div>
  518. );
  519. }
  520. handleConfirm = (e: React.MouseEvent) => this.foundation.handleConfirm();
  521. handleCancel = (e: React.MouseEvent) => this.foundation.handleCancel();
  522. renderFooter = (locale: Locale['DatePicker'], localeCode: string) => {
  523. if (this.adapter.needConfirm()) {
  524. return (
  525. <Footer
  526. {...this.props}
  527. locale={locale}
  528. localeCode={localeCode}
  529. onConfirmClick={this.handleConfirm}
  530. onCancelClick={this.handleCancel}
  531. />
  532. );
  533. }
  534. return null;
  535. };
  536. renderPanel = (locale: Locale['DatePicker'], localeCode: string, dateFnsLocale: Locale['dateFnsLocale']) => {
  537. const { dropdownClassName, dropdownStyle, density, topSlot, bottomSlot } = this.props;
  538. const wrapCls = classnames(
  539. cssClasses.PREFIX,
  540. {
  541. [cssClasses.PANEL_YAM]: this.adapter.typeIsYearOrMonth(),
  542. [`${cssClasses.PREFIX}-compact`]: density === 'compact',
  543. },
  544. dropdownClassName
  545. );
  546. return (
  547. <div ref={this.panelRef} className={wrapCls} style={dropdownStyle}>
  548. {topSlot && <div className={`${cssClasses.PREFIX}-topSlot`}>{topSlot}</div>}
  549. {this.adapter.typeIsYearOrMonth() ?
  550. this.renderYearMonthPanel(locale, localeCode) :
  551. this.renderMonthGrid(locale, localeCode, dateFnsLocale)}
  552. {this.renderQuickControls()}
  553. {bottomSlot && <div className={`${cssClasses.PREFIX}-bottomSlot`}>{bottomSlot}</div>}
  554. {this.renderFooter(locale, localeCode)}
  555. </div>
  556. );
  557. };
  558. renderYearMonthPanel = (locale: Locale['DatePicker'], localeCode: string) => {
  559. const { density } = this.props;
  560. const date = this.state.value[0];
  561. let year = 0;
  562. let month = 0;
  563. if (isDate(date)) {
  564. year = date.getFullYear();
  565. month = date.getMonth() + 1;
  566. }
  567. return (
  568. <YearAndMonth
  569. locale={locale}
  570. localeCode={localeCode}
  571. disabledDate={this.disabledDisposeDate}
  572. noBackBtn
  573. monthCycled
  574. onSelect={this.handleYMSelectedChange}
  575. currentYear={year}
  576. currentMonth={month}
  577. density={density}
  578. />
  579. );
  580. };
  581. wrapPopover = (children: React.ReactNode) => {
  582. const { panelShow } = this.state;
  583. // rtl changes the default position
  584. const { direction } = this.context;
  585. const defaultPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  586. const {
  587. motion,
  588. zIndex,
  589. position = defaultPosition,
  590. getPopupContainer,
  591. locale,
  592. localeCode,
  593. dateFnsLocale,
  594. stopPropagation,
  595. autoAdjustOverflow,
  596. spacing,
  597. } = this.props;
  598. const mergedMotion = this.foundation.getMergedMotion(motion);
  599. return (
  600. <Popover
  601. getPopupContainer={getPopupContainer}
  602. // wrapWhenSpecial={false}
  603. autoAdjustOverflow={autoAdjustOverflow}
  604. zIndex={zIndex}
  605. motion={mergedMotion}
  606. content={this.renderPanel(locale, localeCode, dateFnsLocale)}
  607. trigger="custom"
  608. position={position}
  609. visible={panelShow}
  610. stopPropagation={stopPropagation}
  611. spacing={spacing}
  612. >
  613. {children}
  614. </Popover>
  615. );
  616. };
  617. render() {
  618. const { style, className, prefixCls } = this.props;
  619. const outerProps = {
  620. style,
  621. className: classnames(className, { [prefixCls]: true }),
  622. ref: this.setTriggerRef,
  623. 'aria-invalid': this.props['aria-invalid'],
  624. 'aria-errormessage': this.props['aria-errormessage'],
  625. 'aria-labelledby': this.props['aria-labelledby'],
  626. 'aria-describedby': this.props['aria-describedby'],
  627. 'aria-required': this.props['aria-required'],
  628. };
  629. const inner = this.renderInner();
  630. const wrappedInner = this.wrapPopover(inner);
  631. return <div {...outerProps}>{wrappedInner}</div>;
  632. }
  633. }