@gravity-ui/date-components 1.1.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 (37) hide show
  1. package/dist/cjs/components/Calendar/hooks/useRangeCalendarState.js +10 -5
  2. package/dist/cjs/components/DateField/hooks/useDateFieldProps.js +21 -14
  3. package/dist/cjs/components/DateField/hooks/useDateFieldState.js +16 -160
  4. package/dist/cjs/components/DateField/utils.d.ts +13 -1
  5. package/dist/cjs/components/DateField/utils.js +168 -1
  6. package/dist/cjs/components/DatePicker/DatePicker.css +6 -0
  7. package/dist/cjs/components/DatePicker/DatePicker.d.ts +2 -2
  8. package/dist/cjs/components/DatePicker/DatePicker.js +6 -33
  9. package/dist/cjs/components/DatePicker/hooks/useDatePickerProps.d.ts +20 -0
  10. package/dist/cjs/components/DatePicker/hooks/useDatePickerProps.js +134 -0
  11. package/dist/cjs/components/DatePicker/hooks/useDatePickerState.d.ts +4 -5
  12. package/dist/cjs/components/DatePicker/hooks/useDatePickerState.js +15 -0
  13. package/dist/cjs/components/DatePicker/index.d.ts +1 -0
  14. package/dist/cjs/components/DatePicker/index.js +1 -0
  15. package/dist/cjs/components/RelativeDatePicker/RelativeDatePicker.css +1 -0
  16. package/dist/cjs/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.js +29 -27
  17. package/dist/esm/components/Calendar/hooks/useRangeCalendarState.js +10 -5
  18. package/dist/esm/components/DateField/hooks/useDateFieldProps.js +21 -14
  19. package/dist/esm/components/DateField/hooks/useDateFieldState.js +14 -158
  20. package/dist/esm/components/DateField/utils.d.ts +13 -1
  21. package/dist/esm/components/DateField/utils.js +162 -0
  22. package/dist/esm/components/DatePicker/DatePicker.css +6 -0
  23. package/dist/esm/components/DatePicker/DatePicker.d.ts +2 -2
  24. package/dist/esm/components/DatePicker/DatePicker.js +8 -35
  25. package/dist/esm/components/DatePicker/hooks/useDatePickerProps.d.ts +20 -0
  26. package/dist/esm/components/DatePicker/hooks/useDatePickerProps.js +127 -0
  27. package/dist/esm/components/DatePicker/hooks/useDatePickerState.d.ts +4 -5
  28. package/dist/esm/components/DatePicker/hooks/useDatePickerState.js +15 -0
  29. package/dist/esm/components/DatePicker/index.d.ts +1 -0
  30. package/dist/esm/components/DatePicker/index.js +1 -0
  31. package/dist/esm/components/RelativeDatePicker/RelativeDatePicker.css +1 -0
  32. package/dist/esm/components/RelativeDatePicker/hooks/useRelativeDatePickerProps.js +29 -27
  33. package/package.json +8 -7
  34. package/dist/cjs/components/DatePicker/DesktopCalendar.d.ts +0 -17
  35. package/dist/cjs/components/DatePicker/DesktopCalendar.js +0 -65
  36. package/dist/esm/components/DatePicker/DesktopCalendar.d.ts +0 -17
  37. package/dist/esm/components/DatePicker/DesktopCalendar.js +0 -57
@@ -1,5 +1,17 @@
1
- import type { DateFieldSectionWithoutPosition } from './types';
1
+ import type { DateTime } from '@gravity-ui/date-utils';
2
+ import type { DateFieldSection, DateFieldSectionType, DateFieldSectionWithoutPosition } from './types';
2
3
  export declare const isMac: boolean;
3
4
  export declare const CtrlCmd: string;
4
5
  export declare function expandFormat(format: string): string;
6
+ export declare function getSectionLimits(section: DateFieldSectionWithoutPosition, date: DateTime): {
7
+ minValue: number;
8
+ maxValue: number;
9
+ } | {
10
+ minValue?: undefined;
11
+ maxValue?: undefined;
12
+ };
13
+ export declare function getSectionValue(sections: DateFieldSectionWithoutPosition, date: DateTime): number | undefined;
14
+ export declare function getDurationUnitFromSectionType(type: DateFieldSectionType): "year" | "month" | "day" | "date" | "hour" | "minute" | "second";
15
+ export declare function addSegment(section: DateFieldSection, date: DateTime, amount: number): DateTime;
16
+ export declare function setSegment(section: DateFieldSection, date: DateTime, amount: number): DateTime;
5
17
  export declare function splitFormatIntoSections(format: string): DateFieldSectionWithoutPosition[];
@@ -77,6 +77,168 @@ function getDateSectionConfigFromFormatToken(formatToken) {
77
77
  function isFourDigitYearFormat(format) {
78
78
  return dateTime().format(format).length === 4;
79
79
  }
80
+ function isHour12(format) {
81
+ return dateTime().set('hour', 15).format(format) !== '15';
82
+ }
83
+ export function getSectionLimits(section, date) {
84
+ const { type, format } = section;
85
+ switch (type) {
86
+ case 'year': {
87
+ const isFourDigit = isFourDigitYearFormat(format);
88
+ return {
89
+ minValue: isFourDigit ? 1 : 0,
90
+ maxValue: isFourDigit ? 9999 : 99,
91
+ };
92
+ }
93
+ case 'month': {
94
+ return {
95
+ minValue: 0,
96
+ maxValue: 11,
97
+ };
98
+ }
99
+ case 'weekday': {
100
+ return {
101
+ minValue: 0,
102
+ maxValue: 6,
103
+ };
104
+ }
105
+ case 'day': {
106
+ return {
107
+ minValue: 1,
108
+ maxValue: date ? date.daysInMonth() : 31,
109
+ };
110
+ }
111
+ case 'hour': {
112
+ if (isHour12(format)) {
113
+ const isPM = date.hour() >= 12;
114
+ return {
115
+ minValue: isPM ? 12 : 0,
116
+ maxValue: isPM ? 23 : 11,
117
+ };
118
+ }
119
+ return {
120
+ minValue: 0,
121
+ maxValue: 23,
122
+ };
123
+ }
124
+ case 'minute':
125
+ case 'second': {
126
+ return {
127
+ minValue: 0,
128
+ maxValue: 59,
129
+ };
130
+ }
131
+ }
132
+ return {};
133
+ }
134
+ export function getSectionValue(sections, date) {
135
+ const type = sections.type;
136
+ switch (type) {
137
+ case 'year': {
138
+ return isFourDigitYearFormat(sections.format)
139
+ ? date.year()
140
+ : Number(date.format(sections.format));
141
+ }
142
+ case 'month':
143
+ case 'hour':
144
+ case 'minute':
145
+ case 'second': {
146
+ return date[type]();
147
+ }
148
+ case 'day': {
149
+ return date.date();
150
+ }
151
+ case 'weekday': {
152
+ return date.day();
153
+ }
154
+ case 'dayPeriod': {
155
+ return date.hour() >= 12 ? 12 : 0;
156
+ }
157
+ }
158
+ return undefined;
159
+ }
160
+ const TYPE_MAPPING = {
161
+ weekday: 'day',
162
+ day: 'date',
163
+ dayPeriod: 'hour',
164
+ };
165
+ export function getDurationUnitFromSectionType(type) {
166
+ if (type === 'literal' || type === 'timeZoneName' || type === 'unknown') {
167
+ throw new Error(`${type} section does not have duration unit.`);
168
+ }
169
+ if (type in TYPE_MAPPING) {
170
+ return TYPE_MAPPING[type];
171
+ }
172
+ return type;
173
+ }
174
+ export function addSegment(section, date, amount) {
175
+ var _a;
176
+ let val = (_a = section.value) !== null && _a !== void 0 ? _a : 0;
177
+ if (section.type === 'dayPeriod') {
178
+ val = date.hour() + (date.hour() > 12 ? -12 : 12);
179
+ }
180
+ else {
181
+ val = val + amount;
182
+ const min = section.minValue;
183
+ const max = section.maxValue;
184
+ if (typeof min === 'number' && typeof max === 'number') {
185
+ const length = max - min + 1;
186
+ val = ((val - min + length) % length) + min;
187
+ }
188
+ }
189
+ if (section.type === 'year' && !isFourDigitYearFormat(section.format)) {
190
+ val = dateTime({ input: `${val}`.padStart(2, '0'), format: section.format }).year();
191
+ }
192
+ const type = getDurationUnitFromSectionType(section.type);
193
+ return date.set(type, val);
194
+ }
195
+ export function setSegment(section, date, amount) {
196
+ const type = section.type;
197
+ switch (type) {
198
+ case 'year': {
199
+ return date.set('year', isFourDigitYearFormat(section.format)
200
+ ? amount
201
+ : dateTime({
202
+ input: `${amount}`.padStart(2, '0'),
203
+ format: section.format,
204
+ }).year());
205
+ }
206
+ case 'day':
207
+ case 'weekday':
208
+ case 'month': {
209
+ return date.set(getDurationUnitFromSectionType(type), amount);
210
+ }
211
+ case 'dayPeriod': {
212
+ const hours = date.hour();
213
+ const wasPM = hours >= 12;
214
+ const isPM = amount >= 12;
215
+ if (isPM === wasPM) {
216
+ return date;
217
+ }
218
+ return date.set('hour', wasPM ? hours - 12 : hours + 12);
219
+ }
220
+ case 'hour': {
221
+ // In 12 hour time, ensure that AM/PM does not change
222
+ let sectionAmount = amount;
223
+ if (section.minValue === 12 || section.maxValue === 11) {
224
+ const hours = date.hour();
225
+ const wasPM = hours >= 12;
226
+ if (!wasPM && sectionAmount === 12) {
227
+ sectionAmount = 0;
228
+ }
229
+ if (wasPM && sectionAmount < 12) {
230
+ sectionAmount += 12;
231
+ }
232
+ }
233
+ return date.set('hour', sectionAmount);
234
+ }
235
+ case 'minute':
236
+ case 'second': {
237
+ return date.set(type, amount);
238
+ }
239
+ }
240
+ return date;
241
+ }
80
242
  function doesSectionHaveLeadingZeros(contentType, sectionType, format) {
81
243
  if (contentType !== 'digit') {
82
244
  return false;
@@ -1,6 +1,7 @@
1
1
  .g-date-date-picker {
2
2
  position: relative;
3
3
  display: inline-block;
4
+ outline: none;
4
5
  }
5
6
  .g-date-date-picker__field {
6
7
  width: 100%;
@@ -8,6 +9,11 @@
8
9
  .g-date-date-picker__field_mobile {
9
10
  pointer-events: none;
10
11
  }
12
+ .g-date-date-picker__popup-anchor {
13
+ position: absolute;
14
+ z-index: -1;
15
+ inset: 0;
16
+ }
11
17
  .g-date-date-picker__popup-content {
12
18
  outline: none;
13
19
  }
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import type { CalendarProps } from '../Calendar';
2
+ import { type CalendarProps } from '../Calendar';
3
3
  import type { AccessibilityProps, DateFieldBase, DomProps, FocusableProps, KeyboardEvents, StyleProps, TextInputProps } from '../types';
4
4
  import './DatePicker.css';
5
5
  export interface DatePickerProps extends DateFieldBase, TextInputProps, FocusableProps, KeyboardEvents, DomProps, StyleProps, AccessibilityProps {
6
6
  children?: (props: CalendarProps) => React.ReactNode;
7
7
  }
8
- export declare function DatePicker({ value, defaultValue, onUpdate, className, onFocus, onBlur, children, ...props }: DatePickerProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function DatePicker({ value, defaultValue, onUpdate, className, ...props }: DatePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -11,49 +11,22 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import React from 'react';
14
- import { TextInput, useFocusWithin, useMobile } from '@gravity-ui/uikit';
15
- import { useDateFieldProps, useDateFieldState } from '../DateField';
16
- import { DesktopCalendar, DesktopCalendarButton } from './DesktopCalendar';
14
+ import { Calendar as CalendarIcon } from '@gravity-ui/icons';
15
+ import { Button, Icon, Popup, TextInput, useMobile } from '@gravity-ui/uikit';
16
+ import { Calendar } from '../Calendar';
17
+ import { DateField } from '../DateField';
17
18
  import { MobileCalendar, MobileCalendarIcon } from './MobileCalendar';
19
+ import { useDatePickerProps } from './hooks/useDatePickerProps';
18
20
  import { useDatePickerState } from './hooks/useDatePickerState';
19
21
  import { b } from './utils';
20
22
  import './DatePicker.css';
21
23
  export function DatePicker(_a) {
22
- var { value, defaultValue, onUpdate, className, onFocus, onBlur, children } = _a, props = __rest(_a, ["value", "defaultValue", "onUpdate", "className", "onFocus", "onBlur", "children"]);
24
+ var { value, defaultValue, onUpdate, className } = _a, props = __rest(_a, ["value", "defaultValue", "onUpdate", "className"]);
23
25
  const anchorRef = React.useRef(null);
24
26
  const state = useDatePickerState(Object.assign(Object.assign({}, props), { value,
25
27
  defaultValue,
26
28
  onUpdate }));
29
+ const { groupProps, fieldProps, calendarButtonProps, popupProps, calendarProps, timeInputProps } = useDatePickerProps(state, props);
27
30
  const [isMobile] = useMobile();
28
- const [isActive, setActive] = React.useState(false);
29
- const { focusWithinProps } = useFocusWithin({
30
- onFocusWithin: onFocus,
31
- onBlurWithin: onBlur,
32
- onFocusWithinChange(isFocusWithin) {
33
- setActive(isFocusWithin);
34
- },
35
- });
36
- const fieldState = useDateFieldState({
37
- value: state.value,
38
- onUpdate: state.setValue,
39
- disabled: state.disabled,
40
- readOnly: state.readOnly,
41
- validationState: props.validationState,
42
- minValue: props.minValue,
43
- maxValue: props.maxValue,
44
- isDateUnavailable: props.isDateUnavailable,
45
- format: state.format,
46
- placeholderValue: props.placeholderValue,
47
- timeZone: props.timeZone,
48
- });
49
- const { inputProps } = useDateFieldProps(fieldState, props);
50
- return (
51
- // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
52
- _jsxs("div", Object.assign({ ref: anchorRef, className: b(null, className), style: props.style }, focusWithinProps, { role: "group", "aria-disabled": state.disabled || undefined, onKeyDown: (e) => {
53
- if (e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
54
- e.preventDefault();
55
- e.stopPropagation();
56
- state.setOpen(true);
57
- }
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 })) }))] })));
31
+ return (_jsxs("div", Object.assign({ className: b(null, className) }, groupProps, { children: [isMobile ? (_jsx(MobileCalendar, { props: props, state: state })) : (_jsx("div", { ref: anchorRef, className: b('popup-anchor'), children: _jsx(Popup, Object.assign({ anchorRef: anchorRef }, popupProps, { children: _jsxs("div", { className: b('popup-content'), children: [typeof props.children === 'function' ? (props.children(calendarProps)) : (_jsx(Calendar, Object.assign({}, calendarProps))), state.hasTime && (_jsx("div", { className: b('time-field-wrapper'), children: _jsx(DateField, Object.assign({}, timeInputProps)) }))] }) })) })), _jsx(TextInput, Object.assign({}, fieldProps, { className: b('field', { mobile: isMobile }), hasClear: !isMobile && fieldProps.hasClear, rightContent: isMobile ? (_jsx(MobileCalendarIcon, { props: props, state: state })) : (_jsx(Button, Object.assign({}, calendarButtonProps, { children: _jsx(Icon, { data: CalendarIcon }) }))) }))] })));
59
32
  }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import type { ButtonProps, PopupProps, TextInputProps } from '@gravity-ui/uikit';
3
+ import type { Calendar } from '../../Calendar';
4
+ import type { DateFieldProps } from '../../DateField';
5
+ import type { DatePickerProps } from '../DatePicker';
6
+ import type { DatePickerState } from './useDatePickerState';
7
+ interface InnerRelativeDatePickerProps {
8
+ groupProps: React.HTMLAttributes<unknown> & {
9
+ ref: React.Ref<any>;
10
+ };
11
+ fieldProps: TextInputProps;
12
+ calendarButtonProps: ButtonProps & {
13
+ ref: React.Ref<HTMLButtonElement>;
14
+ };
15
+ popupProps: PopupProps;
16
+ calendarProps: React.ComponentProps<typeof Calendar>;
17
+ timeInputProps: DateFieldProps;
18
+ }
19
+ export declare function useDatePickerProps(state: DatePickerState, { onFocus, onBlur, ...props }: DatePickerProps): InnerRelativeDatePickerProps;
20
+ export {};
@@ -0,0 +1,127 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import React from 'react';
13
+ import { useFocusWithin, useForkRef } from '@gravity-ui/uikit';
14
+ import { useDateFieldProps } from '../../DateField';
15
+ import { getButtonSizeForInput } from '../../utils/getButtonSizeForInput';
16
+ import { mergeProps } from '../../utils/mergeProps';
17
+ import { i18n } from '../i18n';
18
+ export function useDatePickerProps(state, _a) {
19
+ var _b;
20
+ var { onFocus, onBlur } = _a, props = __rest(_a, ["onFocus", "onBlur"]);
21
+ const [isActive, setActive] = React.useState(false);
22
+ const { focusWithinProps } = useFocusWithin({
23
+ onFocusWithin: onFocus,
24
+ onBlurWithin: onBlur,
25
+ onFocusWithinChange(isFocusWithin) {
26
+ setActive(isFocusWithin);
27
+ if (!isFocusWithin) {
28
+ state.setOpen(false);
29
+ }
30
+ },
31
+ });
32
+ const { inputProps } = useDateFieldProps(state.dateFieldState, props);
33
+ let error;
34
+ let validationState = props.validationState;
35
+ if (validationState) {
36
+ error = validationState === 'invalid' ? props.errorMessage || true : undefined;
37
+ }
38
+ else {
39
+ validationState = state.dateFieldState.validationState;
40
+ error = validationState === 'invalid';
41
+ }
42
+ const inputRef = React.useRef(null);
43
+ const handleRef = useForkRef(inputRef, inputProps.controlRef);
44
+ const calendarRef = React.useRef(null);
45
+ const calendarButtonRef = React.useRef(null);
46
+ const groupRef = React.useRef(null);
47
+ function focusInput() {
48
+ setTimeout(() => {
49
+ var _a;
50
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
51
+ });
52
+ }
53
+ return {
54
+ groupProps: Object.assign(Object.assign({ ref: groupRef, tabIndex: -1, role: 'group' }, focusWithinProps), { style: props.style, 'aria-disabled': state.disabled || undefined, onKeyDown: (e) => {
55
+ if (e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
56
+ e.preventDefault();
57
+ e.stopPropagation();
58
+ state.setOpen(true);
59
+ }
60
+ } }),
61
+ fieldProps: mergeProps(inputProps, state.dateFieldState.isEmpty && !isActive && props.placeholder
62
+ ? { value: '' }
63
+ : undefined, { controlRef: handleRef, error }),
64
+ calendarButtonProps: {
65
+ ref: calendarButtonRef,
66
+ size: getButtonSizeForInput(props.size),
67
+ disabled: state.disabled,
68
+ extraProps: {
69
+ 'aria-label': i18n('Calendar'),
70
+ 'aria-haspopup': 'dialog',
71
+ 'aria-expanded': state.isOpen,
72
+ },
73
+ view: 'flat-secondary',
74
+ onClick: () => {
75
+ setActive(true);
76
+ state.setOpen(!state.isOpen);
77
+ },
78
+ },
79
+ popupProps: {
80
+ open: state.isOpen,
81
+ onEscapeKeyDown: () => {
82
+ state.setOpen(false);
83
+ focusInput();
84
+ },
85
+ onOutsideClick: (e) => {
86
+ var _a;
87
+ if (e.target !== calendarButtonRef.current) {
88
+ state.setOpen(false);
89
+ }
90
+ if (e.target && ((_a = groupRef.current) === null || _a === void 0 ? void 0 : _a.contains(e.target))) {
91
+ focusInput();
92
+ }
93
+ },
94
+ // @ts-expect-error focusTrap in popup was introduced in a newer version of uikit
95
+ focusTrap: true,
96
+ },
97
+ calendarProps: {
98
+ ref: calendarRef,
99
+ autoFocus: true,
100
+ size: props.size === 's' ? 'm' : props.size,
101
+ disabled: props.disabled,
102
+ readOnly: props.readOnly,
103
+ onUpdate: (d) => {
104
+ state.setDateValue(d);
105
+ if (!state.hasTime) {
106
+ focusInput();
107
+ }
108
+ },
109
+ defaultFocusedValue: (_b = state.dateValue) !== null && _b !== void 0 ? _b : undefined,
110
+ value: state.dateValue,
111
+ minValue: props.minValue,
112
+ maxValue: props.maxValue,
113
+ isDateUnavailable: props.isDateUnavailable,
114
+ timeZone: props.timeZone,
115
+ },
116
+ timeInputProps: {
117
+ value: state.timeValue,
118
+ onUpdate: state.setTimeValue,
119
+ format: state.timeFormat,
120
+ readOnly: state.readOnly,
121
+ disabled: state.disabled,
122
+ timeZone: props.timeZone,
123
+ hasClear: props.hasClear,
124
+ size: props.size,
125
+ },
126
+ };
127
+ }
@@ -1,5 +1,6 @@
1
1
  import type { DateTime } from '@gravity-ui/date-utils';
2
- import type { InputBase, ValueBase } from '../../types';
2
+ import type { DateFieldState } from '../../DateField';
3
+ import type { DateFieldBase } from '../../types';
3
4
  export type Granularity = 'day' | 'hour' | 'minute' | 'second';
4
5
  export interface DatePickerState {
5
6
  /** The currently selected date. */
@@ -36,10 +37,8 @@ export interface DatePickerState {
36
37
  isOpen: boolean;
37
38
  /** Sets whether the calendar popover is open. */
38
39
  setOpen(isOpen: boolean): void;
40
+ dateFieldState: DateFieldState;
39
41
  }
40
- export interface DatePickerStateOptions extends ValueBase<DateTime>, InputBase {
41
- placeholderValue?: DateTime;
42
- timeZone?: string;
43
- format?: string;
42
+ export interface DatePickerStateOptions extends DateFieldBase {
44
43
  }
45
44
  export declare function useDatePickerState(props: DatePickerStateOptions): DatePickerState;
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { useDateFieldState } from '../../DateField';
2
3
  import { splitFormatIntoSections } from '../../DateField/utils';
3
4
  import { useControlledState } from '../../hooks/useControlledState';
4
5
  import { createPlaceholderValue, mergeDateTime } from '../../utils/dates';
@@ -75,6 +76,19 @@ export function useDatePickerState(props) {
75
76
  setSelectedTime(newValue);
76
77
  }
77
78
  };
79
+ const dateFieldState = useDateFieldState({
80
+ value: value !== null && value !== void 0 ? value : null,
81
+ onUpdate: setValue,
82
+ disabled,
83
+ readOnly,
84
+ validationState: props.validationState,
85
+ minValue: props.minValue,
86
+ maxValue: props.maxValue,
87
+ isDateUnavailable: props.isDateUnavailable,
88
+ format,
89
+ placeholderValue: props.placeholderValue,
90
+ timeZone: props.timeZone,
91
+ });
78
92
  return {
79
93
  value: value !== null && value !== void 0 ? value : null,
80
94
  setValue,
@@ -95,6 +109,7 @@ export function useDatePickerState(props) {
95
109
  }
96
110
  setOpen(newIsOpen);
97
111
  },
112
+ dateFieldState,
98
113
  };
99
114
  }
100
115
  function getPlaceholderTime(placeholderValue, timeZone) {
@@ -1,2 +1,3 @@
1
1
  export * from './DatePicker';
2
2
  export * from './hooks/useDatePickerState';
3
+ export * from './hooks/useDatePickerProps';
@@ -1,2 +1,3 @@
1
1
  export * from './DatePicker';
2
2
  export * from './hooks/useDatePickerState';
3
+ export * from './hooks/useDatePickerProps';
@@ -1,6 +1,7 @@
1
1
  .g-date-relative-date-picker {
2
2
  position: relative;
3
3
  display: inline-flex;
4
+ outline: none;
4
5
  }
5
6
  .g-date-relative-date-picker__field {
6
7
  width: 100%;
@@ -36,7 +36,7 @@ export function useRelativeDatePickerProps(state, _a) {
36
36
  onBlurWithin: onBlur,
37
37
  onFocusWithinChange(isFocusWithin) {
38
38
  if (!isFocusWithin) {
39
- state.setActive(isFocusWithin);
39
+ state.setActive(false);
40
40
  }
41
41
  },
42
42
  });
@@ -44,16 +44,12 @@ export function useRelativeDatePickerProps(state, _a) {
44
44
  if (!state.isActive && isOpen) {
45
45
  setOpen(false);
46
46
  }
47
- const [prevActive, setPrevActive] = React.useState(state.isActive);
48
- if (prevActive !== state.isActive) {
49
- setPrevActive(state.isActive);
50
- if (state.isActive && !isOpen) {
51
- setOpen(true);
52
- }
53
- }
54
47
  const commonInputProps = {
55
48
  onFocus: () => {
56
- state.setActive(true);
49
+ if (!state.isActive) {
50
+ state.setActive(true);
51
+ setOpen(true);
52
+ }
57
53
  },
58
54
  };
59
55
  const { inputProps } = useDateFieldProps(dateFieldState, Object.assign(Object.assign({}, props), { value: undefined, defaultValue: undefined, onUpdate: undefined }));
@@ -62,6 +58,7 @@ export function useRelativeDatePickerProps(state, _a) {
62
58
  value: relativeDateState.text,
63
59
  onUpdate: relativeDateState.setText,
64
60
  hasClear: props.hasClear && !relativeDateState.readOnly,
61
+ placeholder: props.placeholder,
65
62
  size: props.size,
66
63
  };
67
64
  let error;
@@ -76,19 +73,33 @@ export function useRelativeDatePickerProps(state, _a) {
76
73
  : dateFieldState.validationState;
77
74
  error = validationState === 'invalid';
78
75
  }
79
- const wasActiveBeforeClickRef = React.useRef(state.isActive);
80
76
  const inputRef = React.useRef(null);
81
77
  const handleRef = useForkRef(inputRef, mode === 'relative' ? relativeDateProps.controlRef : inputProps.controlRef);
82
78
  const calendarRef = React.useRef(null);
79
+ function focusCalendar() {
80
+ setTimeout(() => {
81
+ var _a;
82
+ (_a = calendarRef.current) === null || _a === void 0 ? void 0 : _a.focus();
83
+ });
84
+ }
85
+ function focusInput() {
86
+ setTimeout(() => {
87
+ var _a;
88
+ (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
89
+ });
90
+ }
83
91
  return {
84
- groupProps: Object.assign(Object.assign({ role: 'group' }, focusWithinProps), { onKeyDown: (e) => {
92
+ groupProps: Object.assign(Object.assign({ tabIndex: -1, role: 'group' }, focusWithinProps), { onKeyDown: (e) => {
85
93
  if (e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
86
94
  e.preventDefault();
87
95
  e.stopPropagation();
88
96
  setOpen(true);
97
+ focusCalendar();
89
98
  }
90
99
  } }),
91
- fieldProps: mergeProps(commonInputProps, mode === 'relative' ? relativeDateProps : inputProps, { controlRef: handleRef, error }),
100
+ fieldProps: mergeProps(commonInputProps, mode === 'relative' ? relativeDateProps : inputProps, mode === 'absolute' && dateFieldState.isEmpty && !state.isActive && props.placeholder
101
+ ? { value: '' }
102
+ : undefined, { controlRef: handleRef, error }),
92
103
  modeSwitcherProps: {
93
104
  size: getButtonSizeForInput(props.size),
94
105
  disabled: state.readOnly || state.disabled,
@@ -109,7 +120,7 @@ export function useRelativeDatePickerProps(state, _a) {
109
120
  else if (relativeDateState.parsedDate) {
110
121
  setFocusedDate(relativeDateState.parsedDate);
111
122
  }
112
- setTimeout(() => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); });
123
+ focusInput();
113
124
  },
114
125
  },
115
126
  calendarButtonProps: {
@@ -121,29 +132,20 @@ export function useRelativeDatePickerProps(state, _a) {
121
132
  'aria-expanded': isOpen,
122
133
  },
123
134
  view: 'flat-secondary',
124
- onFocus: () => {
125
- wasActiveBeforeClickRef.current = state.isActive;
126
- },
127
135
  onClick: () => {
128
136
  state.setActive(true);
129
- if (wasActiveBeforeClickRef.current) {
130
- setOpen(!isOpen);
131
- if (!isOpen) {
132
- setTimeout(() => {
133
- var _a;
134
- (_a = calendarRef.current) === null || _a === void 0 ? void 0 : _a.focus();
135
- });
136
- }
137
+ setOpen(!isOpen);
138
+ if (!isOpen) {
139
+ focusCalendar();
137
140
  }
138
- wasActiveBeforeClickRef.current = state.isActive;
139
141
  },
140
142
  },
141
143
  popupProps: {
142
144
  open: isOpen,
143
145
  onEscapeKeyDown: () => {
144
146
  setOpen(false);
147
+ focusInput();
145
148
  },
146
- restoreFocus: true,
147
149
  },
148
150
  calendarProps: {
149
151
  ref: calendarRef,
@@ -154,7 +156,7 @@ export function useRelativeDatePickerProps(state, _a) {
154
156
  datePickerState.setDateValue(v);
155
157
  if (!state.datePickerState.hasTime) {
156
158
  setOpen(false);
157
- setTimeout(() => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); });
159
+ focusInput();
158
160
  }
159
161
  },
160
162
  focusedValue: focusedDate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/date-components",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "main": "dist/cjs/index.js",
@@ -60,12 +60,13 @@
60
60
  "@gravity-ui/stylelint-config": "^2.0.0",
61
61
  "@gravity-ui/tsconfig": "^1.0.0",
62
62
  "@gravity-ui/uikit": "^5.0.0",
63
- "@storybook/addon-a11y": "^7.2.1",
64
- "@storybook/addon-essentials": "^7.2.1",
65
- "@storybook/api": "^7.2.1",
63
+ "@storybook/addon-a11y": "^7.5.3",
64
+ "@storybook/addon-essentials": "^7.5.3",
65
+ "@storybook/addons": "^7.5.3",
66
+ "@storybook/api": "^7.5.3",
66
67
  "@storybook/preset-scss": "^1.0.3",
67
- "@storybook/react": "^7.2.1",
68
- "@storybook/react-webpack5": "^7.2.1",
68
+ "@storybook/react": "^7.5.3",
69
+ "@storybook/react-webpack5": "^7.5.3",
69
70
  "@testing-library/jest-dom": "^5.17.0",
70
71
  "@testing-library/react": "^14.0.0",
71
72
  "@testing-library/user-event": "^14.4.3",
@@ -91,7 +92,7 @@
91
92
  "react-dom": "^18.2.0",
92
93
  "sass": "^1.64.1",
93
94
  "sass-loader": "^13.3.2",
94
- "storybook": "^7.2.1",
95
+ "storybook": "^7.5.3",
95
96
  "style-loader": "^3.3.3",
96
97
  "stylelint": "^15.10.2",
97
98
  "ts-jest": "^29.1.1",