Skip to content

Commit 3ccebba

Browse files
author
Jakub Jankowski
authored
feat: Display endpoint method and icons for schema and API (#195)
* feat: toc group item and icons * fix: collapsing nested items * fix: storybook icons * chore: update fixture * chore: more margin * chore: cleanup
1 parent 741f5b8 commit 3ccebba

4 files changed

Lines changed: 131 additions & 18 deletions

File tree

.storybook/preview-head.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<!-- .storybook/preview-head.html -->
2+
3+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css">

src/TableOfContents/index.tsx

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export type TableOfContentsItem = {
1919
icon?: FAIconProp;
2020
activeIcon?: FAIconProp;
2121
iconColor?: string;
22+
iconPosition?: 'left' | 'right';
23+
textIcon?: string;
2224
isLoading?: boolean;
2325
isDisabled?: boolean;
2426
showSkeleton?: boolean;
@@ -107,13 +109,24 @@ function TableOfContentsInner<T extends TableOfContentsItem = TableOfContentsIte
107109

108110
// an array of functions. Invoking the N-th function toggles the expanded flag on the N-th content item
109111
const toggleExpandedFunctions = React.useMemo(() => {
110-
return range(contents.length).map(i => () =>
111-
setExpanded(current => ({
112-
...current,
113-
[i]: !current[i],
114-
})),
115-
);
116-
}, [contents.length]);
112+
return range(contents.length).map(i => () => {
113+
setExpanded(current => {
114+
let childrenToCollapse = {};
115+
if (current[i]) {
116+
const item = contents[i];
117+
const children = findDescendantIndices(item.depth ?? 0, i, contents.slice(i + 1));
118+
childrenToCollapse = Object.fromEntries(children.map(i => [i, false]));
119+
}
120+
121+
return {
122+
...current,
123+
[i]: !current[i],
124+
...childrenToCollapse,
125+
};
126+
});
127+
});
128+
// eslint-disable-next-line react-hooks/exhaustive-deps
129+
}, [contents, contents.length]);
117130

118131
// expand ancestors of active items by default
119132
React.useEffect(() => {
@@ -203,6 +216,7 @@ export function TableOfContents<T extends TableOfContentsItem = TableOfContentsI
203216

204217
function DefaultRowImpl<T extends TableOfContentsItem>({ item, isExpanded, toggleExpanded }: RowComponentProps<T>) {
205218
const isGroup = item.type === 'group';
219+
const isGroupItem = isGroup && isTableOfContentsLink(item);
206220
const isChild = item.type !== 'group' && (item.depth ?? 0) > 0;
207221
const isDivider = item.type === 'divider';
208222
const showSkeleton = item.showSkeleton;
@@ -239,7 +253,7 @@ function DefaultRowImpl<T extends TableOfContentsItem>({ item, isExpanded, toggl
239253
return;
240254
}
241255

242-
if (!isGroup) return;
256+
if (!isGroup || isGroupItem) return;
243257

244258
e.preventDefault();
245259
toggleExpanded();
@@ -290,6 +304,27 @@ function DefaultRowImpl<T extends TableOfContentsItem>({ item, isExpanded, toggl
290304
/>
291305
) : null;
292306

307+
const iconElem = icon ? (
308+
<FAIcon
309+
className={cn('fa-fw', {
310+
'mr-3': item.iconPosition !== 'right',
311+
'mx-1': item.iconPosition === 'right',
312+
'text-blue-6': isSelected,
313+
[`text-${item.iconColor}`]: item.iconColor,
314+
'bp3-skeleton': item.showSkeleton,
315+
})}
316+
icon={icon}
317+
/>
318+
) : item.textIcon ? (
319+
<div
320+
className={cn('text-right rounded px-1 text-xs uppercase', {
321+
[`text-${item.iconColor}`]: item.iconColor,
322+
})}
323+
>
324+
{item.textIcon}
325+
</div>
326+
) : null;
327+
293328
return (
294329
<div
295330
onClick={onClick}
@@ -299,12 +334,7 @@ function DefaultRowImpl<T extends TableOfContentsItem>({ item, isExpanded, toggl
299334
>
300335
<div className={cn('-ml-px', innerClassName, { 'opacity-75': isDisabled })}>
301336
<div className="flex flex-row items-center">
302-
{icon && (
303-
<FAIcon
304-
className={cn('mr-3 fa-fw', { 'text-blue-6': isSelected, 'bp3-skeleton': item.showSkeleton })}
305-
icon={icon}
306-
/>
307-
)}
337+
{item.iconPosition !== 'right' && iconElem}
308338

309339
<span className={cn('TableOfContentsItem__name flex-1 truncate', { 'bp3-skeleton': item.showSkeleton })}>
310340
{item.name}
@@ -313,11 +343,11 @@ function DefaultRowImpl<T extends TableOfContentsItem>({ item, isExpanded, toggl
313343
{item.meta && <span className="text-sm text-left text-gray font-medium">{item.meta}</span>}
314344
{loadingElem}
315345
{actionElem}
346+
{item.iconPosition === 'right' && iconElem}
316347
{isGroup && (
317-
<FAIcon
318-
className="TableOfContentsItem__icon"
319-
icon={['far', isExpanded ? 'chevron-down' : 'chevron-right']}
320-
/>
348+
<div onClick={isGroupItem ? toggleExpanded : undefined} className="px-2">
349+
<FAIcon className="TableOfContentsItem__icon" icon={isExpanded ? 'chevron-down' : 'chevron-right'} />
350+
</div>
321351
)}
322352
</div>
323353
{item.footer}
@@ -354,6 +384,23 @@ function findAncestorIndices(currentDepth: number, precedingContents: TableOfCon
354384
];
355385
}
356386

387+
function findDescendantIndices(
388+
currentDepth: number,
389+
currentIndex: number,
390+
succeedingContents: TableOfContentsItem[],
391+
): number[] {
392+
const children: number[] = [];
393+
for (let index = 0; index < succeedingContents.length; index++) {
394+
if ((succeedingContents[index].depth ?? 0) <= currentDepth) {
395+
break;
396+
} else {
397+
children.push(currentIndex + index);
398+
}
399+
}
400+
401+
return children;
402+
}
403+
357404
function isExternalLink(item: TableOfContentsItem): boolean {
358405
return isTableOfContentsLink(item) && item.to !== void 0 && /^(http|#|mailto)/.test(item.to);
359406
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ITableOfContentsLink } from '../../TableOfContents';
2+
3+
export const tree: ITableOfContentsLink[] = [
4+
{
5+
name: 'Tree',
6+
depth: 0,
7+
type: 'divider',
8+
},
9+
{
10+
name: 'Group with link',
11+
depth: 0,
12+
type: 'group',
13+
to: '/path',
14+
icon: 'cloud',
15+
},
16+
{
17+
name: 'Nested Item with text icon',
18+
depth: 1,
19+
type: 'item',
20+
textIcon: 'ONE',
21+
iconColor: 'red',
22+
iconPosition: 'right',
23+
},
24+
{
25+
name: 'Nested Group',
26+
depth: 1,
27+
type: 'group',
28+
},
29+
{
30+
name: 'Nested Group',
31+
depth: 2,
32+
type: 'group',
33+
},
34+
{
35+
name: 'Nested Item',
36+
depth: 3,
37+
type: 'item',
38+
textIcon: 'TWO',
39+
iconColor: 'blue',
40+
iconPosition: 'right',
41+
},
42+
{
43+
name: 'Nested Group',
44+
depth: 1,
45+
type: 'group',
46+
},
47+
{
48+
name: 'Nested Item',
49+
depth: 2,
50+
type: 'item',
51+
textIcon: 'THREE',
52+
iconColor: 'green',
53+
iconPosition: 'left',
54+
},
55+
];

src/__stories__/TableOfContents/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { storiesOf } from '@storybook/react';
44
import * as React from 'react';
55

66
import { studioContents } from '../../__fixtures__/table-of-contents/studio';
7+
import { tree } from '../../__fixtures__/table-of-contents/tree';
78
import { DefaultRow, ITableOfContentsLink, RowComponentType, TableOfContents } from '../../TableOfContents';
89

910
const styles = {
@@ -29,6 +30,13 @@ storiesOf('TableOfContents', module)
2930
</div>
3031
);
3132
})
33+
.add('nested tree', () => {
34+
return (
35+
<div style={styles}>
36+
<TableOfContents className="h-full" contents={tree} />
37+
</div>
38+
);
39+
})
3240
.add('mobile', () => {
3341
return <MobileStory />;
3442
});

0 commit comments

Comments
 (0)