TimePicker.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. /* eslint-disable no-unused-vars */
  2. import React from 'react';
  3. import PropTypes from 'prop-types';
  4. import classNames from 'classnames';
  5. import { noop, get } from 'lodash';
  6. import ConfigContext from '../configProvider/context';
  7. import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
  8. import { strings, cssClasses } from '@douyinfe/semi-foundation/timePicker/constants';
  9. import Popover from '../popover';
  10. import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  11. import TimePickerFoundation, { TimePickerAdapter } from '@douyinfe/semi-foundation/timePicker/foundation';
  12. import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
  13. import Combobox from './Combobox';
  14. import TimeInput from './TimeInput';
  15. import { PanelShape, PanelShapeDefaults } from './PanelShape';
  16. import { TimeShape } from './TimeShape';
  17. import '@douyinfe/semi-foundation/timePicker/timePicker.scss';
  18. import Trigger from '../trigger';
  19. import { InputSize } from '../input';
  20. import { Position } from '../tooltip';
  21. import { ScrollItemProps } from '../scrollList/scrollItem';
  22. import { Locale } from '../locale/interface';
  23. export interface Panel {
  24. panelHeader?: React.ReactNode;
  25. panelFooter?: React.ReactNode
  26. }
  27. export type BaseValueType = string | number | Date;
  28. export type Type = 'time' | 'timeRange';
  29. export type TimePickerProps = {
  30. 'aria-describedby'?: React.AriaAttributes['aria-describedby'];
  31. 'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
  32. 'aria-invalid'?: React.AriaAttributes['aria-invalid'];
  33. 'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
  34. 'aria-required'?: React.AriaAttributes['aria-required'];
  35. autoAdjustOverflow?: boolean;
  36. autoFocus?: boolean; // TODO: autoFocus did not take effect
  37. className?: string;
  38. clearText?: string;
  39. dateFnsLocale?: Locale['dateFnsLocale'];
  40. defaultOpen?: boolean;
  41. defaultValue?: BaseValueType | BaseValueType[];
  42. disabled?: boolean;
  43. disabledHours?: () => number[];
  44. disabledMinutes?: (selectedHour: number) => number[];
  45. disabledSeconds?: (selectedHour: number, selectedMinute: number) => number[];
  46. focusOnOpen?: boolean;
  47. format?: string;
  48. getPopupContainer?: () => HTMLElement;
  49. hideDisabledOptions?: boolean;
  50. hourStep?: number;
  51. id?: string;
  52. inputReadOnly?: boolean;
  53. inputStyle?: React.CSSProperties;
  54. insetLabel?: React.ReactNode;
  55. insetLabelId?: string;
  56. locale?: Locale['TimePicker'];
  57. localeCode?: string;
  58. minuteStep?: number;
  59. motion?: boolean;
  60. open?: boolean;
  61. panelFooter?: React.ReactNode;
  62. panelHeader?: React.ReactNode;
  63. panels?: Panel[]; // FIXME:
  64. placeholder?: string;
  65. popupClassName?: string;
  66. popupStyle?: React.CSSProperties;
  67. position?: Position;
  68. prefixCls?: string;
  69. preventScroll?: boolean;
  70. rangeSeparator?: string;
  71. scrollItemProps?: ScrollItemProps<any>;
  72. secondStep?: number;
  73. showClear?: boolean;
  74. size?: InputSize;
  75. style?: React.CSSProperties;
  76. timeZone?: string | number;
  77. triggerRender?: (props?: any) => React.ReactNode;
  78. type?: Type;
  79. use12Hours?: boolean;
  80. validateStatus?: ValidateStatus;
  81. value?: BaseValueType | BaseValueType[];
  82. zIndex?: number | string;
  83. onBlur?: React.FocusEventHandler<HTMLInputElement>;
  84. onChange?: TimePickerAdapter['notifyChange'];
  85. onChangeWithDateFirst?: boolean;
  86. onFocus?: React.FocusEventHandler<HTMLInputElement>;
  87. onOpenChange?: (open: boolean) => void
  88. };
  89. export interface TimePickerState {
  90. open: boolean;
  91. value: Date[];
  92. inputValue: string;
  93. currentSelectPanel: string | number;
  94. isAM: [boolean, boolean];
  95. showHour: boolean;
  96. showMinute: boolean;
  97. showSecond: boolean;
  98. invalid: boolean
  99. }
  100. export default class TimePicker extends BaseComponent<TimePickerProps, TimePickerState> {
  101. static contextType = ConfigContext;
  102. static propTypes = {
  103. 'aria-labelledby': PropTypes.string,
  104. 'aria-invalid': PropTypes.bool,
  105. 'aria-errormessage': PropTypes.string,
  106. 'aria-describedby': PropTypes.string,
  107. 'aria-required': PropTypes.bool,
  108. prefixCls: PropTypes.string,
  109. clearText: PropTypes.string,
  110. value: TimeShape,
  111. inputReadOnly: PropTypes.bool,
  112. disabled: PropTypes.bool,
  113. showClear: PropTypes.bool,
  114. defaultValue: TimeShape,
  115. open: PropTypes.bool,
  116. defaultOpen: PropTypes.bool,
  117. onOpenChange: PropTypes.func,
  118. position: PropTypes.any,
  119. getPopupContainer: PropTypes.func,
  120. placeholder: PropTypes.string,
  121. format: PropTypes.string,
  122. style: PropTypes.object,
  123. className: PropTypes.string,
  124. popupClassName: PropTypes.string,
  125. popupStyle: PropTypes.object,
  126. disabledHours: PropTypes.func,
  127. disabledMinutes: PropTypes.func,
  128. disabledSeconds: PropTypes.func,
  129. hideDisabledOptions: PropTypes.bool,
  130. onChange: PropTypes.func,
  131. use12Hours: PropTypes.bool,
  132. hourStep: PropTypes.number,
  133. minuteStep: PropTypes.number,
  134. secondStep: PropTypes.number,
  135. focusOnOpen: PropTypes.bool,
  136. autoFocus: PropTypes.bool,
  137. size: PropTypes.oneOf(strings.SIZE),
  138. panels: PropTypes.arrayOf(PropTypes.shape(PanelShape)),
  139. onFocus: PropTypes.func,
  140. onBlur: PropTypes.func,
  141. locale: PropTypes.object,
  142. localeCode: PropTypes.string,
  143. dateFnsLocale: PropTypes.object,
  144. zIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  145. insetLabel: PropTypes.node,
  146. insetLabelId: PropTypes.string,
  147. validateStatus: PropTypes.oneOf(strings.STATUS),
  148. type: PropTypes.oneOf<TimePickerProps['type']>(strings.TYPES),
  149. rangeSeparator: PropTypes.string,
  150. triggerRender: PropTypes.func,
  151. timeZone: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  152. scrollItemProps: PropTypes.object,
  153. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  154. autoAdjustOverflow: PropTypes.bool,
  155. ...PanelShape,
  156. inputStyle: PropTypes.object,
  157. preventScroll: PropTypes.bool,
  158. };
  159. static defaultProps = {
  160. autoAdjustOverflow: true,
  161. getPopupContainer: () => document.body,
  162. showClear: true,
  163. zIndex: popoverNumbers.DEFAULT_Z_INDEX,
  164. rangeSeparator: strings.DEFAULT_RANGE_SEPARATOR,
  165. onOpenChange: noop,
  166. clearText: 'clear',
  167. prefixCls: cssClasses.PREFIX,
  168. inputReadOnly: false,
  169. style: {},
  170. className: '',
  171. popupClassName: '',
  172. popupStyle: { left: '0px', top: '0px' },
  173. disabledHours: () => [] as number[],
  174. disabledMinutes: () => [] as number[],
  175. disabledSeconds: () => [] as number[],
  176. hideDisabledOptions: false,
  177. // position: 'bottomLeft',
  178. onFocus: noop,
  179. onBlur: noop,
  180. onChange: noop,
  181. onChangeWithDateFirst: true,
  182. use12Hours: false,
  183. focusOnOpen: false,
  184. onKeyDown: noop,
  185. size: 'default' as const,
  186. type: strings.DEFAULT_TYPE,
  187. motion: true,
  188. ...PanelShapeDefaults,
  189. // format: strings.DEFAULT_FORMAT,
  190. // open and value controlled
  191. };
  192. foundation: TimePickerFoundation;
  193. timePickerRef: React.MutableRefObject<HTMLDivElement>;
  194. savePanelRef: React.RefObject<HTMLDivElement>;
  195. clickOutSideHandler: (e: MouseEvent) => void;
  196. constructor(props: TimePickerProps) {
  197. super(props);
  198. const { format = strings.DEFAULT_FORMAT } = props;
  199. this.state = {
  200. open: props.open || props.defaultOpen || false,
  201. value: [], // Date[]
  202. inputValue: '', // time string
  203. currentSelectPanel: 0,
  204. isAM: [true, false],
  205. showHour: Boolean(format.match(/HH|hh|H|h/g)),
  206. showMinute: Boolean(format.match(/mm/g)),
  207. showSecond: Boolean(format.match(/ss/g)),
  208. invalid: undefined
  209. };
  210. this.foundation = new TimePickerFoundation(this.adapter);
  211. this.timePickerRef = React.createRef();
  212. this.savePanelRef = React.createRef();
  213. }
  214. get adapter(): TimePickerAdapter<TimePickerProps, TimePickerState> {
  215. return {
  216. ...super.adapter,
  217. togglePanel: show => {
  218. this.setState({ open: show });
  219. },
  220. registerClickOutSide: () => {
  221. if (this.clickOutSideHandler) {
  222. this.adapter.unregisterClickOutSide();
  223. }
  224. this.clickOutSideHandler = e => {
  225. const panel = this.savePanelRef && this.savePanelRef.current;
  226. const isInPanel = e.target && panel && panel.contains(e.target as Node);
  227. const isInTimepicker =
  228. this.timePickerRef &&
  229. this.timePickerRef.current &&
  230. this.timePickerRef.current.contains(e.target as Node);
  231. if (!isInTimepicker && !isInPanel) {
  232. const clickedOutside = true;
  233. this.foundation.handlePanelClose(clickedOutside, e);
  234. }
  235. };
  236. document.addEventListener('mousedown', this.clickOutSideHandler);
  237. },
  238. setInputValue: (inputValue, cb) => this.setState({ inputValue }, cb),
  239. unregisterClickOutSide: () => {
  240. if (this.clickOutSideHandler) {
  241. document.removeEventListener('mousedown', this.clickOutSideHandler);
  242. this.clickOutSideHandler = null;
  243. }
  244. },
  245. notifyOpenChange: (...args) => this.props.onOpenChange(...args),
  246. notifyChange: (agr1, arg2) => this.props.onChange && this.props.onChange(agr1, arg2),
  247. notifyFocus: (...args) => this.props.onFocus && this.props.onFocus(...args),
  248. notifyBlur: (...args) => this.props.onBlur && this.props.onBlur(...args),
  249. isRangePicker: () => this.props.type === strings.TYPE_TIME_RANGE_PICKER,
  250. };
  251. }
  252. static getDerivedStateFromProps(nextProps: TimePickerProps, prevState: TimePickerState) {
  253. if ('open' in nextProps && nextProps.open !== prevState.open) {
  254. return {
  255. open: nextProps.open,
  256. };
  257. }
  258. return null;
  259. }
  260. componentDidUpdate(prevProps: TimePickerProps) {
  261. // if (this.isControlled('open') && this.props.open != null && this.props.open !== prevProps.open) {
  262. // this.foundation.setPanel(this.props.open);
  263. // }
  264. if (this.isControlled('value') && this.props.value !== prevProps.value) {
  265. this.foundation.refreshProps({
  266. ...this.props,
  267. });
  268. } else if (this.props.timeZone !== prevProps.timeZone) {
  269. this.foundation.refreshProps({
  270. timeZone: this.props.timeZone,
  271. __prevTimeZone: prevProps.timeZone,
  272. value: this.state.value,
  273. });
  274. }
  275. }
  276. onCurrentSelectPanelChange = (currentSelectPanel: string) => {
  277. this.setState({ currentSelectPanel });
  278. };
  279. handlePanelChange = (
  280. value: { isAM: boolean; value: string; timeStampValue: number },
  281. index: number
  282. ) => this.foundation.handlePanelChange(value, index);
  283. handleInput = (value: string) => this.foundation.handleInputChange(value);
  284. createPanelProps = (index = 0) => {
  285. const { panels, panelFooter, panelHeader, locale } = this.props;
  286. const panelProps = {
  287. panelHeader,
  288. panelFooter,
  289. };
  290. if (this.adapter.isRangePicker()) {
  291. const defaultHeaderMap = {
  292. 0: locale.begin,
  293. 1: locale.end,
  294. };
  295. panelProps.panelHeader = get(
  296. panels,
  297. index,
  298. isNullOrUndefined(panelHeader) ? get(defaultHeaderMap, index, null) : panelHeader
  299. );
  300. panelProps.panelFooter = get(panels, index, panelFooter) as React.ReactNode;
  301. }
  302. return panelProps;
  303. };
  304. getPanelElement() {
  305. const { prefixCls, type } = this.props;
  306. const { isAM, value } = this.state;
  307. const format = this.foundation.getDefaultFormatIfNeed();
  308. const timePanels = [
  309. <Combobox
  310. {...this.props}
  311. key={0}
  312. format={format}
  313. isAM={isAM[0]}
  314. timeStampValue={value[0]}
  315. prefixCls={`${prefixCls}-panel`}
  316. onChange={v => this.handlePanelChange(v, 0)}
  317. onCurrentSelectPanelChange={this.onCurrentSelectPanelChange}
  318. {...this.createPanelProps(0)}
  319. />,
  320. ];
  321. if (type === strings.TYPE_TIME_RANGE_PICKER) {
  322. timePanels.push(
  323. <Combobox
  324. {...this.props}
  325. key={1}
  326. format={format}
  327. isAM={isAM[1]}
  328. timeStampValue={value[1]}
  329. prefixCls={`${prefixCls}-panel`}
  330. onChange={v => this.handlePanelChange(v, 1)}
  331. onCurrentSelectPanelChange={this.onCurrentSelectPanelChange}
  332. {...this.createPanelProps(1)}
  333. />
  334. );
  335. }
  336. const wrapCls = classNames({
  337. [cssClasses.RANGE_PANEL_LISTS]: this.adapter.isRangePicker(),
  338. });
  339. return (
  340. <div ref={this.savePanelRef} className={wrapCls}>
  341. {timePanels.map(panel => panel)}
  342. </div>
  343. );
  344. }
  345. getPopupClassName() {
  346. const { use12Hours, prefixCls, popupClassName } = this.props;
  347. const { showHour, showMinute, showSecond } = this.state;
  348. let selectColumnCount = 0;
  349. if (showHour) {
  350. selectColumnCount += 1;
  351. }
  352. if (showMinute) {
  353. selectColumnCount += 1;
  354. }
  355. if (showSecond) {
  356. selectColumnCount += 1;
  357. }
  358. if (use12Hours) {
  359. selectColumnCount += 1;
  360. }
  361. return classNames(
  362. `${prefixCls}-panel`,
  363. popupClassName,
  364. {
  365. [`${prefixCls}-panel-narrow`]: (!showHour || !showMinute || !showSecond) && !use12Hours,
  366. [cssClasses.RANGE_PICKER]: this.adapter.isRangePicker(),
  367. },
  368. `${prefixCls}-panel-column-${selectColumnCount}`
  369. );
  370. }
  371. focus() {
  372. // TODO this.picker is undefined, confirm keep this func or not
  373. // this.picker.focus();
  374. }
  375. blur() {
  376. // TODO this.picker is undefined, confirm keep this func or not
  377. // this.picker.blur();
  378. }
  379. /* istanbul ignore next */
  380. handlePanelVisibleChange = (visible: boolean) => this.foundation.handleVisibleChange(visible);
  381. openPanel = () => {
  382. this.foundation.handlePanelOpen();
  383. };
  384. handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
  385. this.foundation.handleFocus(e);
  386. };
  387. handleBlur = (e: React.FocusEvent<HTMLInputElement>) => this.foundation.handleInputBlur(e);
  388. setTimePickerRef: React.LegacyRef<HTMLDivElement> = node => (this.timePickerRef.current = node);
  389. render() {
  390. const {
  391. prefixCls,
  392. placeholder,
  393. disabled,
  394. defaultValue,
  395. className,
  396. popupStyle,
  397. size,
  398. style,
  399. locale,
  400. localeCode,
  401. zIndex,
  402. getPopupContainer,
  403. insetLabel,
  404. insetLabelId,
  405. inputStyle,
  406. showClear,
  407. panelHeader,
  408. panelFooter,
  409. rangeSeparator,
  410. onOpenChange,
  411. onChangeWithDateFirst,
  412. popupClassName: propPopupClassName,
  413. hideDisabledOptions,
  414. use12Hours,
  415. minuteStep,
  416. hourStep,
  417. secondStep,
  418. scrollItemProps,
  419. triggerRender,
  420. motion,
  421. autoAdjustOverflow,
  422. ...rest
  423. } = this.props;
  424. const format = this.foundation.getDefaultFormatIfNeed();
  425. const position = this.foundation.getPosition();
  426. const useCustomTrigger = typeof triggerRender === 'function';
  427. const { open, inputValue, invalid, value } = this.state;
  428. const popupClassName = this.getPopupClassName();
  429. const headerPrefix = classNames({
  430. [`${prefixCls}-header`]: true,
  431. });
  432. const panelPrefix = classNames({
  433. [`${prefixCls}-panel`]: true,
  434. [`${prefixCls}-panel-${size}`]: size,
  435. });
  436. const inputProps = {
  437. ...rest,
  438. disabled,
  439. prefixCls,
  440. size,
  441. showClear: disabled ? false : showClear,
  442. style: inputStyle,
  443. value: inputValue,
  444. onFocus: this.handleFocus,
  445. insetLabel,
  446. insetLabelId,
  447. format,
  448. locale,
  449. localeCode,
  450. invalid,
  451. placeholder,
  452. onChange: this.handleInput,
  453. onBlur: this.handleBlur,
  454. };
  455. const outerProps = {} as { onClick: () => void };
  456. if (useCustomTrigger) {
  457. outerProps.onClick = this.openPanel;
  458. }
  459. console.log("===>", disabled?false:open, motion);
  460. return (
  461. <div
  462. ref={this.setTimePickerRef}
  463. className={classNames({ [prefixCls]: true }, className)}
  464. style={style}
  465. {...outerProps}
  466. >
  467. <Popover
  468. getPopupContainer={getPopupContainer}
  469. zIndex={zIndex as number}
  470. prefixCls={panelPrefix}
  471. contentClassName={popupClassName}
  472. style={popupStyle}
  473. content={this.getPanelElement()}
  474. trigger={'custom'}
  475. position={position}
  476. visible={disabled ? false : Boolean(open)}
  477. motion={motion}
  478. autoAdjustOverflow={autoAdjustOverflow}
  479. >
  480. {useCustomTrigger ? (
  481. <Trigger
  482. triggerRender={triggerRender}
  483. disabled={disabled}
  484. value={value}
  485. inputValue={inputValue}
  486. onChange={this.handleInput}
  487. placeholder={placeholder}
  488. componentName={'TimePicker'}
  489. componentProps={{ ...this.props }}
  490. />
  491. ) : (
  492. <span className={headerPrefix}>
  493. <TimeInput {...inputProps} />
  494. </span>
  495. )}
  496. </Popover>
  497. </div>
  498. );
  499. }
  500. }