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 8 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;
}
}
}
149 changes: 149 additions & 0 deletions src/components/DefinitionList/DefinitionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React from 'react';

import {ClipboardButton, Link} 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: string;
key?: string;
href?: string;
content?: React.ReactNode;
title?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe split into 2 props, for name and for content?

copyText?: string;
note?: DefinitionListItemNote;
multilineName?: boolean;
}

export interface DefinitionListProps {
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}
htmlContent={note}
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use normal content props, not html

placement={['bottom', 'top']}
/>
);
}

if (typeof note === 'object') {
noteElement = <HelpPopover className={popoverClassName} {...note} />;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's also set default placement here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do you think we need it? <Popover> under the hood has default placement and it seems it is enough.

Copy link
Contributor

Choose a reason for hiding this comment

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

Then why we set placement when note is a string?

}
}
return noteElement;
}

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

const valueStyle =
typeof contentMaxWidth === 'number'
? {
flexBasis: contentMaxWidth,
maxWidth: contentMaxWidth,
}
: {};
return (
<dl className={b({responsive}, className)} role="list">
amje marked this conversation as resolved.
Show resolved Hide resolved
{items.map(({name, href, content, title, copyText, note, key, multilineName}) => {
const term = href ? <Link href={href}>{name}</Link> : name;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe instead of having href prop we allow name to be ReactNode to render whatever user want?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Like it! But in this case key property should be required.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can generate them by ourselves

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 ?? name} className={b('item', itemClassName)}>
amje marked this conversation as resolved.
Show resolved Hide resolved
<dt
className={b('term-container', {multiline: multilineName})}
style={keyStyle}
>
<div className={b('term-wrapper')}>
{term}
{multilineName && noteElement}
</div>
{!multilineName && noteElement}
<div className={b('dots', {'with-note': Boolean(note)})} />
</dt>
<dd
className={b('definition')}
title={getTitle(title, content)}
style={{
...valueStyle,
lineBreak:
typeof content === 'string' && isUnbreakableOver(20)(content)
? 'anywhere'
: undefined,
}}
>
{definition}
</dd>
</div>
);
})}
</dl>
);
}
45 changes: 45 additions & 0 deletions src/components/DefinitionList/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## 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 | `String` | true | | Term |
| multilineName | `boolean` | | | If set, term will be multiline |
| key | `String` | | | Item key (if not set, `name` property will be treated as key) |
| href | `String` | | | If set, term will be wrapped with link |
| content | `ReactNode` | | | Definition |
| title | `String` | | | Title for definition. If not set, `content` value will be used |
| copyText | `String` | | | If set, it will be shown icon for copy this text |
Copy link
Contributor

Choose a reason for hiding this comment

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

String with lower "s"

| 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