yearAndMonth.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /* eslint-disable max-len */
  2. import React from 'react';
  3. import PropTypes from 'prop-types';
  4. import YearAndMonthFoundation, { MonthScrollItem, YearAndMonthAdapter, YearAndMonthFoundationProps, YearAndMonthFoundationState, YearScrollItem } from '@douyinfe/semi-foundation/datePicker/yearAndMonthFoundation';
  5. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  6. import ScrollList from '../scrollList/index';
  7. import ScrollItem from '../scrollList/scrollItem';
  8. import { getYears } from '@douyinfe/semi-foundation/datePicker/_utils/index';
  9. import IconButton from '../iconButton';
  10. import { IconChevronLeft } from '@douyinfe/semi-icons';
  11. import { BASE_CLASS_PREFIX } from '@douyinfe/semi-foundation/base/constants';
  12. import { noop, stubFalse, isEqual } from 'lodash';
  13. import { setYear, setMonth, set } from 'date-fns';
  14. import { Locale } from '../locale/interface';
  15. import { strings } from '@douyinfe/semi-foundation/datePicker/constants';
  16. import { PanelType } from '@douyinfe/semi-foundation/datePicker/monthsGridFoundation';
  17. const prefixCls = `${BASE_CLASS_PREFIX}-datepicker`;
  18. export interface YearAndMonthProps extends YearAndMonthFoundationProps, BaseProps {
  19. locale?: Locale['DatePicker']
  20. }
  21. export type YearAndMonthState = YearAndMonthFoundationState;
  22. class YearAndMonth extends BaseComponent<YearAndMonthProps, YearAndMonthState> {
  23. static propTypes = {
  24. currentYear: PropTypes.object,
  25. currentMonth: PropTypes.object,
  26. onSelect: PropTypes.func,
  27. locale: PropTypes.object,
  28. localeCode: PropTypes.string,
  29. monthCycled: PropTypes.bool,
  30. yearCycled: PropTypes.bool,
  31. noBackBtn: PropTypes.bool,
  32. disabledDate: PropTypes.func,
  33. density: PropTypes.string,
  34. presetPosition: PropTypes.oneOf(strings.PRESET_POSITION_SET),
  35. renderQuickControls: PropTypes.node,
  36. renderDateInput: PropTypes.node,
  37. type: PropTypes.oneOf(strings.TYPE_SET),
  38. };
  39. static defaultProps = {
  40. disabledDate: stubFalse,
  41. monthCycled: false,
  42. yearCycled: false,
  43. noBackBtn: false,
  44. onSelect: noop,
  45. type: 'month',
  46. };
  47. foundation: YearAndMonthFoundation;
  48. yearRef: React.RefObject<ScrollItem<YearScrollItem>>;
  49. monthRef: React.RefObject<ScrollItem<MonthScrollItem>>;
  50. constructor(props: YearAndMonthProps) {
  51. super(props);
  52. const now = new Date();
  53. let { currentYear, currentMonth } = props;
  54. const currentLeftYear = currentYear.left || now.getFullYear();
  55. const currentLeftMonth = currentMonth.left || now.getMonth() + 1;
  56. currentYear = { left: currentLeftYear, right: currentLeftYear };
  57. currentMonth = { left: currentLeftMonth, right: currentMonth.right || currentLeftMonth + 1 };
  58. this.state = {
  59. years: getYears().map(year => ({
  60. value: year,
  61. year,
  62. })),
  63. months: Array(12)
  64. .fill(0)
  65. .map((v, idx) => ({
  66. value: idx + 1,
  67. month: idx + 1,
  68. })),
  69. currentYear,
  70. currentMonth,
  71. };
  72. this.yearRef = React.createRef();
  73. this.monthRef = React.createRef();
  74. this.foundation = new YearAndMonthFoundation(this.adapter);
  75. }
  76. get adapter(): YearAndMonthAdapter {
  77. return {
  78. ...super.adapter,
  79. // updateYears: years => this.setState({ years }),
  80. // updateMonths: months => this.setState({ months }),
  81. setCurrentYear: (currentYear, cb) => this.setState({ currentYear }, cb),
  82. setCurrentMonth: currentMonth => this.setState({ currentMonth }),
  83. setCurrentYearAndMonth: (currentYear, currentMonth) => this.setState({ currentYear, currentMonth }),
  84. notifySelectYear: (year) =>
  85. this.props.onSelect({
  86. currentMonth: this.state.currentMonth,
  87. currentYear: year,
  88. }),
  89. notifySelectMonth: (month) =>
  90. this.props.onSelect({
  91. currentYear: this.state.currentYear,
  92. currentMonth: month,
  93. }),
  94. notifySelectYearAndMonth: (year, month) =>
  95. this.props.onSelect({
  96. currentYear: year,
  97. currentMonth: month,
  98. }),
  99. notifyBackToMain: () => this.props.onBackToMain(),
  100. };
  101. }
  102. static getDerivedStateFromProps(props: YearAndMonthProps, state: YearAndMonthState) {
  103. const willUpdateStates: Partial<YearAndMonthState> = {};
  104. if (!isEqual(props.currentYear, state.currentYear) && props.currentYear.left !== 0) {
  105. willUpdateStates.currentYear = props.currentYear;
  106. }
  107. if (!isEqual(props.currentMonth, state.currentMonth) && props.currentMonth.left !== 0) {
  108. willUpdateStates.currentMonth = props.currentMonth;
  109. }
  110. return willUpdateStates;
  111. }
  112. renderColYear(panelType: PanelType) {
  113. const { years, currentYear, currentMonth, months } = this.state;
  114. const { disabledDate, localeCode, yearCycled, yearAndMonthOpts } = this.props;
  115. const currentDate = setMonth(Date.now(), currentMonth[panelType] - 1);
  116. const left = strings.PANEL_TYPE_LEFT;
  117. const right = strings.PANEL_TYPE_RIGHT;
  118. const needDisabled = (year) => {
  119. if (panelType === right && currentYear[left]) {
  120. if (currentMonth[left] <= currentMonth[right]) {
  121. return currentYear[left] > year;
  122. } else {
  123. return currentYear[left] >= year;
  124. }
  125. }
  126. return false;
  127. };
  128. const list: any[] = years.map(({ value, year }) => {
  129. const isAllMonthDisabled = months.every(({ month }) => {
  130. return disabledDate(set(currentDate, { year, month: month - 1 }));
  131. });
  132. const isRightPanelDisabled = needDisabled(year);
  133. return ({
  134. year,
  135. value, // Actual rendered text
  136. disabled: isAllMonthDisabled || isRightPanelDisabled,
  137. });
  138. });
  139. let transform = (val: string) => val;
  140. if (localeCode === 'zh-CN' || localeCode === 'zh-TW') {
  141. // Only Chinese needs to add [year] after the selected year
  142. transform = val => `${val}年`;
  143. }
  144. return (
  145. <ScrollItem
  146. ref={this.yearRef}
  147. cycled={yearCycled}
  148. list={list}
  149. transform={transform}
  150. selectedIndex={years.findIndex(item => item.value === currentYear[panelType])}
  151. type="year"
  152. onSelect={item => this.selectYear(item as YearScrollItem, panelType)}
  153. mode="normal"
  154. {...yearAndMonthOpts}
  155. />
  156. );
  157. }
  158. selectYear = (item: YearScrollItem, panelType?: PanelType) => {
  159. this.foundation.selectYear(item, panelType);
  160. };
  161. selectMonth = (item: MonthScrollItem, panelType?: PanelType) => {
  162. this.foundation.selectMonth(item, panelType);
  163. };
  164. reselect = () => {
  165. const refKeys = ['yearRef', 'monthRef'];
  166. refKeys.forEach(key => {
  167. const ref = this[key];
  168. if (ref && ref.current && ref.current.scrollToIndex) {
  169. ref.current.scrollToIndex();
  170. }
  171. });
  172. };
  173. renderColMonth(panelType: PanelType) {
  174. const { months, currentMonth, currentYear } = this.state;
  175. const { locale, localeCode, monthCycled, disabledDate, yearAndMonthOpts } = this.props;
  176. let transform = (val: string) => val;
  177. const currentDate = setYear(Date.now(), currentYear[panelType]);
  178. const left = strings.PANEL_TYPE_LEFT;
  179. const right = strings.PANEL_TYPE_RIGHT;
  180. if (localeCode === 'zh-CN' || localeCode === 'zh-TW') {
  181. // Only Chinese needs to add [month] after the selected month
  182. transform = val => `${val}月`;
  183. }
  184. // i18n
  185. const list: MonthScrollItem[] = months.map(({ value, month }) => {
  186. const isRightPanelDisabled = panelType === right && currentMonth[left] && currentYear[left] === currentYear[right] && currentMonth[left] > month;
  187. return ({
  188. month,
  189. disabled: disabledDate(setMonth(currentDate, month - 1)) || isRightPanelDisabled,
  190. value: locale.fullMonths[value], // Actual rendered text
  191. });
  192. });
  193. const selectedIndex = list.findIndex(item => item.month === currentMonth[panelType]);
  194. return (
  195. <ScrollItem
  196. ref={this.monthRef}
  197. cycled={monthCycled}
  198. list={list}
  199. transform={transform}
  200. selectedIndex={selectedIndex}
  201. type="month"
  202. onSelect={item => this.selectMonth(item as MonthScrollItem, panelType)}
  203. mode='normal'
  204. {...yearAndMonthOpts}
  205. />
  206. );
  207. }
  208. backToMain: React.MouseEventHandler<HTMLButtonElement> = e => {
  209. e.nativeEvent.stopImmediatePropagation();
  210. this.foundation.backToMain();
  211. };
  212. renderPanel(panelType: PanelType) {
  213. return (
  214. <>
  215. <ScrollList>
  216. {this.renderColYear(panelType)}
  217. {this.renderColMonth(panelType)}
  218. </ScrollList>
  219. </>
  220. );
  221. }
  222. render() {
  223. const { locale, noBackBtn, density, presetPosition, renderQuickControls, renderDateInput, type } = this.props;
  224. const prefix = `${prefixCls}-yearmonth-header`;
  225. const bodyCls = `${prefixCls}-yearmonth-body`;
  226. // i18n
  227. const selectDateText = locale.selectDate;
  228. const iconSize = density === 'compact' ? 'default' : 'large';
  229. const buttonSize = density === 'compact' ? 'small' : 'default';
  230. const panelTypeLeft = strings.PANEL_TYPE_LEFT;
  231. const panelTypeRight = strings.PANEL_TYPE_RIGHT;
  232. let content = null;
  233. if (type === 'month') {
  234. content = this.renderPanel(panelTypeLeft);
  235. } else {
  236. content = (
  237. <div className={bodyCls}>
  238. {this.renderPanel(panelTypeLeft)}
  239. {this.renderPanel(panelTypeRight)}
  240. </div>
  241. );
  242. }
  243. return (
  244. <React.Fragment>
  245. {noBackBtn ? null : (
  246. <div className={prefix}>
  247. <IconButton
  248. noHorizontalPadding={false}
  249. icon={<IconChevronLeft aria-hidden size={iconSize} />}
  250. size={buttonSize}
  251. onClick={this.backToMain}
  252. >
  253. <span>{selectDateText}</span>
  254. </IconButton>
  255. </div>
  256. )}
  257. {
  258. presetPosition ? (
  259. <div style={{ display: 'flex' }}>
  260. {/* todo: monthRange does not support presetPosition temporarily */}
  261. {presetPosition === "left" && type !== 'monthRange' && renderQuickControls}
  262. <div>
  263. {renderDateInput}
  264. {content}
  265. </div>
  266. {/* todo: monthRange does not support presetPosition temporarily */}
  267. {presetPosition === "right" && type !== 'monthRange' && renderQuickControls}
  268. </div>
  269. ) :
  270. <>
  271. {renderDateInput}
  272. {content}
  273. </>
  274. }
  275. </React.Fragment>
  276. );
  277. }
  278. }
  279. export default YearAndMonth;