@gravity-ui/date-components 1.0.0 → 1.1.0

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 (55) 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 +6 -6
  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 +2 -2
  12. package/dist/cjs/components/DateField/hooks/useDateFieldState.d.ts +2 -0
  13. package/dist/cjs/components/DateField/hooks/useDateFieldState.js +33 -6
  14. package/dist/cjs/components/DateField/types.d.ts +1 -1
  15. package/dist/cjs/components/DateField/utils.d.ts +0 -9
  16. package/dist/cjs/components/DateField/utils.js +1 -25
  17. package/dist/cjs/components/DatePicker/DatePicker.d.ts +4 -1
  18. package/dist/cjs/components/DatePicker/DatePicker.js +2 -2
  19. package/dist/cjs/components/DatePicker/DesktopCalendar.d.ts +3 -1
  20. package/dist/cjs/components/DatePicker/DesktopCalendar.js +17 -8
  21. package/dist/cjs/components/DatePicker/MobileCalendar.js +4 -4
  22. package/dist/cjs/components/DatePicker/hooks/useDatePickerState.js +3 -8
  23. package/dist/cjs/components/RelativeDatePicker/RelativeDatePicker.d.ts +3 -0
  24. package/dist/cjs/components/RelativeDatePicker/RelativeDatePicker.js +1 -1
  25. package/dist/cjs/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.js +7 -1
  26. package/dist/cjs/components/utils/dates.d.ts +9 -0
  27. package/dist/cjs/components/utils/dates.js +28 -0
  28. package/dist/esm/components/Calendar/Calendar.css +9 -1
  29. package/dist/esm/components/Calendar/CalendarBase.js +10 -10
  30. package/dist/esm/components/Calendar/hooks/types.d.ts +5 -2
  31. package/dist/esm/components/Calendar/hooks/useCalendarCellProps.js +21 -16
  32. package/dist/esm/components/Calendar/hooks/useCalendarGridProps.js +2 -7
  33. package/dist/esm/components/Calendar/hooks/useCalendarProps.js +6 -3
  34. package/dist/esm/components/Calendar/hooks/useCalendarState.js +70 -27
  35. package/dist/esm/components/Calendar/hooks/useRangeCalendarState.js +6 -6
  36. package/dist/esm/components/Calendar/utils.d.ts +3 -1
  37. package/dist/esm/components/Calendar/utils.js +6 -0
  38. package/dist/esm/components/DateField/hooks/useDateFieldProps.js +2 -2
  39. package/dist/esm/components/DateField/hooks/useDateFieldState.d.ts +2 -0
  40. package/dist/esm/components/DateField/hooks/useDateFieldState.js +28 -1
  41. package/dist/esm/components/DateField/types.d.ts +1 -1
  42. package/dist/esm/components/DateField/utils.d.ts +0 -9
  43. package/dist/esm/components/DateField/utils.js +0 -21
  44. package/dist/esm/components/DatePicker/DatePicker.d.ts +4 -1
  45. package/dist/esm/components/DatePicker/DatePicker.js +2 -2
  46. package/dist/esm/components/DatePicker/DesktopCalendar.d.ts +3 -1
  47. package/dist/esm/components/DatePicker/DesktopCalendar.js +17 -8
  48. package/dist/esm/components/DatePicker/MobileCalendar.js +1 -1
  49. package/dist/esm/components/DatePicker/hooks/useDatePickerState.js +2 -7
  50. package/dist/esm/components/RelativeDatePicker/RelativeDatePicker.d.ts +3 -0
  51. package/dist/esm/components/RelativeDatePicker/RelativeDatePicker.js +1 -1
  52. package/dist/esm/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.js +7 -1
  53. package/dist/esm/components/utils/dates.d.ts +9 -0
  54. package/dist/esm/components/utils/dates.js +22 -0
  55. package/package.json +1 -1
@@ -1,12 +1,20 @@
1
1
  import React from 'react';
2
- import { dateTime } from '@gravity-ui/date-utils';
3
2
  import { useControlledState } from '../../hooks/useControlledState';
4
- import { constrainValue } from '../utils';
3
+ import { createPlaceholderValue } from '../../utils/dates';
4
+ import { calendarLayouts, constrainValue } from '../utils';
5
+ const defaultModes = {
6
+ days: true,
7
+ months: true,
8
+ quarters: false,
9
+ years: true,
10
+ };
5
11
  export function useCalendarState(props) {
6
- const { disabled, readOnly, minValue, maxValue, timeZone } = props;
12
+ const { disabled, readOnly, minValue, maxValue, timeZone, modes = defaultModes } = props;
7
13
  const [value, setValue] = useControlledState(props.value, props.defaultValue, props.onUpdate);
8
14
  const [mode, setMode] = useControlledState(props.mode, props.defaultMode, props.onUpdateMode);
9
- const currentMode = mode || 'days';
15
+ const availableModes = calendarLayouts.filter((l) => modes[l]);
16
+ const minMode = availableModes[0] || 'days';
17
+ const currentMode = mode && availableModes.includes(mode) ? mode : minMode;
10
18
  const focusedValue = React.useMemo(() => {
11
19
  if (!props.focusedValue) {
12
20
  return props.focusedValue;
@@ -14,17 +22,18 @@ export function useCalendarState(props) {
14
22
  return constrainValue(props.focusedValue, minValue, maxValue);
15
23
  }, [props.focusedValue, minValue, maxValue]);
16
24
  const defaultFocusedValue = React.useMemo(() => {
17
- const defaultValue = (props.defaultFocusedValue ? props.defaultFocusedValue : value) || dateTime({ timeZone });
25
+ const defaultValue = (props.defaultFocusedValue ? props.defaultFocusedValue : value) ||
26
+ createPlaceholderValue({ timeZone }).startOf(minMode);
18
27
  return constrainValue(defaultValue, minValue, maxValue);
19
- }, [maxValue, minValue, props.defaultFocusedValue, timeZone, value]);
28
+ }, [maxValue, minValue, props.defaultFocusedValue, timeZone, value, minMode]);
20
29
  const [focusedDateInner, setFocusedDate] = useControlledState(focusedValue, defaultFocusedValue, props.onFocusUpdate);
21
- const focusedDate = focusedDateInner !== null && focusedDateInner !== void 0 ? focusedDateInner : constrainValue(dateTime({ timeZone }), minValue, maxValue);
30
+ const focusedDate = focusedDateInner !== null && focusedDateInner !== void 0 ? focusedDateInner : constrainValue(createPlaceholderValue({ timeZone }).startOf(minMode), minValue, maxValue);
22
31
  if (isInvalid(focusedDate, minValue, maxValue)) {
23
32
  // If the focused date was moved to an invalid value, it can't be focused, so constrain it.
24
33
  setFocusedDate(constrainValue(focusedDate, minValue, maxValue));
25
34
  }
26
35
  function focusCell(date) {
27
- setFocusedDate(constrainValue(date, minValue, maxValue));
36
+ setFocusedDate(constrainValue(date.startOf(currentMode), minValue, maxValue));
28
37
  }
29
38
  const [isFocused, setFocused] = React.useState(props.autoFocus || false);
30
39
  const startDate = getStartDate(focusedDate, currentMode);
@@ -35,13 +44,21 @@ export function useCalendarState(props) {
35
44
  value,
36
45
  setValue,
37
46
  timeZone,
38
- selectDate(date) {
39
- if (!disabled && !readOnly) {
40
- const newValue = constrainValue(date, minValue, maxValue);
41
- if (this.isCellUnavailable(newValue)) {
42
- return;
47
+ selectDate(date, force = false) {
48
+ if (!disabled) {
49
+ if (!readOnly && (force || this.mode === minMode)) {
50
+ const newValue = constrainValue(date, minValue, maxValue);
51
+ if (this.isCellUnavailable(newValue)) {
52
+ return;
53
+ }
54
+ setValue(newValue);
55
+ if (force && currentMode !== minMode) {
56
+ setMode(minMode);
57
+ }
58
+ }
59
+ else {
60
+ this.zoomIn();
43
61
  }
44
- setValue(constrainValue(newValue, minValue, maxValue));
45
62
  }
46
63
  },
47
64
  minValue,
@@ -54,41 +71,53 @@ export function useCalendarState(props) {
54
71
  setFocused(true);
55
72
  },
56
73
  focusNextCell() {
57
- focusCell(focusedDate.add({ [this.mode]: 1 }));
74
+ focusCell(focusedDate.add(1, this.mode));
58
75
  },
59
76
  focusPreviousCell() {
60
- focusCell(focusedDate.subtract({ [this.mode]: 1 }));
77
+ focusCell(focusedDate.subtract(1, this.mode));
61
78
  },
62
79
  focusNextRow() {
63
80
  if (this.mode === 'days') {
64
81
  focusCell(focusedDate.add(1, 'week'));
65
82
  }
83
+ else if (this.mode === 'quarters') {
84
+ focusCell(focusedDate.add(1, 'years'));
85
+ }
66
86
  else {
67
- focusCell(focusedDate.add({ [this.mode]: 3 }));
87
+ focusCell(focusedDate.add(3, this.mode));
68
88
  }
69
89
  },
70
90
  focusPreviousRow() {
71
91
  if (this.mode === 'days') {
72
92
  focusCell(focusedDate.subtract(1, 'week'));
73
93
  }
94
+ else if (this.mode === 'quarters') {
95
+ focusCell(focusedDate.subtract(1, 'years'));
96
+ }
74
97
  else {
75
- focusCell(focusedDate.subtract({ [this.mode]: 3 }));
98
+ focusCell(focusedDate.subtract(3, this.mode));
76
99
  }
77
100
  },
78
101
  focusNextPage(larger) {
79
102
  if (this.mode === 'days') {
80
103
  focusCell(focusedDate.add({ months: larger ? 12 : 1 }));
81
104
  }
105
+ else if (this.mode === 'quarters') {
106
+ focusCell(focusedDate.add(4, 'years'));
107
+ }
82
108
  else {
83
- focusCell(focusedDate.add({ [this.mode]: 12 }));
109
+ focusCell(focusedDate.add(12, this.mode));
84
110
  }
85
111
  },
86
112
  focusPreviousPage(larger) {
87
113
  if (this.mode === 'days') {
88
114
  focusCell(focusedDate.subtract({ months: larger ? 12 : 1 }));
89
115
  }
116
+ else if (this.mode === 'quarters') {
117
+ focusCell(focusedDate.subtract(4, 'years'));
118
+ }
90
119
  else {
91
- focusCell(focusedDate.subtract({ [this.mode]: 12 }));
120
+ focusCell(focusedDate.subtract(12, this.mode));
92
121
  }
93
122
  },
94
123
  focusSectionStart() {
@@ -98,13 +127,19 @@ export function useCalendarState(props) {
98
127
  focusCell(getEndDate(focusedDate, this.mode));
99
128
  },
100
129
  zoomIn() {
101
- this.setMode(this.mode === 'years' ? 'months' : 'days');
130
+ const index = availableModes.indexOf(this.mode) - 1;
131
+ if (index >= 0) {
132
+ this.setMode(availableModes[index]);
133
+ }
102
134
  },
103
135
  zoomOut() {
104
- this.setMode(this.mode === 'days' ? 'months' : 'years');
136
+ const index = availableModes.indexOf(this.mode) + 1;
137
+ if (index < availableModes.length) {
138
+ this.setMode(availableModes[index]);
139
+ }
105
140
  },
106
141
  selectFocusedDate() {
107
- this.selectDate(focusedDate);
142
+ this.selectDate(focusedDate, true);
108
143
  },
109
144
  isFocused,
110
145
  setFocused,
@@ -112,11 +147,11 @@ export function useCalendarState(props) {
112
147
  return isInvalid(date, this.minValue, this.maxValue, this.mode);
113
148
  },
114
149
  isPreviousPageInvalid() {
115
- const prev = this.startDate.add({ days: -1 });
150
+ const prev = this.startDate.subtract(1, 'day');
116
151
  return this.isInvalid(prev);
117
152
  },
118
153
  isNextPageInvalid() {
119
- const next = this.endDate.add({ days: 1 });
154
+ const next = this.endDate.endOf(this.mode).add(1, 'day');
120
155
  return this.isInvalid(next);
121
156
  },
122
157
  isSelected(date) {
@@ -125,7 +160,7 @@ export function useCalendarState(props) {
125
160
  !this.isCellDisabled(date));
126
161
  },
127
162
  isCellUnavailable(date) {
128
- if (this.mode === 'days') {
163
+ if (this.mode === minMode) {
129
164
  return Boolean(props.isDateUnavailable && props.isDateUnavailable(date));
130
165
  }
131
166
  else {
@@ -143,6 +178,7 @@ export function useCalendarState(props) {
143
178
  },
144
179
  mode: currentMode,
145
180
  setMode,
181
+ availableModes,
146
182
  };
147
183
  }
148
184
  function getStartDate(date, mode) {
@@ -152,6 +188,10 @@ function getStartDate(date, mode) {
152
188
  if (mode === 'months') {
153
189
  return date.startOf('year');
154
190
  }
191
+ if (mode === 'quarters') {
192
+ const year = Math.floor(date.year() / 4) * 4;
193
+ return date.startOf('year').set('year', year);
194
+ }
155
195
  const year = Math.floor(date.year() / 12) * 12;
156
196
  return date.startOf('year').set('year', year);
157
197
  }
@@ -160,9 +200,12 @@ function getEndDate(date, mode) {
160
200
  return date.endOf('month').startOf('day');
161
201
  }
162
202
  if (mode === 'months') {
163
- return date.endOf('month').startOf('day');
203
+ return date.endOf('year').startOf('month');
164
204
  }
165
205
  const startDate = getStartDate(date, mode);
206
+ if (mode === 'quarters') {
207
+ return startDate.add(15, 'quarters');
208
+ }
166
209
  return startDate.add({ [mode]: 11 });
167
210
  }
168
211
  function isInvalid(date, minValue, maxValue, mode = 'days') {
@@ -20,8 +20,8 @@ export function useRangeCalendarState(props) {
20
20
  const [anchorDate, setAnchorDateState] = React.useState();
21
21
  const calendar = useCalendarState(Object.assign(Object.assign({}, calendarProps), { value: (_a = value === null || value === void 0 ? void 0 : value.start) !== null && _a !== void 0 ? _a : null }));
22
22
  const highlightedRange = anchorDate
23
- ? makeRange(anchorDate, calendar.focusedDate)
24
- : (_b = (value && makeRange(value.start, value.end))) !== null && _b !== void 0 ? _b : undefined;
23
+ ? makeRange(anchorDate, calendar.focusedDate, calendar.mode)
24
+ : (_b = (value && makeRange(value.start, value.end, calendar.mode))) !== null && _b !== void 0 ? _b : undefined;
25
25
  const selectDate = (date) => {
26
26
  if (props.disabled || props.readOnly) {
27
27
  return;
@@ -31,7 +31,7 @@ export function useRangeCalendarState(props) {
31
31
  return;
32
32
  }
33
33
  if (anchorDate) {
34
- const range = makeRange(anchorDate, date);
34
+ const range = makeRange(anchorDate, date, calendar.mode);
35
35
  setValue(range);
36
36
  setAnchorDateState(undefined);
37
37
  }
@@ -61,15 +61,15 @@ export function useRangeCalendarState(props) {
61
61
  }
62
62
  } });
63
63
  }
64
- function makeRange(start, end) {
64
+ function makeRange(start, end, mode) {
65
65
  if (start.isBefore(end)) {
66
66
  return {
67
67
  start,
68
- end,
68
+ end: end.endOf(mode),
69
69
  };
70
70
  }
71
71
  return {
72
72
  start: end,
73
- end: start,
73
+ end: start.endOf(mode),
74
74
  };
75
75
  }
@@ -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[];
@@ -16,6 +16,11 @@ export function getDaysInPeriod(startDate, _endDate, mode) {
16
16
  days.push(currentDate.add({ days: i }));
17
17
  }
18
18
  }
19
+ else if (mode === 'quarters') {
20
+ for (let i = 0; i < 16; i++) {
21
+ days.push(startDate.add(i, 'quarters'));
22
+ }
23
+ }
19
24
  else {
20
25
  for (let i = 0; i < 12; i++) {
21
26
  days.push(startDate.add({ [mode]: i }));
@@ -32,3 +37,4 @@ export function getWeekDays() {
32
37
  }
33
38
  return weekDays;
34
39
  }
40
+ export const calendarLayouts = ['days', 'months', 'quarters', 'years'];
@@ -115,11 +115,11 @@ export function useDateFieldProps(state, props) {
115
115
  }
116
116
  else if (e.key === 'Home') {
117
117
  e.preventDefault();
118
- state.focusFirstSection();
118
+ state.decrementToMin();
119
119
  }
120
120
  else if (e.key === 'End') {
121
121
  e.preventDefault();
122
- state.focusLastSection();
122
+ state.incrementToMax();
123
123
  }
124
124
  else if (e.key === 'ArrowUp' && !e.altKey) {
125
125
  e.preventDefault();
@@ -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. */
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
  import { dateTime, isValid } from '@gravity-ui/date-utils';
3
3
  import { useControlledState } from '../../hooks/useControlledState';
4
- import { createPlaceholderValue, isInvalid, mergeDateTime, splitFormatIntoSections } from '../utils';
4
+ import { createPlaceholderValue, isInvalid, mergeDateTime } from '../../utils/dates';
5
+ import { splitFormatIntoSections } from '../utils';
5
6
  const EDITABLE_SEGMENTS = {
6
7
  year: true,
7
8
  month: true,
@@ -219,6 +220,32 @@ export function useDateFieldState(props) {
219
220
  adjustSection(sectionIndex, -(PAGE_STEP[this.sections[sectionIndex].type] || 1));
220
221
  }
221
222
  },
223
+ incrementToMax() {
224
+ if (this.readOnly || this.disabled) {
225
+ return;
226
+ }
227
+ enteredKeys.current = '';
228
+ const sectionIndex = getCurrentEditableSectionIndex(this.sections, selectedSections);
229
+ if (sectionIndex !== -1) {
230
+ const section = this.sections[sectionIndex];
231
+ if (typeof section.maxValue === 'number') {
232
+ setSection(section, section.maxValue);
233
+ }
234
+ }
235
+ },
236
+ decrementToMin() {
237
+ if (this.readOnly || this.disabled) {
238
+ return;
239
+ }
240
+ enteredKeys.current = '';
241
+ const sectionIndex = getCurrentEditableSectionIndex(this.sections, selectedSections);
242
+ if (sectionIndex !== -1) {
243
+ const section = this.sections[sectionIndex];
244
+ if (typeof section.minValue === 'number') {
245
+ setSection(section, section.minValue);
246
+ }
247
+ }
248
+ },
222
249
  clearSection() {
223
250
  if (this.readOnly || this.disabled) {
224
251
  return;
@@ -1,4 +1,4 @@
1
- export type DateFieldSectionType = Exclude<Intl.DateTimeFormatPartTypes, 'era'>;
1
+ export type DateFieldSectionType = Extract<Intl.DateTimeFormatPartTypes, 'day' | 'dayPeriod' | 'hour' | 'literal' | 'minute' | 'month' | 'second' | 'timeZoneName' | 'weekday' | 'year' | 'unknown'>;
2
2
  export type DateFormatTokenMap = {
3
3
  [formatToken: string]: DateFieldSectionType | {
4
4
  sectionType: DateFieldSectionType;
@@ -1,14 +1,5 @@
1
- import type { DateTime } from '@gravity-ui/date-utils';
2
1
  import type { DateFieldSectionWithoutPosition } from './types';
3
2
  export declare const isMac: boolean;
4
3
  export declare const CtrlCmd: string;
5
4
  export declare function expandFormat(format: string): string;
6
5
  export declare function splitFormatIntoSections(format: string): DateFieldSectionWithoutPosition[];
7
- interface PlaceholderValueOptions {
8
- placeholderValue?: DateTime;
9
- timeZone?: string;
10
- }
11
- export declare function createPlaceholderValue({ placeholderValue, timeZone }: PlaceholderValueOptions): DateTime;
12
- export declare function isInvalid(value: DateTime | null | undefined, minValue: DateTime | undefined, maxValue: DateTime | undefined): boolean;
13
- export declare function mergeDateTime(date: DateTime, time: DateTime): DateTime;
14
- export {};
@@ -245,24 +245,3 @@ function getSectionOptions(section, token) {
245
245
  }
246
246
  return undefined;
247
247
  }
248
- export function createPlaceholderValue({ placeholderValue, timeZone }) {
249
- return (placeholderValue !== null && placeholderValue !== void 0 ? placeholderValue : dateTime({ timeZone }).set('hour', 0).set('minute', 0).set('second', 0));
250
- }
251
- export function isInvalid(value, minValue, maxValue) {
252
- if (!value) {
253
- return false;
254
- }
255
- if (minValue && value.isBefore(minValue)) {
256
- return true;
257
- }
258
- if (maxValue && maxValue.isBefore(value)) {
259
- return true;
260
- }
261
- return false;
262
- }
263
- export function mergeDateTime(date, time) {
264
- return date
265
- .set('hours', time.hour())
266
- .set('minutes', time.minute())
267
- .set('seconds', time.second());
268
- }
@@ -1,5 +1,8 @@
1
+ import React from 'react';
2
+ import type { CalendarProps } from '../Calendar';
1
3
  import type { AccessibilityProps, DateFieldBase, DomProps, FocusableProps, KeyboardEvents, StyleProps, TextInputProps } from '../types';
2
4
  import './DatePicker.css';
3
5
  export interface DatePickerProps extends DateFieldBase, TextInputProps, FocusableProps, KeyboardEvents, DomProps, StyleProps, AccessibilityProps {
6
+ children?: (props: CalendarProps) => React.ReactNode;
4
7
  }
5
- export declare function DatePicker({ value, defaultValue, onUpdate, className, onFocus, onBlur, ...props }: DatePickerProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function DatePicker({ value, defaultValue, onUpdate, className, onFocus, onBlur, children, ...props }: DatePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -19,7 +19,7 @@ import { useDatePickerState } from './hooks/useDatePickerState';
19
19
  import { b } from './utils';
20
20
  import './DatePicker.css';
21
21
  export function DatePicker(_a) {
22
- var { value, defaultValue, onUpdate, className, onFocus, onBlur } = _a, props = __rest(_a, ["value", "defaultValue", "onUpdate", "className", "onFocus", "onBlur"]);
22
+ var { value, defaultValue, onUpdate, className, onFocus, onBlur, children } = _a, props = __rest(_a, ["value", "defaultValue", "onUpdate", "className", "onFocus", "onBlur", "children"]);
23
23
  const anchorRef = React.useRef(null);
24
24
  const state = useDatePickerState(Object.assign(Object.assign({}, props), { value,
25
25
  defaultValue,
@@ -55,5 +55,5 @@ export function DatePicker(_a) {
55
55
  e.stopPropagation();
56
56
  state.setOpen(true);
57
57
  }
58
- }, children: [isMobile ? (_jsx(MobileCalendar, { props: props, state: state })) : (_jsx(DesktopCalendar, { anchorRef: anchorRef, props: props, state: state })), _jsx(TextInput, Object.assign({}, inputProps, { value: fieldState.isEmpty && !isActive && props.placeholder ? '' : inputProps.value, className: b('field', { mobile: isMobile }), hasClear: !isMobile && inputProps.hasClear, rightContent: isMobile ? (_jsx(MobileCalendarIcon, { props: props, state: state })) : (_jsx(DesktopCalendarButton, { props: props, state: state })) }))] })));
58
+ }, children: [isMobile ? (_jsx(MobileCalendar, { props: props, state: state })) : (_jsx(DesktopCalendar, { anchorRef: anchorRef, props: props, state: state, renderCalendar: children })), _jsx(TextInput, Object.assign({}, inputProps, { value: fieldState.isEmpty && !isActive && props.placeholder ? '' : inputProps.value, className: b('field', { mobile: isMobile }), hasClear: !isMobile && inputProps.hasClear, rightContent: isMobile ? (_jsx(MobileCalendarIcon, { props: props, state: state })) : (_jsx(DesktopCalendarButton, { props: props, state: state })) }))] })));
59
59
  }
@@ -1,12 +1,14 @@
1
1
  import React from 'react';
2
+ import { type CalendarProps } from '../Calendar';
2
3
  import type { DatePickerProps } from './DatePicker';
3
4
  import type { DatePickerState } from './hooks/useDatePickerState';
4
5
  interface DesktopCalendarProps {
5
6
  anchorRef: React.RefObject<HTMLElement>;
6
7
  props: DatePickerProps;
7
8
  state: DatePickerState;
9
+ renderCalendar?: (props: CalendarProps) => React.ReactNode;
8
10
  }
9
- export declare function DesktopCalendar({ anchorRef, props, state }: DesktopCalendarProps): import("react/jsx-runtime").JSX.Element | null;
11
+ export declare function DesktopCalendar({ anchorRef, props, state, renderCalendar }: DesktopCalendarProps): import("react/jsx-runtime").JSX.Element | null;
10
12
  interface DesktopCalendarButtonProps {
11
13
  props: DatePickerProps;
12
14
  state: DatePickerState;
@@ -7,7 +7,7 @@ import { DateField } from '../DateField';
7
7
  import { getButtonSizeForInput } from '../utils/getButtonSizeForInput';
8
8
  import { i18n } from './i18n';
9
9
  import { b } from './utils';
10
- export function DesktopCalendar({ anchorRef, props, state }) {
10
+ export function DesktopCalendar({ anchorRef, props, state, renderCalendar }) {
11
11
  var _a;
12
12
  const { focusWithinProps } = useFocusWithin({
13
13
  isDisabled: !state.isOpen,
@@ -18,15 +18,24 @@ export function DesktopCalendar({ anchorRef, props, state }) {
18
18
  if (!state.hasDate) {
19
19
  return null;
20
20
  }
21
+ const calendarProps = {
22
+ autoFocus: true,
23
+ size: props.size === 's' ? 'm' : props.size,
24
+ disabled: props.disabled,
25
+ readOnly: props.readOnly,
26
+ onUpdate: (d) => {
27
+ state.setDateValue(d);
28
+ },
29
+ defaultFocusedValue: (_a = state.dateValue) !== null && _a !== void 0 ? _a : undefined,
30
+ value: state.dateValue,
31
+ minValue: props.minValue,
32
+ maxValue: props.maxValue,
33
+ isDateUnavailable: props.isDateUnavailable,
34
+ timeZone: props.timeZone,
35
+ };
21
36
  return (_jsx(Popup, { open: state.isOpen, anchorRef: anchorRef, onClose: () => {
22
37
  state.setOpen(false);
23
- }, restoreFocus: true, children: _jsxs("div", Object.assign({}, focusWithinProps, { className: b('popup-content'), children: [_jsx(Calendar
24
- // eslint-disable-next-line jsx-a11y/no-autofocus
25
- , {
26
- // eslint-disable-next-line jsx-a11y/no-autofocus
27
- autoFocus: true, size: props.size === 's' ? 'm' : props.size, disabled: props.disabled, readOnly: props.readOnly, onUpdate: (d) => {
28
- state.setDateValue(d);
29
- }, defaultFocusedValue: (_a = state.dateValue) !== null && _a !== void 0 ? _a : undefined, value: state.dateValue, minValue: props.minValue, maxValue: props.maxValue, isDateUnavailable: props.isDateUnavailable, timeZone: props.timeZone }), state.hasTime && (_jsx("div", { className: b('time-field-wrapper'), children: _jsx(DateField, { className: b('time-field'), size: props.size, readOnly: props.readOnly, value: state.timeValue, onUpdate: state.setTimeValue, placeholderValue: props.placeholderValue, minValue: props.minValue, maxValue: props.maxValue, timeZone: props.timeZone, format: state.timeFormat }) }))] })) }));
38
+ }, restoreFocus: true, children: _jsxs("div", Object.assign({}, focusWithinProps, { className: b('popup-content'), tabIndex: -1, children: [typeof renderCalendar === 'function' ? (renderCalendar(calendarProps)) : (_jsx(Calendar, Object.assign({}, calendarProps))), state.hasTime && (_jsx("div", { className: b('time-field-wrapper'), children: _jsx(DateField, { className: b('time-field'), size: props.size, readOnly: props.readOnly, value: state.timeValue, onUpdate: state.setTimeValue, placeholderValue: props.placeholderValue, minValue: props.minValue, maxValue: props.maxValue, timeZone: props.timeZone, format: state.timeFormat }) }))] })) }));
30
39
  }
31
40
  export function DesktopCalendarButton({ props, state }) {
32
41
  const wasOpenBeforeClickRef = React.useRef(false);
@@ -3,7 +3,7 @@ import { dateTime } from '@gravity-ui/date-utils';
3
3
  import { Calendar as CalendarIcon } from '@gravity-ui/icons';
4
4
  import { Icon } from '@gravity-ui/uikit';
5
5
  import { block } from '../../utils/cn';
6
- import { createPlaceholderValue, mergeDateTime } from '../DateField/utils';
6
+ import { createPlaceholderValue, mergeDateTime } from '../utils/dates';
7
7
  import { getButtonSizeForInput } from '../utils/getButtonSizeForInput';
8
8
  import './MobileCalendar.css';
9
9
  const b = block('mobile-calendar');
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
- import { createPlaceholderValue, splitFormatIntoSections } from '../../DateField/utils';
2
+ import { splitFormatIntoSections } from '../../DateField/utils';
3
3
  import { useControlledState } from '../../hooks/useControlledState';
4
+ import { createPlaceholderValue, mergeDateTime } from '../../utils/dates';
4
5
  export function useDatePickerState(props) {
5
6
  const { disabled, readOnly } = props;
6
7
  const [isOpen, setOpen] = React.useState(false);
@@ -96,12 +97,6 @@ export function useDatePickerState(props) {
96
97
  },
97
98
  };
98
99
  }
99
- function mergeDateTime(date, time) {
100
- return date
101
- .set('hours', time.hour())
102
- .set('minutes', time.minute())
103
- .set('seconds', time.second());
104
- }
105
100
  function getPlaceholderTime(placeholderValue, timeZone) {
106
101
  return createPlaceholderValue({ placeholderValue, timeZone });
107
102
  }
@@ -1,6 +1,9 @@
1
+ import React from 'react';
2
+ import type { CalendarProps } from '../Calendar';
1
3
  import type { AccessibilityProps, DomProps, FocusableProps, KeyboardEvents, StyleProps, TextInputProps } from '../types';
2
4
  import type { RelativeDatePickerStateOptions } from './hooks/useRelativeDatePickerState';
3
5
  import './RelativeDatePicker.css';
4
6
  export interface RelativeDatePickerProps extends RelativeDatePickerStateOptions, TextInputProps, FocusableProps, KeyboardEvents, DomProps, StyleProps, AccessibilityProps {
7
+ children?: (props: CalendarProps) => React.ReactNode;
5
8
  }
6
9
  export declare function RelativeDatePicker(props: RelativeDatePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -21,5 +21,5 @@ export function RelativeDatePicker(props) {
21
21
  readOnly: props.readOnly,
22
22
  placeholderValue: props.placeholderValue,
23
23
  timeZone: props.timeZone,
24
- } })), _jsx(TextInput, Object.assign({}, fieldProps, { leftContent: _jsx(Button, Object.assign({}, modeSwitcherProps, { children: _jsx(Icon, { data: FunctionIcon }) })), rightContent: _jsxs(React.Fragment, { children: [!isMobile && (_jsx(Button, Object.assign({}, calendarButtonProps, { children: _jsx(Icon, { data: CalendarIcon }) }))), isMobile && state.mode === 'absolute' && (_jsx(MobileCalendarIcon, { state: state.datePickerState, props: { size: props.size } }))] }) })), !isMobile && (_jsx(Popup, Object.assign({}, popupProps, { anchorRef: anchorRef, children: _jsxs("div", { className: b('popup-content'), children: [_jsx(Calendar, Object.assign({}, calendarProps)), state.datePickerState.hasTime && (_jsx("div", { className: b('time-field-wrapper'), children: _jsx(DateField, Object.assign({}, timeInputProps)) }))] }) })))] })));
24
+ } })), _jsx(TextInput, Object.assign({}, fieldProps, { leftContent: _jsx(Button, Object.assign({}, modeSwitcherProps, { children: _jsx(Icon, { data: FunctionIcon }) })), rightContent: _jsxs(React.Fragment, { children: [!isMobile && (_jsx(Button, Object.assign({}, calendarButtonProps, { children: _jsx(Icon, { data: CalendarIcon }) }))), isMobile && state.mode === 'absolute' && (_jsx(MobileCalendarIcon, { state: state.datePickerState, props: { size: props.size } }))] }) })), !isMobile && (_jsx(Popup, Object.assign({}, popupProps, { anchorRef: anchorRef, children: _jsxs("div", { className: b('popup-content'), children: [typeof props.children === 'function' ? (props.children(calendarProps)) : (_jsx(Calendar, Object.assign({}, calendarProps))), state.datePickerState.hasTime && (_jsx("div", { className: b('time-field-wrapper'), children: _jsx(DateField, Object.assign({}, timeInputProps)) }))] }) })))] })));
25
25
  }
@@ -81,7 +81,13 @@ export function useRelativeDatePickerProps(state, _a) {
81
81
  const handleRef = useForkRef(inputRef, mode === 'relative' ? relativeDateProps.controlRef : inputProps.controlRef);
82
82
  const calendarRef = React.useRef(null);
83
83
  return {
84
- groupProps: Object.assign({ role: 'group' }, focusWithinProps),
84
+ groupProps: Object.assign(Object.assign({ role: 'group' }, focusWithinProps), { onKeyDown: (e) => {
85
+ if (e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
86
+ e.preventDefault();
87
+ e.stopPropagation();
88
+ setOpen(true);
89
+ }
90
+ } }),
85
91
  fieldProps: mergeProps(commonInputProps, mode === 'relative' ? relativeDateProps : inputProps, { controlRef: handleRef, error }),
86
92
  modeSwitcherProps: {
87
93
  size: getButtonSizeForInput(props.size),
@@ -0,0 +1,9 @@
1
+ import type { DateTime } from '@gravity-ui/date-utils';
2
+ interface PlaceholderValueOptions {
3
+ placeholderValue?: DateTime;
4
+ timeZone?: string;
5
+ }
6
+ export declare function createPlaceholderValue({ placeholderValue, timeZone }: PlaceholderValueOptions): DateTime;
7
+ export declare function isInvalid(value: DateTime | null | undefined, minValue: DateTime | undefined, maxValue: DateTime | undefined): boolean;
8
+ export declare function mergeDateTime(date: DateTime, time: DateTime): DateTime;
9
+ export {};
@@ -0,0 +1,22 @@
1
+ import { dateTime } from '@gravity-ui/date-utils';
2
+ export function createPlaceholderValue({ placeholderValue, timeZone }) {
3
+ return (placeholderValue !== null && placeholderValue !== void 0 ? placeholderValue : dateTime({ timeZone }).set('hour', 0).set('minute', 0).set('second', 0));
4
+ }
5
+ export function isInvalid(value, minValue, maxValue) {
6
+ if (!value) {
7
+ return false;
8
+ }
9
+ if (minValue && value.isBefore(minValue)) {
10
+ return true;
11
+ }
12
+ if (maxValue && maxValue.isBefore(value)) {
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+ export function mergeDateTime(date, time) {
18
+ return date
19
+ .set('hours', time.hour())
20
+ .set('minutes', time.minute())
21
+ .set('seconds', time.second());
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/date-components",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",