Skip to content

Commit

Permalink
feat(Toc): add component (#858)
Browse files Browse the repository at this point in the history
  • Loading branch information
chelentos authored Sep 15, 2023
1 parent 333de3a commit bafed78
Show file tree
Hide file tree
Showing 9 changed files with 447 additions and 0 deletions.
24 changes: 24 additions & 0 deletions src/components/Toc/Toc.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@use '../variables';
@use '../../../styles/mixins.scss';

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

#{$block} {
&__title {
@include mixins.text-body-2();

color: var(--g-color-text-primary);
margin-bottom: 12px;
}

&__sections,
&__subsections {
padding: 0;
margin: 0;

overflow-y: auto;
overflow-x: hidden;

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

import type {QAProps} from '../types';
import {blockNew} from '../utils/cn';

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

import './Toc.scss';

const b = blockNew('toc');

export interface TocProps extends QAProps {
className?: string;
value?: string;
onUpdate?: (value: string) => void;
items: TocItemType[];
}

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

return (
<nav className={b(null, className)} ref={ref} data-qa={qa}>
<ul className={b('sections')}>
{items.map(({value, content, href, items: childrenItems}) => (
<li key={value ?? href}>
<TocItem
content={content}
value={value}
href={href}
active={activeValue === value}
onClick={onUpdate}
/>
{childrenItems?.length && (
<ul className={b('subsections')}>
{childrenItems?.map(
({
value: childrenValue,
content: childrenContent,
href: childrenHref,
}) => (
<li key={childrenValue ?? childrenHref}>
<TocItem
content={childrenContent}
value={childrenValue}
href={childrenHref}
childItem={true}
active={activeValue === childrenValue}
onClick={onUpdate}
/>
</li>
),
)}
</ul>
)}
</li>
))}
</ul>
</nav>
);
});
54 changes: 54 additions & 0 deletions src/components/Toc/TocItem/TocItem.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@use '../../variables';

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

#{$block} {
$class: &;

&__section {
cursor: pointer;

& > #{$class}__section-link {
border-left-color: var(--g-color-line-generic);
}

&-link {
&:focus {
outline: 2px solid var(--g-color-line-focus);
outline-offset: -2px;

&:focus-visible {
border-radius: calc(var(--g-focus-border-radius) + 2px);
}
}

&:focus:not(:focus-visible) {
outline: none;
}

display: flex;
align-items: center;
padding: 6px 6px 6px 12px;
min-height: 28px;

color: var(--g-color-text-secondary);
border-left: 2px solid transparent;
text-decoration: none;

&:hover {
color: var(--g-color-text-complementary);
}
}

&_child {
#{$class}__section-link {
padding-left: 25px;
}
}

&_active > #{$class}__section-link {
color: var(--g-color-text-primary);
border-left-color: var(--g-color-line-brand);
}
}
}
48 changes: 48 additions & 0 deletions src/components/Toc/TocItem/TocItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';

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

import './TocItem.scss';

const b = blockNew('toc-item');

export interface TocItemProps extends TocItemType {
childItem?: boolean;
active?: boolean;
onClick?: (value: string) => void;
}

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

const handleClick = React.useCallback(() => {
if (value === undefined || !onClick) {
return;
}

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

const {onKeyDown} = useActionHandlers(handleClick);

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

return <div className={b('section', {child: childItem, active})}>{item}</div>;
};
9 changes: 9 additions & 0 deletions src/components/Toc/__stories__/Toc.stories.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@use '../../variables';

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

#{$block} {
$class: &;

max-width: 156px;
}
100 changes: 100 additions & 0 deletions src/components/Toc/__stories__/Toc.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';

import type {Meta, StoryFn} from '@storybook/react';

import {block} from '../../utils/cn';
import {Toc, TocProps} from '../Toc';

import './Toc.stories.scss';

const b = block('toc-stories');

export default {
title: 'Components/Toc',
component: Toc,
argTypes: {},
} as Meta;

const DefaultTemplate: StoryFn<TocProps> = (args) => {
const [active, setActive] = React.useState('control');

return <Toc {...args} value={active} onUpdate={(value: string) => setActive(value)} />;
};

export const Default = DefaultTemplate.bind({});
Default.args = {
items: [
{
value: 'vm',
content: 'Virtual machine creation',
},
{
value: 'info',
content: 'Getting information about a group of virtual machines',
},
{
value: 'disk',
content: 'Disk',
items: [
{
value: 'control',
content: 'Disk controls',
},
{
value: 'snapshots',
content: 'Disk snapshots',
},
],
},
{
value: 'images',
content: 'Images with preinstalled software',
},
],
className: b(),
};

const WithLinksTemplate: StoryFn<TocProps> = (args) => {
const [active, setActive] = React.useState('control');

return <Toc {...args} value={active} onUpdate={(value: string) => setActive(value)} />;
};

export const WithLinks = WithLinksTemplate.bind({});
WithLinks.args = {
items: [
{
value: 'vm',
content: 'Virtual machine creation',
href: '#vm',
},
{
value: 'info',
content: 'Getting information about a group of virtual machines',
href: '#info',
},
{
value: 'disk',
content: 'Disk',
href: '#disk',
items: [
{
value: 'control',
content: 'Disk controls',
href: '#control',
},
{
value: 'snapshots',
content: 'Disk snapshots',
href: '#snapshots',
},
],
},
{
value: 'images',
content: 'Images with preinstalled software',
href: '#images',
},
],
className: b(),
};
Loading

0 comments on commit bafed78

Please sign in to comment.