@stack-spot/portal-layout 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/Layout.d.ts +2 -2
- package/dist/Layout.js +1 -1
- package/dist/LayoutOverlayManager.js +6 -6
- package/dist/LayoutOverlayManager.js.map +1 -1
- package/dist/components/Dialog.d.ts +1 -1
- package/dist/components/Dialog.js +1 -1
- package/dist/components/Header.d.ts +1 -1
- package/dist/components/Header.js +1 -1
- package/dist/components/OverlayContent.d.ts +1 -1
- package/dist/components/OverlayContent.js +20 -20
- package/dist/components/PortalSwitcher.d.ts +1 -1
- package/dist/components/PortalSwitcher.js +54 -54
- package/dist/components/Toaster.d.ts +2 -2
- package/dist/components/Toaster.js +1 -1
- package/dist/components/UserMenu.d.ts +1 -1
- package/dist/components/UserMenu.d.ts.map +1 -1
- package/dist/components/UserMenu.js +44 -42
- package/dist/components/UserMenu.js.map +1 -1
- package/dist/components/error/ErrorBoundary.d.ts +1 -1
- package/dist/components/error/ErrorBoundary.js +1 -1
- package/dist/components/error/SilentErrorBoundary.d.ts +1 -1
- package/dist/components/error/SilentErrorBoundary.js +1 -1
- package/dist/components/menu/MenuContent.d.ts +2 -2
- package/dist/components/menu/MenuContent.js +123 -123
- package/dist/components/menu/MenuContent.js.map +1 -1
- package/dist/components/menu/MenuSections.d.ts +1 -1
- package/dist/components/menu/MenuSections.js +1 -1
- package/dist/components/menu/MenuSections.js.map +1 -1
- package/dist/components/menu/PageSelector.d.ts +1 -1
- package/dist/components/menu/PageSelector.js +69 -69
- package/dist/components/menu/PageSelector.js.map +1 -1
- package/dist/components/tour/PortalSwitcherStep.js +1 -1
- package/dist/components/user-menu-manager.d.ts +13 -0
- package/dist/components/user-menu-manager.d.ts.map +1 -0
- package/dist/components/user-menu-manager.js +36 -0
- package/dist/components/user-menu-manager.js.map +1 -0
- package/dist/layout.css +477 -477
- package/dist/toaster.js +1 -1
- package/package.json +9 -6
- package/readme.md +146 -146
- package/src/Layout.tsx +171 -171
- package/src/LayoutOverlayManager.tsx +464 -464
- package/src/components/Dialog.tsx +140 -140
- package/src/components/Header.tsx +62 -62
- package/src/components/OverlayContent.tsx +80 -80
- package/src/components/PortalSwitcher.tsx +161 -161
- package/src/components/Toaster.tsx +95 -95
- package/src/components/UserMenu.tsx +127 -124
- package/src/components/error/ErrorBoundary.tsx +47 -47
- package/src/components/error/ErrorManager.ts +47 -47
- package/src/components/error/SilentErrorBoundary.tsx +64 -64
- package/src/components/menu/MenuContent.tsx +270 -270
- package/src/components/menu/MenuSections.tsx +320 -320
- package/src/components/menu/PageSelector.tsx +164 -164
- package/src/components/menu/constants.ts +2 -2
- package/src/components/menu/types.ts +205 -205
- package/src/components/tour/PortalSwitcherStep.tsx +39 -39
- package/src/components/types.ts +1 -1
- package/src/components/user-menu-manager.ts +31 -0
- package/src/dictionary.ts +28 -28
- package/src/elements.ts +30 -30
- package/src/errors.ts +11 -11
- package/src/index.ts +14 -14
- package/src/layout.css +477 -477
- package/src/toaster.tsx +153 -153
- package/src/utils.ts +29 -29
- package/tsconfig.json +8 -8
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
import { Flex, IconBox, Text } from '@citric/core'
|
|
2
|
-
import { ArrowLeft, ChevronDown } from '@citric/icons'
|
|
3
|
-
import { LoadingCircular } from '@citric/ui'
|
|
4
|
-
import { useCheckTextOverflow } from '@stack-spot/portal-components'
|
|
5
|
-
import { useAnchorTag } from '@stack-spot/portal-components/anchor'
|
|
6
|
-
import { listToClass, theme } from '@stack-spot/portal-theme'
|
|
7
|
-
import { useMemo, useState } from 'react'
|
|
8
|
-
import { styled } from 'styled-components'
|
|
9
|
-
import { hideOverlayImmediately } from './MenuSections'
|
|
10
|
-
import { PageSelector } from './PageSelector'
|
|
11
|
-
import { MENU_CONTENT_ITEM_PADDING as ITEM_PADDING, MENU_CONTENT_PADDING as PADDING } from './constants'
|
|
12
|
-
import { ItemGroup, MenuAction, MenuItem, MenuSectionContent } from './types'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The list (<ul>) used for grouping items in a menu.
|
|
16
|
-
*/
|
|
17
|
-
export const MenuGroup = styled.ul`
|
|
18
|
-
padding: 0 0 0 16px;
|
|
19
|
-
display: flex;
|
|
20
|
-
flex-direction: column;
|
|
21
|
-
visibility: hidden;
|
|
22
|
-
transition: visibility 0s 0.3s;
|
|
23
|
-
|
|
24
|
-
&.no-indentation {
|
|
25
|
-
padding: 0;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.item-row {
|
|
29
|
-
display: flex;
|
|
30
|
-
flex-direction: row;
|
|
31
|
-
gap: 8px;
|
|
32
|
-
align-items: center;
|
|
33
|
-
|
|
34
|
-
&.root {
|
|
35
|
-
padding: 0 16px;
|
|
36
|
-
margin-top: 16px;
|
|
37
|
-
border-radius: 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.label {
|
|
41
|
-
flex: 1;
|
|
42
|
-
&.hidden, &.ellipsis {
|
|
43
|
-
white-space: nowrap;
|
|
44
|
-
overflow: hidden;
|
|
45
|
-
}
|
|
46
|
-
&.ellipsis {
|
|
47
|
-
text-overflow: ellipsis;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
li a {
|
|
53
|
-
position: relative;
|
|
54
|
-
height: 0;
|
|
55
|
-
overflow: hidden;
|
|
56
|
-
transition: height 0.3s, background-color 0.2s;
|
|
57
|
-
margin-left: ${PADDING - ITEM_PADDING}px;
|
|
58
|
-
padding-left: ${ITEM_PADDING}px;
|
|
59
|
-
|
|
60
|
-
&:hover {
|
|
61
|
-
background-color: ${theme.color.light['500']};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
&.action {
|
|
65
|
-
&:before {
|
|
66
|
-
content: '';
|
|
67
|
-
position: absolute;
|
|
68
|
-
left: 2px;
|
|
69
|
-
width: 2px;
|
|
70
|
-
height: 0;
|
|
71
|
-
background: inherit;
|
|
72
|
-
transition: height 0.2s;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
&.active {
|
|
76
|
-
|
|
77
|
-
&:hover {
|
|
78
|
-
background-color: transparent;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
&:before {
|
|
82
|
-
background: ${theme.color.primary['500']};
|
|
83
|
-
height: 24px;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
&:not(.active):hover:before {
|
|
88
|
-
background: ${theme.color.light.contrastText};
|
|
89
|
-
height: 24px;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.chevron {
|
|
94
|
-
transition: transform 0.3s;
|
|
95
|
-
&:not(.open) {
|
|
96
|
-
transform: rotate(-90deg);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.item-row-title {
|
|
101
|
-
opacity: 0.7;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
&.open {
|
|
106
|
-
visibility: visible;
|
|
107
|
-
transition: unset;
|
|
108
|
-
& > li > a {
|
|
109
|
-
height: 40px;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
&:not(.open) &.open > li > a {
|
|
114
|
-
height: 0;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
&.root {
|
|
118
|
-
margin-bottom: ${PADDING}px;
|
|
119
|
-
|
|
120
|
-
& > li {
|
|
121
|
-
.group-title {
|
|
122
|
-
margin-left: ${PADDING}px;
|
|
123
|
-
margin-bottom: 5px;
|
|
124
|
-
margin-top: 40px;
|
|
125
|
-
display: block;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
&:first-child {
|
|
129
|
-
.group-title {
|
|
130
|
-
margin-top: 0;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
`
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* The header (<header>) for a group of items in a menu. Contains the title of the group.
|
|
139
|
-
*/
|
|
140
|
-
export const Title = styled.header`
|
|
141
|
-
display: flex;
|
|
142
|
-
flex-direction: column;
|
|
143
|
-
margin: ${PADDING}px 0 24px ${PADDING}px;
|
|
144
|
-
`
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* A menu item that performs an action.
|
|
148
|
-
* @param props the props for the component {@link MenuAction}.
|
|
149
|
-
*/
|
|
150
|
-
export const ActionItem = ({ label, onClick, href, active, icon, badge, overflow = 'wrap' }: MenuAction) => {
|
|
151
|
-
const Link = useAnchorTag()
|
|
152
|
-
const { ref, overflow: textOverflow } = useCheckTextOverflow()
|
|
153
|
-
const isTextLabel = typeof label === 'string'
|
|
154
|
-
return (
|
|
155
|
-
<Link
|
|
156
|
-
href={href}
|
|
157
|
-
onClick={() => {
|
|
158
|
-
if (active) return
|
|
159
|
-
if (onClick) onClick()
|
|
160
|
-
hideOverlayImmediately()
|
|
161
|
-
}}
|
|
162
|
-
className={listToClass(['action', 'item-row', active ? 'active' : undefined])}
|
|
163
|
-
{...(active ? { 'aria-current': 'page' } : undefined)}
|
|
164
|
-
{...(!href ? { 'tabIndex': 0 } : undefined)}
|
|
165
|
-
>
|
|
166
|
-
{icon}
|
|
167
|
-
{isTextLabel ?
|
|
168
|
-
<Text ref={ref} appearance="body2" className={`label ${overflow}`} title={textOverflow ? label : ''}>{label}</Text> :
|
|
169
|
-
label.element}
|
|
170
|
-
{badge}
|
|
171
|
-
</Link>
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* A menu item that is actually a subgroup and can be collapsed/expanded.
|
|
177
|
-
* @param props the props for the component {@link ItemGroup} & { root: boolean }. Pass root=true to style this group as a root group.
|
|
178
|
-
* Root groups have slightly different visuals.
|
|
179
|
-
*/
|
|
180
|
-
const CollapsibleGroupItem = ({ label, open: initiallyOpened, children, icon, badge, root, overflow = 'wrap' }:
|
|
181
|
-
ItemGroup & { root?: boolean }) => {
|
|
182
|
-
const [open, setOpen] = useState(initiallyOpened ?? children?.some(c => 'active' in c && c.active) ?? false)
|
|
183
|
-
const items = useMemo(() => children?.filter(i => !i.hidden).map(renderOption), [children])
|
|
184
|
-
const id = `menuGroup${label}`
|
|
185
|
-
|
|
186
|
-
return (
|
|
187
|
-
<>
|
|
188
|
-
<a
|
|
189
|
-
onClick={() => setOpen(!open)}
|
|
190
|
-
onKeyDown={e => e.key === 'Enter' && setOpen(!open)}
|
|
191
|
-
className={listToClass(['item-row', root && 'root'])}
|
|
192
|
-
tabIndex={0}
|
|
193
|
-
aria-controls={id}
|
|
194
|
-
aria-expanded={open}
|
|
195
|
-
>
|
|
196
|
-
{icon}
|
|
197
|
-
<Text appearance={root ? 'overheader2' : 'body2'}
|
|
198
|
-
colorScheme="light.contrastText"
|
|
199
|
-
className={`label ${overflow} ${root ? 'item-row-title' : ''}`}>
|
|
200
|
-
{label}
|
|
201
|
-
</Text>
|
|
202
|
-
{badge}
|
|
203
|
-
<IconBox sx={{ mr: root ? undefined : '5' }}>
|
|
204
|
-
<ChevronDown className={listToClass(['chevron', open ? 'open' : ''])} />
|
|
205
|
-
</IconBox>
|
|
206
|
-
</a>
|
|
207
|
-
<MenuGroup id={id}
|
|
208
|
-
className={`${open ? 'open' : ''} ${root ? 'no-indentation' : ''}`}
|
|
209
|
-
aria-hidden={!open}>{items}</MenuGroup>
|
|
210
|
-
</>
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function renderOption({ root, ...option }: MenuItem & { root?: boolean }) {
|
|
215
|
-
const labelText = typeof option.label === 'string' ? option.label : option.label.id
|
|
216
|
-
return (
|
|
217
|
-
<li key={labelText} role="menuitem" aria-selected={'children' in option ? undefined : option.active}>
|
|
218
|
-
{'children' in option ? <CollapsibleGroupItem {...option} root={root} open={option.open ?? root} /> : <ActionItem {...option} />}
|
|
219
|
-
</li >
|
|
220
|
-
)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Renders a menu-content interface.
|
|
225
|
-
*
|
|
226
|
-
* Considering the Stackspot UI, this is the "menu content", not the "menu sections", i.e. it's the second menu from left to right, the
|
|
227
|
-
* one that changes according to section selected.
|
|
228
|
-
* @param props the props for the component {@link MenuSectionContent}.
|
|
229
|
-
*/
|
|
230
|
-
export const MenuContent = ({ pageSelector, goBack, title, subtitle, afterTitle, options = [], loading, error }: MenuSectionContent) => {
|
|
231
|
-
const items = useMemo(() => options.filter(o => !o.hidden).map(o => renderOption({ ...o, root: true })), [options])
|
|
232
|
-
|
|
233
|
-
const Link = useAnchorTag()
|
|
234
|
-
|
|
235
|
-
function renderContent() {
|
|
236
|
-
if (loading) {
|
|
237
|
-
return (
|
|
238
|
-
<Flex justifyContent="center" alignItems="center" flex={1} sx={{ padding: '40px' }}>
|
|
239
|
-
<LoadingCircular />
|
|
240
|
-
</Flex>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
if (error) return <Text colorScheme="danger">{error}</Text>
|
|
244
|
-
return <MenuGroup className="open root no-indentation">{items}</MenuGroup>
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return (
|
|
248
|
-
<>
|
|
249
|
-
{goBack && (
|
|
250
|
-
<Link href={goBack.href} onClick={goBack.onClick} className="goBackLink">
|
|
251
|
-
<IconBox colorIcon="inverse.500" size="sm">
|
|
252
|
-
<ArrowLeft />
|
|
253
|
-
</IconBox>
|
|
254
|
-
{typeof goBack?.label === 'string' ?
|
|
255
|
-
<Text appearance="body2" nowrapEllipsis>{goBack.label}</Text> :
|
|
256
|
-
goBack.label.element}
|
|
257
|
-
</Link>
|
|
258
|
-
)}
|
|
259
|
-
{title && (
|
|
260
|
-
<Title>
|
|
261
|
-
<Text appearance="overheader1" colorScheme="primary" sx={{ fontSize: '0.75rem', mt: 2, mb: 2 }}>{title}</Text>
|
|
262
|
-
{subtitle && <Text appearance="h5">{subtitle}</Text>}
|
|
263
|
-
</Title>
|
|
264
|
-
)}
|
|
265
|
-
{afterTitle}
|
|
266
|
-
{pageSelector && <PageSelector {...pageSelector} />}
|
|
267
|
-
{renderContent()}
|
|
268
|
-
</>
|
|
269
|
-
)
|
|
270
|
-
}
|
|
1
|
+
import { Flex, IconBox, Text } from '@citric/core'
|
|
2
|
+
import { ArrowLeft, ChevronDown } from '@citric/icons'
|
|
3
|
+
import { LoadingCircular } from '@citric/ui'
|
|
4
|
+
import { useCheckTextOverflow } from '@stack-spot/portal-components'
|
|
5
|
+
import { useAnchorTag } from '@stack-spot/portal-components/anchor'
|
|
6
|
+
import { listToClass, theme } from '@stack-spot/portal-theme'
|
|
7
|
+
import { useMemo, useState } from 'react'
|
|
8
|
+
import { styled } from 'styled-components'
|
|
9
|
+
import { hideOverlayImmediately } from './MenuSections'
|
|
10
|
+
import { PageSelector } from './PageSelector'
|
|
11
|
+
import { MENU_CONTENT_ITEM_PADDING as ITEM_PADDING, MENU_CONTENT_PADDING as PADDING } from './constants'
|
|
12
|
+
import { ItemGroup, MenuAction, MenuItem, MenuSectionContent } from './types'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The list (<ul>) used for grouping items in a menu.
|
|
16
|
+
*/
|
|
17
|
+
export const MenuGroup = styled.ul`
|
|
18
|
+
padding: 0 0 0 16px;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
visibility: hidden;
|
|
22
|
+
transition: visibility 0s 0.3s;
|
|
23
|
+
|
|
24
|
+
&.no-indentation {
|
|
25
|
+
padding: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.item-row {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: row;
|
|
31
|
+
gap: 8px;
|
|
32
|
+
align-items: center;
|
|
33
|
+
|
|
34
|
+
&.root {
|
|
35
|
+
padding: 0 16px;
|
|
36
|
+
margin-top: 16px;
|
|
37
|
+
border-radius: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.label {
|
|
41
|
+
flex: 1;
|
|
42
|
+
&.hidden, &.ellipsis {
|
|
43
|
+
white-space: nowrap;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
}
|
|
46
|
+
&.ellipsis {
|
|
47
|
+
text-overflow: ellipsis;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
li a {
|
|
53
|
+
position: relative;
|
|
54
|
+
height: 0;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
transition: height 0.3s, background-color 0.2s;
|
|
57
|
+
margin-left: ${PADDING - ITEM_PADDING}px;
|
|
58
|
+
padding-left: ${ITEM_PADDING}px;
|
|
59
|
+
|
|
60
|
+
&:hover {
|
|
61
|
+
background-color: ${theme.color.light['500']};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&.action {
|
|
65
|
+
&:before {
|
|
66
|
+
content: '';
|
|
67
|
+
position: absolute;
|
|
68
|
+
left: 2px;
|
|
69
|
+
width: 2px;
|
|
70
|
+
height: 0;
|
|
71
|
+
background: inherit;
|
|
72
|
+
transition: height 0.2s;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&.active {
|
|
76
|
+
|
|
77
|
+
&:hover {
|
|
78
|
+
background-color: transparent;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
&:before {
|
|
82
|
+
background: ${theme.color.primary['500']};
|
|
83
|
+
height: 24px;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&:not(.active):hover:before {
|
|
88
|
+
background: ${theme.color.light.contrastText};
|
|
89
|
+
height: 24px;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.chevron {
|
|
94
|
+
transition: transform 0.3s;
|
|
95
|
+
&:not(.open) {
|
|
96
|
+
transform: rotate(-90deg);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.item-row-title {
|
|
101
|
+
opacity: 0.7;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&.open {
|
|
106
|
+
visibility: visible;
|
|
107
|
+
transition: unset;
|
|
108
|
+
& > li > a {
|
|
109
|
+
height: 40px;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&:not(.open) &.open > li > a {
|
|
114
|
+
height: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
&.root {
|
|
118
|
+
margin-bottom: ${PADDING}px;
|
|
119
|
+
|
|
120
|
+
& > li {
|
|
121
|
+
.group-title {
|
|
122
|
+
margin-left: ${PADDING}px;
|
|
123
|
+
margin-bottom: 5px;
|
|
124
|
+
margin-top: 40px;
|
|
125
|
+
display: block;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&:first-child {
|
|
129
|
+
.group-title {
|
|
130
|
+
margin-top: 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
`
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The header (<header>) for a group of items in a menu. Contains the title of the group.
|
|
139
|
+
*/
|
|
140
|
+
export const Title = styled.header`
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-direction: column;
|
|
143
|
+
margin: ${PADDING}px 0 24px ${PADDING}px;
|
|
144
|
+
`
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A menu item that performs an action.
|
|
148
|
+
* @param props the props for the component {@link MenuAction}.
|
|
149
|
+
*/
|
|
150
|
+
export const ActionItem = ({ label, onClick, href, active, icon, badge, overflow = 'wrap' }: MenuAction) => {
|
|
151
|
+
const Link = useAnchorTag()
|
|
152
|
+
const { ref, overflow: textOverflow } = useCheckTextOverflow()
|
|
153
|
+
const isTextLabel = typeof label === 'string'
|
|
154
|
+
return (
|
|
155
|
+
<Link
|
|
156
|
+
href={href}
|
|
157
|
+
onClick={() => {
|
|
158
|
+
if (active) return
|
|
159
|
+
if (onClick) onClick()
|
|
160
|
+
hideOverlayImmediately()
|
|
161
|
+
}}
|
|
162
|
+
className={listToClass(['action', 'item-row', active ? 'active' : undefined])}
|
|
163
|
+
{...(active ? { 'aria-current': 'page' } : undefined)}
|
|
164
|
+
{...(!href ? { 'tabIndex': 0 } : undefined)}
|
|
165
|
+
>
|
|
166
|
+
{icon}
|
|
167
|
+
{isTextLabel ?
|
|
168
|
+
<Text ref={ref} appearance="body2" className={`label ${overflow}`} title={textOverflow ? label : ''}>{label}</Text> :
|
|
169
|
+
label.element}
|
|
170
|
+
{badge}
|
|
171
|
+
</Link>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A menu item that is actually a subgroup and can be collapsed/expanded.
|
|
177
|
+
* @param props the props for the component {@link ItemGroup} & { root: boolean }. Pass root=true to style this group as a root group.
|
|
178
|
+
* Root groups have slightly different visuals.
|
|
179
|
+
*/
|
|
180
|
+
const CollapsibleGroupItem = ({ label, open: initiallyOpened, children, icon, badge, root, overflow = 'wrap' }:
|
|
181
|
+
ItemGroup & { root?: boolean }) => {
|
|
182
|
+
const [open, setOpen] = useState(initiallyOpened ?? children?.some(c => 'active' in c && c.active) ?? false)
|
|
183
|
+
const items = useMemo(() => children?.filter(i => !i.hidden).map(renderOption), [children])
|
|
184
|
+
const id = `menuGroup${label}`
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<>
|
|
188
|
+
<a
|
|
189
|
+
onClick={() => setOpen(!open)}
|
|
190
|
+
onKeyDown={e => e.key === 'Enter' && setOpen(!open)}
|
|
191
|
+
className={listToClass(['item-row', root && 'root'])}
|
|
192
|
+
tabIndex={0}
|
|
193
|
+
aria-controls={id}
|
|
194
|
+
aria-expanded={open}
|
|
195
|
+
>
|
|
196
|
+
{icon}
|
|
197
|
+
<Text appearance={root ? 'overheader2' : 'body2'}
|
|
198
|
+
colorScheme="light.contrastText"
|
|
199
|
+
className={`label ${overflow} ${root ? 'item-row-title' : ''}`}>
|
|
200
|
+
{label}
|
|
201
|
+
</Text>
|
|
202
|
+
{badge}
|
|
203
|
+
<IconBox sx={{ mr: root ? undefined : '5' }}>
|
|
204
|
+
<ChevronDown className={listToClass(['chevron', open ? 'open' : ''])} />
|
|
205
|
+
</IconBox>
|
|
206
|
+
</a>
|
|
207
|
+
<MenuGroup id={id}
|
|
208
|
+
className={`${open ? 'open' : ''} ${root ? 'no-indentation' : ''}`}
|
|
209
|
+
aria-hidden={!open}>{items}</MenuGroup>
|
|
210
|
+
</>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderOption({ root, ...option }: MenuItem & { root?: boolean }) {
|
|
215
|
+
const labelText = typeof option.label === 'string' ? option.label : option.label.id
|
|
216
|
+
return (
|
|
217
|
+
<li key={labelText} role="menuitem" aria-selected={'children' in option ? undefined : option.active}>
|
|
218
|
+
{'children' in option ? <CollapsibleGroupItem {...option} root={root} open={option.open ?? root} /> : <ActionItem {...option} />}
|
|
219
|
+
</li >
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Renders a menu-content interface.
|
|
225
|
+
*
|
|
226
|
+
* Considering the Stackspot UI, this is the "menu content", not the "menu sections", i.e. it's the second menu from left to right, the
|
|
227
|
+
* one that changes according to section selected.
|
|
228
|
+
* @param props the props for the component {@link MenuSectionContent}.
|
|
229
|
+
*/
|
|
230
|
+
export const MenuContent = ({ pageSelector, goBack, title, subtitle, afterTitle, options = [], loading, error }: MenuSectionContent) => {
|
|
231
|
+
const items = useMemo(() => options.filter(o => !o.hidden).map(o => renderOption({ ...o, root: true })), [options])
|
|
232
|
+
|
|
233
|
+
const Link = useAnchorTag()
|
|
234
|
+
|
|
235
|
+
function renderContent() {
|
|
236
|
+
if (loading) {
|
|
237
|
+
return (
|
|
238
|
+
<Flex justifyContent="center" alignItems="center" flex={1} sx={{ padding: '40px' }}>
|
|
239
|
+
<LoadingCircular />
|
|
240
|
+
</Flex>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
if (error) return <Text colorScheme="danger">{error}</Text>
|
|
244
|
+
return <MenuGroup className="open root no-indentation">{items}</MenuGroup>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<>
|
|
249
|
+
{goBack && (
|
|
250
|
+
<Link href={goBack.href} onClick={goBack.onClick} className="goBackLink">
|
|
251
|
+
<IconBox colorIcon="inverse.500" size="sm">
|
|
252
|
+
<ArrowLeft />
|
|
253
|
+
</IconBox>
|
|
254
|
+
{typeof goBack?.label === 'string' ?
|
|
255
|
+
<Text appearance="body2" nowrapEllipsis>{goBack.label}</Text> :
|
|
256
|
+
goBack.label.element}
|
|
257
|
+
</Link>
|
|
258
|
+
)}
|
|
259
|
+
{title && (
|
|
260
|
+
<Title>
|
|
261
|
+
<Text appearance="overheader1" colorScheme="primary" sx={{ fontSize: '0.75rem', mt: 2, mb: 2 }}>{title}</Text>
|
|
262
|
+
{subtitle && <Text appearance="h5">{subtitle}</Text>}
|
|
263
|
+
</Title>
|
|
264
|
+
)}
|
|
265
|
+
{afterTitle}
|
|
266
|
+
{pageSelector && <PageSelector {...pageSelector} />}
|
|
267
|
+
{renderContent()}
|
|
268
|
+
</>
|
|
269
|
+
)
|
|
270
|
+
}
|