Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(docs): side menu sticky on desktop + mobile #988

Merged
merged 10 commits into from
Oct 5, 2023
2 changes: 2 additions & 0 deletions .changeset/giant-lizards-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 1 addition & 0 deletions packages/apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"react-dom": "^18.2.0",
"react-markdown": "~8.0.7",
"react-tweet": "~3.1.1",
"react-use": "^17.4.0",
"redoc": "~2.0.0",
"rehype-pretty-code": "~0.9.5",
"rehype-raw": "^7.0.0",
Expand Down
52 changes: 31 additions & 21 deletions packages/apps/docs/src/components/Layout/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,44 @@ import {
} from './menu.css';

import classNames from 'classnames';
import type { FC, ReactNode } from 'react';
import React from 'react';
import type { FC, ForwardedRef, ReactNode } from 'react';
import React, { forwardRef } from 'react';

interface IProps {
children?: ReactNode;
dataCy?: string;
isOpen?: boolean;
inLayout?: boolean;
layout: 'landing' | 'normal';
style?: React.CSSProperties;
ref?: ForwardedRef<HTMLDivElement>;
}

export const Menu: FC<IProps> = ({
children,
dataCy,
isOpen = false,
inLayout = false,
layout = 'normal',
}) => {
const classes = classNames(
menuClass,
menuOpenVariants[isOpen ? 'isOpen' : 'isClosed'],
menuInLayoutVariants[inLayout ? 'true' : 'false'],
menuLayoutVariants[layout],
);
export const Menu: FC<IProps> = forwardRef<HTMLDivElement, IProps>(
(
{
children,
dataCy,
isOpen = false,
inLayout = false,
layout = 'normal',
style,
},
ref,
) => {
const classes = classNames(
menuClass,
menuOpenVariants[isOpen ? 'isOpen' : 'isClosed'],
menuInLayoutVariants[inLayout ? 'true' : 'false'],
menuLayoutVariants[layout],
);

return (
<div data-cy={dataCy} className={classes}>
{children}
</div>
);
};
return (
<div data-cy={dataCy} className={classes} style={style} ref={ref}>
{children}
</div>
);
},
);

Menu.displayName = 'Menu';
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { breakpoints, sprinkles } from '@kadena/react-ui/theme';
import { breakpoints, sprinkles, vars } from '@kadena/react-ui/theme';

import { $$leftSideWidth, $$sideMenu } from '../../global.css';

import { style, styleVariants } from '@vanilla-extract/css';

export const menuClass = style([
sprinkles({
position: 'absolute',
paddingBottom: '$40',
position: 'fixed',
height: '100%',
width: '100%',
background: '$background',
overflow: 'hidden',
top: '$17',
bottom: 0,
}),
{
height: `calc(100vh - ${vars.sizes.$13})`,
gridArea: 'menu',
gridRow: '2 / span 3',
zIndex: $$sideMenu,
Expand All @@ -25,9 +27,13 @@ export const menuClass = style([
width: $$leftSideWidth,
},
[`screen and ${breakpoints.md}`]: {
position: 'relative',
position: 'sticky',
top: vars.sizes.$18,
bottom: 'auto',
height: `calc(100vh - ${vars.sizes.$18})`,
transform: 'translateX(0)',
background: 'transparent',
paddingBottom: vars.sizes.$40,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sprinkles } from '@kadena/react-ui/theme';
import { breakpoints, sprinkles } from '@kadena/react-ui/theme';

import { style, styleVariants } from '@vanilla-extract/css';

Expand All @@ -10,8 +10,12 @@ export const menuCardClass = style([
paddingX: '$6',
}),
{
overflowY: 'scroll',
transition: 'transform .2s ease',
'@media': {
[`screen and ${breakpoints.md}`]: {
overflowY: 'scroll',
},
},
},
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import { style } from '@vanilla-extract/css';
export const sideMenuClass = style([
sprinkles({
position: 'relative',

height: '100%',
paddingBottom: '$25',
}),
{
overflowY: 'auto',
overflowX: 'hidden',
},
]);

export const listClass = style([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { breakpoints } from '@kadena/react-ui/theme';

import { Footer } from '../Footer';
import { Menu, MenuBack } from '../Menu';
import { SideMenu } from '../SideMenu';

import { useMenu } from '@/hooks';
import { useMenu, useWindowScroll } from '@/hooks';
import type { IMenuItem } from '@/types/Layout';
import type { FC, ReactNode } from 'react';
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useMedia } from 'react-use';

interface IProps {
children?: ReactNode;
Expand All @@ -21,6 +24,47 @@ export const Template: FC<IProps> = ({
hideSideMenu = false,
}) => {
const { isMenuOpen, closeMenu } = useMenu();
const isMediumDevice = useMedia(breakpoints.md);
const [{ y }] = useWindowScroll();
const mainContentRef = useRef<HTMLDivElement>(null);
const [initialTopSpacing, setInitialTopSpacing] = useState('');
const [style, setStyle] = useState<React.CSSProperties>({});
// Enable position if it's minimum medium device size
// and layout type is landing
const enablePositioning = layout === 'landing' && isMediumDevice;

useEffect(() => {
if (!enablePositioning) return;
// Get the initial paddingTop value at initial rendering
const paddingTop = getComputedStyle(
mainContentRef.current as HTMLDivElement,
)?.paddingTop;
// When we get css from computed style it comes with `px` suffix
const onlyValue = paddingTop.split('px')[0];
setInitialTopSpacing(onlyValue);

// Reset style value when we navigate to different pages
return () => {
setStyle({});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (!mainContentRef.current || !enablePositioning) {
setStyle({});
return;
}

// From the initial top spacing subtract the window scroll value
// to maintain the scrolling effect
const paddingValue = parseInt(initialTopSpacing) - (y || 0);

if (paddingValue <= 0) return;
setStyle({
paddingTop: paddingValue,
});
}, [y, initialTopSpacing, enablePositioning]);

return (
<>
Expand All @@ -30,6 +74,8 @@ export const Template: FC<IProps> = ({
isOpen={isMenuOpen}
inLayout={!hideSideMenu}
layout={layout}
ref={mainContentRef}
style={style}
>
<SideMenu closeMenu={closeMenu} menuItems={menuItems} />
</Menu>
Expand Down
1 change: 1 addition & 0 deletions packages/apps/docs/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { useOpenSearch } from './useOpenSearch';
export { useSearch } from './useSearch';
export * from './useGetBlogs';
export { useMenu, MenuProvider } from './useMenu';
export { useWindowScroll } from './useWindowScroll';
28 changes: 28 additions & 0 deletions packages/apps/docs/src/hooks/useWindowScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';

export function useWindowScroll(): [
{
x: number;
y: number;
},
] {
const [state, setState] = React.useState({
x: 0,
y: 0,
});

React.useLayoutEffect((): (() => void) => {
const handleScroll = (): void => {
setState({ x: window.scrollX, y: window.scrollY });
};

handleScroll();
window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);

return [state];
}
Loading