mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-25 18:13:44 +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 { AppStore } from '../../services/AppStore';
|
||||||
|
|
||||||
import { MiddlePanel, Row, EmptyDarkRightPanel } from '../../common-elements/';
|
import { EmptyDarkRightPanel, MiddlePanel, Row } from '../../common-elements/';
|
||||||
import { Markdown } from '../Markdown/Markdown';
|
import { Markdown } from '../Markdown/Markdown';
|
||||||
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
|
import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes';
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { ApiLogo } from '../ApiLogo/ApiLogo';
|
||||||
import { ContentItems } from '../ContentItems/ContentItems';
|
import { ContentItems } from '../ContentItems/ContentItems';
|
||||||
import { OptionsProvider } from '../OptionsProvider';
|
import { OptionsProvider } from '../OptionsProvider';
|
||||||
import { SideMenu } from '../SideMenu/SideMenu';
|
import { SideMenu } from '../SideMenu/SideMenu';
|
||||||
import { StickySidebar } from '../StickySidebar/StickySidebar';
|
import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar';
|
||||||
import { ApiContent, RedocWrap } from './elements';
|
import { ApiContent, RedocWrap } from './elements';
|
||||||
|
|
||||||
export interface RedocProps {
|
export interface RedocProps {
|
||||||
|
@ -36,10 +36,10 @@ export class Redoc extends React.Component<RedocProps> {
|
||||||
<ThemeProvider theme={options.theme}>
|
<ThemeProvider theme={options.theme}>
|
||||||
<OptionsProvider options={options}>
|
<OptionsProvider options={options}>
|
||||||
<RedocWrap className="redoc-wrap">
|
<RedocWrap className="redoc-wrap">
|
||||||
<StickySidebar className="menu-content">
|
<StickyResponsiveSidebar menu={menu} className="menu-content">
|
||||||
<ApiLogo info={spec.info} />
|
<ApiLogo info={spec.info} />
|
||||||
<SideMenu menu={menu} />
|
<SideMenu menu={menu} />
|
||||||
</StickySidebar>
|
</StickyResponsiveSidebar>
|
||||||
<ApiContent className="api-content">
|
<ApiContent className="api-content">
|
||||||
<ApiInfo store={store} />
|
<ApiInfo store={store} />
|
||||||
<ContentItems items={menu.items as any} />
|
<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;
|
activeItemIdx: number = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether sidebar with menu is opened or not
|
||||||
|
*/
|
||||||
|
@observable sideBarOpened: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* cached flattened menu items to support absolute indexing
|
* cached flattened menu items to support absolute indexing
|
||||||
*/
|
*/
|
||||||
|
@ -56,6 +61,16 @@ export class MenuStore {
|
||||||
this._hashUnsubscribe = HistoryService.subscribe(this.updateOnHash);
|
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)
|
* top level menu items (not flattened)
|
||||||
*/
|
*/
|
||||||
|
@ -224,6 +239,9 @@ export class MenuStore {
|
||||||
activateAndScroll(item: IMenuItem | undefined, updateHash: boolean, rewriteHistory?: boolean) {
|
activateAndScroll(item: IMenuItem | undefined, updateHash: boolean, rewriteHistory?: boolean) {
|
||||||
this.activate(item, updateHash, rewriteHistory);
|
this.activate(item, updateHash, rewriteHistory);
|
||||||
this.scrollToActive();
|
this.scrollToActive();
|
||||||
|
if (!item || !item.items.length) {
|
||||||
|
this.closeSidebar();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue
Block a user