@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,41 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ItemRenderMode } from '../../constants';
|
|
4
|
+
import { Item } from '../../types';
|
|
5
|
+
import { Crumb } from '../Crumb';
|
|
6
|
+
|
|
7
|
+
type RenderModes = [ItemRenderMode, 'label' | 'shortLabel'][];
|
|
8
|
+
|
|
9
|
+
type UseItemModesRenderProps = { firstItemIconOnly: boolean };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Варианты отображения для обычного айтема
|
|
13
|
+
*/
|
|
14
|
+
const NORMAL_MODES: RenderModes = [
|
|
15
|
+
[ItemRenderMode.ShortLabel, 'shortLabel'],
|
|
16
|
+
[ItemRenderMode.Ellipsis, 'label'],
|
|
17
|
+
[ItemRenderMode.Full, 'label'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Варианты отображения айтемов состоящих из одной иконки
|
|
22
|
+
*/
|
|
23
|
+
const ICON_ONLY_MODES: RenderModes = [[ItemRenderMode.Full, 'label']];
|
|
24
|
+
|
|
25
|
+
const getItemModesRenderer = (firstItemIconOnly: boolean) => (item: Item, index: number) => {
|
|
26
|
+
const isIconOnlyMode = !index && firstItemIconOnly;
|
|
27
|
+
const modes: RenderModes = isIconOnlyMode ? ICON_ONLY_MODES : NORMAL_MODES;
|
|
28
|
+
|
|
29
|
+
return modes.map(([mode, labelProp]) => {
|
|
30
|
+
const label = item[labelProp];
|
|
31
|
+
|
|
32
|
+
if (!label) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return <Crumb useIconOnly={isIconOnlyMode} renderMode={mode} key={item.id + mode} item={item} />;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const useItemModesRender = ({ firstItemIconOnly }: UseItemModesRenderProps) =>
|
|
41
|
+
useMemo(() => getItemModesRenderer(firstItemIconOnly), [firstItemIconOnly]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './HiddenChain';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ElementType } from '../../constants';
|
|
4
|
+
import { BreadcrumbsContext } from '../../context';
|
|
5
|
+
import { getTestId } from '../../utils';
|
|
6
|
+
import { CrumbsTypography } from '../CrumbsTypography';
|
|
7
|
+
import styles from './styles.module.scss';
|
|
8
|
+
|
|
9
|
+
export function Separator() {
|
|
10
|
+
const { size, separator, testId } = useContext(BreadcrumbsContext);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
aria-hidden={true}
|
|
15
|
+
data-size={size}
|
|
16
|
+
className={styles.separator}
|
|
17
|
+
data-test-id={getTestId('separator', testId)}
|
|
18
|
+
data-element-type={ElementType.Separator}
|
|
19
|
+
>
|
|
20
|
+
<CrumbsTypography size={size}>{separator}</CrumbsTypography>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Separator';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-breadcrumbs';
|
|
2
|
+
@import '@snack-uikit/figma-tokens/build/scss/styles-theme-variables';
|
|
3
|
+
|
|
4
|
+
$sizes: 'xs', 's';
|
|
5
|
+
|
|
6
|
+
.separator {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
color: simple-var($theme-variables, 'sys', 'neutral', 'text-disabled');
|
|
10
|
+
|
|
11
|
+
@each $size in $sizes {
|
|
12
|
+
&[data-size='#{$size}'] {
|
|
13
|
+
@include composite-var($breadcrumbs, 'separator', $size);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import { forwardRef, PropsWithChildren } from 'react';
|
|
3
|
+
|
|
4
|
+
import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
|
|
5
|
+
|
|
6
|
+
import { Size } from '../../constants';
|
|
7
|
+
import { BreadcrumbsContext } from '../../context';
|
|
8
|
+
import styles from './styles.module.scss';
|
|
9
|
+
|
|
10
|
+
export type WrapperProps = PropsWithChildren<
|
|
11
|
+
WithSupportProps<{
|
|
12
|
+
hidden: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
size: Size;
|
|
15
|
+
separator: string;
|
|
16
|
+
}>
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
export const Wrapper = forwardRef<HTMLUListElement, WrapperProps>(function HiddenWrapper(
|
|
20
|
+
{ children, className, size, hidden, separator, 'data-test-id': testId, ...rest },
|
|
21
|
+
ref,
|
|
22
|
+
) {
|
|
23
|
+
return (
|
|
24
|
+
<nav
|
|
25
|
+
{...extractSupportProps(rest)}
|
|
26
|
+
className={cn(styles.wrapper, className)}
|
|
27
|
+
data-test-id={testId}
|
|
28
|
+
data-hidden={hidden}
|
|
29
|
+
aria-hidden={hidden}
|
|
30
|
+
>
|
|
31
|
+
<BreadcrumbsContext.Provider value={{ hidden, size, separator, testId }}>
|
|
32
|
+
<ul className={styles.row} ref={ref} data-hidden={hidden} data-size={size}>
|
|
33
|
+
{children}
|
|
34
|
+
</ul>
|
|
35
|
+
</BreadcrumbsContext.Provider>
|
|
36
|
+
</nav>
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Wrapper';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-breadcrumbs';
|
|
2
|
+
|
|
3
|
+
$sizes: 'xs', 's';
|
|
4
|
+
|
|
5
|
+
.wrapper {
|
|
6
|
+
@include composite-var($breadcrumbs, 'container');
|
|
7
|
+
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
|
|
10
|
+
&[data-hidden='true'] {
|
|
11
|
+
position: absolute;
|
|
12
|
+
width: 1px;
|
|
13
|
+
height: 1px;
|
|
14
|
+
opacity: 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.row {
|
|
19
|
+
display: flex;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
|
|
23
|
+
&[data-hidden='true'] {
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Breadcrumbs';
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const SEPARATOR = '›';
|
|
2
|
+
|
|
3
|
+
export enum ItemRenderMode {
|
|
4
|
+
Full = 'full',
|
|
5
|
+
ShortLabel = 'shortLabel',
|
|
6
|
+
Ellipsis = 'ellipsis',
|
|
7
|
+
Collapsed = 'collapsed',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export enum ElementType {
|
|
11
|
+
Separator = 'separator',
|
|
12
|
+
Collapse = 'collapse',
|
|
13
|
+
Item = 'item',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export enum Size {
|
|
17
|
+
Xs = 'xs',
|
|
18
|
+
S = 's',
|
|
19
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { SEPARATOR, Size } from './constants';
|
|
4
|
+
|
|
5
|
+
type BreadcrumbsContextValue = {
|
|
6
|
+
hidden: boolean;
|
|
7
|
+
size: Size;
|
|
8
|
+
separator: string;
|
|
9
|
+
testId?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const BreadcrumbsContext = createContext<BreadcrumbsContextValue>({
|
|
13
|
+
hidden: false,
|
|
14
|
+
size: Size.S,
|
|
15
|
+
separator: SEPARATOR,
|
|
16
|
+
});
|
package/src/hooks.tsx
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import debounce from 'lodash.debounce';
|
|
2
|
+
import { RefObject, useCallback, useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { BreadcrumbsConfig, CurrentConfigState } from './types';
|
|
5
|
+
import { getMaxPossibleWidth } from './utils';
|
|
6
|
+
|
|
7
|
+
export type BreadcrumbsLayout = {
|
|
8
|
+
setConfigs: (configs: BreadcrumbsConfig[]) => void;
|
|
9
|
+
currentConfig?: CurrentConfigState;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const selectConfig = (containerWidth: number, configs: BreadcrumbsConfig[]): CurrentConfigState | undefined => {
|
|
13
|
+
if (!configs.length) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let [bestConfig] = configs;
|
|
18
|
+
|
|
19
|
+
for (const config of configs) {
|
|
20
|
+
if (config.width <= containerWidth) {
|
|
21
|
+
if (bestConfig.width > containerWidth) {
|
|
22
|
+
bestConfig = config;
|
|
23
|
+
} else {
|
|
24
|
+
if (bestConfig.weight > config.weight) {
|
|
25
|
+
bestConfig = config;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (bestConfig) {
|
|
32
|
+
return { chain: bestConfig.chain, containerWidth, chainWidth: bestConfig.width };
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Расчет способа отображения breadcrumbs работает следующим образом:
|
|
38
|
+
* Каждый item может быть отображен в одном из режимов:
|
|
39
|
+
* full - целиком
|
|
40
|
+
* shortLabel - с использованием укороченного лейбла
|
|
41
|
+
* ellipsis - cо скруглением текста в ...
|
|
42
|
+
* collapse - спрятан в специальный узел "..." с тултипом.
|
|
43
|
+
* Для того чтоб эффективно распределить свободное по ширине пространство между айтемами в компоненте создается скрытый контейнер (HiddenChain),
|
|
44
|
+
* в котором рендерятся все айтемы во всех возможных способах отображения. Замеряется размер каждого из айтемов и записывается в sizeMap.
|
|
45
|
+
* Далее перебираются все возможные варианты отображения всей цепочки - конфиги (buildBreadcrumbsConfigs). У такого конфига есть два атрибута:
|
|
46
|
+
* width - суммарная ширина
|
|
47
|
+
* weight - вес, отображает общее количество сокращений, примененных к цепочке (например, отображение в режиме full - 0, сокращение shortLabel - +1, а ellipsis - +100 и тд)
|
|
48
|
+
* Для того чтоб отобразить breadcrumbs, нужно подобрать конфиг, максимальный по ширине, влезающий в контейнер, но с минимальным весом (наименьшим количеством сокращений).
|
|
49
|
+
*/
|
|
50
|
+
export function useBreadcrumbsLayout(containerRef: RefObject<HTMLElement>): BreadcrumbsLayout {
|
|
51
|
+
const [currentConfig, setCurrentConfig] = useState<CurrentConfigState | undefined>(undefined);
|
|
52
|
+
const [configs, setConfigs] = useState<BreadcrumbsConfig[]>([]);
|
|
53
|
+
|
|
54
|
+
const selectConfigForWidth = useCallback((width: number) => selectConfig(width, configs), [configs]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Подбор подходящего конфига триггерится изменением ширины контейнера и изменением набора конфигов
|
|
58
|
+
*/
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const visibleContainer = containerRef.current?.parentElement;
|
|
61
|
+
|
|
62
|
+
if (!visibleContainer) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (configs.length) {
|
|
67
|
+
setCurrentConfig(selectConfig(getMaxPossibleWidth(visibleContainer), configs));
|
|
68
|
+
}
|
|
69
|
+
}, [configs, containerRef]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const visibleContainer = containerRef.current?.parentElement;
|
|
73
|
+
|
|
74
|
+
if (!visibleContainer) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const reselectConfig = debounce(() => {
|
|
79
|
+
const width = getMaxPossibleWidth(visibleContainer);
|
|
80
|
+
setCurrentConfig(prevConfig => {
|
|
81
|
+
if (prevConfig?.containerWidth === width) {
|
|
82
|
+
return prevConfig;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const newConf = selectConfigForWidth(width);
|
|
86
|
+
return newConf || (prevConfig ? { ...prevConfig, containerWidth: width } : prevConfig);
|
|
87
|
+
});
|
|
88
|
+
}, 100);
|
|
89
|
+
|
|
90
|
+
const visibleContainerObserver = new ResizeObserver(reselectConfig);
|
|
91
|
+
|
|
92
|
+
visibleContainerObserver.observe(visibleContainer);
|
|
93
|
+
visibleContainerObserver.observe(window.document.body);
|
|
94
|
+
|
|
95
|
+
return () => visibleContainerObserver.disconnect();
|
|
96
|
+
}, [containerRef, selectConfigForWidth]);
|
|
97
|
+
|
|
98
|
+
return { setConfigs, currentConfig };
|
|
99
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { JSXElementConstructor, MouseEventHandler } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ElementType, ItemRenderMode } from './constants';
|
|
4
|
+
|
|
5
|
+
export type Item = {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: JSXElementConstructor<{ size: number }>;
|
|
9
|
+
href?: string;
|
|
10
|
+
shortLabel?: string;
|
|
11
|
+
onClick?: MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type InnerItem = Item & {
|
|
15
|
+
renderMode: ItemRenderMode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ItemSizeMap = {
|
|
19
|
+
[ItemRenderMode.ShortLabel]: number;
|
|
20
|
+
[ItemRenderMode.Collapsed]: number;
|
|
21
|
+
[ItemRenderMode.Ellipsis]: number;
|
|
22
|
+
[ItemRenderMode.Full]: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type SizeMap = {
|
|
26
|
+
separator: number;
|
|
27
|
+
collapse: number;
|
|
28
|
+
items: {
|
|
29
|
+
[key: string]: ItemSizeMap;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type BreadcrumbsConfigChain = Array<
|
|
34
|
+
| {
|
|
35
|
+
element: ElementType.Collapse;
|
|
36
|
+
width: number;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
element: ElementType.Separator;
|
|
40
|
+
width: number;
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
element: ElementType.Item;
|
|
44
|
+
width: number;
|
|
45
|
+
item: InnerItem;
|
|
46
|
+
}
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
export type CurrentConfigState = {
|
|
50
|
+
chain: BreadcrumbsConfigChain;
|
|
51
|
+
chainWidth: number;
|
|
52
|
+
containerWidth: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type BreadcrumbsConfig = {
|
|
56
|
+
chain: BreadcrumbsConfigChain;
|
|
57
|
+
hasCollapsed: boolean;
|
|
58
|
+
weight: number;
|
|
59
|
+
width: number;
|
|
60
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ElementType, ItemRenderMode } from '../constants';
|
|
2
|
+
import { BreadcrumbsConfig, InnerItem, Item, SizeMap } from '../types';
|
|
3
|
+
|
|
4
|
+
type Chain = InnerItem[];
|
|
5
|
+
|
|
6
|
+
const RENDER_MODE_WEIGHT = {
|
|
7
|
+
[ItemRenderMode.Full]: 0,
|
|
8
|
+
[ItemRenderMode.ShortLabel]: 1,
|
|
9
|
+
[ItemRenderMode.Ellipsis]: 100,
|
|
10
|
+
[ItemRenderMode.Collapsed]: 10000,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const RENDER_MODE_WITH_WIDTH = [ItemRenderMode.Full, ItemRenderMode.ShortLabel, ItemRenderMode.Ellipsis];
|
|
14
|
+
|
|
15
|
+
const startsWith =
|
|
16
|
+
(renderMode: ItemRenderMode) =>
|
|
17
|
+
(item: InnerItem, tail: Chain): Chain =>
|
|
18
|
+
[{ ...item, renderMode }, ...tail];
|
|
19
|
+
const startsWithFull = startsWith(ItemRenderMode.Full);
|
|
20
|
+
const startsWithShortLabel = startsWith(ItemRenderMode.ShortLabel);
|
|
21
|
+
const startsWithEllipsis = startsWith(ItemRenderMode.Ellipsis);
|
|
22
|
+
const startsWithCollapsed = startsWith(ItemRenderMode.Collapsed);
|
|
23
|
+
|
|
24
|
+
type BuildSubChainOptions = {
|
|
25
|
+
useCollapse: boolean;
|
|
26
|
+
useEllipse: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function buildSubChain(
|
|
30
|
+
[current, ...rest]: Chain,
|
|
31
|
+
{ useCollapse = true, useEllipse = true }: BuildSubChainOptions,
|
|
32
|
+
): Chain[] {
|
|
33
|
+
const result: Chain[] = [];
|
|
34
|
+
|
|
35
|
+
if (!current) {
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!rest.length) {
|
|
40
|
+
result.push(startsWithFull(current, []));
|
|
41
|
+
current.shortLabel && result.push(startsWithShortLabel(current, []));
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildSubChain(rest, { useCollapse: false, useEllipse: false }).forEach(variant => {
|
|
46
|
+
result.push(startsWithFull(current, variant));
|
|
47
|
+
current.shortLabel && result.push(startsWithShortLabel(current, variant));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (useEllipse) {
|
|
51
|
+
buildSubChain(rest, { useCollapse: false, useEllipse: true }).forEach(variant => {
|
|
52
|
+
result.push(startsWithEllipsis(current, variant));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (useCollapse) {
|
|
57
|
+
buildSubChain(rest, { useCollapse: true, useEllipse: true }).forEach(variant => {
|
|
58
|
+
result.push(startsWithCollapsed(current, variant));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const collapseAllRest = (lastElementRenderMode: ItemRenderMode, rest: InnerItem[]) =>
|
|
66
|
+
rest.map((element, index, array) => {
|
|
67
|
+
const lastItem = index === array.length - 1;
|
|
68
|
+
return { ...element, renderMode: lastItem ? lastElementRenderMode : ItemRenderMode.Collapsed };
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export function buildBreadcrumbsConfigs(items: Item[], sizeMap: SizeMap): BreadcrumbsConfig[] {
|
|
72
|
+
const chains: InnerItem[][] = [];
|
|
73
|
+
const [first, ...rest] = items.map(item => ({ ...item, renderMode: ItemRenderMode.Full }));
|
|
74
|
+
|
|
75
|
+
buildSubChain(rest, { useCollapse: true, useEllipse: true }).forEach(subset => {
|
|
76
|
+
chains.push(startsWithFull(first, subset));
|
|
77
|
+
first.shortLabel && chains.push(startsWithShortLabel(first, subset));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** Первый элемент можно схлопывать/сокращать только когда уже сокращено все что можно */
|
|
81
|
+
chains.push(startsWithEllipsis(first, collapseAllRest(ItemRenderMode.Full, rest)));
|
|
82
|
+
chains.push(startsWithEllipsis(first, collapseAllRest(ItemRenderMode.Ellipsis, rest)));
|
|
83
|
+
chains.push(startsWithCollapsed(first, collapseAllRest(ItemRenderMode.Full, rest)));
|
|
84
|
+
chains.push(startsWithCollapsed(first, collapseAllRest(ItemRenderMode.Ellipsis, rest)));
|
|
85
|
+
|
|
86
|
+
return chains.map(chain =>
|
|
87
|
+
chain.reduce<BreadcrumbsConfig>(
|
|
88
|
+
(acc, item, index) => {
|
|
89
|
+
const { renderMode } = item;
|
|
90
|
+
|
|
91
|
+
if (index && RENDER_MODE_WITH_WIDTH.includes(renderMode)) {
|
|
92
|
+
acc.width += sizeMap.separator;
|
|
93
|
+
acc.chain.push({ element: ElementType.Separator, width: sizeMap.separator });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!acc.hasCollapsed && renderMode === ItemRenderMode.Collapsed) {
|
|
97
|
+
if (index) {
|
|
98
|
+
acc.width += sizeMap.separator;
|
|
99
|
+
acc.chain.push({ element: ElementType.Separator, width: sizeMap.separator });
|
|
100
|
+
}
|
|
101
|
+
acc.hasCollapsed = true;
|
|
102
|
+
acc.width += sizeMap.collapse;
|
|
103
|
+
acc.chain.push({ element: ElementType.Collapse, width: sizeMap.collapse });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const width = sizeMap.items[item.id][renderMode];
|
|
107
|
+
acc.weight += RENDER_MODE_WEIGHT[item.renderMode];
|
|
108
|
+
acc.width += width;
|
|
109
|
+
acc.chain.push({ element: ElementType.Item, item, width });
|
|
110
|
+
|
|
111
|
+
return acc;
|
|
112
|
+
},
|
|
113
|
+
{ chain: [], weight: 0, width: 0, hasCollapsed: false },
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ItemRenderMode } from '../constants';
|
|
2
|
+
import { ItemSizeMap, SizeMap } from '../types';
|
|
3
|
+
|
|
4
|
+
const getEmptyItemSizeMap = (): ItemSizeMap => ({
|
|
5
|
+
[ItemRenderMode.ShortLabel]: 0,
|
|
6
|
+
[ItemRenderMode.Collapsed]: 0,
|
|
7
|
+
[ItemRenderMode.Ellipsis]: 0,
|
|
8
|
+
[ItemRenderMode.Full]: 0,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const getElementWidth = (element: HTMLElement): number => element.getBoundingClientRect().width;
|
|
12
|
+
|
|
13
|
+
export function buildSizeMap(container?: Element): SizeMap | undefined {
|
|
14
|
+
if (!container) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const children = container.children as unknown as ArrayLike<HTMLElement>;
|
|
19
|
+
|
|
20
|
+
const [separator, collapse, ...itemElements] = Array.from(children);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
separator: getElementWidth(separator),
|
|
24
|
+
collapse: getElementWidth(collapse),
|
|
25
|
+
items: itemElements.reduce((result, element) => {
|
|
26
|
+
const id = element.getAttribute('data-id') as string;
|
|
27
|
+
const renderMode = element.getAttribute('data-render-mode') as ItemRenderMode;
|
|
28
|
+
result[id] = result[id] || getEmptyItemSizeMap();
|
|
29
|
+
result[id][renderMode] = getElementWidth(element);
|
|
30
|
+
return result;
|
|
31
|
+
}, {} as SizeMap['items']),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Функция предназначена для измерения максимально возможной ширины контейнера
|
|
3
|
+
* она вставляет широкую распорку, замеряет ширину и удаляет ее
|
|
4
|
+
* это происходит в одном обороте eventLoop, поэтому новый кадр с распоркой не рендерится браузером.
|
|
5
|
+
*/
|
|
6
|
+
export const getMaxPossibleWidth = (element: HTMLElement): number => {
|
|
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
|
+
|
|
16
|
+
return width;
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const getTestId = (prefix: string, testId?: string) => (testId ? `${prefix}-element-${testId}` : '');
|