Skip to content

Commit

Permalink
feat: responsive side menu
Browse files Browse the repository at this point in the history
  • Loading branch information
RomanHotsiy committed Jan 30, 2018
1 parent a29c3cc commit 3aab2d9
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 81 deletions.
2 changes: 1 addition & 1 deletion demo/playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
<redoc id="example"></redoc>
</body>

</html>
</html>
2 changes: 1 addition & 1 deletion src/components/ApiInfo/ApiInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react';

import { AppStore } from '../../services/AppStore';

import { MiddlePanel, Row, EmptyDarkRightPanel } from '../../common-elements/';
import { EmptyDarkRightPanel, MiddlePanel, Row } from '../../common-elements/';
import { Markdown } from '../Markdown/Markdown';
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';

Expand Down
6 changes: 3 additions & 3 deletions src/components/Redoc/Redoc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ApiLogo } from '../ApiLogo/ApiLogo';
import { ContentItems } from '../ContentItems/ContentItems';
import { OptionsProvider } from '../OptionsProvider';
import { SideMenu } from '../SideMenu/SideMenu';
import { StickySidebar } from '../StickySidebar/StickySidebar';
import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar';
import { ApiContent, RedocWrap } from './elements';

export interface RedocProps {
Expand All @@ -36,10 +36,10 @@ export class Redoc extends React.Component<RedocProps> {
<ThemeProvider theme={options.theme}>
<OptionsProvider options={options}>
<RedocWrap className="redoc-wrap">
<StickySidebar className="menu-content">
<StickyResponsiveSidebar menu={menu} className="menu-content">
<ApiLogo info={spec.info} />
<SideMenu menu={menu} />
</StickySidebar>
</StickyResponsiveSidebar>
<ApiContent className="api-content">
<ApiInfo store={store} />
<ContentItems items={menu.items as any} />
Expand Down
66 changes: 66 additions & 0 deletions src/components/StickySidebar/ChevronSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';

import styled from '../../styled-components';

export const AnimatedChevronButton = ({ open }: { open: boolean }) => {
const iconOffset = open ? 8 : -4;

return (
<ChevronContainer>
<ChevronSvg
size={15}
style={{
transform: `translate(2px, ${iconOffset}px) rotate(180deg)`,
transition: 'transform 0.2s ease',
}}
/>
<ChevronSvg
size={15}
style={{
transform: `translate(2px, ${0 - iconOffset}px)`,
transition: 'transform 0.2s ease',
}}
/>
</ChevronContainer>
);
};

// adapted from reactjs.org
const ChevronSvg = ({ size = 10, className = '', style = {} }) => (
<svg
className={className}
style={style}
viewBox="0 0 926.23699 573.74994"
version="1.1"
x="0px"
y="0px"
width={size}
height={size}
>
<g transform="translate(904.92214,-879.1482)">
<path
d={`
m -673.67664,1221.6502 -231.2455,-231.24803 55.6165,
-55.627 c 30.5891,-30.59485 56.1806,-55.627 56.8701,-55.627 0.6894,
0 79.8637,78.60862 175.9427,174.68583 l 174.6892,174.6858 174.6892,
-174.6858 c 96.079,-96.07721 175.253196,-174.68583 175.942696,
-174.68583 0.6895,0 26.281,25.03215 56.8701,
55.627 l 55.6165,55.627 -231.245496,231.24803 c -127.185,127.1864
-231.5279,231.248 -231.873,231.248 -0.3451,0 -104.688,
-104.0616 -231.873,-231.248 z
`}
fill="currentColor"
/>
</g>
</svg>
);

const ChevronContainer = styled.div`
user-select: none;
width: 20px;
height: 20px;
align-self: center;
display: flex;
flex-direction: column;
color: ${props => props.theme.colors.main};
`;
127 changes: 127 additions & 0 deletions src/components/StickySidebar/StickyResponsiveSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { observer } from 'mobx-react';
import * as React from 'react';

import { MenuStore } from '../../services/MenuStore';
import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions';
import styled, { media, withProps } from '../../styled-components';
import { ComponentWithOptions } from '../OptionsProvider';
import { AnimatedChevronButton } from './ChevronSvg';

let Stickyfill;
if (typeof window !== 'undefined') {
Stickyfill = require('stickyfill').default;
}

export interface StickySidebarProps {
className?: string;
scrollYOffset?: RedocRawOptions['scrollYOffset']; // passed directly or via context
menu: MenuStore;
}

const stickyfill = Stickyfill && Stickyfill();

const StyledStickySidebar = withProps<{ open?: boolean }>(styled.div)`
width: ${props => props.theme.menu.width};
background-color: ${props => props.theme.menu.backgroundColor};
overflow: hidden;
display: flex;
flex-direction: column;
transform: translateZ(0);
height: 100vh;
position: sticky;
position: -webkit-sticky;
top: 0;
${media.lessThan('small')`
position: fixed;
z-index: 20;
width: 100%;
background: #ffffff;
display: ${props => (props.open ? 'flex' : 'none')};
`};
`;

const FloatingButton = styled.div`
outline: none;
user-select: none;
background-color: #f2f2f2;
color: ${props => props.theme.colors.main};
display: none;
cursor: pointer;
position: fixed;
right: 20px;
z-index: 100;
border-radius: 50%;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
${media.lessThan('small')`
display: flex;
`};
bottom: 44px;
width: 60px;
height: 60px;
padding: 0 20px;
`;

@observer
export class StickyResponsiveSidebar extends ComponentWithOptions<StickySidebarProps> {
stickyElement: Element;

componentDidMount() {
if (stickyfill) {
stickyfill.add(this.stickyElement);
}
}

componentWillUnmount() {
if (stickyfill) {
stickyfill.remove(this.stickyElement);
}
}

get scrollYOffset() {
let top;
if (this.props.scrollYOffset !== undefined) {
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
} else {
top = this.options.scrollYOffset();
}
return top + 'px';
}

render() {
const top = this.scrollYOffset;
const open = this.props.menu.sideBarOpened;

const height = `calc(100vh - ${top})`;

return (
<>
<StyledStickySidebar
open={open}
className={this.props.className}
style={{ top, height }}
// tslint:disable-next-line
innerRef={el => {
this.stickyElement = el;
}}
>
{this.props.children}
</StyledStickySidebar>
<FloatingButton onClick={this.toggleNavMenu}>
<AnimatedChevronButton open={open} />
</FloatingButton>
</>
);
}

private toggleNavMenu = () => {
this.props.menu.toggleSidebar();
};

// private closeNavMenu = () => {
// this.setState({ open: false });
// };
}
76 changes: 0 additions & 76 deletions src/components/StickySidebar/StickySidebar.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions src/services/MenuStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export class MenuStore {
*/
activeItemIdx: number = -1;

/**
* whether sidebar with menu is opened or not
*/
@observable sideBarOpened: boolean = false;

/**
* cached flattened menu items to support absolute indexing
*/
Expand All @@ -56,6 +61,16 @@ export class MenuStore {
this._hashUnsubscribe = HistoryService.subscribe(this.updateOnHash);
}

@action
toggleSidebar() {
this.sideBarOpened = this.sideBarOpened ? false : true;
}

@action
closeSidebar() {
this.sideBarOpened = false;
}

/**
* top level menu items (not flattened)
*/
Expand Down Expand Up @@ -224,6 +239,9 @@ export class MenuStore {
activateAndScroll(item: IMenuItem | undefined, updateHash: boolean, rewriteHistory?: boolean) {
this.activate(item, updateHash, rewriteHistory);
this.scrollToActive();
if (!item || !item.items.length) {
this.closeSidebar();
}
}

/**
Expand Down

0 comments on commit 3aab2d9

Please sign in to comment.