Skip to content

Commit

Permalink
feat(DefinitionList): support grouped items (#191)
Browse files Browse the repository at this point in the history
Co-authored-by: Elena Makarova <[email protected]>
  • Loading branch information
Raubzeug and Elena Makarova authored May 28, 2024
1 parent d492cc4 commit 27bc3f6
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 138 deletions.
21 changes: 19 additions & 2 deletions src/components/DefinitionList/DefinitionList.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,20 @@
$block: '.#{variables.$ns}definition-list';

#{$block} {
$class: &;

margin: 0;

&__title {
margin-block-end: var(--g-spacing-3);

&:not(:first-of-type) {
margin-block-start: var(--g-spacing-5);
}
}

#{$block}__item:is(#{$block}__item_grouped) + #{$block}__item:not(#{$block}__item_grouped) {
margin-block-start: var(--g-spacing-5);
}

&__item {
display: flex;
align-items: baseline;
Expand All @@ -18,6 +28,12 @@ $block: '.#{variables.$ns}definition-list';
}
}

&__item_grouped {
& + & {
margin-block-start: var(--g-spacing-3);
}
}

&__term-container {
flex: 0 0 300px;
display: flex;
Expand Down Expand Up @@ -73,6 +89,7 @@ $block: '.#{variables.$ns}definition-list';
&__copy-container {
position: relative;
display: inline-flex;
align-items: center;
padding-inline-end: var(--g-spacing-7);

margin-inline-end: calc(-1 * var(--g-spacing-7));
Expand Down
189 changes: 57 additions & 132 deletions src/components/DefinitionList/DefinitionList.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,13 @@
import React from 'react';

import {ClipboardButton, QAProps} from '@gravity-ui/uikit';

import {HelpPopover} from '../HelpPopover';
import type {HelpPopoverProps} from '../HelpPopover';
import {block} from '../utils/cn';

import i18n from './i18n';
import {isUnbreakableOver} from './utils';
import {Definition} from './components/Definition';
import {GroupLabel} from './components/GroupLabel';
import {Term} from './components/Term';
import {DefinitionListProps} from './types';
import {b, getFlattenItems, getTitle, isGroup, isUnbreakableOver} from './utils';

import './DefinitionList.scss';

type DefinitionListItemNote = string | HelpPopoverProps;

export interface DefinitionListItem {
name: React.ReactNode;
content?: React.ReactNode;
contentTitle?: string;
nameTitle?: string;
copyText?: string;
note?: DefinitionListItemNote;
multilineName?: boolean;
}

export interface DefinitionListProps extends QAProps {
items: DefinitionListItem[];
copyPosition?: 'inside' | 'outside';
responsive?: boolean;
nameMaxWidth?: number;
contentMaxWidth?: number | 'auto';
className?: string;
itemClassName?: string;
}

export const b = block('definition-list');

function getTitle(title?: string, content?: React.ReactNode) {
if (title) {
return title;
}

if (typeof content === 'string' || typeof content === 'number') {
return String(content);
}

return undefined;
}

function getNoteElement(note?: DefinitionListItemNote) {
let noteElement = null;
const popoverClassName = b('item-note-tooltip');
if (note) {
if (typeof note === 'string') {
noteElement = (
<HelpPopover
className={popoverClassName}
content={note}
placement={['bottom', 'top']}
buttonProps={{
'aria-label': i18n('label_note'),
}}
/>
);
}

if (typeof note === 'object') {
noteElement = (
<HelpPopover
className={popoverClassName}
placement={['bottom', 'top']}
{...note}
buttonProps={{
'aria-label': i18n('label_note'),
...note.buttonProps,
}}
/>
);
}
}
return noteElement;
}

export function DefinitionList({
items,
responsive,
Expand All @@ -104,65 +31,63 @@ export function DefinitionList({
maxWidth: contentMaxWidth,
}
: {};

const normalizedItems = React.useMemo(() => {
return items.map((value, index) => ({...value, key: index}));
return getFlattenItems(items).map((value, index) => ({...value, key: index}));
}, [items]);

return (
<dl className={b({responsive}, className)} data-qa={qa}>
{normalizedItems.map(
({name, key, content, contentTitle, nameTitle, copyText, note, multilineName}) => {
const definitionContent = content ?? '—';
const iconInside = copyPosition === 'inside';
const definition = copyText ? (
<div className={b('copy-container', {'icon-inside': iconInside})}>
<span>{definitionContent}</span>
<ClipboardButton
size="s"
text={copyText}
className={b('copy-button')}
view={iconInside ? 'raised' : 'flat-secondary'}
{normalizedItems.map((item) => {
if (isGroup(item)) {
const {key, label} = item;
return <GroupLabel key={key} label={label} />;
}
const {
name,
key,
content,
contentTitle,
nameTitle,
copyText,
note,
multilineName,
isGrouped,
} = item;

return (
<div key={key} className={b('item', {grouped: isGrouped}, itemClassName)}>
<dt
className={b('term-container', {multiline: multilineName})}
style={keyStyle}
>
<Term
name={name}
nameTitle={nameTitle}
note={note}
multilineName={multilineName}
/>
</dt>
<dd
className={b('definition')}
title={getTitle(contentTitle, content)}
style={{
...valueStyle,
lineBreak:
typeof content === 'string' && isUnbreakableOver(20)(content)
? 'anywhere'
: undefined,
}}
>
<Definition
copyPosition={copyPosition}
copyText={copyText}
content={content}
/>
</div>
) : (
definitionContent
);
const noteElement = (
<React.Fragment>
&nbsp;
{getNoteElement(note)}
</React.Fragment>
);
return (
<div key={key} className={b('item', itemClassName)}>
<dt
className={b('term-container', {multiline: multilineName})}
style={keyStyle}
>
<div className={b('term-wrapper')}>
<span title={getTitle(nameTitle, name)}>{name}</span>
{multilineName && noteElement}
</div>
{!multilineName && noteElement}
<div className={b('dots', {'with-note': Boolean(note)})} />
</dt>
<dd
className={b('definition')}
title={getTitle(contentTitle, content)}
style={{
...valueStyle,
lineBreak:
typeof content === 'string' &&
isUnbreakableOver(20)(content)
? 'anywhere'
: undefined,
}}
>
{definition}
</dd>
</div>
);
},
)}
</dd>
</div>
);
})}
</dl>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import React from 'react';
import {Label, Link, User} from '@gravity-ui/uikit';
import {Meta, StoryFn} from '@storybook/react';

import {DefinitionList, DefinitionListItem, DefinitionListProps} from '../DefinitionList';
import {DefinitionList} from '../DefinitionList';
import type {DefinitionListProps, DefinitionListSingleItem} from '../types';

const items: DefinitionListItem[] = [
const items: DefinitionListSingleItem[] = [
{name: <Link href="https://cloud.yandex.ru/docs">String value</Link>, content: 'value'},
{
name: (
Expand Down Expand Up @@ -121,6 +122,8 @@ const items: DefinitionListItem[] = [
avatar={{text: 'Charles Darwin', theme: 'brand', title: 'Charles Darwin avatar'}}
/>
),
copyText:
'The HTML <dl> element represents a description list. The element encloses a list of groups of terms (specified using the <dt> element) and descriptions (provided by <dd> elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)',
note: 'This is avatar',
},
{
Expand Down Expand Up @@ -161,3 +164,29 @@ const TemplateWithIconInside: StoryFn<DefinitionListProps> = (args) => {
);
};
export const ListWithIconInside = TemplateWithIconInside.bind({});

const groupedItems = [
{
label: 'Compute',
items: [{name: 'Link', content: 'value'}],
},
{
label: 'VPC',
items: [
{name: 'Number value', content: 2},
{name: 'Node value', content: <strong>value</strong>},
{name: 'Link', content: 'value'},
],
},
{name: 'Simple value', content: 2},
{name: 'Something else', content: <strong>value</strong>},
{name: 'Foo bar', content: 'value'},
{label: 'Test', items: [{name: 'Node value', content: <strong>value</strong>}]},
];

export const GroupedItems = DefaultTemplate.bind({});
GroupedItems.args = {
items: groupedItems,
responsive: false,
contentMaxWidth: 480,
};
28 changes: 27 additions & 1 deletion src/components/DefinitionList/__tests__/DefinitionList.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';

import {render, screen} from '../../../../test-utils/utils';
import {DefinitionList, b} from '../DefinitionList';
import {DefinitionList} from '../DefinitionList';
import {b} from '../utils';

const qaAttribute = 'definition-list';

Expand Down Expand Up @@ -69,4 +70,29 @@ describe('components: DefinitionList', () => {
const component = screen.getByRole('term');
expect(component).toHaveClass(b('term-container', {multiline: true}));
});
it('should render group label', () => {
const items = [
{
label: 'Test group',
items: [{name: 'test1', content: 'value1'}],
},
];
getComponent({items});

const component = screen.getByText('Test group');
expect(component).toBeVisible();
});
it('should render grouped items', () => {
const items = [
{
label: 'Test group',
items: [{name: 'test1', content: 'value1'}],
},
];
getComponent({items});

const component = screen.getByText('value1');
expect(component).toBeVisible();
expect(component).toHaveClass(b('definition'));
});
});
29 changes: 29 additions & 0 deletions src/components/DefinitionList/components/Definition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';

import {ClipboardButton} from '@gravity-ui/uikit';

import type {DefinitionListProps, DefinitionListSingleItem} from '../types';
import {b} from '../utils';

interface DefinitionProps
extends Pick<DefinitionListSingleItem, 'copyText' | 'content'>,
Pick<DefinitionListProps, 'copyPosition'> {}

export function Definition({copyText, content, copyPosition}: DefinitionProps) {
const iconInside = copyPosition === 'inside';
const definitionContent = content ?? '—';

return copyText ? (
<div className={b('copy-container', {'icon-inside': iconInside})}>
<span>{definitionContent}</span>
<ClipboardButton
size="s"
text={copyText}
className={b('copy-button')}
view={iconInside ? 'raised' : 'flat-secondary'}
/>
</div>
) : (
definitionContent
);
}
19 changes: 19 additions & 0 deletions src/components/DefinitionList/components/GroupLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';

import {Text} from '@gravity-ui/uikit';

import {b} from '../utils';

interface GroupLabelProps {
label: React.ReactNode;
}

export function GroupLabel({label}: GroupLabelProps) {
return (
<div className={b('title')}>
<Text variant="subheader-1" color="complementary">
{label}
</Text>
</div>
);
}
Loading

0 comments on commit 27bc3f6

Please sign in to comment.