datePicker.tsx 35 KB

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