@via-profit/ui-kit 0.2.6 → 0.3.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.
@@ -52,6 +52,34 @@ const Autocomplete = react_1.default.forwardRef((props, ref) => {
52
52
  react_1.default.useImperativeHandle(ref, () => ({
53
53
  clear: () => clear(),
54
54
  }), [clear]);
55
+ const applyFilterForItems = react_1.default.useCallback((inputValue, itemList) => {
56
+ const query = inputValue.toLowerCase().trim();
57
+ // Apply filter
58
+ return typeof filterItems !== 'function' || query.length === 0
59
+ ? itemList
60
+ : filterItems(itemList, { query, inputValue });
61
+ }, [filterItems]);
62
+ /**
63
+ * Restore the input text from the current value (used by clearOnBlur)
64
+ */
65
+ const restoreInputValue = react_1.default.useCallback(() => {
66
+ const newInputValue = currentValue === null
67
+ ? ''
68
+ : selectedItemToString(currentValue);
69
+ const newFilteredItems = multiple ? filteredItems : applyFilterForItems(newInputValue, items);
70
+ dispatch((0, context_1.actionSetPartial)({
71
+ filteredItems: newFilteredItems,
72
+ inputValue: newInputValue,
73
+ }));
74
+ }, [
75
+ currentValue,
76
+ selectedItemToString,
77
+ multiple,
78
+ filteredItems,
79
+ applyFilterForItems,
80
+ items,
81
+ dispatch,
82
+ ]);
55
83
  const inputKeydownEvent = react_1.default.useCallback((event) => {
56
84
  var _a, _b, _c, _d, _e;
57
85
  if (!isFocusedRef.current) {
@@ -98,8 +126,16 @@ const Autocomplete = react_1.default.forwardRef((props, ref) => {
98
126
  // case 'PageDown':
99
127
  // event.preventDefault();
100
128
  // break;
101
- case 'Escape':
102
129
  case 'Tab':
130
+ // Focus leaves the field, so clearOnBlur applies as for the outside click
131
+ if (clearOnBlur) {
132
+ restoreInputValue();
133
+ }
134
+ if (currentOpen) {
135
+ onRequestClose(event);
136
+ }
137
+ break;
138
+ case 'Escape':
103
139
  if (currentOpen) {
104
140
  onRequestClose(event);
105
141
  }
@@ -108,14 +144,14 @@ const Autocomplete = react_1.default.forwardRef((props, ref) => {
108
144
  // do nothing
109
145
  break;
110
146
  }
111
- }, [currentOpen, onRequestClose, onRequestOpen, filteredItems.length]);
112
- const applyFilterForItems = react_1.default.useCallback((inputValue, itemList) => {
113
- const query = inputValue.toLowerCase().trim();
114
- // Apply filter
115
- return typeof filterItems !== 'function' || query.length === 0
116
- ? itemList
117
- : filterItems(itemList, { query, inputValue });
118
- }, [filterItems]);
147
+ }, [
148
+ currentOpen,
149
+ onRequestClose,
150
+ onRequestOpen,
151
+ filteredItems.length,
152
+ clearOnBlur,
153
+ restoreInputValue,
154
+ ]);
119
155
  const renderChildren = react_1.default.useCallback(({ index, item }, itemProps) => children({ index, item: item, inputValue }, itemProps), [children, inputValue]);
120
156
  /**
121
157
  * Only affected value change action
@@ -178,16 +214,7 @@ const Autocomplete = react_1.default.forwardRef((props, ref) => {
178
214
  // Click outside
179
215
  if (needToClose && currentOpen) {
180
216
  if (clearOnBlur) {
181
- const newInputValue = currentValue === null
182
- ? ''
183
- : selectedItemToString(currentValue);
184
- const newFilteredItems = multiple
185
- ? filteredItems
186
- : applyFilterForItems(newInputValue, items);
187
- dispatch((0, context_1.actionSetPartial)({
188
- filteredItems: newFilteredItems,
189
- inputValue: newInputValue,
190
- }));
217
+ restoreInputValue();
191
218
  }
192
219
  if (isOpen) {
193
220
  onRequestClose(event);
@@ -198,22 +225,7 @@ const Autocomplete = react_1.default.forwardRef((props, ref) => {
198
225
  return () => {
199
226
  window.document.removeEventListener(ClickOutside_1.mouseEventMap.onMouseDown, mouseDownEvent);
200
227
  };
201
- }, [
202
- onRequestClose,
203
- anchorElement,
204
- filteredItems,
205
- items,
206
- isOpen,
207
- clearOnBlur,
208
- currentOpen,
209
- dispatch,
210
- value,
211
- selectedItemToString,
212
- currentValue,
213
- applyFilterForItems,
214
- inputValue,
215
- multiple,
216
- ]);
228
+ }, [onRequestClose, anchorElement, isOpen, clearOnBlur, currentOpen, restoreInputValue]);
217
229
  const onSelectMenuItem = react_1.default.useCallback(item => {
218
230
  if (!multiple) {
219
231
  dispatch({
@@ -35,13 +35,15 @@ __exportStar(require("./AutocompleteContainer"), exports);
35
35
  __exportStar(require("./AutocompleteItem"), exports);
36
36
  const Autocomplete = react_1.default.forwardRef((props, ref) => {
37
37
  const { children, items, filterItems, isOpen, multiple, selectedItemToString, value } = props, restProps = __rest(props, ["children", "items", "filterItems", "isOpen", "multiple", "selectedItemToString", "value"]);
38
- return (react_1.default.createElement(context_1.ContextProvider, { initialState: (0, context_1.createInitialState)({
39
- items,
40
- selectedItemToString,
41
- filterItems,
42
- value,
43
- isOpen,
44
- }) },
38
+ // The initial state is used only on mount, so do not recompute it (and run filterItems) on every render
39
+ const [initialState] = react_1.default.useState(() => (0, context_1.createInitialState)({
40
+ items,
41
+ selectedItemToString,
42
+ filterItems,
43
+ value,
44
+ isOpen,
45
+ }));
46
+ return (react_1.default.createElement(context_1.ContextProvider, { initialState: initialState },
45
47
  react_1.default.createElement(AutocompleteContainer_1.default, Object.assign({}, restProps, { items: items, filterItems: filterItems, isOpen: isOpen, multiple: multiple, selectedItemToString: selectedItemToString, value: value, ref: ref }), children)));
46
48
  });
47
49
  Autocomplete.displayName = 'Autocomplete';
@@ -39,7 +39,7 @@ const Autocomplete2 = React.forwardRef((props, ref) => {
39
39
  * Filter items list by input value
40
40
  */
41
41
  const filterItems = React.useCallback((items, data) => {
42
- const { query } = data;
42
+ const query = data.query.toLocaleLowerCase();
43
43
  return items.filter(item => item.label.toLocaleLowerCase().indexOf(query) !== -1);
44
44
  }, []);
45
45
  /**
package/CHANGELOG.md CHANGED
@@ -0,0 +1,38 @@
1
+ # Список изменений
2
+
3
+ Все заметные изменения пакета `@via-profit/ui-kit`. Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/), версии следуют [семантическому версионированию](https://semver.org/lang/ru/).
4
+
5
+ ## Не выпущено
6
+
7
+ ### Изменено
8
+
9
+ - **Calendar**: номера недель при начале недели с понедельника считаются по ISO 8601; при другом первом дне недели первой считается неделя, содержащая 1 января.
10
+ - **Calendar**: свойство `markToday` по умолчанию `true`, как указано в документации.
11
+ - **Selectbox**: поле отображается и при пустом списке `items` (раньше компонент не рендерился вовсе).
12
+ - **DatePicker**: добавлено свойство `todayButtonLabel`; `toodayButtonLabel` оставлено как устаревший псевдоним.
13
+
14
+ ### Исправлено
15
+
16
+ - **Selectbox**: типы в опубликованном пакете ссылались на несуществующий модуль `Selectbox`.
17
+ - **Selectbox**: `label` не был связан с кнопкой, `onFocus` не вызывался, звёздочка отображалась при `requiredAsterisk={false}`.
18
+ - **Popper**: фактическое положение после автоматического переворота сбрасывалось к `anchorPos`; поповер, открытый сразу при монтировании, оставался невидимым; положение не пересчитывалось при прокрутке вложенных контейнеров и изменении размеров.
19
+ - **Menu**: в режиме `multiple` нельзя было снять выбор, если элементы `value` — копии объектов из `items`; `onRequestClose` вызывался дважды.
20
+ - **ClickOutside**: собственный `ref` дочернего элемента перезаписывался.
21
+ - **TextField**, **TextArea**: идентификаторы генерировались случайно и не совпадали при серверном рендеринге; `TextArea` показывала звёздочку при `requiredAsterisk={false}`.
22
+ - **Autocomplete**: `clearOnBlur` не срабатывал при уходе из поля по Tab.
23
+ - **Calendar**: первый клик в режиме диапазона не вызывал `onChange`; кнопка «Сегодня» создавала неверный диапазон; ограничения `minDate`/`maxDate` сравнивались с точностью до миллисекунд (при `minDate={new Date()}` сегодняшний день был недоступен); список месяцев строился для текущего года вместо отображаемого.
24
+ - **DatePicker**: поле не очищалось при `value={null}`; без значения в календаре подсвечивалась сегодняшняя дата; свойства `overrides`, `markToday` и `weekDayLabelFormat` игнорировались; шаблоны с `D` и `Y` не разбирались, двузначный год превращался в 19xx, некорректные даты (31.02) принимались.
25
+ - **MaskedField**: курсор смещался при редактировании в середине значения, а часть введённого текста терялась; маски, заканчивающиеся литералом, никогда не считались заполненными.
26
+ - **Swiper**: после клика без перетаскивания переставали работать автопрокрутка и `goToIndex`.
27
+ - **Modal**: окно пропадало при повторном открытии во время анимации закрытия; использовались значения `onRequestClose` и других свойств из первого рендера; прокрутка страницы оставалась заблокированной после размонтирования открытого окна.
28
+ - **TabManager**: порядок перехода по Tab не совпадал с браузерным, учитывались отключённые элементы, не учитывался фокус, поставленный мышью.
29
+ - **Color**: парсер не поддерживал проценты, отрицательный оттенок и `transparent`.
30
+
31
+ ## 0.2.6 — 2026-09-09
32
+
33
+ Основные изменения с версии 0.1.2 (подробный список не вёлся):
34
+
35
+ - **Calendar**: режим выбора диапазона дат, обработка `null` в режиме диапазона.
36
+ - **Swiper**: новый компонент, в том числе с бесконечной прокруткой.
37
+ - **Menu**: виртуализированный список.
38
+ - **Popper**: переработка позиционирования, исправления производительности и расчёта положения.
@@ -135,7 +135,7 @@ const computeCalendarDate = (params) => {
135
135
  };
136
136
  const CalendarComponent = react_1.default.forwardRef((props, ref) => {
137
137
  var _a;
138
- const { minDate = new Date(new Date().getFullYear() - 100, 0, 1, 0, 0, 0), maxDate = new Date(new Date().getFullYear() + 100, 0, 1, 0, 0, 0), weekStartDay = 'monday', locale = 'ru-RU', displayLeadingZero = false, onChange, badges = [], markToday, weekDayLabelFormat = 'short', accentColor = 'primary', defaultValue, value: inputValue, initialView: inputInitialView, view: inputView, views: inputViews, resetButtonLabel, todayButtonLabel, heading, subheading, footer, overrides, range = false, } = props;
138
+ const { minDate: inputMinDate, maxDate: inputMaxDate, weekStartDay = 'monday', locale = 'ru-RU', displayLeadingZero = false, onChange, badges = [], markToday = true, weekDayLabelFormat = 'short', accentColor = 'primary', defaultValue, value: inputValue, initialView: inputInitialView, view: inputView, views: inputViews, resetButtonLabel, todayButtonLabel, heading, subheading, footer, overrides, range = false, } = props;
139
139
  /**
140
140
  * Validations
141
141
  */
@@ -160,12 +160,21 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
160
160
  throw new Error('The `weeks` view allowed only in range mode');
161
161
  }
162
162
  }
163
- if (!inputValue && typeof onChange === 'undefined') {
163
+ if (typeof inputValue !== 'undefined' && typeof onChange !== 'function') {
164
164
  throw new Error('You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange`.');
165
165
  }
166
166
  if (inputView && inputInitialView) {
167
167
  throw new Error('You can not use view and initialView at the same time.');
168
168
  }
169
+ // Default limits are created once, otherwise every render gets new Date instances
170
+ // and invalidates all memoized values depending on them
171
+ const minDate = react_1.default.useMemo(() => inputMinDate !== null && inputMinDate !== void 0 ? inputMinDate : new Date(new Date().getFullYear() - 100, 0, 1, 0, 0, 0), [inputMinDate]);
172
+ const maxDate = react_1.default.useMemo(() => inputMaxDate !== null && inputMaxDate !== void 0 ? inputMaxDate : new Date(new Date().getFullYear() + 100, 0, 1, 0, 0, 0), [inputMaxDate]);
173
+ const emitChange = react_1.default.useCallback((newValue) => {
174
+ if (typeof onChange === 'function') {
175
+ onChange(newValue);
176
+ }
177
+ }, [onChange]);
169
178
  /**
170
179
  * Overrides
171
180
  */
@@ -221,15 +230,26 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
221
230
  const setViews = react_1.default.useCallback((variants) => setComputedViews(variants), []);
222
231
  const selectView = react_1.default.useCallback((view) => {
223
232
  var _a;
224
- if (!views.includes(view)) {
225
- setViews([...views, view]);
233
+ const nextViews = views.includes(view) ? views : [...views, view];
234
+ if (nextViews !== views) {
235
+ setViews(nextViews);
226
236
  }
227
237
  setView(view);
228
- const index = views.findIndex(v => v === view);
238
+ const index = nextViews.findIndex(v => v === view);
229
239
  if (index > -1) {
230
240
  (_a = swiperRef.current) === null || _a === void 0 ? void 0 : _a.goToIndex(index);
231
241
  }
232
242
  }, [setViews, views]);
243
+ /**
244
+ * Controlled `view` property was changed
245
+ */
246
+ const inputViewRef = react_1.default.useRef(inputView);
247
+ react_1.default.useEffect(() => {
248
+ if (inputView && inputView !== inputViewRef.current) {
249
+ inputViewRef.current = inputView;
250
+ selectView(inputView);
251
+ }
252
+ }, [inputView, selectView]);
233
253
  const resetVariablesRef = react_1.default.useRef({
234
254
  calendarDate,
235
255
  value,
@@ -292,10 +312,10 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
292
312
  }
293
313
  if (view === 'months') {
294
314
  if (type === 'prev') {
295
- newCalendarDate.setFullYear(newCalendarDate.getFullYear() + 1);
315
+ newCalendarDate.setFullYear(newCalendarDate.getFullYear() - 1);
296
316
  }
297
317
  else {
298
- newCalendarDate.setFullYear(newCalendarDate.getFullYear() - 1);
318
+ newCalendarDate.setFullYear(newCalendarDate.getFullYear() + 1);
299
319
  }
300
320
  }
301
321
  if (view === 'days' || view === 'weeks') {
@@ -316,45 +336,29 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
316
336
  }
317
337
  // Single-date mode
318
338
  if (!range) {
319
- onChange(selectedDate);
339
+ emitChange(selectedDate);
320
340
  setValue(selectedDate);
321
341
  return;
322
342
  }
323
343
  // Range-date mode
324
344
  const [from, to] = (_a = value) !== null && _a !== void 0 ? _a : [];
325
- // начало нового периода
326
- if (from && to) {
327
- setValue([selectedDate, null]);
328
- onChange([selectedDate, null]);
329
- return;
330
- }
331
- // если выбрана только первая дата или обе даты равны - завершение периода
332
- if ((from && to === null) || (from && to && from.getTime() === to.getTime())) {
345
+ // выбрана только первая дата - завершение периода
346
+ if (from && !to) {
333
347
  /// check to swap
334
- if (selectedDate.getTime() < from.getTime()) {
335
- onChange([selectedDate, from]);
336
- setValue([selectedDate, from]);
337
- }
338
- else {
339
- onChange([from, selectedDate]);
340
- setValue([from, selectedDate]);
341
- }
342
- }
343
- else {
344
- // обе даты выбраны или ничего не выбрано ([Date, Date] | [null, null] | null) - начало нового периода
345
- if ((from && to) || (from === null && to === null) || value === null) {
346
- setValue([selectedDate, null]);
347
- return;
348
- }
348
+ const newValue = selectedDate.getTime() < from.getTime() ? [selectedDate, from] : [from, selectedDate];
349
+ emitChange(newValue);
350
+ setValue(newValue);
351
+ return;
349
352
  }
350
- }, [onChange, getNextAvailableView, range, value]);
353
+ // обе даты выбраны или ничего не выбрано ([Date, Date] | [null, null] | null) - начало нового периода
354
+ setValue([selectedDate, null]);
355
+ emitChange([selectedDate, null]);
356
+ }, [emitChange, getNextAvailableView, range, value]);
351
357
  /**
352
358
  * Handle of click on year item
353
359
  */
354
360
  const handleYearSelected = react_1.default.useCallback((selectedYear) => () => {
355
- const newDate = new Date(calendarDate);
356
- newDate.setFullYear(selectedYear, 0, 1);
357
- newDate.setHours(0, 0, 0);
361
+ const newDate = new Date(selectedYear, 0, 1, 0, 0, 0, 0);
358
362
  setCalendarDate(newDate);
359
363
  const nextView = getNextAvailableView();
360
364
  if (nextView) {
@@ -362,9 +366,7 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
362
366
  }
363
367
  // if the next `view` does not exist
364
368
  if (!nextView && typeof onChange === 'function') {
365
- const a = new Date();
366
- a.setFullYear(newDate.getFullYear(), 11, 31);
367
- a.setHours(23, 59, 59);
369
+ const a = new Date(newDate.getFullYear(), 11, 31, 23, 59, 59, 999);
368
370
  const selectedValue = range ? [newDate, a] : newDate;
369
371
  onChange(selectedValue);
370
372
  if (!inputValue) {
@@ -376,9 +378,7 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
376
378
  * Handle of click on month item
377
379
  */
378
380
  const handleMonthSelected = react_1.default.useCallback((monthIndex) => () => {
379
- const newDate = new Date();
380
- newDate.setFullYear(calendarDate.getFullYear(), monthIndex, 1);
381
- newDate.setHours(0, 0, 0);
381
+ const newDate = new Date(calendarDate.getFullYear(), monthIndex, 1, 0, 0, 0, 0);
382
382
  setCalendarDate(newDate);
383
383
  const nextView = getNextAvailableView();
384
384
  if (nextView) {
@@ -386,9 +386,8 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
386
386
  }
387
387
  // if the next `view` does not exist
388
388
  if (!nextView && typeof onChange === 'function') {
389
- const a = new Date();
390
- a.setFullYear(newDate.getFullYear(), newDate.getMonth() + 1, 0);
391
- a.setHours(0, 0, 0);
389
+ // The end of the last day of the month, the same way as for the year range
390
+ const a = new Date(newDate.getFullYear(), newDate.getMonth() + 1, 0, 23, 59, 59, 999);
392
391
  const selectedValue = range ? [newDate, a] : newDate;
393
392
  onChange(selectedValue);
394
393
  if (!inputValue) {
@@ -411,12 +410,16 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
411
410
  * Handle click on «Today» button
412
411
  */
413
412
  const handleToday = react_1.default.useCallback(() => {
414
- const today = new Date();
415
- today.setHours(0, 0, 0);
413
+ const now = new Date();
414
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
415
+ const newValue = (range ? [today, today] : today);
416
416
  setCalendarDate(today);
417
- setValue((range ? [today, today] : today));
418
- handleCellDateClick(today)();
419
- }, [handleCellDateClick, range]);
417
+ setValue(newValue);
418
+ emitChange(newValue);
419
+ if (view !== 'days' && views.includes('days')) {
420
+ selectView('days');
421
+ }
422
+ }, [emitChange, range, selectView, view, views]);
420
423
  const handleClickWeek = react_1.default.useCallback((week) => () => {
421
424
  const days = week.days;
422
425
  const dates = [days[0].date, days[days.length - 1].date];
@@ -445,7 +448,7 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
445
448
  }, [views, range, selectView, onChange]);
446
449
  const weeks = react_1.default.useMemo(() => getWeeks(calendarDate), [calendarDate, getWeeks]);
447
450
  const yearsRange = react_1.default.useMemo(() => getYearsRange(minDate, maxDate), [minDate, maxDate, getYearsRange]);
448
- const monthsRange = react_1.default.useMemo(() => getMonthsRange(minDate, maxDate), [minDate, maxDate, getMonthsRange]);
451
+ const monthsRange = react_1.default.useMemo(() => getMonthsRange(minDate, maxDate, calendarDate.getFullYear()), [minDate, maxDate, getMonthsRange, calendarDate]);
449
452
  const renderViewDays = react_1.default.useCallback(() => (react_1.default.createElement(overridesMap.DateContainer, null, getWeeks(calendarDate).map(week => (react_1.default.createElement(overridesMap.WeekRow, { key: week.weekNumber }, week.days.map(day => {
450
453
  if (day.date.getMonth() === calendarDate.getMonth()) {
451
454
  const badge = badges.find(b => isSameDay(b.date, day.date));
@@ -481,7 +484,7 @@ const CalendarComponent = react_1.default.forwardRef((props, ref) => {
481
484
  ]);
482
485
  const renderViewMonths = react_1.default.useCallback(() => (react_1.default.createElement(overridesMap.MonthsSelector, null, monthsRange.map(monthIndex => {
483
486
  const isSelected = calendarDate.getMonth() === monthIndex;
484
- return (react_1.default.createElement(overridesMap.MonthCell, { key: monthIndex + monthsRange[monthIndex], accentColor: accentColor, isSelected: isSelected, onClick: handleMonthSelected(monthIndex) }, getMonthLabel(new Date(calendarDate.getFullYear(), monthIndex, 1, 0, 0, 0, 0))));
487
+ return (react_1.default.createElement(overridesMap.MonthCell, { key: monthIndex, accentColor: accentColor, isSelected: isSelected, onClick: handleMonthSelected(monthIndex) }, getMonthLabel(new Date(calendarDate.getFullYear(), monthIndex, 1, 0, 0, 0, 0))));
485
488
  }))), [accentColor, calendarDate, getMonthLabel, handleMonthSelected, monthsRange, overridesMap]);
486
489
  const renderViewYears = react_1.default.useCallback(() => (react_1.default.createElement(overridesMap.YearsSelector, { accentColor: accentColor, date: calendarDate, onChange: y => handleYearSelected(y)(), years: yearsRange, locale: locale, minDate: minDate, maxDate: maxDate })), [
487
490
  accentColor,
@@ -61,9 +61,10 @@ export type UseCalendarPayload = {
61
61
  */
62
62
  getYearsRange: (minDate: Date, maxDate: Date) => number[];
63
63
  /**
64
- * Returns range of monthes (array of month indexes) between passed dates
64
+ * Returns range of monthes (array of month indexes) of the passed year (current year by default)
65
+ * which are intersected with the range between passed dates
65
66
  */
66
- getMonthsRange: (minDate: Date, maxDate: Date) => number[];
67
+ getMonthsRange: (minDate: Date, maxDate: Date, year?: number) => number[];
67
68
  };
68
69
  export declare const useCalendar: (props: UseCalendarProps) => UseCalendarPayload;
69
70
  export default useCalendar;
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.useCalendar = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const WHITESPACE = '\u{0020}';
9
- const WEEK_IN_MS = 604800000; // 7 * 24 * 60 * 60 * 1000
9
+ const DAY_IN_MS = 86400000; // 24 * 60 * 60 * 1000
10
10
  // Константа для маппинга дней недели
11
11
  const WEEK_DAYS_MAP = {
12
12
  sunday: 0,
@@ -19,9 +19,10 @@ const WEEK_DAYS_MAP = {
19
19
  };
20
20
  const useCalendar = (props) => {
21
21
  const { minDate, maxDate, weekStartDay, displayLeadingZero, locale } = props;
22
- // Мемоизируем timestamp граничных дат для оптимизации проверок isDisabled
23
- const minTime = react_1.default.useMemo(() => minDate.getTime(), [minDate]);
24
- const maxTime = react_1.default.useMemo(() => maxDate.getTime(), [maxDate]);
22
+ // Мемоизируем timestamp граничных дат для оптимизации проверок isDisabled.
23
+ // Границы расширяются до целых дней, иначе при minDate={new Date()} сегодняшний день недоступен
24
+ const minTime = react_1.default.useMemo(() => new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()).getTime(), [minDate]);
25
+ const maxTime = react_1.default.useMemo(() => new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999).getTime(), [maxDate]);
25
26
  // Мемоизируем номер дня начала недели
26
27
  const weekStartDayNumber = react_1.default.useMemo(() => WEEK_DAYS_MAP[weekStartDay], [weekStartDay]);
27
28
  // Оптимизированная функция isSameDay
@@ -39,22 +40,26 @@ const useCalendar = (props) => {
39
40
  return currentTime > maxTime || currentTime < minTime;
40
41
  }, [maxTime, minTime]);
41
42
  const calculateWeekNumber = react_1.default.useCallback((date) => {
42
- // Используем UTC методы для избежания проблем с часовыми поясами
43
- const year = date.getFullYear();
44
- const month = date.getMonth();
45
- const day = date.getDate();
46
- // Создаем дату без времени
47
- const d = new Date(Date.UTC(year, month, day));
48
- // День недели (0-6)
49
- const dayOfWeek = d.getUTCDay();
50
- // Смещение от начала недели
51
- const diff = (dayOfWeek - weekStartDayNumber + 7) % 7;
52
- // Первый день недели
53
- const weekStart = new Date(Date.UTC(year, month, day - diff));
54
- // Первый день года
55
- const yearStart = new Date(Date.UTC(year, 0, 1));
56
- // Вычисляем номер недели
57
- return Math.floor((weekStart.getTime() - yearStart.getTime()) / WEEK_IN_MS) + 1;
43
+ // Используем UTC методы для избежания проблем с часовыми поясами (DST)
44
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
45
+ // Неделя с понедельника - ISO 8601: первая неделя года содержит первый четверг
46
+ if (weekStartDayNumber === WEEK_DAYS_MAP.monday) {
47
+ const isoDay = d.getUTCDay() || 7;
48
+ const thursday = new Date(d.getTime() + (4 - isoDay) * DAY_IN_MS);
49
+ const yearStart = Date.UTC(thursday.getUTCFullYear(), 0, 1);
50
+ return Math.ceil(((thursday.getTime() - yearStart) / DAY_IN_MS + 1) / 7);
51
+ }
52
+ // Остальные системы: первая неделя года содержит 1 января
53
+ const diff = (d.getUTCDay() - weekStartDayNumber + 7) % 7;
54
+ const weekStart = d.getTime() - diff * DAY_IN_MS;
55
+ const weekEnd = new Date(weekStart + 6 * DAY_IN_MS);
56
+ // Неделя содержит 1 января следующего года
57
+ if (weekEnd.getUTCFullYear() !== new Date(weekStart).getUTCFullYear()) {
58
+ return 1;
59
+ }
60
+ const yearStart = new Date(Date.UTC(new Date(weekStart).getUTCFullYear(), 0, 1));
61
+ const firstWeekStart = yearStart.getTime() - ((yearStart.getUTCDay() - weekStartDayNumber + 7) % 7) * DAY_IN_MS;
62
+ return Math.round((weekStart - firstWeekStart) / (7 * DAY_IN_MS)) + 1;
58
63
  }, [weekStartDayNumber]);
59
64
  const getDayLabel = react_1.default.useCallback((day) => {
60
65
  const dateNum = day.getDate();
@@ -79,12 +84,14 @@ const useCalendar = (props) => {
79
84
  }
80
85
  return years;
81
86
  }, []);
82
- const getMonthsRange = react_1.default.useCallback((minDate, maxDate) => {
87
+ const getMonthsRange = react_1.default.useCallback((minDate, maxDate, year) => {
83
88
  const m = [];
84
- const y = new Date().getFullYear();
89
+ const y = typeof year === 'number' ? year : new Date().getFullYear();
85
90
  for (let index = 0; index < 12; index++) {
86
- const d = new Date(y, index, 1, 0, 0, 0);
87
- if (d.getTime() > minDate.getTime() && d.getTime() < maxDate.getTime()) {
91
+ const monthStart = new Date(y, index, 1, 0, 0, 0, 0);
92
+ const monthEnd = new Date(y, index + 1, 0, 23, 59, 59, 999);
93
+ // Месяц подходит, если хотя бы частично попадает в диапазон
94
+ if (monthStart.getTime() <= maxDate.getTime() && monthEnd.getTime() >= minDate.getTime()) {
88
95
  m.push(index);
89
96
  }
90
97
  }
@@ -37,6 +37,17 @@ const ClickOutside = props => {
37
37
  }
38
38
  };
39
39
  }, [onOutsideClick, mouseEvent]);
40
- return react_1.default.createElement(react_1.default.Fragment, null, react_1.default.cloneElement(children, { ref }));
40
+ // Keep the child's own ref working alongside ours
41
+ const childRef = children.ref;
42
+ const setRef = react_1.default.useCallback((el) => {
43
+ ref.current = el;
44
+ if (typeof childRef === 'function') {
45
+ childRef(el);
46
+ }
47
+ else if (childRef && typeof childRef === 'object') {
48
+ childRef.current = el;
49
+ }
50
+ }, [childRef]);
51
+ return react_1.default.createElement(react_1.default.Fragment, null, react_1.default.cloneElement(children, { ref: setRef }));
41
52
  };
42
53
  exports.default = ClickOutside;
@@ -12,6 +12,15 @@ export declare class ColorParser {
12
12
  */
13
13
  static parse(value: string): ParsedColor;
14
14
  private static parseHex;
15
+ /**
16
+ * Returns function arguments: `rgb(10, 20 30 / 50%)` -> ['10', '20', '30', '50%']
17
+ */
18
+ private static getArgs;
19
+ private static parseNumber;
20
+ /**
21
+ * Alpha may be a number (0..1) or a percentage (0%..100%)
22
+ */
23
+ private static parseAlpha;
15
24
  private static parseRgb;
16
25
  private static parseHsl;
17
26
  private static hslToRgb;
@@ -11,6 +11,9 @@ class ColorParser {
11
11
  throw new Error(`Invalid color value: ${value}`);
12
12
  }
13
13
  const inputColor = value.trim().toLowerCase();
14
+ if (inputColor === 'transparent') {
15
+ return { r: 0, g: 0, b: 0, a: 0 };
16
+ }
14
17
  // Проверка на именованный цвет
15
18
  if (this.webColors[inputColor]) {
16
19
  return this.parse(this.webColors[inputColor]);
@@ -70,27 +73,64 @@ class ColorParser {
70
73
  }
71
74
  throw new Error(`Invalid HEX color: ${hex}`);
72
75
  }
76
+ /**
77
+ * Returns function arguments: `rgb(10, 20 30 / 50%)` -> ['10', '20', '30', '50%']
78
+ */
79
+ static getArgs(value) {
80
+ const inner = value.substring(value.indexOf('(') + 1, value.lastIndexOf(')'));
81
+ return inner.split(/[\s,/]+/).filter(Boolean);
82
+ }
83
+ static parseNumber(value, color) {
84
+ const num = parseFloat(value);
85
+ if (isNaN(num)) {
86
+ throw new Error(`Invalid color value: ${color}`);
87
+ }
88
+ return num;
89
+ }
90
+ /**
91
+ * Alpha may be a number (0..1) or a percentage (0%..100%)
92
+ */
93
+ static parseAlpha(value, color) {
94
+ if (typeof value === 'undefined') {
95
+ return 1;
96
+ }
97
+ const num = this.parseNumber(value, color);
98
+ return this.clamp(value.endsWith('%') ? num / 100 : num, 0, 1);
99
+ }
73
100
  static parseRgb(rgb) {
74
- const matches = rgb.match(/[\d.]+/g);
75
- if (!matches || matches.length < 3) {
101
+ const args = this.getArgs(rgb);
102
+ if (args.length < 3) {
76
103
  throw new Error(`Invalid RGB color: ${rgb}`);
77
104
  }
105
+ // Channel may be a number (0..255) or a percentage (0%..100%)
106
+ const channel = (value) => {
107
+ const num = this.parseNumber(value, rgb);
108
+ return this.clamp(Math.round(value.endsWith('%') ? (num * 255) / 100 : num), 0, 255);
109
+ };
78
110
  return {
79
- r: this.clamp(parseInt(matches[0], 10), 0, 255),
80
- g: this.clamp(parseInt(matches[1], 10), 0, 255),
81
- b: this.clamp(parseInt(matches[2], 10), 0, 255),
82
- a: matches[3] ? this.clamp(parseFloat(matches[3]), 0, 1) : 1,
111
+ r: channel(args[0]),
112
+ g: channel(args[1]),
113
+ b: channel(args[2]),
114
+ a: this.parseAlpha(args[3], rgb),
83
115
  };
84
116
  }
85
117
  static parseHsl(hsl) {
86
- const matches = hsl.match(/[\d.]+/g);
87
- if (!matches || matches.length < 3) {
118
+ const args = this.getArgs(hsl);
119
+ if (args.length < 3) {
88
120
  throw new Error(`Invalid HSL color: ${hsl}`);
89
121
  }
90
- const h = parseFloat(matches[0]) / 360;
91
- const s = parseFloat(matches[1]) / 100;
92
- const l = parseFloat(matches[2]) / 100;
93
- const a = matches[3] ? this.clamp(parseFloat(matches[3]), 0, 1) : 1;
122
+ let hue = this.parseNumber(args[0], hsl);
123
+ if (args[0].endsWith('turn')) {
124
+ hue *= 360;
125
+ }
126
+ else if (args[0].endsWith('rad')) {
127
+ hue = (hue * 180) / Math.PI;
128
+ }
129
+ // Negative and >360 hues are wrapped around the color wheel
130
+ const h = (((hue % 360) + 360) % 360) / 360;
131
+ const s = this.clamp(this.parseNumber(args[1], hsl), 0, 100) / 100;
132
+ const l = this.clamp(this.parseNumber(args[2], hsl), 0, 100) / 100;
133
+ const a = this.parseAlpha(args[3], hsl);
94
134
  const rgb = this.hslToRgb(h, s, l);
95
135
  return {
96
136
  r: rgb[0],
@@ -63,6 +63,10 @@ export type DatePickerProps = Omit<TextFieldProps, 'value' | 'onChange' | 'overr
63
63
  /**
64
64
  * Label for the Today button. If label passed, then button will be rendered
65
65
  */
66
+ readonly todayButtonLabel?: string;
67
+ /**
68
+ * @deprecated Use `todayButtonLabel` instead
69
+ */
66
70
  readonly toodayButtonLabel?: string;
67
71
  /**
68
72
  * Heading