datePicker.tsx 28 KB

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