1
0

datePicker.tsx 35 KB

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