@sima-land/ui-nucleons 44.2.0 → 44.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/expandable/expandable-group.module.scss +5 -3
- package/expandable/index.d.ts +1 -1
- package/expandable/index.js +23 -15
- package/expandable/utils.d.ts +17 -9
- package/expandable/utils.js +63 -59
- package/package.json +1 -1
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
.inner {
|
|
6
6
|
display: flex;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
7
9
|
flex-wrap: wrap;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
.item {
|
|
13
|
+
display: block;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
11
16
|
max-width: 100%;
|
|
12
17
|
&.hidden {
|
|
13
18
|
display: none;
|
|
14
19
|
}
|
|
15
|
-
&.invisible {
|
|
16
|
-
opacity: 0;
|
|
17
|
-
}
|
|
18
20
|
}
|
package/expandable/index.d.ts
CHANGED
|
@@ -36,5 +36,5 @@ export interface GroupItemProps {
|
|
|
36
36
|
export declare function ExpandableGroup({ gap, itemHeight, lineLimit, defaultExpanded, children, onExpand, opener, className, style, expanded: expandedProp, }: ExpandableGroupProps): JSX.Element;
|
|
37
37
|
export declare const Expandable: {
|
|
38
38
|
readonly Group: typeof ExpandableGroup;
|
|
39
|
-
readonly Item: React.ForwardRefExoticComponent<GroupItemProps & React.RefAttributes<
|
|
39
|
+
readonly Item: React.ForwardRefExoticComponent<GroupItemProps & React.RefAttributes<HTMLLIElement>>;
|
|
40
40
|
};
|
package/expandable/index.js
CHANGED
|
@@ -33,7 +33,6 @@ const bind_1 = __importDefault(require("classnames/bind"));
|
|
|
33
33
|
const expandable_group_module_scss_1 = __importDefault(require("./expandable-group.module.scss"));
|
|
34
34
|
const cx = bind_1.default.bind(expandable_group_module_scss_1.default);
|
|
35
35
|
const GroupContext = (0, react_1.createContext)({ gap: 8, itemHeight: 32 });
|
|
36
|
-
const ItemContext = (0, react_1.createContext)({});
|
|
37
36
|
/**
|
|
38
37
|
* Группа элементов с ограничением на число выводимых строк и возможностью показать все.
|
|
39
38
|
* @param props Свойства.
|
|
@@ -42,36 +41,45 @@ const ItemContext = (0, react_1.createContext)({});
|
|
|
42
41
|
function ExpandableGroup({ gap = 8, itemHeight = 32, lineLimit = 2, defaultExpanded = false, children, onExpand, opener = data => react_1.default.createElement(react_1.default.Fragment, null,
|
|
43
42
|
"\u0415\u0449\u0451 ",
|
|
44
43
|
data.hiddenCount), className, style, expanded: expandedProp, }) {
|
|
44
|
+
const wrapperRef = (0, react_1.useRef)(null);
|
|
45
45
|
const containerRef = (0, react_1.useRef)(null);
|
|
46
46
|
const openerRef = (0, react_1.useRef)(null);
|
|
47
|
+
const [mounted, setMounted] = (0, react_1.useState)(false);
|
|
47
48
|
const [expanded, setExpanded] = (0, react_1.useState)(defaultExpanded);
|
|
48
|
-
const
|
|
49
|
-
const needHideItems = viewState.lastVisibleIndex !== -1 && !expanded;
|
|
50
|
-
const items = react_1.Children.toArray(children).reduce((result, child, index) => {
|
|
51
|
-
const isDisplayed = !needHideItems || index < viewState.lastVisibleIndex;
|
|
49
|
+
const items = react_1.Children.toArray(children).reduce((result, child) => {
|
|
52
50
|
if ((0, react_1.isValidElement)(child) && child.type === ExpandableItem) {
|
|
53
|
-
result.push(
|
|
51
|
+
result.push(child);
|
|
54
52
|
}
|
|
55
53
|
return result;
|
|
56
54
|
}, []);
|
|
55
|
+
(0, react_1.useEffect)(() => {
|
|
56
|
+
setMounted(true);
|
|
57
|
+
}, []);
|
|
57
58
|
(0, react_1.useEffect)(() => {
|
|
58
59
|
expandedProp !== undefined && setExpanded(expandedProp);
|
|
59
60
|
}, [expandedProp]);
|
|
61
|
+
(0, react_1.useEffect)(() => {
|
|
62
|
+
setMounted(true);
|
|
63
|
+
}, []);
|
|
64
|
+
const { hiddenCount } = (0, utils_1.useExpandable)({
|
|
65
|
+
expanded,
|
|
66
|
+
wrapperRef,
|
|
67
|
+
containerRef,
|
|
68
|
+
openerRef,
|
|
69
|
+
});
|
|
60
70
|
return (react_1.default.createElement(GroupContext.Provider, { value: { gap, itemHeight } },
|
|
61
|
-
react_1.default.createElement("div", { ref:
|
|
62
|
-
react_1.default.createElement("
|
|
71
|
+
react_1.default.createElement("div", { ref: wrapperRef, className: cx('root', className), style: Object.assign(Object.assign({}, style), { maxHeight: expanded ? undefined : `${itemHeight * lineLimit + gap * (lineLimit - 1)}px` }) },
|
|
72
|
+
react_1.default.createElement("ul", { ref: containerRef, className: expandable_group_module_scss_1.default.inner, style: { marginRight: `${-gap}px`, marginBottom: `${-gap}px` } },
|
|
63
73
|
items,
|
|
64
|
-
!expanded && (react_1.default.createElement(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}, "data-testid": 'expandable:opener' }, opener({ hiddenCount: items.length - viewState.lastVisibleIndex }))))))));
|
|
74
|
+
mounted && !expanded && (react_1.default.createElement(ExpandableItem, { ref: openerRef, onClick: () => {
|
|
75
|
+
setExpanded(true);
|
|
76
|
+
onExpand === null || onExpand === void 0 ? void 0 : onExpand();
|
|
77
|
+
}, "data-testid": 'expandable:opener' }, opener({ hiddenCount })))))));
|
|
69
78
|
}
|
|
70
79
|
exports.ExpandableGroup = ExpandableGroup;
|
|
71
80
|
const ExpandableItem = (0, react_1.forwardRef)(({ children, onClick, 'data-testid': testId = 'expandable:item' }, ref) => {
|
|
72
81
|
const { gap, itemHeight } = (0, react_1.useContext)(GroupContext);
|
|
73
|
-
|
|
74
|
-
return (react_1.default.createElement("div", { ref: ref, className: cx('item', { hidden, invisible }), "data-testid": testId, onClick: onClick, style: {
|
|
82
|
+
return (react_1.default.createElement("li", { ref: ref, className: cx('item'), "data-testid": testId, onClick: onClick, style: {
|
|
75
83
|
height: `${itemHeight}px`,
|
|
76
84
|
margin: `0 ${gap}px ${gap}px 0`,
|
|
77
85
|
} }, children));
|
package/expandable/utils.d.ts
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
2
|
export interface ViewState {
|
|
3
|
-
phase: 'default' | 'ready';
|
|
4
3
|
lastVisibleIndex: number;
|
|
5
4
|
}
|
|
6
|
-
export declare const initialViewState: ViewState;
|
|
7
5
|
/**
|
|
8
6
|
* Хук состояния группы элементов ограниченных несколькими строками с возможностью показать все.
|
|
9
|
-
* @param
|
|
10
|
-
* @param
|
|
11
|
-
* @
|
|
7
|
+
* @param options.expanded Развернут ли список.
|
|
8
|
+
* @param options.wrapperRef Реф элемента, формирующего ограничение по высоте.
|
|
9
|
+
* @param options.containerRef Реф элемента, содержащего список.
|
|
10
|
+
* @param options.openerRef Реф элемента "открывашки".
|
|
11
|
+
* @return Рассчитанные данные: кол-во скрытых элементов.
|
|
12
12
|
*/
|
|
13
|
-
export declare function
|
|
13
|
+
export declare function useExpandable({ expanded, wrapperRef, containerRef, openerRef, }: {
|
|
14
|
+
expanded: boolean;
|
|
15
|
+
wrapperRef: RefObject<HTMLElement>;
|
|
16
|
+
containerRef: RefObject<HTMLElement>;
|
|
17
|
+
openerRef: RefObject<HTMLElement>;
|
|
18
|
+
}): {
|
|
19
|
+
hiddenCount: number;
|
|
20
|
+
};
|
|
14
21
|
/**
|
|
15
22
|
* Определяет индекс последнего видимого элемента для свернутого списка.
|
|
16
|
-
* @param
|
|
17
|
-
* @param
|
|
23
|
+
* @param wrapper Элемент, формирующий ограничение по высоте.
|
|
24
|
+
* @param items Элементы, все кроме "закрывашки".
|
|
25
|
+
* @param opener Элемент "закрывашки".
|
|
18
26
|
* @return Состояние.
|
|
19
27
|
*/
|
|
20
|
-
export declare function defineViewState(
|
|
28
|
+
export declare function defineViewState(wrapper: HTMLElement, items: Element[], opener: HTMLElement): ViewState;
|
package/expandable/utils.js
CHANGED
|
@@ -3,78 +3,82 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.defineViewState = exports.
|
|
6
|
+
exports.defineViewState = exports.useExpandable = void 0;
|
|
7
7
|
const react_1 = require("react");
|
|
8
8
|
const hooks_1 = require("../hooks");
|
|
9
9
|
const on_1 = __importDefault(require("../helpers/on"));
|
|
10
|
-
|
|
11
|
-
phase: 'default',
|
|
12
|
-
lastVisibleIndex: -1,
|
|
13
|
-
};
|
|
10
|
+
const expandable_group_module_scss_1 = __importDefault(require("./expandable-group.module.scss"));
|
|
14
11
|
/**
|
|
15
12
|
* Хук состояния группы элементов ограниченных несколькими строками с возможностью показать все.
|
|
16
|
-
* @param
|
|
17
|
-
* @param
|
|
18
|
-
* @
|
|
13
|
+
* @param options.expanded Развернут ли список.
|
|
14
|
+
* @param options.wrapperRef Реф элемента, формирующего ограничение по высоте.
|
|
15
|
+
* @param options.containerRef Реф элемента, содержащего список.
|
|
16
|
+
* @param options.openerRef Реф элемента "открывашки".
|
|
17
|
+
* @return Рассчитанные данные: кол-во скрытых элементов.
|
|
19
18
|
*/
|
|
20
|
-
function
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
function useExpandable({ expanded, wrapperRef, containerRef, openerRef, }) {
|
|
20
|
+
const [hiddenCount, setHiddenCount] = (0, react_1.useState)(0);
|
|
21
|
+
const calc = (0, react_1.useCallback)(() => {
|
|
22
|
+
const wrapperEl = wrapperRef.current;
|
|
23
|
+
const containerEl = containerRef.current;
|
|
24
|
+
const openerEl = openerRef.current;
|
|
25
|
+
if (expanded) {
|
|
26
|
+
if (containerEl) {
|
|
27
|
+
for (const item of containerEl.children) {
|
|
28
|
+
item.classList.remove(expandable_group_module_scss_1.default.hidden);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
32
31
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
else {
|
|
33
|
+
if (wrapperEl && containerEl && openerEl) {
|
|
34
|
+
const items = [...containerEl.children].filter(child => child !== openerEl);
|
|
35
|
+
// делаем все элементы списка видимыми чтобы правильно считать размеры
|
|
36
|
+
for (const item of items) {
|
|
37
|
+
item.classList.remove(expandable_group_module_scss_1.default.hidden);
|
|
38
|
+
}
|
|
39
|
+
// определяем какие элементы не влезли в ограничение по высоте
|
|
40
|
+
const state = defineViewState(wrapperEl, items, openerEl);
|
|
41
|
+
// делаем невидимыми элементы списка которые не влезли
|
|
42
|
+
for (const [index, item] of items.entries()) {
|
|
43
|
+
if (item !== openerEl && index >= state.lastVisibleIndex) {
|
|
44
|
+
item.classList.add(expandable_group_module_scss_1.default.hidden);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
setHiddenCount(items.length - state.lastVisibleIndex);
|
|
48
|
+
}
|
|
39
49
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
}, [expanded]);
|
|
51
|
+
// пересчитываем после каждого rerender'а
|
|
52
|
+
(0, hooks_1.useIsomorphicLayoutEffect)(calc);
|
|
53
|
+
// пересчитываем при resize
|
|
54
|
+
(0, react_1.useEffect)(() => (0, on_1.default)(window, 'resize', calc), [calc]);
|
|
55
|
+
return { hiddenCount };
|
|
43
56
|
}
|
|
44
|
-
exports.
|
|
57
|
+
exports.useExpandable = useExpandable;
|
|
45
58
|
/**
|
|
46
59
|
* Определяет индекс последнего видимого элемента для свернутого списка.
|
|
47
|
-
* @param
|
|
48
|
-
* @param
|
|
60
|
+
* @param wrapper Элемент, формирующий ограничение по высоте.
|
|
61
|
+
* @param items Элементы, все кроме "закрывашки".
|
|
62
|
+
* @param opener Элемент "закрывашки".
|
|
49
63
|
* @return Состояние.
|
|
50
64
|
*/
|
|
51
|
-
function defineViewState(
|
|
52
|
-
const state = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const lastVisibleIndex = firstHiddenNodeIndex - 1;
|
|
69
|
-
const lastVisible = childList[lastVisibleIndex];
|
|
70
|
-
if (lastVisible) {
|
|
71
|
-
const rightBound = lastVisible.getBoundingClientRect().right;
|
|
72
|
-
const rightContainerBound = container.getBoundingClientRect().right;
|
|
73
|
-
// проверяем, хватает ли кнопке места (с запасом в половину) после последнего видимого дочернего элемента
|
|
74
|
-
const isOpenerFit = rightContainerBound - rightBound >= opener.clientWidth * 1.5;
|
|
75
|
-
// если хватает места - ставим кнопку после последнего, иначе - вместо
|
|
76
|
-
state.lastVisibleIndex = lastVisibleIndex + Number(isOpenerFit);
|
|
77
|
-
}
|
|
65
|
+
function defineViewState(wrapper, items, opener) {
|
|
66
|
+
const state = { lastVisibleIndex: -1 };
|
|
67
|
+
const parentRect = wrapper.getBoundingClientRect();
|
|
68
|
+
const firstHiddenNodeIndex = items.findIndex(child => {
|
|
69
|
+
const childRect = child.getBoundingClientRect();
|
|
70
|
+
return childRect.top - parentRect.top >= parentRect.height;
|
|
71
|
+
});
|
|
72
|
+
if (firstHiddenNodeIndex !== -1) {
|
|
73
|
+
const lastVisibleIndex = firstHiddenNodeIndex - 1;
|
|
74
|
+
const lastVisible = items[lastVisibleIndex];
|
|
75
|
+
if (lastVisible) {
|
|
76
|
+
const rightBound = lastVisible.getBoundingClientRect().right;
|
|
77
|
+
const rightContainerBound = wrapper.getBoundingClientRect().right;
|
|
78
|
+
// проверяем, хватает ли кнопке места (с запасом в половину) после последнего видимого дочернего элемента
|
|
79
|
+
const isOpenerFit = rightContainerBound - rightBound >= opener.clientWidth * 1.5;
|
|
80
|
+
// если хватает места - ставим кнопку после последнего, иначе - вместо
|
|
81
|
+
state.lastVisibleIndex = lastVisibleIndex + Number(isOpenerFit);
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
return state;
|