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:
commit
8001cd4aa9
@ -5,19 +5,18 @@ import BurgerIcon from "@Assets/icons/burger.svg";
|
|||||||
import CrossIcon from "@Assets/icons/cross.svg";
|
import CrossIcon from "@Assets/icons/cross.svg";
|
||||||
import BurgerModal from "./BurgerModal";
|
import BurgerModal from "./BurgerModal";
|
||||||
|
|
||||||
type IProps = {};
|
type IProps = {
|
||||||
type IState = {
|
|
||||||
isModalOpen: boolean;
|
isModalOpen: boolean;
|
||||||
|
openBurgerMenu: () => void;
|
||||||
|
closeBurgerMenu: () => void;
|
||||||
|
};
|
||||||
|
type IState = {
|
||||||
|
// isModalOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class BurgerMenu extends React.Component<IProps, IState> {
|
export default class BurgerMenu extends React.Component<IProps, IState> {
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
|
||||||
isModalOpen: false,
|
|
||||||
};
|
|
||||||
this.openModal = this.openModal.bind(this);
|
|
||||||
this.closeModal = this.closeModal.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render(): JSX.Element {
|
public override render(): JSX.Element {
|
||||||
@ -25,20 +24,12 @@ export default class BurgerMenu extends React.Component<IProps, IState> {
|
|||||||
<div className={classes["root"]}>
|
<div className={classes["root"]}>
|
||||||
<Image
|
<Image
|
||||||
alt="burger"
|
alt="burger"
|
||||||
src={this.state.isModalOpen ? CrossIcon : BurgerIcon}
|
src={this.props.isModalOpen ? CrossIcon : BurgerIcon}
|
||||||
className={classes["burger-icon"]}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private openModal() {
|
|
||||||
this.setState({ isModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private closeModal() {
|
|
||||||
this.setState({ isModalOpen: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,39 +6,39 @@ import Toasts, { IToast } from "@Front/Stores/Toasts";
|
|||||||
import NotificationModal from "./NotificationModal";
|
import NotificationModal from "./NotificationModal";
|
||||||
import InfoIcon from "@Assets/icons/info.svg";
|
import InfoIcon from "@Assets/icons/info.svg";
|
||||||
|
|
||||||
type IProps = {};
|
type IProps = {
|
||||||
|
isModalOpen: boolean;
|
||||||
|
openNotificationModal: () => void;
|
||||||
|
closeNotificationModal: () => void;
|
||||||
|
};
|
||||||
type IState = {
|
type IState = {
|
||||||
hasNotifications: boolean;
|
hasNotifications: boolean;
|
||||||
isModalOpen: boolean;
|
|
||||||
toastList: IToast[] | null;
|
toastList: IToast[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Notifications extends React.Component<IProps, IState> {
|
export default class Notifications extends React.Component<IProps, IState> {
|
||||||
private removeOnToastChange: () => void = () => {};
|
private removeOnToastChange: () => void = () => { };
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isModalOpen: false,
|
|
||||||
toastList: Toasts.getInstance().toasts, //TODO : Get from bbd
|
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
|
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);
|
this.handleToastChange = this.handleToastChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render(): JSX.Element {
|
public override render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={classes["root"]}>
|
<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"]} />
|
<Image alt="notifications" src={NotificationIcon} className={classes["notification-icon"]} />
|
||||||
{this.state.hasNotifications && (
|
{this.state.hasNotifications && (
|
||||||
<Image className={classes["notification-dot"]} src={InfoIcon} alt="Unread notification" />
|
<Image className={classes["notification-dot"]} src={InfoIcon} alt="Unread notification" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -57,12 +57,4 @@ export default class Notifications extends React.Component<IProps, IState> {
|
|||||||
hasNotifications: toastList ? toastList.length > 0 : false,
|
hasNotifications: toastList ? toastList.length > 0 : false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openModal() {
|
|
||||||
this.setState({ isModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private closeModal() {
|
|
||||||
this.setState({ isModalOpen: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,12 @@ import Image from "next/image";
|
|||||||
import ProfileIcon from "@Assets/icons/user.svg";
|
import ProfileIcon from "@Assets/icons/user.svg";
|
||||||
import ProfileModal from "./ProfileModal";
|
import ProfileModal from "./ProfileModal";
|
||||||
|
|
||||||
type IProps = {};
|
type IProps = {
|
||||||
type IState = {
|
|
||||||
isModalOpen: boolean;
|
isModalOpen: boolean;
|
||||||
|
openProfileModal: () => void;
|
||||||
|
closeProfileModal: () => void;
|
||||||
|
};
|
||||||
|
type IState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Profile extends React.Component<IProps, 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 = {
|
this.state = {
|
||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
};
|
};
|
||||||
this.openModal = this.openModal.bind(this);
|
|
||||||
this.closeModal = this.closeModal.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override render(): JSX.Element {
|
public override render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={classes["root"]}>
|
<div className={classes["root"]}>
|
||||||
<Image alt="profile" src={ProfileIcon} className={classes["profile-icon"]} onClick={this.openModal} />
|
<Image alt="profile" src={ProfileIcon} className={classes["profile-icon"]} onClick={this.props.openProfileModal} />
|
||||||
{this.state.isModalOpen && <ProfileModal isOpen={this.state.isModalOpen} closeModal={this.closeModal} />}
|
{this.props.isModalOpen && <ProfileModal isOpen={this.props.isModalOpen} closeModal={this.props.closeProfileModal} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private openModal() {
|
|
||||||
this.setState({ isModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private closeModal() {
|
|
||||||
this.setState({ isModalOpen: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,16 @@
|
|||||||
background-color: $white;
|
background-color: $white;
|
||||||
box-shadow: $shadow-nav;
|
box-shadow: $shadow-nav;
|
||||||
padding: 0 48px;
|
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-container {
|
||||||
.logo {
|
.logo {
|
||||||
@ -20,6 +29,7 @@
|
|||||||
.right-section {
|
.right-section {
|
||||||
.profile-section {
|
.profile-section {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
> :first-child {
|
> :first-child {
|
||||||
margin-right: 32px;
|
margin-right: 32px;
|
||||||
}
|
}
|
||||||
@ -31,6 +41,7 @@
|
|||||||
|
|
||||||
.notification-section {
|
.notification-section {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
> :first-child {
|
> :first-child {
|
||||||
margin-right: 32px;
|
margin-right: 32px;
|
||||||
}
|
}
|
||||||
@ -39,8 +50,10 @@
|
|||||||
|
|
||||||
.burger-menu {
|
.burger-menu {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@media (max-width: $screen-ls) {
|
@media (max-width: $screen-ls) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
@ -7,14 +7,49 @@ import Navigation from "./Navigation";
|
|||||||
import Notifications from "./Notifications";
|
import Notifications from "./Notifications";
|
||||||
import Profile from "./Profile";
|
import Profile from "./Profile";
|
||||||
import BurgerMenu from "./BurgerMenu";
|
import BurgerMenu from "./BurgerMenu";
|
||||||
|
import WindowStore from "@Front/Stores/WindowStore";
|
||||||
|
|
||||||
type IProps = {};
|
enum EHeaderOpeningState {
|
||||||
type IState = {};
|
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> {
|
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 {
|
public override render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className={classes["root"]}>
|
<div className={classes["root"]} data-open={this.state.open}>
|
||||||
<div className={classes["logo-container"]}>
|
<div className={classes["logo-container"]}>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<Image src={LogoIcon} alt="logo" className={classes["logo"]} />
|
<Image src={LogoIcon} alt="logo" className={classes["logo"]} />
|
||||||
@ -23,16 +58,71 @@ export default class Header extends React.Component<IProps, IState> {
|
|||||||
<Navigation />
|
<Navigation />
|
||||||
<div className={classes["right-section"]}>
|
<div className={classes["right-section"]}>
|
||||||
<div className={classes["notification-section"]}>
|
<div className={classes["notification-section"]}>
|
||||||
<Notifications />
|
<Notifications isModalOpen={this.state.isNotificationMenuOpen} openNotificationModal={this.openNotificationMenu} closeNotificationModal={this.closeNotificationMenu} />
|
||||||
</div>
|
</div>
|
||||||
<div className={classes["profile-section"]}>
|
<div className={classes["profile-section"]}>
|
||||||
<Profile />
|
<Profile isModalOpen={this.state.isProfileMenuOpen} closeProfileModal={this.closeProfileMenu} openProfileModal={this.openProfileMenu} />
|
||||||
</div>
|
</div>
|
||||||
<div className={classes["burger-menu"]}>
|
<div className={classes["burger-menu"]}>
|
||||||
<BurgerMenu />
|
<BurgerMenu isModalOpen={this.state.isBurgerMenuOpen} closeBurgerMenu={this.closeBurgerMenu} openBurgerMenu={this.openBurgerMenu} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
56
src/front/Stores/WindowStore.ts
Normal file
56
src/front/Stores/WindowStore.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user