@gravity-ui/date-components 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/cjs/components/Calendar/Calendar.css +9 -1
  2. package/dist/cjs/components/Calendar/CalendarBase.js +9 -9
  3. package/dist/cjs/components/Calendar/hooks/types.d.ts +5 -2
  4. package/dist/cjs/components/Calendar/hooks/useCalendarCellProps.js +21 -16
  5. package/dist/cjs/components/Calendar/hooks/useCalendarGridProps.js +2 -7
  6. package/dist/cjs/components/Calendar/hooks/useCalendarProps.js +6 -3
  7. package/dist/cjs/components/Calendar/hooks/useCalendarState.js +69 -26
  8. package/dist/cjs/components/Calendar/hooks/useRangeCalendarState.js +16 -11
  9. package/dist/cjs/components/Calendar/utils.d.ts +3 -1
  10. package/dist/cjs/components/Calendar/utils.js +7 -1
  11. package/dist/cjs/components/DateField/hooks/useDateFieldProps.js +23 -16
  12. package/dist/cjs/components/DateField/hooks/useDateFieldState.d.ts +2 -0
  13. package/dist/cjs/components/DateField/hooks/useDateFieldState.js +49 -166
  14. package/dist/cjs/components/DateField/types.d.ts +1 -1
  15. package/dist/cjs/components/DateField/utils.d.ts +12 -9
  16. package/dist/cjs/components/DateField/utils.js +168 -25
  17. package/dist/cjs/components/DatePicker/DatePicker.css +6 -0
  18. package/dist/cjs/components/DatePicker/DatePicker.d.ts +4 -1
  19. package/dist/cjs/components/DatePicker/DatePicker.js +6 -33
  20. package/dist/cjs/components/DatePicker/MobileCalendar.js +4 -4
  21. package/dist/cjs/components/DatePicker/hooks/useDatePickerProps.d.ts +20 -0
  22. package/dist/cjs/components/DatePicker/hooks/useDatePickerProps.js +134 -0
  23. package/dist/cjs/components/DatePicker/hooks/useDatePickerState.d.ts +4 -5
  24. package/dist/cjs/components/DatePicker/hooks/useDatePickerState.js +18 -8
  25. package/dist/cjs/components/DatePicker/index.d.ts +1 -0
  26. package/dist/cjs/components/DatePicker/index.js +1 -0
  27. package/dist/cjs/components/RelativeDatePicker/RelativeDatePicker.css +1 -0
  28. package/dist/cjs/components/RelativeDatePicker/RelativeDatePicker.d.ts +3 -0
  29. package/dist/cjs/components/RelativeDatePicker/RelativeDatePicker.js +1 -1
  30. package/dist/cjs/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.js +35 -27
  31. package/dist/cjs/components/utils/dates.d.ts +9 -0
  32. package/dist/cjs/components/utils/dates.js +28 -0
  33. package/dist/esm/components/Calendar/Calendar.css +9 -1
  34. package/dist/esm/components/Calendar/CalendarBase.js +10 -10
  35. package/dist/esm/components/Calendar/hooks/types.d.ts +5 -2
  36. package/dist/esm/components/Calendar/hooks/useCalendarCellProps.js +21 -16
  37. package/dist/esm/components/Calendar/hooks/useCalendarGridProps.js +2 -7
  38. package/dist/esm/components/Calendar/hooks/useCalendarProps.js +6 -3
  39. package/dist/esm/components/Calendar/hooks/useCalendarState.js +70 -27
  40. package/dist/esm/components/Calendar/hooks/useRangeCalendarState.js +16 -11
  41. package/dist/esm/components/Calendar/utils.d.ts +3 -1
  42. package/dist/esm/components/Calendar/utils.js +6 -0
  43. package/dist/esm/components/DateField/hooks/useDateFieldProps.js +23 -16
  44. package/dist/esm/components/DateField/hooks/useDateFieldState.d.ts +2 -0
  45. package/dist/esm/components/DateField/hooks/useDateFieldState.js +41 -158
  46. package/dist/esm/components/DateField/types.d.ts +1 -1
  47. package/dist/esm/components/DateField/utils.d.ts +12 -9
  48. package/dist/esm/components/DateField/utils.js +162 -21
  49. package/dist/esm/components/DatePicker/DatePicker.css +6 -0
  50. package/dist/esm/components/DatePicker/DatePicker.d.ts +4 -1
  51. package/dist/esm/components/DatePicker/DatePicker.js +8 -35
  52. package/dist/esm/components/DatePicker/MobileCalendar.js +1 -1
  53. package/dist/esm/components/DatePicker/hooks/useDatePickerProps.d.ts +20 -0
  54. package/dist/esm/components/DatePicker/hooks/useDatePickerProps.js +127 -0
  55. package/dist/esm/components/DatePicker/hooks/useDatePickerState.d.ts +4 -5
  56. package/dist/esm/components/DatePicker/hooks/useDatePickerState.js +17 -7
  57. package/dist/esm/components/DatePicker/index.d.ts +1 -0
  58. package/dist/esm/components/DatePicker/index.js +1 -0
  59. package/dist/esm/components/RelativeDatePicker/RelativeDatePicker.css +1 -0
  60. package/dist/esm/components/RelativeDatePicker/RelativeDatePicker.d.ts +3 -0
  61. package/dist/esm/components/RelativeDatePicker/RelativeDatePicker.js +1 -1
  62. package/dist/esm/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.js +35 -27
  63. package/dist/esm/components/utils/dates.d.ts +9 -0
  64. package/dist/esm/components/utils/dates.js +22 -0
  65. package/package.json +8 -7
  66. package/dist/cjs/components/DatePicker/DesktopCalendar.d.ts +0 -15
  67. package/dist/cjs/components/DatePicker/DesktopCalendar.js +0 -56
  68. package/dist/esm/components/DatePicker/DesktopCalendar.d.ts +0 -15
  69. package/dist/esm/components/DatePicker/DesktopCalendar.js +0 -48
@@ -82,6 +82,11 @@
82
82
  .g-date-calendar__content_animation_zoom-out.g-date-calendar__previous-state {
83
83
  animation: calendar-zoom-out-hiding 0.25s ease forwards;
84
84
  }
85
+ @media screen and (prefers-reduced-motion: reduce), (update: slow) {
86
+ .g-date-calendar__content[class] {
87
+ animation-duration: 0.001ms;
88
+ }
89
+ }
85
90
  @keyframes calendar-forward {
86
91
  from {
87
92
  transform: translateX(0);
@@ -142,10 +147,13 @@
142
147
  display: grid;
143
148
  gap: var(--_--calendar-days-gap);
144
149
  }
145
- .g-date-calendar__grid-rowgroup_mode_months, .g-date-calendar__grid-rowgroup_mode_years {
150
+ .g-date-calendar__grid-rowgroup_mode_months, .g-date-calendar__grid-rowgroup_mode_quarters, .g-date-calendar__grid-rowgroup_mode_years {
146
151
  grid-row: 1/-1;
147
152
  padding: 12px 0 0;
148
153
  }
154
+ .g-date-calendar__grid-rowgroup-header {
155
+ align-self: center;
156
+ }
149
157
  .g-date-calendar__grid-row {
150
158
  display: grid;
151
159
  grid-auto-columns: 1fr;
@@ -22,7 +22,8 @@ exports.CalendarBase = react_1.default.forwardRef(function Calendar(props, ref)
22
22
  state.setFocused(true);
23
23
  },
24
24
  }));
25
- return ((0, jsx_runtime_1.jsxs)("div", Object.assign({}, calendarProps, { className: b({ size: props.size }), children: [(0, jsx_runtime_1.jsxs)("div", { className: b('header'), children: [(0, jsx_runtime_1.jsx)(uikit_1.Button, Object.assign({}, modeButtonProps, { view: "flat", size: props.size, children: state.mode === 'years' ? ((0, jsx_runtime_1.jsx)("span", { className: b('mode-label', b(`years-label`)), children: modeButtonProps.children }, "label")) : ([
25
+ return ((0, jsx_runtime_1.jsxs)("div", Object.assign({}, calendarProps, { className: b({ size: props.size }), children: [(0, jsx_runtime_1.jsxs)("div", { className: b('header'), children: [(0, jsx_runtime_1.jsx)(uikit_1.Button, Object.assign({}, modeButtonProps, { view: "flat", size: props.size, children: state.availableModes.indexOf(state.mode) + 1 ===
26
+ state.availableModes.length ? ((0, jsx_runtime_1.jsx)("span", { className: b('mode-label', b(`years-label`)), children: modeButtonProps.children }, "label")) : ([
26
27
  (0, jsx_runtime_1.jsx)("span", { className: b('mode-label'), children: modeButtonProps.children }, "label"),
27
28
  (0, jsx_runtime_1.jsx)(uikit_1.Button.Icon, { children: (0, jsx_runtime_1.jsx)(uikit_1.ArrowToggle, { direction: "bottom" }) }, "icon"),
28
29
  ]) })), (0, jsx_runtime_1.jsxs)("div", { className: b('controls'), children: [(0, jsx_runtime_1.jsx)(uikit_1.Button, Object.assign({}, previousButtonProps, { view: "flat", size: props.size, children: (0, jsx_runtime_1.jsx)(uikit_1.Button.Icon, { children: (0, jsx_runtime_1.jsx)(uikit_1.ArrowToggle, { direction: "left" }) }) })), (0, jsx_runtime_1.jsx)(uikit_1.Button, Object.assign({}, nextButtonProps, { view: "flat", size: props.size, children: (0, jsx_runtime_1.jsx)(uikit_1.Button.Icon, { children: (0, jsx_runtime_1.jsx)(uikit_1.ArrowToggle, { direction: "right" }) }) }))] })] }), (0, jsx_runtime_1.jsx)(CalendarGrid, { state: state })] })));
@@ -33,8 +34,7 @@ function CalendarGrid({ state }) {
33
34
  const startDateChanged = !state.startDate.isSame(prevState.startDate, 'days');
34
35
  let animation;
35
36
  if (modeChanged) {
36
- if ((state.mode === 'days' && prevState.mode === 'months') ||
37
- (state.mode === 'months' && prevState.mode === 'years')) {
37
+ if (utils_1.calendarLayouts.indexOf(prevState.mode) > utils_1.calendarLayouts.indexOf(state.mode)) {
38
38
  animation = 'zoom-out';
39
39
  }
40
40
  else {
@@ -65,13 +65,13 @@ function Weekdays({ state }) {
65
65
  }
66
66
  function CalendarGridCells({ state }) {
67
67
  const rowsInPeriod = state.mode === 'days' ? 6 : 4;
68
- const columnsInRow = state.mode === 'days' ? 7 : 3;
68
+ const columnsInRow = state.mode === 'days' ? 7 : 3 + (state.mode === 'quarters' ? 1 : 0);
69
69
  const days = (0, utils_1.getDaysInPeriod)(state.startDate, state.endDate, state.mode);
70
- return ((0, jsx_runtime_1.jsx)("div", { className: b('grid-rowgroup', { mode: state.mode }), role: "rowgroup", children: [...new Array(rowsInPeriod).keys()].map((rowIndex) => ((0, jsx_runtime_1.jsx)("div", { className: b('grid-row'), role: "row", children: days
71
- .slice(rowIndex * columnsInRow, (rowIndex + 1) * columnsInRow)
72
- .map((date) => {
73
- return (0, jsx_runtime_1.jsx)(CalendarCell, { date: date, state: state }, date.unix());
74
- }) }, rowIndex))) }));
70
+ return ((0, jsx_runtime_1.jsx)("div", { className: b('grid-rowgroup', { mode: state.mode }), role: "rowgroup", children: [...new Array(rowsInPeriod).keys()].map((rowIndex) => ((0, jsx_runtime_1.jsxs)("div", { className: b('grid-row'), role: "row", children: [state.mode === 'quarters' ? ((0, jsx_runtime_1.jsx)("span", { role: "rowheader", className: b('grid-rowgroup-header'), children: days[rowIndex * columnsInRow].format('YYYY') })) : null, days
71
+ .slice(rowIndex * columnsInRow, (rowIndex + 1) * columnsInRow)
72
+ .map((date) => {
73
+ return (0, jsx_runtime_1.jsx)(CalendarCell, { date: date, state: state }, date.unix());
74
+ })] }, rowIndex))) }));
75
75
  }
76
76
  function CalendarCell({ date, state }) {
77
77
  const { cellProps, buttonProps, formattedDate, isDisabled, isSelected, isRangeSelection, isSelectionStart, isSelectionEnd, isOutsideCurrentRange, isUnavailable, isCurrent, isWeekend, } = (0, useCalendarCellProps_1.useCalendarCellProps)(date, state);
@@ -1,6 +1,6 @@
1
1
  import type { DateTime } from '@gravity-ui/date-utils';
2
2
  import type { InputBase, RangeValue } from '../../types';
3
- export type CalendarLayout = 'days' | 'months' | 'years';
3
+ export type CalendarLayout = 'days' | 'months' | 'quarters' | 'years';
4
4
  export interface CalendarStateOptionsBase extends InputBase {
5
5
  /** The minimum allowed date that a user may select. */
6
6
  minValue?: DateTime;
@@ -27,6 +27,8 @@ export interface CalendarStateOptionsBase extends InputBase {
27
27
  defaultMode?: CalendarLayout;
28
28
  /** Handler that is called when the mode changes */
29
29
  onUpdateMode?: (mode: CalendarLayout) => void;
30
+ /** Controls which modes to use */
31
+ modes?: Partial<Record<CalendarLayout, boolean>>;
30
32
  }
31
33
  interface CalendarStateBase {
32
34
  /** Whether the calendar is disabled. */
@@ -45,7 +47,7 @@ interface CalendarStateBase {
45
47
  /** Selects the currently focused date. */
46
48
  selectFocusedDate(): void;
47
49
  /** Selects the given date. */
48
- selectDate(date: DateTime): void;
50
+ selectDate(date: DateTime, force?: boolean): void;
49
51
  /** Moves focus to the next calendar date. */
50
52
  focusNextCell(): void;
51
53
  /** Moves focus to the previous calendar date. */
@@ -98,6 +100,7 @@ interface CalendarStateBase {
98
100
  readonly mode: CalendarLayout;
99
101
  /** Sets calendar layout */
100
102
  setMode(mode: CalendarLayout): void;
103
+ readonly availableModes: CalendarLayout[];
101
104
  readonly startDate: DateTime;
102
105
  readonly endDate: DateTime;
103
106
  }
@@ -27,16 +27,7 @@ function useCalendarCellProps(date, state) {
27
27
  const isSelectable = !isDisabled && !isUnavailable;
28
28
  const isCurrent = (0, date_utils_1.dateTime)().isSame(date, state.mode);
29
29
  const isWeekend = state.isWeekend(date);
30
- let label = '';
31
- if (state.mode === 'days') {
32
- label = `${date.format('dddd')}, ${date.format('LL')}`;
33
- }
34
- else if (state.mode === 'months') {
35
- label = `${date.format('MMMM YYYY')}`;
36
- }
37
- else if (state.mode === 'years') {
38
- label = `${date.format('YYYY')}`;
39
- }
30
+ const label = getDateLabel(date, state.mode);
40
31
  const cellProps = {
41
32
  role: 'gridcell',
42
33
  'aria-selected': isSelected ? 'true' : undefined,
@@ -51,12 +42,7 @@ function useCalendarCellProps(date, state) {
51
42
  onClick: isSelectable
52
43
  ? () => {
53
44
  state.setFocusedDate(date);
54
- if (state.mode === 'days') {
55
- state.selectDate(date);
56
- }
57
- else {
58
- state.zoomIn();
59
- }
45
+ state.selectDate(date);
60
46
  }
61
47
  : undefined,
62
48
  onPointerEnter() {
@@ -77,6 +63,9 @@ function useCalendarCellProps(date, state) {
77
63
  if (state.mode === 'months') {
78
64
  formattedDate = date.format('MMM');
79
65
  }
66
+ else if (state.mode === 'quarters') {
67
+ formattedDate = date.format('[Q]Q');
68
+ }
80
69
  else if (state.mode === 'years') {
81
70
  formattedDate = date.format('YYYY');
82
71
  }
@@ -96,3 +85,19 @@ function useCalendarCellProps(date, state) {
96
85
  };
97
86
  }
98
87
  exports.useCalendarCellProps = useCalendarCellProps;
88
+ function getDateLabel(date, mode) {
89
+ let label = '';
90
+ if (mode === 'days') {
91
+ label = `${date.format('dddd')}, ${date.format('LL')}`;
92
+ }
93
+ else if (mode === 'months') {
94
+ label = `${date.format('MMMM YYYY')}`;
95
+ }
96
+ else if (mode === 'quarters') {
97
+ label = `${date.format('[Q]Q YYYY')}`;
98
+ }
99
+ else if (mode === 'years') {
100
+ label = `${date.format('YYYY')}`;
101
+ }
102
+ return label;
103
+ }
@@ -8,7 +8,7 @@ function useCalendarGridProps(state) {
8
8
  state.setFocused(isFocused);
9
9
  },
10
10
  });
11
- const gridProps = Object.assign(Object.assign({ role: 'grid', 'aria-label': state.mode === 'years'
11
+ const gridProps = Object.assign(Object.assign({ role: 'grid', 'aria-label': state.mode === 'years' || state.mode === 'quarters'
12
12
  ? `${state.startDate.year()} — ${state.endDate.year()}`
13
13
  : state.focusedDate.format(state.mode === 'days' ? 'MMMM YYYY' : 'YYYY'), 'aria-disabled': state.disabled ? 'true' : undefined, 'aria-readonly': state.readOnly ? 'true' : undefined }, focusWithinProps), { onKeyDown: (e) => {
14
14
  if (e.key === 'ArrowRight') {
@@ -42,12 +42,7 @@ function useCalendarGridProps(state) {
42
42
  state.zoomIn();
43
43
  }
44
44
  else if (e.key === 'Enter' || e.key === ' ') {
45
- if (state.mode === 'days') {
46
- state.selectFocusedDate();
47
- }
48
- else {
49
- state.setMode(state.mode === 'months' ? 'days' : 'months');
50
- }
45
+ state.selectDate(state.focusedDate);
51
46
  }
52
47
  } });
53
48
  return {
@@ -10,7 +10,7 @@ const i18n_1 = require("../i18n");
10
10
  const buttonDisabledClassName = 'yc-button_disabled g-button_disabled';
11
11
  // eslint-disable-next-line complexity
12
12
  function useCalendarProps(props, state) {
13
- const title = state.mode === 'years'
13
+ const title = state.mode === 'years' || state.mode === 'quarters'
14
14
  ? `${state.startDate.year()} — ${state.endDate.year()}`
15
15
  : state.focusedDate.format(state.mode === 'days' ? 'MMMM YYYY' : 'YYYY');
16
16
  const { focusWithinProps } = (0, uikit_1.useFocusWithin)({
@@ -18,7 +18,10 @@ function useCalendarProps(props, state) {
18
18
  onBlurWithin: props.onBlur,
19
19
  });
20
20
  const calendarProps = Object.assign({ role: 'group', id: props.id, 'aria-label': [props['aria-label'], title].filter(Boolean).join(', '), 'aria-labelledby': props['aria-labelledby'] || undefined, 'aria-describedby': props['aria-describedby'] || undefined, 'aria-details': props['aria-details'] || undefined, 'aria-disabled': state.disabled || undefined }, focusWithinProps);
21
- const modeDisabled = state.disabled || state.mode === 'years';
21
+ const modeIndex = state.availableModes.indexOf(state.mode);
22
+ const isModeLast = modeIndex + 1 === state.availableModes.length;
23
+ const isNextModeLast = modeIndex + 2 === state.availableModes.length;
24
+ const modeDisabled = state.disabled || isModeLast;
22
25
  const modeButtonProps = {
23
26
  disabled: state.disabled,
24
27
  // FIXME: do not use button class name
@@ -27,7 +30,7 @@ function useCalendarProps(props, state) {
27
30
  ? undefined
28
31
  : () => {
29
32
  state.zoomOut();
30
- if (state.mode === 'months') {
33
+ if (isNextModeLast) {
31
34
  state.setFocused(true);
32
35
  }
33
36
  },
@@ -5,14 +5,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.useCalendarState = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
- const date_utils_1 = require("@gravity-ui/date-utils");
9
8
  const useControlledState_1 = require("../../hooks/useControlledState");
9
+ const dates_1 = require("../../utils/dates");
10
10
  const utils_1 = require("../utils");
11
+ const defaultModes = {
12
+ days: true,
13
+ months: true,
14
+ quarters: false,
15
+ years: true,
16
+ };
11
17
  function useCalendarState(props) {
12
- const { disabled, readOnly, minValue, maxValue, timeZone } = props;
18
+ const { disabled, readOnly, minValue, maxValue, timeZone, modes = defaultModes } = props;
13
19
  const [value, setValue] = (0, useControlledState_1.useControlledState)(props.value, props.defaultValue, props.onUpdate);
14
20
  const [mode, setMode] = (0, useControlledState_1.useControlledState)(props.mode, props.defaultMode, props.onUpdateMode);
15
- const currentMode = mode || 'days';
21
+ const availableModes = utils_1.calendarLayouts.filter((l) => modes[l]);
22
+ const minMode = availableModes[0] || 'days';
23
+ const currentMode = mode && availableModes.includes(mode) ? mode : minMode;
16
24
  const focusedValue = react_1.default.useMemo(() => {
17
25
  if (!props.focusedValue) {
18
26
  return props.focusedValue;
@@ -20,17 +28,18 @@ function useCalendarState(props) {
20
28
  return (0, utils_1.constrainValue)(props.focusedValue, minValue, maxValue);
21
29
  }, [props.focusedValue, minValue, maxValue]);
22
30
  const defaultFocusedValue = react_1.default.useMemo(() => {
23
- const defaultValue = (props.defaultFocusedValue ? props.defaultFocusedValue : value) || (0, date_utils_1.dateTime)({ timeZone });
31
+ const defaultValue = (props.defaultFocusedValue ? props.defaultFocusedValue : value) ||
32
+ (0, dates_1.createPlaceholderValue)({ timeZone }).startOf(minMode);
24
33
  return (0, utils_1.constrainValue)(defaultValue, minValue, maxValue);
25
- }, [maxValue, minValue, props.defaultFocusedValue, timeZone, value]);
34
+ }, [maxValue, minValue, props.defaultFocusedValue, timeZone, value, minMode]);
26
35
  const [focusedDateInner, setFocusedDate] = (0, useControlledState_1.useControlledState)(focusedValue, defaultFocusedValue, props.onFocusUpdate);
27
- const focusedDate = focusedDateInner !== null && focusedDateInner !== void 0 ? focusedDateInner : (0, utils_1.constrainValue)((0, date_utils_1.dateTime)({ timeZone }), minValue, maxValue);
36
+ const focusedDate = focusedDateInner !== null && focusedDateInner !== void 0 ? focusedDateInner : (0, utils_1.constrainValue)((0, dates_1.createPlaceholderValue)({ timeZone }).startOf(minMode), minValue, maxValue);
28
37
  if (isInvalid(focusedDate, minValue, maxValue)) {
29
38
  // If the focused date was moved to an invalid value, it can't be focused, so constrain it.
30
39
  setFocusedDate((0, utils_1.constrainValue)(focusedDate, minValue, maxValue));
31
40
  }
32
41
  function focusCell(date) {
33
- setFocusedDate((0, utils_1.constrainValue)(date, minValue, maxValue));
42
+ setFocusedDate((0, utils_1.constrainValue)(date.startOf(currentMode), minValue, maxValue));
34
43
  }
35
44
  const [isFocused, setFocused] = react_1.default.useState(props.autoFocus || false);
36
45
  const startDate = getStartDate(focusedDate, currentMode);
@@ -41,13 +50,21 @@ function useCalendarState(props) {
41
50
  value,
42
51
  setValue,
43
52
  timeZone,
44
- selectDate(date) {
45
- if (!disabled && !readOnly) {
46
- const newValue = (0, utils_1.constrainValue)(date, minValue, maxValue);
47
- if (this.isCellUnavailable(newValue)) {
48
- return;
53
+ selectDate(date, force = false) {
54
+ if (!disabled) {
55
+ if (!readOnly && (force || this.mode === minMode)) {
56
+ const newValue = (0, utils_1.constrainValue)(date, minValue, maxValue);
57
+ if (this.isCellUnavailable(newValue)) {
58
+ return;
59
+ }
60
+ setValue(newValue);
61
+ if (force && currentMode !== minMode) {
62
+ setMode(minMode);
63
+ }
64
+ }
65
+ else {
66
+ this.zoomIn();
49
67
  }
50
- setValue((0, utils_1.constrainValue)(newValue, minValue, maxValue));
51
68
  }
52
69
  },
53
70
  minValue,
@@ -60,41 +77,53 @@ function useCalendarState(props) {
60
77
  setFocused(true);
61
78
  },
62
79
  focusNextCell() {
63
- focusCell(focusedDate.add({ [this.mode]: 1 }));
80
+ focusCell(focusedDate.add(1, this.mode));
64
81
  },
65
82
  focusPreviousCell() {
66
- focusCell(focusedDate.subtract({ [this.mode]: 1 }));
83
+ focusCell(focusedDate.subtract(1, this.mode));
67
84
  },
68
85
  focusNextRow() {
69
86
  if (this.mode === 'days') {
70
87
  focusCell(focusedDate.add(1, 'week'));
71
88
  }
89
+ else if (this.mode === 'quarters') {
90
+ focusCell(focusedDate.add(1, 'years'));
91
+ }
72
92
  else {
73
- focusCell(focusedDate.add({ [this.mode]: 3 }));
93
+ focusCell(focusedDate.add(3, this.mode));
74
94
  }
75
95
  },
76
96
  focusPreviousRow() {
77
97
  if (this.mode === 'days') {
78
98
  focusCell(focusedDate.subtract(1, 'week'));
79
99
  }
100
+ else if (this.mode === 'quarters') {
101
+ focusCell(focusedDate.subtract(1, 'years'));
102
+ }
80
103
  else {
81
- focusCell(focusedDate.subtract({ [this.mode]: 3 }));
104
+ focusCell(focusedDate.subtract(3, this.mode));
82
105
  }
83
106
  },
84
107
  focusNextPage(larger) {
85
108
  if (this.mode === 'days') {
86
109
  focusCell(focusedDate.add({ months: larger ? 12 : 1 }));
87
110
  }
111
+ else if (this.mode === 'quarters') {
112
+ focusCell(focusedDate.add(4, 'years'));
113
+ }
88
114
  else {
89
- focusCell(focusedDate.add({ [this.mode]: 12 }));
115
+ focusCell(focusedDate.add(12, this.mode));
90
116
  }
91
117
  },
92
118
  focusPreviousPage(larger) {
93
119
  if (this.mode === 'days') {
94
120
  focusCell(focusedDate.subtract({ months: larger ? 12 : 1 }));
95
121
  }
122
+ else if (this.mode === 'quarters') {
123
+ focusCell(focusedDate.subtract(4, 'years'));
124
+ }
96
125
  else {
97
- focusCell(focusedDate.subtract({ [this.mode]: 12 }));
126
+ focusCell(focusedDate.subtract(12, this.mode));
98
127
  }
99
128
  },
100
129
  focusSectionStart() {
@@ -104,13 +133,19 @@ function useCalendarState(props) {
104
133
  focusCell(getEndDate(focusedDate, this.mode));
105
134
  },
106
135
  zoomIn() {
107
- this.setMode(this.mode === 'years' ? 'months' : 'days');
136
+ const index = availableModes.indexOf(this.mode) - 1;
137
+ if (index >= 0) {
138
+ this.setMode(availableModes[index]);
139
+ }
108
140
  },
109
141
  zoomOut() {
110
- this.setMode(this.mode === 'days' ? 'months' : 'years');
142
+ const index = availableModes.indexOf(this.mode) + 1;
143
+ if (index < availableModes.length) {
144
+ this.setMode(availableModes[index]);
145
+ }
111
146
  },
112
147
  selectFocusedDate() {
113
- this.selectDate(focusedDate);
148
+ this.selectDate(focusedDate, true);
114
149
  },
115
150
  isFocused,
116
151
  setFocused,
@@ -118,11 +153,11 @@ function useCalendarState(props) {
118
153
  return isInvalid(date, this.minValue, this.maxValue, this.mode);
119
154
  },
120
155
  isPreviousPageInvalid() {
121
- const prev = this.startDate.add({ days: -1 });
156
+ const prev = this.startDate.subtract(1, 'day');
122
157
  return this.isInvalid(prev);
123
158
  },
124
159
  isNextPageInvalid() {
125
- const next = this.endDate.add({ days: 1 });
160
+ const next = this.endDate.endOf(this.mode).add(1, 'day');
126
161
  return this.isInvalid(next);
127
162
  },
128
163
  isSelected(date) {
@@ -131,7 +166,7 @@ function useCalendarState(props) {
131
166
  !this.isCellDisabled(date));
132
167
  },
133
168
  isCellUnavailable(date) {
134
- if (this.mode === 'days') {
169
+ if (this.mode === minMode) {
135
170
  return Boolean(props.isDateUnavailable && props.isDateUnavailable(date));
136
171
  }
137
172
  else {
@@ -149,6 +184,7 @@ function useCalendarState(props) {
149
184
  },
150
185
  mode: currentMode,
151
186
  setMode,
187
+ availableModes,
152
188
  };
153
189
  }
154
190
  exports.useCalendarState = useCalendarState;
@@ -159,6 +195,10 @@ function getStartDate(date, mode) {
159
195
  if (mode === 'months') {
160
196
  return date.startOf('year');
161
197
  }
198
+ if (mode === 'quarters') {
199
+ const year = Math.floor(date.year() / 4) * 4;
200
+ return date.startOf('year').set('year', year);
201
+ }
162
202
  const year = Math.floor(date.year() / 12) * 12;
163
203
  return date.startOf('year').set('year', year);
164
204
  }
@@ -167,9 +207,12 @@ function getEndDate(date, mode) {
167
207
  return date.endOf('month').startOf('day');
168
208
  }
169
209
  if (mode === 'months') {
170
- return date.endOf('month').startOf('day');
210
+ return date.endOf('year').startOf('month');
171
211
  }
172
212
  const startDate = getStartDate(date, mode);
213
+ if (mode === 'quarters') {
214
+ return startDate.add(15, 'quarters');
215
+ }
173
216
  return startDate.add({ [mode]: 11 });
174
217
  }
175
218
  function isInvalid(date, minValue, maxValue, mode = 'days') {
@@ -26,10 +26,18 @@ function useRangeCalendarState(props) {
26
26
  const [anchorDate, setAnchorDateState] = react_1.default.useState();
27
27
  const calendar = (0, useCalendarState_1.useCalendarState)(Object.assign(Object.assign({}, calendarProps), { value: (_a = value === null || value === void 0 ? void 0 : value.start) !== null && _a !== void 0 ? _a : null }));
28
28
  const highlightedRange = anchorDate
29
- ? makeRange(anchorDate, calendar.focusedDate)
30
- : (_b = (value && makeRange(value.start, value.end))) !== null && _b !== void 0 ? _b : undefined;
31
- const selectDate = (date) => {
32
- if (props.disabled || props.readOnly) {
29
+ ? makeRange(anchorDate, calendar.focusedDate, calendar.mode)
30
+ : (_b = (value && makeRange(value.start, value.end, calendar.mode))) !== null && _b !== void 0 ? _b : undefined;
31
+ const minMode = calendar.availableModes[0];
32
+ const selectDate = (date, force = false) => {
33
+ if (props.disabled) {
34
+ return;
35
+ }
36
+ if (!force && calendar.mode !== minMode) {
37
+ calendar.zoomIn();
38
+ return;
39
+ }
40
+ if (props.readOnly) {
33
41
  return;
34
42
  }
35
43
  date = (0, utils_1.constrainValue)(date, props.minValue, props.maxValue);
@@ -37,7 +45,7 @@ function useRangeCalendarState(props) {
37
45
  return;
38
46
  }
39
47
  if (anchorDate) {
40
- const range = makeRange(anchorDate, date);
48
+ const range = makeRange(anchorDate, date, calendar.mode);
41
49
  setValue(range);
42
50
  setAnchorDateState(undefined);
43
51
  }
@@ -49,9 +57,6 @@ function useRangeCalendarState(props) {
49
57
  setValue,
50
58
  selectDate,
51
59
  anchorDate, setAnchorDate: setAnchorDateState, highlightedRange,
52
- selectFocusedDate() {
53
- selectDate(calendar.focusedDate);
54
- },
55
60
  isSelected(date) {
56
61
  if (!highlightedRange) {
57
62
  return false;
@@ -68,15 +73,15 @@ function useRangeCalendarState(props) {
68
73
  } });
69
74
  }
70
75
  exports.useRangeCalendarState = useRangeCalendarState;
71
- function makeRange(start, end) {
76
+ function makeRange(start, end, mode) {
72
77
  if (start.isBefore(end)) {
73
78
  return {
74
79
  start,
75
- end,
80
+ end: end.endOf(mode),
76
81
  };
77
82
  }
78
83
  return {
79
84
  start: end,
80
- end: start,
85
+ end: start.endOf(mode),
81
86
  };
82
87
  }
@@ -1,4 +1,6 @@
1
1
  import type { DateTime } from '@gravity-ui/date-utils';
2
+ import type { CalendarLayout } from './hooks/types';
2
3
  export declare function constrainValue(value: DateTime, minValue: DateTime | undefined, maxValue: DateTime | undefined): DateTime;
3
- export declare function getDaysInPeriod(startDate: DateTime, _endDate: DateTime, mode: 'days' | 'months' | 'years'): DateTime[];
4
+ export declare function getDaysInPeriod(startDate: DateTime, _endDate: DateTime, mode: CalendarLayout): DateTime[];
4
5
  export declare function getWeekDays(): DateTime[];
6
+ export declare const calendarLayouts: CalendarLayout[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getWeekDays = exports.getDaysInPeriod = exports.constrainValue = void 0;
3
+ exports.calendarLayouts = exports.getWeekDays = exports.getDaysInPeriod = exports.constrainValue = void 0;
4
4
  const date_utils_1 = require("@gravity-ui/date-utils");
5
5
  function constrainValue(value, minValue, maxValue) {
6
6
  if (minValue && value.isBefore(minValue)) {
@@ -20,6 +20,11 @@ function getDaysInPeriod(startDate, _endDate, mode) {
20
20
  days.push(currentDate.add({ days: i }));
21
21
  }
22
22
  }
23
+ else if (mode === 'quarters') {
24
+ for (let i = 0; i < 16; i++) {
25
+ days.push(startDate.add(i, 'quarters'));
26
+ }
27
+ }
23
28
  else {
24
29
  for (let i = 0; i < 12; i++) {
25
30
  days.push(startDate.add({ [mode]: i }));
@@ -38,3 +43,4 @@ function getWeekDays() {
38
43
  return weekDays;
39
44
  }
40
45
  exports.getWeekDays = getWeekDays;
46
+ exports.calendarLayouts = ['days', 'months', 'quarters', 'years'];
@@ -86,12 +86,16 @@ function useDateFieldProps(state, props) {
86
86
  if (state.selectedSectionIndexes !== null) {
87
87
  return;
88
88
  }
89
- const input = inputRef.current;
89
+ const input = e.target;
90
+ const isAutofocus = !inputRef.current;
90
91
  setTimeout(() => {
91
92
  if (!input || input !== inputRef.current) {
92
93
  return;
93
94
  }
94
- if (
95
+ if (isAutofocus) {
96
+ state.focusSectionInPosition(0);
97
+ }
98
+ else if (
95
99
  // avoid selecting all sections when focusing empty field without value
96
100
  input.value.length &&
97
101
  Number(input.selectionEnd) - Number(input.selectionStart) ===
@@ -121,11 +125,11 @@ function useDateFieldProps(state, props) {
121
125
  }
122
126
  else if (e.key === 'Home') {
123
127
  e.preventDefault();
124
- state.focusFirstSection();
128
+ state.decrementToMin();
125
129
  }
126
130
  else if (e.key === 'End') {
127
131
  e.preventDefault();
128
- state.focusLastSection();
132
+ state.incrementToMax();
129
133
  }
130
134
  else if (e.key === 'ArrowUp' && !e.altKey) {
131
135
  e.preventDefault();
@@ -169,32 +173,35 @@ function useDateFieldProps(state, props) {
169
173
  onBeforeInput(e) {
170
174
  e.preventDefault();
171
175
  // @ts-expect-error
172
- const key = e.nativeEvent.data;
173
- // eslint-disable-next-line no-eq-null, eqeqeq
174
- if (key != null) {
176
+ const key = e.data;
177
+ if (key !== undefined && key !== null) {
175
178
  state.onInput(key);
176
179
  }
177
180
  },
178
181
  onPaste(e) {
182
+ e.preventDefault();
179
183
  if (state.readOnly) {
180
- e.preventDefault();
181
184
  return;
182
185
  }
183
186
  const pastedValue = e.clipboardData.getData('text');
184
- if (state.setValueFromString(pastedValue)) {
185
- e.preventDefault();
186
- }
187
- else if (state.selectedSectionIndexes &&
187
+ if (state.selectedSectionIndexes &&
188
188
  state.selectedSectionIndexes.startIndex ===
189
189
  state.selectedSectionIndexes.endIndex) {
190
190
  const activeSection = state.sections[state.selectedSectionIndexes.startIndex];
191
+ const digitsOnly = /^\d+$/.test(pastedValue);
192
+ const lettersOnly = /^[a-zA-Z]+$/.test(pastedValue);
191
193
  const isValidValue = Boolean(activeSection) &&
192
- ((activeSection.contentType === 'digit' && /^\d+$/.test(pastedValue)) ||
193
- activeSection.contentType === 'letter');
194
- if (!isValidValue) {
195
- e.preventDefault();
194
+ ((activeSection.contentType === 'digit' && digitsOnly) ||
195
+ (activeSection.contentType === 'letter' && lettersOnly));
196
+ if (isValidValue) {
197
+ state.onInput(pastedValue);
198
+ return;
199
+ }
200
+ if (digitsOnly || lettersOnly) {
201
+ return;
196
202
  }
197
203
  }
204
+ state.setValueFromString(pastedValue);
198
205
  },
199
206
  },
200
207
  },
@@ -56,6 +56,8 @@ export type DateFieldState = {
56
56
  * Upon reaching the minimum or maximum value, the value wraps around to the opposite limit.
57
57
  */
58
58
  decrementPage: () => void;
59
+ incrementToMax: () => void;
60
+ decrementToMin: () => void;
59
61
  /** Clears the value of the currently selected segment, reverting it to the placeholder. */
60
62
  clearSection: () => void;
61
63
  /** Clears all segments, reverting them to the placeholder. */