@snack-uikit/breadcrumbs 0.4.14-preview-104ad22c.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/CHANGELOG.md +286 -0
- package/LICENSE +201 -0
- package/README.md +40 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +33 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.js +49 -0
- package/dist/components/Breadcrumbs/index.d.ts +1 -0
- package/dist/components/Breadcrumbs/index.js +1 -0
- package/dist/components/Collapse/Collapse.d.ts +6 -0
- package/dist/components/Collapse/Collapse.js +23 -0
- package/dist/components/Collapse/index.d.ts +1 -0
- package/dist/components/Collapse/index.js +1 -0
- package/dist/components/Collapse/styles.module.css +18 -0
- package/dist/components/Crumb/Crumb.d.ts +13 -0
- package/dist/components/Crumb/Crumb.js +50 -0
- package/dist/components/Crumb/index.d.ts +1 -0
- package/dist/components/Crumb/index.js +1 -0
- package/dist/components/Crumb/styles.module.css +55 -0
- package/dist/components/CrumbsTypography/CrumbsTypography.d.ts +7 -0
- package/dist/components/CrumbsTypography/CrumbsTypography.js +6 -0
- package/dist/components/CrumbsTypography/index.d.ts +1 -0
- package/dist/components/CrumbsTypography/index.js +1 -0
- package/dist/components/HiddenChain/HiddenChain.d.ts +11 -0
- package/dist/components/HiddenChain/HiddenChain.js +27 -0
- package/dist/components/HiddenChain/hooks.d.ts +6 -0
- package/dist/components/HiddenChain/hooks.js +28 -0
- package/dist/components/HiddenChain/index.d.ts +1 -0
- package/dist/components/HiddenChain/index.js +1 -0
- package/dist/components/Separator/Separator.d.ts +1 -0
- package/dist/components/Separator/Separator.js +11 -0
- package/dist/components/Separator/index.d.ts +1 -0
- package/dist/components/Separator/index.js +1 -0
- package/dist/components/Separator/styles.module.css +15 -0
- package/dist/components/Wrapper/Wrapper.d.ts +19 -0
- package/dist/components/Wrapper/Wrapper.js +21 -0
- package/dist/components/Wrapper/index.d.ts +1 -0
- package/dist/components/Wrapper/index.js +1 -0
- package/dist/components/Wrapper/styles.module.css +19 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/styles.module.css +3 -0
- package/dist/constants.d.ts +16 -0
- package/dist/constants.js +19 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.js +7 -0
- package/dist/hooks.d.ts +21 -0
- package/dist/hooks.js +78 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.js +1 -0
- package/dist/utils/buildBreadcrumbsConfigs.d.ts +2 -0
- package/dist/utils/buildBreadcrumbsConfigs.js +77 -0
- package/dist/utils/buildSizeMap.d.ts +2 -0
- package/dist/utils/buildSizeMap.js +26 -0
- package/dist/utils/getMaxPossibleWidth.d.ts +6 -0
- package/dist/utils/getMaxPossibleWidth.js +16 -0
- package/dist/utils/getTestId.d.ts +1 -0
- package/dist/utils/getTestId.js +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +4 -0
- package/package.json +42 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +112 -0
- package/src/components/Breadcrumbs/index.ts +1 -0
- package/src/components/Collapse/Collapse.tsx +52 -0
- package/src/components/Collapse/index.ts +1 -0
- package/src/components/Collapse/styles.module.scss +22 -0
- package/src/components/Crumb/Crumb.tsx +118 -0
- package/src/components/Crumb/index.ts +1 -0
- package/src/components/Crumb/styles.module.scss +66 -0
- package/src/components/CrumbsTypography/CrumbsTypography.tsx +24 -0
- package/src/components/CrumbsTypography/index.ts +1 -0
- package/src/components/HiddenChain/HiddenChain.tsx +54 -0
- package/src/components/HiddenChain/hooks.tsx +41 -0
- package/src/components/HiddenChain/index.ts +1 -0
- package/src/components/Separator/Separator.tsx +23 -0
- package/src/components/Separator/index.ts +1 -0
- package/src/components/Separator/styles.module.scss +16 -0
- package/src/components/Wrapper/Wrapper.tsx +38 -0
- package/src/components/Wrapper/index.ts +1 -0
- package/src/components/Wrapper/styles.module.scss +26 -0
- package/src/components/index.ts +1 -0
- package/src/components/styles.module.scss +3 -0
- package/src/constants.ts +19 -0
- package/src/context.ts +16 -0
- package/src/hooks.tsx +99 -0
- package/src/index.ts +2 -0
- package/src/types.ts +60 -0
- package/src/utils/buildBreadcrumbsConfigs.ts +116 -0
- package/src/utils/buildSizeMap.ts +33 -0
- package/src/utils/getMaxPossibleWidth.ts +17 -0
- package/src/utils/getTestId.ts +1 -0
- package/src/utils/index.ts +4 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ElementType, ItemRenderMode } from '../constants';
|
|
2
|
+
const RENDER_MODE_WEIGHT = {
|
|
3
|
+
[ItemRenderMode.Full]: 0,
|
|
4
|
+
[ItemRenderMode.ShortLabel]: 1,
|
|
5
|
+
[ItemRenderMode.Ellipsis]: 100,
|
|
6
|
+
[ItemRenderMode.Collapsed]: 10000,
|
|
7
|
+
};
|
|
8
|
+
const RENDER_MODE_WITH_WIDTH = [ItemRenderMode.Full, ItemRenderMode.ShortLabel, ItemRenderMode.Ellipsis];
|
|
9
|
+
const startsWith = (renderMode) => (item, tail) => [Object.assign(Object.assign({}, item), { renderMode }), ...tail];
|
|
10
|
+
const startsWithFull = startsWith(ItemRenderMode.Full);
|
|
11
|
+
const startsWithShortLabel = startsWith(ItemRenderMode.ShortLabel);
|
|
12
|
+
const startsWithEllipsis = startsWith(ItemRenderMode.Ellipsis);
|
|
13
|
+
const startsWithCollapsed = startsWith(ItemRenderMode.Collapsed);
|
|
14
|
+
function buildSubChain([current, ...rest], { useCollapse = true, useEllipse = true }) {
|
|
15
|
+
const result = [];
|
|
16
|
+
if (!current) {
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
if (!rest.length) {
|
|
20
|
+
result.push(startsWithFull(current, []));
|
|
21
|
+
current.shortLabel && result.push(startsWithShortLabel(current, []));
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
buildSubChain(rest, { useCollapse: false, useEllipse: false }).forEach(variant => {
|
|
25
|
+
result.push(startsWithFull(current, variant));
|
|
26
|
+
current.shortLabel && result.push(startsWithShortLabel(current, variant));
|
|
27
|
+
});
|
|
28
|
+
if (useEllipse) {
|
|
29
|
+
buildSubChain(rest, { useCollapse: false, useEllipse: true }).forEach(variant => {
|
|
30
|
+
result.push(startsWithEllipsis(current, variant));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (useCollapse) {
|
|
34
|
+
buildSubChain(rest, { useCollapse: true, useEllipse: true }).forEach(variant => {
|
|
35
|
+
result.push(startsWithCollapsed(current, variant));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
const collapseAllRest = (lastElementRenderMode, rest) => rest.map((element, index, array) => {
|
|
41
|
+
const lastItem = index === array.length - 1;
|
|
42
|
+
return Object.assign(Object.assign({}, element), { renderMode: lastItem ? lastElementRenderMode : ItemRenderMode.Collapsed });
|
|
43
|
+
});
|
|
44
|
+
export function buildBreadcrumbsConfigs(items, sizeMap) {
|
|
45
|
+
const chains = [];
|
|
46
|
+
const [first, ...rest] = items.map(item => (Object.assign(Object.assign({}, item), { renderMode: ItemRenderMode.Full })));
|
|
47
|
+
buildSubChain(rest, { useCollapse: true, useEllipse: true }).forEach(subset => {
|
|
48
|
+
chains.push(startsWithFull(first, subset));
|
|
49
|
+
first.shortLabel && chains.push(startsWithShortLabel(first, subset));
|
|
50
|
+
});
|
|
51
|
+
/** Первый элемент можно схлопывать/сокращать только когда уже сокращено все что можно */
|
|
52
|
+
chains.push(startsWithEllipsis(first, collapseAllRest(ItemRenderMode.Full, rest)));
|
|
53
|
+
chains.push(startsWithEllipsis(first, collapseAllRest(ItemRenderMode.Ellipsis, rest)));
|
|
54
|
+
chains.push(startsWithCollapsed(first, collapseAllRest(ItemRenderMode.Full, rest)));
|
|
55
|
+
chains.push(startsWithCollapsed(first, collapseAllRest(ItemRenderMode.Ellipsis, rest)));
|
|
56
|
+
return chains.map(chain => chain.reduce((acc, item, index) => {
|
|
57
|
+
const { renderMode } = item;
|
|
58
|
+
if (index && RENDER_MODE_WITH_WIDTH.includes(renderMode)) {
|
|
59
|
+
acc.width += sizeMap.separator;
|
|
60
|
+
acc.chain.push({ element: ElementType.Separator, width: sizeMap.separator });
|
|
61
|
+
}
|
|
62
|
+
if (!acc.hasCollapsed && renderMode === ItemRenderMode.Collapsed) {
|
|
63
|
+
if (index) {
|
|
64
|
+
acc.width += sizeMap.separator;
|
|
65
|
+
acc.chain.push({ element: ElementType.Separator, width: sizeMap.separator });
|
|
66
|
+
}
|
|
67
|
+
acc.hasCollapsed = true;
|
|
68
|
+
acc.width += sizeMap.collapse;
|
|
69
|
+
acc.chain.push({ element: ElementType.Collapse, width: sizeMap.collapse });
|
|
70
|
+
}
|
|
71
|
+
const width = sizeMap.items[item.id][renderMode];
|
|
72
|
+
acc.weight += RENDER_MODE_WEIGHT[item.renderMode];
|
|
73
|
+
acc.width += width;
|
|
74
|
+
acc.chain.push({ element: ElementType.Item, item, width });
|
|
75
|
+
return acc;
|
|
76
|
+
}, { chain: [], weight: 0, width: 0, hasCollapsed: false }));
|
|
77
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ItemRenderMode } from '../constants';
|
|
2
|
+
const getEmptyItemSizeMap = () => ({
|
|
3
|
+
[ItemRenderMode.ShortLabel]: 0,
|
|
4
|
+
[ItemRenderMode.Collapsed]: 0,
|
|
5
|
+
[ItemRenderMode.Ellipsis]: 0,
|
|
6
|
+
[ItemRenderMode.Full]: 0,
|
|
7
|
+
});
|
|
8
|
+
const getElementWidth = (element) => element.getBoundingClientRect().width;
|
|
9
|
+
export function buildSizeMap(container) {
|
|
10
|
+
if (!container) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const children = container.children;
|
|
14
|
+
const [separator, collapse, ...itemElements] = Array.from(children);
|
|
15
|
+
return {
|
|
16
|
+
separator: getElementWidth(separator),
|
|
17
|
+
collapse: getElementWidth(collapse),
|
|
18
|
+
items: itemElements.reduce((result, element) => {
|
|
19
|
+
const id = element.getAttribute('data-id');
|
|
20
|
+
const renderMode = element.getAttribute('data-render-mode');
|
|
21
|
+
result[id] = result[id] || getEmptyItemSizeMap();
|
|
22
|
+
result[id][renderMode] = getElementWidth(element);
|
|
23
|
+
return result;
|
|
24
|
+
}, {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Функция предназначена для измерения максимально возможной ширины контейнера
|
|
3
|
+
* она вставляет широкую распорку, замеряет ширину и удаляет ее
|
|
4
|
+
* это происходит в одном обороте eventLoop, поэтому новый кадр с распоркой не рендерится браузером.
|
|
5
|
+
*/
|
|
6
|
+
export declare const getMaxPossibleWidth: (element: HTMLElement) => number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Функция предназначена для измерения максимально возможной ширины контейнера
|
|
3
|
+
* она вставляет широкую распорку, замеряет ширину и удаляет ее
|
|
4
|
+
* это происходит в одном обороте eventLoop, поэтому новый кадр с распоркой не рендерится браузером.
|
|
5
|
+
*/
|
|
6
|
+
export const getMaxPossibleWidth = (element) => {
|
|
7
|
+
if (!element) {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
const spreader = document.createElement('DIV');
|
|
11
|
+
spreader.style.width = '10000px';
|
|
12
|
+
element.append(spreader);
|
|
13
|
+
const width = element.getBoundingClientRect().width;
|
|
14
|
+
element.removeChild(spreader);
|
|
15
|
+
return width;
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getTestId: (prefix: string, testId?: string) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const getTestId = (prefix, testId) => (testId ? `${prefix}-element-${testId}` : '');
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@snack-uikit/breadcrumbs",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"title": "Breadcrumbs",
|
|
7
|
+
"version": "0.4.14-preview-104ad22c.0",
|
|
8
|
+
"sideEffects": [
|
|
9
|
+
"*.css",
|
|
10
|
+
"*.woff",
|
|
11
|
+
"*.woff2"
|
|
12
|
+
],
|
|
13
|
+
"description": "",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"homepage": "https://git.sbercloud.tech/sbercloud-ui/tokens-design-system/snack-uikit/-/tree/master/packages/breadcrumbs",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://git.sbercloud.tech/sbercloud-ui/tokens-design-system/snack-uikit.git",
|
|
20
|
+
"directory": "packages/breadcrumbs"
|
|
21
|
+
},
|
|
22
|
+
"author": "Сергей Хлупин <svhlupin@cloud.ru>",
|
|
23
|
+
"contributors": [
|
|
24
|
+
"Сергей Хлупин <svhlupin@cloud.ru>"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src",
|
|
29
|
+
"./CHANGELOG.md",
|
|
30
|
+
"./LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"scripts": {},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@snack-uikit/popover": "0.4.1-preview-104ad22c.0",
|
|
36
|
+
"@snack-uikit/typography": "0.4.5-preview-104ad22c.0",
|
|
37
|
+
"@snack-uikit/utils": "2.0.2-preview-104ad22c.0",
|
|
38
|
+
"classnames": "2.3.2",
|
|
39
|
+
"lodash.debounce": "4.0.8"
|
|
40
|
+
},
|
|
41
|
+
"gitHead": "580e0d5c22d04d609bcdf7cb0873114f9fce5f66"
|
|
42
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { memo, useMemo, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { WithSupportProps } from '@snack-uikit/utils';
|
|
4
|
+
|
|
5
|
+
import { ElementType, ItemRenderMode, SEPARATOR, Size } from '../../constants';
|
|
6
|
+
import { useBreadcrumbsLayout } from '../../hooks';
|
|
7
|
+
import { Item } from '../../types';
|
|
8
|
+
import { Collapse } from '../Collapse';
|
|
9
|
+
import { Crumb } from '../Crumb';
|
|
10
|
+
import { HiddenChain } from '../HiddenChain';
|
|
11
|
+
import { Separator } from '../Separator';
|
|
12
|
+
import { Wrapper } from '../Wrapper';
|
|
13
|
+
|
|
14
|
+
export type BreadcrumbsProps = WithSupportProps<{
|
|
15
|
+
/** Массив айтемов */
|
|
16
|
+
items: Item[];
|
|
17
|
+
/** CSS-класс */
|
|
18
|
+
className?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Разделитель
|
|
21
|
+
* @default "›"
|
|
22
|
+
*/
|
|
23
|
+
separator?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Размер
|
|
26
|
+
* @default Breadcrumbs.sizes.S
|
|
27
|
+
*/
|
|
28
|
+
size?: Size;
|
|
29
|
+
/**
|
|
30
|
+
* Использовать иконку без лейбла в первом айтеме
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
firstItemIconOnly?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Делает некликабельным последний элемент, даже если для него переданы `href` или `onClick`
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
inactiveLastItem?: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Компонент хлебных крошек
|
|
43
|
+
*/
|
|
44
|
+
const BreadcrumbsComponent = memo(function Breadcrumbs({
|
|
45
|
+
items: itemsProps,
|
|
46
|
+
size = Size.S,
|
|
47
|
+
separator = SEPARATOR,
|
|
48
|
+
className,
|
|
49
|
+
firstItemIconOnly = false,
|
|
50
|
+
inactiveLastItem = false,
|
|
51
|
+
...rest
|
|
52
|
+
}: BreadcrumbsProps) {
|
|
53
|
+
const containerRef = useRef<HTMLUListElement>(null);
|
|
54
|
+
|
|
55
|
+
const { setConfigs, currentConfig } = useBreadcrumbsLayout(containerRef);
|
|
56
|
+
|
|
57
|
+
const items = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
inactiveLastItem
|
|
60
|
+
? itemsProps.map((item, index) =>
|
|
61
|
+
index === itemsProps.length - 1 ? { ...item, onClick: undefined, href: undefined } : item,
|
|
62
|
+
)
|
|
63
|
+
: itemsProps,
|
|
64
|
+
[inactiveLastItem, itemsProps],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<HiddenChain
|
|
70
|
+
items={items}
|
|
71
|
+
size={size}
|
|
72
|
+
separator={separator}
|
|
73
|
+
onConfigsBuilt={setConfigs}
|
|
74
|
+
firstItemIconOnly={firstItemIconOnly}
|
|
75
|
+
/>
|
|
76
|
+
<Wrapper {...rest} ref={containerRef} hidden={false} size={size} separator={separator} className={className}>
|
|
77
|
+
{currentConfig?.chain.map((block, index, array) => {
|
|
78
|
+
const isLastElement = index === array.length - 1;
|
|
79
|
+
|
|
80
|
+
switch (block.element) {
|
|
81
|
+
case ElementType.Separator:
|
|
82
|
+
return <Separator key={index} />;
|
|
83
|
+
case ElementType.Collapse:
|
|
84
|
+
return <Collapse key={index} currentConfig={currentConfig.chain} />;
|
|
85
|
+
case ElementType.Item:
|
|
86
|
+
const { renderMode, id } = block.item;
|
|
87
|
+
if (renderMode !== ItemRenderMode.Collapsed) {
|
|
88
|
+
return (
|
|
89
|
+
<Crumb
|
|
90
|
+
useIconOnly={!index && firstItemIconOnly}
|
|
91
|
+
current={isLastElement}
|
|
92
|
+
renderMode={renderMode}
|
|
93
|
+
minWidth={block.width}
|
|
94
|
+
item={block.item}
|
|
95
|
+
key={id}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
})}
|
|
103
|
+
</Wrapper>
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const Breadcrumbs = BreadcrumbsComponent as typeof BreadcrumbsComponent & {
|
|
109
|
+
sizes: typeof Size;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
Breadcrumbs.sizes = Size;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Breadcrumbs';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Popover } from '@snack-uikit/popover';
|
|
4
|
+
|
|
5
|
+
import { ElementType, ItemRenderMode } from '../../constants';
|
|
6
|
+
import { BreadcrumbsContext } from '../../context';
|
|
7
|
+
import { BreadcrumbsConfigChain } from '../../types';
|
|
8
|
+
import { getTestId } from '../../utils';
|
|
9
|
+
import { Crumb } from '../Crumb';
|
|
10
|
+
import { CrumbsTypography } from '../CrumbsTypography';
|
|
11
|
+
import { Separator } from '../Separator';
|
|
12
|
+
import styles from './styles.module.scss';
|
|
13
|
+
|
|
14
|
+
export type CollapseProps = {
|
|
15
|
+
className?: string;
|
|
16
|
+
currentConfig: BreadcrumbsConfigChain;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function Collapse({ currentConfig, className }: CollapseProps) {
|
|
20
|
+
const ctx = useContext(BreadcrumbsContext);
|
|
21
|
+
const { hidden, size, testId } = ctx;
|
|
22
|
+
|
|
23
|
+
const collapsedItems = currentConfig.map(node => {
|
|
24
|
+
if (node.element === ElementType.Item && node.item.renderMode === ItemRenderMode.Collapsed) {
|
|
25
|
+
const { id } = node.item;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div key={id} className={styles.collapsedRow}>
|
|
29
|
+
<Crumb item={node.item} renderMode={ItemRenderMode.Full} />
|
|
30
|
+
<Separator />
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const tip = (
|
|
38
|
+
<BreadcrumbsContext.Provider value={{ ...ctx, testId: `${testId}-collapsed` }}>
|
|
39
|
+
{collapsedItems}
|
|
40
|
+
</BreadcrumbsContext.Provider>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={className} data-test-id={getTestId('collapse', testId)} data-element-type={ElementType.Collapse}>
|
|
45
|
+
<Popover tip={tip} trigger={Popover.triggers.HoverAndFocusVisible} placement={Popover.placements.Top}>
|
|
46
|
+
<button className={styles.collapse} tabIndex={hidden ? -1 : 0}>
|
|
47
|
+
<CrumbsTypography size={size}>...</CrumbsTypography>
|
|
48
|
+
</button>
|
|
49
|
+
</Popover>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Collapse';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
|
|
2
|
+
|
|
3
|
+
.collapse {
|
|
4
|
+
color: simple-var($theme-variables, 'sys', 'neutral', 'text-light');
|
|
5
|
+
background: none;
|
|
6
|
+
border: none;
|
|
7
|
+
|
|
8
|
+
&:focus-visible,
|
|
9
|
+
&:hover {
|
|
10
|
+
color: simple-var($theme-variables, 'sys', 'neutral', 'text-support');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
&:focus-visible {
|
|
14
|
+
@include outline-var($container-focused-s);
|
|
15
|
+
|
|
16
|
+
outline-color: simple-var($theme-variables, 'sys', 'available', 'complementary');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.collapsedRow {
|
|
21
|
+
display: flex;
|
|
22
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import { memo, useContext } from 'react';
|
|
3
|
+
|
|
4
|
+
import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
|
|
5
|
+
|
|
6
|
+
import { ElementType, ItemRenderMode } from '../../constants';
|
|
7
|
+
import { BreadcrumbsContext } from '../../context';
|
|
8
|
+
import { Item } from '../../types';
|
|
9
|
+
import { getTestId } from '../../utils';
|
|
10
|
+
import { CrumbsTypography } from '../CrumbsTypography';
|
|
11
|
+
import styles from './styles.module.scss';
|
|
12
|
+
|
|
13
|
+
export type CrumbProps = WithSupportProps<{
|
|
14
|
+
renderMode: ItemRenderMode;
|
|
15
|
+
className?: string;
|
|
16
|
+
minWidth?: number;
|
|
17
|
+
current?: boolean;
|
|
18
|
+
item: Item;
|
|
19
|
+
useIconOnly?: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
const ELLIPSIS_LABEL_LENGTH = 8;
|
|
23
|
+
|
|
24
|
+
export const Crumb = memo(function Crumb({
|
|
25
|
+
minWidth: minWidthProp = 0,
|
|
26
|
+
current = false,
|
|
27
|
+
renderMode,
|
|
28
|
+
className,
|
|
29
|
+
item: { label: labelProp, shortLabel, icon: Icon, id, onClick, href },
|
|
30
|
+
useIconOnly: useIconOnlyProp,
|
|
31
|
+
...rest
|
|
32
|
+
}: CrumbProps) {
|
|
33
|
+
const { hidden, size, testId } = useContext(BreadcrumbsContext);
|
|
34
|
+
const isLabelShouldBeEllipse = hidden && labelProp.length > ELLIPSIS_LABEL_LENGTH;
|
|
35
|
+
const label =
|
|
36
|
+
isLabelShouldBeEllipse && renderMode === ItemRenderMode.Ellipsis ? `${labelProp.substring(0, 4)}...` : labelProp;
|
|
37
|
+
const useIconOnly = Boolean(Icon && useIconOnlyProp);
|
|
38
|
+
|
|
39
|
+
const minWidth = minWidthProp && renderMode === ItemRenderMode.Ellipsis ? minWidthProp : 'auto';
|
|
40
|
+
|
|
41
|
+
const dataAttributes = {
|
|
42
|
+
'data-render-mode': renderMode,
|
|
43
|
+
'data-current': current,
|
|
44
|
+
'data-hidden': hidden,
|
|
45
|
+
'data-size': size,
|
|
46
|
+
'data-id': id,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const content = (
|
|
50
|
+
<>
|
|
51
|
+
{Icon && (
|
|
52
|
+
<div className={styles.icon}>
|
|
53
|
+
<Icon size={24} />
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
{!useIconOnly && (
|
|
57
|
+
<CrumbsTypography size={size} className={styles.label}>
|
|
58
|
+
{renderMode === ItemRenderMode.ShortLabel ? shortLabel : label}
|
|
59
|
+
</CrumbsTypography>
|
|
60
|
+
)}
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const tabIndex = hidden ? { tabIndex: -1 } : {};
|
|
65
|
+
|
|
66
|
+
const title = renderMode === ItemRenderMode.Ellipsis ? label : '';
|
|
67
|
+
|
|
68
|
+
let crumb = null;
|
|
69
|
+
if (href) {
|
|
70
|
+
crumb = (
|
|
71
|
+
<a
|
|
72
|
+
className={styles.crumb}
|
|
73
|
+
onClick={onClick}
|
|
74
|
+
href={href}
|
|
75
|
+
aria-label={useIconOnly ? label : undefined}
|
|
76
|
+
data-clickable={true}
|
|
77
|
+
{...dataAttributes}
|
|
78
|
+
{...tabIndex}
|
|
79
|
+
>
|
|
80
|
+
{content}
|
|
81
|
+
</a>
|
|
82
|
+
);
|
|
83
|
+
} else if (onClick) {
|
|
84
|
+
crumb = (
|
|
85
|
+
<button
|
|
86
|
+
title={title}
|
|
87
|
+
onClick={onClick}
|
|
88
|
+
className={styles.crumb}
|
|
89
|
+
aria-label={useIconOnly ? label : undefined}
|
|
90
|
+
data-clickable={true}
|
|
91
|
+
{...dataAttributes}
|
|
92
|
+
{...tabIndex}
|
|
93
|
+
>
|
|
94
|
+
{content}
|
|
95
|
+
</button>
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
crumb = (
|
|
99
|
+
<div className={styles.crumb} {...dataAttributes}>
|
|
100
|
+
{content}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<li
|
|
107
|
+
{...extractSupportProps(rest)}
|
|
108
|
+
{...dataAttributes}
|
|
109
|
+
data-test-id={getTestId('crumb', testId)}
|
|
110
|
+
className={cn(styles.wrapper, className)}
|
|
111
|
+
data-element-type={ElementType.Item}
|
|
112
|
+
style={{ minWidth }}
|
|
113
|
+
title={title}
|
|
114
|
+
>
|
|
115
|
+
{crumb}
|
|
116
|
+
</li>
|
|
117
|
+
);
|
|
118
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Crumb';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
@import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-breadcrumbs';
|
|
2
|
+
@import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element';
|
|
3
|
+
|
|
4
|
+
$sizes: 'xs', 's';
|
|
5
|
+
|
|
6
|
+
.wrapper {
|
|
7
|
+
flex-shrink: 0;
|
|
8
|
+
list-style: none;
|
|
9
|
+
|
|
10
|
+
&[data-render-mode=ellipsis] {
|
|
11
|
+
flex-shrink: 1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.crumb, a.crumb {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
|
|
19
|
+
width: 100%;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
|
|
23
|
+
color: simple-var($theme-variables, 'sys', 'neutral', 'text-light');
|
|
24
|
+
text-decoration: none;
|
|
25
|
+
|
|
26
|
+
background: none;
|
|
27
|
+
border: none;
|
|
28
|
+
|
|
29
|
+
&[data-clickable] {
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
&:focus-visible,
|
|
32
|
+
&:hover {
|
|
33
|
+
color: simple-var($theme-variables, 'sys', 'neutral', 'text-support');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&[data-current='true'] {
|
|
38
|
+
color: simple-var($theme-variables, 'sys', 'neutral', 'text-support');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&:focus-visible {
|
|
42
|
+
@include outline-var($container-focused-s);
|
|
43
|
+
|
|
44
|
+
outline-color: simple-var($theme-variables, 'sys', 'available', 'complementary');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@each $size in $sizes {
|
|
48
|
+
&[data-size='#{$size}'] {
|
|
49
|
+
@include composite-var($breadcrumbs, 'crumb', $size);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.icon {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
align-items: center;
|
|
57
|
+
width: fit-content;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.label {
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
width: 100%;
|
|
63
|
+
text-overflow: ellipsis;
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PropsWithChildren } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Typography } from '@snack-uikit/typography';
|
|
4
|
+
|
|
5
|
+
import { Size } from '../../constants';
|
|
6
|
+
|
|
7
|
+
export type CrumbsTypographyProps = PropsWithChildren<{
|
|
8
|
+
size: Size;
|
|
9
|
+
className?: string;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export function CrumbsTypography({ size, children, className }: CrumbsTypographyProps) {
|
|
13
|
+
return (
|
|
14
|
+
<Typography
|
|
15
|
+
className={className}
|
|
16
|
+
size={size === Size.S ? Typography.sizes.L : Typography.sizes.M}
|
|
17
|
+
tag={Typography.tags.div}
|
|
18
|
+
family={Typography.families.Sans}
|
|
19
|
+
role={Typography.roles.Body}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</Typography>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CrumbsTypography';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { memo, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Size } from '../../constants';
|
|
4
|
+
import { BreadcrumbsConfig, Item } from '../../types';
|
|
5
|
+
import { buildBreadcrumbsConfigs, buildSizeMap } from '../../utils';
|
|
6
|
+
import { Collapse } from '../Collapse';
|
|
7
|
+
import { Separator } from '../Separator';
|
|
8
|
+
import { Wrapper } from '../Wrapper';
|
|
9
|
+
import { useItemModesRender } from './hooks';
|
|
10
|
+
|
|
11
|
+
export type HiddenChainProps = {
|
|
12
|
+
separator: string;
|
|
13
|
+
items: Item[];
|
|
14
|
+
size: Size;
|
|
15
|
+
onConfigsBuilt(config: BreadcrumbsConfig[]): void;
|
|
16
|
+
firstItemIconOnly?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const HiddenChain = memo(function HiddenChain({
|
|
20
|
+
size,
|
|
21
|
+
separator,
|
|
22
|
+
items,
|
|
23
|
+
onConfigsBuilt,
|
|
24
|
+
firstItemIconOnly = false,
|
|
25
|
+
}: HiddenChainProps) {
|
|
26
|
+
const containerRef = useRef<HTMLUListElement>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const container = containerRef.current;
|
|
30
|
+
if (container) {
|
|
31
|
+
const observer = new ResizeObserver(([{ target }]) => {
|
|
32
|
+
const sizeMap = buildSizeMap(target);
|
|
33
|
+
if (sizeMap) {
|
|
34
|
+
onConfigsBuilt(buildBreadcrumbsConfigs(items, sizeMap));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
observer.observe(container);
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
observer.disconnect();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}, [items, onConfigsBuilt]);
|
|
44
|
+
|
|
45
|
+
const renderItemModes = useItemModesRender({ firstItemIconOnly });
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Wrapper size={size} hidden ref={containerRef} separator={separator} data-test-id='hidden-wrapper'>
|
|
49
|
+
<Separator />
|
|
50
|
+
<Collapse currentConfig={[]} />
|
|
51
|
+
{items.map(renderItemModes)}
|
|
52
|
+
</Wrapper>
|
|
53
|
+
);
|
|
54
|
+
});
|