datePicker.tsx 40 KB

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