datePicker.tsx 39 KB

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