yearAndMonth.tsx 11 KB

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