Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Toc): render 6 levels of nested items #2010

Merged
merged 1 commit into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions src/components/Toc/Toc.scss

This file was deleted.

65 changes: 12 additions & 53 deletions src/components/Toc/Toc.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import * as React from 'react';

import type {AriaLabelingProps, QAProps} from '../types';
import type {QAProps} from '../types';
import {block} from '../utils/cn';
import {filterDOMProps} from '../utils/filterDOMProps';

import {TocItem} from './TocItem/TocItem';
import {TocSections} from './TocSections';
import type {TocItem as TocItemType} from './types';

import './Toc.scss';

const b = block('toc');

export interface TocProps extends AriaLabelingProps, QAProps {
export interface TocProps extends QAProps {
className?: string;
items: TocItemType[];
value?: string;
Expand All @@ -20,55 +17,17 @@ export interface TocProps extends AriaLabelingProps, QAProps {
}

export const Toc = React.forwardRef<HTMLElement, TocProps>(function Toc(props, ref) {
const {value: activeValue, items, className, onUpdate, onItemClick, qa, ...restProps} = props;
const {value: activeValue, items, className, onUpdate, qa, onItemClick} = props;

return (
<nav
{...filterDOMProps(restProps, {labelable: true})}
className={b(null, className)}
ref={ref}
data-qa={qa}
>
<ul className={b('sections')}>
{items.map(({value, content, href, items: childrenItems}) => (
<li key={value ?? href} aria-current={activeValue === value}>
<TocItem
content={content}
value={value}
href={href}
active={activeValue === value}
onClick={onUpdate}
onItemClick={onItemClick}
/>
{childrenItems && childrenItems.length > 0 && (
<ul className={b('subsections')}>
{childrenItems?.map(
({
value: childrenValue,
content: childrenContent,
href: childrenHref,
}) => (
<li
key={childrenValue ?? childrenHref}
aria-current={activeValue === childrenValue}
>
<TocItem
content={childrenContent}
value={childrenValue}
href={childrenHref}
childItem={true}
active={activeValue === childrenValue}
onClick={onUpdate}
onItemClick={onItemClick}
/>
</li>
),
)}
</ul>
)}
</li>
))}
</ul>
<nav className={b(null, className)} ref={ref} data-qa={qa}>
<TocSections
items={items}
value={activeValue}
onUpdate={onUpdate}
depth={1}
onItemClick={onItemClick}
/>
</nav>
);
});
10 changes: 7 additions & 3 deletions src/components/Toc/TocItem/TocItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ $block: '.#{variables.$ns}toc-item';
}
}

&_child {
#{$class}__section-link {
padding-inline-start: 25px;
@for $i from 1 through 6 {
$item-padding: 12px * $i;

&_depth_#{$i} {
#{$class}__section-link {
padding-inline-start: $item-padding;
}
}
}

Expand Down
28 changes: 8 additions & 20 deletions src/components/Toc/TocItem/TocItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import * as React from 'react';
import type * as React from 'react';

import {useActionHandlers} from '../../../hooks';
import {block} from '../../utils/cn';
Expand All @@ -13,43 +13,31 @@ const b = block('toc-item');
export interface TocItemProps extends TocItemType {
childItem?: boolean;
active?: boolean;
onClick?: (value: string) => void;
onItemClick?: (event: React.MouseEvent) => void;
onClick?: (event: React.MouseEvent) => void;
depth: number;
}

export const TocItem = (props: TocItemProps) => {
const {active = false, childItem = false, content, href, value, onClick, onItemClick} = props;
const {active = false, childItem = false, content, href, onClick, depth} = props;

const handleClick = React.useCallback(
(event: React.MouseEvent) => {
onItemClick?.(event);
if (value === undefined || !onClick) {
return;
}

onClick(value);
},
[onClick, onItemClick, value],
);

const {onKeyDown} = useActionHandlers(handleClick);
const {onKeyDown} = useActionHandlers(onClick);

const item =
href === undefined ? (
<div
role="button"
tabIndex={0}
className={b('section-link')}
onClick={handleClick}
onClick={onClick}
onKeyDown={onKeyDown}
>
{content}
</div>
) : (
<a href={href} onClick={handleClick} className={b('section-link')}>
<a href={href} onClick={onClick} className={b('section-link')}>
{content}
</a>
);

return <div className={b('section', {child: childItem, active})}>{item}</div>;
return <div className={b('section', {child: childItem, depth, active})}>{item}</div>;
};
1 change: 1 addition & 0 deletions src/components/Toc/TocItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TocItem';
13 changes: 13 additions & 0 deletions src/components/Toc/TocSections/TocSections.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@use '../../variables';
@use '../../../../styles/mixins.scss';

$block: '.#{variables.$ns}toc';

#{$block}__sections {
padding: 0;
margin: 0;

overflow: hidden auto;

list-style: none;
}
62 changes: 62 additions & 0 deletions src/components/Toc/TocSections/TocSections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as React from 'react';

import {block} from '../../utils/cn';
import {TocItem} from '../TocItem';
import type {TocItem as TocItemType} from '../types';

import './TocSections.scss';

const b = block('toc');

export interface TocSectionsProps {
items: TocItemType[];
value?: string;
onUpdate?: (value: string) => void;
depth?: number;
childItem?: boolean;
onItemClick?: (event: React.MouseEvent) => void;
}

export const TocSections = React.forwardRef<HTMLElement, TocSectionsProps>(
function TocSections(props) {
const {value: activeValue, items, onUpdate, childItem, depth = 1, onItemClick} = props;

if (depth > 6) {
return null;
}

return (
<ul className={b('sections')}>
{items.map(({value, content, href, items: childrenItems}) => (
<li key={value ?? href} aria-current={activeValue === value}>
<TocItem
content={content}
href={href}
active={activeValue === value}
onClick={(event: React.MouseEvent) => {
onItemClick?.(event);

if (value === undefined || !onUpdate) {
return;
}

onUpdate?.(value);
}}
childItem={childItem}
depth={depth}
/>
{childrenItems && childrenItems.length > 0 && (
<TocSections
items={childrenItems}
onUpdate={onUpdate}
childItem
depth={depth + 1}
value={activeValue}
/>
)}
</li>
))}
</ul>
);
},
);
1 change: 1 addition & 0 deletions src/components/Toc/TocSections/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TocSections';
17 changes: 17 additions & 0 deletions src/components/Toc/__stories__/Toc.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,27 @@ Default.args = {
{
value: 'control',
content: 'Disk controls',
items: [
{
value: 'floppy',
content: 'Floppy',
},
{
value: 'hard',
content: 'Hard',
items: [],
},
],
},
{
value: 'snapshots',
content: 'Disk snapshots',
items: [
{
value: 'standard',
content: 'Standard',
},
],
},
],
},
Expand Down
49 changes: 46 additions & 3 deletions src/components/Toc/__tests__/Toc.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';

import {render, screen} from '../../../../test-utils/utils';
import {Toc} from '../Toc';
const titles = ['l1Title', 'l2Title', 'l3Title', 'l4Title', 'l5Title', 'l6Title', 'l7Title'];

const defaultItems = [
{
Expand All @@ -18,12 +19,42 @@ const defaultItems = [
},
{
value: 'thirdItem',
content: 'Third item',
content: titles[0],
items: [
{
value: 'firstChildItem',
content: 'First child item',
items: [],
content: titles[1],
items: [
{
value: 'firstChildItem-depth3',
content: titles[2],
items: [
{
value: 'firstChildItem-depth4',
content: titles[3],
items: [
{
value: 'firstChildItem-depth5',
content: titles[4],
items: [
{
value: 'firstChildItem-depth6',
content: titles[5],
items: [
{
value: 'firstChildItem-depth7',
content: titles[6],
items: [],
},
],
},
],
},
],
},
],
},
],
},
{
value: 'secondChildItem',
Expand Down Expand Up @@ -160,4 +191,16 @@ describe('Toc', () => {

expect(currentItem.textContent).toBe(content);
});

test('should render 6 levels', async () => {
const value = defaultItems[0].value;

render(<Toc value={value} items={defaultItems} qa={qaId} />);

for (let i = 0; i <= 5; i++) {
expect(screen.getByText(titles[i])).toBeVisible();
}

expect(screen.queryByText(titles[6])).not.toBeInTheDocument();
});
});
Loading