Feature/header animations (#2)

Animation on header responsiv, closing header submenu (burger and
notifications) when the menu translate


https://app.ora.pm/p/fb56ed95daa7456b888d266a050b9afa?v=86662&s=28429&t=k&c=051bbefaeab247749ec90d5c437abcc2
This commit is contained in:
Arnaud D. Natali 2023-04-04 16:11:56 +02:00 committed by GitHub
commit 8001cd4aa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 191 additions and 56 deletions

View File

@ -5,19 +5,18 @@ import BurgerIcon from "@Assets/icons/burger.svg";
import CrossIcon from "@Assets/icons/cross.svg";
import BurgerModal from "./BurgerModal";
type IProps = {};
type IState = {
type IProps = {
isModalOpen: boolean;
openBurgerMenu: () => void;
closeBurgerMenu: () => void;
};
type IState = {
// isModalOpen: boolean;
};
export default class BurgerMenu extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
isModalOpen: false,
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
}
public override render(): JSX.Element {
@ -25,20 +24,12 @@ export default class BurgerMenu extends React.Component<IProps, IState> {
<div className={classes["root"]}>
<Image
alt="burger"
src={this.state.isModalOpen ? CrossIcon : BurgerIcon}
src={this.props.isModalOpen ? CrossIcon : BurgerIcon}
className={classes["burger-icon"]}
onClick={this.openModal}
onClick={this.props.openBurgerMenu}
/>
{this.state.isModalOpen && <BurgerModal isOpen={this.state.isModalOpen} closeModal={this.closeModal} />}
{this.props.isModalOpen && <BurgerModal isOpen={this.props.isModalOpen} closeModal={this.props.closeBurgerMenu} />}
</div>
);
}
private openModal() {
this.setState({ isModalOpen: true });
}
private closeModal() {
this.setState({ isModalOpen: false });
}
}

View File

@ -6,10 +6,13 @@ import Toasts, { IToast } from "@Front/Stores/Toasts";
import NotificationModal from "./NotificationModal";
import InfoIcon from "@Assets/icons/info.svg";
type IProps = {};
type IProps = {
isModalOpen: boolean;
openNotificationModal: () => void;
closeNotificationModal: () => void;
};
type IState = {
hasNotifications: boolean;
isModalOpen: boolean;
toastList: IToast[] | null;
};
@ -19,26 +22,23 @@ export default class Notifications extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
isModalOpen: false,
toastList: Toasts.getInstance().toasts, //TODO : Get from bbd
hasNotifications: Toasts.getInstance().toasts.length > 0, // TODO: Change this when we have notification stored in bbd, unread notifications
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
this.handleToastChange = this.handleToastChange.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<div className={classes["icon-container"]} onClick={this.openModal}>
<div className={classes["icon-container"]} onClick={this.props.openNotificationModal}>
<Image alt="notifications" src={NotificationIcon} className={classes["notification-icon"]} />
{this.state.hasNotifications && (
<Image className={classes["notification-dot"]} src={InfoIcon} alt="Unread notification" />
)}
</div>
{this.state.isModalOpen && <NotificationModal isOpen={this.state.isModalOpen} closeModal={this.closeModal} />}
{this.props.isModalOpen && <NotificationModal isOpen={this.props.isModalOpen} closeModal={this.props.closeNotificationModal} />}
</div>
);
}
@ -57,12 +57,4 @@ export default class Notifications extends React.Component<IProps, IState> {
hasNotifications: toastList ? toastList.length > 0 : false,
});
}
private openModal() {
this.setState({ isModalOpen: true });
}
private closeModal() {
this.setState({ isModalOpen: false });
}
}

View File

@ -4,9 +4,12 @@ import Image from "next/image";
import ProfileIcon from "@Assets/icons/user.svg";
import ProfileModal from "./ProfileModal";
type IProps = {};
type IState = {
type IProps = {
isModalOpen: boolean;
openProfileModal: () => void;
closeProfileModal: () => void;
};
type IState = {
};
export default class Profile extends React.Component<IProps, IState> {
@ -15,24 +18,14 @@ export default class Profile extends React.Component<IProps, IState> {
this.state = {
isModalOpen: false,
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<Image alt="profile" src={ProfileIcon} className={classes["profile-icon"]} onClick={this.openModal} />
{this.state.isModalOpen && <ProfileModal isOpen={this.state.isModalOpen} closeModal={this.closeModal} />}
<Image alt="profile" src={ProfileIcon} className={classes["profile-icon"]} onClick={this.props.openProfileModal} />
{this.props.isModalOpen && <ProfileModal isOpen={this.props.isModalOpen} closeModal={this.props.closeProfileModal} />}
</div>
);
}
private openModal() {
this.setState({ isModalOpen: true });
}
private closeModal() {
this.setState({ isModalOpen: false });
}
}

View File

@ -8,7 +8,16 @@
background-color: $white;
box-shadow: $shadow-nav;
padding: 0 48px;
position: relative;
position: sticky;
top: 0;
z-index: 1;
@media (max-width: $screen-m) {
transition: transform 500ms ease-out;
&[data-open="closed"] {
transform: translateY(-85px);
}
}
.logo-container {
.logo {
@ -20,6 +29,7 @@
.right-section {
.profile-section {
display: inline-flex;
> :first-child {
margin-right: 32px;
}
@ -31,6 +41,7 @@
.notification-section {
display: inline-flex;
> :first-child {
margin-right: 32px;
}
@ -39,8 +50,10 @@
.burger-menu {
display: none;
@media (max-width: $screen-ls) {
display: inline-flex;
.icon {
width: 24px;
height: 24px;

View File

@ -7,14 +7,49 @@ import Navigation from "./Navigation";
import Notifications from "./Notifications";
import Profile from "./Profile";
import BurgerMenu from "./BurgerMenu";
import WindowStore from "@Front/Stores/WindowStore";
type IProps = {};
type IState = {};
enum EHeaderOpeningState {
OPEN = "open",
CLOSED = "closed",
IDLE = "idle",
}
type IProps = {
};
type IState = {
open: EHeaderOpeningState;
isBurgerMenuOpen: boolean;
isNotificationMenuOpen: boolean;
isProfileMenuOpen: boolean;
};
export default class Header extends React.Component<IProps, IState> {
private onScrollYDirectionChange = () => { };
private onWindowResize = () => { };
private headerBreakpoint = 1300;
constructor(props: IProps) {
super(props);
this.state = {
open: EHeaderOpeningState.OPEN,
isBurgerMenuOpen: false,
isNotificationMenuOpen: false,
isProfileMenuOpen: false,
};
this.openBurgerMenu = this.openBurgerMenu.bind(this);
this.closeBurgerMenu = this.closeBurgerMenu.bind(this);
this.openNotificationMenu = this.openNotificationMenu.bind(this);
this.closeNotificationMenu = this.closeNotificationMenu.bind(this);
this.closeProfileMenu = this.closeProfileMenu.bind(this);
this.openProfileMenu = this.openProfileMenu.bind(this);
this.visibility = this.visibility.bind(this);
}
public override render(): JSX.Element {
return (
<div className={classes["root"]}>
<div className={classes["root"]} data-open={this.state.open}>
<div className={classes["logo-container"]}>
<Link href="/">
<Image src={LogoIcon} alt="logo" className={classes["logo"]} />
@ -23,16 +58,71 @@ export default class Header extends React.Component<IProps, IState> {
<Navigation />
<div className={classes["right-section"]}>
<div className={classes["notification-section"]}>
<Notifications />
<Notifications isModalOpen={this.state.isNotificationMenuOpen} openNotificationModal={this.openNotificationMenu} closeNotificationModal={this.closeNotificationMenu} />
</div>
<div className={classes["profile-section"]}>
<Profile />
<Profile isModalOpen={this.state.isProfileMenuOpen} closeProfileModal={this.closeProfileMenu} openProfileModal={this.openProfileMenu} />
</div>
<div className={classes["burger-menu"]}>
<BurgerMenu />
<BurgerMenu isModalOpen={this.state.isBurgerMenuOpen} closeBurgerMenu={this.closeBurgerMenu} openBurgerMenu={this.openBurgerMenu} />
</div>
</div>
</div>
);
}
public override componentDidMount() {
this.onScrollYDirectionChange = WindowStore.getInstance().onScrollYDirectionChange((scrollYDirection) =>
this.visibility(scrollYDirection),
);
this.onWindowResize = WindowStore.getInstance().onResize((window) =>
this.onResize(window)
);
}
public override componentWillUnmount() {
this.onScrollYDirectionChange();
this.onWindowResize();
}
private visibility(scrollYDirection: number) {
let open: IState["open"] = EHeaderOpeningState.OPEN;
if (window.scrollY > 50 && scrollYDirection < 0 && Math.abs(scrollYDirection) > 8 && window.innerWidth < this.headerBreakpoint) {
open = EHeaderOpeningState.CLOSED;
this.closeBurgerMenu();
this.closeNotificationMenu();
}
if (open !== this.state.open) this.setState({ open });
}
private onResize(window: Window) {
if(window.innerWidth > this.headerBreakpoint && this.state.isBurgerMenuOpen) this.setState({ isBurgerMenuOpen: false });
if (window.innerWidth < this.headerBreakpoint && this.state.isProfileMenuOpen) this.setState({ isProfileMenuOpen: false });
}
private openBurgerMenu() {
this.setState({ isBurgerMenuOpen: true });
}
private closeBurgerMenu() {
this.setState({ isBurgerMenuOpen: false });
}
private openNotificationMenu() {
this.setState({ isNotificationMenuOpen: true });
}
private closeNotificationMenu() {
this.setState({ isNotificationMenuOpen: false });
}
private openProfileMenu() {
this.setState({ isProfileMenuOpen: true });
}
private closeProfileMenu() {
this.setState({ isProfileMenuOpen: false });
}
}

View File

@ -0,0 +1,56 @@
import EventEmitter from "events";
export default class WindowStore {
private static ctx: WindowStore;
private readonly event = new EventEmitter();
private constructor() {
WindowStore.ctx = this;
this.iniEvents();
}
public static getInstance() {
if (!WindowStore.ctx) new this();
return WindowStore.ctx;
}
public onScrollYDirectionChange(callback: (scrollYDifference: number) => void) {
this.event.on("scrollYDirectionChange", callback);
return () => {
this.event.off("scrollYDirectionChange", callback);
};
}
public onResize(callback: (window: Window) => void) {
this.event.on("resize", callback);
return () => {
this.event.off("resize", callback);
};
}
private iniEvents(): void {
window.addEventListener("scroll", (e: Event) => this.scrollYHandler(e));
window.addEventListener("resize", (e: Event) => this.resizeHandler());
}
private scrollYHandler = (() => {
let previousY: number = window.scrollY;
let snapShotY: number = previousY;
let previousYDirection: number = 1;
return (e: Event): void => {
const scrollYDirection = window.scrollY - previousY > 0 ? 1 : -1;
if (previousYDirection !== scrollYDirection) {
snapShotY = window.scrollY;
}
this.event.emit("scrollYDirectionChange", snapShotY - window.scrollY);
previousY = window.scrollY;
previousYDirection = scrollYDirection;
};
})();
private resizeHandler() {
this.event.emit("resize", window);
}
}