diff --git a/demo/playground/index.html b/demo/playground/index.html index 7a3a5092..d322ee3e 100644 --- a/demo/playground/index.html +++ b/demo/playground/index.html @@ -22,4 +22,4 @@ - + \ No newline at end of file diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index e0aaa7d5..329fc644 100644 --- a/src/components/ApiInfo/ApiInfo.tsx +++ b/src/components/ApiInfo/ApiInfo.tsx @@ -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'; diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx index 95220a15..272d69a9 100644 --- a/src/components/Redoc/Redoc.tsx +++ b/src/components/Redoc/Redoc.tsx @@ -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 { - + - + diff --git a/src/components/StickySidebar/ChevronSvg.tsx b/src/components/StickySidebar/ChevronSvg.tsx new file mode 100644 index 00000000..5afb677b --- /dev/null +++ b/src/components/StickySidebar/ChevronSvg.tsx @@ -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 ( + + + + + ); +}; + +// adapted from reactjs.org +const ChevronSvg = ({ size = 10, className = '', style = {} }) => ( + + + + + +); + +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}; +`; diff --git a/src/components/StickySidebar/StickyResponsiveSidebar.tsx b/src/components/StickySidebar/StickyResponsiveSidebar.tsx new file mode 100644 index 00000000..7383ca43 --- /dev/null +++ b/src/components/StickySidebar/StickyResponsiveSidebar.tsx @@ -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 { + 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 ( + <> + { + this.stickyElement = el; + }} + > + {this.props.children} + + + + + + ); + } + + private toggleNavMenu = () => { + this.props.menu.toggleSidebar(); + }; + + // private closeNavMenu = () => { + // this.setState({ open: false }); + // }; +} diff --git a/src/components/StickySidebar/StickySidebar.tsx b/src/components/StickySidebar/StickySidebar.tsx deleted file mode 100644 index 5d327045..00000000 --- a/src/components/StickySidebar/StickySidebar.tsx +++ /dev/null @@ -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 { - 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 ( - { - this.stickyElement = el; - }} - > - {this.props.children} - - ); - } -} diff --git a/src/services/MenuStore.ts b/src/services/MenuStore.ts index 4a1cb3fd..401c3e64 100644 --- a/src/services/MenuStore.ts +++ b/src/services/MenuStore.ts @@ -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(); + } } /**