@arcblock/ux 2.12.3 → 2.12.4

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.
@@ -38,7 +38,7 @@ export interface ItemProps extends React.HTMLAttributes<HTMLLIElement> {
38
38
  }
39
39
  export declare const Item: import("react").ForwardRefExoticComponent<ItemProps & import("react").RefAttributes<HTMLLIElement>>;
40
40
  export interface SubProps extends Omit<ItemProps, 'children' | 'active'> {
41
- children?: Array<React.ReactElement> | ((props: {
41
+ children?: Array<React.ReactElement | null> | ((props: {
42
42
  isOpen: boolean;
43
43
  }) => React.ReactElement | null);
44
44
  expandIcon?: React.ReactNode | ((props: {
@@ -5,7 +5,8 @@ import { MoreHoriz as MoreHorizIcon, ExpandMore as ExpandMoreIcon, Menu as MenuI
5
5
  import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
6
6
  import { useCreation, useMemoizedFn, useReactive, useSize, useThrottleFn } from 'ahooks';
7
7
  import { NavMenuProvider, useNavMenuContext } from './nav-menu-context';
8
- import { NavMenuRoot, NavMenuList, NavMenuItem, NavMenuSub, NavMenuSubList } from './style';
8
+ import { NavMenuRoot, NavMenuItem, NavMenuSub, NavMenuSubList, NavMenuStyled } from './style';
9
+ import { SubContainer } from './sub-container';
9
10
 
10
11
  // 过滤 children 中的 Item/Sub, 忽略其它类型的 element
11
12
  function filterItems(children) {
@@ -73,6 +74,7 @@ function NavMenu({
73
74
  const navMenuRef = useRef(null);
74
75
  const itemRefs = useRef([]);
75
76
  const moreIconRef = useRef(null);
77
+ const containerWidth = useRef(0);
76
78
  const isAllItemsHidden = currentState.hiddenItemCount === itemRefs.current?.length;
77
79
  const style = isAllItemsHidden ? {
78
80
  marginLeft: '0px'
@@ -96,14 +98,13 @@ function NavMenu({
96
98
  let totalWidthUsed = 0;
97
99
  let newHiddenCount = 0;
98
100
  let leftAllHidden = false;
99
- const containerWidth = containerSize?.width || 0;
100
101
  const moreIconWidth = moreIconRef.current ? moreIconRef.current.offsetWidth + parseFloat(window.getComputedStyle(moreIconRef.current).marginLeft) : 0;
101
102
  itemRefs.current.forEach((item, index) => {
102
103
  if (item) {
103
104
  item.style.display = 'flex';
104
105
  const marginLeft = index > 0 ? parseFloat(window.getComputedStyle(item).marginLeft) : 0;
105
106
  const currentItemWidth = item.offsetWidth + marginLeft;
106
- if (containerWidth - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
107
+ if (containerWidth.current - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
107
108
  totalWidthUsed += currentItemWidth;
108
109
  } else {
109
110
  item.style.display = 'none';
@@ -119,6 +120,7 @@ function NavMenu({
119
120
  wait: 100
120
121
  });
121
122
  useLayoutEffect(() => {
123
+ containerWidth.current = containerSize?.width || navMenuRef.current?.clientWidth || 0;
122
124
  if (mode === 'horizontal') {
123
125
  checkItemsFit();
124
126
  }
@@ -144,8 +146,14 @@ function NavMenu({
144
146
  // eslint-disable-next-line react-hooks/exhaustive-deps
145
147
  }, [activeId]);
146
148
  const classes = clsx('navmenu', `navmenu--${mode}`, rest.className);
147
- const renderItem = (item, index, isTopLevel = false) => {
149
+ const renderItem = (item, index, level = 0) => {
150
+ const isTopLevel = level === 0;
148
151
  if (item?.children) {
152
+ // 只渲染两级子菜单
153
+ if (level > 0) {
154
+ return null;
155
+ }
156
+
149
157
  // 对于 Sub 组件,如果它是顶级组件,则包含 ref
150
158
  return /*#__PURE__*/_jsx(Sub, {
151
159
  id: item.id,
@@ -158,7 +166,7 @@ function NavMenu({
158
166
  children: typeof item.children === 'function' ? item.children : item.children.map((childItem, childIndex) => renderItem({
159
167
  ...childItem,
160
168
  variant: 'panel'
161
- }, childIndex, false))
169
+ }, childIndex, level + 1))
162
170
  }, item.id);
163
171
  }
164
172
 
@@ -175,18 +183,21 @@ function NavMenu({
175
183
  } : undefined
176
184
  }, item.id);
177
185
  };
178
- const content = items ? items?.slice(-currentState.hiddenItemCount).map((item, index) => renderItem(item, index)) : children?.slice(-currentState.hiddenItemCount);
186
+ const content = items ? items?.slice(-currentState.hiddenItemCount).map((item, index) => renderItem(item, index, 1)) : children?.slice(-currentState.hiddenItemCount);
187
+
188
+ // 当前展开的子菜单
189
+ const openedId = currentState.openedIds[0];
179
190
  return /*#__PURE__*/_jsx(NavMenuProvider, {
180
191
  value: contextValue,
181
- children: /*#__PURE__*/_jsx(NavMenuRoot, {
192
+ children: /*#__PURE__*/_jsx(NavMenuStyled, {
182
193
  ...rest,
183
194
  className: classes,
184
195
  $textColor: textColor,
185
196
  $bgColor: bgColor,
186
- children: /*#__PURE__*/_jsxs(NavMenuList, {
187
- className: clsx('navmenu-list', `navmenu-list--${mode}`),
197
+ children: /*#__PURE__*/_jsxs(NavMenuRoot, {
198
+ className: clsx('navmenu-root', `navmenu-root--${mode}`),
188
199
  ref: navMenuRef,
189
- children: [items ? items.map((item, index) => renderItem(item, index, true)) : renderChildrenWithRef(children || []), currentState.hiddenItemCount > 0 && /*#__PURE__*/_jsx(Sub, {
200
+ children: [items ? items.map((item, index) => renderItem(item, index)) : renderChildrenWithRef(children || []), currentState.hiddenItemCount > 0 && /*#__PURE__*/_jsx(Sub, {
190
201
  expandIcon: false,
191
202
  icon: icon,
192
203
  label: "",
@@ -329,8 +340,7 @@ export const Sub = /*#__PURE__*/forwardRef(({
329
340
  children: typeof expandIcon === 'function' ? expandIcon({
330
341
  isOpen
331
342
  }) : expandIcon
332
- }), /*#__PURE__*/_jsx("div", {
333
- className: "navmenu-sub__container",
343
+ }), /*#__PURE__*/_jsx(SubContainer, {
334
344
  ...containerProps,
335
345
  children: typeof children === 'function' ? children({
336
346
  isOpen
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { useLayoutEffect, useRef } from 'react';
3
+ import { useRef } from 'react';
4
4
  import { Link } from 'react-router-dom';
5
5
  import { useCreation, useMemoizedFn } from 'ahooks';
6
6
  import { Box, Grid } from '@mui/material';
7
- import { useWindowSize } from 'react-use';
8
7
  import SubItemGroup from './sub-item-group';
9
8
  import { Item } from './nav-menu';
10
9
  import { styled } from '../Theme';
@@ -1437,10 +1436,6 @@ export default function Products({
1437
1436
  mode
1438
1437
  } = useNavMenuContext();
1439
1438
  const wrapperRef = useRef(null);
1440
- const {
1441
- width,
1442
- height
1443
- } = useWindowSize();
1444
1439
  const {
1445
1440
  locale = 'en'
1446
1441
  } = useLocaleContext() || {};
@@ -1452,6 +1447,8 @@ export default function Products({
1452
1447
  children: [[{
1453
1448
  label: /*#__PURE__*/_jsx(Link, {
1454
1449
  to: `https://www.nftstudio.rocks/${locale}`,
1450
+ target: "_blank",
1451
+ rel: "noreferrer noopener",
1455
1452
  children: "NFT Studio"
1456
1453
  }),
1457
1454
  description: t('products.nftStudio.description'),
@@ -1459,6 +1456,8 @@ export default function Products({
1459
1456
  }, {
1460
1457
  label: /*#__PURE__*/_jsx(Link, {
1461
1458
  to: `https://www.arcblock.io/content/collections/${locale}/creator-studio`,
1459
+ target: "_blank",
1460
+ rel: "noreferrer noopener",
1462
1461
  children: "Creator Studio"
1463
1462
  }),
1464
1463
  description: t('products.creatorStudio.description'),
@@ -1466,6 +1465,8 @@ export default function Products({
1466
1465
  }], [{
1467
1466
  label: /*#__PURE__*/_jsx(Link, {
1468
1467
  to: `https://www.aigne.io/${locale}`,
1468
+ target: "_blank",
1469
+ rel: "noreferrer noopener",
1469
1470
  children: "AIGNE"
1470
1471
  }),
1471
1472
  description: t('products.aigne.description'),
@@ -1473,6 +1474,8 @@ export default function Products({
1473
1474
  }, {
1474
1475
  label: /*#__PURE__*/_jsx(Link, {
1475
1476
  to: `https://www.aistro.io/${locale}`,
1477
+ target: "_blank",
1478
+ rel: "noreferrer noopener",
1476
1479
  children: "Aistro"
1477
1480
  }),
1478
1481
  description: t('products.aistro.description'),
@@ -1484,6 +1487,8 @@ export default function Products({
1484
1487
  children: [[{
1485
1488
  label: /*#__PURE__*/_jsx(Link, {
1486
1489
  to: `https://launcher.arcblock.io/${locale}`,
1490
+ target: "_blank",
1491
+ rel: "noreferrer noopener",
1487
1492
  children: "Blocklet Launcher"
1488
1493
  }),
1489
1494
  description: t('products.blockletLauncher.description'),
@@ -1491,6 +1496,8 @@ export default function Products({
1491
1496
  }, {
1492
1497
  label: /*#__PURE__*/_jsx(Link, {
1493
1498
  to: `https://www.arcblock.io/content/collections/${locale}/ai-kit`,
1499
+ target: "_blank",
1500
+ rel: "noreferrer noopener",
1494
1501
  children: "Al Kit"
1495
1502
  }),
1496
1503
  description: t('products.alKit.description'),
@@ -1498,6 +1505,8 @@ export default function Products({
1498
1505
  }], [{
1499
1506
  label: /*#__PURE__*/_jsx(Link, {
1500
1507
  to: `https://store.blocklet.dev/${locale}`,
1508
+ target: "_blank",
1509
+ rel: "noreferrer noopener",
1501
1510
  children: "Blocklet Store"
1502
1511
  }),
1503
1512
  description: t('products.blockletStore.description'),
@@ -1505,6 +1514,8 @@ export default function Products({
1505
1514
  }, {
1506
1515
  label: /*#__PURE__*/_jsx(Link, {
1507
1516
  to: `https://www.web3kit.rocks/${locale}`,
1517
+ target: "_blank",
1518
+ rel: "noreferrer noopener",
1508
1519
  children: "Web3 Kit"
1509
1520
  }),
1510
1521
  description: t('products.web3Kit.description'),
@@ -1516,6 +1527,8 @@ export default function Products({
1516
1527
  children: [[{
1517
1528
  label: /*#__PURE__*/_jsx(Link, {
1518
1529
  to: `https://www.arcblock.io/content/collections/${locale}/blocklet`,
1530
+ target: "_blank",
1531
+ rel: "noreferrer noopener",
1519
1532
  children: "Blocklet Framework"
1520
1533
  }),
1521
1534
  description: t('products.blockletFramework.description'),
@@ -1523,6 +1536,8 @@ export default function Products({
1523
1536
  }, {
1524
1537
  label: /*#__PURE__*/_jsx(Link, {
1525
1538
  to: `https://www.didspaces.com/${locale}`,
1539
+ target: "_blank",
1540
+ rel: "noreferrer noopener",
1526
1541
  children: "DID Spaces"
1527
1542
  }),
1528
1543
  description: t('products.didSpaces.description'),
@@ -1530,6 +1545,8 @@ export default function Products({
1530
1545
  }, {
1531
1546
  label: /*#__PURE__*/_jsx(Link, {
1532
1547
  to: `https://main.abtnetwork.io/${locale}`,
1548
+ target: "_blank",
1549
+ rel: "noreferrer noopener",
1533
1550
  children: "ABT Network"
1534
1551
  }),
1535
1552
  description: t('products.abtNetwork.description'),
@@ -1537,6 +1554,8 @@ export default function Products({
1537
1554
  }], [{
1538
1555
  label: /*#__PURE__*/_jsx(Link, {
1539
1556
  to: `https://www.arcblock.io/content/collections/${locale}/blocklet-server`,
1557
+ target: "_blank",
1558
+ rel: "noreferrer noopener",
1540
1559
  children: "Blocklet Server"
1541
1560
  }),
1542
1561
  description: t('products.blockletServer.description'),
@@ -1544,6 +1563,8 @@ export default function Products({
1544
1563
  }, {
1545
1564
  label: /*#__PURE__*/_jsx(Link, {
1546
1565
  to: `https://www.arcblock.io/content/collections/${locale}/blockchain`,
1566
+ target: "_blank",
1567
+ rel: "noreferrer noopener",
1547
1568
  children: "\u041E\u0421\u0410\u0420"
1548
1569
  }),
1549
1570
  description: t('products.osar.description'),
@@ -1555,6 +1576,8 @@ export default function Products({
1555
1576
  children: [[{
1556
1577
  label: /*#__PURE__*/_jsx(Link, {
1557
1578
  to: `https://www.arcblock.io/content/collections/${locale}/did`,
1579
+ target: "_blank",
1580
+ rel: "noreferrer noopener",
1558
1581
  children: "DID"
1559
1582
  }),
1560
1583
  description: t('products.did.description'),
@@ -1562,6 +1585,8 @@ export default function Products({
1562
1585
  }, {
1563
1586
  label: /*#__PURE__*/_jsx(Link, {
1564
1587
  to: `https://www.didwallet.io/${locale}`,
1588
+ target: "_blank",
1589
+ rel: "noreferrer noopener",
1565
1590
  children: "DID Wallet"
1566
1591
  }),
1567
1592
  description: t('products.didWallet.description'),
@@ -1569,13 +1594,17 @@ export default function Products({
1569
1594
  }, {
1570
1595
  label: /*#__PURE__*/_jsx(Link, {
1571
1596
  to: `https://www.didnames.io/${locale}`,
1572
- children: "DID Name Service"
1597
+ target: "_blank",
1598
+ rel: "noreferrer noopener",
1599
+ children: "DID Names"
1573
1600
  }),
1574
1601
  description: t('products.didNameService.description'),
1575
1602
  icon: /*#__PURE__*/_jsx(DidNameServiceSvg, {})
1576
1603
  }], [{
1577
1604
  label: /*#__PURE__*/_jsx(Link, {
1578
1605
  to: `https://www.arcblock.io/content/collections/${locale}/verifiable-credential`,
1606
+ target: "_blank",
1607
+ rel: "noreferrer noopener",
1579
1608
  children: "VC"
1580
1609
  }),
1581
1610
  description: t('products.vc.description'),
@@ -1583,6 +1612,8 @@ export default function Products({
1583
1612
  }, {
1584
1613
  label: /*#__PURE__*/_jsx(Link, {
1585
1614
  to: `https://www.didconnect.io/${locale}`,
1615
+ target: "_blank",
1616
+ rel: "noreferrer noopener",
1586
1617
  children: "DID Connect"
1587
1618
  }),
1588
1619
  description: t('products.didConnect.description'),
@@ -1590,26 +1621,6 @@ export default function Products({
1590
1621
  }]]
1591
1622
  }];
1592
1623
  }, [t, locale]);
1593
-
1594
- // 防止弹框超出 window
1595
- useLayoutEffect(() => {
1596
- const wrapper = wrapperRef.current;
1597
- if (!wrapper) return;
1598
- if (!isOpen) {
1599
- wrapper.style.transform = '';
1600
- return;
1601
- }
1602
- const rect = wrapper.getBoundingClientRect();
1603
- const windowWidth = window.innerWidth;
1604
- if (rect.right > windowWidth) {
1605
- const offset = rect.right - windowWidth;
1606
- wrapper.style.transform = `translateX(-${offset + 16}px)`;
1607
- } else if (rect.left < 0) {
1608
- wrapper.style.transform = `translateX(${Math.abs(rect.left) + 16}px)`;
1609
- } else {
1610
- wrapper.style.transform = '';
1611
- }
1612
- }, [width, height, isOpen]);
1613
1624
  return /*#__PURE__*/_jsx(Wrapper, {
1614
1625
  ref: wrapperRef,
1615
1626
  className: `is-${mode} ${className}`,
@@ -2,8 +2,8 @@ type NavMenuProps = {
2
2
  $bgColor: string;
3
3
  $textColor: string;
4
4
  };
5
- export declare const NavMenuRoot: import("@emotion/styled").StyledComponent<import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme> & NavMenuProps, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLElement>, HTMLElement>, {}>;
6
- export declare const NavMenuList: import("@emotion/styled").StyledComponent<import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme>, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLUListElement>, HTMLUListElement>, {}>;
5
+ export declare const NavMenuStyled: import("@emotion/styled").StyledComponent<import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme> & NavMenuProps, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLElement>, HTMLElement>, {}>;
6
+ export declare const NavMenuRoot: import("@emotion/styled").StyledComponent<import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme>, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLUListElement>, HTMLUListElement>, {}>;
7
7
  type NavMenuItemProps = {
8
8
  $activeTextColor: string;
9
9
  };
@@ -1,12 +1,13 @@
1
1
  import { styled } from '../Theme';
2
- // .navmenu-root
3
- export const NavMenuRoot = styled('nav', {
2
+ // .navmenu
3
+ export const NavMenuStyled = styled('nav', {
4
4
  shouldForwardProp: prop => prop !== '$bgColor' && prop !== '$textColor'
5
5
  })(({
6
6
  $bgColor,
7
7
  $textColor
8
8
  }) => ({
9
- padding: '8px 16px',
9
+ position: 'relative',
10
+ padding: '0 16px',
10
11
  minWidth: '50px',
11
12
  // FIXME: @zhanghan 这个只是临时的解决方案,会导致 header align right 不能真正的右对齐,需要修改 header 才能真正解决这个问题
12
13
  flexGrow: 100,
@@ -15,15 +16,15 @@ export const NavMenuRoot = styled('nav', {
15
16
  fontSize: '16px'
16
17
  }));
17
18
 
18
- // .navmenu-list
19
- export const NavMenuList = styled('ul')(() => ({
19
+ // .navmenu-root
20
+ export const NavMenuRoot = styled('ul')(() => ({
20
21
  listStyle: 'none',
21
22
  margin: 0,
22
23
  padding: 0,
23
24
  display: 'flex',
24
25
  alignItems: 'center',
25
26
  // inline 布局
26
- '&.navmenu-list--inline ': {
27
+ '&.navmenu-root--inline ': {
27
28
  flexDirection: 'column',
28
29
  alignItems: 'stretch'
29
30
  }
@@ -32,15 +33,18 @@ export const NavMenuList = styled('ul')(() => ({
32
33
  export const NavMenuItem = styled('li', {
33
34
  shouldForwardProp: prop => prop !== '$activeTextColor'
34
35
  })(({
35
- $activeTextColor
36
+ $activeTextColor,
37
+ theme
36
38
  }) => ({
37
39
  display: 'flex',
38
40
  alignItems: 'center',
39
41
  position: 'relative',
40
- padding: '0 12px',
42
+ padding: '8px 12px',
41
43
  whiteSpace: 'nowrap',
42
44
  cursor: 'pointer',
43
- transition: 'color 0.2s ease-in-out',
45
+ transition: theme.transitions.create('color', {
46
+ duration: theme.transitions.duration.standard
47
+ }),
44
48
  // 间距调整
45
49
  '&:first-of-type': {
46
50
  paddingLeft: 0
@@ -93,7 +97,9 @@ export const NavMenuItem = styled('li', {
93
97
  marginLeft: '6px',
94
98
  fontSize: '14px',
95
99
  opacity: 0,
96
- transition: 'opacity 0.2s ease-in-out'
100
+ transition: theme.transitions.create('opacity', {
101
+ duration: theme.transitions.duration.standard
102
+ })
97
103
  }
98
104
  },
99
105
  '.navmenu-item__desc': {
@@ -131,13 +137,17 @@ export const NavMenuItem = styled('li', {
131
137
  },
132
138
  '&:hover': {
133
139
  background: '#f9f9fb',
134
- transition: 'background 0.2s ease-in-out',
140
+ transition: theme.transitions.create('background', {
141
+ duration: theme.transitions.duration.standard
142
+ }),
135
143
  '.navmenu-item__label-arrow': {
136
144
  opacity: 1
137
145
  },
138
146
  '.navmenu-item__desc': {
139
147
  color: '#26292e',
140
- transition: 'color 0.2s ease-in-out'
148
+ transition: theme.transitions.create('color', {
149
+ duration: theme.transitions.duration.standard
150
+ })
141
151
  }
142
152
  },
143
153
  '&.navmenu-item--active': {
@@ -155,24 +165,28 @@ export const NavMenuItem = styled('li', {
155
165
  }));
156
166
 
157
167
  // 包含子菜单的导航项 .navmenu-item .navmenu-sub
158
- export const NavMenuSub = styled(NavMenuItem)(() => ({
168
+ export const NavMenuSub = styled(NavMenuItem)(({
169
+ theme
170
+ }) => ({
159
171
  '.navmenu-item': {
160
- marginLeft: 0
172
+ marginLeft: 0,
173
+ overflow: 'hidden'
161
174
  },
162
175
  '& .navmenu-sub__container': {
163
- visibility: 'hidden',
176
+ pointerEvents: 'none',
164
177
  opacity: 0,
165
178
  position: 'absolute',
166
179
  top: '100%',
167
- left: '50%',
168
- zIndex: 1,
169
- transform: 'translateX(-50%)',
170
- paddingTop: '16px',
171
- transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out'
180
+ left: 0,
181
+ zIndex: 1
172
182
  },
173
183
  '&.navmenu-sub--opened > .navmenu-sub__container': {
174
- visibility: 'visible',
175
- opacity: 1
184
+ pointerEvents: 'auto',
185
+ opacity: 1,
186
+ transition: theme.transitions.create('opacity', {
187
+ duration: theme.transitions.duration.standard,
188
+ easing: theme.transitions.easing.easeOut
189
+ })
176
190
  },
177
191
  // 扩展图标
178
192
  '.navmenu-sub__expand-icon': {
@@ -183,7 +197,9 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
183
197
  '& > *': {
184
198
  width: '0.8em',
185
199
  height: '0.8em',
186
- transition: 'transform 0.2s ease-in-out'
200
+ transition: theme.transitions.create('transform', {
201
+ duration: theme.transitions.duration.standard
202
+ })
187
203
  },
188
204
  '.navmenu-item--inline &': {
189
205
  marginLeft: 'auto'
@@ -199,7 +215,9 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
199
215
  padding: 0,
200
216
  height: 0,
201
217
  overflow: 'hidden',
202
- transition: 'height 0.2s ease-in-out'
218
+ transition: theme.transitions.create('height', {
219
+ duration: theme.transitions.duration.standard
220
+ })
203
221
  },
204
222
  '&.navmenu-sub--opened > .navmenu-sub__container': {
205
223
  height: 'auto'
@@ -208,11 +226,14 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
208
226
  padding: 0,
209
227
  paddingLeft: '16px',
210
228
  boxShadow: 'none'
229
+ },
230
+ '.navmenu-item__content': {
231
+ height: '48px'
211
232
  }
212
233
  }
213
234
  }));
214
235
 
215
- // 下拉子菜单 .navmenu-sub-list
236
+ // 下拉子菜单 .navmenu-sub__list
216
237
  export const NavMenuSubList = styled('ul')(() => ({
217
238
  margin: 0,
218
239
  padding: '16px',
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface SubContainerProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ children: React.ReactNode;
4
+ }
5
+ export declare function SubContainer({ children, ...props }: SubContainerProps): import("react/jsx-runtime").JSX.Element;
6
+ export default SubContainer;
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ const paddingTop = 8;
4
+
5
+ // 下拉子菜单容器
6
+ export function SubContainer({
7
+ children,
8
+ ...props
9
+ }) {
10
+ const rootRef = useRef(null);
11
+ const [position, setPosition] = useState(0); // 只需要保存水平偏移量
12
+
13
+ const updatePosition = () => {
14
+ if (!rootRef.current) return;
15
+ const anchor = rootRef.current.parentElement; // 以父容器作为参照
16
+ if (!anchor) return;
17
+ const anchorRect = anchor.getBoundingClientRect();
18
+ const containerRect = rootRef.current.getBoundingClientRect();
19
+ const windowWidth = window.innerWidth;
20
+ const padding = 32;
21
+
22
+ // 1. 计算相对于父元素的水平居中位置
23
+ let left = (anchorRect.width - containerRect.width) / 2;
24
+
25
+ // 2. 检查容器在视口中的绝对位置
26
+ const absoluteLeft = anchorRect.left + left;
27
+
28
+ // 3. 调整水平位置确保不超出窗口
29
+ if (absoluteLeft < 0) {
30
+ // 如果超出左边界,向右偏移
31
+ left = left - absoluteLeft + padding;
32
+ } else if (absoluteLeft + containerRect.width > windowWidth) {
33
+ // 如果超出右边界,向左偏移
34
+ left -= absoluteLeft + containerRect.width - windowWidth + padding;
35
+ }
36
+ setPosition(left);
37
+ };
38
+
39
+ // 监听自身大小变化
40
+ useEffect(() => {
41
+ const resizeObserver = new ResizeObserver(() => {
42
+ updatePosition();
43
+ });
44
+ resizeObserver.observe(rootRef.current);
45
+ return () => resizeObserver.disconnect();
46
+ // eslint-disable-next-line react-hooks/exhaustive-deps
47
+ }, []);
48
+
49
+ // 监听 anchor 的大小变化
50
+ useEffect(() => {
51
+ const anchor = rootRef.current?.parentElement;
52
+ let resizeObserver = null;
53
+ if (anchor) {
54
+ resizeObserver = new ResizeObserver(() => {
55
+ updatePosition();
56
+ });
57
+ resizeObserver.observe(anchor);
58
+ }
59
+ return () => resizeObserver?.disconnect();
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
+ }, []);
62
+
63
+ // 监听窗口大小变化
64
+ useEffect(() => {
65
+ const handleResize = () => {
66
+ updatePosition();
67
+ };
68
+ window.addEventListener('resize', handleResize);
69
+ return () => window.removeEventListener('resize', handleResize);
70
+ // eslint-disable-next-line react-hooks/exhaustive-deps
71
+ }, []);
72
+ return /*#__PURE__*/_jsx("div", {
73
+ ref: rootRef,
74
+ className: "navmenu-sub__container",
75
+ style: {
76
+ paddingTop: `${paddingTop}px`,
77
+ transform: `translateX(${position}px)`
78
+ },
79
+ ...props,
80
+ children: children
81
+ });
82
+ }
83
+ export default SubContainer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.12.3",
3
+ "version": "2.12.4",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -68,12 +68,12 @@
68
68
  "react": ">=18.2.0",
69
69
  "react-router-dom": ">=6.22.3"
70
70
  },
71
- "gitHead": "d0ad7fcbd9bbae010ad58235494150e3c0814d43",
71
+ "gitHead": "f8648a2f55bb57212a24a5948a5f026e2c20b1a7",
72
72
  "dependencies": {
73
73
  "@arcblock/did-motif": "^1.1.13",
74
- "@arcblock/icons": "^2.12.3",
75
- "@arcblock/nft-display": "^2.12.3",
76
- "@arcblock/react-hooks": "^2.12.3",
74
+ "@arcblock/icons": "^2.12.4",
75
+ "@arcblock/nft-display": "^2.12.4",
76
+ "@arcblock/react-hooks": "^2.12.4",
77
77
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
78
78
  "@fontsource/inter": "^5.0.16",
79
79
  "@fontsource/ubuntu-mono": "^5.0.18",
@@ -5,7 +5,8 @@ import { MoreHoriz as MoreHorizIcon, ExpandMore as ExpandMoreIcon, Menu as MenuI
5
5
  import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
6
6
  import { useCreation, useMemoizedFn, useReactive, useSize, useThrottleFn } from 'ahooks';
7
7
  import { NavMenuProvider, useNavMenuContext } from './nav-menu-context';
8
- import { NavMenuRoot, NavMenuList, NavMenuItem, NavMenuSub, NavMenuSubList } from './style';
8
+ import { NavMenuRoot, NavMenuItem, NavMenuSub, NavMenuSubList, NavMenuStyled } from './style';
9
+ import { SubContainer } from './sub-container';
9
10
 
10
11
  // 过滤 children 中的 Item/Sub, 忽略其它类型的 element
11
12
  function filterItems(children: React.ReactNode) {
@@ -67,7 +68,6 @@ function NavMenu({
67
68
  if (!items?.length && !children?.length) {
68
69
  throw new Error("One of 'items' or 'children' is required by NavMenu component.");
69
70
  }
70
-
71
71
  const currentState = useReactive({
72
72
  activeId,
73
73
  openedIds: [] as string[],
@@ -112,6 +112,7 @@ function NavMenu({
112
112
  const navMenuRef = useRef<HTMLUListElement | null>(null);
113
113
  const itemRefs = useRef<HTMLElement[]>([]);
114
114
  const moreIconRef = useRef<HTMLLIElement | null>(null);
115
+ const containerWidth = useRef(0);
115
116
  const isAllItemsHidden = currentState.hiddenItemCount === itemRefs.current?.length;
116
117
  const style = isAllItemsHidden ? { marginLeft: '0px' } : undefined;
117
118
 
@@ -136,7 +137,6 @@ function NavMenu({
136
137
  let totalWidthUsed = 0;
137
138
  let newHiddenCount = 0;
138
139
  let leftAllHidden = false;
139
- const containerWidth = containerSize?.width || 0;
140
140
  const moreIconWidth = moreIconRef.current
141
141
  ? moreIconRef.current.offsetWidth + parseFloat(window.getComputedStyle(moreIconRef.current).marginLeft)
142
142
  : 0;
@@ -147,7 +147,7 @@ function NavMenu({
147
147
  const marginLeft = index > 0 ? parseFloat(window.getComputedStyle(item).marginLeft) : 0;
148
148
  const currentItemWidth = item.offsetWidth + marginLeft;
149
149
 
150
- if (containerWidth - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
150
+ if (containerWidth.current - moreIconWidth >= totalWidthUsed + currentItemWidth && !leftAllHidden) {
151
151
  totalWidthUsed += currentItemWidth;
152
152
  } else {
153
153
  item.style.display = 'none';
@@ -165,6 +165,7 @@ function NavMenu({
165
165
  );
166
166
 
167
167
  useLayoutEffect(() => {
168
+ containerWidth.current = containerSize?.width || navMenuRef.current?.clientWidth || 0;
168
169
  if (mode === 'horizontal') {
169
170
  checkItemsFit();
170
171
  }
@@ -195,8 +196,15 @@ function NavMenu({
195
196
 
196
197
  const classes = clsx('navmenu', `navmenu--${mode}`, rest.className);
197
198
 
198
- const renderItem = (item: ItemOptions, index: number, isTopLevel = false) => {
199
+ const renderItem = (item: ItemOptions, index: number, level = 0) => {
200
+ const isTopLevel = level === 0;
201
+
199
202
  if (item?.children) {
203
+ // 只渲染两级子菜单
204
+ if (level > 0) {
205
+ return null;
206
+ }
207
+
200
208
  // 对于 Sub 组件,如果它是顶级组件,则包含 ref
201
209
  return (
202
210
  <Sub
@@ -215,7 +223,7 @@ function NavMenu({
215
223
  {typeof item.children === 'function'
216
224
  ? item.children
217
225
  : item.children.map((childItem, childIndex: number) =>
218
- renderItem({ ...childItem, variant: 'panel' }, childIndex, false)
226
+ renderItem({ ...childItem, variant: 'panel' }, childIndex, level + 1)
219
227
  )}
220
228
  </Sub>
221
229
  );
@@ -243,21 +251,24 @@ function NavMenu({
243
251
  };
244
252
 
245
253
  const content = items
246
- ? items?.slice(-currentState.hiddenItemCount).map((item, index) => renderItem(item, index))
254
+ ? items?.slice(-currentState.hiddenItemCount).map((item, index) => renderItem(item, index, 1))
247
255
  : children?.slice(-currentState.hiddenItemCount);
248
256
 
257
+ // 当前展开的子菜单
258
+ const openedId = currentState.openedIds[0];
259
+
249
260
  return (
250
261
  <NavMenuProvider value={contextValue}>
251
- <NavMenuRoot {...rest} className={classes} $textColor={textColor} $bgColor={bgColor}>
252
- <NavMenuList className={clsx('navmenu-list', `navmenu-list--${mode}`)} ref={navMenuRef}>
253
- {items ? items.map((item, index) => renderItem(item, index, true)) : renderChildrenWithRef(children || [])}
262
+ <NavMenuStyled {...rest} className={classes} $textColor={textColor} $bgColor={bgColor}>
263
+ <NavMenuRoot className={clsx('navmenu-root', `navmenu-root--${mode}`)} ref={navMenuRef}>
264
+ {items ? items.map((item, index) => renderItem(item, index)) : renderChildrenWithRef(children || [])}
254
265
  {currentState.hiddenItemCount > 0 && (
255
266
  <Sub expandIcon={false} icon={icon} label="" ref={moreIconRef} style={style}>
256
267
  {content}
257
268
  </Sub>
258
269
  )}
259
- </NavMenuList>
260
- </NavMenuRoot>
270
+ </NavMenuRoot>
271
+ </NavMenuStyled>
261
272
  </NavMenuProvider>
262
273
  );
263
274
  }
@@ -324,7 +335,7 @@ export const Item = forwardRef<HTMLLIElement, ItemProps>(
324
335
  Item.displayName = 'NavMenu.Item';
325
336
 
326
337
  export interface SubProps extends Omit<ItemProps, 'children' | 'active'> {
327
- children?: Array<React.ReactElement> | ((props: { isOpen: boolean }) => React.ReactElement | null);
338
+ children?: Array<React.ReactElement | null> | ((props: { isOpen: boolean }) => React.ReactElement | null);
328
339
  expandIcon?: React.ReactNode | ((props: { isOpen: boolean }) => React.ReactNode);
329
340
  }
330
341
 
@@ -394,13 +405,13 @@ export const Sub = forwardRef<HTMLLIElement, SubProps>(
394
405
  {typeof expandIcon === 'function' ? expandIcon({ isOpen }) : expandIcon}
395
406
  </span>
396
407
  )}
397
- <div className="navmenu-sub__container" {...containerProps}>
408
+ <SubContainer {...containerProps}>
398
409
  {typeof children === 'function' ? (
399
410
  children({ isOpen }) // 自定义渲染
400
411
  ) : (
401
412
  <NavMenuSubList className="navmenu-sub__list">{filterItems(children)}</NavMenuSubList>
402
413
  )}
403
- </div>
414
+ </SubContainer>
404
415
  </NavMenuSub>
405
416
  );
406
417
  }
@@ -1,8 +1,7 @@
1
- import { useLayoutEffect, useRef } from 'react';
1
+ import { useRef } from 'react';
2
2
  import { Link } from 'react-router-dom';
3
3
  import { useCreation, useMemoizedFn } from 'ahooks';
4
4
  import { Box, BoxProps, Grid } from '@mui/material';
5
- import { useWindowSize } from 'react-use';
6
5
  import SubItemGroup from './sub-item-group';
7
6
  import { Item } from './nav-menu';
8
7
  import { styled } from '../Theme';
@@ -191,7 +190,6 @@ export interface ProductsProps extends BoxProps {
191
190
  export default function Products({ className, isOpen, ...rest }: ProductsProps) {
192
191
  const { mode } = useNavMenuContext();
193
192
  const wrapperRef = useRef<HTMLDivElement>(null);
194
- const { width, height } = useWindowSize();
195
193
  const { locale = 'en' } = useLocaleContext() || {};
196
194
  const t = useMemoizedFn((key, data = {}) => translate(translations, key, locale, 'en', data));
197
195
  const groups = useCreation(() => {
@@ -202,13 +200,22 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
202
200
  children: [
203
201
  [
204
202
  {
205
- label: <Link to={`https://www.nftstudio.rocks/${locale}`}>NFT Studio</Link>,
203
+ label: (
204
+ <Link to={`https://www.nftstudio.rocks/${locale}`} target="_blank" rel="noreferrer noopener">
205
+ NFT Studio
206
+ </Link>
207
+ ),
206
208
  description: t('products.nftStudio.description'),
207
209
  icon: <NftStudioSvg />,
208
210
  },
209
211
  {
210
212
  label: (
211
- <Link to={`https://www.arcblock.io/content/collections/${locale}/creator-studio`}>Creator Studio</Link>
213
+ <Link
214
+ to={`https://www.arcblock.io/content/collections/${locale}/creator-studio`}
215
+ target="_blank"
216
+ rel="noreferrer noopener">
217
+ Creator Studio
218
+ </Link>
212
219
  ),
213
220
  description: t('products.creatorStudio.description'),
214
221
  icon: <CreatorStudioSvg />,
@@ -216,12 +223,20 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
216
223
  ],
217
224
  [
218
225
  {
219
- label: <Link to={`https://www.aigne.io/${locale}`}>AIGNE</Link>,
226
+ label: (
227
+ <Link to={`https://www.aigne.io/${locale}`} target="_blank" rel="noreferrer noopener">
228
+ AIGNE
229
+ </Link>
230
+ ),
220
231
  description: t('products.aigne.description'),
221
232
  icon: <AigneSvg />,
222
233
  },
223
234
  {
224
- label: <Link to={`https://www.aistro.io/${locale}`}>Aistro</Link>,
235
+ label: (
236
+ <Link to={`https://www.aistro.io/${locale}`} target="_blank" rel="noreferrer noopener">
237
+ Aistro
238
+ </Link>
239
+ ),
225
240
  description: t('products.aistro.description'),
226
241
  icon: <AistroSvg />,
227
242
  },
@@ -234,24 +249,43 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
234
249
  children: [
235
250
  [
236
251
  {
237
- label: <Link to={`https://launcher.arcblock.io/${locale}`}>Blocklet Launcher</Link>,
252
+ label: (
253
+ <Link to={`https://launcher.arcblock.io/${locale}`} target="_blank" rel="noreferrer noopener">
254
+ Blocklet Launcher
255
+ </Link>
256
+ ),
238
257
  description: t('products.blockletLauncher.description'),
239
258
  icon: <BlockletLauncherSvg />,
240
259
  },
241
260
  {
242
- label: <Link to={`https://www.arcblock.io/content/collections/${locale}/ai-kit`}>Al Kit</Link>,
261
+ label: (
262
+ <Link
263
+ to={`https://www.arcblock.io/content/collections/${locale}/ai-kit`}
264
+ target="_blank"
265
+ rel="noreferrer noopener">
266
+ Al Kit
267
+ </Link>
268
+ ),
243
269
  description: t('products.alKit.description'),
244
270
  icon: <AIKitSvg />,
245
271
  },
246
272
  ],
247
273
  [
248
274
  {
249
- label: <Link to={`https://store.blocklet.dev/${locale}`}>Blocklet Store</Link>,
275
+ label: (
276
+ <Link to={`https://store.blocklet.dev/${locale}`} target="_blank" rel="noreferrer noopener">
277
+ Blocklet Store
278
+ </Link>
279
+ ),
250
280
  description: t('products.blockletStore.description'),
251
281
  icon: <BlockletStoreSvg />,
252
282
  },
253
283
  {
254
- label: <Link to={`https://www.web3kit.rocks/${locale}`}>Web3 Kit</Link>,
284
+ label: (
285
+ <Link to={`https://www.web3kit.rocks/${locale}`} target="_blank" rel="noreferrer noopener">
286
+ Web3 Kit
287
+ </Link>
288
+ ),
255
289
  description: t('products.web3Kit.description'),
256
290
  icon: <Web3KitSvg />,
257
291
  },
@@ -265,18 +299,31 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
265
299
  [
266
300
  {
267
301
  label: (
268
- <Link to={`https://www.arcblock.io/content/collections/${locale}/blocklet`}>Blocklet Framework</Link>
302
+ <Link
303
+ to={`https://www.arcblock.io/content/collections/${locale}/blocklet`}
304
+ target="_blank"
305
+ rel="noreferrer noopener">
306
+ Blocklet Framework
307
+ </Link>
269
308
  ),
270
309
  description: t('products.blockletFramework.description'),
271
310
  icon: <BlockletFrameworkSvg />,
272
311
  },
273
312
  {
274
- label: <Link to={`https://www.didspaces.com/${locale}`}>DID Spaces</Link>,
313
+ label: (
314
+ <Link to={`https://www.didspaces.com/${locale}`} target="_blank" rel="noreferrer noopener">
315
+ DID Spaces
316
+ </Link>
317
+ ),
275
318
  description: t('products.didSpaces.description'),
276
319
  icon: <DidSvg />,
277
320
  },
278
321
  {
279
- label: <Link to={`https://main.abtnetwork.io/${locale}`}>ABT Network</Link>,
322
+ label: (
323
+ <Link to={`https://main.abtnetwork.io/${locale}`} target="_blank" rel="noreferrer noopener">
324
+ ABT Network
325
+ </Link>
326
+ ),
280
327
  description: t('products.abtNetwork.description'),
281
328
  icon: <AbtNetworkSvg />,
282
329
  },
@@ -284,7 +331,10 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
284
331
  [
285
332
  {
286
333
  label: (
287
- <Link to={`https://www.arcblock.io/content/collections/${locale}/blocklet-server`}>
334
+ <Link
335
+ to={`https://www.arcblock.io/content/collections/${locale}/blocklet-server`}
336
+ target="_blank"
337
+ rel="noreferrer noopener">
288
338
  Blocklet Server
289
339
  </Link>
290
340
  ),
@@ -292,7 +342,14 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
292
342
  icon: <BlockletServerSvg />,
293
343
  },
294
344
  {
295
- label: <Link to={`https://www.arcblock.io/content/collections/${locale}/blockchain`}>ОСАР</Link>,
345
+ label: (
346
+ <Link
347
+ to={`https://www.arcblock.io/content/collections/${locale}/blockchain`}
348
+ target="_blank"
349
+ rel="noreferrer noopener">
350
+ ОСАР
351
+ </Link>
352
+ ),
296
353
  description: t('products.osar.description'),
297
354
  icon: <OCAPSvg />,
298
355
  },
@@ -305,29 +362,55 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
305
362
  children: [
306
363
  [
307
364
  {
308
- label: <Link to={`https://www.arcblock.io/content/collections/${locale}/did`}>DID</Link>,
365
+ label: (
366
+ <Link
367
+ to={`https://www.arcblock.io/content/collections/${locale}/did`}
368
+ target="_blank"
369
+ rel="noreferrer noopener">
370
+ DID
371
+ </Link>
372
+ ),
309
373
  description: t('products.did.description'),
310
374
  icon: <DidSvg />,
311
375
  },
312
376
  {
313
- label: <Link to={`https://www.didwallet.io/${locale}`}>DID Wallet</Link>,
377
+ label: (
378
+ <Link to={`https://www.didwallet.io/${locale}`} target="_blank" rel="noreferrer noopener">
379
+ DID Wallet
380
+ </Link>
381
+ ),
314
382
  description: t('products.didWallet.description'),
315
383
  icon: <DidWalletSvg />,
316
384
  },
317
385
  {
318
- label: <Link to={`https://www.didnames.io/${locale}`}>DID Name Service</Link>,
386
+ label: (
387
+ <Link to={`https://www.didnames.io/${locale}`} target="_blank" rel="noreferrer noopener">
388
+ DID Names
389
+ </Link>
390
+ ),
319
391
  description: t('products.didNameService.description'),
320
392
  icon: <DidNameServiceSvg />,
321
393
  },
322
394
  ],
323
395
  [
324
396
  {
325
- label: <Link to={`https://www.arcblock.io/content/collections/${locale}/verifiable-credential`}>VC</Link>,
397
+ label: (
398
+ <Link
399
+ to={`https://www.arcblock.io/content/collections/${locale}/verifiable-credential`}
400
+ target="_blank"
401
+ rel="noreferrer noopener">
402
+ VC
403
+ </Link>
404
+ ),
326
405
  description: t('products.vc.description'),
327
406
  icon: <VCSvg />,
328
407
  },
329
408
  {
330
- label: <Link to={`https://www.didconnect.io/${locale}`}>DID Connect</Link>,
409
+ label: (
410
+ <Link to={`https://www.didconnect.io/${locale}`} target="_blank" rel="noreferrer noopener">
411
+ DID Connect
412
+ </Link>
413
+ ),
331
414
  description: t('products.didConnect.description'),
332
415
  icon: <DidConnectSvg />,
333
416
  },
@@ -337,27 +420,6 @@ export default function Products({ className, isOpen, ...rest }: ProductsProps)
337
420
  ];
338
421
  }, [t, locale]);
339
422
 
340
- // 防止弹框超出 window
341
- useLayoutEffect(() => {
342
- const wrapper = wrapperRef.current;
343
- if (!wrapper) return;
344
- if (!isOpen) {
345
- wrapper.style.transform = '';
346
- return;
347
- }
348
-
349
- const rect = wrapper.getBoundingClientRect();
350
- const windowWidth = window.innerWidth;
351
- if (rect.right > windowWidth) {
352
- const offset = rect.right - windowWidth;
353
- wrapper.style.transform = `translateX(-${offset + 16}px)`;
354
- } else if (rect.left < 0) {
355
- wrapper.style.transform = `translateX(${Math.abs(rect.left) + 16}px)`;
356
- } else {
357
- wrapper.style.transform = '';
358
- }
359
- }, [width, height, isOpen]);
360
-
361
423
  return (
362
424
  <Wrapper ref={wrapperRef} className={`is-${mode} ${className}`} {...rest}>
363
425
  {groups.map((group) => (
@@ -5,11 +5,12 @@ type NavMenuProps = {
5
5
  $textColor: string;
6
6
  };
7
7
 
8
- // .navmenu-root
9
- export const NavMenuRoot = styled('nav', {
8
+ // .navmenu
9
+ export const NavMenuStyled = styled('nav', {
10
10
  shouldForwardProp: (prop) => prop !== '$bgColor' && prop !== '$textColor',
11
11
  })<NavMenuProps>(({ $bgColor, $textColor }) => ({
12
- padding: '8px 16px',
12
+ position: 'relative',
13
+ padding: '0 16px',
13
14
  minWidth: '50px',
14
15
  // FIXME: @zhanghan 这个只是临时的解决方案,会导致 header align right 不能真正的右对齐,需要修改 header 才能真正解决这个问题
15
16
  flexGrow: 100,
@@ -18,15 +19,15 @@ export const NavMenuRoot = styled('nav', {
18
19
  fontSize: '16px',
19
20
  }));
20
21
 
21
- // .navmenu-list
22
- export const NavMenuList = styled('ul')(() => ({
22
+ // .navmenu-root
23
+ export const NavMenuRoot = styled('ul')(() => ({
23
24
  listStyle: 'none',
24
25
  margin: 0,
25
26
  padding: 0,
26
27
  display: 'flex',
27
28
  alignItems: 'center',
28
29
  // inline 布局
29
- '&.navmenu-list--inline ': {
30
+ '&.navmenu-root--inline ': {
30
31
  flexDirection: 'column',
31
32
  alignItems: 'stretch',
32
33
  },
@@ -38,14 +39,16 @@ type NavMenuItemProps = {
38
39
  // 菜单项 .navmenu-item
39
40
  export const NavMenuItem = styled('li', {
40
41
  shouldForwardProp: (prop) => prop !== '$activeTextColor',
41
- })<NavMenuItemProps>(({ $activeTextColor }) => ({
42
+ })<NavMenuItemProps>(({ $activeTextColor, theme }) => ({
42
43
  display: 'flex',
43
44
  alignItems: 'center',
44
45
  position: 'relative',
45
- padding: '0 12px',
46
+ padding: '8px 12px',
46
47
  whiteSpace: 'nowrap',
47
48
  cursor: 'pointer',
48
- transition: 'color 0.2s ease-in-out',
49
+ transition: theme.transitions.create('color', {
50
+ duration: theme.transitions.duration.standard,
51
+ }),
49
52
  // 间距调整
50
53
  '&:first-of-type': {
51
54
  paddingLeft: 0,
@@ -98,7 +101,9 @@ export const NavMenuItem = styled('li', {
98
101
  marginLeft: '6px',
99
102
  fontSize: '14px',
100
103
  opacity: 0,
101
- transition: 'opacity 0.2s ease-in-out',
104
+ transition: theme.transitions.create('opacity', {
105
+ duration: theme.transitions.duration.standard,
106
+ }),
102
107
  },
103
108
  },
104
109
  '.navmenu-item__desc': {
@@ -136,13 +141,17 @@ export const NavMenuItem = styled('li', {
136
141
  },
137
142
  '&:hover': {
138
143
  background: '#f9f9fb',
139
- transition: 'background 0.2s ease-in-out',
144
+ transition: theme.transitions.create('background', {
145
+ duration: theme.transitions.duration.standard,
146
+ }),
140
147
  '.navmenu-item__label-arrow': {
141
148
  opacity: 1,
142
149
  },
143
150
  '.navmenu-item__desc': {
144
151
  color: '#26292e',
145
- transition: 'color 0.2s ease-in-out',
152
+ transition: theme.transitions.create('color', {
153
+ duration: theme.transitions.duration.standard,
154
+ }),
146
155
  },
147
156
  },
148
157
  '&.navmenu-item--active': {
@@ -160,24 +169,26 @@ export const NavMenuItem = styled('li', {
160
169
  }));
161
170
 
162
171
  // 包含子菜单的导航项 .navmenu-item .navmenu-sub
163
- export const NavMenuSub = styled(NavMenuItem)(() => ({
172
+ export const NavMenuSub = styled(NavMenuItem)(({ theme }) => ({
164
173
  '.navmenu-item': {
165
174
  marginLeft: 0,
175
+ overflow: 'hidden',
166
176
  },
167
177
  '& .navmenu-sub__container': {
168
- visibility: 'hidden',
178
+ pointerEvents: 'none',
169
179
  opacity: 0,
170
180
  position: 'absolute',
171
181
  top: '100%',
172
- left: '50%',
182
+ left: 0,
173
183
  zIndex: 1,
174
- transform: 'translateX(-50%)',
175
- paddingTop: '16px',
176
- transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out',
177
184
  },
178
185
  '&.navmenu-sub--opened > .navmenu-sub__container': {
179
- visibility: 'visible',
186
+ pointerEvents: 'auto',
180
187
  opacity: 1,
188
+ transition: theme.transitions.create('opacity', {
189
+ duration: theme.transitions.duration.standard,
190
+ easing: theme.transitions.easing.easeOut,
191
+ }),
181
192
  },
182
193
  // 扩展图标
183
194
  '.navmenu-sub__expand-icon': {
@@ -188,7 +199,9 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
188
199
  '& > *': {
189
200
  width: '0.8em',
190
201
  height: '0.8em',
191
- transition: 'transform 0.2s ease-in-out',
202
+ transition: theme.transitions.create('transform', {
203
+ duration: theme.transitions.duration.standard,
204
+ }),
192
205
  },
193
206
  '.navmenu-item--inline &': {
194
207
  marginLeft: 'auto',
@@ -204,7 +217,9 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
204
217
  padding: 0,
205
218
  height: 0,
206
219
  overflow: 'hidden',
207
- transition: 'height 0.2s ease-in-out',
220
+ transition: theme.transitions.create('height', {
221
+ duration: theme.transitions.duration.standard,
222
+ }),
208
223
  },
209
224
  '&.navmenu-sub--opened > .navmenu-sub__container': {
210
225
  height: 'auto',
@@ -214,10 +229,13 @@ export const NavMenuSub = styled(NavMenuItem)(() => ({
214
229
  paddingLeft: '16px',
215
230
  boxShadow: 'none',
216
231
  },
232
+ '.navmenu-item__content': {
233
+ height: '48px',
234
+ },
217
235
  },
218
236
  }));
219
237
 
220
- // 下拉子菜单 .navmenu-sub-list
238
+ // 下拉子菜单 .navmenu-sub__list
221
239
  export const NavMenuSubList = styled('ul')(() => ({
222
240
  margin: 0,
223
241
  padding: '16px',
@@ -0,0 +1,96 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+
3
+ interface SubContainerProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ children: React.ReactNode;
5
+ }
6
+
7
+ const paddingTop = 8;
8
+
9
+ // 下拉子菜单容器
10
+ export function SubContainer({ children, ...props }: SubContainerProps) {
11
+ const rootRef = useRef<HTMLDivElement>(null);
12
+ const [position, setPosition] = useState(0); // 只需要保存水平偏移量
13
+
14
+ const updatePosition = () => {
15
+ if (!rootRef.current) return;
16
+ const anchor = rootRef.current.parentElement; // 以父容器作为参照
17
+ if (!anchor) return;
18
+
19
+ const anchorRect = anchor.getBoundingClientRect();
20
+ const containerRect = rootRef.current.getBoundingClientRect();
21
+ const windowWidth = window.innerWidth;
22
+ const padding = 32;
23
+
24
+ // 1. 计算相对于父元素的水平居中位置
25
+ let left = (anchorRect.width - containerRect.width) / 2;
26
+
27
+ // 2. 检查容器在视口中的绝对位置
28
+ const absoluteLeft = anchorRect.left + left;
29
+
30
+ // 3. 调整水平位置确保不超出窗口
31
+ if (absoluteLeft < 0) {
32
+ // 如果超出左边界,向右偏移
33
+ left = left - absoluteLeft + padding;
34
+ } else if (absoluteLeft + containerRect.width > windowWidth) {
35
+ // 如果超出右边界,向左偏移
36
+ left -= absoluteLeft + containerRect.width - windowWidth + padding;
37
+ }
38
+
39
+ setPosition(left);
40
+ };
41
+
42
+ // 监听自身大小变化
43
+ useEffect(() => {
44
+ const resizeObserver = new ResizeObserver(() => {
45
+ updatePosition();
46
+ });
47
+
48
+ resizeObserver.observe(rootRef.current!);
49
+
50
+ return () => resizeObserver.disconnect();
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, []);
53
+
54
+ // 监听 anchor 的大小变化
55
+ useEffect(() => {
56
+ const anchor = rootRef.current?.parentElement;
57
+ let resizeObserver: ResizeObserver | null = null;
58
+
59
+ if (anchor) {
60
+ resizeObserver = new ResizeObserver(() => {
61
+ updatePosition();
62
+ });
63
+
64
+ resizeObserver.observe(anchor);
65
+ }
66
+
67
+ return () => resizeObserver?.disconnect();
68
+ // eslint-disable-next-line react-hooks/exhaustive-deps
69
+ }, []);
70
+
71
+ // 监听窗口大小变化
72
+ useEffect(() => {
73
+ const handleResize = () => {
74
+ updatePosition();
75
+ };
76
+
77
+ window.addEventListener('resize', handleResize);
78
+ return () => window.removeEventListener('resize', handleResize);
79
+ // eslint-disable-next-line react-hooks/exhaustive-deps
80
+ }, []);
81
+
82
+ return (
83
+ <div
84
+ ref={rootRef}
85
+ className="navmenu-sub__container"
86
+ style={{
87
+ paddingTop: `${paddingTop}px`,
88
+ transform: `translateX(${position}px)`,
89
+ }}
90
+ {...props}>
91
+ {children}
92
+ </div>
93
+ );
94
+ }
95
+
96
+ export default SubContainer;