@arcblock/ux 1.17.13 → 1.17.16

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.
@@ -0,0 +1,136 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import styled from 'styled-components';
4
+ import MenuIcon from '@material-ui/icons/Menu';
5
+ import Button from '@material-ui/core/IconButton';
6
+ import useTheme from '@material-ui/core/styles/useTheme';
7
+ import useMediaQuery from '@material-ui/core/useMediaQuery';
8
+ import Drawer from '@material-ui/core/Drawer';
9
+ import Header from './header';
10
+
11
+ /**
12
+ * ResponsiveHeader
13
+ * - 窄屏下显示 burge menu
14
+ * - 窄屏下将 children 区域显示到 menu 中
15
+ *
16
+ * 注意: 暂时不要通过 display: none 隐藏 logo, https://blog.patw.me/archives/1820/inline-svg-same-id-and-display-none-issue/
17
+ */
18
+ function ResponsiveHeader({ menu, prepend, children, ...rest }) {
19
+ const theme = useTheme();
20
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
21
+ const [drawerOpen, setDrawerOpen] = useState(false);
22
+ const _children =
23
+ typeof children === 'function'
24
+ ? children({ isMobile, closeMenu: () => setDrawerOpen(false) })
25
+ : children;
26
+ const { logo, brand, brandAddon, description } = rest;
27
+ // 如果 children 没有值, 则使用普通的 Header 组件渲染 (此时并没有什么内容需要在 menu 中显示)
28
+ if (!children) {
29
+ return <Header prepend={prepend} {...rest} />;
30
+ }
31
+ return (
32
+ <Root
33
+ prepend={
34
+ prepend || (
35
+ <Button
36
+ color="inherit"
37
+ edge="start"
38
+ className="header-menu"
39
+ onClick={() => setDrawerOpen(true)}>
40
+ <MenuIcon />
41
+ </Button>
42
+ )
43
+ }
44
+ {...rest}
45
+ logo={isMobile ? null : logo}
46
+ $theme={theme}>
47
+ {!isMobile && _children}
48
+ {isMobile && (
49
+ <Drawer
50
+ open={drawerOpen}
51
+ onClose={() => setDrawerOpen(false)}
52
+ ModalProps={{ disablePortal: true, keepMounted: true }}>
53
+ <Sidebar>
54
+ <div className="header-sidebar-head">
55
+ <div className="header-sidebar-logo">{logo}</div>
56
+ {brand && <h2>{brand}</h2>}
57
+ {description && <p className="header-sidebar-description">{description}</p>}
58
+ {brandAddon && <div className="header-sidebar-brandaddon">{brandAddon}</div>}
59
+ </div>
60
+ <div className="header-sidebar-content">{_children}</div>
61
+ </Sidebar>
62
+ </Drawer>
63
+ )}
64
+ </Root>
65
+ );
66
+ }
67
+
68
+ ResponsiveHeader.propTypes = {
69
+ ...Header.PropTypes,
70
+ // 如果是 function, 则
71
+ // - 会传入一个 isMobile 参数, isMobile 为 true 时, 表示 children 会显示在 menu 中, 可以根据 isMobile 参数调整要渲染的内容, 比如如果 isMobile 为 true 则使用 inline 模式的 NavMenu (适用于移动端)
72
+ // - 会传入一个 closeMenu 参数, 可以关闭 menu
73
+ children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
74
+ };
75
+
76
+ ResponsiveHeader.defaultProps = {
77
+ ...Header.defaultProps,
78
+ };
79
+
80
+ const Root = styled(Header)`
81
+ .header-menu {
82
+ display: none;
83
+ }
84
+ ${props => props.$theme.breakpoints.down('sm')} {
85
+ .header-menu {
86
+ display: block;
87
+ }
88
+ }
89
+ `;
90
+
91
+ /**
92
+ * Sidebar
93
+ */
94
+ const Sidebar = styled.div`
95
+ min-width: 280px;
96
+ font-size: 14px;
97
+ .header-sidebar-head {
98
+ display: flex;
99
+ flex-direction: column;
100
+ align-items: center;
101
+ padding: 24px 0;
102
+ border-bottom: 1px solid #eee;
103
+ font-size: 12px;
104
+ .header-sidebar-logo {
105
+ min-width: 44px;
106
+ height: 44px;
107
+ font-size: 44px;
108
+ > * {
109
+ width: auto;
110
+ height: 100%;
111
+ }
112
+ > a {
113
+ display: block;
114
+ }
115
+ img {
116
+ max-width: 100%;
117
+ max-height: 100%;
118
+ }
119
+ }
120
+ h2 {
121
+ margin-top: 12px;
122
+ font-size: 14px;
123
+ }
124
+ .header-sidebar-description {
125
+ margin: 2px 0 0 0;
126
+ }
127
+ .header-sidebar-brandaddon {
128
+ margin-top: 8px;
129
+ }
130
+ }
131
+ .header-sidebar-content {
132
+ padding: 16px 0;
133
+ }
134
+ `;
135
+
136
+ export default ResponsiveHeader;
@@ -1,8 +1,8 @@
1
1
  import React, { createContext, useContext, useMemo, useState, useRef } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import styled from 'styled-components';
4
3
  import clsx from 'clsx';
5
- import './style.css';
4
+ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
5
+ import { HorizontalStyle, InlineStyle } from './style';
6
6
 
7
7
  const NavMenuContext = createContext();
8
8
 
@@ -26,12 +26,13 @@ function useUniqueId(id) {
26
26
  */
27
27
  function NavMenu({
28
28
  items,
29
- orientation,
29
+ mode,
30
30
  children,
31
31
  defaultActiveId,
32
32
  textColor,
33
33
  activeTextColor,
34
34
  bgColor,
35
+ onSelected,
35
36
  ...rest
36
37
  }) {
37
38
  // eslint-disable-next-line no-param-reassign
@@ -43,8 +44,10 @@ function NavMenu({
43
44
  const contextValue = useMemo(() => {
44
45
  return {
45
46
  ...state,
47
+ mode,
46
48
  activate: id => {
47
49
  setState(prev => ({ ...prev, activeId: id }));
50
+ onSelected?.(id);
48
51
  },
49
52
  open: id => {
50
53
  setState(prev => ({ ...prev, openedIds: [...prev.openedIds, id] }));
@@ -54,7 +57,7 @@ function NavMenu({
54
57
  },
55
58
  };
56
59
  }, [state]);
57
- const classes = clsx('navmenu', `navmenu--${orientation}`, rest.className);
60
+ const classes = clsx('navmenu', `navmenu--${mode}`, rest.className);
58
61
  const renderItem = (item, index) => {
59
62
  if (item.children) {
60
63
  return (
@@ -68,67 +71,44 @@ function NavMenu({
68
71
  <Item key={index} id={item.id} icon={item.icon} label={item.label} active={item.active} />
69
72
  );
70
73
  };
74
+ const StyledRoot = mode === 'inline' ? InlineStyle : HorizontalStyle;
71
75
  return (
72
76
  <NavMenuContext.Provider value={contextValue}>
73
- <Root
77
+ <StyledRoot
74
78
  {...rest}
75
79
  className={classes}
76
80
  $textColor={textColor}
77
81
  $activeTextColor={activeTextColor}
78
82
  $bgColor={bgColor}>
79
83
  <ul className="navmenu-root">{items ? items.map(renderItem) : children}</ul>
80
- </Root>
84
+ </StyledRoot>
81
85
  </NavMenuContext.Provider>
82
86
  );
83
87
  }
84
88
 
85
89
  NavMenu.propTypes = {
86
90
  items: PropTypes.array,
87
- // 默认水平方向布局
88
- orientation: PropTypes.oneOf(['horizontal', 'vertical']),
91
+ // 默认水平方向布局,
92
+ // inline 模式: 垂直布局, 且通过 click 事件来收缩/伸展子菜单, 适用于移动端
93
+ mode: PropTypes.oneOf(['horizontal', 'vertical', 'inline']),
89
94
  children: PropTypes.array,
90
95
  defaultActiveId: PropTypes.string,
91
96
  textColor: PropTypes.string,
92
97
  activeTextColor: PropTypes.string,
93
98
  bgColor: PropTypes.string,
99
+ onSelected: PropTypes.func,
94
100
  };
95
101
  NavMenu.defaultProps = {
96
102
  items: null,
97
- orientation: 'horizontal',
103
+ mode: 'horizontal',
98
104
  children: null,
99
105
  defaultActiveId: null,
100
106
  textColor: '#9397a1',
101
107
  activeTextColor: '#25292f',
102
108
  bgColor: '#fff',
109
+ onSelected: null,
103
110
  };
104
111
 
105
- const Root = styled.nav`
106
- --text-color: ${props => props.$textColor};
107
- --active-text-color: ${props => props.$activeTextColor};
108
- --bg-color: ${props => props.$bgColor};
109
-
110
- padding: 8px 16px;
111
- background-color: var(--bg-color);
112
- font-size: 14px;
113
- ul {
114
- list-style: none;
115
- margin: 0;
116
- padding: 0;
117
- }
118
- .navmenu-item,
119
- .navmenu-item a,
120
- .navmenu-sub {
121
- color: var(--text-color);
122
- }
123
- .navmenu-item--active,
124
- .navmenu-item:hover,
125
- .navmenu-item:hover a,
126
- .navmenu-item--active a,
127
- .navmenu-sub--opened {
128
- color: var(--active-text-color);
129
- }
130
- `;
131
-
132
112
  /**
133
113
  * Item
134
114
  */
@@ -146,10 +126,11 @@ function Item({ id: _id, icon, label, active, onClick, ...rest }) {
146
126
  activate(id);
147
127
  };
148
128
  return (
149
- <ItemRoot {...rest} className={classes} onClick={handleClick}>
129
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
130
+ <li {...rest} className={classes} onClick={handleClick}>
150
131
  {icon && <span className="navmenu-item-icon">{icon}</span>}
151
- <span className="navmenu-item-content">{label}</span>
152
- </ItemRoot>
132
+ <span className="navmenu-item-label">{label}</span>
133
+ </li>
153
134
  );
154
135
  }
155
136
 
@@ -171,47 +152,39 @@ Item.defaultProps = {
171
152
  onClick: null,
172
153
  };
173
154
 
174
- const ItemRoot = styled.li`
175
- position: relative;
176
- cursor: pointer;
177
- a {
178
- text-decoration: none;
179
- white-space: nowrap;
180
- }
181
- a::before {
182
- position: absolute;
183
- top: 0;
184
- right: 0;
185
- bottom: 0;
186
- left: 0;
187
- background-color: transparent;
188
- content: '';
189
- }
190
- `;
191
-
192
155
  /**
193
156
  * Sub
194
157
  */
195
- function Sub({ id: _id, icon, label, children, ...rest }) {
158
+ function Sub({ id: _id, icon, label, children, expandIcon, ...rest }) {
196
159
  const id = useUniqueId(_id);
197
- const { openedIds, open, close } = useContext(NavMenuContext);
198
- const classes = clsx(
199
- 'navmenu-sub',
200
- { 'navmenu-sub--opened': openedIds.includes(id) },
201
- rest.className
202
- );
160
+ const { openedIds, open, close, mode } = useContext(NavMenuContext);
161
+ const isOpen = openedIds.includes(id);
162
+ const classes = clsx('navmenu-sub', { 'navmenu-sub--opened': isOpen }, rest.className);
163
+ const isInlineMode = mode === 'inline';
164
+ // inline mode 时使用 click 事件控制收缩/伸展子菜单
165
+ const props = isInlineMode
166
+ ? { onClick: () => (openedIds.includes(id) ? close(id) : open(id)) }
167
+ : {
168
+ onMouseEnter: () => open(id),
169
+ onMouseLeave: () => close(id),
170
+ };
171
+ // inline mode, 避免点击子菜单项时触发父菜单的 open/close
172
+ const containerProps = isInlineMode
173
+ ? {
174
+ onClick: e => e.stopPropagation(),
175
+ }
176
+ : {};
203
177
  return (
204
- <SubRoot
205
- {...rest}
206
- className={classes}
207
- onMouseEnter={() => open(id)}
208
- onMouseLeave={() => close(id)}>
178
+ <li {...rest} className={classes} {...props}>
209
179
  {icon && <span className="navmenu-sub-icon">{icon}</span>}
210
- <span className="navmenu-sub-content">{label}</span>
211
- <div className="navmenu-sub-container">
180
+ <span className="navmenu-sub-label">{label}</span>
181
+ {expandIcon && (
182
+ <span className="navmenu-sub-expand-icon">{expandIcon?.({ isOpen }) || expandIcon}</span>
183
+ )}
184
+ <div className="navmenu-sub-container" {...containerProps}>
212
185
  <ul className="navmenu-sub-list">{filterItems(children)}</ul>
213
186
  </div>
214
- </SubRoot>
187
+ </li>
215
188
  );
216
189
  }
217
190
 
@@ -220,26 +193,24 @@ Sub.propTypes = {
220
193
  icon: PropTypes.element,
221
194
  label: PropTypes.node.isRequired,
222
195
  children: PropTypes.array.isRequired,
196
+ expandIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
223
197
  };
224
198
 
225
199
  Sub.defaultProps = {
226
200
  id: null,
227
201
  icon: null,
202
+ // eslint-disable-next-line react/prop-types
203
+ expandIcon: ({ isOpen }) => {
204
+ return (
205
+ <ExpandMoreIcon
206
+ style={{
207
+ transform: `rotate(${isOpen ? 180 : 0}deg)`,
208
+ }}
209
+ />
210
+ );
211
+ },
228
212
  };
229
213
 
230
- const SubRoot = styled.li`
231
- position: relative;
232
- cursor: pointer;
233
- .navmenu-sub-container {
234
- display: none;
235
- position: absolute;
236
- }
237
- &.navmenu-sub--opened > .navmenu-sub-container {
238
- display: block;
239
- top: 100%;
240
- }
241
- `;
242
-
243
214
  NavMenu.Item = Item;
244
215
  NavMenu.Sub = Sub;
245
216
 
@@ -0,0 +1,182 @@
1
+ import styled from 'styled-components';
2
+
3
+ const NavMenuBase = styled.nav`
4
+ --text-color: ${props => props.$textColor};
5
+ --active-text-color: ${props => props.$activeTextColor};
6
+ --bg-color: ${props => props.$bgColor};
7
+
8
+ background-color: var(--bg-color);
9
+ font-size: 14px;
10
+ ul {
11
+ list-style: none;
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+ .navmenu-item,
16
+ .navmenu-sub {
17
+ display: flex;
18
+ align-items: center;
19
+ }
20
+ // active/hover
21
+ .navmenu-item,
22
+ .navmenu-item a,
23
+ .navmenu-sub {
24
+ color: var(--text-color);
25
+ }
26
+ .navmenu-item--active,
27
+ .navmenu-item:hover,
28
+ .navmenu-item:hover a,
29
+ .navmenu-item--active a,
30
+ .navmenu-sub--opened {
31
+ color: var(--active-text-color);
32
+ }
33
+
34
+ .navmenu-item {
35
+ position: relative;
36
+ cursor: pointer;
37
+ a {
38
+ text-decoration: none;
39
+ white-space: nowrap;
40
+ }
41
+ a::before {
42
+ position: absolute;
43
+ top: 0;
44
+ right: 0;
45
+ bottom: 0;
46
+ left: 0;
47
+ background-color: transparent;
48
+ content: '';
49
+ }
50
+ }
51
+
52
+ .navmenu-sub {
53
+ position: relative;
54
+ cursor: pointer;
55
+ }
56
+ // icon & expand icon
57
+ .navmenu-item-icon,
58
+ .navmenu-sub-icon,
59
+ .navmenu-sub-expand-icon {
60
+ display: flex;
61
+ line-height: 1;
62
+ }
63
+ .navmenu-item-icon,
64
+ .navmenu-sub-icon {
65
+ margin-right: 4px;
66
+ }
67
+ .navmenu-item-icon > *,
68
+ .navmenu-sub-icon > * {
69
+ font-size: 1.5em;
70
+ }
71
+ .navmenu-sub-expand-icon {
72
+ margin-left: 8px;
73
+ > * {
74
+ width: 0.8em;
75
+ height: 0.8em;
76
+ transition: transform 0.2s ease-in-out;
77
+ }
78
+ }
79
+ `;
80
+
81
+ export const HorizontalStyle = styled(NavMenuBase)`
82
+ padding: 8px 16px;
83
+ .navmenu-root {
84
+ display: flex;
85
+ align-items: center;
86
+ }
87
+ // 顶级菜单间隔
88
+ .navmenu-root > .navmenu-item,
89
+ .navmenu-root > .navmenu-sub {
90
+ margin-left: 24px;
91
+ }
92
+ .navmenu-root > .navmenu-item:first-child,
93
+ .navmenu-root > .navmenu-sub:first-child {
94
+ margin-left: 0;
95
+ }
96
+
97
+ /* 子级列表 */
98
+ .navmenu-sub-container {
99
+ display: none;
100
+ position: absolute;
101
+ top: 100%;
102
+ }
103
+ .navmenu-sub-list {
104
+ padding: 16px;
105
+ border-radius: 4px;
106
+ background: #fff;
107
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
108
+ .navmenu-item + .navmenu-item {
109
+ margin-top: 8px;
110
+ }
111
+ }
112
+ // 二级 sub menu
113
+ .navmenu-root > .navmenu-sub {
114
+ > .navmenu-sub-container {
115
+ left: 50%;
116
+ transform: translateX(-50%);
117
+ padding-top: 16px;
118
+ }
119
+ &.navmenu-sub--opened > .navmenu-sub-container {
120
+ display: block;
121
+ }
122
+ }
123
+ `;
124
+
125
+ /* inline mode */
126
+ export const InlineStyle = styled(NavMenuBase)`
127
+ .navmenu-root {
128
+ display: flex;
129
+ flex-direction: column;
130
+ align-items: stretch;
131
+ }
132
+ .navmenu-item,
133
+ .navmenu-sub {
134
+ padding: 0 16px;
135
+ }
136
+ // 顶级
137
+ .navmenu-root > .navmenu-item,
138
+ .navmenu-root > .navmenu-sub {
139
+ height: 48px;
140
+ line-height: 48px;
141
+ border-bottom: 1px solid #eee;
142
+ }
143
+ // icon
144
+ .navmenu-item-icon,
145
+ .navmenu-sub-icon {
146
+ width: 32px;
147
+ margin: 0;
148
+ }
149
+ .navmenu-sub-expand-icon {
150
+ margin-left: auto;
151
+ }
152
+ /* 子级列表 */
153
+ .navmenu-sub-container {
154
+ display: none;
155
+ position: absolute;
156
+ top: 100%;
157
+ left: 0;
158
+ right: 0;
159
+ .navmenu-item,
160
+ .navmenu-sub {
161
+ line-height: 24px;
162
+ }
163
+ }
164
+ .navmenu-sub-list {
165
+ padding-left: 16px;
166
+ .navmenu-item,
167
+ .navmenu-sub {
168
+ padding-left: 32px;
169
+ margin-top: 8px;
170
+ font-size: 13px;
171
+ }
172
+ }
173
+ // 二级 menu
174
+ .navmenu-root > .navmenu-sub {
175
+ &.navmenu-sub--opened {
176
+ background: #eee;
177
+ }
178
+ }
179
+ .navmenu-sub--opened > .navmenu-sub-container {
180
+ display: block;
181
+ }
182
+ `;
@@ -1,48 +0,0 @@
1
- .navmenu .navmenu-root {
2
- display: flex;
3
- align-items: center;
4
- }
5
- .navmenu-item,
6
- .navmenu-sub {
7
- display: flex;
8
- align-items: center;
9
- }
10
- .navmenu-root > .navmenu-item,
11
- .navmenu-root > .navmenu-sub {
12
- margin-left: 24px;
13
- }
14
- .navmenu-root > .navmenu-item:first-child,
15
- .navmenu-root > .navmenu-sub:first-child {
16
- margin-left: 0;
17
- }
18
- .navmenu-item-icon,
19
- .navmenu-sub-icon {
20
- display: flex;
21
- justify-content: center;
22
- align-items: center;
23
- height: 20px;
24
- line-height: 1;
25
- }
26
- .navmenu-item-icon > *,
27
- .navmenu-sub-icon > * {
28
- width: auto !important;
29
- height: 100% !important;
30
- }
31
- .navmenu-item-icon {
32
- margin-right: 4px;
33
- }
34
-
35
- /* 二级列表 */
36
- .navmenu-root > .navmenu-sub > .navmenu-sub-container {
37
- left: 50%;
38
- transform: translateX(-50%);
39
- padding-top: 16px;
40
- }
41
- .navmenu-root .navmenu-sub-list {
42
- padding: 16px;
43
- border-radius: 4px;
44
- background: #fff;
45
- }
46
- .navmenu-sub-list > .navmenu-item + .navmenu-item {
47
- margin-top: 8px;
48
- }
@@ -1,48 +0,0 @@
1
- .navmenu .navmenu-root {
2
- display: flex;
3
- align-items: center;
4
- }
5
- .navmenu-item,
6
- .navmenu-sub {
7
- display: flex;
8
- align-items: center;
9
- }
10
- .navmenu-root > .navmenu-item,
11
- .navmenu-root > .navmenu-sub {
12
- margin-left: 24px;
13
- }
14
- .navmenu-root > .navmenu-item:first-child,
15
- .navmenu-root > .navmenu-sub:first-child {
16
- margin-left: 0;
17
- }
18
- .navmenu-item-icon,
19
- .navmenu-sub-icon {
20
- display: flex;
21
- justify-content: center;
22
- align-items: center;
23
- height: 20px;
24
- line-height: 1;
25
- }
26
- .navmenu-item-icon > *,
27
- .navmenu-sub-icon > * {
28
- width: auto !important;
29
- height: 100% !important;
30
- }
31
- .navmenu-item-icon {
32
- margin-right: 4px;
33
- }
34
-
35
- /* 二级列表 */
36
- .navmenu-root > .navmenu-sub > .navmenu-sub-container {
37
- left: 50%;
38
- transform: translateX(-50%);
39
- padding-top: 16px;
40
- }
41
- .navmenu-root .navmenu-sub-list {
42
- padding: 16px;
43
- border-radius: 4px;
44
- background: #fff;
45
- }
46
- .navmenu-sub-list > .navmenu-item + .navmenu-item {
47
- margin-top: 8px;
48
- }