Skip to content

Commit 7be3e48

Browse files
NiklasRamstromPrisoners of Azkaban
andauthored
feat(topbar): add optional application drawer as alternative to application menu (#165)
Co-authored-by: Prisoners of Azkaban <healthmonitoring@axis.com>
1 parent 4e8be8f commit 7be3e48

15 files changed

+649
-47
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { makeStyles, shorthands, tokens } from "@fluentui/react-components";
2+
3+
export const useApplicationDrawrStyles = makeStyles({
4+
header: {
5+
display: "flex",
6+
flexDirection: "row",
7+
justifyContent: "space-between",
8+
backgroundColor: tokens.colorNeutralBackground5,
9+
color: tokens.colorNeutralForeground3,
10+
...shorthands.padding(tokens.spacingVerticalS, tokens.spacingHorizontalM),
11+
},
12+
headerTitle: {
13+
display: "flex",
14+
flexDirection: "row",
15+
alignItems: "center",
16+
...shorthands.gap(tokens.spacingHorizontalS),
17+
},
18+
iconAndText: {
19+
display: "flex",
20+
flexDirection: "row",
21+
alignItems: "center",
22+
...shorthands.gap(tokens.spacingHorizontalS),
23+
...shorthands.padding(tokens.spacingVerticalXS, tokens.spacingHorizontalM),
24+
},
25+
content: {
26+
display: "flex",
27+
flexDirection: "column",
28+
alignItems: "flex-start",
29+
width: "100%",
30+
...shorthands.gap(tokens.spacingVerticalM),
31+
paddingTop: tokens.spacingVerticalXXXL,
32+
},
33+
contentChildren: {
34+
display: "flex",
35+
flexDirection: "column",
36+
alignItems: "flex-start",
37+
width: "100%",
38+
paddingLeft: "24px",
39+
boxSizing: "border-box",
40+
},
41+
contentButton: {
42+
width: "100%",
43+
justifyContent: "flex-start",
44+
},
45+
selectedContentButton: {
46+
backgroundColor: tokens.colorNeutralBackground2Hover,
47+
},
48+
linkWrapper: {
49+
display: "flex",
50+
justifyContent: "flex-end",
51+
...shorthands.padding(tokens.spacingVerticalS, 0),
52+
},
53+
link: {
54+
display: "flex",
55+
alignItems: "center",
56+
...shorthands.gap(tokens.spacingHorizontalXS),
57+
},
58+
});
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { ArrowRightRegular, Dismiss20Regular } from "@fluentui/react-icons";
2+
import React, { Fragment, useState } from "react";
3+
4+
import { ApplicationArea } from "./top-bar.types";
5+
import { useTranslation } from "./translation-context";
6+
import {
7+
Body1Strong,
8+
Button,
9+
Divider,
10+
Drawer,
11+
DrawerBody,
12+
DrawerHeader,
13+
Link,
14+
mergeClasses,
15+
tokens,
16+
} from "@fluentui/react-components";
17+
import { useApplicationDrawrStyles as useApplicationDrawerStyles } from "./application-drawer.styles";
18+
import {
19+
ApplicationAreaFlaworedIcon,
20+
ApplicationAreaIcon,
21+
applicationAreaLabel,
22+
} from "./application-utils";
23+
import {
24+
ApplicationDrawerContent,
25+
ApplicationDrawerProps,
26+
SingleApplicationDrawerContent,
27+
} from "./application-drawer.types";
28+
29+
const IconAndText = ({
30+
icon,
31+
text,
32+
}: {
33+
icon: JSX.Element;
34+
text: string;
35+
}): JSX.Element => {
36+
const styles = useApplicationDrawerStyles();
37+
return (
38+
<div className={styles.iconAndText}>
39+
<div style={{ fontSize: "1.8em" }}>{icon}</div>
40+
<Body1Strong>{text}</Body1Strong>
41+
</div>
42+
);
43+
};
44+
45+
const findCurrent = (
46+
applicationId: string,
47+
content?: ApplicationDrawerContent[]
48+
): SingleApplicationDrawerContent | undefined => {
49+
if (!content) {
50+
return undefined;
51+
}
52+
53+
let currentApplication: SingleApplicationDrawerContent | undefined =
54+
undefined;
55+
56+
content.forEach((c) => {
57+
if (c.id === applicationId) {
58+
currentApplication = c;
59+
}
60+
return c.children?.forEach((child) => {
61+
if (child.id === applicationId) {
62+
currentApplication = child;
63+
}
64+
});
65+
});
66+
67+
return currentApplication;
68+
};
69+
70+
const iconConverter = (
71+
icon: JSX.Element,
72+
isCurrent: boolean,
73+
applicationArea: ApplicationArea
74+
) => {
75+
return isCurrent
76+
? (
77+
<ApplicationAreaFlaworedIcon
78+
applicationArea={applicationArea}
79+
icon={icon}
80+
/>
81+
)
82+
: (
83+
icon
84+
);
85+
};
86+
87+
const SingleApplication = ({
88+
application,
89+
currentSelectionId,
90+
onChange,
91+
applicationArea,
92+
}: {
93+
application: SingleApplicationDrawerContent;
94+
currentSelectionId: string;
95+
onChange: (id: string) => void;
96+
applicationArea: ApplicationArea;
97+
}): JSX.Element => {
98+
const styles = useApplicationDrawerStyles();
99+
100+
const buttonStyle = mergeClasses(
101+
styles.contentButton,
102+
application.id === currentSelectionId && styles.selectedContentButton
103+
);
104+
105+
return (
106+
<Button
107+
data-testid={`application-drawer-item-${application.id}`}
108+
className={buttonStyle}
109+
size="large"
110+
appearance="subtle"
111+
icon={iconConverter(
112+
application.icon,
113+
application.id === currentSelectionId,
114+
applicationArea
115+
)}
116+
onClick={() => onChange(application.id)}
117+
>
118+
{application.label}
119+
</Button>
120+
);
121+
};
122+
123+
const ApplicationWithChildren = ({
124+
application,
125+
currentSelectionId,
126+
onChange,
127+
applicationArea,
128+
}: {
129+
application: ApplicationDrawerContent;
130+
currentSelectionId: string;
131+
onChange: (id: string) => void;
132+
applicationArea: ApplicationArea;
133+
}): JSX.Element => {
134+
const styles = useApplicationDrawerStyles();
135+
136+
return (
137+
<>
138+
{IconAndText({
139+
icon: application.icon,
140+
text: application.label.toLocaleUpperCase(),
141+
})}
142+
<div className={styles.contentChildren}>
143+
{application.children?.map((child) => {
144+
return (
145+
<SingleApplication
146+
key={child.id}
147+
application={child}
148+
applicationArea={applicationArea}
149+
currentSelectionId={currentSelectionId}
150+
onChange={onChange}
151+
/>
152+
);
153+
})}
154+
</div>
155+
</>
156+
);
157+
};
158+
159+
export const ApplicationDrawer = ({
160+
link,
161+
title,
162+
applicationId,
163+
content,
164+
onChange,
165+
applicationArea,
166+
}: ApplicationDrawerProps & { applicationArea: ApplicationArea }) => {
167+
const { t } = useTranslation();
168+
const [isOpen, setIsOpen] = useState(false);
169+
const styles = useApplicationDrawerStyles();
170+
const currentSelection = findCurrent(applicationId, content);
171+
172+
const onClickItem = (id: string) => {
173+
if (id === currentSelection?.id) {
174+
setIsOpen(false);
175+
} else {
176+
onChange(id);
177+
}
178+
};
179+
180+
return (
181+
<>
182+
<Button
183+
data-testid="application-drawer-trigger"
184+
appearance="subtle"
185+
onClick={() => setIsOpen(true)}
186+
>
187+
{<ApplicationAreaIcon applicationArea={applicationArea} />}
188+
<Divider vertical style={{ padding: "0 0 0 12px" }}></Divider>
189+
{currentSelection
190+
? (
191+
<IconAndText
192+
icon={currentSelection.icon}
193+
text={currentSelection.label}
194+
/>
195+
)
196+
: null}
197+
</Button>
198+
<Drawer open={isOpen} onOpenChange={(_, { open }) => setIsOpen(open)}>
199+
<DrawerHeader className={styles.header}>
200+
<div className={styles.headerTitle}>
201+
<ApplicationAreaIcon applicationArea={applicationArea} />
202+
<Body1Strong>
203+
{applicationAreaLabel(t, applicationArea)}
204+
</Body1Strong>
205+
</div>
206+
<Button
207+
data-testid={"application-drawer-dismiss"}
208+
size="small"
209+
appearance="subtle"
210+
aria-label="Close"
211+
icon={<Dismiss20Regular color={tokens.colorNeutralForeground3} />}
212+
onClick={() => setIsOpen(false)}
213+
/>
214+
</DrawerHeader>
215+
216+
<DrawerBody>
217+
{link && (
218+
<div className={styles.linkWrapper}>
219+
<Link
220+
data-testid={"application-drawer-link"}
221+
className={styles.link}
222+
href={link.url}
223+
>
224+
{link.text}
225+
<ArrowRightRegular />
226+
</Link>
227+
</div>
228+
)}
229+
<div className={styles.content}>
230+
{title}
231+
<Divider></Divider>
232+
{content?.map((c) => {
233+
return (
234+
<Fragment key={c.id}>
235+
{c.children
236+
? (
237+
<ApplicationWithChildren
238+
application={c}
239+
currentSelectionId={currentSelection?.id ?? ""}
240+
onChange={onClickItem}
241+
applicationArea={applicationArea ?? ""}
242+
/>
243+
)
244+
: (
245+
<SingleApplication
246+
application={c}
247+
currentSelectionId={currentSelection?.id ?? ""}
248+
onChange={onClickItem}
249+
applicationArea={applicationArea}
250+
/>
251+
)}
252+
<Divider></Divider>
253+
</Fragment>
254+
);
255+
})}
256+
</div>
257+
</DrawerBody>
258+
</Drawer>
259+
</>
260+
);
261+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export type ApplicationDrawerContent = SingleApplicationDrawerContent & {
2+
children?: SingleApplicationDrawerContent[];
3+
};
4+
5+
export type SingleApplicationDrawerContent = {
6+
id: string;
7+
icon: JSX.Element;
8+
label: string;
9+
};
10+
11+
export type ApplicationDrawerProps = {
12+
link?: { text: string; url: string };
13+
title: JSX.Element;
14+
content?: ApplicationDrawerContent[];
15+
applicationId: string;
16+
onChange: (id: string) => void;
17+
};

0 commit comments

Comments
 (0)