@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.
- package/Autocomplete/AutocompleteContainer.js +47 -35
- package/Autocomplete/index.js +9 -7
- package/Autocomplete2/Autocomplete2.js +1 -1
- package/CHANGELOG.md +38 -0
- package/Calendar/CalendarComponent.js +54 -51
- package/Calendar/use-calendar.d.ts +3 -2
- package/Calendar/use-calendar.js +31 -24
- package/ClickOutside/index.js +12 -1
- package/Color/ColorParser.d.ts +9 -0
- package/Color/ColorParser.js +52 -12
- package/DatePicker/index.d.ts +4 -0
- package/DatePicker/index.js +28 -14
- package/DatePicker/useDatePickerFormat.js +20 -3
- package/MaskedField/useMasked.js +17 -6
- package/Menu/MenuContainer.d.ts +3 -3
- package/Menu/MenuContainer.js +26 -35
- package/Menu/VirtualizedList.js +1 -3
- package/Modal/BaseModal/ModalWrapper.js +65 -38
- package/Modal/BaseModal/context.js +6 -0
- package/Modal/BaseModal/index.js +0 -8
- package/Popper/Popper.d.ts +2 -2
- package/Popper/Popper.js +9 -7
- package/Popper/usePopper.js +79 -46
- package/Selectbox/SelectboxMenu.d.ts +1 -1
- package/Selectbox/index.js +11 -27
- package/Swiper/Swiper.js +17 -5
- package/TextArea/index.js +6 -14
- package/TextField/index.js +6 -18
- package/package.json +1 -1
- package/utils/TabManager.d.ts +7 -1
- package/utils/TabManager.js +39 -45
|
@@ -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
|
-
}, [
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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({
|
package/Autocomplete/index.js
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
225
|
-
|
|
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 =
|
|
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()
|
|
315
|
+
newCalendarDate.setFullYear(newCalendarDate.getFullYear() - 1);
|
|
296
316
|
}
|
|
297
317
|
else {
|
|
298
|
-
newCalendarDate.setFullYear(newCalendarDate.getFullYear()
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
390
|
-
a
|
|
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
|
|
415
|
-
today.
|
|
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(
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
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)
|
|
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;
|
package/Calendar/use-calendar.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
-
const
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const weekStart =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
87
|
-
|
|
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
|
}
|
package/ClickOutside/index.js
CHANGED
|
@@ -37,6 +37,17 @@ const ClickOutside = props => {
|
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
}, [onOutsideClick, mouseEvent]);
|
|
40
|
-
|
|
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;
|
package/Color/ColorParser.d.ts
CHANGED
|
@@ -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;
|
package/Color/ColorParser.js
CHANGED
|
@@ -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
|
|
75
|
-
if (
|
|
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:
|
|
80
|
-
g:
|
|
81
|
-
b:
|
|
82
|
-
a:
|
|
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
|
|
87
|
-
if (
|
|
118
|
+
const args = this.getArgs(hsl);
|
|
119
|
+
if (args.length < 3) {
|
|
88
120
|
throw new Error(`Invalid HSL color: ${hsl}`);
|
|
89
121
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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],
|
package/DatePicker/index.d.ts
CHANGED
|
@@ -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
|