datePicker.tsx 37 KB

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