@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
package/DatePicker/index.js
CHANGED
|
@@ -49,23 +49,38 @@ const StyledMaskedField = (0, styled_1.default)(MaskedField_1.default) `
|
|
|
49
49
|
`}
|
|
50
50
|
`;
|
|
51
51
|
const DatePicker = props => {
|
|
52
|
-
const { value, template, onChange, calendarButtonTooltip, readOnly, disabled, 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', badges = [], displayLeadingZero = false, resetButtonLabel, toodayButtonLabel, heading, subheading, view, views, footer, overrides } = props, restInputProps = __rest(props, ["value", "template", "onChange", "calendarButtonTooltip", "readOnly", "disabled", "minDate", "maxDate", "weekStartDay", "locale", "badges", "displayLeadingZero", "resetButtonLabel", "toodayButtonLabel", "heading", "subheading", "view", "views", "footer", "overrides"]);
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
const { value, template, onChange, calendarButtonTooltip, readOnly, disabled, 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', badges = [], displayLeadingZero = false, resetButtonLabel, todayButtonLabel: todayButtonLabelProp, toodayButtonLabel, markToday, weekDayLabelFormat, heading, subheading, view, views, footer, overrides } = props, restInputProps = __rest(props, ["value", "template", "onChange", "calendarButtonTooltip", "readOnly", "disabled", "minDate", "maxDate", "weekStartDay", "locale", "badges", "displayLeadingZero", "resetButtonLabel", "todayButtonLabel", "toodayButtonLabel", "markToday", "weekDayLabelFormat", "heading", "subheading", "view", "views", "footer", "overrides"]);
|
|
53
|
+
const todayButtonLabel = todayButtonLabelProp !== null && todayButtonLabelProp !== void 0 ? todayButtonLabelProp : toodayButtonLabel;
|
|
54
|
+
const calendarOverrides = react_1.default.useMemo(() => {
|
|
55
|
+
if (!overrides) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const { Input: _Input, ErrorText: _ErrorText, IconWrapper: _IconWrapper, InputWrapper: _InputWrapper, Label: _Label, Asterisk: _Asterisk, Container: _Container } = overrides, rest = __rest(overrides, ["Input", "ErrorText", "IconWrapper", "InputWrapper", "Label", "Asterisk", "Container"]);
|
|
59
|
+
return rest;
|
|
60
|
+
}, [overrides]);
|
|
61
|
+
const textFieldOverrides = react_1.default.useMemo(() => {
|
|
62
|
+
if (!overrides) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const { Input, ErrorText, IconWrapper, InputWrapper, Label, Asterisk, Container } = overrides;
|
|
66
|
+
return { Input, ErrorText, IconWrapper, InputWrapper, Label, Asterisk, Container };
|
|
67
|
+
}, [overrides]);
|
|
58
68
|
const [currentValue, setCurrentValue] = react_1.default.useState(value);
|
|
59
69
|
const valueRef = react_1.default.useRef(value);
|
|
60
70
|
const [textFieldRef, setTextFieldRef] = react_1.default.useState(null);
|
|
61
71
|
const [isOpen, setOpenSate] = react_1.default.useState(false);
|
|
62
72
|
const { getMaskByTemplate, formatInputByTemplate, parseInputByTemplate } = (0, useDatePickerFormat_1.default)();
|
|
63
73
|
react_1.default.useEffect(() => {
|
|
64
|
-
const a = value
|
|
65
|
-
const b = valueRef.current
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
a
|
|
74
|
+
const a = value;
|
|
75
|
+
const b = valueRef.current;
|
|
76
|
+
// null <-> date transitions must be detected too
|
|
77
|
+
const isSameDay = a === b ||
|
|
78
|
+
(a !== null &&
|
|
79
|
+
b !== null &&
|
|
80
|
+
a.getFullYear() === b.getFullYear() &&
|
|
81
|
+
a.getMonth() === b.getMonth() &&
|
|
82
|
+
a.getDate() === b.getDate());
|
|
83
|
+
if (!isSameDay) {
|
|
69
84
|
valueRef.current = value;
|
|
70
85
|
setCurrentValue(value);
|
|
71
86
|
}
|
|
@@ -118,7 +133,6 @@ const DatePicker = props => {
|
|
|
118
133
|
parseInputByTemplate,
|
|
119
134
|
template,
|
|
120
135
|
]);
|
|
121
|
-
const calendarValue = react_1.default.useMemo(() => currentValue || new Date(), [currentValue]);
|
|
122
136
|
const calendarChange = react_1.default.useCallback(date => {
|
|
123
137
|
if (date) {
|
|
124
138
|
onChange(date);
|
|
@@ -126,9 +140,9 @@ const DatePicker = props => {
|
|
|
126
140
|
setOpenSate(false);
|
|
127
141
|
}, [onChange]);
|
|
128
142
|
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
129
|
-
react_1.default.createElement(StyledMaskedField, Object.assign({ onClick: handleClick, disabled: disabled, mask: getMaskByTemplate(template), ref: setTextFieldRef, readOnly: readOnly, endIcon: endIcon }, restInputProps, { value: currentValue ? formatInputByTemplate(currentValue, template) : '', onChange: handleChange })),
|
|
143
|
+
react_1.default.createElement(StyledMaskedField, Object.assign({ onClick: handleClick, disabled: disabled, mask: getMaskByTemplate(template), ref: setTextFieldRef, readOnly: readOnly, endIcon: endIcon }, restInputProps, { value: currentValue ? formatInputByTemplate(currentValue, template) : '', onChange: handleChange, overrides: textFieldOverrides })),
|
|
130
144
|
react_1.default.createElement(ClickOutside_1.default, { onOutsideClick: () => setOpenSate(false), mouseEvent: "onMouseDown" },
|
|
131
145
|
react_1.default.createElement(Popper_1.default, { isOpen: isOpen, anchorElement: textFieldRef },
|
|
132
|
-
react_1.default.createElement(Calendar_1.default, { minDate: minDate, maxDate: maxDate, weekStartDay: weekStartDay, locale: locale, badges: badges, displayLeadingZero: displayLeadingZero, resetButtonLabel: resetButtonLabel, todayButtonLabel:
|
|
146
|
+
react_1.default.createElement(Calendar_1.default, { minDate: minDate, maxDate: maxDate, weekStartDay: weekStartDay, locale: locale, badges: badges, displayLeadingZero: displayLeadingZero, resetButtonLabel: resetButtonLabel, todayButtonLabel: todayButtonLabel, markToday: markToday, weekDayLabelFormat: weekDayLabelFormat, heading: heading, subheading: subheading, view: view, views: views, footer: footer, value: currentValue, onChange: calendarChange, overrides: calendarOverrides })))));
|
|
133
147
|
};
|
|
134
148
|
exports.default = DatePicker;
|
|
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.useDatePickerFormat = void 0;
|
|
7
7
|
const react_1 = __importDefault(require("react"));
|
|
8
8
|
const useDatePickerFormat = () => {
|
|
9
|
-
const templateValidChars = react_1.default.useMemo(() => ['y', 'Y', 'yy', 'yyyy', 'd', 'dd', 'D', 'm', 'mm'], []);
|
|
9
|
+
const templateValidChars = react_1.default.useMemo(() => ['y', 'Y', 'yy', 'yyyy', 'YYYY', 'd', 'dd', 'D', 'DD', 'm', 'mm'], []);
|
|
10
10
|
const isValidTemplate = react_1.default.useCallback((template) => {
|
|
11
11
|
const matches = template.match(/[yYmMdD]+/g);
|
|
12
12
|
if (!matches) {
|
|
@@ -48,12 +48,14 @@ const useDatePickerFormat = () => {
|
|
|
48
48
|
template.split('').forEach((char, charIndex) => {
|
|
49
49
|
switch (char) {
|
|
50
50
|
case 'y':
|
|
51
|
+
case 'Y':
|
|
51
52
|
data.years.push(input[charIndex]);
|
|
52
53
|
break;
|
|
53
54
|
case 'm':
|
|
54
55
|
data.months.push(input[charIndex]);
|
|
55
56
|
break;
|
|
56
57
|
case 'd':
|
|
58
|
+
case 'D':
|
|
57
59
|
data.days.push(input[charIndex]);
|
|
58
60
|
break;
|
|
59
61
|
default:
|
|
@@ -61,8 +63,23 @@ const useDatePickerFormat = () => {
|
|
|
61
63
|
break;
|
|
62
64
|
}
|
|
63
65
|
});
|
|
64
|
-
|
|
65
|
-
parseInt(data.
|
|
66
|
+
let year = parseInt(data.years.join(''), 10);
|
|
67
|
+
const month = parseInt(data.months.join(''), 10) - 1; // Fucking month indexes
|
|
68
|
+
const day = parseInt(data.days.join(''), 10);
|
|
69
|
+
if (isNaN(year) || isNaN(month) || isNaN(day)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// Two-digit year means the current century (new Date() maps 0-99 to 1900-1999)
|
|
73
|
+
if (data.years.length <= 2) {
|
|
74
|
+
year += 2000;
|
|
75
|
+
}
|
|
76
|
+
const date = new Date(year, month, day);
|
|
77
|
+
date.setFullYear(year);
|
|
78
|
+
// Reject overflowed dates like 31.02 instead of silently moving them to March
|
|
79
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return date;
|
|
66
83
|
}, [validateTemplate]);
|
|
67
84
|
/**
|
|
68
85
|
* Format date to string by template
|
package/MaskedField/useMasked.js
CHANGED
|
@@ -44,28 +44,38 @@ const useMasked = () => {
|
|
|
44
44
|
if (parsedValue === '') {
|
|
45
45
|
return data;
|
|
46
46
|
}
|
|
47
|
+
const requiredCount = mask.filter(pattern => pattern instanceof RegExp).length;
|
|
48
|
+
let filledCount = 0;
|
|
47
49
|
let patternIndex = 0;
|
|
48
50
|
for (patternIndex = 0; patternIndex < mask.length; patternIndex++) {
|
|
49
|
-
data.isValid = false;
|
|
50
51
|
if (data.charIndex >= parsedValue.length) {
|
|
51
52
|
break;
|
|
52
53
|
}
|
|
53
54
|
const char = parsedValue[data.charIndex];
|
|
54
55
|
const pattern = mask[patternIndex];
|
|
56
|
+
// A mask literal (e.g. a shifted separator) got into a RegExp position:
|
|
57
|
+
// drop the char and try the same pattern with the next one, otherwise the rest of input is lost
|
|
58
|
+
if (pattern instanceof RegExp && !new RegExp(pattern).test(char) && mask.includes(char)) {
|
|
59
|
+
if (data.charIndex < (caret || 0)) {
|
|
60
|
+
data.caret -= 1;
|
|
61
|
+
}
|
|
62
|
+
data.charIndex += 1;
|
|
63
|
+
patternIndex -= 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
55
66
|
// If char is correct (matched with Regexp pattern)
|
|
56
67
|
// then accept char into text
|
|
57
68
|
// and increment the charIndex and caret
|
|
58
69
|
if (pattern instanceof RegExp && new RegExp(pattern).test(char)) {
|
|
59
70
|
data.text = `${data.text}${char}`;
|
|
60
71
|
data.charIndex += 1;
|
|
61
|
-
|
|
72
|
+
filledCount += 1;
|
|
62
73
|
}
|
|
63
74
|
// If char is correct (equal with pattern string)
|
|
64
|
-
// then accept char into text
|
|
65
|
-
//
|
|
75
|
+
// then accept char into text and increment the charIndex.
|
|
76
|
+
// The caret is not changed: this char is already counted by parseInput
|
|
66
77
|
if (typeof pattern === 'string' && pattern === char) {
|
|
67
78
|
data.text = `${data.text}${char}`;
|
|
68
|
-
data.caret += 1;
|
|
69
79
|
data.charIndex += 1;
|
|
70
80
|
}
|
|
71
81
|
// If pattern is a string and char is not correct
|
|
@@ -81,7 +91,8 @@ const useMasked = () => {
|
|
|
81
91
|
const result = {
|
|
82
92
|
text: data.text,
|
|
83
93
|
caret: data.caret,
|
|
84
|
-
|
|
94
|
+
// Valid when every RegExp position of the mask is filled
|
|
95
|
+
isValid: requiredCount > 0 && filledCount === requiredCount,
|
|
85
96
|
};
|
|
86
97
|
return result;
|
|
87
98
|
}, []);
|
package/Menu/MenuContainer.d.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface MenuProps<T, Multiple extends boolean | undefined = undefined>
|
|
|
97
97
|
* if the preferred placement doesn't fit in the viewport.
|
|
98
98
|
* The component will iterate through possible placements until it finds one that fits.
|
|
99
99
|
*
|
|
100
|
-
* **Default**: `
|
|
100
|
+
* **Default**: `false`
|
|
101
101
|
* ```
|
|
102
102
|
*/
|
|
103
103
|
readonly autoFlip?: boolean;
|
|
@@ -115,7 +115,7 @@ export interface MenuProps<T, Multiple extends boolean | undefined = undefined>
|
|
|
115
115
|
* - `'absolute'`: Positions relative to the nearest positioned ancestor.
|
|
116
116
|
* When using 'absolute', make sure a parent element has `position: relative`.
|
|
117
117
|
*
|
|
118
|
-
* **Default**: `
|
|
118
|
+
* **Default**: `fixed`
|
|
119
119
|
* ```
|
|
120
120
|
*/
|
|
121
121
|
readonly positionStrategy?: PositionStrategy;
|
|
@@ -124,7 +124,7 @@ export interface MenuProps<T, Multiple extends boolean | undefined = undefined>
|
|
|
124
124
|
* Used to prevent the popper from being positioned too close to the screen boundaries.
|
|
125
125
|
* The popper will try to flip to another placement if it cannot maintain this margin.
|
|
126
126
|
*
|
|
127
|
-
* **Default**: `
|
|
127
|
+
* **Default**: `30`
|
|
128
128
|
* ```
|
|
129
129
|
*/
|
|
130
130
|
readonly viewportMargin?: number;
|
package/Menu/MenuContainer.js
CHANGED
|
@@ -47,27 +47,24 @@ const isEqual = (a, b) => {
|
|
|
47
47
|
};
|
|
48
48
|
const MenuContainer = react_1.default.forwardRef((props, ref) => {
|
|
49
49
|
const { items, value, anchorElement = null, overrides, children, closeOutsideClick = true, isOpen = false, anchorPos = 'bottom', alternativePlacements = ['bottom', 'top'], positionStrategy, autoFlip, viewportMargin, offset, multiple = false, autofocus = true, closeOnSelect = !multiple, onRequestClose = onRequestCloseDefault, onAnchorPosChanged, zIndex, onSelectItem, getOptionSelected, maxWidth, } = props;
|
|
50
|
-
// const [actualPlacement, setActualPlacement] = React.useState(anchorPos);
|
|
51
|
-
const [currentAnchorElement, setAnchorElement] = react_1.default.useState(anchorElement);
|
|
52
50
|
const overridesMap = react_1.default.useMemo(() => ({
|
|
53
51
|
List: (overrides === null || overrides === void 0 ? void 0 : overrides.List) || MenuList_1.default,
|
|
54
52
|
Popper: (overrides === null || overrides === void 0 ? void 0 : overrides.Popper) || Popper_1.default,
|
|
55
53
|
}), [overrides === null || overrides === void 0 ? void 0 : overrides.List, overrides === null || overrides === void 0 ? void 0 : overrides.Popper]);
|
|
56
54
|
const menuListRef = react_1.default.useRef(null);
|
|
57
55
|
const menuPopperRef = react_1.default.useRef(null);
|
|
58
|
-
|
|
56
|
+
// Starts as `false` so that a menu mounted in the open state is focused and scrolled too
|
|
57
|
+
const isOpenRef = react_1.default.useRef(false);
|
|
59
58
|
const focusTimeoutRef = react_1.default.useRef();
|
|
60
59
|
const { dispatch, state: { selectedIndexes, markedIndex, hoveredIndex }, } = (0, context_1.default)();
|
|
61
60
|
const selectedIndexesRef = react_1.default.useRef(selectedIndexes);
|
|
61
|
+
const compareFunc = react_1.default.useCallback((item, val) => getOptionSelected ? getOptionSelected({ item, value: val }) : isEqual(item, val), [getOptionSelected]);
|
|
62
62
|
// Оптимизированная функция получения выбранных индексов
|
|
63
63
|
const getSelectedIndexes = react_1.default.useCallback(() => {
|
|
64
64
|
if (value === null || (multiple && value.length === 0)) {
|
|
65
65
|
return [];
|
|
66
66
|
}
|
|
67
67
|
const indexes = [];
|
|
68
|
-
const compareFunc = getOptionSelected
|
|
69
|
-
? (item, val) => getOptionSelected({ item, value: val })
|
|
70
|
-
: (item, val) => isEqual(item, val);
|
|
71
68
|
if (multiple) {
|
|
72
69
|
const valueArray = value;
|
|
73
70
|
items.forEach((item, index) => {
|
|
@@ -84,7 +81,7 @@ const MenuContainer = react_1.default.forwardRef((props, ref) => {
|
|
|
84
81
|
}
|
|
85
82
|
}
|
|
86
83
|
return indexes;
|
|
87
|
-
}, [items, value, multiple,
|
|
84
|
+
}, [items, value, multiple, compareFunc]);
|
|
88
85
|
const scrollToIndex = react_1.default.useCallback((index) => {
|
|
89
86
|
if (index === -1) {
|
|
90
87
|
return;
|
|
@@ -94,20 +91,24 @@ const MenuContainer = react_1.default.forwardRef((props, ref) => {
|
|
|
94
91
|
if (container && option) {
|
|
95
92
|
const containerRect = container.getBoundingClientRect();
|
|
96
93
|
const optionRect = option.getBoundingClientRect();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
top
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
// Scroll only when the option is out of the visible area
|
|
95
|
+
if (optionRect.top < containerRect.top) {
|
|
96
|
+
container.scrollTop -= containerRect.top - optionRect.top;
|
|
97
|
+
}
|
|
98
|
+
else if (optionRect.bottom > containerRect.bottom) {
|
|
99
|
+
container.scrollTop += optionRect.bottom - containerRect.bottom;
|
|
100
|
+
}
|
|
102
101
|
}
|
|
103
102
|
}, []);
|
|
104
103
|
const scrollToFirstSelected = react_1.default.useCallback(() => {
|
|
105
|
-
|
|
104
|
+
// Computed from the props, the context state may not be updated yet
|
|
105
|
+
const indexes = getSelectedIndexes();
|
|
106
|
+
if (indexes.length === 0)
|
|
106
107
|
return;
|
|
107
|
-
const firstSelectedIndex = Math.min(...
|
|
108
|
+
const firstSelectedIndex = Math.min(...indexes);
|
|
108
109
|
dispatch((0, context_1.actionSetmenuState)({ markedIndex: firstSelectedIndex }));
|
|
109
110
|
scrollToIndex(firstSelectedIndex);
|
|
110
|
-
}, [dispatch,
|
|
111
|
+
}, [dispatch, getSelectedIndexes, scrollToIndex]);
|
|
111
112
|
const selectItem = react_1.default.useCallback((index) => {
|
|
112
113
|
if (index < 0 || index >= items.length)
|
|
113
114
|
return;
|
|
@@ -117,17 +118,13 @@ const MenuContainer = react_1.default.forwardRef((props, ref) => {
|
|
|
117
118
|
if (typeof onSelectItem === 'function') {
|
|
118
119
|
if (multiple) {
|
|
119
120
|
// Для multiple режима
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
else {
|
|
128
|
-
selectedSet.add(item);
|
|
129
|
-
}
|
|
130
|
-
onSelectItem(Array.from(selectedSet));
|
|
121
|
+
// Value entries may be other instances than items, so compare them by compareFunc
|
|
122
|
+
const currentValue = value || [];
|
|
123
|
+
const isSelected = currentValue.some(v => compareFunc(item, v));
|
|
124
|
+
const newValue = isSelected
|
|
125
|
+
? currentValue.filter(v => !compareFunc(item, v))
|
|
126
|
+
: [...currentValue, item];
|
|
127
|
+
onSelectItem(newValue);
|
|
131
128
|
}
|
|
132
129
|
else {
|
|
133
130
|
// Для single режима
|
|
@@ -137,7 +134,7 @@ const MenuContainer = react_1.default.forwardRef((props, ref) => {
|
|
|
137
134
|
onRequestClose();
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
|
-
}, [onSelectItem, onRequestClose,
|
|
137
|
+
}, [onSelectItem, onRequestClose, compareFunc, closeOnSelect, items, value, multiple]);
|
|
141
138
|
const highlightIndex = react_1.default.useCallback((index) => {
|
|
142
139
|
const validIndex = Math.max(-1, Math.min(index, items.length - 1));
|
|
143
140
|
if (validIndex !== markedIndex) {
|
|
@@ -189,12 +186,6 @@ const MenuContainer = react_1.default.forwardRef((props, ref) => {
|
|
|
189
186
|
scrollToFirstSelected,
|
|
190
187
|
selectItem,
|
|
191
188
|
]);
|
|
192
|
-
// Обновление anchor элемента
|
|
193
|
-
react_1.default.useEffect(() => {
|
|
194
|
-
if (anchorElement !== currentAnchorElement) {
|
|
195
|
-
setAnchorElement(anchorElement);
|
|
196
|
-
}
|
|
197
|
-
}, [anchorElement, currentAnchorElement]);
|
|
198
189
|
// Обработчик клавиатуры
|
|
199
190
|
const listKeydownEvent = react_1.default.useCallback((event) => {
|
|
200
191
|
switch (event.code) {
|
|
@@ -244,7 +235,7 @@ const MenuContainer = react_1.default.forwardRef((props, ref) => {
|
|
|
244
235
|
return;
|
|
245
236
|
isOpenRef.current = isOpen;
|
|
246
237
|
if (!isOpen) {
|
|
247
|
-
onRequestClose
|
|
238
|
+
// The menu is already closed by the parent, so onRequestClose must not be called again
|
|
248
239
|
dispatch((0, context_1.actionSetmenuState)({ markedIndex: -1, hoveredIndex: -1 }));
|
|
249
240
|
return;
|
|
250
241
|
}
|
|
@@ -260,7 +251,7 @@ const MenuContainer = react_1.default.forwardRef((props, ref) => {
|
|
|
260
251
|
(_a = menuListRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
261
252
|
}, 15);
|
|
262
253
|
}
|
|
263
|
-
}, [isOpen, autofocus, dispatch,
|
|
254
|
+
}, [isOpen, autofocus, dispatch, scrollToFirstSelected]);
|
|
264
255
|
// Обновление выбранных индексов
|
|
265
256
|
react_1.default.useEffect(() => {
|
|
266
257
|
const indexes = getSelectedIndexes();
|
package/Menu/VirtualizedList.js
CHANGED
|
@@ -21,8 +21,6 @@ const react_1 = __importDefault(require("react"));
|
|
|
21
21
|
const AutoHeightWrapper_1 = __importDefault(require("./AutoHeightWrapper"));
|
|
22
22
|
const VirtualizedListComponent_1 = __importDefault(require("./VirtualizedListComponent"));
|
|
23
23
|
__exportStar(require("./VirtualizedListComponent"), exports);
|
|
24
|
-
const VirtualizedList = react_1.default.forwardRef((props, ref) => {
|
|
25
|
-
return (react_1.default.createElement(AutoHeightWrapper_1.default, null, height => height > 0 && react_1.default.createElement(VirtualizedListComponent_1.default, Object.assign({}, props, { height: height, ref: ref }))));
|
|
26
|
-
});
|
|
24
|
+
const VirtualizedList = react_1.default.forwardRef((props, ref) => (react_1.default.createElement(AutoHeightWrapper_1.default, null, height => height > 0 && react_1.default.createElement(VirtualizedListComponent_1.default, Object.assign({}, props, { height: height, ref: ref })))));
|
|
27
25
|
VirtualizedList.displayName = 'VirtualizedList';
|
|
28
26
|
exports.default = VirtualizedList;
|
|
@@ -7,12 +7,17 @@ const react_1 = __importDefault(require("react"));
|
|
|
7
7
|
const react_2 = require("@emotion/react");
|
|
8
8
|
const context_1 = require("./context");
|
|
9
9
|
const TabManager_1 = __importDefault(require("../../utils/TabManager"));
|
|
10
|
+
/**
|
|
11
|
+
* Count of the modals which lock the body scroll
|
|
12
|
+
*/
|
|
13
|
+
let bodyLocksCount = 0;
|
|
10
14
|
const ModalWrapper = props => {
|
|
11
15
|
const { children, isOpen: isOpenProp, autofocus = true } = props;
|
|
12
16
|
const { state, dispatch } = (0, context_1.useContext)();
|
|
13
|
-
const initialOpenStateRef = react_1.default.useRef(isOpenProp);
|
|
14
|
-
const [alreadyMounted, setMountState] = react_1.default.useState(true);
|
|
15
17
|
const { closeOnEscape, isMounted, isOpen, destroyTimeout, onRequestClose } = state;
|
|
18
|
+
const timerRef = react_1.default.useRef(null);
|
|
19
|
+
const bodyLockedRef = react_1.default.useRef(false);
|
|
20
|
+
const isFirstRunRef = react_1.default.useRef(true);
|
|
16
21
|
const containerRef = react_1.default.useRef(null);
|
|
17
22
|
const id = context_1.PORTAL_ID + react_1.default.useId();
|
|
18
23
|
react_1.default.useEffect(() => {
|
|
@@ -53,13 +58,34 @@ const ModalWrapper = props => {
|
|
|
53
58
|
(_a = outer.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(outer);
|
|
54
59
|
return widthNoScroll - widthWithScroll;
|
|
55
60
|
}, []);
|
|
61
|
+
const lockBody = react_1.default.useCallback(() => {
|
|
62
|
+
var _a;
|
|
63
|
+
if (!bodyLockedRef.current) {
|
|
64
|
+
bodyLockedRef.current = true;
|
|
65
|
+
bodyLocksCount += 1;
|
|
66
|
+
(_a = window.document.body) === null || _a === void 0 ? void 0 : _a.classList.add('-modal-over');
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
|
+
const unlockBody = react_1.default.useCallback(() => {
|
|
70
|
+
var _a;
|
|
71
|
+
if (bodyLockedRef.current) {
|
|
72
|
+
bodyLockedRef.current = false;
|
|
73
|
+
bodyLocksCount = Math.max(0, bodyLocksCount - 1);
|
|
74
|
+
if (bodyLocksCount === 0) {
|
|
75
|
+
(_a = window.document.body) === null || _a === void 0 ? void 0 : _a.classList.remove('-modal-over');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}, []);
|
|
56
79
|
/**
|
|
57
80
|
* Component will unmount
|
|
58
|
-
*
|
|
81
|
+
* Clear pending timers and unlock the body scroll even if the modal is still open
|
|
59
82
|
*/
|
|
60
83
|
react_1.default.useEffect(() => () => {
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
if (timerRef.current) {
|
|
85
|
+
clearTimeout(timerRef.current);
|
|
86
|
+
}
|
|
87
|
+
unlockBody();
|
|
88
|
+
}, [unlockBody]);
|
|
63
89
|
react_1.default.useEffect(() => {
|
|
64
90
|
/**
|
|
65
91
|
* The last dialog will be found and closed
|
|
@@ -101,40 +127,41 @@ const ModalWrapper = props => {
|
|
|
101
127
|
* and sets visibility property to false otherwise
|
|
102
128
|
*/
|
|
103
129
|
react_1.default.useEffect(() => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (isOpenProp) {
|
|
110
|
-
dispatch((0, context_1.actionSetState)({ isMounted: true }));
|
|
111
|
-
(_a = window.document.body) === null || _a === void 0 ? void 0 : _a.classList.add('-modal-over');
|
|
112
|
-
setTimeout(() => {
|
|
113
|
-
if (alreadyMounted) {
|
|
114
|
-
dispatch((0, context_1.actionSetState)({ isOpen: true }));
|
|
115
|
-
}
|
|
116
|
-
}, 120);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Mark as closed and wait destroyTimeout then mark as unmounted
|
|
121
|
-
*/
|
|
122
|
-
if (!isOpenProp) {
|
|
123
|
-
dispatch((0, context_1.actionSetState)({ isOpen: false }));
|
|
124
|
-
setTimeout(() => {
|
|
125
|
-
var _a, _b;
|
|
126
|
-
if (alreadyMounted) {
|
|
127
|
-
const portal = window.document.querySelector(`#${context_1.PORTAL_ID}`);
|
|
128
|
-
if (((_a = portal === null || portal === void 0 ? void 0 : portal.children) === null || _a === void 0 ? void 0 : _a.length) === 1) {
|
|
129
|
-
(_b = window.document.body) === null || _b === void 0 ? void 0 : _b.classList.remove('-modal-over');
|
|
130
|
-
}
|
|
131
|
-
dispatch((0, context_1.actionSetState)({ isMounted: false }));
|
|
132
|
-
}
|
|
133
|
-
}, destroyTimeout);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
130
|
+
const isFirstRun = isFirstRunRef.current;
|
|
131
|
+
isFirstRunRef.current = false;
|
|
132
|
+
// Nothing to close on mount
|
|
133
|
+
if (isFirstRun && !isOpenProp) {
|
|
134
|
+
return;
|
|
136
135
|
}
|
|
137
|
-
|
|
136
|
+
// Previous transition (open or close) must not finish after the new one started
|
|
137
|
+
if (timerRef.current) {
|
|
138
|
+
clearTimeout(timerRef.current);
|
|
139
|
+
timerRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Mark as mounted and wait minimum loop of event then mark as open
|
|
143
|
+
*/
|
|
144
|
+
if (isOpenProp) {
|
|
145
|
+
dispatch((0, context_1.actionSetState)({ isMounted: true }));
|
|
146
|
+
lockBody();
|
|
147
|
+
timerRef.current = setTimeout(() => {
|
|
148
|
+
timerRef.current = null;
|
|
149
|
+
dispatch((0, context_1.actionSetState)({ isOpen: true }));
|
|
150
|
+
}, 120);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Mark as closed and wait destroyTimeout then mark as unmounted
|
|
155
|
+
*/
|
|
156
|
+
dispatch((0, context_1.actionSetState)({ isOpen: false }));
|
|
157
|
+
timerRef.current = setTimeout(() => {
|
|
158
|
+
timerRef.current = null;
|
|
159
|
+
unlockBody();
|
|
160
|
+
dispatch((0, context_1.actionSetState)({ isMounted: false }));
|
|
161
|
+
}, destroyTimeout);
|
|
162
|
+
// destroyTimeout is read at the moment of closing
|
|
163
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
164
|
+
}, [isOpenProp, dispatch, lockBody, unlockBody]);
|
|
138
165
|
return react_1.default.useMemo(() => isMounted ? (react_1.default.createElement("div", { ref: containerRef, tabIndex: -1, id: id },
|
|
139
166
|
children,
|
|
140
167
|
react_1.default.createElement(react_2.Global, { styles: (0, react_2.css) `
|
|
@@ -31,6 +31,12 @@ const context = react_1.default.createContext({ state: exports.defaultState, dis
|
|
|
31
31
|
const ContextProvider = props => {
|
|
32
32
|
const { children, initialState } = props;
|
|
33
33
|
const [state, dispatch] = react_1.default.useReducer(reducer, Object.assign(Object.assign({}, exports.defaultState), initialState));
|
|
34
|
+
// Keep the props up to date (isOpen is managed by ModalWrapper itself),
|
|
35
|
+
// otherwise a stale onRequestClose callback would be called
|
|
36
|
+
const { onRequestClose, closeOnEscape, closeOnOverlayClick, destroyTimeout } = initialState;
|
|
37
|
+
react_1.default.useEffect(() => {
|
|
38
|
+
dispatch((0, exports.actionSetState)({ onRequestClose, closeOnEscape, closeOnOverlayClick, destroyTimeout }));
|
|
39
|
+
}, [onRequestClose, closeOnEscape, closeOnOverlayClick, destroyTimeout]);
|
|
34
40
|
return react_1.default.createElement(context.Provider, { value: { state, dispatch } }, children);
|
|
35
41
|
};
|
|
36
42
|
exports.ContextProvider = ContextProvider;
|
package/Modal/BaseModal/index.js
CHANGED
|
@@ -14,7 +14,6 @@ const RenderModal_1 = __importDefault(require("./RenderModal"));
|
|
|
14
14
|
const NoSSR_1 = __importDefault(require("../../NoSSR"));
|
|
15
15
|
const BaseModal = props => {
|
|
16
16
|
const { isOpen, children, overrides, autofocus = true, closeOnOverlayClick = context_1.defaultState.closeOnOverlayClick, closeOnEscape = context_1.defaultState.closeOnEscape, destroyTimeout = context_1.defaultState.destroyTimeout, onRequestClose, } = props;
|
|
17
|
-
const destroyTimeoutRef = react_1.default.useRef(null);
|
|
18
17
|
const [domLoaded, setDomLoaded] = react_1.default.useState(false);
|
|
19
18
|
const overridesMap = react_1.default.useMemo(() => ({
|
|
20
19
|
Overlay: (overrides === null || overrides === void 0 ? void 0 : overrides.Overlay) || ModalOverlay_1.default,
|
|
@@ -26,13 +25,6 @@ const BaseModal = props => {
|
|
|
26
25
|
* Client render detection
|
|
27
26
|
*/
|
|
28
27
|
setDomLoaded(true);
|
|
29
|
-
const timeout = destroyTimeoutRef.current;
|
|
30
|
-
// unload destroy timeout
|
|
31
|
-
return () => {
|
|
32
|
-
if (timeout) {
|
|
33
|
-
clearTimeout(timeout);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
28
|
}, []);
|
|
37
29
|
const portalEl = react_1.default.useMemo(() => {
|
|
38
30
|
if (typeof window === 'undefined') {
|
package/Popper/Popper.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export interface PopperProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
56
56
|
* if the preferred placement doesn't fit in the viewport.
|
|
57
57
|
* The component will iterate through possible placements until it finds one that fits.
|
|
58
58
|
*
|
|
59
|
-
* @default
|
|
59
|
+
* @default false
|
|
60
60
|
* @example
|
|
61
61
|
* ```tsx
|
|
62
62
|
* <Popper autoFlip={true}>...</Popper> // Will flip if needed
|
|
@@ -95,7 +95,7 @@ export interface PopperProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
95
95
|
* Used to prevent the popper from being positioned too close to the screen boundaries.
|
|
96
96
|
* The popper will try to flip to another placement if it cannot maintain this margin.
|
|
97
97
|
*
|
|
98
|
-
* @default
|
|
98
|
+
* @default 30
|
|
99
99
|
* @example
|
|
100
100
|
* ```tsx
|
|
101
101
|
* // Larger margin for better visibility
|
package/Popper/Popper.js
CHANGED
|
@@ -67,17 +67,19 @@ const Popper = (props, ref) => {
|
|
|
67
67
|
}
|
|
68
68
|
const newNode = window.document.createElement('div');
|
|
69
69
|
newNode.setAttribute('id', exports.PORTAL_ID);
|
|
70
|
-
if (positionStrategy === 'absolute') {
|
|
71
|
-
newNode.style.position = 'relative';
|
|
72
|
-
}
|
|
73
70
|
window.document.body.appendChild(newNode);
|
|
74
71
|
return newNode;
|
|
75
|
-
}, [
|
|
72
|
+
}, []);
|
|
73
|
+
const onAnchorPosChangedRef = react_1.default.useRef(onAnchorPosChanged);
|
|
74
|
+
onAnchorPosChangedRef.current = onAnchorPosChanged;
|
|
75
|
+
const prevPlacementRef = react_1.default.useRef(actualPlacement);
|
|
76
76
|
react_1.default.useEffect(() => {
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
var _a;
|
|
78
|
+
if (prevPlacementRef.current !== actualPlacement) {
|
|
79
|
+
prevPlacementRef.current = actualPlacement;
|
|
80
|
+
(_a = onAnchorPosChangedRef.current) === null || _a === void 0 ? void 0 : _a.call(onAnchorPosChangedRef, actualPlacement);
|
|
79
81
|
}
|
|
80
|
-
}, [
|
|
82
|
+
}, [actualPlacement]);
|
|
81
83
|
const childrenMemo = react_1.default.useMemo(() => children, [children]);
|
|
82
84
|
const renderNode = react_1.default.useCallback(() => (react_1.default.createElement(overridesMap.Container, Object.assign({}, nativeProps, { style: Object.assign(Object.assign(Object.assign(Object.assign({}, style), nativeProps.style), { opacity: isVisible ? 1 : 0, pointerEvents: isVisible ? 'auto' : 'none' }), (positionStrategy === 'fixed' && {
|
|
83
85
|
position: 'fixed',
|