datePicker.tsx 35 KB

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