mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-22 08:36:33 +03:00
feat: responsive side menu
This commit is contained in:
parent
a29c3ccc2d
commit
3aab2d97d3
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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} />
|
||||
|
|
66
src/components/StickySidebar/ChevronSvg.tsx
Normal file
66
src/components/StickySidebar/ChevronSvg.tsx
Normal file
|
@ -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
src/components/StickySidebar/StickyResponsiveSidebar.tsx
Normal file
127
src/components/StickySidebar/StickyResponsiveSidebar.tsx
Normal file
|
@ -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 });
|
||||
// };
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions';
|
||||
import styled from '../../styled-components';
|
||||
import { ComponentWithOptions } from '../OptionsProvider';
|
||||
|
||||
let Stickyfill;
|
||||
if (typeof window !== 'undefined') {
|
||||
Stickyfill = require('stickyfill').default;
|
||||
}
|
||||
|
||||
export interface StickySidebarProps {
|
||||
className?: string;
|
||||
scrollYOffset?: RedocRawOptions['scrollYOffset']; // passed directly or via context
|
||||
}
|
||||
|
||||
const stickyfill = Stickyfill && Stickyfill();
|
||||
|
||||
const StyledStickySidebar = 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;
|
||||
`;
|
||||
|
||||
export class StickySidebar 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 height = `calc(100vh - ${top})`;
|
||||
|
||||
return (
|
||||
<StyledStickySidebar
|
||||
className={this.props.className}
|
||||
style={{ top, height }}
|
||||
// tslint:disable-next-line
|
||||
innerRef={el => {
|
||||
this.stickyElement = el;
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</StyledStickySidebar>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
|
@ -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)
|
||||
*/
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user