Refactor header
This commit is contained in:
parent
87e5cafb77
commit
eb620e9220
16 changed files with 307 additions and 130 deletions
|
@ -1,11 +0,0 @@
|
|||
import "../src/index.css";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
25
.storybook/preview.jsx
Normal file
25
.storybook/preview.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from "react";
|
||||
import { addDecorator } from "@storybook/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { usePageFocusStyle } from "../src/usePageFocusStyle";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
import "../src/index.css";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
addDecorator((story) => {
|
||||
usePageFocusStyle();
|
||||
return (
|
||||
<MemoryRouter initialEntries={["/"]}>
|
||||
<OverlayProvider>{story()}</OverlayProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
21
src/App.jsx
21
src/App.jsx
|
@ -32,29 +32,12 @@ import {
|
|||
ClientProvider,
|
||||
defaultHomeserverHost,
|
||||
} from "./ConferenceCallManagerHooks";
|
||||
import { useFocusVisible } from "@react-aria/interactions";
|
||||
import styles from "./App.module.css";
|
||||
import { LoadingView } from "./FullScreenView";
|
||||
|
||||
import { usePageFocusStyle } from "./usePageFocusStyle";
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
export default function App({ history }) {
|
||||
const { isFocusVisible } = useFocusVisible();
|
||||
|
||||
useEffect(() => {
|
||||
const classList = document.body.classList;
|
||||
const hasClass = classList.contains(styles.hideFocus);
|
||||
|
||||
if (isFocusVisible && hasClass) {
|
||||
classList.remove(styles.hideFocus);
|
||||
} else if (!isFocusVisible && !hasClass) {
|
||||
classList.add(styles.hideFocus);
|
||||
}
|
||||
|
||||
return () => {
|
||||
classList.remove(styles.hideFocus);
|
||||
};
|
||||
}, [isFocusVisible]);
|
||||
usePageFocusStyle();
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
|
|
|
@ -15,10 +15,15 @@ export function Header({ children, className, ...rest }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function LeftNav({ children, className, ...rest }) {
|
||||
export function LeftNav({ children, className, hideMobile, ...rest }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.nav, styles.leftNav, className)}
|
||||
className={classNames(
|
||||
styles.nav,
|
||||
styles.leftNav,
|
||||
{ [styles.hideMobile]: hideMobile },
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
@ -39,7 +44,7 @@ export function RightNav({ children, className, ...rest }) {
|
|||
|
||||
export function HeaderLogo() {
|
||||
return (
|
||||
<Link className={styles.logo} to="/">
|
||||
<Link className={styles.headerLogo} to="/">
|
||||
<Logo />
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -16,16 +16,24 @@
|
|||
height: 64px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
.headerLogo {
|
||||
display: none;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.leftNav.hideMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.leftNav > * {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.leftNav h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.rightNav {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
@ -40,7 +48,7 @@
|
|||
|
||||
.roomAvatar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 36px;
|
||||
|
@ -93,7 +101,17 @@
|
|||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.headerLogo,
|
||||
.roomAvatar,
|
||||
.leftNav.hideMobile {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.leftNav h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: 98px;
|
||||
height: 76px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,106 @@
|
|||
import React from "react";
|
||||
import { Header, LeftNav, RightNav } from "./Header";
|
||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||
import {
|
||||
Header,
|
||||
HeaderLogo,
|
||||
LeftNav,
|
||||
RightNav,
|
||||
RoomHeaderInfo,
|
||||
} from "./Header";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
|
||||
export default {
|
||||
title: "Header",
|
||||
component: Header,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
};
|
||||
|
||||
export const Home = () => (
|
||||
export const HomeAnonymous = () => (
|
||||
<Header>
|
||||
<LeftNav></LeftNav>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export const HomeNamedGuest = () => (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export const HomeLoggedIn = () => (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu isAuthenticated displayName="Yara" />
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export const LobbyNamedGuest = () => (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName="Q4Roadmap" />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export const LobbyLoggedIn = () => (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName="Q4Roadmap" />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu isAuthenticated displayName="Yara" />
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export const InRoomNamedGuest = () => (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName="Q4Roadmap" />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout="freedom" />
|
||||
<UserMenu isAuthenticated isPasswordlessUser displayName="Yara" />
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export const InRoomLoggedIn = () => (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName="Q4Roadmap" />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout="freedom" />
|
||||
<UserMenu isAuthenticated displayName="Yara" />
|
||||
</RightNav>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export const CreateAccount = () => (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav></RightNav>
|
||||
</Header>
|
||||
);
|
||||
|
|
|
@ -36,6 +36,7 @@ import { useModalTriggerState } from "./Modal";
|
|||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||
import { RecaptchaInput } from "./RecaptchaInput";
|
||||
import { UserMenuContainer } from "./UserMenuContainer";
|
||||
|
||||
export function Home() {
|
||||
const {
|
||||
|
@ -188,7 +189,7 @@ function UnregisteredView({
|
|||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.splitContainer}>
|
||||
|
@ -319,7 +320,7 @@ function RegisteredView({
|
|||
<HeaderLogo />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.splitContainer}>
|
||||
|
|
|
@ -3,11 +3,11 @@ import { DismissButton, useOverlay } from "@react-aria/overlays";
|
|||
import { FocusScope } from "@react-aria/focus";
|
||||
import classNames from "classnames";
|
||||
import styles from "./Popover.module.css";
|
||||
import { useObjectRef } from "@react-aria/utils";
|
||||
|
||||
export const Popover = forwardRef(
|
||||
({ isOpen = true, onClose, className, children, ...rest }, ref) => {
|
||||
const fallbackRef = useRef();
|
||||
const popoverRef = ref || fallbackRef;
|
||||
const popoverRef = useObjectRef(ref);
|
||||
|
||||
const { overlayProps } = useOverlay(
|
||||
{
|
||||
|
|
|
@ -51,6 +51,7 @@ import { GridLayoutMenu } from "./GridLayoutMenu";
|
|||
import { UserMenu } from "./UserMenu";
|
||||
import classNames from "classnames";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { UserMenuContainer } from "./UserMenuContainer";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
|
@ -309,7 +310,7 @@ function RoomSetupView({
|
|||
<RoomHeaderInfo roomName={roomName} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenu />
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
<div className={styles.joinRoom}>
|
||||
|
@ -470,7 +471,7 @@ function InRoomView({
|
|||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
{!isGuest && <UserMenu disableLogout />}
|
||||
{!isGuest && <UserMenuContainer disableLogout />}
|
||||
</RightNav>
|
||||
</Header>
|
||||
{items.length === 0 ? (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { forwardRef, useRef } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTooltipTriggerState } from "@react-stately/tooltip";
|
||||
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
||||
import { mergeProps } from "@react-aria/utils";
|
||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||
import styles from "./Tooltip.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
|
@ -20,8 +20,7 @@ export function Tooltip({ position, state, ...props }) {
|
|||
|
||||
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
||||
const tooltipState = useTooltipTriggerState(rest);
|
||||
const fallbackRef = useRef();
|
||||
const triggerRef = ref || fallbackRef;
|
||||
const triggerRef = useObjectRef(ref);
|
||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||
rest,
|
||||
tooltipState,
|
||||
|
|
124
src/UserMenu.jsx
124
src/UserMenu.jsx
|
@ -1,58 +1,34 @@
|
|||
import React, { useCallback, useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { Button, LinkButton } from "./button";
|
||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||
import { Menu } from "./Menu";
|
||||
import { Tooltip, TooltipTrigger } from "./Tooltip";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
||||
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
||||
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
||||
import styles from "./UserMenu.module.css";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { Menu } from "./Menu";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useClient, useProfile } from "./ConferenceCallManagerHooks";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { ProfileModal } from "./ProfileModal";
|
||||
import { Tooltip, TooltipTrigger } from "./Tooltip";
|
||||
import { Avatar } from "./Avatar";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export function UserMenu({ disableLogout }) {
|
||||
export function UserMenu({
|
||||
disableLogout,
|
||||
isAuthenticated,
|
||||
isPasswordlessUser,
|
||||
displayName,
|
||||
avatarUrl,
|
||||
onAction,
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const {
|
||||
isAuthenticated,
|
||||
isGuest,
|
||||
isPasswordlessUser,
|
||||
logout,
|
||||
userName,
|
||||
client,
|
||||
} = useClient();
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
const onAction = useCallback(
|
||||
(value) => {
|
||||
switch (value) {
|
||||
case "user":
|
||||
modalState.open();
|
||||
break;
|
||||
case "logout":
|
||||
logout();
|
||||
break;
|
||||
case "login":
|
||||
history.push("/login", { state: { from: location } });
|
||||
break;
|
||||
}
|
||||
},
|
||||
[history, location, logout, modalState]
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const arr = [];
|
||||
|
||||
if (isAuthenticated && !isGuest) {
|
||||
if (isAuthenticated) {
|
||||
arr.push({
|
||||
key: "user",
|
||||
icon: UserIcon,
|
||||
label: displayName || userName,
|
||||
label: displayName,
|
||||
});
|
||||
|
||||
if (isPasswordlessUser) {
|
||||
|
@ -73,9 +49,9 @@ export function UserMenu({ disableLogout }) {
|
|||
}
|
||||
|
||||
return arr;
|
||||
}, [isAuthenticated, isGuest, userName, displayName]);
|
||||
}, [isAuthenticated, isPasswordlessUser, displayName, disableLogout]);
|
||||
|
||||
if (isGuest || !isAuthenticated) {
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<LinkButton to={{ pathname: "/login", state: { from: location } }}>
|
||||
Log in
|
||||
|
@ -84,46 +60,36 @@ export function UserMenu({ disableLogout }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<TooltipTrigger>
|
||||
<Button variant="icon" className={styles.userButton}>
|
||||
{isAuthenticated && !isGuest && !isPasswordlessUser ? (
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={avatarUrl}
|
||||
fallback={(displayName || userName).slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
) : (
|
||||
<UserIcon />
|
||||
)}
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Tooltip position="bottomLeft" {...props}>
|
||||
Profile
|
||||
</Tooltip>
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<TooltipTrigger>
|
||||
<Button variant="icon" className={styles.userButton}>
|
||||
{isAuthenticated && !isPasswordlessUser ? (
|
||||
<Avatar
|
||||
size="sm"
|
||||
className={styles.avatar}
|
||||
src={avatarUrl}
|
||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||
/>
|
||||
) : (
|
||||
<UserIcon />
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
</Button>
|
||||
{(props) => (
|
||||
<Menu {...props} label="User menu" onAction={onAction}>
|
||||
{items.map(({ key, icon: Icon, label }) => (
|
||||
<Item key={key} textValue={label}>
|
||||
<Icon />
|
||||
<span>{label}</span>
|
||||
</Item>
|
||||
))}
|
||||
</Menu>
|
||||
<Tooltip position="bottomLeft" {...props}>
|
||||
Profile
|
||||
</Tooltip>
|
||||
)}
|
||||
</PopoverMenuTrigger>
|
||||
{modalState.isOpen && (
|
||||
<ProfileModal
|
||||
client={client}
|
||||
isAuthenticated={isAuthenticated}
|
||||
isGuest={isGuest}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
{...modalProps}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
{(props) => (
|
||||
<Menu {...props} label="User menu" onAction={onAction}>
|
||||
{items.map(({ key, icon: Icon, label }) => (
|
||||
<Item key={key} textValue={label}>
|
||||
<Icon />
|
||||
<span>{label}</span>
|
||||
</Item>
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</>
|
||||
</PopoverMenuTrigger>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
.userButton svg * {
|
||||
fill: var(--textColor1);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
|
62
src/UserMenuContainer.jsx
Normal file
62
src/UserMenuContainer.jsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useClient, useProfile } from "./ConferenceCallManagerHooks";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { ProfileModal } from "./ProfileModal";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
|
||||
export function UserMenuContainer({ disableLogout }) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const {
|
||||
isAuthenticated,
|
||||
isGuest,
|
||||
isPasswordlessUser,
|
||||
logout,
|
||||
userName,
|
||||
client,
|
||||
} = useClient();
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const { modalState, modalProps } = useModalTriggerState();
|
||||
|
||||
const onAction = useCallback(
|
||||
(value) => {
|
||||
switch (value) {
|
||||
case "user":
|
||||
modalState.open();
|
||||
break;
|
||||
case "logout":
|
||||
logout();
|
||||
break;
|
||||
case "login":
|
||||
history.push("/login", { state: { from: location } });
|
||||
break;
|
||||
}
|
||||
},
|
||||
[history, location, logout, modalState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserMenu
|
||||
disableLogout={disableLogout}
|
||||
isAuthenticated={isAuthenticated}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
avatarUrl={avatarUrl}
|
||||
onAction={onAction}
|
||||
displayName={
|
||||
displayName || (userName ? userName.replace("@", "") : undefined)
|
||||
}
|
||||
/>
|
||||
{modalState.isOpen && (
|
||||
<ProfileModal
|
||||
client={client}
|
||||
isAuthenticated={isAuthenticated}
|
||||
isGuest={isGuest}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
{...modalProps}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -62,7 +62,7 @@ export const Button = forwardRef(
|
|||
[styles.off]: off,
|
||||
}
|
||||
)}
|
||||
{...filteredButtonProps}
|
||||
{...mergeProps(rest, filteredButtonProps)}
|
||||
ref={buttonRef}
|
||||
>
|
||||
{children}
|
||||
|
|
22
src/usePageFocusStyle.js
Normal file
22
src/usePageFocusStyle.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { useEffect } from "react";
|
||||
import { useFocusVisible } from "@react-aria/interactions";
|
||||
import styles from "./usePageFocusStyle.module.css";
|
||||
|
||||
export function usePageFocusStyle() {
|
||||
const { isFocusVisible } = useFocusVisible();
|
||||
|
||||
useEffect(() => {
|
||||
const classList = document.body.classList;
|
||||
const hasClass = classList.contains(styles.hideFocus);
|
||||
|
||||
if (isFocusVisible && hasClass) {
|
||||
classList.remove(styles.hideFocus);
|
||||
} else if (!isFocusVisible && !hasClass) {
|
||||
classList.add(styles.hideFocus);
|
||||
}
|
||||
|
||||
return () => {
|
||||
classList.remove(styles.hideFocus);
|
||||
};
|
||||
}, [isFocusVisible]);
|
||||
}
|
Loading…
Add table
Reference in a new issue