From cd3d86fc18f42a0a23a3fd7df6de579dfd616ef9 Mon Sep 17 00:00:00 2001 From: Sofiya Pavlenko Date: Mon, 23 Dec 2024 19:05:54 +0300 Subject: [PATCH] feat(Toc): render 6 levels of nested items --- src/components/Toc/Toc.scss | 23 ------ src/components/Toc/Toc.tsx | 52 +++----------- src/components/Toc/TocItem/TocItem.scss | 10 ++- src/components/Toc/TocItem/TocItem.tsx | 32 ++++++--- src/components/Toc/TocItem/index.ts | 1 + .../Toc/TocSections/TocSections.scss | 13 ++++ .../Toc/TocSections/TocSections.tsx | 55 ++++++++++++++ src/components/Toc/TocSections/index.ts | 1 + .../Toc/__stories__/Toc.stories.tsx | 17 +++++ src/components/Toc/__tests__/Toc.test.tsx | 71 ++++++++++++++++--- 10 files changed, 189 insertions(+), 86 deletions(-) delete mode 100644 src/components/Toc/Toc.scss create mode 100644 src/components/Toc/TocItem/index.ts create mode 100644 src/components/Toc/TocSections/TocSections.scss create mode 100644 src/components/Toc/TocSections/TocSections.tsx create mode 100644 src/components/Toc/TocSections/index.ts diff --git a/src/components/Toc/Toc.scss b/src/components/Toc/Toc.scss deleted file mode 100644 index 14b59f6d35..0000000000 --- a/src/components/Toc/Toc.scss +++ /dev/null @@ -1,23 +0,0 @@ -@use '../variables'; -@use '../../../styles/mixins.scss'; - -$block: '.#{variables.$ns}toc'; - -#{$block} { - &__title { - @include mixins.text-body-2(); - - color: var(--g-color-text-primary); - margin-block-end: 12px; - } - - &__sections, - &__subsections { - padding: 0; - margin: 0; - - overflow: hidden auto; - - list-style: none; - } -} diff --git a/src/components/Toc/Toc.tsx b/src/components/Toc/Toc.tsx index a29575dbe1..0191c3e3c7 100644 --- a/src/components/Toc/Toc.tsx +++ b/src/components/Toc/Toc.tsx @@ -3,11 +3,9 @@ import React from 'react'; import type {QAProps} from '../types'; import {block} from '../utils/cn'; -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 QAProps { @@ -15,51 +13,21 @@ export interface TocProps extends QAProps { items: TocItemType[]; value?: string; onUpdate?: (value: string) => void; + onItemClick?: (event: React.MouseEvent) => void; } export const Toc = React.forwardRef(function Toc(props, ref) { - const {value: activeValue, items, className, onUpdate, qa} = props; + const {value: activeValue, items, className, onUpdate, qa, onItemClick} = props; return ( ); }); diff --git a/src/components/Toc/TocItem/TocItem.scss b/src/components/Toc/TocItem/TocItem.scss index eeda96d709..f123b39523 100644 --- a/src/components/Toc/TocItem/TocItem.scss +++ b/src/components/Toc/TocItem/TocItem.scss @@ -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; + } } } diff --git a/src/components/Toc/TocItem/TocItem.tsx b/src/components/Toc/TocItem/TocItem.tsx index d67c0cf3df..9ecb6fcc10 100644 --- a/src/components/Toc/TocItem/TocItem.tsx +++ b/src/components/Toc/TocItem/TocItem.tsx @@ -14,18 +14,32 @@ export interface TocItemProps extends TocItemType { childItem?: boolean; active?: boolean; onClick?: (value: string) => void; + depth: number; + onItemClick?: (event: React.MouseEvent) => void; } export const TocItem = (props: TocItemProps) => { - const {active = false, childItem = false, content, href, value, onClick} = props; + const { + active = false, + childItem = false, + content, + href, + value, + onClick, + onItemClick, + depth, + } = props; - const handleClick = React.useCallback(() => { - if (value === undefined || !onClick) { - return; - } - - onClick(value); - }, [onClick, value]); + const handleClick = React.useCallback( + (event: React.MouseEvent) => { + onItemClick?.(event); + if (value === undefined || !onClick) { + return; + } + onClick(value); + }, + [onClick, onItemClick, value], + ); const {onKeyDown} = useActionHandlers(handleClick); @@ -46,5 +60,5 @@ export const TocItem = (props: TocItemProps) => { ); - return
{item}
; + return
{item}
; }; diff --git a/src/components/Toc/TocItem/index.ts b/src/components/Toc/TocItem/index.ts new file mode 100644 index 0000000000..74cdc8aad0 --- /dev/null +++ b/src/components/Toc/TocItem/index.ts @@ -0,0 +1 @@ +export * from './TocItem'; diff --git a/src/components/Toc/TocSections/TocSections.scss b/src/components/Toc/TocSections/TocSections.scss new file mode 100644 index 0000000000..8c73bf98e0 --- /dev/null +++ b/src/components/Toc/TocSections/TocSections.scss @@ -0,0 +1,13 @@ +@use '../../variables'; +@use '../../../../styles/mixins.scss'; + +$block: '.#{variables.$ns}toc-sections'; + +#{$block} { + padding: 0; + margin: 0; + + overflow: hidden auto; + + list-style: none; +} diff --git a/src/components/Toc/TocSections/TocSections.tsx b/src/components/Toc/TocSections/TocSections.tsx new file mode 100644 index 0000000000..699311f336 --- /dev/null +++ b/src/components/Toc/TocSections/TocSections.tsx @@ -0,0 +1,55 @@ +import 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-sections'); + +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(function Toc(props) { + const {value: activeValue, items, onUpdate, childItem, depth = 1, onItemClick} = props; + + if (depth > 6) { + return null; + } + + return ( +
    + {items.map(({value, content, href, items: childrenItems}) => ( +
  • + + {childrenItems && childrenItems.length > 0 && ( + + )} +
  • + ))} +
+ ); +}); diff --git a/src/components/Toc/TocSections/index.ts b/src/components/Toc/TocSections/index.ts new file mode 100644 index 0000000000..b1600b6f47 --- /dev/null +++ b/src/components/Toc/TocSections/index.ts @@ -0,0 +1 @@ +export * from './TocSections'; diff --git a/src/components/Toc/__stories__/Toc.stories.tsx b/src/components/Toc/__stories__/Toc.stories.tsx index 8acbe463d1..0211b78554 100644 --- a/src/components/Toc/__stories__/Toc.stories.tsx +++ b/src/components/Toc/__stories__/Toc.stories.tsx @@ -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', + }, + ], }, ], }, diff --git a/src/components/Toc/__tests__/Toc.test.tsx b/src/components/Toc/__tests__/Toc.test.tsx index 5daa2f66a8..8ea0c62445 100644 --- a/src/components/Toc/__tests__/Toc.test.tsx +++ b/src/components/Toc/__tests__/Toc.test.tsx @@ -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 = [ { @@ -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', @@ -52,13 +83,22 @@ describe('Toc', () => { const nextValue = defaultItems[0].value; const nextTitle = defaultItems[0].content; const onUpdateFn = jest.fn(); + const onItemClickFn = jest.fn(); const user = userEvent.setup(); - render(); + render( + , + ); const nextItem = screen.getByText(nextTitle); await user.click(nextItem); - expect(onUpdateFn).toBeCalledWith(nextValue); + expect(onUpdateFn).toHaveBeenCalledWith(nextValue); + expect(onItemClickFn).toHaveBeenCalledWith(expect.objectContaining({})); }); test('calls onUpdate with correct item with link', async () => { @@ -71,13 +111,14 @@ describe('Toc', () => { const nextItem = screen.getByText(nextTitle); await user.click(nextItem); - expect(onUpdateFn).toBeCalledWith(nextValue); + expect(onUpdateFn).toHaveBeenCalledWith(nextValue); }); test('accessible for keyboard', async () => { const firstTitle = defaultItems[0].content; const secondValue = defaultItems[1].value; const onUpdateFn = jest.fn(); + const user = userEvent.setup(); render(); @@ -86,7 +127,7 @@ describe('Toc', () => { await user.tab(); await user.keyboard('{Enter}'); - expect(onUpdateFn).toBeCalledWith(secondValue); + expect(onUpdateFn).toHaveBeenCalledWith(secondValue); }); test('accessible for keyboard with links', async () => { @@ -101,7 +142,7 @@ describe('Toc', () => { await user.tab(); await user.keyboard('{Enter}'); - expect(onUpdateFn).toBeCalledWith(secondValue); + expect(onUpdateFn).toHaveBeenCalledWith(secondValue); }); test('add className', () => { @@ -148,6 +189,18 @@ describe('Toc', () => { const currentItem = screen.getByRole('listitem', {current: true}); - expect(currentItem.textContent).toContain(content); + expect(currentItem.textContent).toBe(content); + }); + + test('should render 6 levels', async () => { + const value = defaultItems[0].value; + + render(); + + for (let i = 0; i <= 5; i++) { + expect(screen.getByText(titles[i])).toBeVisible(); + } + + expect(screen.queryByText(titles[6])).not.toBeInTheDocument(); }); });