@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +286 -0
  2. package/LICENSE +201 -0
  3. package/README.md +40 -0
  4. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +33 -0
  5. package/dist/components/Breadcrumbs/Breadcrumbs.js +49 -0
  6. package/dist/components/Breadcrumbs/index.d.ts +1 -0
  7. package/dist/components/Breadcrumbs/index.js +1 -0
  8. package/dist/components/Collapse/Collapse.d.ts +6 -0
  9. package/dist/components/Collapse/Collapse.js +23 -0
  10. package/dist/components/Collapse/index.d.ts +1 -0
  11. package/dist/components/Collapse/index.js +1 -0
  12. package/dist/components/Collapse/styles.module.css +18 -0
  13. package/dist/components/Crumb/Crumb.d.ts +13 -0
  14. package/dist/components/Crumb/Crumb.js +50 -0
  15. package/dist/components/Crumb/index.d.ts +1 -0
  16. package/dist/components/Crumb/index.js +1 -0
  17. package/dist/components/Crumb/styles.module.css +55 -0
  18. package/dist/components/CrumbsTypography/CrumbsTypography.d.ts +7 -0
  19. package/dist/components/CrumbsTypography/CrumbsTypography.js +6 -0
  20. package/dist/components/CrumbsTypography/index.d.ts +1 -0
  21. package/dist/components/CrumbsTypography/index.js +1 -0
  22. package/dist/components/HiddenChain/HiddenChain.d.ts +11 -0
  23. package/dist/components/HiddenChain/HiddenChain.js +27 -0
  24. package/dist/components/HiddenChain/hooks.d.ts +6 -0
  25. package/dist/components/HiddenChain/hooks.js +28 -0
  26. package/dist/components/HiddenChain/index.d.ts +1 -0
  27. package/dist/components/HiddenChain/index.js +1 -0
  28. package/dist/components/Separator/Separator.d.ts +1 -0
  29. package/dist/components/Separator/Separator.js +11 -0
  30. package/dist/components/Separator/index.d.ts +1 -0
  31. package/dist/components/Separator/index.js +1 -0
  32. package/dist/components/Separator/styles.module.css +15 -0
  33. package/dist/components/Wrapper/Wrapper.d.ts +19 -0
  34. package/dist/components/Wrapper/Wrapper.js +21 -0
  35. package/dist/components/Wrapper/index.d.ts +1 -0
  36. package/dist/components/Wrapper/index.js +1 -0
  37. package/dist/components/Wrapper/styles.module.css +19 -0
  38. package/dist/components/index.d.ts +1 -0
  39. package/dist/components/index.js +1 -0
  40. package/dist/components/styles.module.css +3 -0
  41. package/dist/constants.d.ts +16 -0
  42. package/dist/constants.js +19 -0
  43. package/dist/context.d.ts +10 -0
  44. package/dist/context.js +7 -0
  45. package/dist/hooks.d.ts +21 -0
  46. package/dist/hooks.js +78 -0
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +1 -0
  49. package/dist/types.d.ts +50 -0
  50. package/dist/types.js +1 -0
  51. package/dist/utils/buildBreadcrumbsConfigs.d.ts +2 -0
  52. package/dist/utils/buildBreadcrumbsConfigs.js +77 -0
  53. package/dist/utils/buildSizeMap.d.ts +2 -0
  54. package/dist/utils/buildSizeMap.js +26 -0
  55. package/dist/utils/getMaxPossibleWidth.d.ts +6 -0
  56. package/dist/utils/getMaxPossibleWidth.js +16 -0
  57. package/dist/utils/getTestId.d.ts +1 -0
  58. package/dist/utils/getTestId.js +1 -0
  59. package/dist/utils/index.d.ts +4 -0
  60. package/dist/utils/index.js +4 -0
  61. package/package.json +42 -0
  62. package/src/components/Breadcrumbs/Breadcrumbs.tsx +112 -0
  63. package/src/components/Breadcrumbs/index.ts +1 -0
  64. package/src/components/Collapse/Collapse.tsx +52 -0
  65. package/src/components/Collapse/index.ts +1 -0
  66. package/src/components/Collapse/styles.module.scss +22 -0
  67. package/src/components/Crumb/Crumb.tsx +118 -0
  68. package/src/components/Crumb/index.ts +1 -0
  69. package/src/components/Crumb/styles.module.scss +66 -0
  70. package/src/components/CrumbsTypography/CrumbsTypography.tsx +24 -0
  71. package/src/components/CrumbsTypography/index.ts +1 -0
  72. package/src/components/HiddenChain/HiddenChain.tsx +54 -0
  73. package/src/components/HiddenChain/hooks.tsx +41 -0
  74. package/src/components/HiddenChain/index.ts +1 -0
  75. package/src/components/Separator/Separator.tsx +23 -0
  76. package/src/components/Separator/index.ts +1 -0
  77. package/src/components/Separator/styles.module.scss +16 -0
  78. package/src/components/Wrapper/Wrapper.tsx +38 -0
  79. package/src/components/Wrapper/index.ts +1 -0
  80. package/src/components/Wrapper/styles.module.scss +26 -0
  81. package/src/components/index.ts +1 -0
  82. package/src/components/styles.module.scss +3 -0
  83. package/src/constants.ts +19 -0
  84. package/src/context.ts +16 -0
  85. package/src/hooks.tsx +99 -0
  86. package/src/index.ts +2 -0
  87. package/src/types.ts +60 -0
  88. package/src/utils/buildBreadcrumbsConfigs.ts +116 -0
  89. package/src/utils/buildSizeMap.ts +33 -0
  90. package/src/utils/getMaxPossibleWidth.ts +17 -0
  91. package/src/utils/getTestId.ts +1 -0
  92. 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,2 @@
1
+ import { SizeMap } from '../types';
2
+ export declare function buildSizeMap(container?: Element): SizeMap | undefined;
@@ -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}` : '');
@@ -0,0 +1,4 @@
1
+ export * from './buildBreadcrumbsConfigs';
2
+ export * from './buildSizeMap';
3
+ export * from './getMaxPossibleWidth';
4
+ export * from './getTestId';
@@ -0,0 +1,4 @@
1
+ export * from './buildBreadcrumbsConfigs';
2
+ export * from './buildSizeMap';
3
+ export * from './getMaxPossibleWidth';
4
+ export * from './getTestId';
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
+ });