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(