Skip to content

Commit

Permalink
feat: add DefinitionList (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
Raubzeug committed Mar 12, 2024
1 parent 60bd280 commit 3db3bbf
Show file tree
Hide file tree
Showing 9 changed files with 535 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
* @amje @ValeraS @korvin89
/src/components/DefinitionList @Raubzeug
/src/components/FilePreview @KirillDyachkovskiy
/src/components/FormRow @ogonkov
/src/components/HelpPopover @Raubzeug
Expand Down
106 changes: 106 additions & 0 deletions src/components/DefinitionList/DefinitionList.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
@use '../variables';
@use '@gravity-ui/uikit/styles/mixins';

$block: '.#{variables.$ns}definition-list';

#{$block} {
$class: &;

margin: 0;

&__item {
display: flex;
align-items: baseline;
gap: var(--g-spacing-1);

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

&__term-container {
flex: 0 0 300px;
display: flex;
align-items: baseline;

overflow: hidden;
position: relative;
}

&__term-wrapper {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

flex: 0 1 auto;
color: var(--g-color-text-secondary);

position: relative;
}

&__term-container_multiline &__term-wrapper {
white-space: unset;
}

&__term-container_multiline &__item-note-tooltip {
position: absolute;
}

&__dots {
box-sizing: border-box;
flex: 1 0 auto;
min-width: 40px;
margin: 0 2px;
border-block-end: 1px dotted var(--g-color-line-generic-active);
}

&__dots_with-note {
margin-inline-start: 15px;
min-width: 25px;
}

&__definition {
flex: 0 1 auto;
margin: 0;
}

&_responsive {
#{$block}__term-container {
flex: 1 0 auto;
}
}

&__copy-container {
position: relative;
display: inline-flex;
padding-inline-end: var(--g-spacing-7);

margin-inline-end: calc(-1 * var(--g-spacing-7));

&:hover {
#{$block}__copy-button {
opacity: 1;
}
}
}

&__copy-container_icon-inside {
padding-inline-end: unset;
margin-inline-end: unset;

#{$block}__copy-button {
inset-block-start: 0;
}
}

&__copy-button {
position: absolute;
display: inline-block;
inset-inline-end: 0;
margin-inline-start: 10px;
opacity: 0;
&:focus-visible {
opacity: 1;
}
}
}
156 changes: 156 additions & 0 deletions src/components/DefinitionList/DefinitionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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 {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']}
/>
);
}

if (typeof note === 'object') {
noteElement = (
<HelpPopover className={popoverClassName} placement={['bottom', 'top']} {...note} />
);
}
}
return noteElement;
}

export function DefinitionList({
items,
responsive,
nameMaxWidth,
contentMaxWidth = 'auto',
className,
itemClassName,
copyPosition = 'outside',
qa,
}: DefinitionListProps) {
const keyStyle = nameMaxWidth
? {
flexBasis: nameMaxWidth,
}
: {};

const valueStyle =
typeof contentMaxWidth === 'number'
? {
flexBasis: contentMaxWidth,
maxWidth: contentMaxWidth,
}
: {};
const normalizedItems = React.useMemo(() => {
return 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'}
/>
</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>
);
},
)}
</dl>
);
}
44 changes: 44 additions & 0 deletions src/components/DefinitionList/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## DefinitionList

The component to display definition list with term and definition separated by dots.

### PropTypes

| Property | Type | Required | Default | Description |
| :-------------- | :---------------------- | :-------: | :------ | :----------------------------------------------------------- |
| [items](#items) | `DefinitionListItem[]` | yes | | Items of the list |
| responsive | `boolean` | | | If set to `true` list will take 100% width of its parent |
| nameMaxWidth | `number` | | | Maximum width of term |
| contentMaxWidth | `number \| 'auto'` | | 'auto' | Maximum width of definition |
| className | `string` | | | Class name for the list container |
| itemClassName | `string` | | | Class name for the list item |
| copyPosition | `'inside' \| 'outside'` | 'outside' | | If set to `inside`, copy icon will be placed over definition |

#### Items

Configuration for list items

| Property | Type | Required | Default | Description |
| ------------- | ---------------------------- | -------- | ------- | -------------------------------------------------------------- |
| name | `ReactNode` | true | | Term |
| multilineName | `boolean` | | | If set, term will be multiline |
| content | `ReactNode` | | | Definition |
| contentTitle | `string` | | | Title for definition. If not set, `content` value will be used |
| nameTitle | `string` | | | Title for term. If not set, `name` value will be used |
| copyText | `string` | | | If set, it will be shown icon for copy this text |
| note | `string \| HelpPopoverProps` | | | If set, HelpPopover will be shown next to term |

```jsx
<DefinitionList
items={[
{
name: 'Node value with copy',
content: <strong>value with copy</strong>,
copyText: 'value',
},
{name: 'Empty value with copy', copyText: 'nothing to copy'},
]}
nameMaxWidth="100"
contentMaxWidth="100"
/>
```

0 comments on commit 3db3bbf

Please sign in to comment.