@arcblock/ux 2.8.23 → 2.8.25

Sign up to get free protection for your applications and to get access to all the features.
@@ -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;
@@ -175,6 +187,7 @@ const Root = styled('div')`
175
187
  .header-addons {
176
188
  display: flex;
177
189
  align-items: center;
190
+ min-width: 150px;
178
191
  }
179
192
  ${props => props.theme.breakpoints.down('lg')} {
180
193
  .header-brand {
@@ -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 min-width: 150px;\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.23",
3
+ "version": "2.8.25",
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": "f119af381de46921d0bddcaef6f6326a1c31b958",
327
+ "gitHead": "130b4405cfaf4ed6ecb05006abe50d2611595141",
328
328
  "dependencies": {
329
329
  "@arcblock/did-motif": "^1.1.13",
330
- "@arcblock/icons": "^2.8.23",
331
- "@arcblock/react-hooks": "^2.8.23",
330
+ "@arcblock/icons": "^2.8.25",
331
+ "@arcblock/react-hooks": "^2.8.25",
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;
@@ -154,6 +170,7 @@ const Root = styled('div')`
154
170
  .header-addons {
155
171
  display: flex;
156
172
  align-items: center;
173
+ min-width: 150px;
157
174
  }
158
175
  ${(props) => props.theme.breakpoints.down('lg')} {
159
176
  .header-brand {
@@ -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%);