@arcblock/ux 2.8.24 → 2.8.26

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.
@@ -1,6 +1,7 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import Box from '@mui/material/Box';
3
3
  import Container from '@mui/material/Container';
4
+ import { useRef, useState, useEffect } from 'react';
4
5
  import AutoHidden from './auto-hidden';
5
6
  import { styled } from '../Theme';
6
7
 
@@ -24,10 +25,21 @@ function Header({
24
25
  homeLink,
25
26
  ...rest
26
27
  }) {
28
+ const logoRef = useRef();
29
+ const [brandWrapperMinWidth, setBrandWrapperMinWidth] = useState('0px');
30
+ const style = {
31
+ minWidth: brandWrapperMinWidth
32
+ };
33
+ useEffect(() => {
34
+ if (logoRef.current) {
35
+ setBrandWrapperMinWidth(`${logoRef.current.offsetWidth}px`);
36
+ }
37
+ }, []);
27
38
  const renderBrand = () => {
28
39
  const brandContent = /*#__PURE__*/_jsxs(_Fragment, {
29
40
  children: [logo && /*#__PURE__*/_jsx("div", {
30
41
  className: "header-logo",
42
+ ref: logoRef,
31
43
  children: logo
32
44
  }), brand && /*#__PURE__*/_jsx(AutoHidden, {
33
45
  height: 44,
@@ -65,6 +77,7 @@ function Header({
65
77
  className: "header-container",
66
78
  children: [prepend, /*#__PURE__*/_jsx("div", {
67
79
  className: "header-brand-wrapper",
80
+ style: style,
68
81
  children: renderBrand()
69
82
  }), /*#__PURE__*/_jsx("div", {
70
83
  className: "header-brand-addon",
@@ -122,8 +135,7 @@ const Root = styled('div')`
122
135
  }
123
136
 
124
137
  .header-brand-wrapper {
125
- flex-shrink: 1;
126
- min-width: 0;
138
+ flex-shrink: 2;
127
139
  > a {
128
140
  display: flex;
129
141
  align-items: center;
@@ -1,7 +1,9 @@
1
- import { Children, useEffect, createContext, useContext, useMemo, useState, useRef, useCallback } from 'react';
1
+ import { Children, cloneElement, useEffect, createContext, useContext, useMemo, useState, useRef, useCallback, forwardRef } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import clsx from 'clsx';
4
+ import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
4
5
  import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
6
+ import MenuIcon from '@mui/icons-material/Menu';
5
7
  import { HorizontalStyle, InlineStyle } from './style';
6
8
  import { jsx as _jsx } from "react/jsx-runtime";
7
9
  import { jsxs as _jsxs } from "react/jsx-runtime";
@@ -70,6 +72,59 @@ function NavMenu({
70
72
  close
71
73
  };
72
74
  }, [state, mode, activate, open, close]);
75
+ const [hiddenItemCount, setHiddenItemCount] = useState(0);
76
+ const navMenuRef = useRef();
77
+ const itemRefs = useRef([]);
78
+ const moreIconRef = useRef();
79
+ const isAllItemsHidden = hiddenItemCount === itemRefs.current?.length;
80
+ const icon = isAllItemsHidden ? /*#__PURE__*/_jsx(MenuIcon, {}) : /*#__PURE__*/_jsx(MoreHorizIcon, {});
81
+ const style = isAllItemsHidden ? {
82
+ marginLeft: '0px'
83
+ } : undefined;
84
+ const renderChildrenWithRef = childrenElement => {
85
+ return Children.map(childrenElement, (child, index) => {
86
+ return /*#__PURE__*/cloneElement(child, {
87
+ ref: el => {
88
+ itemRefs.current[index] = el;
89
+ }
90
+ });
91
+ });
92
+ };
93
+ const checkItemsFit = () => {
94
+ let totalWidthUsed = 0;
95
+ let newHiddenCount = 0;
96
+ let leftAllHidden = false;
97
+ const containerWidth = navMenuRef.current?.offsetWidth || 0;
98
+ const moreIconWidth = moreIconRef.current ? moreIconRef.current.offsetWidth + parseFloat(window.getComputedStyle(moreIconRef.current).marginLeft) : 0;
99
+ itemRefs.current.forEach((item, index) => {
100
+ if (item) {
101
+ item.style.display = 'flex';
102
+ const marginLeft = index > 0 ? parseFloat(window.getComputedStyle(item).marginLeft) : 0;
103
+ const currentItemWidth = item.offsetWidth + marginLeft;
104
+ if (containerWidth - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
105
+ totalWidthUsed += currentItemWidth;
106
+ } else {
107
+ item.style.display = 'none';
108
+ leftAllHidden = true;
109
+ newHiddenCount++;
110
+ }
111
+ }
112
+ });
113
+ if (newHiddenCount !== hiddenItemCount) {
114
+ setHiddenItemCount(newHiddenCount);
115
+ }
116
+ };
117
+ useEffect(() => {
118
+ if (mode === 'horizontal') {
119
+ checkItemsFit();
120
+ window.addEventListener('resize', checkItemsFit);
121
+ return () => {
122
+ window.removeEventListener('resize', checkItemsFit);
123
+ };
124
+ }
125
+ return undefined;
126
+ // eslint-disable-next-line react-hooks/exhaustive-deps
127
+ }, [mode, hiddenItemCount]);
73
128
  useEffect(() => {
74
129
  // NavMenu#activeId 和 Item#active prop 都可以用来控制激活状态 (一般不会混用这两种方式)
75
130
  // 如果未传入 NavMenu#activeId, 应该避免设置一个空值的 activeId 状态 (会与 Item#active 冲突)
@@ -81,26 +136,32 @@ function NavMenu({
81
136
  }
82
137
  }, [activeId]);
83
138
  const classes = clsx('navmenu', `navmenu--${mode}`, rest.className);
84
- const renderItem = (item, index) => {
85
- if (item.children) {
139
+ const renderItem = (item, index, isTopLevel = false) => {
140
+ if (item?.children) {
141
+ // 对于 Sub 组件,如果它是顶级组件,则包含 ref
86
142
  return /*#__PURE__*/_jsx(Sub, {
87
143
  id: item.id,
88
144
  icon: item.icon,
89
145
  label: item.label,
90
- children: item.children.map(renderItem)
146
+ ref: isTopLevel ? el => {
147
+ itemRefs.current[index] = el;
148
+ } : undefined,
149
+ children: item.children.map((childItem, childIndex) => renderItem(childItem, childIndex, false))
91
150
  }, index);
92
151
  }
93
- return (
94
- /*#__PURE__*/
95
- // eslint-disable-next-line react/no-array-index-key
96
- _jsx(Item, {
97
- id: item.id,
98
- icon: item.icon,
99
- label: item.label,
100
- active: item.active
101
- }, index)
102
- );
152
+
153
+ // 顶级 Item 组件总是包含 ref
154
+ return /*#__PURE__*/_jsx(Item, {
155
+ id: item.id,
156
+ icon: item.icon,
157
+ label: item.label,
158
+ active: item.active,
159
+ ref: isTopLevel ? el => {
160
+ itemRefs.current[index] = el;
161
+ } : undefined
162
+ }, index);
103
163
  };
164
+ const content = items ? items?.slice(-hiddenItemCount).map((item, index) => renderItem(item, index)) : children?.slice(-hiddenItemCount);
104
165
  const StyledRoot = mode === 'inline' ? InlineStyle : HorizontalStyle;
105
166
  return /*#__PURE__*/_jsx(NavMenuContext.Provider, {
106
167
  value: contextValue,
@@ -110,9 +171,17 @@ function NavMenu({
110
171
  $textColor: textColor,
111
172
  $activeTextColor: activeTextColor,
112
173
  $bgColor: bgColor,
113
- children: /*#__PURE__*/_jsx("ul", {
174
+ children: /*#__PURE__*/_jsxs("ul", {
114
175
  className: "navmenu-root",
115
- children: items ? items.map(renderItem) : children
176
+ ref: navMenuRef,
177
+ children: [items ? items.map((item, index) => renderItem(item, index, true)) : renderChildrenWithRef(children), hiddenItemCount > 0 && /*#__PURE__*/_jsx(Sub, {
178
+ expandIcon: false,
179
+ icon: icon,
180
+ label: "",
181
+ ref: moreIconRef,
182
+ style: style,
183
+ children: content
184
+ })]
116
185
  })
117
186
  })
118
187
  });
@@ -143,14 +212,14 @@ NavMenu.defaultProps = {
143
212
  /**
144
213
  * Item
145
214
  */
146
- function Item({
215
+ const Item = /*#__PURE__*/forwardRef(({
147
216
  id: _id,
148
217
  icon,
149
218
  label,
150
219
  active,
151
220
  onClick,
152
221
  ...rest
153
- }) {
222
+ }, ref) => {
154
223
  const id = useUniqueId(_id);
155
224
  const {
156
225
  activeId,
@@ -176,6 +245,7 @@ function Item({
176
245
  ...rest,
177
246
  className: classes,
178
247
  onClick: handleClick,
248
+ ref: ref,
179
249
  children: [icon && /*#__PURE__*/_jsx("span", {
180
250
  className: "navmenu-item-icon",
181
251
  children: icon
@@ -185,7 +255,7 @@ function Item({
185
255
  })]
186
256
  })
187
257
  );
188
- }
258
+ });
189
259
  Item.propTypes = {
190
260
  id: PropTypes.string,
191
261
  icon: PropTypes.element,
@@ -204,14 +274,14 @@ Item.defaultProps = {
204
274
  /**
205
275
  * Sub
206
276
  */
207
- function Sub({
277
+ const Sub = /*#__PURE__*/forwardRef(({
208
278
  id: _id,
209
279
  icon,
210
280
  label,
211
281
  children,
212
282
  expandIcon,
213
283
  ...rest
214
- }) {
284
+ }, ref) => {
215
285
  const id = useUniqueId(_id);
216
286
  const {
217
287
  openedIds,
@@ -239,6 +309,7 @@ function Sub({
239
309
  ...rest,
240
310
  className: classes,
241
311
  ...props,
312
+ ref: ref,
242
313
  children: [icon && /*#__PURE__*/_jsx("span", {
243
314
  className: "navmenu-sub-icon",
244
315
  children: icon
@@ -259,7 +330,7 @@ function Sub({
259
330
  })
260
331
  })]
261
332
  });
262
- }
333
+ });
263
334
  Sub.propTypes = {
264
335
  id: PropTypes.string,
265
336
  icon: PropTypes.element,
@@ -78,6 +78,9 @@ const NavMenuBase = styled('nav')`
78
78
  `;
79
79
  export const HorizontalStyle = styled(NavMenuBase)`
80
80
  padding: 8px 16px;
81
+ min-width: 50px;
82
+ flex-grow: 1;
83
+
81
84
  .navmenu-root {
82
85
  display: flex;
83
86
  align-items: center;
@@ -86,6 +89,7 @@ export const HorizontalStyle = styled(NavMenuBase)`
86
89
  .navmenu-root > .navmenu-item,
87
90
  .navmenu-root > .navmenu-sub {
88
91
  margin-left: 24px;
92
+ white-space: nowrap;
89
93
  }
90
94
  .navmenu-root > .navmenu-item:first-of-type,
91
95
  .navmenu-root > .navmenu-sub:first-of-type {
@@ -109,6 +113,7 @@ export const HorizontalStyle = styled(NavMenuBase)`
109
113
  }
110
114
  /* 二级 sub menu */
111
115
  .navmenu-root > .navmenu-sub {
116
+ white-space: nowrap;
112
117
  > .navmenu-sub-container {
113
118
  left: 50%;
114
119
  transform: translateX(-50%);
@@ -7,6 +7,7 @@ exports.default = void 0;
7
7
  var _propTypes = _interopRequireDefault(require("prop-types"));
8
8
  var _Box = _interopRequireDefault(require("@mui/material/Box"));
9
9
  var _Container = _interopRequireDefault(require("@mui/material/Container"));
10
+ var _react = require("react");
10
11
  var _autoHidden = _interopRequireDefault(require("./auto-hidden"));
11
12
  var _Theme = require("../Theme");
12
13
  var _jsxRuntime = require("react/jsx-runtime");
@@ -39,10 +40,21 @@ function Header(_ref) {
39
40
  homeLink
40
41
  } = _ref,
41
42
  rest = _objectWithoutProperties(_ref, _excluded);
43
+ const logoRef = (0, _react.useRef)();
44
+ const [brandWrapperMinWidth, setBrandWrapperMinWidth] = (0, _react.useState)('0px');
45
+ const style = {
46
+ minWidth: brandWrapperMinWidth
47
+ };
48
+ (0, _react.useEffect)(() => {
49
+ if (logoRef.current) {
50
+ setBrandWrapperMinWidth("".concat(logoRef.current.offsetWidth, "px"));
51
+ }
52
+ }, []);
42
53
  const renderBrand = () => {
43
54
  const brandContent = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
44
55
  children: [logo && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
45
56
  className: "header-logo",
57
+ ref: logoRef,
46
58
  children: logo
47
59
  }), brand && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoHidden.default, {
48
60
  height: 44,
@@ -79,6 +91,7 @@ function Header(_ref) {
79
91
  className: "header-container",
80
92
  children: [prepend, /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
81
93
  className: "header-brand-wrapper",
94
+ style: style,
82
95
  children: renderBrand()
83
96
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
84
97
  className: "header-brand-addon",
@@ -124,6 +137,6 @@ Header.defaultProps = {
124
137
  maxWidth: undefined,
125
138
  homeLink: '/'
126
139
  };
127
- const Root = (0, _Theme.styled)('div')(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n position: relative;\n z-index: ", ";\n font-size: 14px;\n background: ", ";\n .header-container {\n display: flex;\n align-items: center;\n height: 64px;\n }\n\n .header-brand-wrapper {\n flex-shrink: 1;\n min-width: 0;\n > a {\n display: flex;\n align-items: center;\n height: 100%;\n line-height: 1;\n color: inherit;\n text-decoration: none;\n }\n }\n .header-brand-wrapper .header-logo {\n display: inline-flex;\n position: relative;\n height: 44px;\n margin-right: 16px;\n img,\n svg {\n width: auto;\n height: 100%;\n max-height: 100%;\n }\n }\n .header-brand {\n display: flex;\n flex-direction: column;\n justify-content: center;\n height: 44px;\n margin-right: 16px;\n line-height: 1;\n a {\n color: inherit;\n text-decoration: none;\n }\n .header-brand-title {\n display: flex;\n align-items: center;\n h2 {\n margin: 0;\n font-size: 16px;\n }\n }\n .header-brand-desc {\n margin-top: 4px;\n color: #9397a1;\n }\n }\n .header-brand-addon {\n margin-right: 16px;\n }\n .header-addons {\n display: flex;\n align-items: center;\n }\n ", " {\n .header-brand {\n margin-right: 12px;\n .header-brand-title {\n h2 {\n font-size: 14px;\n }\n }\n }\n .header-brand-addon {\n display: none;\n }\n }\n ", " {\n .header-menu {\n display: inline-block;\n }\n .header-logo {\n height: 32px;\n }\n .header-brand {\n .header-brand-title {\n h2 {\n font-size: 13px;\n }\n }\n .header-brand-desc {\n font-size: 12px;\n }\n }\n }\n"])), props => props.theme.zIndex.appBar, props => props.theme.palette.common.white, props => props.theme.breakpoints.down('lg'), props => props.theme.breakpoints.down('md'));
140
+ const Root = (0, _Theme.styled)('div')(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n position: relative;\n z-index: ", ";\n font-size: 14px;\n background: ", ";\n .header-container {\n display: flex;\n align-items: center;\n height: 64px;\n }\n\n .header-brand-wrapper {\n flex-shrink: 2;\n > a {\n display: flex;\n align-items: center;\n height: 100%;\n line-height: 1;\n color: inherit;\n text-decoration: none;\n }\n }\n .header-brand-wrapper .header-logo {\n display: inline-flex;\n position: relative;\n height: 44px;\n margin-right: 16px;\n img,\n svg {\n width: auto;\n height: 100%;\n max-height: 100%;\n }\n }\n .header-brand {\n display: flex;\n flex-direction: column;\n justify-content: center;\n height: 44px;\n margin-right: 16px;\n line-height: 1;\n a {\n color: inherit;\n text-decoration: none;\n }\n .header-brand-title {\n display: flex;\n align-items: center;\n h2 {\n margin: 0;\n font-size: 16px;\n }\n }\n .header-brand-desc {\n margin-top: 4px;\n color: #9397a1;\n }\n }\n .header-brand-addon {\n margin-right: 16px;\n }\n .header-addons {\n display: flex;\n align-items: center;\n }\n ", " {\n .header-brand {\n margin-right: 12px;\n .header-brand-title {\n h2 {\n font-size: 14px;\n }\n }\n }\n .header-brand-addon {\n display: none;\n }\n }\n ", " {\n .header-menu {\n display: inline-block;\n }\n .header-logo {\n height: 32px;\n }\n .header-brand {\n .header-brand-title {\n h2 {\n font-size: 13px;\n }\n }\n .header-brand-desc {\n font-size: 12px;\n }\n }\n }\n"])), props => props.theme.zIndex.appBar, props => props.theme.palette.common.white, props => props.theme.breakpoints.down('lg'), props => props.theme.breakpoints.down('md'));
128
141
  var _default = Header;
129
142
  exports.default = _default;
@@ -7,7 +7,9 @@ exports.default = void 0;
7
7
  var _react = require("react");
8
8
  var _propTypes = _interopRequireDefault(require("prop-types"));
9
9
  var _clsx = _interopRequireDefault(require("clsx"));
10
+ var _MoreHoriz = _interopRequireDefault(require("@mui/icons-material/MoreHoriz"));
10
11
  var _ExpandMore = _interopRequireDefault(require("@mui/icons-material/ExpandMore"));
12
+ var _Menu = _interopRequireDefault(require("@mui/icons-material/Menu"));
11
13
  var _style = require("./style");
12
14
  var _jsxRuntime = require("react/jsx-runtime");
13
15
  const _excluded = ["items", "mode", "children", "activeId", "textColor", "activeTextColor", "bgColor", "onSelected"],
@@ -39,7 +41,7 @@ function useUniqueId(id) {
39
41
  * NavMenu, 导航组件, 可用于 header/footer/sidebar
40
42
  */
41
43
  function NavMenu(_ref) {
42
- var _children;
44
+ var _children, _itemRefs$current, _children2;
43
45
  let {
44
46
  items,
45
47
  mode,
@@ -84,6 +86,60 @@ function NavMenu(_ref) {
84
86
  close
85
87
  });
86
88
  }, [state, mode, activate, open, close]);
89
+ const [hiddenItemCount, setHiddenItemCount] = (0, _react.useState)(0);
90
+ const navMenuRef = (0, _react.useRef)();
91
+ const itemRefs = (0, _react.useRef)([]);
92
+ const moreIconRef = (0, _react.useRef)();
93
+ const isAllItemsHidden = hiddenItemCount === ((_itemRefs$current = itemRefs.current) === null || _itemRefs$current === void 0 ? void 0 : _itemRefs$current.length);
94
+ const icon = isAllItemsHidden ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_Menu.default, {}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_MoreHoriz.default, {});
95
+ const style = isAllItemsHidden ? {
96
+ marginLeft: '0px'
97
+ } : undefined;
98
+ const renderChildrenWithRef = childrenElement => {
99
+ return _react.Children.map(childrenElement, (child, index) => {
100
+ return /*#__PURE__*/(0, _react.cloneElement)(child, {
101
+ ref: el => {
102
+ itemRefs.current[index] = el;
103
+ }
104
+ });
105
+ });
106
+ };
107
+ const checkItemsFit = () => {
108
+ var _navMenuRef$current;
109
+ let totalWidthUsed = 0;
110
+ let newHiddenCount = 0;
111
+ let leftAllHidden = false;
112
+ const containerWidth = ((_navMenuRef$current = navMenuRef.current) === null || _navMenuRef$current === void 0 ? void 0 : _navMenuRef$current.offsetWidth) || 0;
113
+ const moreIconWidth = moreIconRef.current ? moreIconRef.current.offsetWidth + parseFloat(window.getComputedStyle(moreIconRef.current).marginLeft) : 0;
114
+ itemRefs.current.forEach((item, index) => {
115
+ if (item) {
116
+ item.style.display = 'flex';
117
+ const marginLeft = index > 0 ? parseFloat(window.getComputedStyle(item).marginLeft) : 0;
118
+ const currentItemWidth = item.offsetWidth + marginLeft;
119
+ if (containerWidth - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
120
+ totalWidthUsed += currentItemWidth;
121
+ } else {
122
+ item.style.display = 'none';
123
+ leftAllHidden = true;
124
+ newHiddenCount++;
125
+ }
126
+ }
127
+ });
128
+ if (newHiddenCount !== hiddenItemCount) {
129
+ setHiddenItemCount(newHiddenCount);
130
+ }
131
+ };
132
+ (0, _react.useEffect)(() => {
133
+ if (mode === 'horizontal') {
134
+ checkItemsFit();
135
+ window.addEventListener('resize', checkItemsFit);
136
+ return () => {
137
+ window.removeEventListener('resize', checkItemsFit);
138
+ };
139
+ }
140
+ return undefined;
141
+ // eslint-disable-next-line react-hooks/exhaustive-deps
142
+ }, [mode, hiddenItemCount]);
87
143
  (0, _react.useEffect)(() => {
88
144
  // NavMenu#activeId 和 Item#active prop 都可以用来控制激活状态 (一般不会混用这两种方式)
89
145
  // 如果未传入 NavMenu#activeId, 应该避免设置一个空值的 activeId 状态 (会与 Item#active 冲突)
@@ -94,26 +150,33 @@ function NavMenu(_ref) {
94
150
  }
95
151
  }, [activeId]);
96
152
  const classes = (0, _clsx.default)('navmenu', "navmenu--".concat(mode), rest.className);
97
- const renderItem = (item, index) => {
98
- if (item.children) {
153
+ const renderItem = function renderItem(item, index) {
154
+ let isTopLevel = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
155
+ if (item !== null && item !== void 0 && item.children) {
156
+ // 对于 Sub 组件,如果它是顶级组件,则包含 ref
99
157
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(Sub, {
100
158
  id: item.id,
101
159
  icon: item.icon,
102
160
  label: item.label,
103
- children: item.children.map(renderItem)
161
+ ref: isTopLevel ? el => {
162
+ itemRefs.current[index] = el;
163
+ } : undefined,
164
+ children: item.children.map((childItem, childIndex) => renderItem(childItem, childIndex, false))
104
165
  }, index);
105
166
  }
106
- return (
107
- /*#__PURE__*/
108
- // eslint-disable-next-line react/no-array-index-key
109
- (0, _jsxRuntime.jsx)(Item, {
110
- id: item.id,
111
- icon: item.icon,
112
- label: item.label,
113
- active: item.active
114
- }, index)
115
- );
167
+
168
+ // 顶级 Item 组件总是包含 ref
169
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(Item, {
170
+ id: item.id,
171
+ icon: item.icon,
172
+ label: item.label,
173
+ active: item.active,
174
+ ref: isTopLevel ? el => {
175
+ itemRefs.current[index] = el;
176
+ } : undefined
177
+ }, index);
116
178
  };
179
+ const content = items ? items === null || items === void 0 ? void 0 : items.slice(-hiddenItemCount).map((item, index) => renderItem(item, index)) : (_children2 = children) === null || _children2 === void 0 ? void 0 : _children2.slice(-hiddenItemCount);
117
180
  const StyledRoot = mode === 'inline' ? _style.InlineStyle : _style.HorizontalStyle;
118
181
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(NavMenuContext.Provider, {
119
182
  value: contextValue,
@@ -122,9 +185,17 @@ function NavMenu(_ref) {
122
185
  $textColor: textColor,
123
186
  $activeTextColor: activeTextColor,
124
187
  $bgColor: bgColor,
125
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)("ul", {
188
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("ul", {
126
189
  className: "navmenu-root",
127
- children: items ? items.map(renderItem) : children
190
+ ref: navMenuRef,
191
+ children: [items ? items.map((item, index) => renderItem(item, index, true)) : renderChildrenWithRef(children), hiddenItemCount > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(Sub, {
192
+ expandIcon: false,
193
+ icon: icon,
194
+ label: "",
195
+ ref: moreIconRef,
196
+ style: style,
197
+ children: content
198
+ })]
128
199
  })
129
200
  }))
130
201
  });
@@ -155,7 +226,7 @@ NavMenu.defaultProps = {
155
226
  /**
156
227
  * Item
157
228
  */
158
- function Item(_ref2) {
229
+ const Item = /*#__PURE__*/(0, _react.forwardRef)((_ref2, ref) => {
159
230
  let {
160
231
  id: _id,
161
232
  icon,
@@ -188,6 +259,7 @@ function Item(_ref2) {
188
259
  (0, _jsxRuntime.jsxs)("li", _objectSpread(_objectSpread({}, rest), {}, {
189
260
  className: classes,
190
261
  onClick: handleClick,
262
+ ref: ref,
191
263
  children: [icon && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
192
264
  className: "navmenu-item-icon",
193
265
  children: icon
@@ -197,7 +269,7 @@ function Item(_ref2) {
197
269
  })]
198
270
  }))
199
271
  );
200
- }
272
+ });
201
273
  Item.propTypes = {
202
274
  id: _propTypes.default.string,
203
275
  icon: _propTypes.default.element,
@@ -216,7 +288,7 @@ Item.defaultProps = {
216
288
  /**
217
289
  * Sub
218
290
  */
219
- function Sub(_ref3) {
291
+ const Sub = /*#__PURE__*/(0, _react.forwardRef)((_ref3, ref) => {
220
292
  let {
221
293
  id: _id,
222
294
  icon,
@@ -251,6 +323,7 @@ function Sub(_ref3) {
251
323
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)("li", _objectSpread(_objectSpread(_objectSpread({}, rest), {}, {
252
324
  className: classes
253
325
  }, props), {}, {
326
+ ref: ref,
254
327
  children: [icon && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
255
328
  className: "navmenu-sub-icon",
256
329
  children: icon
@@ -271,7 +344,7 @@ function Sub(_ref3) {
271
344
  })
272
345
  }))]
273
346
  }));
274
- }
347
+ });
275
348
  Sub.propTypes = {
276
349
  id: _propTypes.default.string,
277
350
  icon: _propTypes.default.element,
@@ -8,7 +8,7 @@ var _Theme = require("../Theme");
8
8
  var _templateObject, _templateObject2, _templateObject3;
9
9
  function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
10
10
  const NavMenuBase = (0, _Theme.styled)('nav')(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n background-color: ", ";\n font-size: 16px;\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n .navmenu-item,\n .navmenu-sub {\n display: flex;\n align-items: center;\n }\n a {\n color: inherit;\n text-decoration: none;\n }\n /* active/hover */\n .navmenu-item,\n .navmenu-sub {\n color: ", ";\n }\n .navmenu-item--active,\n .navmenu-item:hover,\n .navmenu-sub--opened {\n color: ", ";\n }\n\n .navmenu-item {\n position: relative;\n cursor: pointer;\n transition: color 0.2s ease-in-out;\n a {\n white-space: nowrap;\n }\n a::before {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n background-color: transparent;\n content: '';\n }\n }\n\n .navmenu-sub {\n position: relative;\n cursor: pointer;\n }\n /* icon & expand icon */\n .navmenu-item-icon,\n .navmenu-sub-icon,\n .navmenu-sub-expand-icon {\n display: flex;\n line-height: 1;\n }\n .navmenu-item-icon,\n .navmenu-sub-icon {\n margin-right: 4px;\n }\n .navmenu-item-icon > *,\n .navmenu-sub-icon > * {\n width: auto;\n height: 22px;\n max-height: 22px;\n font-size: 1.5em;\n }\n .navmenu-sub-expand-icon {\n margin-left: 8px;\n > * {\n width: 0.8em;\n height: 0.8em;\n transition: transform 0.2s ease-in-out;\n }\n }\n"])), props => props.$bgColor, props => props.$textColor, props => props.$activeTextColor);
11
- const HorizontalStyle = (0, _Theme.styled)(NavMenuBase)(_templateObject2 || (_templateObject2 = _taggedTemplateLiteral(["\n padding: 8px 16px;\n .navmenu-root {\n display: flex;\n align-items: center;\n }\n /* \u9876\u7EA7\u83DC\u5355\u95F4\u9694 */\n .navmenu-root > .navmenu-item,\n .navmenu-root > .navmenu-sub {\n margin-left: 24px;\n }\n .navmenu-root > .navmenu-item:first-of-type,\n .navmenu-root > .navmenu-sub:first-of-type {\n margin-left: 0;\n }\n\n /* \u5B50\u7EA7\u5217\u8868 */\n .navmenu-sub-container {\n display: none;\n position: absolute;\n top: 100%;\n }\n .navmenu-sub-list {\n padding: 16px;\n border-radius: 4px;\n background: #fff;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n .navmenu-item + .navmenu-item {\n margin-top: 8px;\n }\n }\n /* \u4E8C\u7EA7 sub menu */\n .navmenu-root > .navmenu-sub {\n > .navmenu-sub-container {\n left: 50%;\n transform: translateX(-50%);\n padding-top: 16px;\n }\n &.navmenu-sub--opened > .navmenu-sub-container {\n display: block;\n }\n }\n"])));
11
+ const HorizontalStyle = (0, _Theme.styled)(NavMenuBase)(_templateObject2 || (_templateObject2 = _taggedTemplateLiteral(["\n padding: 8px 16px;\n min-width: 50px;\n flex-grow: 1;\n\n .navmenu-root {\n display: flex;\n align-items: center;\n }\n /* \u9876\u7EA7\u83DC\u5355\u95F4\u9694 */\n .navmenu-root > .navmenu-item,\n .navmenu-root > .navmenu-sub {\n margin-left: 24px;\n white-space: nowrap;\n }\n .navmenu-root > .navmenu-item:first-of-type,\n .navmenu-root > .navmenu-sub:first-of-type {\n margin-left: 0;\n }\n\n /* \u5B50\u7EA7\u5217\u8868 */\n .navmenu-sub-container {\n display: none;\n position: absolute;\n top: 100%;\n }\n .navmenu-sub-list {\n padding: 16px;\n border-radius: 4px;\n background: #fff;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n .navmenu-item + .navmenu-item {\n margin-top: 8px;\n }\n }\n /* \u4E8C\u7EA7 sub menu */\n .navmenu-root > .navmenu-sub {\n white-space: nowrap;\n > .navmenu-sub-container {\n left: 50%;\n transform: translateX(-50%);\n padding-top: 16px;\n }\n &.navmenu-sub--opened > .navmenu-sub-container {\n display: block;\n }\n }\n"])));
12
12
 
13
13
  /* inline mode */
14
14
  exports.HorizontalStyle = HorizontalStyle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.8.24",
3
+ "version": "2.8.26",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -324,11 +324,11 @@
324
324
  "peerDependencies": {
325
325
  "react": ">=18.1.0"
326
326
  },
327
- "gitHead": "833bf851689c10567615f93fdfe934ec4fd7a492",
327
+ "gitHead": "05d8234199a6770e3a26a8d11b72d0956ff24f4a",
328
328
  "dependencies": {
329
329
  "@arcblock/did-motif": "^1.1.13",
330
- "@arcblock/icons": "^2.8.24",
331
- "@arcblock/react-hooks": "^2.8.24",
330
+ "@arcblock/icons": "^2.8.26",
331
+ "@arcblock/react-hooks": "^2.8.26",
332
332
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
333
333
  "@emotion/react": "^11.10.4",
334
334
  "@emotion/styled": "^11.10.4",
@@ -1,6 +1,7 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import Box from '@mui/material/Box';
3
3
  import Container from '@mui/material/Container';
4
+ import { useRef, useState, useEffect } from 'react';
4
5
  import AutoHidden from './auto-hidden';
5
6
  import { styled } from '../Theme';
6
7
 
@@ -21,10 +22,24 @@ function Header({
21
22
  homeLink,
22
23
  ...rest
23
24
  }) {
25
+ const logoRef = useRef();
26
+ const [brandWrapperMinWidth, setBrandWrapperMinWidth] = useState('0px');
27
+ const style = { minWidth: brandWrapperMinWidth };
28
+
29
+ useEffect(() => {
30
+ if (logoRef.current) {
31
+ setBrandWrapperMinWidth(`${logoRef.current.offsetWidth}px`);
32
+ }
33
+ }, []);
34
+
24
35
  const renderBrand = () => {
25
36
  const brandContent = (
26
37
  <>
27
- {logo && <div className="header-logo">{logo}</div>}
38
+ {logo && (
39
+ <div className="header-logo" ref={logoRef}>
40
+ {logo}
41
+ </div>
42
+ )}
28
43
  {brand && (
29
44
  <AutoHidden height={44} sx={{ flexShrink: { xs: 1 } }}>
30
45
  <div className="header-brand">
@@ -46,7 +61,9 @@ function Header({
46
61
  <Root {...rest}>
47
62
  <Container maxWidth={maxWidth} className="header-container">
48
63
  {prepend}
49
- <div className="header-brand-wrapper">{renderBrand()}</div>
64
+ <div className="header-brand-wrapper" style={style}>
65
+ {renderBrand()}
66
+ </div>
50
67
  <div className="header-brand-addon">{brandAddon}</div>
51
68
  {align === 'right' && <Box flexGrow={1} />}
52
69
  {children}
@@ -101,8 +118,7 @@ const Root = styled('div')`
101
118
  }
102
119
 
103
120
  .header-brand-wrapper {
104
- flex-shrink: 1;
105
- min-width: 0;
121
+ flex-shrink: 2;
106
122
  > a {
107
123
  display: flex;
108
124
  align-items: center;
@@ -1,7 +1,20 @@
1
- import { Children, useEffect, createContext, useContext, useMemo, useState, useRef, useCallback } from 'react';
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ useEffect,
5
+ createContext,
6
+ useContext,
7
+ useMemo,
8
+ useState,
9
+ useRef,
10
+ useCallback,
11
+ forwardRef,
12
+ } from 'react';
2
13
  import PropTypes from 'prop-types';
3
14
  import clsx from 'clsx';
15
+ import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
4
16
  import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
17
+ import MenuIcon from '@mui/icons-material/Menu';
5
18
  import { HorizontalStyle, InlineStyle } from './style';
6
19
 
7
20
  const NavMenuContext = createContext();
@@ -52,6 +65,65 @@ function NavMenu({ items, mode, children, activeId, textColor, activeTextColor,
52
65
  close,
53
66
  };
54
67
  }, [state, mode, activate, open, close]);
68
+ const [hiddenItemCount, setHiddenItemCount] = useState(0);
69
+ const navMenuRef = useRef();
70
+ const itemRefs = useRef([]);
71
+ const moreIconRef = useRef();
72
+ const isAllItemsHidden = hiddenItemCount === itemRefs.current?.length;
73
+ const icon = isAllItemsHidden ? <MenuIcon /> : <MoreHorizIcon />;
74
+ const style = isAllItemsHidden ? { marginLeft: '0px' } : undefined;
75
+
76
+ const renderChildrenWithRef = (childrenElement) => {
77
+ return Children.map(childrenElement, (child, index) => {
78
+ return cloneElement(child, {
79
+ ref: (el) => {
80
+ itemRefs.current[index] = el;
81
+ },
82
+ });
83
+ });
84
+ };
85
+ const checkItemsFit = () => {
86
+ let totalWidthUsed = 0;
87
+ let newHiddenCount = 0;
88
+ let leftAllHidden = false;
89
+ const containerWidth = navMenuRef.current?.offsetWidth || 0;
90
+ const moreIconWidth = moreIconRef.current
91
+ ? moreIconRef.current.offsetWidth + parseFloat(window.getComputedStyle(moreIconRef.current).marginLeft)
92
+ : 0;
93
+
94
+ itemRefs.current.forEach((item, index) => {
95
+ if (item) {
96
+ item.style.display = 'flex';
97
+ const marginLeft = index > 0 ? parseFloat(window.getComputedStyle(item).marginLeft) : 0;
98
+ const currentItemWidth = item.offsetWidth + marginLeft;
99
+
100
+ if (containerWidth - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
101
+ totalWidthUsed += currentItemWidth;
102
+ } else {
103
+ item.style.display = 'none';
104
+ leftAllHidden = true;
105
+ newHiddenCount++;
106
+ }
107
+ }
108
+ });
109
+
110
+ if (newHiddenCount !== hiddenItemCount) {
111
+ setHiddenItemCount(newHiddenCount);
112
+ }
113
+ };
114
+
115
+ useEffect(() => {
116
+ if (mode === 'horizontal') {
117
+ checkItemsFit();
118
+ window.addEventListener('resize', checkItemsFit);
119
+
120
+ return () => {
121
+ window.removeEventListener('resize', checkItemsFit);
122
+ };
123
+ }
124
+ return undefined;
125
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
+ }, [mode, hiddenItemCount]);
55
127
 
56
128
  useEffect(() => {
57
129
  // NavMenu#activeId 和 Item#active prop 都可以用来控制激活状态 (一般不会混用这两种方式)
@@ -62,20 +134,53 @@ function NavMenu({ items, mode, children, activeId, textColor, activeTextColor,
62
134
  }, [activeId]);
63
135
 
64
136
  const classes = clsx('navmenu', `navmenu--${mode}`, rest.className);
65
- const renderItem = (item, index) => {
66
- if (item.children) {
137
+
138
+ const renderItem = (item, index, isTopLevel = false) => {
139
+ if (item?.children) {
140
+ // 对于 Sub 组件,如果它是顶级组件,则包含 ref
67
141
  return (
68
- <Sub key={index} id={item.id} icon={item.icon} label={item.label}>
69
- {item.children.map(renderItem)}
142
+ <Sub
143
+ key={index}
144
+ id={item.id}
145
+ icon={item.icon}
146
+ label={item.label}
147
+ ref={
148
+ isTopLevel
149
+ ? (el) => {
150
+ itemRefs.current[index] = el;
151
+ }
152
+ : undefined
153
+ }>
154
+ {item.children.map((childItem, childIndex) => renderItem(childItem, childIndex, false))}
70
155
  </Sub>
71
156
  );
72
157
  }
158
+
159
+ // 顶级 Item 组件总是包含 ref
73
160
  return (
74
- // eslint-disable-next-line react/no-array-index-key
75
- <Item key={index} id={item.id} icon={item.icon} label={item.label} active={item.active} />
161
+ <Item
162
+ key={index}
163
+ id={item.id}
164
+ icon={item.icon}
165
+ label={item.label}
166
+ active={item.active}
167
+ ref={
168
+ isTopLevel
169
+ ? (el) => {
170
+ itemRefs.current[index] = el;
171
+ }
172
+ : undefined
173
+ }
174
+ />
76
175
  );
77
176
  };
177
+
178
+ const content = items
179
+ ? items?.slice(-hiddenItemCount).map((item, index) => renderItem(item, index))
180
+ : children?.slice(-hiddenItemCount);
181
+
78
182
  const StyledRoot = mode === 'inline' ? InlineStyle : HorizontalStyle;
183
+
79
184
  return (
80
185
  <NavMenuContext.Provider value={contextValue}>
81
186
  <StyledRoot
@@ -84,7 +189,14 @@ function NavMenu({ items, mode, children, activeId, textColor, activeTextColor,
84
189
  $textColor={textColor}
85
190
  $activeTextColor={activeTextColor}
86
191
  $bgColor={bgColor}>
87
- <ul className="navmenu-root">{items ? items.map(renderItem) : children}</ul>
192
+ <ul className="navmenu-root" ref={navMenuRef}>
193
+ {items ? items.map((item, index) => renderItem(item, index, true)) : renderChildrenWithRef(children)}
194
+ {hiddenItemCount > 0 && (
195
+ <Sub expandIcon={false} icon={icon} label="" ref={moreIconRef} style={style}>
196
+ {content}
197
+ </Sub>
198
+ )}
199
+ </ul>
88
200
  </StyledRoot>
89
201
  </NavMenuContext.Provider>
90
202
  );
@@ -116,28 +228,31 @@ NavMenu.defaultProps = {
116
228
  /**
117
229
  * Item
118
230
  */
119
- function Item({ id: _id, icon, label, active, onClick, ...rest }) {
231
+ const Item = forwardRef(({ id: _id, icon, label, active, onClick, ...rest }, ref) => {
120
232
  const id = useUniqueId(_id);
121
233
  const { activeId, activate } = useContext(NavMenuContext);
122
234
  const classes = clsx('navmenu-item', { 'navmenu-item--active': activeId === id }, rest.className);
235
+
123
236
  useEffect(() => {
124
237
  if (active) {
125
238
  activate(id);
126
239
  }
127
240
  // eslint-disable-next-line react-hooks/exhaustive-deps
128
241
  }, [active]);
242
+
129
243
  const handleClick = () => {
130
244
  onClick?.();
131
245
  activate(id);
132
246
  };
247
+
133
248
  return (
134
249
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
135
- <li {...rest} className={classes} onClick={handleClick}>
250
+ <li {...rest} className={classes} onClick={handleClick} ref={ref}>
136
251
  {icon && <span className="navmenu-item-icon">{icon}</span>}
137
252
  <span className="navmenu-item-label">{label}</span>
138
253
  </li>
139
254
  );
140
- }
255
+ });
141
256
 
142
257
  Item.propTypes = {
143
258
  id: PropTypes.string,
@@ -158,7 +273,7 @@ Item.defaultProps = {
158
273
  /**
159
274
  * Sub
160
275
  */
161
- function Sub({ id: _id, icon, label, children, expandIcon, ...rest }) {
276
+ const Sub = forwardRef(({ id: _id, icon, label, children, expandIcon, ...rest }, ref) => {
162
277
  const id = useUniqueId(_id);
163
278
  const { openedIds, open, close, mode } = useContext(NavMenuContext);
164
279
  const isOpen = openedIds.includes(id);
@@ -177,8 +292,9 @@ function Sub({ id: _id, icon, label, children, expandIcon, ...rest }) {
177
292
  onClick: (e) => e.stopPropagation(),
178
293
  }
179
294
  : {};
295
+
180
296
  return (
181
- <li {...rest} className={classes} {...props}>
297
+ <li {...rest} className={classes} {...props} ref={ref}>
182
298
  {icon && <span className="navmenu-sub-icon">{icon}</span>}
183
299
  <span className="navmenu-sub-label">{label}</span>
184
300
  {expandIcon && <span className="navmenu-sub-expand-icon">{expandIcon?.({ isOpen }) || expandIcon}</span>}
@@ -187,7 +303,7 @@ function Sub({ id: _id, icon, label, children, expandIcon, ...rest }) {
187
303
  </div>
188
304
  </li>
189
305
  );
190
- }
306
+ });
191
307
 
192
308
  Sub.propTypes = {
193
309
  id: PropTypes.string,
@@ -80,6 +80,9 @@ const NavMenuBase = styled('nav')`
80
80
 
81
81
  export const HorizontalStyle = styled(NavMenuBase)`
82
82
  padding: 8px 16px;
83
+ min-width: 50px;
84
+ flex-grow: 1;
85
+
83
86
  .navmenu-root {
84
87
  display: flex;
85
88
  align-items: center;
@@ -88,6 +91,7 @@ export const HorizontalStyle = styled(NavMenuBase)`
88
91
  .navmenu-root > .navmenu-item,
89
92
  .navmenu-root > .navmenu-sub {
90
93
  margin-left: 24px;
94
+ white-space: nowrap;
91
95
  }
92
96
  .navmenu-root > .navmenu-item:first-of-type,
93
97
  .navmenu-root > .navmenu-sub:first-of-type {
@@ -111,6 +115,7 @@ export const HorizontalStyle = styled(NavMenuBase)`
111
115
  }
112
116
  /* 二级 sub menu */
113
117
  .navmenu-root > .navmenu-sub {
118
+ white-space: nowrap;
114
119
  > .navmenu-sub-container {
115
120
  left: 50%;
116
121
  transform: translateX(-50%);