datePicker.tsx 30 KB

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