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: add DefinitionList #143

Merged
merged 11 commits into from
Mar 12, 2024
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
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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works without ~ correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This syntax is used all over in the repo. No problem.


$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;
amje marked this conversation as resolved.
Show resolved Hide resolved

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
amje marked this conversation as resolved.
Show resolved Hide resolved
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"
/>
```
Loading
Loading