diff --git a/src/app/content/components/TableOfContents/index.spec.tsx b/src/app/content/components/TableOfContents/index.spec.tsx index c84571d857..b4dee97b32 100644 --- a/src/app/content/components/TableOfContents/index.spec.tsx +++ b/src/app/content/components/TableOfContents/index.spec.tsx @@ -13,6 +13,7 @@ import * as actions from '../../actions'; import { initialState } from '../../reducer'; import { formatBookData } from '../../utils'; import * as domUtils from '../../utils/domUtils'; +import * as reactUtils from '../../../reactUtils'; const book = formatBookData(archiveBook, mockCmsBook); @@ -64,6 +65,8 @@ describe('TableOfContents', () => { jest.useFakeTimers(); it('opens and closes', () => { + jest.spyOn(reactUtils, 'useMatchMobileMediumQuery') + .mockReturnValue(true); const component = renderer.create( ); diff --git a/src/app/content/components/TableOfContents/index.tsx b/src/app/content/components/TableOfContents/index.tsx index 25534f9abb..5a2a97c166 100644 --- a/src/app/content/components/TableOfContents/index.tsx +++ b/src/app/content/components/TableOfContents/index.tsx @@ -1,5 +1,5 @@ import { HTMLElement } from '@openstax/types/lib.dom'; -import React, { Component } from 'react'; +import React, { Component, MutableRefObject } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { connect } from 'react-redux'; import { AppState, Dispatch } from '../../../types'; @@ -16,6 +16,7 @@ import { Header, HeaderText, SidebarPaneBody } from '../SidebarPane'; import { LeftArrow, TimesIcon } from '../Toolbar/styled'; import * as Styled from './styled'; import { assertWindow } from '../../../utils'; +import { createTrapTab, useMatchMobileMediumQuery } from '../../../reactUtils'; interface SidebarProps { onNavigate: () => void; @@ -24,28 +25,160 @@ interface SidebarProps { page?: Page; } +function TabTrapper({ + mRef, + isTocOpen, +}: { + mRef: MutableRefObject; + isTocOpen: boolean; +}) { + const isMobile = useMatchMobileMediumQuery(); + + React.useEffect(() => { + if (!isMobile) { + return; + } + const listener = createTrapTab(mRef); + + if (isTocOpen) { + document?.addEventListener('keydown', listener, true); + } else { + document?.removeEventListener('keydown', listener, true); + } + + return () => document?.removeEventListener('keydown', listener, true); + }, [mRef, isTocOpen, isMobile]); + + return null; +} + // tslint:disable-next-line:variable-name -const SidebarBody = React.forwardRef>( - (props, ref) => -); +const SidebarBody = React.forwardRef< + HTMLElement, + React.ComponentProps +>((props, ref) => { + return ( + + {typeof window !== 'undefined' && ( + } + isTocOpen={props.isTocOpen} + /> + )} + + + ); +}); + +function TocHeader() { + return ( +
+ + + {(msg) => {msg}} + + + +
+ ); +} + +function TocNode({ + defaultOpen, + title, + children, +}: React.PropsWithChildren<{ defaultOpen: boolean; title: string }>) { + return ( + + + + + + + + + {children} + + ); +} + +function TocSection({ + book, + page, + section, + activeSection, + onNavigate, +}: { + book: Book | undefined; + page: Page | undefined; + section: ArchiveTree; + activeSection: React.RefObject; + onNavigate: () => void; +}) { + return ( + + {linkContents(section).map((item) => { + const sectionType = getArchiveTreeSectionType(item); + const active = page && stripIdVersion(item.id) === page.id; + + return isArchiveTree(item) + ? + + + + + : + + ; + })} + + ); +} + +function shouldBeOpen(page: Page | undefined, node: ArchiveTree) { + return Boolean(page && archiveTreeContainsNode(node, page.id)); +} export class TableOfContents extends Component { public sidebar = React.createRef(); public activeSection = React.createRef(); public render() { - const {isOpen, book} = this.props; + const { isOpen, book } = this.props; - return - {this.renderTocHeader()} - {book && this.renderToc(book)} - ; + return ( + + + {book && ( + + )} + + ); } public componentDidMount() { @@ -89,61 +222,6 @@ export class TableOfContents extends Component { private scrollToSelectedPage() { scrollSidebarSectionIntoView(this.sidebar.current, this.activeSection.current); } - - private renderChildren = (book: Book, section: ArchiveTree) => - - {linkContents(section).map((item) => { - const sectionType = getArchiveTreeSectionType(item); - const active = this.props.page && stripIdVersion(item.id) === this.props.page.id; - - return isArchiveTree(item) - ? - {this.renderTocNode(book, item)} - - : - - ; - })} - ; - - private renderTocNode = (book: Book, node: ArchiveTree) => - - - - - - - - {this.renderChildren(book, node)} - ; - - private renderTocHeader = () => { - return
- - - {(msg) => {msg}} - - - -
; - }; - - private renderToc = (book: Book) => this.renderChildren(book, book.tree); } export default connect(