You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DateInput.tsx 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2022 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import classNames from 'classnames';
  21. import { addMonths, setMonth, setYear, subMonths } from 'date-fns';
  22. import { range } from 'lodash';
  23. import * as React from 'react';
  24. import DayPicker, { DayModifiers, Modifier, Modifiers } from 'react-day-picker';
  25. import { injectIntl, WrappedComponentProps } from 'react-intl';
  26. import { ButtonIcon, ClearButton } from '../../components/controls/buttons';
  27. import OutsideClickHandler from '../../components/controls/OutsideClickHandler';
  28. import CalendarIcon from '../../components/icons/CalendarIcon';
  29. import ChevronLeftIcon from '../../components/icons/ChevronLeftIcon';
  30. import ChevronRightIcon from '../../components/icons/ChevronRightIcon';
  31. import {
  32. getShortMonthName,
  33. getShortWeekDayName,
  34. getWeekDayName,
  35. translate
  36. } from '../../helpers/l10n';
  37. import './DayPicker.css';
  38. import Select from './Select';
  39. import './styles.css';
  40. interface Props {
  41. className?: string;
  42. currentMonth?: Date;
  43. highlightFrom?: Date;
  44. highlightTo?: Date;
  45. inputClassName?: string;
  46. maxDate?: Date;
  47. minDate?: Date;
  48. name?: string;
  49. onChange: (date: Date | undefined) => void;
  50. placeholder: string;
  51. value?: Date;
  52. }
  53. interface State {
  54. currentMonth: Date;
  55. open: boolean;
  56. lastHovered?: Date;
  57. }
  58. type Week = [string, string, string, string, string, string, string];
  59. export default class DateInput extends React.PureComponent<Props, State> {
  60. input?: HTMLInputElement | null;
  61. constructor(props: Props) {
  62. super(props);
  63. this.state = { currentMonth: props.value || props.currentMonth || new Date(), open: false };
  64. }
  65. focus = () => {
  66. if (this.input) {
  67. this.input.focus();
  68. }
  69. this.openCalendar();
  70. };
  71. handleResetClick = () => {
  72. this.closeCalendar();
  73. this.props.onChange(undefined);
  74. };
  75. openCalendar = () => {
  76. this.setState({
  77. currentMonth: this.props.value || this.props.currentMonth || new Date(),
  78. lastHovered: undefined,
  79. open: true
  80. });
  81. };
  82. closeCalendar = () => {
  83. this.setState({ open: false });
  84. };
  85. handleDayClick = (day: Date, modifiers: DayModifiers) => {
  86. if (!modifiers.disabled) {
  87. this.closeCalendar();
  88. this.props.onChange(day);
  89. }
  90. };
  91. handleDayMouseEnter = (day: Date, modifiers: DayModifiers) => {
  92. this.setState({ lastHovered: modifiers.disabled ? undefined : day });
  93. };
  94. handleCurrentMonthChange = ({ value }: { value: number }) => {
  95. this.setState((state: State) => ({ currentMonth: setMonth(state.currentMonth, value) }));
  96. };
  97. handleCurrentYearChange = ({ value }: { value: number }) => {
  98. this.setState(state => ({ currentMonth: setYear(state.currentMonth, value) }));
  99. };
  100. handlePreviousMonthClick = () => {
  101. this.setState(state => ({ currentMonth: subMonths(state.currentMonth, 1) }));
  102. };
  103. handleNextMonthClick = () => {
  104. this.setState(state => ({ currentMonth: addMonths(state.currentMonth, 1) }));
  105. };
  106. render() {
  107. const {
  108. highlightFrom,
  109. highlightTo,
  110. minDate,
  111. value,
  112. name,
  113. className,
  114. inputClassName,
  115. placeholder
  116. } = this.props;
  117. const { lastHovered, currentMonth, open } = this.state;
  118. const after = this.props.maxDate || new Date();
  119. const months = range(12);
  120. const years = range(new Date().getFullYear() - 10, new Date().getFullYear() + 1);
  121. const selectedDays: Modifier[] = value ? [value] : [];
  122. let modifiers: Partial<Modifiers> | undefined;
  123. const lastHoveredOrValue = lastHovered || value;
  124. if (highlightFrom && lastHoveredOrValue) {
  125. modifiers = { highlighted: { from: highlightFrom, to: lastHoveredOrValue } };
  126. selectedDays.push(highlightFrom);
  127. }
  128. if (highlightTo && lastHoveredOrValue) {
  129. modifiers = { highlighted: { from: lastHoveredOrValue, to: highlightTo } };
  130. selectedDays.push(highlightTo);
  131. }
  132. const weekdaysLong = range(7).map(getWeekDayName) as Week;
  133. const weekdaysShort = range(7).map(getShortWeekDayName) as Week;
  134. const monthOptions = months.map(month => ({
  135. label: getShortMonthName(month),
  136. value: month
  137. }));
  138. const yearOptions = years.map(year => ({ label: String(year), value: year }));
  139. return (
  140. <OutsideClickHandler onClickOutside={this.closeCalendar}>
  141. <span className={classNames('date-input-control', className)}>
  142. <InputWrapper
  143. className={classNames('date-input-control-input', inputClassName, {
  144. 'is-filled': value !== undefined
  145. })}
  146. innerRef={(node: HTMLInputElement | null) => (this.input = node)}
  147. name={name}
  148. onFocus={this.openCalendar}
  149. placeholder={placeholder}
  150. readOnly={true}
  151. type="text"
  152. value={value}
  153. />
  154. <CalendarIcon className="date-input-control-icon" fill="" />
  155. {value !== undefined && (
  156. <ClearButton
  157. aria-label={translate('reset_verb')}
  158. className="button-tiny date-input-control-reset"
  159. iconProps={{ size: 12 }}
  160. onClick={this.handleResetClick}
  161. />
  162. )}
  163. {open && (
  164. <div className="date-input-calendar">
  165. <nav className="date-input-calendar-nav">
  166. <ButtonIcon className="button-small" onClick={this.handlePreviousMonthClick}>
  167. <ChevronLeftIcon />
  168. </ButtonIcon>
  169. <div className="date-input-calender-month">
  170. <Select
  171. aria-label={translate('select_month')}
  172. className="date-input-calender-month-select"
  173. onChange={this.handleCurrentMonthChange}
  174. options={monthOptions}
  175. value={monthOptions.find(month => month.value === currentMonth.getMonth())}
  176. />
  177. <Select
  178. aria-label={translate('select_year')}
  179. className="date-input-calender-month-select spacer-left"
  180. onChange={this.handleCurrentYearChange}
  181. options={yearOptions}
  182. value={yearOptions.find(year => year.value === currentMonth.getFullYear())}
  183. />
  184. </div>
  185. <ButtonIcon className="button-small" onClick={this.handleNextMonthClick}>
  186. <ChevronRightIcon />
  187. </ButtonIcon>
  188. </nav>
  189. <DayPicker
  190. captionElement={<NullComponent />}
  191. disabledDays={{ after, before: minDate }}
  192. firstDayOfWeek={1}
  193. modifiers={modifiers}
  194. month={currentMonth}
  195. navbarElement={<NullComponent />}
  196. onDayClick={this.handleDayClick}
  197. onDayMouseEnter={this.handleDayMouseEnter}
  198. selectedDays={selectedDays}
  199. weekdaysLong={weekdaysLong}
  200. weekdaysShort={weekdaysShort}
  201. />
  202. </div>
  203. )}
  204. </span>
  205. </OutsideClickHandler>
  206. );
  207. }
  208. }
  209. function NullComponent() {
  210. return null;
  211. }
  212. type InputWrapperProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value'> &
  213. WrappedComponentProps & {
  214. innerRef: React.Ref<HTMLInputElement>;
  215. value: Date | undefined;
  216. };
  217. const InputWrapper = injectIntl(({ innerRef, intl, value, ...other }: InputWrapperProps) => {
  218. const formattedValue =
  219. value && intl.formatDate(value, { year: 'numeric', month: 'short', day: 'numeric' });
  220. return <input {...other} ref={innerRef} value={formattedValue || ''} />;
  221. });