monthsGrid.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. /* eslint-disable jsx-a11y/interactive-supports-focus,jsx-a11y/click-events-have-key-events */
  2. /* eslint-disable jsx-a11y/no-static-element-interactions */
  3. /* eslint-disable react/no-did-update-set-state */
  4. /* eslint-disable max-len */
  5. /* eslint-disable no-nested-ternary */
  6. import React from 'react';
  7. import classnames from 'classnames';
  8. import PropTypes from 'prop-types';
  9. import { format as formatFn, addMonths, isSameDay } from 'date-fns';
  10. import MonthsGridFoundation, { MonthInfo, MonthsGridAdapter, MonthsGridDateAdapter, MonthsGridFoundationProps, MonthsGridFoundationState, MonthsGridRangeAdapter, PanelType } from '@douyinfe/semi-foundation/datePicker/monthsGridFoundation';
  11. import { strings, numbers, cssClasses } from '@douyinfe/semi-foundation/datePicker/constants';
  12. import { compatibleParse } from '@douyinfe/semi-foundation/datePicker/_utils/parser';
  13. import { noop, stubFalse } from 'lodash';
  14. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  15. import Navigation from './navigation';
  16. import Month from './month';
  17. import Combobox from '../timePicker/Combobox';
  18. import YearAndMonth from './yearAndMonth';
  19. import { IconClock, IconCalendar } from '@douyinfe/semi-icons';
  20. import { getDefaultFormatTokenByType } from '@douyinfe/semi-foundation/datePicker/_utils/getDefaultFormatToken';
  21. import getDefaultPickerDate from '@douyinfe/semi-foundation/datePicker/_utils/getDefaultPickerDate';
  22. const prefixCls = cssClasses.PREFIX;
  23. export interface MonthsGridProps extends MonthsGridFoundationProps, BaseProps {
  24. navPrev?: React.ReactNode;
  25. navNext?: React.ReactNode;
  26. renderDate?: () => React.ReactNode;
  27. renderFullDate?: () => React.ReactNode;
  28. focusRecordsRef?: React.RefObject<{ rangeStart: boolean; rangeEnd: boolean }>;
  29. }
  30. export type MonthsGridState = MonthsGridFoundationState;
  31. export default class MonthsGrid extends BaseComponent<MonthsGridProps, MonthsGridState> {
  32. static propTypes = {
  33. type: PropTypes.oneOf(strings.TYPE_SET),
  34. defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object, PropTypes.array]),
  35. defaultPickerValue: PropTypes.oneOfType([
  36. PropTypes.string,
  37. PropTypes.number,
  38. PropTypes.object,
  39. PropTypes.array,
  40. ]),
  41. multiple: PropTypes.bool,
  42. max: PropTypes.number, // only work when multiple is true
  43. weekStartsOn: PropTypes.number,
  44. disabledDate: PropTypes.func,
  45. disabledTime: PropTypes.func,
  46. disabledTimePicker: PropTypes.bool,
  47. hideDisabledOptions: PropTypes.bool,
  48. navPrev: PropTypes.node,
  49. navNext: PropTypes.node,
  50. onMaxSelect: PropTypes.func,
  51. timePickerOpts: PropTypes.object,
  52. // Whether the outer datePicker is a controlled component
  53. isControlledComponent: PropTypes.bool,
  54. rangeStart: PropTypes.oneOfType([PropTypes.string]),
  55. rangeInputFocus: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
  56. locale: PropTypes.object,
  57. localeCode: PropTypes.string,
  58. format: PropTypes.string,
  59. renderDate: PropTypes.func,
  60. renderFullDate: PropTypes.func,
  61. startDateOffset: PropTypes.func,
  62. endDateOffset: PropTypes.func,
  63. autoSwitchDate: PropTypes.bool,
  64. motionEnd: PropTypes.bool,
  65. density: PropTypes.string,
  66. dateFnsLocale: PropTypes.object.isRequired,
  67. timeZone: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  68. // Support synchronous switching of months
  69. syncSwitchMonth: PropTypes.bool,
  70. // Callback function for panel date switching
  71. onPanelChange: PropTypes.func,
  72. focusRecordsRef: PropTypes.object,
  73. triggerRender: PropTypes.func,
  74. presetPosition: PropTypes.oneOf(strings.PRESET_POSITION_SET),
  75. renderQuickControls: PropTypes.node,
  76. renderDateInput: PropTypes.node
  77. };
  78. static defaultProps = {
  79. type: 'date',
  80. rangeStart: '',
  81. multiple: false,
  82. weekStartsOn: numbers.WEEK_START_ON,
  83. disabledDate: stubFalse,
  84. onMaxSelect: noop,
  85. locale: {},
  86. };
  87. foundation: MonthsGridFoundation;
  88. constructor(props: MonthsGridProps) {
  89. super(props);
  90. const validFormat = props.format || getDefaultFormatTokenByType(props.type);
  91. const { nowDate, nextDate } = getDefaultPickerDate({ defaultPickerValue: props.defaultPickerValue, format: validFormat, dateFnsLocale: props.dateFnsLocale });
  92. const dateState = {
  93. // Direct use of full date string storage, mainly considering the month rendering comparison to save a conversion
  94. // The selected value for single or multiple selection, full date string, eg. {'2019-10-01', '2019-10-02'}
  95. selected: new Set<string>(),
  96. };
  97. const rangeState = {
  98. monthLeft: {
  99. pickerDate: nowDate,
  100. showDate: nowDate,
  101. isTimePickerOpen: false,
  102. isYearPickerOpen: false,
  103. },
  104. monthRight: {
  105. pickerDate: nextDate,
  106. showDate: nextDate,
  107. isTimePickerOpen: false,
  108. isYearPickerOpen: false,
  109. },
  110. maxWeekNum: 0, // Maximum number of weeks left and right for manual height adjustment
  111. hoverDay: '', // Real-time hover date
  112. rangeStart: props.rangeStart, // Start date for range selection
  113. rangeEnd: '', // End date of range selection
  114. currentPanelHeight: 0, // current month panel height,
  115. offsetRangeStart: '',
  116. offsetRangeEnd: '',
  117. };
  118. this.state = {
  119. ...dateState,
  120. ...rangeState,
  121. };
  122. this.foundation = new MonthsGridFoundation(this.adapter);
  123. }
  124. get dateAdapter(): MonthsGridDateAdapter {
  125. return {
  126. updateDaySelected: selected => this.setState({ selected }),
  127. };
  128. }
  129. get rangeAdapter(): MonthsGridRangeAdapter {
  130. return {
  131. setRangeStart: rangeStart => this.setState({ rangeStart }),
  132. setRangeEnd: rangeEnd => this.setState({ rangeEnd }),
  133. setHoverDay: hoverDay => this.setState({ hoverDay }),
  134. setWeeksHeight: maxWeekNum => this.setState({ maxWeekNum }),
  135. setOffsetRangeStart: offsetRangeStart => this.setState({ offsetRangeStart }),
  136. setOffsetRangeEnd: offsetRangeEnd => this.setState({ offsetRangeEnd }),
  137. };
  138. }
  139. get adapter(): MonthsGridAdapter {
  140. return {
  141. ...super.adapter,
  142. ...this.dateAdapter,
  143. ...this.rangeAdapter,
  144. updateMonthOnLeft: v => this.setState({ monthLeft: v }),
  145. updateMonthOnRight: v => this.setState({ monthRight: v }),
  146. notifySelectedChange: (value, options) => this.props.onChange(value, options),
  147. notifyMaxLimit: v => this.props.onMaxSelect(v),
  148. notifyPanelChange: (date, dateString) => this.props.onPanelChange(date, dateString),
  149. setRangeInputFocus: rangeInputFocus => this.props.setRangeInputFocus(rangeInputFocus),
  150. isAnotherPanelHasOpened: currentRangeInput => this.props.isAnotherPanelHasOpened(currentRangeInput)
  151. };
  152. }
  153. componentDidMount() {
  154. super.componentDidMount();
  155. }
  156. componentDidUpdate(prevProps: MonthsGridProps, prevState: MonthsGridState) {
  157. const { defaultValue, defaultPickerValue, motionEnd } = this.props;
  158. if (prevProps.defaultValue !== defaultValue) {
  159. // we should always update panel state when value changes
  160. this.foundation.updateSelectedFromProps(defaultValue);
  161. }
  162. if (prevProps.defaultPickerValue !== defaultPickerValue) {
  163. this.foundation.initDefaultPickerValue();
  164. }
  165. if (prevProps.motionEnd !== motionEnd && motionEnd === true) {
  166. if (this.foundation.isRangeType()) {
  167. const currentPanelHeight = this.calcScrollListHeight();
  168. this.setState({ currentPanelHeight });
  169. }
  170. }
  171. const isRange = this.foundation.isRangeType();
  172. if (isRange) {
  173. /**
  174. * we have to add these code to ensure that scroll list's selector places center
  175. */
  176. const prevAll = this.leftIsYearOrTime(prevState) && this.rightIsYearOrTime(prevState);
  177. const prevSome =
  178. (this.leftIsYearOrTime(prevState) && !this.rightIsYearOrTime(prevState)) ||
  179. (!this.leftIsYearOrTime(prevState) && this.rightIsYearOrTime(prevState));
  180. const nowAll = this.leftIsYearOrTime() && this.rightIsYearOrTime();
  181. const nowSome =
  182. (this.leftIsYearOrTime() && !this.rightIsYearOrTime()) ||
  183. (!this.leftIsYearOrTime() && this.rightIsYearOrTime());
  184. const prevAllToSome = prevAll && nowSome;
  185. const prevSomeToAll = prevSome && nowAll;
  186. if (prevSomeToAll) {
  187. this.setState({ currentPanelHeight: this.calcScrollListHeight() }, this.reselect);
  188. } else if (prevAllToSome) {
  189. this.reselect();
  190. }
  191. }
  192. }
  193. cacheRefCurrent = (key: string, current: Combobox | YearAndMonth | HTMLDivElement) => {
  194. if (typeof key === 'string' && key.length) {
  195. this.adapter.setCache(key, current);
  196. }
  197. };
  198. leftIsYearOrTime = (state?: MonthsGridState) => {
  199. const { monthLeft } = state || this.state;
  200. if (monthLeft && (monthLeft.isTimePickerOpen || monthLeft.isYearPickerOpen)) {
  201. return true;
  202. } else {
  203. return false;
  204. }
  205. };
  206. rightIsYearOrTime = (state?: MonthsGridState) => {
  207. const { monthRight } = state || this.state;
  208. if (monthRight && (monthRight.isTimePickerOpen || monthRight.isYearPickerOpen)) {
  209. return true;
  210. } else {
  211. return false;
  212. }
  213. };
  214. /**
  215. * Calculate the height of the scrolling list, if the animation is not over, return 0
  216. */
  217. calcScrollListHeight = () => {
  218. const { motionEnd } = this.props;
  219. let wrapLeft, wrapRight, switchLeft, switchRight;
  220. if (motionEnd) {
  221. wrapLeft = this.adapter.getCache(`wrap-${strings.PANEL_TYPE_LEFT}`);
  222. wrapRight = this.adapter.getCache(`wrap-${strings.PANEL_TYPE_RIGHT}`);
  223. switchLeft = this.adapter.getCache(`switch-${strings.PANEL_TYPE_LEFT}`);
  224. switchRight = this.adapter.getCache(`switch-${strings.PANEL_TYPE_RIGHT}`);
  225. }
  226. const leftRect = wrapLeft && wrapLeft.getBoundingClientRect();
  227. const rightRect = wrapRight && wrapRight.getBoundingClientRect();
  228. let leftHeight = (leftRect && leftRect.height) || 0;
  229. let rightHeight = (rightRect && rightRect.height) || 0;
  230. if (switchLeft) {
  231. leftHeight += switchLeft.getBoundingClientRect().height;
  232. }
  233. if (switchRight) {
  234. rightHeight += switchRight.getBoundingClientRect().height;
  235. }
  236. return Math.max(leftHeight, rightHeight);
  237. };
  238. renderPanel(month: Date, panelType: PanelType) {
  239. let monthCls = classnames(`${prefixCls}-month-grid-${panelType}`);
  240. const { monthLeft, monthRight, currentPanelHeight } = this.state;
  241. const { insetInput } = this.props;
  242. const panelDetail = panelType === strings.PANEL_TYPE_RIGHT ? monthRight : monthLeft;
  243. const { isTimePickerOpen, isYearPickerOpen } = panelDetail;
  244. const panelContent = this.renderMonth(month, panelType);
  245. const yearAndMonthLayer = isYearPickerOpen ? (
  246. <div className={`${prefixCls}-yam`}>{this.renderYearAndMonth(panelType, panelDetail)}</div>
  247. ) : null;
  248. const timePickerLayer = isTimePickerOpen ? (
  249. <div className={`${prefixCls}-tpk`}>{this.renderTimePicker(panelType, panelDetail)}</div>
  250. ) : null;
  251. const style: React.CSSProperties = {};
  252. const wrapLeft = this.adapter.getCache(`wrap-${strings.PANEL_TYPE_LEFT}`);
  253. const wrapRight = this.adapter.getCache(`wrap-${strings.PANEL_TYPE_RIGHT}`);
  254. const wrap = panelType === strings.PANEL_TYPE_RIGHT ? wrapRight : wrapLeft;
  255. if (this.foundation.isRangeType()) {
  256. if (isYearPickerOpen || isTimePickerOpen) {
  257. style.minWidth = wrap.getBoundingClientRect().width;
  258. }
  259. if (this.leftIsYearOrTime() && this.rightIsYearOrTime() && !insetInput) {
  260. /**
  261. * left和right同时为tpk时,panel会有一个minHeight
  262. * 如果缓存的currentPanelHeight为0,则需要计算滚动列表的高度
  263. * 如果有缓存的值则使用currentPanelHeight(若此高度<实际值,则会影响ScrollList中渲染列表的循环次数)
  264. * 详见 packages/semi-foundation/scrollList/itemFoundation.js initWheelList函数
  265. *
  266. * When left and right are tpk at the same time, the panel will have a minHeight
  267. * If the cached currentPanelHeight is 0, you need to calculate the height of the scrolling list
  268. * If there is a cached value, use currentPanelHeight (if this height is less than the actual value, it will affect the number of cycles in the ScrollList to render the list)
  269. * See packages/semi-foundation/scrollList/itemFoundation.js initWheelList function
  270. */
  271. style.minHeight = currentPanelHeight ? currentPanelHeight : this.calcScrollListHeight();
  272. }
  273. } else if (
  274. this.props.type !== 'year' &&
  275. this.props.type !== 'month' &&
  276. (isTimePickerOpen || isYearPickerOpen)
  277. ) {
  278. monthCls = classnames(monthCls, `${prefixCls}-yam-showing`);
  279. }
  280. const _isDatePanelOpen = !(isYearPickerOpen || isTimePickerOpen);
  281. const xOpenType = _isDatePanelOpen ? 'date' : isYearPickerOpen ? 'year' : 'time';
  282. return (
  283. <div className={monthCls} key={panelType} style={style} x-open-type={xOpenType}>
  284. {yearAndMonthLayer}
  285. {timePickerLayer}
  286. {/* {isYearPickerOpen || isTimePickerOpen ? null : panelContent} */}
  287. {this.foundation.isRangeType() ? panelContent : isYearPickerOpen || isTimePickerOpen ? null : panelContent}
  288. {this.renderSwitch(panelType)}
  289. </div>
  290. );
  291. }
  292. showYearPicker(panelType: PanelType, e: React.MouseEvent) {
  293. // e.stopPropagation();
  294. // When switching to the year and month, the e.target at this time is generated from Navigation, and the Navigation module will be removed from the DOM after switching
  295. // If you do not prevent the event from spreading to index.jsx, panel.contain (e.target) in clickOutSide will call closePanel because there is no Nav in the Panel and think this click is clickOutSide
  296. // Cause the entire component pop-up window to be closed by mistake
  297. // console.log(this.navRef.current.clientHeight, this.monthRef.current.clientHeight);
  298. // this.wrapRef.current.style.height = this.wrapRef.current.clientHeight + 'px';
  299. // this.wrapRef.current.style.overflow = 'hidden';
  300. e.nativeEvent.stopImmediatePropagation();
  301. this.foundation.showYearPicker(panelType);
  302. }
  303. renderMonth(month: Date, panelType: PanelType) {
  304. const { selected, rangeStart, rangeEnd, hoverDay, maxWeekNum, offsetRangeStart, offsetRangeEnd } = this.state;
  305. const { weekStartsOn, disabledDate, locale, localeCode, renderDate, renderFullDate, startDateOffset, endDateOffset, density, rangeInputFocus, syncSwitchMonth, multiple } = this.props;
  306. let monthText = '';
  307. // i18n monthText
  308. if (month) {
  309. // Get the absolute value of the year and month
  310. const yearNumber = month ? formatFn(month, 'yyyy') : '';
  311. const monthNumber = month ? formatFn(month, 'L') : '';
  312. // Display the month as the corresponding language text
  313. const mText = locale.months[monthNumber];
  314. const monthFormatToken = locale.monthText;
  315. // Display the year and month in a specific language format order
  316. monthText = monthFormatToken.replace('${year}', yearNumber).replace('${month}', mText);
  317. }
  318. let style = {};
  319. const detail = panelType === strings.PANEL_TYPE_RIGHT ? this.state.monthRight : this.state.monthLeft;
  320. // Whether to select type for range
  321. const isRangeType = this.foundation.isRangeType();
  322. // Whether to switch synchronously for two panels
  323. const shouldBimonthSwitch = isRangeType && syncSwitchMonth;
  324. if (isRangeType && detail && (detail.isYearPickerOpen || detail.isTimePickerOpen)) {
  325. style = {
  326. visibility: 'hidden',
  327. position: 'absolute',
  328. pointerEvents: 'none',
  329. };
  330. }
  331. return (
  332. <div ref={current => this.cacheRefCurrent(`wrap-${panelType}`, current)} style={style}>
  333. <Navigation
  334. forwardRef={current => this.cacheRefCurrent(`nav-${panelType}`, current)}
  335. monthText={monthText}
  336. density={density}
  337. onMonthClick={e => this.showYearPicker(panelType, e)}
  338. onPrevMonth={() => this.foundation.prevMonth(panelType)}
  339. onNextMonth={() => this.foundation.nextMonth(panelType)}
  340. onNextYear={() => this.foundation.nextYear(panelType)}
  341. onPrevYear={() => this.foundation.prevYear(panelType)}
  342. shouldBimonthSwitch={shouldBimonthSwitch}
  343. panelType={panelType}
  344. />
  345. <Month
  346. locale={locale}
  347. localeCode={localeCode}
  348. forwardRef={current => this.cacheRefCurrent(`month-${panelType}`, current)}
  349. disabledDate={disabledDate}
  350. weekStartsOn={weekStartsOn}
  351. month={month}
  352. selected={selected}
  353. rangeStart={rangeStart}
  354. rangeEnd={rangeEnd}
  355. rangeInputFocus={rangeInputFocus}
  356. offsetRangeStart={offsetRangeStart}
  357. offsetRangeEnd={offsetRangeEnd}
  358. hoverDay={hoverDay}
  359. weeksRowNum={maxWeekNum}
  360. renderDate={renderDate}
  361. renderFullDate={renderFullDate}
  362. onDayClick={day => this.foundation.handleDayClick(day, panelType)}
  363. onDayHover={day => this.foundation.handleDayHover(day, panelType)}
  364. onWeeksRowNumChange={weeksRowNum => this.handleWeeksRowNumChange(weeksRowNum, panelType)}
  365. startDateOffset={startDateOffset}
  366. endDateOffset={endDateOffset}
  367. focusRecordsRef={this.props.focusRecordsRef}
  368. multiple={multiple}
  369. />
  370. </div>
  371. );
  372. }
  373. handleWeeksRowNumChange = (weeksRowNum: number, panelType: PanelType) => {
  374. const isLeft = panelType === strings.PANEL_TYPE_RIGHT;
  375. const isRight = panelType === strings.PANEL_TYPE_RIGHT;
  376. const allIsYearOrTime = this.leftIsYearOrTime() && this.rightIsYearOrTime();
  377. if (this.foundation.isRangeType() && !allIsYearOrTime) {
  378. const states = { weeksRowNum, currentPanelHeight: this.calcScrollListHeight() };
  379. this.setState(states, () => {
  380. if ((this.leftIsYearOrTime() && isRight) || (this.rightIsYearOrTime() && isLeft)) {
  381. this.reselect();
  382. }
  383. });
  384. }
  385. };
  386. reselect = () => {
  387. const refKeys = [
  388. `timepicker-${strings.PANEL_TYPE_LEFT}`,
  389. `timepicker-${strings.PANEL_TYPE_RIGHT}`,
  390. `yam-${strings.PANEL_TYPE_LEFT}`,
  391. `yam-${strings.PANEL_TYPE_RIGHT}`,
  392. ];
  393. refKeys.forEach(key => {
  394. const current = this.adapter.getCache(key);
  395. if (current && typeof current.reselect === 'function') {
  396. current.reselect();
  397. }
  398. });
  399. };
  400. getYAMOpenType = () => {
  401. return this.foundation.getYAMOpenType();
  402. }
  403. renderTimePicker(panelType: PanelType, panelDetail: MonthInfo) {
  404. const { type, locale, format, hideDisabledOptions, timePickerOpts, dateFnsLocale } = this.props;
  405. const { pickerDate } = panelDetail;
  406. const timePanelCls = classnames(`${prefixCls}-time`);
  407. const restProps = {
  408. ...timePickerOpts,
  409. hideDisabledOptions,
  410. };
  411. const disabledOptions = this.foundation.calcDisabledTime(panelType);
  412. if (disabledOptions) {
  413. ['disabledHours', 'disabledMinutes', 'disabledSeconds'].forEach(key => {
  414. if (disabledOptions[key]) {
  415. restProps[key] = disabledOptions[key];
  416. }
  417. });
  418. }
  419. const { rangeStart, rangeEnd } = this.state;
  420. const dateFormat = this.foundation.getValidDateFormat();
  421. let startDate,
  422. endDate;
  423. if (
  424. type === 'dateTimeRange' &&
  425. rangeStart &&
  426. rangeEnd &&
  427. isSameDay(
  428. (startDate = compatibleParse(rangeStart, dateFormat, undefined, dateFnsLocale)),
  429. (endDate = compatibleParse(rangeEnd, dateFormat, undefined, dateFnsLocale))
  430. )
  431. ) {
  432. if (panelType === strings.PANEL_TYPE_RIGHT) {
  433. rangeStart && (restProps.startDate = startDate);
  434. } else {
  435. rangeEnd && (restProps.endDate = endDate);
  436. }
  437. }
  438. // i18n placeholder
  439. const placeholder = locale.selectTime;
  440. return (
  441. <div className={timePanelCls}>
  442. <Combobox
  443. ref={current => this.cacheRefCurrent(`timepicker-${panelType}`, current)}
  444. panelHeader={placeholder}
  445. format={format || strings.FORMAT_TIME_PICKER}
  446. timeStampValue={pickerDate}
  447. onChange={(newTime: { timeStampValue: number }) => this.foundation.handleTimeChange(newTime, panelType)}
  448. {...restProps}
  449. />
  450. </div>
  451. );
  452. }
  453. renderYearAndMonth(panelType: PanelType, panelDetail: MonthInfo) {
  454. const { pickerDate } = panelDetail;
  455. const { locale, localeCode, density } = this.props;
  456. const y = pickerDate.getFullYear();
  457. const m = pickerDate.getMonth() + 1;
  458. return (
  459. <YearAndMonth
  460. ref={current => this.cacheRefCurrent(`yam-${panelType}`, current)}
  461. locale={locale}
  462. localeCode={localeCode}
  463. currentYear={y}
  464. currentMonth={m}
  465. onSelect={item =>
  466. this.foundation.toYearMonth(panelType, new Date(item.currentYear, item.currentMonth - 1))
  467. }
  468. onBackToMain={() => {
  469. this.foundation.showDatePanel(panelType);
  470. const wrapCurrent = this.adapter.getCache(`wrap-${panelType}`);
  471. if (wrapCurrent) {
  472. wrapCurrent.style.height = 'auto';
  473. }
  474. }}
  475. density={density}
  476. />
  477. );
  478. }
  479. renderSwitch(panelType: PanelType) {
  480. const { rangeStart, rangeEnd, monthLeft, monthRight } = this.state;
  481. const { type, locale, disabledTimePicker, density, dateFnsLocale, insetInput } = this.props;
  482. // Type: date, dateRange, year, month, inset input no rendering required
  483. if (!type.includes('Time') || insetInput) {
  484. return null;
  485. }
  486. // switch year/month & time
  487. let panelDetail,
  488. dateText;
  489. // i18n
  490. const { FORMAT_SWITCH_DATE } = locale.localeFormatToken;
  491. // Timepicker format is constant and does not change with language
  492. // const FORMAT_TIME_PICKER = strings.FORMAT_TIME_PICKER;
  493. const formatTimePicker = this.foundation.getValidTimeFormat();
  494. const dateFormat = this.foundation.getValidDateFormat();
  495. if (panelType === strings.PANEL_TYPE_LEFT) {
  496. panelDetail = monthLeft;
  497. dateText = rangeStart ? formatFn(compatibleParse(rangeStart, dateFormat, undefined, dateFnsLocale), FORMAT_SWITCH_DATE) : '';
  498. } else {
  499. panelDetail = monthRight;
  500. dateText = rangeEnd ? formatFn(compatibleParse(rangeEnd, dateFormat, undefined, dateFnsLocale), FORMAT_SWITCH_DATE) : '';
  501. }
  502. const { isTimePickerOpen, showDate } = panelDetail;
  503. const monthText = showDate ? formatFn(showDate, FORMAT_SWITCH_DATE) : '';
  504. const timeText = showDate ? formatFn(showDate, formatTimePicker) : '';
  505. const showSwitchIcon = ['default'].includes(density);
  506. const switchCls = classnames(`${prefixCls}-switch`);
  507. const dateCls = classnames({
  508. [`${prefixCls }-switch-date`]: true,
  509. [`${prefixCls }-switch-date-active`]: !isTimePickerOpen,
  510. });
  511. const timeCls = classnames({
  512. [`${prefixCls }-switch-time`]: true,
  513. [`${prefixCls}-switch-time-disabled`]: disabledTimePicker,
  514. [`${prefixCls }-switch-date-active`]: isTimePickerOpen,
  515. });
  516. const textCls = classnames(`${prefixCls}-switch-text`);
  517. return (
  518. <div className={switchCls} ref={current => this.adapter.setCache(`switch-${panelType}`, current)}>
  519. <div
  520. role="button"
  521. aria-label="Switch to date panel"
  522. className={dateCls}
  523. onClick={e => this.foundation.showDatePanel(panelType)}
  524. >
  525. {showSwitchIcon && <IconCalendar aria-hidden />}
  526. <span className={textCls}>{dateText || monthText}</span>
  527. </div>
  528. <div
  529. role="button"
  530. aria-label="Switch to time panel"
  531. className={timeCls}
  532. onClick={e => this.foundation.showTimePicker(panelType, true)}
  533. >
  534. {showSwitchIcon && <IconClock aria-hidden />}
  535. <span className={textCls}>{timeText}</span>
  536. </div>
  537. </div>
  538. );
  539. }
  540. render() {
  541. const { monthLeft, monthRight } = this.state;
  542. const { type, insetInput, presetPosition, renderQuickControls, renderDateInput } = this.props;
  543. const monthGridCls = classnames({
  544. [`${prefixCls }-month-grid`]: true,
  545. });
  546. const panelTypeLeft = strings.PANEL_TYPE_LEFT;
  547. const panelTypeRight = strings.PANEL_TYPE_RIGHT;
  548. let content = null;
  549. if (type === 'date' || type === 'dateTime') {
  550. content = this.renderPanel(monthLeft.pickerDate, panelTypeLeft);
  551. } else if (type === 'dateRange' || type === 'dateTimeRange') {
  552. content = [
  553. this.renderPanel(monthLeft.pickerDate, panelTypeLeft),
  554. this.renderPanel(monthRight.pickerDate, panelTypeRight),
  555. ];
  556. } else if (type === 'year' || type === 'month') {
  557. content = 'year month';
  558. }
  559. const yearOpenType = this.getYAMOpenType();
  560. return (
  561. <div style={{ display: 'flex' }}>
  562. {presetPosition === "left" && renderQuickControls}
  563. <div>
  564. {renderDateInput}
  565. <div
  566. className={monthGridCls}
  567. x-type={type}
  568. x-panel-yearandmonth-open-type={yearOpenType}
  569. // FIXME:
  570. x-insetinput={insetInput ? "true" : "false"}
  571. x-preset-position={renderQuickControls === null ? 'null' : presetPosition}
  572. ref={current => this.cacheRefCurrent('monthGrid', current)}
  573. >
  574. {content}
  575. </div>
  576. </div>
  577. {presetPosition === "right" && renderQuickControls}
  578. </div>
  579. );
  580. }
  581. }