Skip to content

Commit

Permalink
Indicate ToC hierarchy level in table of contents presentation
Browse files Browse the repository at this point in the history
Part of #5821
  • Loading branch information
robertknight committed Nov 13, 2023
1 parent c03e0cd commit a5b040d
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 4 deletions.
13 changes: 12 additions & 1 deletion lms/static/scripts/frontend_apps/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,24 @@ export type Book = {
cover_image: string;
};

/** Metadata for a chapter within an ebook. */
/**
* Metadata for a table of contents entry within an ebook.
*
* The name "Chapter" is a misnomer. Although many ebooks do have one
* table-of-contents entry per chapter, the entries can be more or less
* fine-grained than this.
*/
export type Chapter = {
page: string;
title: string;

/** Document URL to use for this chapter when creating an assignment. */
url: string;

/**
* The nesting depth of this entry in the table of contents.
*/
level?: number;
};

export type JSTORContentItemInfo = {
Expand Down
32 changes: 29 additions & 3 deletions lms/static/scripts/frontend_apps/components/ChapterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
Scroll,
ScrollContainer,
} from '@hypothesis/frontend-shared';
import { useEffect, useMemo, useRef } from 'preact/hooks';
import { useCallback, useEffect, useMemo, useRef } from 'preact/hooks';

import type { Chapter } from '../api-types';

Expand All @@ -23,8 +23,8 @@ export type ChapterListProps = {
};

/**
* Component that presents a list of chapters from a book and allows the user
* to choose one for an assignment.
* Component that presents a book's table of contents and allows them to
* select a range for an assignment.
*/
export default function ChapterList({
chapters,
Expand Down Expand Up @@ -58,6 +58,31 @@ export default function ChapterList({
[]
);

const renderItem = useCallback((chapter: Chapter, field: keyof Chapter) => {
switch (field) {
case 'page':
return chapter.page;
case 'title': {
// DataTable doesn't have true support for hierarchical data structures.
// Here we indicate the ToC level visually by indenting rows.
const level = typeof chapter.level === 'number' ? chapter.level - 1 : 0;
return (
<>
<span
data-testid="toc-indent"
data-level={level}
style={{ display: 'inline-block', width: `${level * 20}px` }}
/>
{chapter.title}
</>
);
}
/* istanbul ignore next */
default:
return '';
}
}, []);

return (
<ScrollContainer rounded>
<Scroll>
Expand All @@ -66,6 +91,7 @@ export default function ChapterList({
title="Table of Contents"
columns={columns}
loading={isLoading}
renderItem={renderItem}
rows={chapters}
onSelectRow={onSelectChapter}
onConfirmRow={onUseChapter}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ describe('ChapterList', () => {
const chapterData = [
{
title: 'Chapter One',
level: 1,
page: '10',
},
{
title: 'Chapter Two',
level: 1,
page: '20',
},
{
title: 'Chapter Two - Part 1',
level: 2,
page: '20',
},
];
Expand Down Expand Up @@ -68,6 +75,13 @@ describe('ChapterList', () => {
assert.equal(rows.length, chapterData.length);
assert.equal(rows.at(0).find('td').at(0).text(), chapterData[0].title);
assert.equal(rows.at(0).find('td').at(1).text(), chapterData[0].page);

const tocLevels = [
rows.at(0).find('[data-testid="toc-indent"]').prop('data-level'),
rows.at(1).find('[data-testid="toc-indent"]').prop('data-level'),
rows.at(2).find('[data-testid="toc-indent"]').prop('data-level'),
];
assert.deepEqual(tocLevels, [0, 0, 1]);
});

[true, false].forEach(isLoading => {
Expand Down

0 comments on commit a5b040d

Please sign in to comment.