Skip to content

Commit b69838a

Browse files
committed
feat: add useCollapseChildren hook
1 parent 5ff5047 commit b69838a

File tree

8 files changed

+446
-89
lines changed

8 files changed

+446
-89
lines changed

src/components/Breadcrumbs/Breadcrumbs.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ $block: '.#{variables.$ns}breadcrumbs';
3737

3838
&_calculating {
3939
overflow: visible;
40+
41+
#{$block}__link {
42+
overflow: visible;
43+
}
4044
}
4145
}
4246

src/components/Breadcrumbs/Breadcrumbs.tsx

Lines changed: 17 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import * as React from 'react';
44

55
import {useForkRef, useResizeObserver} from '../../hooks';
6+
import {useCollapseChildren} from '../../hooks/useCollapseChildren';
67
import type {PopupPlacement} from '../Popup';
78
import type {AriaLabelingProps, DOMProps, Key, QAProps} from '../types';
89
import {filterDOMProps} from '../utils/filterDOMProps';
@@ -35,6 +36,7 @@ export const Breadcrumbs = React.forwardRef(function Breadcrumbs(
3536
) {
3637
const listRef = React.useRef<HTMLOListElement>(null);
3738
const containerRef = useForkRef(ref, listRef);
39+
const menuRef = React.useRef<HTMLLIElement>(null);
3840
const endContentRef = React.useRef<HTMLLIElement>(null);
3941

4042
const items: React.ReactElement<any>[] = [];
@@ -47,103 +49,30 @@ export const Breadcrumbs = React.forwardRef(function Breadcrumbs(
4749
}
4850
});
4951

50-
const [visibleItemsCount, setVisibleItemsCount] = React.useState(items.length);
51-
const [calculated, setCalculated] = React.useState(false);
52-
const recalculate = (visibleItems: number) => {
53-
const list = listRef.current;
54-
if (!list) {
55-
return;
56-
}
57-
const listItems = Array.from(list.children) as HTMLElement[];
58-
const endElement = endContentRef.current;
59-
if (endElement) {
60-
listItems.pop();
61-
}
62-
if (listItems.length === 0) {
63-
setCalculated(true);
64-
return;
65-
}
66-
const containerWidth = list.offsetWidth - (endElement?.offsetWidth ?? 0);
67-
let newVisibleItemsCount = 0;
68-
let calculatedWidth = 0;
69-
let maxItems = props.maxItems || Infinity;
70-
71-
let rootWidth = 0;
72-
if (props.showRoot) {
73-
const item = listItems.shift();
74-
if (item) {
75-
rootWidth = item.offsetWidth;
76-
calculatedWidth += rootWidth;
77-
}
78-
newVisibleItemsCount++;
79-
}
80-
81-
const hasMenu = items.length > visibleItems;
82-
if (hasMenu) {
83-
const item = listItems.shift();
84-
if (item) {
85-
calculatedWidth += item.offsetWidth;
86-
}
87-
maxItems--;
88-
}
89-
90-
if (props.showRoot && calculatedWidth >= containerWidth) {
91-
calculatedWidth -= rootWidth;
92-
newVisibleItemsCount--;
93-
}
94-
95-
const lastItem = listItems.pop();
96-
if (lastItem) {
97-
calculatedWidth += Math.min(lastItem.offsetWidth, 200);
98-
if (calculatedWidth < containerWidth) {
99-
newVisibleItemsCount++;
100-
}
101-
}
102-
103-
for (let i = listItems.length - 1; i >= 0; i--) {
104-
const item = listItems[i];
105-
calculatedWidth += item.offsetWidth;
106-
if (calculatedWidth >= containerWidth) {
107-
break;
108-
}
109-
newVisibleItemsCount++;
110-
}
111-
112-
newVisibleItemsCount = Math.max(Math.min(maxItems, newVisibleItemsCount), 1);
113-
if (newVisibleItemsCount === visibleItemsCount) {
114-
setCalculated(true);
115-
} else {
116-
setVisibleItemsCount(newVisibleItemsCount);
117-
}
118-
};
119-
120-
const handleResize = React.useCallback(() => {
121-
setVisibleItemsCount(items.length);
122-
setCalculated(false);
123-
}, [items.length]);
124-
useResizeObserver({
125-
ref: listRef,
126-
onResize: handleResize,
52+
const {
53+
calculated,
54+
recalculate,
55+
visibleCount: visibleItemsCount,
56+
} = useCollapseChildren({
57+
containerRef: listRef,
58+
preservedRefs: [menuRef, endContentRef],
59+
direction: 'start',
60+
minCount: 1,
61+
maxCount: typeof props.maxItems === 'number' ? props.maxItems - 1 : undefined,
12762
});
63+
12864
useResizeObserver({
12965
ref: props.endContent ? endContentRef : undefined,
130-
onResize: handleResize,
66+
onResize: recalculate,
13167
});
13268

13369
const lastChildren = React.useRef<typeof props.children | null>(null);
13470
React.useLayoutEffect(() => {
13571
if (calculated && props.children !== lastChildren.current) {
13672
lastChildren.current = props.children;
137-
setVisibleItemsCount(items.length);
138-
setCalculated(false);
139-
}
140-
}, [calculated, items.length, props.children]);
141-
142-
React.useLayoutEffect(() => {
143-
if (!calculated) {
144-
recalculate(visibleItemsCount);
73+
recalculate();
14574
}
146-
});
75+
}, [calculated, recalculate, props.children]);
14776

14877
let contents = items;
14978
if (items.length > visibleItemsCount) {
@@ -220,6 +149,7 @@ export const Breadcrumbs = React.forwardRef(function Breadcrumbs(
220149
}
221150
return (
222151
<li
152+
ref={isMenu ? menuRef : undefined}
223153
key={isMenu ? 'menu' : `item-${key}`}
224154
className={b('item', {calculating: isCurrent && !calculated, current: isCurrent})}
225155
>

src/demo/ShowcaseItem/ShowcaseItem.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import './ShowcaseItem.scss';
77
interface ShowcaseItemProps {
88
title: string;
99
children: React.ReactNode;
10+
className?: string;
1011
}
1112

1213
const b = cn('showcase-item');
1314

14-
export function ShowcaseItem({title, children}: ShowcaseItemProps) {
15+
export function ShowcaseItem({title, children, className}: ShowcaseItemProps) {
1516
return (
16-
<div className={b()}>
17+
<div className={b(null, className)}>
1718
<div className={b('title')}>{title}</div>
1819
<div className={b('content')}>{children}</div>
1920
</div>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!--GITHUB_BLOCK-->
2+
3+
# useCollapseChildren
4+
5+
<!--/GITHUB_BLOCK-->
6+
7+
```tsx
8+
import {useCollapseChildren} from '@gravity-ui/uikit';
9+
```
10+
11+
The `useCollapseChildren` hook calculates visible children count for a specified container element.
12+
13+
## Properties
14+
15+
| Name | Description | Type | Default |
16+
| :------------ | :------------------------------------------------------------------ | :------------------------------: | :--------: |
17+
| enabled | Whether or not the hook is enabled. | `boolean` | `true` |
18+
| containerRef | React ref for the container element. | `React.RefObject` | |
19+
| preservedRefs | React refs for elements that should not participate in calculation. | `React.RefObject[]` | |
20+
| minCount | The minimum count of items to be visible. | `number` | `0` |
21+
| maxCount | The maximum count of items to be visible. | `number` | `Infinity` |
22+
| direction | Collapse direction of items. | `"start"` `"end"` | `"end"` |
23+
| gap | The distance between items. | `number` | `0` |
24+
| childSelector | CSS-selector to pick child items in the container. | `string` | `"*"` |
25+
| getChildWidth | Custom measure function of item's width. | `(child: HTMLElement) => number` | |
26+
27+
## Result
28+
29+
```ts
30+
interface UseCollapseChildrenResult {
31+
/**
32+
* Whether or not calulation is complete.
33+
* Your items should be in measurable state when it's not calculated.
34+
*/
35+
calculated: boolean;
36+
/**
37+
* Trigger recalculation manually.
38+
*/
39+
recalculate: () => void;
40+
/**
41+
* Nubmer of items that can be visible in the container, excluding preserved items.
42+
*/
43+
visibleCount: number;
44+
}
45+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.use-collapse-children-story {
2+
&__showcase {
3+
width: 100%;
4+
}
5+
6+
&__list {
7+
display: flex;
8+
overflow: hidden;
9+
}
10+
11+
&__item {
12+
box-sizing: border-box;
13+
flex: 0 0 auto;
14+
padding: 5px;
15+
border: 2px dashed rgb(188 143 143);
16+
background-color: rgb(221 190 225);
17+
white-space: nowrap;
18+
text-align: center;
19+
20+
&_root {
21+
border-color: rgb(164, 63, 63);
22+
background-color: rgb(236, 112, 112);
23+
}
24+
25+
&_active {
26+
border-color: rgb(63, 164, 74);
27+
background-color: rgb(112, 236, 124);
28+
}
29+
30+
&_more {
31+
border-color: rgb(63, 107, 164);
32+
background-color: rgb(112, 193, 236);
33+
}
34+
35+
&_current:not(&_calculating) {
36+
flex-shrink: 1;
37+
overflow: hidden;
38+
text-overflow: ellipsis;
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)