@box/blueprint-web 12.135.1 → 12.136.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/dist/lib-esm/breadcrumb/breadcrumb-dropdown.d.ts +1 -1
- package/dist/lib-esm/breadcrumb/breadcrumb-dropdown.js +6 -4
- package/dist/lib-esm/breadcrumb/breadcrumb.js +58 -98
- package/dist/lib-esm/breadcrumb/breadcrumb.module.js +1 -1
- package/dist/lib-esm/breadcrumb/ellipsis-truncation-view.d.ts +2 -0
- package/dist/lib-esm/breadcrumb/ellipsis-truncation-view.js +42 -0
- package/dist/lib-esm/breadcrumb/folder-tree-truncation-view.d.ts +2 -0
- package/dist/lib-esm/breadcrumb/folder-tree-truncation-view.js +41 -0
- package/dist/lib-esm/breadcrumb/full-view.d.ts +2 -0
- package/dist/lib-esm/breadcrumb/full-view.js +19 -0
- package/dist/lib-esm/breadcrumb/mobile-view.d.ts +2 -0
- package/dist/lib-esm/breadcrumb/mobile-view.js +37 -0
- package/dist/lib-esm/breadcrumb/page-link.d.ts +2 -17
- package/dist/lib-esm/breadcrumb/page-link.js +13 -4
- package/dist/lib-esm/breadcrumb/types.d.ts +52 -1
- package/dist/lib-esm/breadcrumb/useFolderTreeTruncation.d.ts +8 -21
- package/dist/lib-esm/breadcrumb/useFolderTreeTruncation.js +105 -143
- package/dist/lib-esm/combobox/types.d.ts +4 -0
- package/dist/lib-esm/index.css +13 -8
- package/package.json +3 -3
|
@@ -6,5 +6,5 @@ interface BreadcrumbDropdownProps {
|
|
|
6
6
|
onPageLinkClick: (id: string) => void;
|
|
7
7
|
size: 'xsmall' | 'small' | 'medium' | 'large';
|
|
8
8
|
}
|
|
9
|
-
export declare function BreadcrumbDropdown({ crumbsToRender, iconButton, onPageLinkClick, size,
|
|
9
|
+
export declare function BreadcrumbDropdown({ crumbsToRender, iconButton, listRef, onPageLinkClick, size, }: BreadcrumbDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
export {};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { useCallback } from 'react';
|
|
3
3
|
import { PointerRight } from '@box/blueprint-web-assets/icons/Fill';
|
|
4
|
+
import { EllipsizableText } from '../ellipsizable-text/ellipsizable-text.js';
|
|
4
5
|
import { DropdownMenu } from '../primitives/dropdown-menu/index.js';
|
|
5
|
-
import { Text } from '../text/text.js';
|
|
6
6
|
import { getSeparatorSize } from './utils.js';
|
|
7
7
|
import styles from './breadcrumb.module.js';
|
|
8
8
|
|
|
9
9
|
function BreadcrumbDropdown({
|
|
10
10
|
crumbsToRender,
|
|
11
11
|
iconButton,
|
|
12
|
+
listRef,
|
|
12
13
|
onPageLinkClick,
|
|
13
|
-
size
|
|
14
|
-
listRef
|
|
14
|
+
size
|
|
15
15
|
}) {
|
|
16
16
|
const handlePageLinkClick = useCallback(crumbId => () => {
|
|
17
17
|
onPageLinkClick(crumbId);
|
|
@@ -27,8 +27,10 @@ function BreadcrumbDropdown({
|
|
|
27
27
|
className: styles.dropdownContent,
|
|
28
28
|
children: crumbsToRender.map(crumb => jsx(DropdownMenu.Item, {
|
|
29
29
|
onSelect: handlePageLinkClick(crumb.id),
|
|
30
|
-
children: jsx(
|
|
30
|
+
children: jsx(EllipsizableText, {
|
|
31
31
|
as: "span",
|
|
32
|
+
lineClamp: 1,
|
|
33
|
+
tooltipSide: "bottom",
|
|
32
34
|
children: crumb.name
|
|
33
35
|
})
|
|
34
36
|
}, crumb.id))
|
|
@@ -1,50 +1,82 @@
|
|
|
1
|
-
import { jsx, jsxs
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import noop from 'lodash/noop';
|
|
2
3
|
import { forwardRef, useRef } from 'react';
|
|
3
4
|
import { FolderTree, PointerRight } from '@box/blueprint-web-assets/icons/Fill';
|
|
4
|
-
import { Ellipsis } from '@box/blueprint-web-assets/icons/Medium';
|
|
5
5
|
import { Home } from '@box/blueprint-web-assets/icons/MediumFilled';
|
|
6
|
-
import noop from 'lodash/noop';
|
|
7
|
-
import { IconButton } from '../primitives/icon-button/icon-button.js';
|
|
8
6
|
import { useBreakpoint, Breakpoint } from '../utils/useBreakpoint.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { EllipsisTruncationView } from './ellipsis-truncation-view.js';
|
|
8
|
+
import { FolderTreeTruncationView } from './folder-tree-truncation-view.js';
|
|
9
|
+
import { FullView } from './full-view.js';
|
|
10
|
+
import { MobileView } from './mobile-view.js';
|
|
11
11
|
import { useFolderTreeTruncation } from './useFolderTreeTruncation.js';
|
|
12
12
|
import { getSeparatorSize } from './utils.js';
|
|
13
13
|
import styles from './breadcrumb.module.js';
|
|
14
14
|
|
|
15
|
+
const ELLIPSIS_TRUNCATION_THRESHOLD = 7;
|
|
15
16
|
const Breadcrumb = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
16
17
|
const {
|
|
17
18
|
breadcrumbAriaLabel,
|
|
18
19
|
crumbs,
|
|
19
|
-
truncatedLinksIconAriaLabel,
|
|
20
|
-
rootIconAriaLabel,
|
|
21
|
-
rootIconVariant,
|
|
22
20
|
isInteractive = true,
|
|
23
21
|
isResponsiveEnabled,
|
|
22
|
+
onPageLinkClick = noop,
|
|
23
|
+
rootIconAriaLabel,
|
|
24
|
+
rootIconVariant,
|
|
24
25
|
size = 'medium',
|
|
26
|
+
truncatedLinksIconAriaLabel,
|
|
25
27
|
truncationMethod = 'ellipsis',
|
|
26
|
-
onPageLinkClick = noop,
|
|
27
28
|
...rest
|
|
28
29
|
} = props;
|
|
29
|
-
// Responsive detection: mobile/tablet takes priority over consumer-controlled truncationMethod
|
|
30
30
|
const breakpoint = useBreakpoint();
|
|
31
31
|
const isMobile = isResponsiveEnabled || breakpoint <= Breakpoint.Medium;
|
|
32
|
-
// If there are more than 7 crumbs, break up crumbs into first link, ellipsis icon button, and current page ancestor
|
|
33
|
-
const shouldUseEllipsisTruncation = !isMobile && truncationMethod === 'ellipsis' && crumbs && crumbs.length > 7;
|
|
34
|
-
// Get the current page (last crumb) and all ancestors (all crumbs except last)
|
|
35
|
-
const currentPage = crumbs[crumbs.length - 1];
|
|
36
|
-
const ancestorCrumbs = crumbs.slice(0, -1);
|
|
37
|
-
// Folder-tree truncation: detect overflow and show up to 3 visible crumbs
|
|
38
32
|
const breadcrumbListRef = useRef(null);
|
|
39
|
-
const isFolderTreeTruncationEnabled = !isMobile && truncationMethod === 'folder-tree';
|
|
40
33
|
const {
|
|
34
|
+
ellipsizeLastCrumb,
|
|
35
|
+
iconButtonRef,
|
|
41
36
|
isTruncationRequired,
|
|
42
|
-
visibleCrumbCount
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
visibleCrumbCount
|
|
38
|
+
} = useFolderTreeTruncation(breadcrumbListRef, crumbs, isMobile);
|
|
39
|
+
if (!crumbs || crumbs.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
// Responsive detection: mobile/tablet takes priority over consumer-controlled truncationMethod
|
|
43
|
+
let breadcrumbList;
|
|
44
|
+
if (isMobile) {
|
|
45
|
+
breadcrumbList = jsx(MobileView, {
|
|
46
|
+
crumbs: crumbs,
|
|
47
|
+
isInteractive: isInteractive,
|
|
48
|
+
onPageLinkClick: onPageLinkClick,
|
|
49
|
+
size: size,
|
|
50
|
+
truncatedLinksIconAriaLabel: truncatedLinksIconAriaLabel
|
|
51
|
+
});
|
|
52
|
+
} else if (truncationMethod === 'ellipsis' && crumbs.length > ELLIPSIS_TRUNCATION_THRESHOLD) {
|
|
53
|
+
breadcrumbList = jsx(EllipsisTruncationView, {
|
|
54
|
+
crumbs: crumbs,
|
|
55
|
+
isInteractive: isInteractive,
|
|
56
|
+
onPageLinkClick: onPageLinkClick,
|
|
57
|
+
size: size,
|
|
58
|
+
truncatedLinksIconAriaLabel: truncatedLinksIconAriaLabel
|
|
59
|
+
});
|
|
60
|
+
} else if (truncationMethod === 'folder-tree' && isTruncationRequired) {
|
|
61
|
+
breadcrumbList = jsx(FolderTreeTruncationView, {
|
|
62
|
+
crumbs: crumbs,
|
|
63
|
+
ellipsizeLastCrumb: ellipsizeLastCrumb,
|
|
64
|
+
iconButtonRef: iconButtonRef,
|
|
65
|
+
isInteractive: isInteractive,
|
|
66
|
+
onPageLinkClick: onPageLinkClick,
|
|
67
|
+
size: size,
|
|
68
|
+
truncatedLinksIconAriaLabel: truncatedLinksIconAriaLabel,
|
|
69
|
+
visibleCrumbCount: visibleCrumbCount
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
breadcrumbList = jsx(FullView, {
|
|
73
|
+
crumbs: crumbs,
|
|
74
|
+
ellipsizeLastCrumb: ellipsizeLastCrumb,
|
|
75
|
+
isInteractive: isInteractive,
|
|
76
|
+
onPageLinkClick: onPageLinkClick,
|
|
77
|
+
size: size
|
|
78
|
+
});
|
|
79
|
+
}
|
|
48
80
|
return jsx("nav", {
|
|
49
81
|
ref: forwardedRef,
|
|
50
82
|
"aria-label": breadcrumbAriaLabel,
|
|
@@ -59,84 +91,12 @@ const Breadcrumb = /*#__PURE__*/forwardRef((props, forwardedRef) => {
|
|
|
59
91
|
"aria-label": rootIconAriaLabel
|
|
60
92
|
}) : jsx(FolderTree, {
|
|
61
93
|
"aria-label": rootIconAriaLabel
|
|
62
|
-
}),
|
|
94
|
+
}), jsx(PointerRight, {
|
|
63
95
|
height: getSeparatorSize(size),
|
|
64
96
|
role: "presentation",
|
|
65
97
|
width: getSeparatorSize(size)
|
|
66
98
|
})]
|
|
67
|
-
}),
|
|
68
|
-
children: [ancestorCrumbs.length > 0 && jsx(BreadcrumbDropdown, {
|
|
69
|
-
crumbsToRender: ancestorCrumbs,
|
|
70
|
-
iconButton: jsx(IconButton, {
|
|
71
|
-
"aria-label": truncatedLinksIconAriaLabel,
|
|
72
|
-
icon: FolderTree,
|
|
73
|
-
size: "small"
|
|
74
|
-
}),
|
|
75
|
-
onPageLinkClick: onPageLinkClick,
|
|
76
|
-
size: size
|
|
77
|
-
}), jsx(PageLink, {
|
|
78
|
-
crumb: currentPage,
|
|
79
|
-
isInteractive: isInteractive,
|
|
80
|
-
isLast: true,
|
|
81
|
-
onPageLinkClick: onPageLinkClick,
|
|
82
|
-
size: size
|
|
83
|
-
})]
|
|
84
|
-
}), shouldUseEllipsisTruncation && jsxs(Fragment, {
|
|
85
|
-
children: [jsx(PageLink, {
|
|
86
|
-
crumb: crumbs[0],
|
|
87
|
-
isInteractive: isInteractive,
|
|
88
|
-
isLast: false,
|
|
89
|
-
onPageLinkClick: onPageLinkClick,
|
|
90
|
-
size: size
|
|
91
|
-
}), jsx(BreadcrumbDropdown, {
|
|
92
|
-
crumbsToRender: crumbs.slice(1, crumbs.length - 2),
|
|
93
|
-
iconButton: jsx(IconButton, {
|
|
94
|
-
"aria-label": truncatedLinksIconAriaLabel,
|
|
95
|
-
icon: Ellipsis,
|
|
96
|
-
size: "small"
|
|
97
|
-
}),
|
|
98
|
-
onPageLinkClick: onPageLinkClick,
|
|
99
|
-
size: size
|
|
100
|
-
}), jsx(PageLink, {
|
|
101
|
-
crumb: crumbs[crumbs.length - 2],
|
|
102
|
-
isInteractive: isInteractive,
|
|
103
|
-
isLast: false,
|
|
104
|
-
onPageLinkClick: onPageLinkClick,
|
|
105
|
-
size: size
|
|
106
|
-
}), jsx(PageLink, {
|
|
107
|
-
crumb: currentPage,
|
|
108
|
-
isInteractive: isInteractive,
|
|
109
|
-
isLast: true,
|
|
110
|
-
onPageLinkClick: onPageLinkClick,
|
|
111
|
-
size: size
|
|
112
|
-
})]
|
|
113
|
-
}), shouldUseFolderTreeTruncation && jsxs(Fragment, {
|
|
114
|
-
children: [jsx(BreadcrumbDropdown, {
|
|
115
|
-
crumbsToRender: hiddenCrumbs,
|
|
116
|
-
iconButton: jsx(IconButton, {
|
|
117
|
-
"aria-label": truncatedLinksIconAriaLabel,
|
|
118
|
-
icon: FolderTree,
|
|
119
|
-
size: "small"
|
|
120
|
-
}),
|
|
121
|
-
listRef: iconButtonRef,
|
|
122
|
-
onPageLinkClick: onPageLinkClick,
|
|
123
|
-
size: size
|
|
124
|
-
}), visibleCrumbs.map((crumb, index) => jsx(PageLink, {
|
|
125
|
-
crumb: crumb,
|
|
126
|
-
isInteractive: isInteractive,
|
|
127
|
-
isLast: index === visibleCrumbs.length - 1,
|
|
128
|
-
onPageLinkClick: onPageLinkClick,
|
|
129
|
-
size: size
|
|
130
|
-
}, crumb.id))]
|
|
131
|
-
}), !isMobile && !shouldUseEllipsisTruncation && !shouldUseFolderTreeTruncation && crumbs?.map((crumb, index) => {
|
|
132
|
-
return jsx(PageLink, {
|
|
133
|
-
crumb: crumb,
|
|
134
|
-
isInteractive: isInteractive,
|
|
135
|
-
isLast: index === crumbs.length - 1,
|
|
136
|
-
onPageLinkClick: onPageLinkClick,
|
|
137
|
-
size: size
|
|
138
|
-
}, crumb.id);
|
|
139
|
-
})]
|
|
99
|
+
}), breadcrumbList]
|
|
140
100
|
})
|
|
141
101
|
});
|
|
142
102
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import '../index.css';
|
|
2
|
-
var styles = {"container":"bp_breadcrumb_module_container--
|
|
2
|
+
var styles = {"container":"bp_breadcrumb_module_container--67af7","breadcrumb":"bp_breadcrumb_module_breadcrumb--67af7","pageLink":"bp_breadcrumb_module_pageLink--67af7","linkWithHover":"bp_breadcrumb_module_linkWithHover--67af7","dropdownContent":"bp_breadcrumb_module_dropdownContent--67af7"};
|
|
3
3
|
|
|
4
4
|
export { styles as default };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { Ellipsis } from '@box/blueprint-web-assets/icons/Medium';
|
|
3
|
+
import { IconButton } from '../primitives/icon-button/icon-button.js';
|
|
4
|
+
import { BreadcrumbDropdown } from './breadcrumb-dropdown.js';
|
|
5
|
+
import { PageLink } from './page-link.js';
|
|
6
|
+
|
|
7
|
+
const EllipsisTruncationView = ({
|
|
8
|
+
crumbs,
|
|
9
|
+
isInteractive,
|
|
10
|
+
onPageLinkClick,
|
|
11
|
+
size,
|
|
12
|
+
truncatedLinksIconAriaLabel
|
|
13
|
+
}) => {
|
|
14
|
+
const middleCrumbs = crumbs.slice(1, -2);
|
|
15
|
+
const lastTwoCrumbs = crumbs.slice(-2);
|
|
16
|
+
return jsxs(Fragment, {
|
|
17
|
+
children: [jsx(PageLink, {
|
|
18
|
+
crumb: crumbs[0],
|
|
19
|
+
isInteractive: isInteractive,
|
|
20
|
+
isLast: false,
|
|
21
|
+
onPageLinkClick: onPageLinkClick,
|
|
22
|
+
size: size
|
|
23
|
+
}), jsx(BreadcrumbDropdown, {
|
|
24
|
+
crumbsToRender: middleCrumbs,
|
|
25
|
+
iconButton: jsx(IconButton, {
|
|
26
|
+
"aria-label": truncatedLinksIconAriaLabel,
|
|
27
|
+
icon: Ellipsis,
|
|
28
|
+
size: "small"
|
|
29
|
+
}),
|
|
30
|
+
onPageLinkClick: onPageLinkClick,
|
|
31
|
+
size: size
|
|
32
|
+
}), lastTwoCrumbs.map((crumb, index) => jsx(PageLink, {
|
|
33
|
+
crumb: crumb,
|
|
34
|
+
isInteractive: isInteractive,
|
|
35
|
+
isLast: index === lastTwoCrumbs.length - 1,
|
|
36
|
+
onPageLinkClick: onPageLinkClick,
|
|
37
|
+
size: size
|
|
38
|
+
}, crumb.id))]
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { EllipsisTruncationView };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { type FolderTreeTruncationViewProps } from './types';
|
|
2
|
+
export declare const FolderTreeTruncationView: ({ crumbs, ellipsizeLastCrumb, iconButtonRef, isInteractive, onPageLinkClick, size, truncatedLinksIconAriaLabel, visibleCrumbCount, }: FolderTreeTruncationViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { FolderTree } from '@box/blueprint-web-assets/icons/Fill';
|
|
3
|
+
import { IconButton } from '../primitives/icon-button/icon-button.js';
|
|
4
|
+
import { BreadcrumbDropdown } from './breadcrumb-dropdown.js';
|
|
5
|
+
import { PageLink } from './page-link.js';
|
|
6
|
+
|
|
7
|
+
const FolderTreeTruncationView = ({
|
|
8
|
+
crumbs,
|
|
9
|
+
ellipsizeLastCrumb,
|
|
10
|
+
iconButtonRef,
|
|
11
|
+
isInteractive,
|
|
12
|
+
onPageLinkClick,
|
|
13
|
+
size,
|
|
14
|
+
truncatedLinksIconAriaLabel,
|
|
15
|
+
visibleCrumbCount
|
|
16
|
+
}) => {
|
|
17
|
+
const visibleCrumbs = crumbs.slice(-visibleCrumbCount);
|
|
18
|
+
const hiddenCrumbs = crumbs.slice(0, -visibleCrumbCount);
|
|
19
|
+
return jsxs(Fragment, {
|
|
20
|
+
children: [jsx(BreadcrumbDropdown, {
|
|
21
|
+
crumbsToRender: hiddenCrumbs,
|
|
22
|
+
iconButton: jsx(IconButton, {
|
|
23
|
+
"aria-label": truncatedLinksIconAriaLabel,
|
|
24
|
+
icon: FolderTree,
|
|
25
|
+
size: "small"
|
|
26
|
+
}),
|
|
27
|
+
listRef: iconButtonRef,
|
|
28
|
+
onPageLinkClick: onPageLinkClick,
|
|
29
|
+
size: size
|
|
30
|
+
}), visibleCrumbs.map((crumb, index) => jsx(PageLink, {
|
|
31
|
+
crumb: crumb,
|
|
32
|
+
ellipsizeLastCrumb: ellipsizeLastCrumb,
|
|
33
|
+
isInteractive: isInteractive,
|
|
34
|
+
isLast: index === visibleCrumbs.length - 1,
|
|
35
|
+
onPageLinkClick: onPageLinkClick,
|
|
36
|
+
size: size
|
|
37
|
+
}, crumb.id))]
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { FolderTreeTruncationView };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { PageLink } from './page-link.js';
|
|
3
|
+
|
|
4
|
+
const FullView = ({
|
|
5
|
+
crumbs,
|
|
6
|
+
ellipsizeLastCrumb,
|
|
7
|
+
isInteractive,
|
|
8
|
+
onPageLinkClick,
|
|
9
|
+
size
|
|
10
|
+
}) => crumbs.map((crumb, index) => jsx(PageLink, {
|
|
11
|
+
crumb: crumb,
|
|
12
|
+
ellipsizeLastCrumb: ellipsizeLastCrumb,
|
|
13
|
+
isInteractive: isInteractive,
|
|
14
|
+
isLast: index === crumbs.length - 1,
|
|
15
|
+
onPageLinkClick: onPageLinkClick,
|
|
16
|
+
size: size
|
|
17
|
+
}, crumb.id));
|
|
18
|
+
|
|
19
|
+
export { FullView };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { FolderTree } from '@box/blueprint-web-assets/icons/Fill';
|
|
3
|
+
import { IconButton } from '../primitives/icon-button/icon-button.js';
|
|
4
|
+
import { BreadcrumbDropdown } from './breadcrumb-dropdown.js';
|
|
5
|
+
import { PageLink } from './page-link.js';
|
|
6
|
+
|
|
7
|
+
const MobileView = ({
|
|
8
|
+
crumbs,
|
|
9
|
+
isInteractive,
|
|
10
|
+
onPageLinkClick,
|
|
11
|
+
size,
|
|
12
|
+
truncatedLinksIconAriaLabel
|
|
13
|
+
}) => {
|
|
14
|
+
const currentPage = crumbs[crumbs.length - 1];
|
|
15
|
+
const hiddenCrumbs = crumbs.slice(0, -1);
|
|
16
|
+
return jsxs(Fragment, {
|
|
17
|
+
children: [hiddenCrumbs.length > 0 && jsx(BreadcrumbDropdown, {
|
|
18
|
+
crumbsToRender: hiddenCrumbs,
|
|
19
|
+
iconButton: jsx(IconButton, {
|
|
20
|
+
"aria-label": truncatedLinksIconAriaLabel,
|
|
21
|
+
icon: FolderTree,
|
|
22
|
+
size: "small"
|
|
23
|
+
}),
|
|
24
|
+
onPageLinkClick: onPageLinkClick,
|
|
25
|
+
size: size
|
|
26
|
+
}), jsx(PageLink, {
|
|
27
|
+
crumb: currentPage,
|
|
28
|
+
ellipsizeLastCrumb: true,
|
|
29
|
+
isInteractive: isInteractive,
|
|
30
|
+
isLast: true,
|
|
31
|
+
onPageLinkClick: onPageLinkClick,
|
|
32
|
+
size: size
|
|
33
|
+
})]
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { MobileView };
|
|
@@ -1,17 +1,2 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
3
|
-
* A page link represents a combination of an optional folder icon, a single crumb, and a separator.
|
|
4
|
-
*/
|
|
5
|
-
export interface PageLinkProps {
|
|
6
|
-
/** The crumb to display in the page link. */
|
|
7
|
-
crumb: Crumb;
|
|
8
|
-
/** Whether the page link is the last crumb in the breadcrumb. */
|
|
9
|
-
isLast: boolean;
|
|
10
|
-
/** Whether the page link is interactive. */
|
|
11
|
-
isInteractive: boolean;
|
|
12
|
-
/** The callback to call when the page link is clicked. */
|
|
13
|
-
onPageLinkClick: (id: string) => void;
|
|
14
|
-
/** The text size of the page link. */
|
|
15
|
-
size: 'xsmall' | 'small' | 'medium' | 'large';
|
|
16
|
-
}
|
|
17
|
-
export declare const PageLink: ({ crumb, isLast, isInteractive, onPageLinkClick, size }: PageLinkProps) => import("react/jsx-runtime").JSX.Element;
|
|
1
|
+
import { type PageLinkProps } from './types';
|
|
2
|
+
export declare const PageLink: ({ crumb, ellipsizeLastCrumb, isInteractive, isLast, onPageLinkClick, size, }: PageLinkProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import { useCallback } from 'react';
|
|
3
3
|
import { PointerRight } from '@box/blueprint-web-assets/icons/Fill';
|
|
4
|
+
import { EllipsizableText } from '../ellipsizable-text/ellipsizable-text.js';
|
|
4
5
|
import { Link } from '../primitives/link/link.js';
|
|
5
6
|
import { Text } from '../text/text.js';
|
|
6
|
-
import styles from './breadcrumb.module.js';
|
|
7
7
|
import { getBoldTextVariantFromSize, getTextVariantFromSize, getSeparatorSize } from './utils.js';
|
|
8
|
+
import styles from './breadcrumb.module.js';
|
|
8
9
|
|
|
9
10
|
const PageLink = ({
|
|
10
11
|
crumb,
|
|
11
|
-
|
|
12
|
+
ellipsizeLastCrumb = false,
|
|
12
13
|
isInteractive,
|
|
14
|
+
isLast,
|
|
13
15
|
onPageLinkClick,
|
|
14
16
|
size
|
|
15
17
|
}) => {
|
|
16
18
|
const handlePageLinkClick = useCallback(() => {
|
|
17
19
|
onPageLinkClick(crumb.id);
|
|
18
|
-
}, [
|
|
20
|
+
}, [crumb.id, onPageLinkClick]);
|
|
19
21
|
if (isLast) {
|
|
20
22
|
return jsx("li", {
|
|
21
23
|
className: styles.pageLink,
|
|
22
|
-
children: jsx(
|
|
24
|
+
children: ellipsizeLastCrumb ? jsx(EllipsizableText, {
|
|
25
|
+
"aria-current": "page",
|
|
26
|
+
as: "span",
|
|
27
|
+
color: "textOnLightDefault",
|
|
28
|
+
lineClamp: 1,
|
|
29
|
+
variant: getBoldTextVariantFromSize(size),
|
|
30
|
+
children: crumb.name
|
|
31
|
+
}) : jsx(Text, {
|
|
23
32
|
"aria-current": "page",
|
|
24
33
|
as: "span",
|
|
25
34
|
color: "textOnLightDefault",
|
|
@@ -23,7 +23,8 @@ export type BreadcrumbProps = {
|
|
|
23
23
|
size?: 'xsmall' | 'small' | 'medium' | 'large';
|
|
24
24
|
/** Aria label for the icon button when breadcrumb is truncated. */
|
|
25
25
|
truncatedLinksIconAriaLabel: string;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Controls behavior when there are too many crumbs to display.
|
|
27
28
|
* Ellipsis shows the crumbs at the beginning and end, with an ellipsis icon in between.
|
|
28
29
|
* Folder-tree dynamically detects container overflow and shows up to 3 visible crumbs with a folder tree icon dropdown.
|
|
29
30
|
*/
|
|
@@ -36,3 +37,53 @@ export interface rootIconProps {
|
|
|
36
37
|
/** Determines which icon is displayed at the root. */
|
|
37
38
|
rootIconVariant: 'home' | 'folder-tree';
|
|
38
39
|
}
|
|
40
|
+
export interface BreadcrumbViewProps {
|
|
41
|
+
crumbs: Crumb[];
|
|
42
|
+
ellipsizeLastCrumb?: boolean;
|
|
43
|
+
isInteractive: boolean;
|
|
44
|
+
onPageLinkClick: (id: string) => void;
|
|
45
|
+
size: NonNullable<BreadcrumbProps['size']>;
|
|
46
|
+
truncatedLinksIconAriaLabel: string;
|
|
47
|
+
}
|
|
48
|
+
export interface FolderTreeTruncationViewProps extends BreadcrumbViewProps {
|
|
49
|
+
iconButtonRef: React.RefObject<HTMLLIElement>;
|
|
50
|
+
visibleCrumbCount: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A page link represents a combination of an optional folder icon, a single crumb, and a separator.
|
|
54
|
+
*/
|
|
55
|
+
export interface PageLinkProps {
|
|
56
|
+
/** The crumb to display in the page link. */
|
|
57
|
+
crumb: Crumb;
|
|
58
|
+
/** Whether the last crumb should be ellipsized. */
|
|
59
|
+
ellipsizeLastCrumb?: boolean;
|
|
60
|
+
/** Whether the page link is interactive. */
|
|
61
|
+
isInteractive: boolean;
|
|
62
|
+
/** Whether the page link is the last crumb in the breadcrumb. */
|
|
63
|
+
isLast: boolean;
|
|
64
|
+
/** The callback to call when the page link is clicked. */
|
|
65
|
+
onPageLinkClick: (id: string) => void;
|
|
66
|
+
/** The text size of the page link. */
|
|
67
|
+
size: 'xsmall' | 'small' | 'medium' | 'large';
|
|
68
|
+
}
|
|
69
|
+
export interface TruncationState {
|
|
70
|
+
/** Whether the last crumb should be ellipsized (when it doesn't fit even with truncation) */
|
|
71
|
+
ellipsizeLastCrumb: boolean;
|
|
72
|
+
/** Whether the breadcrumb content would exceed the container width IF no truncation was being applied */
|
|
73
|
+
isTruncationRequired: boolean;
|
|
74
|
+
/** Number of visible crumbs (not in dropdown) to show (1-3) */
|
|
75
|
+
visibleCrumbCount: number;
|
|
76
|
+
}
|
|
77
|
+
export interface FolderTreeTruncationResult extends TruncationState {
|
|
78
|
+
/** Ref to attach to the icon button container for width measurement */
|
|
79
|
+
iconButtonRef: React.RefObject<HTMLLIElement>;
|
|
80
|
+
}
|
|
81
|
+
export interface PerformTruncationParams {
|
|
82
|
+
container: HTMLOListElement;
|
|
83
|
+
crumbCount: number;
|
|
84
|
+
iconButtonRef: React.RefObject<HTMLLIElement>;
|
|
85
|
+
isTruncationRequired: boolean;
|
|
86
|
+
measuredIconButtonWidth: React.MutableRefObject<number>;
|
|
87
|
+
setState: React.Dispatch<React.SetStateAction<TruncationState>>;
|
|
88
|
+
storedCrumbWidths: React.MutableRefObject<number[]>;
|
|
89
|
+
}
|
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
import { type Crumb } from './types';
|
|
2
|
-
interface FolderTreeTruncationState {
|
|
3
|
-
/** Whether the breadcrumb content would exceed the container width IF no truncation was being applied */
|
|
4
|
-
isTruncationRequired: boolean;
|
|
5
|
-
/** Number of visible crumbs (not in dropdown) to show (1-3) */
|
|
6
|
-
visibleCrumbCount: number;
|
|
7
|
-
}
|
|
8
|
-
interface FolderTreeTruncationResult extends FolderTreeTruncationState {
|
|
9
|
-
/** Ref to attach to the icon button container li element for width measurement */
|
|
10
|
-
iconButtonRef: React.RefObject<HTMLLIElement>;
|
|
11
|
-
}
|
|
1
|
+
import { type Crumb, type FolderTreeTruncationResult } from './types';
|
|
12
2
|
/**
|
|
13
|
-
* Hook
|
|
3
|
+
* Hook that calculates optimal breadcrumb truncation for folder-tree display.
|
|
14
4
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - **>3 crumbs**: Render truncated state immediately, show icon button + up to 3 crumbs
|
|
5
|
+
* Uses a cascading fallback strategy to show as many crumbs as possible without
|
|
6
|
+
* text ellipsis, falling back to ellipsis only as a last resort.
|
|
18
7
|
*
|
|
19
|
-
* @param containerRef - Ref to the
|
|
20
|
-
* @param crumbs - Array of breadcrumb items
|
|
21
|
-
* @param
|
|
22
|
-
* @returns Object containing wouldCrumbsOverflow, visibleCrumbCount, and iconButtonRef
|
|
8
|
+
* @param containerRef - Ref to the breadcrumb list container (ol element)
|
|
9
|
+
* @param crumbs - Array of breadcrumb items to display
|
|
10
|
+
* @param isMobile - Whether the breadcrumb is on a mobile device
|
|
23
11
|
*/
|
|
24
|
-
export declare const useFolderTreeTruncation: (containerRef: React.RefObject<HTMLOListElement>, crumbs: Crumb[],
|
|
25
|
-
export {};
|
|
12
|
+
export declare const useFolderTreeTruncation: (containerRef: React.RefObject<HTMLOListElement>, crumbs: Crumb[], isMobile: boolean) => FolderTreeTruncationResult;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import noop from 'lodash/noop';
|
|
2
|
-
import { useState, useRef, useCallback,
|
|
2
|
+
import { useState, useRef, useCallback, useLayoutEffect } from 'react';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const MAX_VISIBLE_CRUMBS = 3;
|
|
5
|
+
/**
|
|
6
|
+
* Calculates how many crumbs (from the end) can fit within the available width.
|
|
7
|
+
* Returns at least 1 to ensure we always show the current page.
|
|
8
|
+
*/
|
|
8
9
|
const calculateVisibleCrumbCount = (crumbWidths, availableWidth, gap, maxVisible) => {
|
|
9
10
|
let crumbWidthWithoutGaps = 0;
|
|
10
11
|
let visibleCount = 0;
|
|
@@ -20,15 +21,21 @@ const calculateVisibleCrumbCount = (crumbWidths, availableWidth, gap, maxVisible
|
|
|
20
21
|
}
|
|
21
22
|
return Math.max(1, visibleCount);
|
|
22
23
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Measures the width of each crumb element.
|
|
26
|
+
*
|
|
27
|
+
* For the last crumb (no separator), we measure just the text element.
|
|
28
|
+
* For other crumbs, we measure the full <li> to include the separator.
|
|
29
|
+
*/
|
|
30
|
+
const measureCrumbWidths = (crumbElements, isLastCrumbIncluded) => {
|
|
31
|
+
return crumbElements.map((element, index) => {
|
|
32
|
+
const isLastCrumb = isLastCrumbIncluded && index === crumbElements.length - 1;
|
|
33
|
+
if (isLastCrumb) {
|
|
34
|
+
const textElement = element.firstElementChild;
|
|
35
|
+
return textElement?.offsetWidth ?? element.offsetWidth;
|
|
36
|
+
}
|
|
37
|
+
return element.offsetWidth;
|
|
30
38
|
});
|
|
31
|
-
return widths;
|
|
32
39
|
};
|
|
33
40
|
/**
|
|
34
41
|
* Calculate total width of crumbs including gaps
|
|
@@ -39,97 +46,50 @@ const calculateTotalWidth = (widths, gap) => {
|
|
|
39
46
|
return totalCrumbsWidth + totalGapWidth;
|
|
40
47
|
};
|
|
41
48
|
/**
|
|
42
|
-
*
|
|
49
|
+
* Determines the best truncation state using a cascading fallback strategy.
|
|
50
|
+
*
|
|
51
|
+
* Priority order (from most preferred to last resort):
|
|
52
|
+
* 1. Show all crumbs without truncation or ellipsis
|
|
53
|
+
* 2. Show icon + fewer crumbs without ellipsis
|
|
54
|
+
* 3. Show icon + 1 crumb with ellipsis (last resort)
|
|
43
55
|
*/
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
iconButtonWidth,
|
|
49
|
-
crumbCount,
|
|
50
|
-
storedCrumbWidths
|
|
51
|
-
} = context;
|
|
52
|
-
// If we don't have stored crumb widths yet, measure them
|
|
53
|
-
const crumbWidths = storedCrumbWidths.length < crumbCount ? measureCrumbWidths(children, crumbCount) : storedCrumbWidths;
|
|
54
|
-
// Icon button not yet rendered - use conservative fallback
|
|
55
|
-
if (iconButtonWidth === 0) {
|
|
56
|
+
const handleTruncationState = (crumbWidths, containerWidth, iconButtonWidth, gap) => {
|
|
57
|
+
const crumbCount = crumbWidths.length;
|
|
58
|
+
// Edge case: no crumbs
|
|
59
|
+
if (crumbCount === 0) {
|
|
56
60
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
crumbWidths
|
|
61
|
+
isTruncationRequired: false,
|
|
62
|
+
ellipsizeLastCrumb: false,
|
|
63
|
+
visibleCrumbCount: 0
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
isTruncationRequired: true,
|
|
69
|
-
visibleCrumbCount: visibleCount
|
|
70
|
-
},
|
|
71
|
-
crumbWidths
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
/**
|
|
75
|
-
* Get crumb widths for total width calculation
|
|
76
|
-
* Factored out into its own function to reduce cognitive complexity
|
|
77
|
-
*/
|
|
78
|
-
const getCrumbWidths = (children, storedWidths, crumbCount, isCurrentlyTruncated) => {
|
|
79
|
-
if (storedWidths.length === crumbCount) {
|
|
80
|
-
return storedWidths; // Already have all widths
|
|
81
|
-
}
|
|
82
|
-
// The first child could be the icon button, so we don't want to measure it as part of the crumbs
|
|
83
|
-
const crumbElements = isCurrentlyTruncated ? children.slice(1) : children;
|
|
84
|
-
return crumbElements.map(crumbElement => crumbElement.offsetWidth);
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* Handle ≤3 crumbs case: Render all first, detect overflow, truncate if needed
|
|
88
|
-
*/
|
|
89
|
-
const handleConditionalTruncate = (children, context, isCurrentlyTruncated) => {
|
|
90
|
-
const {
|
|
91
|
-
containerWidth,
|
|
92
|
-
gap,
|
|
93
|
-
iconButtonWidth,
|
|
94
|
-
crumbCount,
|
|
95
|
-
storedCrumbWidths
|
|
96
|
-
} = context;
|
|
97
|
-
// Get crumb widths when all crumbs are visible (not truncated)
|
|
98
|
-
const crumbWidths = !isCurrentlyTruncated && children.length === crumbCount ? children.map(crumbElement => crumbElement.offsetWidth) // fresh measurement
|
|
99
|
-
: storedCrumbWidths; // Reuse stored
|
|
100
|
-
// Calculate total width to check for overflow
|
|
101
|
-
const widthsForCalculation = getCrumbWidths(children, crumbWidths, crumbCount, isCurrentlyTruncated);
|
|
102
|
-
const totalWidth = calculateTotalWidth(widthsForCalculation, gap);
|
|
103
|
-
const areCrumbsOverflowingWithoutTruncation = totalWidth > containerWidth;
|
|
104
|
-
// No overflow - show all crumbs
|
|
105
|
-
if (!areCrumbsOverflowingWithoutTruncation) {
|
|
66
|
+
const totalWidthAllCrumbs = calculateTotalWidth(crumbWidths, gap);
|
|
67
|
+
const availableWidthWithIcon = Math.max(0, containerWidth - iconButtonWidth - gap);
|
|
68
|
+
// Check if all crumbs fit without truncation (up to MAX_VISIBLE_CRUMBS)
|
|
69
|
+
if (crumbCount <= MAX_VISIBLE_CRUMBS && totalWidthAllCrumbs <= containerWidth) {
|
|
106
70
|
return {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
crumbWidths
|
|
71
|
+
isTruncationRequired: false,
|
|
72
|
+
ellipsizeLastCrumb: false,
|
|
73
|
+
visibleCrumbCount: crumbCount
|
|
112
74
|
};
|
|
113
75
|
}
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
if (iconButtonWidth === 0) {
|
|
76
|
+
// Single crumb: never show icon, just ellipsize if needed
|
|
77
|
+
if (crumbCount === 1) {
|
|
117
78
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
},
|
|
122
|
-
crumbWidths
|
|
79
|
+
isTruncationRequired: false,
|
|
80
|
+
visibleCrumbCount: 1,
|
|
81
|
+
ellipsizeLastCrumb: totalWidthAllCrumbs > containerWidth
|
|
123
82
|
};
|
|
124
83
|
}
|
|
125
|
-
|
|
126
|
-
const
|
|
84
|
+
// Calculate how many crumbs fit with the icon button
|
|
85
|
+
const visibleCrumbCount = calculateVisibleCrumbCount(crumbWidths, availableWidthWithIcon, gap, MAX_VISIBLE_CRUMBS);
|
|
86
|
+
// If only 1 crumb fits and it still doesn't fit fully, enable ellipsis
|
|
87
|
+
const lastCrumbWidth = crumbWidths[crumbWidths.length - 1];
|
|
88
|
+
const ellipsizeLastCrumb = visibleCrumbCount === 1 && lastCrumbWidth > availableWidthWithIcon;
|
|
127
89
|
return {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
crumbWidths
|
|
90
|
+
isTruncationRequired: true,
|
|
91
|
+
visibleCrumbCount,
|
|
92
|
+
ellipsizeLastCrumb
|
|
133
93
|
};
|
|
134
94
|
};
|
|
135
95
|
/**
|
|
@@ -141,79 +101,84 @@ const performTruncationCalculation = ({
|
|
|
141
101
|
measuredIconButtonWidth,
|
|
142
102
|
storedCrumbWidths,
|
|
143
103
|
crumbCount,
|
|
144
|
-
isTruncationRequiredBaseOnCrumbLength,
|
|
145
104
|
isTruncationRequired,
|
|
146
105
|
setState
|
|
147
106
|
}) => {
|
|
148
107
|
const containerWidth = container.clientWidth;
|
|
149
|
-
const children = Array.from(container.children);
|
|
150
108
|
const computedStyle = getComputedStyle(container);
|
|
151
109
|
const gap = parseFloat(computedStyle.gap) || 0;
|
|
110
|
+
const children = Array.from(container.children);
|
|
152
111
|
if (iconButtonRef.current) {
|
|
153
112
|
measuredIconButtonWidth.current = iconButtonRef.current.offsetWidth;
|
|
154
113
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
crumbCount
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
114
|
+
// When truncated, first child is icon button, so skip it
|
|
115
|
+
const crumbElements = isTruncationRequired ? children.slice(1) : children;
|
|
116
|
+
if (storedCrumbWidths.current.length < crumbCount && crumbElements.length > 0) {
|
|
117
|
+
const startIndex = crumbCount - crumbElements.length;
|
|
118
|
+
const isLastCrumbIncluded = startIndex + crumbElements.length === crumbCount;
|
|
119
|
+
const measuredWidths = measureCrumbWidths(crumbElements, isLastCrumbIncluded);
|
|
120
|
+
measuredWidths.forEach((width, i) => {
|
|
121
|
+
storedCrumbWidths.current[startIndex + i] = width;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (storedCrumbWidths.current.length < crumbCount) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setState(handleTruncationState(storedCrumbWidths.current, containerWidth, measuredIconButtonWidth.current, gap));
|
|
165
128
|
};
|
|
166
129
|
/**
|
|
167
|
-
* Hook
|
|
130
|
+
* Hook that calculates optimal breadcrumb truncation for folder-tree display.
|
|
168
131
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* - **>3 crumbs**: Render truncated state immediately, show icon button + up to 3 crumbs
|
|
132
|
+
* Uses a cascading fallback strategy to show as many crumbs as possible without
|
|
133
|
+
* text ellipsis, falling back to ellipsis only as a last resort.
|
|
172
134
|
*
|
|
173
|
-
* @param containerRef - Ref to the
|
|
174
|
-
* @param crumbs - Array of breadcrumb items
|
|
175
|
-
* @param
|
|
176
|
-
* @returns Object containing wouldCrumbsOverflow, visibleCrumbCount, and iconButtonRef
|
|
135
|
+
* @param containerRef - Ref to the breadcrumb list container (ol element)
|
|
136
|
+
* @param crumbs - Array of breadcrumb items to display
|
|
137
|
+
* @param isMobile - Whether the breadcrumb is on a mobile device
|
|
177
138
|
*/
|
|
178
|
-
const useFolderTreeTruncation = (containerRef, crumbs,
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const
|
|
139
|
+
const useFolderTreeTruncation = (containerRef, crumbs, isMobile) => {
|
|
140
|
+
const crumbCount = crumbs.length;
|
|
141
|
+
// Initial state: for 4+ crumbs, start truncated; otherwise show all
|
|
142
|
+
const initialState = {
|
|
143
|
+
isTruncationRequired: crumbCount > MAX_VISIBLE_CRUMBS,
|
|
144
|
+
ellipsizeLastCrumb: false,
|
|
145
|
+
visibleCrumbCount: Math.min(crumbCount, MAX_VISIBLE_CRUMBS)
|
|
146
|
+
};
|
|
147
|
+
const [state, setState] = useState(initialState);
|
|
182
148
|
const iconButtonRef = useRef(null);
|
|
183
|
-
//
|
|
184
|
-
const measuredIconButtonWidth = useRef(0);
|
|
149
|
+
// Cache for measured values to avoid re-measuring on every resize
|
|
185
150
|
const storedCrumbWidths = useRef([]);
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
151
|
+
const measuredIconButtonWidth = useRef(0);
|
|
152
|
+
const prevCrumbCount = useRef(crumbCount);
|
|
153
|
+
// Reset cache when crumbs change (user navigated)
|
|
154
|
+
if (prevCrumbCount.current !== crumbCount) {
|
|
189
155
|
storedCrumbWidths.current = [];
|
|
190
|
-
|
|
191
|
-
|
|
156
|
+
measuredIconButtonWidth.current = 0;
|
|
157
|
+
prevCrumbCount.current = crumbCount;
|
|
158
|
+
setState(initialState);
|
|
192
159
|
}
|
|
193
160
|
const calculateTruncation = useCallback(() => {
|
|
194
161
|
const container = containerRef.current;
|
|
195
162
|
if (!container) {
|
|
196
163
|
return;
|
|
197
164
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
isTruncationRequired: state.isTruncationRequired,
|
|
207
|
-
setState
|
|
208
|
-
});
|
|
165
|
+
performTruncationCalculation({
|
|
166
|
+
container,
|
|
167
|
+
iconButtonRef,
|
|
168
|
+
measuredIconButtonWidth,
|
|
169
|
+
storedCrumbWidths,
|
|
170
|
+
crumbCount,
|
|
171
|
+
isTruncationRequired: state.isTruncationRequired,
|
|
172
|
+
setState
|
|
209
173
|
});
|
|
210
|
-
}, [containerRef,
|
|
211
|
-
|
|
174
|
+
}, [containerRef, crumbCount, state.isTruncationRequired]);
|
|
175
|
+
useLayoutEffect(() => {
|
|
212
176
|
// Reset state when truncation is disabled (e.g., responsive breakpoint triggers mobile view)
|
|
213
|
-
if (
|
|
177
|
+
if (isMobile) {
|
|
214
178
|
setState({
|
|
215
179
|
isTruncationRequired: false,
|
|
216
|
-
|
|
180
|
+
ellipsizeLastCrumb: false,
|
|
181
|
+
visibleCrumbCount: crumbCount
|
|
217
182
|
});
|
|
218
183
|
measuredIconButtonWidth.current = 0;
|
|
219
184
|
storedCrumbWidths.current = [];
|
|
@@ -230,11 +195,8 @@ const useFolderTreeTruncation = (containerRef, crumbs, isEnabled) => {
|
|
|
230
195
|
calculateTruncation();
|
|
231
196
|
return () => {
|
|
232
197
|
observer.disconnect();
|
|
233
|
-
if (animationFrameId.current !== null) {
|
|
234
|
-
cancelAnimationFrame(animationFrameId.current);
|
|
235
|
-
}
|
|
236
198
|
};
|
|
237
|
-
}, [
|
|
199
|
+
}, [calculateTruncation, containerRef, crumbCount, isMobile]);
|
|
238
200
|
return {
|
|
239
201
|
...state,
|
|
240
202
|
iconButtonRef
|
|
@@ -204,6 +204,10 @@ export interface ComboboxBaseProps<Multiple extends boolean, FreeInput extends b
|
|
|
204
204
|
* Callback used when combobox input/textarea loses focus
|
|
205
205
|
*/
|
|
206
206
|
onBlur?: (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
207
|
+
/**
|
|
208
|
+
* Callback when text is pasted into the input
|
|
209
|
+
*/
|
|
210
|
+
onPaste?: React.ClipboardEventHandler<HTMLInputElement>;
|
|
207
211
|
/**
|
|
208
212
|
* aria-label passed to the Combobox clear button. If not provided, the clear button is not shown.
|
|
209
213
|
*/
|
package/dist/lib-esm/index.css
CHANGED
|
@@ -2623,11 +2623,11 @@
|
|
|
2623
2623
|
.bp_menu_item_sections_module_menuItemSideContent--5517f.bp_menu_item_sections_module_textOnLightSecondary--5517f{
|
|
2624
2624
|
color:var(--menu-item-text-color);
|
|
2625
2625
|
}
|
|
2626
|
-
.bp_breadcrumb_module_container--
|
|
2626
|
+
.bp_breadcrumb_module_container--67af7{
|
|
2627
2627
|
height:var(--bp-size-060);
|
|
2628
2628
|
width:100%;
|
|
2629
2629
|
}
|
|
2630
|
-
.bp_breadcrumb_module_container--
|
|
2630
|
+
.bp_breadcrumb_module_container--67af7 .bp_breadcrumb_module_breadcrumb--67af7{
|
|
2631
2631
|
align-items:center;
|
|
2632
2632
|
display:flex;
|
|
2633
2633
|
flex-wrap:nowrap;
|
|
@@ -2635,22 +2635,26 @@
|
|
|
2635
2635
|
height:100%;
|
|
2636
2636
|
list-style:none;
|
|
2637
2637
|
margin:0;
|
|
2638
|
-
overflow:hidden;
|
|
2639
2638
|
padding:0;
|
|
2640
2639
|
}
|
|
2641
|
-
.bp_breadcrumb_module_container--
|
|
2640
|
+
.bp_breadcrumb_module_container--67af7 .bp_breadcrumb_module_pageLink--67af7{
|
|
2642
2641
|
align-items:center;
|
|
2643
2642
|
display:flex;
|
|
2644
2643
|
flex-shrink:0;
|
|
2645
2644
|
gap:var(--bp-size-010);
|
|
2645
|
+
min-width:0;
|
|
2646
|
+
overflow:hidden;
|
|
2646
2647
|
white-space:nowrap;
|
|
2647
2648
|
}
|
|
2649
|
+
.bp_breadcrumb_module_container--67af7 .bp_breadcrumb_module_pageLink--67af7:last-child{
|
|
2650
|
+
flex-shrink:1;
|
|
2651
|
+
}
|
|
2648
2652
|
|
|
2649
|
-
.bp_breadcrumb_module_linkWithHover--
|
|
2653
|
+
.bp_breadcrumb_module_linkWithHover--67af7{
|
|
2650
2654
|
cursor:pointer;
|
|
2651
2655
|
position:relative;
|
|
2652
2656
|
}
|
|
2653
|
-
.bp_breadcrumb_module_linkWithHover--
|
|
2657
|
+
.bp_breadcrumb_module_linkWithHover--67af7::after{
|
|
2654
2658
|
background-color:var(--bp-text-text-on-light-secondary);
|
|
2655
2659
|
bottom:0;
|
|
2656
2660
|
content:"";
|
|
@@ -2662,12 +2666,13 @@
|
|
|
2662
2666
|
transition:transform var(--animation-duration-2) var(--animation-easing-ease-base);
|
|
2663
2667
|
width:100%;
|
|
2664
2668
|
}
|
|
2665
|
-
.bp_breadcrumb_module_linkWithHover--
|
|
2669
|
+
.bp_breadcrumb_module_linkWithHover--67af7:hover::after{
|
|
2666
2670
|
transform:scaleX(1);
|
|
2667
2671
|
}
|
|
2668
2672
|
|
|
2669
|
-
.bp_breadcrumb_module_dropdownContent--
|
|
2673
|
+
.bp_breadcrumb_module_dropdownContent--67af7{
|
|
2670
2674
|
--blueprint-web-dropdown-menu-max-height:calc(var(--bp-size-300)*2);
|
|
2675
|
+
z-index:2147483646;
|
|
2671
2676
|
}
|
|
2672
2677
|
.bp_link_module_link--27104{
|
|
2673
2678
|
color:var(--text-cta-link);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@box/blueprint-web",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.136.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"publishConfig": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@ariakit/react": "0.4.15",
|
|
49
49
|
"@ariakit/react-core": "0.4.15",
|
|
50
|
-
"@box/blueprint-web-assets": "^4.101.
|
|
50
|
+
"@box/blueprint-web-assets": "^4.101.3",
|
|
51
51
|
"@internationalized/date": "^3.7.0",
|
|
52
52
|
"@radix-ui/react-accordion": "1.1.2",
|
|
53
53
|
"@radix-ui/react-checkbox": "1.0.4",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"type-fest": "^3.2.0"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@box/storybook-utils": "^0.16.
|
|
80
|
+
"@box/storybook-utils": "^0.16.40",
|
|
81
81
|
"@figma/code-connect": "1.3.12",
|
|
82
82
|
"@types/react": "^18.0.0",
|
|
83
83
|
"@types/react-dom": "^18.0.0",
|