From 8faf1a9a6443ff095e63340c137b540190a50c6d Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 2 Aug 2024 16:10:49 +0300 Subject: [PATCH 1/7] feat: add DefinitionList and HelpMark --- CODEOWNERS | 2 + .../DefinitionList/DefinitionList.scss | 148 +++++++++++ .../DefinitionList/DefinitionList.tsx | 136 +++++++++++ src/components/DefinitionList/README.md | 45 ++++ .../__stories__/DefinitionList.stories.tsx | 229 ++++++++++++++++++ .../__tests__/DefinitionList.test.tsx | 103 ++++++++ .../DefinitionList/components/Definition.tsx | 28 +++ .../DefinitionList/components/GroupLabel.tsx | 18 ++ .../DefinitionList/components/Term.tsx | 76 ++++++ src/components/DefinitionList/i18n/en.json | 3 + src/components/DefinitionList/i18n/index.ts | 7 + src/components/DefinitionList/i18n/ru.json | 3 + src/components/DefinitionList/index.ts | 2 + src/components/DefinitionList/types.ts | 42 ++++ src/components/DefinitionList/utils.ts | 57 +++++ src/components/HelpMark/HelpMark.scss | 16 ++ src/components/HelpMark/HelpMark.tsx | 34 +++ src/components/HelpMark/README.md | 60 +++++ .../HelpMark/__stories__/HelpMark.stories.tsx | 85 +++++++ .../__stories__/HelpMarkShowcase.scss | 9 + .../HelpMark/__tests__/HelpMark.test.tsx | 36 +++ src/components/HelpMark/index.ts | 1 + src/components/index.ts | 2 + 23 files changed, 1142 insertions(+) create mode 100644 src/components/DefinitionList/DefinitionList.scss create mode 100644 src/components/DefinitionList/DefinitionList.tsx create mode 100644 src/components/DefinitionList/README.md create mode 100644 src/components/DefinitionList/__stories__/DefinitionList.stories.tsx create mode 100644 src/components/DefinitionList/__tests__/DefinitionList.test.tsx create mode 100644 src/components/DefinitionList/components/Definition.tsx create mode 100644 src/components/DefinitionList/components/GroupLabel.tsx create mode 100644 src/components/DefinitionList/components/Term.tsx create mode 100644 src/components/DefinitionList/i18n/en.json create mode 100644 src/components/DefinitionList/i18n/index.ts create mode 100644 src/components/DefinitionList/i18n/ru.json create mode 100644 src/components/DefinitionList/index.ts create mode 100644 src/components/DefinitionList/types.ts create mode 100644 src/components/DefinitionList/utils.ts create mode 100644 src/components/HelpMark/HelpMark.scss create mode 100644 src/components/HelpMark/HelpMark.tsx create mode 100644 src/components/HelpMark/README.md create mode 100644 src/components/HelpMark/__stories__/HelpMark.stories.tsx create mode 100644 src/components/HelpMark/__stories__/HelpMarkShowcase.scss create mode 100644 src/components/HelpMark/__tests__/HelpMark.test.tsx create mode 100644 src/components/HelpMark/index.ts diff --git a/CODEOWNERS b/CODEOWNERS index dc1b672284..fc9184ebfe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -12,6 +12,8 @@ /src/components/ClipboardIcon @Raubzeug /src/components/ControlLabel @korvin89 /src/components/CopyToClipboard @SeqviriouM +/src/components/DefinitionList @Raubzeug +/src/components/HelpMark @Raubzeug #/src/components/Dialog /src/components/Disclosure @Raubzeug /src/components/Divider @v4dyar4 diff --git a/src/components/DefinitionList/DefinitionList.scss b/src/components/DefinitionList/DefinitionList.scss new file mode 100644 index 0000000000..0e4749b8d9 --- /dev/null +++ b/src/components/DefinitionList/DefinitionList.scss @@ -0,0 +1,148 @@ +@use '../../../styles/mixins.scss'; +@use '../variables'; + +$block: '.#{variables.$ns}definition-list'; + +#{$block} { + &__list { + margin: 0; + } + + &__group-title { + margin-block-end: var(--g-spacing-3); + + &:not(:first-of-type) { + margin-block-start: var(--g-spacing-5); + } + } + + &__item { + display: flex; + align-items: baseline; + gap: var(--g-spacing-1); + + & + & { + margin-block-start: var(--g-spacing-4); + } + } + + &__item_grouped { + & + & { + margin-block-start: var(--g-spacing-3); + } + } + + &_margin { + &:not(:first-of-type) { + margin-block-start: var(--g-spacing-5); + } + } + + &__term-container { + flex: 0 0 auto; + width: 300px; + max-width: 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; + align-items: center; + 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; + } + } +} + +#{$block}_vertical { + #{$block}__term-container { + flex: 1 0 auto; + } + #{$block}__item { + flex-direction: column; + gap: var(--g-spacing-half); + } + #{$block}__item + #{$block}__item { + margin-block-start: var(--g-spacing-3); + } + #{$block}__group-title:not(:first-of-type) { + margin-block-start: var(--g-spacing-8); + } + #{$block}_margin:not(:first-of-type) { + margin-block-start: var(--g-spacing-8); + } +} diff --git a/src/components/DefinitionList/DefinitionList.tsx b/src/components/DefinitionList/DefinitionList.tsx new file mode 100644 index 0000000000..c0e35f6bd4 --- /dev/null +++ b/src/components/DefinitionList/DefinitionList.tsx @@ -0,0 +1,136 @@ +import React from 'react'; + +import {Definition} from './components/Definition'; +import {GroupLabel} from './components/GroupLabel'; +import {Term} from './components/Term'; +import type { + DefinitionListGranularProps, + DefinitionListGroupedProps, + DefinitionListProps, +} from './types'; +import {b, getAllItemsAsGroups, getTitle, isUnbreakableOver, onlySingleItems} from './utils'; + +import './DefinitionList.scss'; + +function DefinitionListGranular({ + items, + responsive, + direction = 'horizontal', + nameMaxWidth, + contentMaxWidth = 'auto', + className, + itemClassName, + copyPosition = 'outside', + qa, +}: DefinitionListGranularProps) { + const keyStyle = nameMaxWidth ? {maxWidth: nameMaxWidth, width: nameMaxWidth} : {}; + + const valueStyle = + typeof contentMaxWidth === 'number' + ? {width: contentMaxWidth, maxWidth: contentMaxWidth} + : {}; + + const normalizedItems = React.useMemo(() => { + return items.map((value, index) => ({...value, key: index})); + }, [items]); + + return ( +
+
+ {normalizedItems.map((item) => { + const { + name, + key, + content, + contentTitle, + nameTitle, + copyText, + note, + multilineName, + } = item; + + return ( +
+
+ +
+
+ +
+
+ ); + })} +
+
+ ); +} + +function DefinitionListGrouped({ + items, + className, + itemClassName, + ...rest +}: DefinitionListGroupedProps) { + const normalizedItems = React.useMemo(() => { + return items.map((value, index) => ({...value, key: index})); + }, [items]); + + return ( +
+ {normalizedItems.map((item) => { + const {key, label} = item; + + return ( + + {label && } + {item.items && ( + + )} + + ); + })} +
+ ); +} + +export function DefinitionList({items, ...rest}: DefinitionListProps) { + if (onlySingleItems(items)) { + return ; + } + + const preparedItems = getAllItemsAsGroups(items); + + return ; +} diff --git a/src/components/DefinitionList/README.md b/src/components/DefinitionList/README.md new file mode 100644 index 0000000000..d8331c69e1 --- /dev/null +++ b/src/components/DefinitionList/README.md @@ -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 | +| direction | `'horizontal'` \| `'vertical'` | | 'horizontal' | If set to `vertical` content will be located under name and 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 \| HelpMarkProps` | | | If set, HelpMark will be shown next to term | + +```jsx +value with copy, + copyText: 'value', + }, + {name: 'Empty value with copy', copyText: 'nothing to copy'}, + ]} + nameMaxWidth="100" + contentMaxWidth="100" +/> +``` diff --git a/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx new file mode 100644 index 0000000000..0ab4767596 --- /dev/null +++ b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx @@ -0,0 +1,229 @@ +import React from 'react'; + +import type {Meta, StoryFn} from '@storybook/react'; + +import {Label} from '../../Label'; +import {Link} from '../../Link'; +import {User} from '../../User'; +import {DefinitionList} from '../DefinitionList'; +import type {DefinitionListProps, DefinitionListSingleItem} from '../types'; + +const items: DefinitionListSingleItem[] = [ + {name: String value, content: 'value'}, + { + name: ( + + ), + content: 'value', + note: 'This is avatar', + }, + {name: 'Number value', content: 2}, + {name: 'Node value', content: value}, + {name: 'Empty value'}, + {name: 'String value with copy', content: 'value', copyText: 'value'}, + {name: 'Number value with copy', content: 2, copyText: 'two'}, + {name: 'Node value with copy', content: value, copyText: 'value'}, + {name: 'Empty value with copy', copyText: 'nothing to copy'}, + {name: 'String value with custom title', content: 'value', contentTitle: "value's title"}, + {name: 'Number value with custom title', content: 2, contentTitle: "value's title"}, + { + name: 'Node value with custom title', + content: value, + contentTitle: "value's title", + }, + {name: 'Empty value with custom title', contentTitle: "value's title"}, + { + name: 'String long value', + content: + 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', + }, + { + name: 'String long value with copy', + content: + 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', + copyText: + 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', + }, + { + name: 'Number long value', + // eslint-disable-next-line no-loss-of-precision + content: 12345678901234567890123456789012345678901234567890123456789012345678901234567890, + }, + { + name: 'Node long value', + content: ( + + 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). + + ), + }, + { + name: 'String long value without whitespace', + content: + 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', + }, + { + name: 'String long looooooooooooooong looooooooooooooong looooooooooooooong looooooooooooooong value without multiline and with copy icon', + multilineName: true, + note: 'This is multiline value', + content: + 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', + copyText: + 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', + }, + { + name: 'String value with tooltip', + content: 'value', + note: 'This is simple string value', + }, + { + name: 'String value with very very very looooooooooooooong key', + content: 'value', + }, + { + name: 'String value with very very very looooooooooooooong key and tooltip', + content: 'value', + note: 'This is simple string value', + }, + { + name: 'Avatar with tooltip', + content: ( + + ), + copyText: + 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
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', + }, + { + name: 'Label', + content: , + }, +]; + +export default { + title: 'Components/Data Display/DefinitionList', + component: DefinitionList, + args: { + items, + responsive: false, + contentMaxWidth: 480, + }, + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'color-contrast', + enabled: false, + }, + { + id: 'definition-list', // todo: https://github.com/gravity-ui/components/issues/207 + enabled: false, + }, + ], + }, + }, + }, +} as Meta; + +const DefaultTemplate: StoryFn = (args) => ; +export const Default = DefaultTemplate.bind({}); + +const TemplateWithIconInside: StoryFn = (args) => { + return ( + e.copyText)} copyPosition="inside" /> + ); +}; +export const ListWithIconInside = TemplateWithIconInside.bind({}); + +const groupedItems = [ + { + label: 'Group 1', + items: [{name: 'Link', content: 'value'}], + }, + { + label: 'Group 2', + items: [ + {name: 'Number value', content: 2}, + {name: 'Node value', content: value}, + {name: 'Link', content: 'value'}, + ], + }, + {name: 'Simple value', content: 2}, + {name: 'Something else', content: value}, + {name: 'Foo bar', content: 'value'}, + { + label: 'Group 3', + items: [ + { + name: 'String long value with copy', + content: + 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', + copyText: + 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', + }, + { + name: 'String long looooooooooooooong looooooooooooooong looooooooooooooong looooooooooooooong value', + multilineName: true, + note: 'This is multiline value', + content: + 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', + copyText: + 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', + }, + ], + }, +]; + +export const GroupedItems = DefaultTemplate.bind({}); +GroupedItems.args = { + items: groupedItems, + responsive: false, + contentMaxWidth: 480, +}; + +const TemplateVertical: StoryFn = (args) => { + return ; +}; +export const VerticalList = TemplateVertical.bind({}); +VerticalList.args = { + items: groupedItems, + direction: 'vertical', + contentMaxWidth: 'auto', + copyPosition: 'inside', +}; diff --git a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx new file mode 100644 index 0000000000..3299326be4 --- /dev/null +++ b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx @@ -0,0 +1,103 @@ +import React from 'react'; + +import {render, screen} from '../../../../test-utils/utils'; +import {DefinitionList} from '../DefinitionList'; +import {b} from '../utils'; + +const qaAttribute = 'definition-list'; + +const getComponent = (props = {}) => + render( + node value}, + ]} + {...props} + />, + ).container; + +describe('components: DefinitionList', () => { + it('should render', () => { + getComponent(); + const component = screen.getByTestId(qaAttribute); + expect(component).toBeVisible(); + }); + it('should render passed className', () => { + getComponent({className: 'testClassName'}); + const component = screen.getByTestId(qaAttribute); + expect(component).toHaveClass('testClassName'); + }); + + it('should render passed content title', () => { + const items = [{name: 'test1', content: 'value1', contentTitle: 'contentTitle1'}]; + getComponent({items}); + const component = screen.getByText('value1'); + expect(component).toHaveAttribute('title', 'contentTitle1'); + }); + it('should render passed name title', () => { + const items = [{name: 'test1', nameTitle: 'nameTitle1'}]; + getComponent({items}); + const component = screen.getByText('test1'); + expect(component).toHaveAttribute('title', 'nameTitle1'); + }); + it('should not render clipboard button by default', () => { + getComponent(); + const copyButton = screen.queryByRole('button'); + expect(copyButton).toBeNull(); + }); + it('should render clipboard button', () => { + const items = [{name: 'test1', content: 'value1', copyText: 'value1'}]; + getComponent({items}); + + const copyButton = screen.getByRole('button'); + + expect(copyButton).toHaveClass(b('copy-button')); + }); + it('should render in responsive mode', () => { + const items = [{name: 'test1', content: 'value1', copyText: 'value1'}]; + getComponent({items, responsive: true}); + + const component = screen.getByTestId(qaAttribute); + expect(component).toHaveClass(b({responsive: true})); + }); + it('should render with multiline term', () => { + const items = [{name: 'test1', content: 'value1', copyText: 'value1', multilineName: true}]; + getComponent({items}); + + 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')); + }); + it('should render vertical view', () => { + getComponent({direction: 'vertical'}); + const component = screen.getByTestId(qaAttribute); + expect(component).toHaveClass(b({vertical: true})); + }); +}); diff --git a/src/components/DefinitionList/components/Definition.tsx b/src/components/DefinitionList/components/Definition.tsx new file mode 100644 index 0000000000..b744e170d4 --- /dev/null +++ b/src/components/DefinitionList/components/Definition.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import {ClipboardButton} from '../../ClipboardButton'; +import type {DefinitionListProps, DefinitionListSingleItem} from '../types'; +import {b} from '../utils'; + +interface DefinitionProps + extends Pick, + Pick {} + +export function Definition({copyText, content, copyPosition}: DefinitionProps) { + const iconInside = copyPosition === 'inside'; + const definitionContent = content ?? '—'; + + return copyText ? ( +
+ {definitionContent} + +
+ ) : ( + definitionContent + ); +} diff --git a/src/components/DefinitionList/components/GroupLabel.tsx b/src/components/DefinitionList/components/GroupLabel.tsx new file mode 100644 index 0000000000..8cbcd7ef8d --- /dev/null +++ b/src/components/DefinitionList/components/GroupLabel.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import {Text} from '../../Text'; +import {b} from '../utils'; + +interface GroupLabelProps { + label: React.ReactNode; +} + +export function GroupLabel({label}: GroupLabelProps) { + return ( +
+ + {label} + +
+ ); +} diff --git a/src/components/DefinitionList/components/Term.tsx b/src/components/DefinitionList/components/Term.tsx new file mode 100644 index 0000000000..cff9c25764 --- /dev/null +++ b/src/components/DefinitionList/components/Term.tsx @@ -0,0 +1,76 @@ +import React from 'react'; + +import {HelpMark} from '../../HelpMark'; +import i18n from '../i18n'; +import type { + DefinitionListDirection, + DefinitionListItemNote, + DefinitionListSingleItem, +} from '../types'; +import {b, getTitle} from '../utils'; + +interface NoteElementsProps { + note?: DefinitionListItemNote; +} + +function NoteElement({note}: NoteElementsProps) { + if (!note) { + return null; + } + const popoverClassName = b('item-note-tooltip'); + if (typeof note === 'string') { + return ( + + ); + } + + if (typeof note === 'object') { + const {buttonProps, ...rest} = note; + + return ( + + ); + } + return null; +} + +export interface TermProps + extends Pick { + direction?: DefinitionListDirection; +} + +export function Term({note, name, nameTitle, multilineName, direction}: TermProps) { + const noteElement = ( + +   + + + ); + return ( + +
+ {name} + {multilineName && noteElement} +
+ {!multilineName && noteElement} + {direction === 'horizontal' && ( +
+ )} + + ); +} diff --git a/src/components/DefinitionList/i18n/en.json b/src/components/DefinitionList/i18n/en.json new file mode 100644 index 0000000000..1e930ef2d4 --- /dev/null +++ b/src/components/DefinitionList/i18n/en.json @@ -0,0 +1,3 @@ +{ + "label_note": "Note" +} diff --git a/src/components/DefinitionList/i18n/index.ts b/src/components/DefinitionList/i18n/index.ts new file mode 100644 index 0000000000..c0f5f18898 --- /dev/null +++ b/src/components/DefinitionList/i18n/index.ts @@ -0,0 +1,7 @@ +import {addComponentKeysets} from '../../utils/addComponentKeysets'; +import {NAMESPACE} from '../../utils/cn'; + +import en from './en.json'; +import ru from './ru.json'; + +export default addComponentKeysets({en, ru}, `${NAMESPACE}definition-list`); diff --git a/src/components/DefinitionList/i18n/ru.json b/src/components/DefinitionList/i18n/ru.json new file mode 100644 index 0000000000..0ea255da98 --- /dev/null +++ b/src/components/DefinitionList/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "label_note": "Справка" +} diff --git a/src/components/DefinitionList/index.ts b/src/components/DefinitionList/index.ts new file mode 100644 index 0000000000..3b199e51a0 --- /dev/null +++ b/src/components/DefinitionList/index.ts @@ -0,0 +1,2 @@ +export {DefinitionList} from './DefinitionList'; +export type {DefinitionListProps, DefinitionListItem, DefinitionListSingleItem} from './types'; diff --git a/src/components/DefinitionList/types.ts b/src/components/DefinitionList/types.ts new file mode 100644 index 0000000000..7e25841caf --- /dev/null +++ b/src/components/DefinitionList/types.ts @@ -0,0 +1,42 @@ +import type React from 'react'; + +import type {HelpMarkProps} from '../HelpMark'; +import type {QAProps} from '../types'; +export type DefinitionListItemNote = string | HelpMarkProps; + +export interface DefinitionListGroup { + label: React.ReactNode; + items?: DefinitionListSingleItem[]; +} + +export interface DefinitionListSingleItem { + name: React.ReactNode; + content?: React.ReactNode; + contentTitle?: string; + nameTitle?: string; + copyText?: string; + note?: DefinitionListItemNote; + multilineName?: boolean; +} + +export type DefinitionListItem = DefinitionListSingleItem | DefinitionListGroup; + +export type DefinitionListDirection = 'vertical' | 'horizontal'; + +export interface DefinitionListProps extends QAProps { + items: DefinitionListItem[]; + copyPosition?: 'inside' | 'outside'; + responsive?: boolean; + direction?: DefinitionListDirection; + nameMaxWidth?: number; + contentMaxWidth?: number | 'auto'; + className?: string; + itemClassName?: string; +} + +export interface DefinitionListGranularProps extends Omit { + items: DefinitionListSingleItem[]; +} +export interface DefinitionListGroupedProps extends Omit { + items: DefinitionListGroup[]; +} diff --git a/src/components/DefinitionList/utils.ts b/src/components/DefinitionList/utils.ts new file mode 100644 index 0000000000..eba2cc6533 --- /dev/null +++ b/src/components/DefinitionList/utils.ts @@ -0,0 +1,57 @@ +import type React from 'react'; + +import {block} from '../utils/cn'; + +import type {DefinitionListGroup, DefinitionListItem, DefinitionListSingleItem} from './types'; + +export const b = block('definition-list'); + +export function isUnbreakableOver(limit: number) { + return function (value: string): boolean { + const posibleLines = value.split(/\s+/); + + return posibleLines.some((line) => line.length > limit); + }; +} + +export const isGroup = (item: DefinitionListItem): item is DefinitionListGroup => + 'label' in item && !('name' in item); + +export const onlySingleItems = (items: DefinitionListItem[]): items is DefinitionListSingleItem[] => + !items.some((el) => isGroup(el)); + +export function getAllItemsAsGroups( + items: (DefinitionListSingleItem | DefinitionListGroup)[], +): DefinitionListGroup[] { + const result: DefinitionListGroup[] = []; + let temporaryList: DefinitionListSingleItem[] = []; + for (const item of items) { + if (isGroup(item)) { + if (temporaryList.length) { + result.push({items: temporaryList, label: null}); + temporaryList = []; + } + + result.push(item); + } else { + temporaryList.push(item); + } + } + if (temporaryList.length) { + result.push({items: temporaryList, label: null}); + temporaryList = []; + } + return result; +} + +export function getTitle(title?: string, content?: React.ReactNode) { + if (title) { + return title; + } + + if (typeof content === 'string' || typeof content === 'number') { + return String(content); + } + + return undefined; +} diff --git a/src/components/HelpMark/HelpMark.scss b/src/components/HelpMark/HelpMark.scss new file mode 100644 index 0000000000..ad38a2f64d --- /dev/null +++ b/src/components/HelpMark/HelpMark.scss @@ -0,0 +1,16 @@ +@use '../../../styles/mixins.scss'; +@use '../variables'; + +$block: '.#{variables.$ns}help-mark'; + +#{$block} { + &__button { + @include mixins.button-reset(); + color: var(--g-color-text-hint); + } + + &__button:focus-visible { + outline: 2px solid var(--g-color-line-focus); + border-radius: 50%; + } +} diff --git a/src/components/HelpMark/HelpMark.tsx b/src/components/HelpMark/HelpMark.tsx new file mode 100644 index 0000000000..d9d7a5cc35 --- /dev/null +++ b/src/components/HelpMark/HelpMark.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import {CircleQuestion} from '@gravity-ui/icons'; + +import {Icon} from '../Icon'; +import type {PopoverProps} from '../Popover'; +import {Popover} from '../Popover'; +import type {QAProps} from '../types'; +import {block} from '../utils/cn'; + +import './HelpMark.scss'; + +const b = block('help-mark'); +const ICON_SIZE = 16; + +export interface HelpMarkProps extends Omit, QAProps { + buttonProps?: React.ButtonHTMLAttributes; + buttonRef?: React.RefObject; +} + +export function HelpMark(props: HelpMarkProps) { + return ( + + + + ); +} diff --git a/src/components/HelpMark/README.md b/src/components/HelpMark/README.md new file mode 100644 index 0000000000..eee8694f32 --- /dev/null +++ b/src/components/HelpMark/README.md @@ -0,0 +1,60 @@ +## HelpMark + +Component to display help icon with popover + +### PropTypes + +| Property | Type | Required | Values | Default | Description | +| :------------ | :---------------------------------------------- | :------- | :---------------- | :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| theme | `String` | | `info`, `special` | `info` | Appearance | +| className | `String` | | | | Control class name | +| placement | `Array` | | | [`right`, `bottom`] | Allowed popover positions | +| autoclosable | `Boolean` | | | `true` | Close popover when pointer is outside of control | +| delayClosing | `Number` | | | `300` | Timeout before closing popover (see `autoclosable`) | +| title | `String` | | | | Popover title | +| content | `ReactNode` | | | | Popover content | +| htmlContent | `String` | | | | Render HTML via `dangerouslySetInnerHTML` | +| links | `Array` | | | [] | Links below content, could be
`{ text: 'Link 1', href: 'https://example.com'}` or
`{ text: 'Link 2', onClick: () => onLinkClick() }` | +| tooltipButton | `Object` | | | | Render button with this value
`{ text: 'Button', onClick: () => onClick() }` | +| offset | `Object` | | | `{ left: 4 }` | Control popup toggle position offset
`{ top: 0, left: 0 }` | +| buttonProps | `React.ButtonHTMLAttributes` | | | | Set attributes to the underlying button element | +| buttonRef | `React.RefObject` | | | | Ref to the underlying button element | + +### Examples + +Component with rendered raw html (use `content` for plain text) and close on mouse leave after timeout set by `delayClosing`: + +```js + alert('Lorem ipsum onClick'), + }, + ]} + to={['right', 'bottom']} + title="Simple tooltip" + htmlContent={ + 'Lorem ipsum dolor sit amet, at scelerisque suspendisse' + } + tooltipButton={{ + text: 'Actions', + onClick: () => console.log('just action happened'), + }} +/> +``` + +Popover with JSX component as content: + +```js +} /> +``` diff --git a/src/components/HelpMark/__stories__/HelpMark.stories.tsx b/src/components/HelpMark/__stories__/HelpMark.stories.tsx new file mode 100644 index 0000000000..e3bc0b646e --- /dev/null +++ b/src/components/HelpMark/__stories__/HelpMark.stories.tsx @@ -0,0 +1,85 @@ +import React from 'react'; + +import type {Meta, StoryFn} from '@storybook/react'; + +import {cn} from '../../utils/cn'; +import {HelpMark} from '../HelpMark'; +import type {HelpMarkProps} from '../HelpMark'; + +import './HelpMarkShowcase.scss'; + +const b = cn('help-mark-showcase'); + +export default { + title: 'Components/Utils/HelpMark', + id: 'components/utils/HelpMark', + component: HelpMark, + args: { + buttonProps: { + 'aria-label': 'Note', + }, + }, + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'button-name', + enabled: false, + // aria-labelledby id is valid after tooltip content is rendered + selector: 'button[aria-labelledby="helpMarkWithoutActionsId"]', + }, + ], + }, + }, + }, +} as Meta; + +const DefaultTemplate: StoryFn = (args) => ; +export const Default = DefaultTemplate.bind({}); +Default.args = { + content: 'Some content', +}; + +export const Accessible: StoryFn = (args) => { + const helpMarkWithoutActionsId = 'helpMarkWithoutActionsId'; + const helpMarkWithActionsId = 'helpMarkWithActionsId'; + const [openPopover, setOpenPopover] = React.useState(false); + const ref = React.useRef(null); + return ( +
+
+ Without actions: + +
+
+ With actions: + Some link} + openOnHover={false} + onOpenChange={setOpenPopover} + focusTrap + autoFocus + restoreFocusRef={ref} + buttonProps={{ + 'aria-expanded': openPopover, + 'aria-controls': helpMarkWithActionsId, + 'aria-label': 'More info', + }} + buttonRef={ref} + /> +
+
+ ); +}; diff --git a/src/components/HelpMark/__stories__/HelpMarkShowcase.scss b/src/components/HelpMark/__stories__/HelpMarkShowcase.scss new file mode 100644 index 0000000000..da034acf54 --- /dev/null +++ b/src/components/HelpMark/__stories__/HelpMarkShowcase.scss @@ -0,0 +1,9 @@ +.help-mark-showcase { + &__container { + display: flex; + margin-block-end: 10px; + } + &__container-title { + margin-inline-end: 5px; + } +} diff --git a/src/components/HelpMark/__tests__/HelpMark.test.tsx b/src/components/HelpMark/__tests__/HelpMark.test.tsx new file mode 100644 index 0000000000..104dd6fe85 --- /dev/null +++ b/src/components/HelpMark/__tests__/HelpMark.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import userEvent from '@testing-library/user-event'; + +import {setupTimersMock} from '../../../../test-utils/setupTimersMock'; +import {act, render, screen} from '../../../../test-utils/utils'; +import {HelpMark} from '../HelpMark'; + +const qaId = 'help-mark-component'; + +function waitForTooltipOpenedStateChange() { + jest.advanceTimersByTime(300); +} + +setupTimersMock(); + +describe('HelpMark', () => { + test('render popup when hover help icon', async () => { + const title = 'HelpMark title'; + + render(); + + const icon = screen.getByTestId(qaId); + expect(icon).toBeVisible(); + + // eslint-disable-next-line testing-library/await-async-events + userEvent.hover(icon); + + act(() => { + waitForTooltipOpenedStateChange(); + }); + + const popoverTitle = await screen.findByText(title); + expect(popoverTitle).toBeVisible(); + }); +}); diff --git a/src/components/HelpMark/index.ts b/src/components/HelpMark/index.ts new file mode 100644 index 0000000000..a7235b989a --- /dev/null +++ b/src/components/HelpMark/index.ts @@ -0,0 +1 @@ +export * from './HelpMark'; diff --git a/src/components/index.ts b/src/components/index.ts index 9257d9dca5..ad560ca179 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -13,6 +13,8 @@ export * from './Card'; export * from './ClipboardButton'; export * from './ClipboardIcon'; export * from './CopyToClipboard'; +export * from './DefinitionList'; +export * from './HelpMark'; export * from './Dialog'; export * from './Disclosure'; export * from './Divider'; From bf0a9907680c1dd12279da11736ea22a1f9d2988 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 2 Aug 2024 17:18:32 +0300 Subject: [PATCH 2/7] fix: add expamples for landing --- src/components/DefinitionList/README.md | 80 ++++++++++---- src/components/HelpMark/README.md | 134 ++++++++++++++++++++---- 2 files changed, 174 insertions(+), 40 deletions(-) diff --git a/src/components/DefinitionList/README.md b/src/components/DefinitionList/README.md index d8331c69e1..ee4a7eaf26 100644 --- a/src/components/DefinitionList/README.md +++ b/src/components/DefinitionList/README.md @@ -1,8 +1,67 @@ -## DefinitionList + + +# DefinitionList + + The component to display definition list with term and definition separated by dots. -### PropTypes +## Examples + + + + + +```tsx +value with copy, + copyText: 'value', + }, + {name: 'Empty value with copy', copyText: 'nothing to copy'}, + ]} + nameMaxWidth="100" + contentMaxWidth="100" +/> +``` + + + +## Properties | Property | Type | Required | Default | Description | | :-------------- | :----------------------------- | :-------: | :----------- | :-------------------------------------------------------------------------------------------------- | @@ -15,7 +74,7 @@ The component to display definition list with term and definition separated by d | itemClassName | `string` | | | Class name for the list item | | copyPosition | `'inside' \| 'outside'` | 'outside' | | If set to `inside`, copy icon will be placed over definition | -#### Items +### Items Configuration for list items @@ -28,18 +87,3 @@ Configuration for list items | 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 \| HelpMarkProps` | | | If set, HelpMark will be shown next to term | - -```jsx -value with copy, - copyText: 'value', - }, - {name: 'Empty value with copy', copyText: 'nothing to copy'}, - ]} - nameMaxWidth="100" - contentMaxWidth="100" -/> -``` diff --git a/src/components/HelpMark/README.md b/src/components/HelpMark/README.md index eee8694f32..6f40f4f33c 100644 --- a/src/components/HelpMark/README.md +++ b/src/components/HelpMark/README.md @@ -1,30 +1,84 @@ -## HelpMark + + +# HelpMark + + + +```tsx +import {HelpMark} from '@gravity-ui/uikit'; +``` Component to display help icon with popover -### PropTypes - -| Property | Type | Required | Values | Default | Description | -| :------------ | :---------------------------------------------- | :------- | :---------------- | :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | -| theme | `String` | | `info`, `special` | `info` | Appearance | -| className | `String` | | | | Control class name | -| placement | `Array` | | | [`right`, `bottom`] | Allowed popover positions | -| autoclosable | `Boolean` | | | `true` | Close popover when pointer is outside of control | -| delayClosing | `Number` | | | `300` | Timeout before closing popover (see `autoclosable`) | -| title | `String` | | | | Popover title | -| content | `ReactNode` | | | | Popover content | -| htmlContent | `String` | | | | Render HTML via `dangerouslySetInnerHTML` | -| links | `Array` | | | [] | Links below content, could be
`{ text: 'Link 1', href: 'https://example.com'}` or
`{ text: 'Link 2', onClick: () => onLinkClick() }` | -| tooltipButton | `Object` | | | | Render button with this value
`{ text: 'Button', onClick: () => onClick() }` | -| offset | `Object` | | | `{ left: 4 }` | Control popup toggle position offset
`{ top: 0, left: 0 }` | -| buttonProps | `React.ButtonHTMLAttributes` | | | | Set attributes to the underlying button element | -| buttonRef | `React.RefObject` | | | | Ref to the underlying button element | - -### Examples +## Examples Component with rendered raw html (use `content` for plain text) and close on mouse leave after timeout set by `delayClosing`: -```js + + + + +```tsx ``` + + Popover with JSX component as content: -```js + + + + +```tsx } /> ``` + + + +## Properties + +| Property | Type | Required | Default | Description | +| :------------ | :---------------------------------------------- | :------- | :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| theme | `info` `special` | | `info` | Appearance | +| className | `String` | | | Control class name | +| placement | `Array` | | [`right`, `bottom`] | Allowed popover positions | +| autoclosable | `Boolean` | | `true` | Close popover when pointer is outside of control | +| delayClosing | `Number` | | `300` | Timeout before closing popover (see `autoclosable`) | +| title | `String` | | | Popover title | +| content | `ReactNode` | | | Popover content | +| htmlContent | `String` | | | Render HTML via `dangerouslySetInnerHTML` | +| links | `Array` | | [] | Links below content, could be
`{ text: 'Link 1', href: 'https://example.com'}` or
`{ text: 'Link 2', onClick: () => onLinkClick() }` | +| tooltipButton | `Object` | | | Render button with this value
`{ text: 'Button', onClick: () => onClick() }` | +| offset | `Object` | | `{ left: 4 }` | Control popup toggle position offset
`{ top: 0, left: 0 }` | +| buttonProps | `React.ButtonHTMLAttributes` | | | Set attributes to the underlying button element | +| buttonRef | `React.RefObject` | | | Ref to the underlying button element | From caa44ac29fc30d4ce4808bd18acc56c974016c5e Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 7 Aug 2024 11:44:01 +0300 Subject: [PATCH 3/7] fix: styles --- .../DefinitionList/DefinitionList.scss | 19 ++++++++++------- .../DefinitionList/DefinitionList.tsx | 7 ++++--- src/components/DefinitionList/README.md | 21 ++++++++++--------- .../__tests__/DefinitionList.test.tsx | 12 +++++++++++ .../DefinitionList/components/GroupLabel.tsx | 5 +++-- src/components/DefinitionList/types.ts | 1 + 6 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/components/DefinitionList/DefinitionList.scss b/src/components/DefinitionList/DefinitionList.scss index 0e4749b8d9..5d0e47f10b 100644 --- a/src/components/DefinitionList/DefinitionList.scss +++ b/src/components/DefinitionList/DefinitionList.scss @@ -11,7 +11,8 @@ $block: '.#{variables.$ns}definition-list'; &__group-title { margin-block-end: var(--g-spacing-3); - &:not(:first-of-type) { + //decreace specifity to allow groupLabelClassName work properly + :where(&:not(:first-of-type)) { margin-block-start: var(--g-spacing-5); } } @@ -21,19 +22,19 @@ $block: '.#{variables.$ns}definition-list'; align-items: baseline; gap: var(--g-spacing-1); - & + & { + :where(& + &) { margin-block-start: var(--g-spacing-4); } } &__item_grouped { - & + & { + :where(& + &) { margin-block-start: var(--g-spacing-3); } } &_margin { - &:not(:first-of-type) { + :where(&:not(:first-of-type)) { margin-block-start: var(--g-spacing-5); } } @@ -136,13 +137,17 @@ $block: '.#{variables.$ns}definition-list'; flex-direction: column; gap: var(--g-spacing-half); } - #{$block}__item + #{$block}__item { + :where(#{$block}__item + #{$block}__item) { margin-block-start: var(--g-spacing-3); } - #{$block}__group-title:not(:first-of-type) { + :where(#{$block}__group-title:not(:first-of-type)) { margin-block-start: var(--g-spacing-8); } - #{$block}_margin:not(:first-of-type) { + :where(#{$block}_margin:not(:first-of-type)) { margin-block-start: var(--g-spacing-8); } } + +.unique { + margin: unset; +} diff --git a/src/components/DefinitionList/DefinitionList.tsx b/src/components/DefinitionList/DefinitionList.tsx index c0e35f6bd4..35d9616623 100644 --- a/src/components/DefinitionList/DefinitionList.tsx +++ b/src/components/DefinitionList/DefinitionList.tsx @@ -96,6 +96,7 @@ function DefinitionListGrouped({ items, className, itemClassName, + groupLabelClassName, ...rest }: DefinitionListGroupedProps) { const normalizedItems = React.useMemo(() => { @@ -103,17 +104,17 @@ function DefinitionListGrouped({ }, [items]); return ( -
+
{normalizedItems.map((item) => { const {key, label} = item; return ( - {label && } + {label && } {item.items && ( diff --git a/src/components/DefinitionList/README.md b/src/components/DefinitionList/README.md index ee4a7eaf26..f04125f3ad 100644 --- a/src/components/DefinitionList/README.md +++ b/src/components/DefinitionList/README.md @@ -63,16 +63,17 @@ LANDING_BLOCK--> ## Properties -| 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 | -| direction | `'horizontal'` \| `'vertical'` | | 'horizontal' | If set to `vertical` content will be located under name and 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 | +| 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 | +| direction | `'horizontal'` \| `'vertical'` | | 'horizontal' | If set to `vertical` content will be located under name and 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 definition list (for every granular definition list in group view) | +| itemClassName | `string` | | | Class name for the list item | +| groupLabelClassName | `string` | | | Class name for the group label in group view | +| copyPosition | `'inside' \| 'outside'` | 'outside' | | If set to `inside`, copy icon will be placed over definition | ### Items diff --git a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx index 3299326be4..7b01fd2d65 100644 --- a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx +++ b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx @@ -82,6 +82,18 @@ describe('components: DefinitionList', () => { const component = screen.getByText('Test group'); expect(component).toBeVisible(); }); + it('should render group label class name', () => { + const items = [ + { + label: 'Test group', + items: [{name: 'test1', content: 'value1'}], + }, + ]; + getComponent({items, groupLabelClassName: 'test'}); + + const component = screen.getByText('Test group').parentElement; + expect(component).toHaveClass('test'); + }); it('should render grouped items', () => { const items = [ { diff --git a/src/components/DefinitionList/components/GroupLabel.tsx b/src/components/DefinitionList/components/GroupLabel.tsx index 8cbcd7ef8d..0fed188afc 100644 --- a/src/components/DefinitionList/components/GroupLabel.tsx +++ b/src/components/DefinitionList/components/GroupLabel.tsx @@ -5,11 +5,12 @@ import {b} from '../utils'; interface GroupLabelProps { label: React.ReactNode; + className?: string; } -export function GroupLabel({label}: GroupLabelProps) { +export function GroupLabel({label, className}: GroupLabelProps) { return ( -
+
{label} diff --git a/src/components/DefinitionList/types.ts b/src/components/DefinitionList/types.ts index 7e25841caf..2e307a362e 100644 --- a/src/components/DefinitionList/types.ts +++ b/src/components/DefinitionList/types.ts @@ -32,6 +32,7 @@ export interface DefinitionListProps extends QAProps { contentMaxWidth?: number | 'auto'; className?: string; itemClassName?: string; + groupLabelClassName?: string; } export interface DefinitionListGranularProps extends Omit { From 02cb48ea4ba6e6b6790021d59f38a811bad57d23 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Tue, 1 Oct 2024 12:32:53 +0300 Subject: [PATCH 4/7] fix: remove group view --- .../DefinitionList/DefinitionList.scss | 60 ++++------------ .../DefinitionList/DefinitionList.tsx | 69 ++----------------- src/components/DefinitionList/README.md | 33 +++++---- .../__stories__/DefinitionList.stories.tsx | 64 ++--------------- .../__tests__/DefinitionList.test.tsx | 37 ---------- .../DefinitionList/components/Definition.tsx | 13 ++-- .../DefinitionList/components/GroupLabel.tsx | 19 ----- .../DefinitionList/components/Term.tsx | 10 +-- src/components/DefinitionList/i18n/index.ts | 5 +- src/components/DefinitionList/index.ts | 2 +- src/components/DefinitionList/types.ts | 19 +---- src/components/DefinitionList/utils.ts | 32 --------- 12 files changed, 58 insertions(+), 305 deletions(-) delete mode 100644 src/components/DefinitionList/components/GroupLabel.tsx diff --git a/src/components/DefinitionList/DefinitionList.scss b/src/components/DefinitionList/DefinitionList.scss index 5d0e47f10b..77071626d7 100644 --- a/src/components/DefinitionList/DefinitionList.scss +++ b/src/components/DefinitionList/DefinitionList.scss @@ -4,45 +4,29 @@ $block: '.#{variables.$ns}definition-list'; #{$block} { + --_--item-block-start: var(--g-spacing-4); + --_--term-width: 300px; &__list { margin: 0; } - &__group-title { - margin-block-end: var(--g-spacing-3); - - //decreace specifity to allow groupLabelClassName work properly - :where(&:not(:first-of-type)) { - margin-block-start: var(--g-spacing-5); - } - } - &__item { display: flex; align-items: baseline; gap: var(--g-spacing-1); :where(& + &) { - margin-block-start: var(--g-spacing-4); - } - } - - &__item_grouped { - :where(& + &) { - margin-block-start: var(--g-spacing-3); - } - } - - &_margin { - :where(&:not(:first-of-type)) { - margin-block-start: var(--g-spacing-5); + margin-block-start: var( + --g-definition-list-item-block-start, + var(--_--item-block-start) + ); } } &__term-container { flex: 0 0 auto; - width: 300px; - max-width: 300px; + width: var(--_--term-width); + max-width: var(--_--term-width); display: flex; align-items: baseline; @@ -89,7 +73,8 @@ $block: '.#{variables.$ns}definition-list'; &_responsive { #{$block}__term-container { - flex: 1 0 auto; + --_--term-width: auto; + flex: 1 0 50%; } } @@ -108,15 +93,6 @@ $block: '.#{variables.$ns}definition-list'; } } - &__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; @@ -130,6 +106,9 @@ $block: '.#{variables.$ns}definition-list'; } #{$block}_vertical { + --_--item-block-start: var(--g-spacing-3); + --_--term-width: auto; + #{$block}__term-container { flex: 1 0 auto; } @@ -137,17 +116,4 @@ $block: '.#{variables.$ns}definition-list'; flex-direction: column; gap: var(--g-spacing-half); } - :where(#{$block}__item + #{$block}__item) { - margin-block-start: var(--g-spacing-3); - } - :where(#{$block}__group-title:not(:first-of-type)) { - margin-block-start: var(--g-spacing-8); - } - :where(#{$block}_margin:not(:first-of-type)) { - margin-block-start: var(--g-spacing-8); - } -} - -.unique { - margin: unset; } diff --git a/src/components/DefinitionList/DefinitionList.tsx b/src/components/DefinitionList/DefinitionList.tsx index 35d9616623..5bc809a95d 100644 --- a/src/components/DefinitionList/DefinitionList.tsx +++ b/src/components/DefinitionList/DefinitionList.tsx @@ -1,28 +1,21 @@ import React from 'react'; import {Definition} from './components/Definition'; -import {GroupLabel} from './components/GroupLabel'; import {Term} from './components/Term'; -import type { - DefinitionListGranularProps, - DefinitionListGroupedProps, - DefinitionListProps, -} from './types'; -import {b, getAllItemsAsGroups, getTitle, isUnbreakableOver, onlySingleItems} from './utils'; +import type {DefinitionListProps} from './types'; +import {b, getTitle, isUnbreakableOver} from './utils'; import './DefinitionList.scss'; -function DefinitionListGranular({ +export function DefinitionList({ items, responsive, direction = 'horizontal', nameMaxWidth, - contentMaxWidth = 'auto', + contentMaxWidth, className, - itemClassName, - copyPosition = 'outside', qa, -}: DefinitionListGranularProps) { +}: DefinitionListProps) { const keyStyle = nameMaxWidth ? {maxWidth: nameMaxWidth, width: nameMaxWidth} : {}; const valueStyle = @@ -53,7 +46,7 @@ function DefinitionListGranular({ } = item; return ( -
+
- +
); @@ -91,47 +80,3 @@ function DefinitionListGranular({ ); } - -function DefinitionListGrouped({ - items, - className, - itemClassName, - groupLabelClassName, - ...rest -}: DefinitionListGroupedProps) { - const normalizedItems = React.useMemo(() => { - return items.map((value, index) => ({...value, key: index})); - }, [items]); - - return ( -
- {normalizedItems.map((item) => { - const {key, label} = item; - - return ( - - {label && } - {item.items && ( - - )} - - ); - })} -
- ); -} - -export function DefinitionList({items, ...rest}: DefinitionListProps) { - if (onlySingleItems(items)) { - return ; - } - - const preparedItems = getAllItemsAsGroups(items); - - return ; -} diff --git a/src/components/DefinitionList/README.md b/src/components/DefinitionList/README.md index f04125f3ad..1e6cbbacf8 100644 --- a/src/components/DefinitionList/README.md +++ b/src/components/DefinitionList/README.md @@ -21,8 +21,8 @@ The component to display definition list with term and definition separated by d }, {name: 'Empty value with copy', copyText: 'nothing to copy'}, ]} - nameMaxWidth="100" - contentMaxWidth="100" + nameMaxWidth={100} + contentMaxWidth={100} /> `} > @@ -35,8 +35,8 @@ The component to display definition list with term and definition separated by d }, {name: 'Empty value with copy', copyText: 'nothing to copy'}, ]} - nameMaxWidth="100" - contentMaxWidth="100" + nameMaxWidth={100} + contentMaxWidth={100} /> @@ -63,17 +63,14 @@ LANDING_BLOCK--> ## Properties -| 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 | -| direction | `'horizontal'` \| `'vertical'` | | 'horizontal' | If set to `vertical` content will be located under name and 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 definition list (for every granular definition list in group view) | -| itemClassName | `string` | | | Class name for the list item | -| groupLabelClassName | `string` | | | Class name for the group label in group view | -| copyPosition | `'inside' \| 'outside'` | 'outside' | | If set to `inside`, copy icon will be placed over definition | +| 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 | +| direction | `'horizontal'` \| `'vertical'` | | 'horizontal' | If set to `vertical` content will be located under name and list will take 100% width of its parent | +| nameMaxWidth | `number` | | | Maximum width of term | +| contentMaxWidth | `number` | | | Maximum width of definition | +| className | `string` | | | Class name for the definition list | ### Items @@ -88,3 +85,9 @@ Configuration for list items | 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 \| HelpMarkProps` | | | If set, HelpMark will be shown next to term | + +## CSS API + +| Name | Description | +| :------------------------------------- | :---------------------------------- | +| `--g-definition-list-item-block-start` | Space between definition list items | diff --git a/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx index 0ab4767596..38423156eb 100644 --- a/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx +++ b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx @@ -6,9 +6,9 @@ import {Label} from '../../Label'; import {Link} from '../../Link'; import {User} from '../../User'; import {DefinitionList} from '../DefinitionList'; -import type {DefinitionListProps, DefinitionListSingleItem} from '../types'; +import type {DefinitionListItem, DefinitionListProps} from '../types'; -const items: DefinitionListSingleItem[] = [ +const items: DefinitionListItem[] = [ {name: String value, content: 'value'}, { name: ( @@ -140,7 +140,6 @@ export default { args: { items, responsive: false, - contentMaxWidth: 480, }, parameters: { a11y: { @@ -163,58 +162,12 @@ export default { const DefaultTemplate: StoryFn = (args) => ; export const Default = DefaultTemplate.bind({}); +Default.args = {contentMaxWidth: 480}; -const TemplateWithIconInside: StoryFn = (args) => { - return ( - e.copyText)} copyPosition="inside" /> - ); -}; -export const ListWithIconInside = TemplateWithIconInside.bind({}); - -const groupedItems = [ - { - label: 'Group 1', - items: [{name: 'Link', content: 'value'}], - }, - { - label: 'Group 2', - items: [ - {name: 'Number value', content: 2}, - {name: 'Node value', content: value}, - {name: 'Link', content: 'value'}, - ], - }, - {name: 'Simple value', content: 2}, - {name: 'Something else', content: value}, - {name: 'Foo bar', content: 'value'}, - { - label: 'Group 3', - items: [ - { - name: 'String long value with copy', - content: - 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', - copyText: - 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', - }, - { - name: 'String long looooooooooooooong looooooooooooooong looooooooooooooong looooooooooooooong value', - multilineName: true, - note: 'This is multiline value', - content: - 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', - copyText: - 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', - }, - ], - }, -]; - -export const GroupedItems = DefaultTemplate.bind({}); -GroupedItems.args = { - items: groupedItems, - responsive: false, - contentMaxWidth: 480, +const TemplateResponsive: StoryFn = (args) => ; +export const ResponsiveList = TemplateResponsive.bind({}); +ResponsiveList.args = { + responsive: true, }; const TemplateVertical: StoryFn = (args) => { @@ -222,8 +175,5 @@ const TemplateVertical: StoryFn = (args) => { }; export const VerticalList = TemplateVertical.bind({}); VerticalList.args = { - items: groupedItems, direction: 'vertical', - contentMaxWidth: 'auto', - copyPosition: 'inside', }; diff --git a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx index 7b01fd2d65..8fd623c7c7 100644 --- a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx +++ b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx @@ -70,43 +70,6 @@ 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 group label class name', () => { - const items = [ - { - label: 'Test group', - items: [{name: 'test1', content: 'value1'}], - }, - ]; - getComponent({items, groupLabelClassName: 'test'}); - - const component = screen.getByText('Test group').parentElement; - expect(component).toHaveClass('test'); - }); - 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')); - }); it('should render vertical view', () => { getComponent({direction: 'vertical'}); const component = screen.getByTestId(qaAttribute); diff --git a/src/components/DefinitionList/components/Definition.tsx b/src/components/DefinitionList/components/Definition.tsx index b744e170d4..a1e16ebb6e 100644 --- a/src/components/DefinitionList/components/Definition.tsx +++ b/src/components/DefinitionList/components/Definition.tsx @@ -1,25 +1,22 @@ import React from 'react'; import {ClipboardButton} from '../../ClipboardButton'; -import type {DefinitionListProps, DefinitionListSingleItem} from '../types'; +import type {DefinitionListItem} from '../types'; import {b} from '../utils'; -interface DefinitionProps - extends Pick, - Pick {} +interface DefinitionProps extends Pick {} -export function Definition({copyText, content, copyPosition}: DefinitionProps) { - const iconInside = copyPosition === 'inside'; +export function Definition({copyText, content}: DefinitionProps) { const definitionContent = content ?? '—'; return copyText ? ( -
+
{definitionContent}
) : ( diff --git a/src/components/DefinitionList/components/GroupLabel.tsx b/src/components/DefinitionList/components/GroupLabel.tsx deleted file mode 100644 index 0fed188afc..0000000000 --- a/src/components/DefinitionList/components/GroupLabel.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -import {Text} from '../../Text'; -import {b} from '../utils'; - -interface GroupLabelProps { - label: React.ReactNode; - className?: string; -} - -export function GroupLabel({label, className}: GroupLabelProps) { - return ( -
- - {label} - -
- ); -} diff --git a/src/components/DefinitionList/components/Term.tsx b/src/components/DefinitionList/components/Term.tsx index cff9c25764..eb85645bd7 100644 --- a/src/components/DefinitionList/components/Term.tsx +++ b/src/components/DefinitionList/components/Term.tsx @@ -2,11 +2,7 @@ import React from 'react'; import {HelpMark} from '../../HelpMark'; import i18n from '../i18n'; -import type { - DefinitionListDirection, - DefinitionListItemNote, - DefinitionListSingleItem, -} from '../types'; +import type {DefinitionListDirection, DefinitionListItem, DefinitionListItemNote} from '../types'; import {b, getTitle} from '../utils'; interface NoteElementsProps { @@ -49,8 +45,8 @@ function NoteElement({note}: NoteElementsProps) { return null; } -export interface TermProps - extends Pick { +interface TermProps + extends Pick { direction?: DefinitionListDirection; } diff --git a/src/components/DefinitionList/i18n/index.ts b/src/components/DefinitionList/i18n/index.ts index c0f5f18898..e274729cd5 100644 --- a/src/components/DefinitionList/i18n/index.ts +++ b/src/components/DefinitionList/i18n/index.ts @@ -1,7 +1,6 @@ -import {addComponentKeysets} from '../../utils/addComponentKeysets'; -import {NAMESPACE} from '../../utils/cn'; +import {addComponentKeysets} from '../../../i18n'; import en from './en.json'; import ru from './ru.json'; -export default addComponentKeysets({en, ru}, `${NAMESPACE}definition-list`); +export default addComponentKeysets({en, ru}, 'DefinitionList'); diff --git a/src/components/DefinitionList/index.ts b/src/components/DefinitionList/index.ts index 3b199e51a0..cfe7f5f06f 100644 --- a/src/components/DefinitionList/index.ts +++ b/src/components/DefinitionList/index.ts @@ -1,2 +1,2 @@ export {DefinitionList} from './DefinitionList'; -export type {DefinitionListProps, DefinitionListItem, DefinitionListSingleItem} from './types'; +export type {DefinitionListProps, DefinitionListItem} from './types'; diff --git a/src/components/DefinitionList/types.ts b/src/components/DefinitionList/types.ts index 2e307a362e..589b8fb340 100644 --- a/src/components/DefinitionList/types.ts +++ b/src/components/DefinitionList/types.ts @@ -4,12 +4,7 @@ import type {HelpMarkProps} from '../HelpMark'; import type {QAProps} from '../types'; export type DefinitionListItemNote = string | HelpMarkProps; -export interface DefinitionListGroup { - label: React.ReactNode; - items?: DefinitionListSingleItem[]; -} - -export interface DefinitionListSingleItem { +export interface DefinitionListItem { name: React.ReactNode; content?: React.ReactNode; contentTitle?: string; @@ -19,25 +14,15 @@ export interface DefinitionListSingleItem { multilineName?: boolean; } -export type DefinitionListItem = DefinitionListSingleItem | DefinitionListGroup; - export type DefinitionListDirection = 'vertical' | 'horizontal'; export interface DefinitionListProps extends QAProps { items: DefinitionListItem[]; - copyPosition?: 'inside' | 'outside'; responsive?: boolean; direction?: DefinitionListDirection; nameMaxWidth?: number; - contentMaxWidth?: number | 'auto'; + contentMaxWidth?: number; className?: string; itemClassName?: string; groupLabelClassName?: string; } - -export interface DefinitionListGranularProps extends Omit { - items: DefinitionListSingleItem[]; -} -export interface DefinitionListGroupedProps extends Omit { - items: DefinitionListGroup[]; -} diff --git a/src/components/DefinitionList/utils.ts b/src/components/DefinitionList/utils.ts index eba2cc6533..8004627f0a 100644 --- a/src/components/DefinitionList/utils.ts +++ b/src/components/DefinitionList/utils.ts @@ -2,8 +2,6 @@ import type React from 'react'; import {block} from '../utils/cn'; -import type {DefinitionListGroup, DefinitionListItem, DefinitionListSingleItem} from './types'; - export const b = block('definition-list'); export function isUnbreakableOver(limit: number) { @@ -14,36 +12,6 @@ export function isUnbreakableOver(limit: number) { }; } -export const isGroup = (item: DefinitionListItem): item is DefinitionListGroup => - 'label' in item && !('name' in item); - -export const onlySingleItems = (items: DefinitionListItem[]): items is DefinitionListSingleItem[] => - !items.some((el) => isGroup(el)); - -export function getAllItemsAsGroups( - items: (DefinitionListSingleItem | DefinitionListGroup)[], -): DefinitionListGroup[] { - const result: DefinitionListGroup[] = []; - let temporaryList: DefinitionListSingleItem[] = []; - for (const item of items) { - if (isGroup(item)) { - if (temporaryList.length) { - result.push({items: temporaryList, label: null}); - temporaryList = []; - } - - result.push(item); - } else { - temporaryList.push(item); - } - } - if (temporaryList.length) { - result.push({items: temporaryList, label: null}); - temporaryList = []; - } - return result; -} - export function getTitle(title?: string, content?: React.ReactNode) { if (title) { return title; From dc0b37220fe1f711f2f390689aabe8ec33cb04c0 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Wed, 2 Oct 2024 12:02:36 +0300 Subject: [PATCH 5/7] fix: review --- CODEOWNERS | 121 ++++++++-------- .../DefinitionList/DefinitionList.scss | 56 +++----- .../DefinitionList/DefinitionList.tsx | 93 +++++-------- src/components/DefinitionList/README.md | 93 +++++-------- .../__stories__/DefinitionList.stories.tsx | 62 +++++---- .../__tests__/DefinitionList.test.tsx | 51 +++---- .../DefinitionList/components/Definition.tsx | 6 +- .../components/DefinitionListContext.tsx | 52 +++++++ .../components/DefinitionListItem.tsx | 34 +++++ .../DefinitionList/components/Term.tsx | 23 ++-- src/components/DefinitionList/types.ts | 8 +- src/components/DefinitionList/utils.ts | 6 +- src/components/HelpMark/HelpMark.tsx | 30 +++- src/components/HelpMark/README.md | 130 ++++++------------ .../HelpMark/__stories__/HelpMark.stories.tsx | 49 +------ .../__stories__/HelpMarkShowcase.scss | 9 -- .../HelpMark/__tests__/HelpMark.test.tsx | 2 +- 17 files changed, 367 insertions(+), 458 deletions(-) create mode 100644 src/components/DefinitionList/components/DefinitionListContext.tsx create mode 100644 src/components/DefinitionList/components/DefinitionListItem.tsx delete mode 100644 src/components/HelpMark/__stories__/HelpMarkShowcase.scss diff --git a/CODEOWNERS b/CODEOWNERS index fc9184ebfe..bf4622efbb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,63 +1,63 @@ -* @amje @ValeraS @korvin89 -/src/components/ActionTooltip @amje -/src/components/Alert @IsaevAlexandr -/src/components/ArrowToggle @Marginy605 -/src/components/Avatar @DakEnviy -/src/components/AvatarStack @ogonkov -#/src/components/Breadcrumbs -/src/components/Button @amje -/src/components/Card @Lunory -/src/components/Checkbox @zamkovskaya -/src/components/ClipboardButton @Raubzeug -/src/components/ClipboardIcon @Raubzeug -/src/components/ControlLabel @korvin89 -/src/components/CopyToClipboard @SeqviriouM -/src/components/DefinitionList @Raubzeug -/src/components/HelpMark @Raubzeug -#/src/components/Dialog -/src/components/Disclosure @Raubzeug -/src/components/Divider @v4dyar4 -/src/components/DropdownMenu @axtk -/src/components/Hotkey @d3m1d0v -/src/components/Icon @amje -/src/components/Label @goshander -/src/components/Link @Estasie -/src/components/List @korvin89 -/src/components/Loader @SeqviriouM -/src/components/Menu @NikitaCG -/src/components/Modal @amje -/src/components/Overlay @Vladeeg -/src/components/Pagination @jhoncool -/src/components/Palette @Ruminat -/src/components/PinInput @amje -/src/components/PlaceholderContainer @Marginy605 -/src/components/Popover @kseniya57 -/src/components/Popup @amje -/src/components/Portal @amje -/src/components/Progress @Lunory -/src/components/Radio @zamkovskaya -/src/components/RadioButton @zamkovskaya -/src/components/RadioGroup @zamkovskaya -/src/components/User @DakEnviy -/src/components/UserLabel @DakEnviy -/src/components/useList @IsaevAlexandr -/src/components/Select @korvin89 -/src/components/Sheet @mournfulCoroner -/src/components/Skeleton @SeqviriouM -/src/components/Slider @Arucard89 -/src/components/Spin @SeqviriouM -/src/components/Stories @DarkGenius -/src/components/Switch @zamkovskaya -/src/components/Table @Raubzeug -/src/components/Tabs @sofiushko -/src/components/Text @IsaevAlexandr -/src/components/TreeList @IsaevAlexandr -/src/components/TreeSelect @IsaevAlexandr -/src/components/controls/TextArea @korvin89 -/src/components/controls/TextInput @korvin89 -/src/components/Toaster @ogonkov -/src/components/Tooltip @amje -/src/components/layout @IsaevAlexandr +- @amje @ValeraS @korvin89 + /src/components/ActionTooltip @amje + /src/components/Alert @IsaevAlexandr + /src/components/ArrowToggle @Marginy605 + /src/components/Avatar @DakEnviy + /src/components/AvatarStack @ogonkov + #/src/components/Breadcrumbs + /src/components/Button @amje + /src/components/Card @Lunory + /src/components/Checkbox @zamkovskaya + /src/components/ClipboardButton @Raubzeug + /src/components/ClipboardIcon @Raubzeug + /src/components/ControlLabel @korvin89 + /src/components/CopyToClipboard @SeqviriouM + /src/components/DefinitionList @Raubzeug + #/src/components/Dialog + /src/components/Disclosure @Raubzeug + /src/components/Divider @v4dyar4 + /src/components/DropdownMenu @axtk + /src/components/HelpMark @Raubzeug + /src/components/Hotkey @d3m1d0v + /src/components/Icon @amje + /src/components/Label @goshander + /src/components/Link @Estasie + /src/components/List @korvin89 + /src/components/Loader @SeqviriouM + /src/components/Menu @NikitaCG + /src/components/Modal @amje + /src/components/Overlay @Vladeeg + /src/components/Pagination @jhoncool + /src/components/Palette @Ruminat + /src/components/PinInput @amje + /src/components/PlaceholderContainer @Marginy605 + /src/components/Popover @kseniya57 + /src/components/Popup @amje + /src/components/Portal @amje + /src/components/Progress @Lunory + /src/components/Radio @zamkovskaya + /src/components/RadioButton @zamkovskaya + /src/components/RadioGroup @zamkovskaya + /src/components/User @DakEnviy + /src/components/UserLabel @DakEnviy + /src/components/useList @IsaevAlexandr + /src/components/Select @korvin89 + /src/components/Sheet @mournfulCoroner + /src/components/Skeleton @SeqviriouM + /src/components/Slider @Arucard89 + /src/components/Spin @SeqviriouM + /src/components/Stories @DarkGenius + /src/components/Switch @zamkovskaya + /src/components/Table @Raubzeug + /src/components/Tabs @sofiushko + /src/components/Text @IsaevAlexandr + /src/components/TreeList @IsaevAlexandr + /src/components/TreeSelect @IsaevAlexandr + /src/components/controls/TextArea @korvin89 + /src/components/controls/TextInput @korvin89 + /src/components/Toaster @ogonkov + /src/components/Tooltip @amje + /src/components/layout @IsaevAlexandr /src/hooks/useActionHandlers @ogonkov /src/hooks/useFileInput @korvin89 @@ -69,5 +69,6 @@ /src/hooks/useUniqId @ValeraS # Allow everyone to update dependencies + /package.json /package-lock.json diff --git a/src/components/DefinitionList/DefinitionList.scss b/src/components/DefinitionList/DefinitionList.scss index 77071626d7..2454d572bd 100644 --- a/src/components/DefinitionList/DefinitionList.scss +++ b/src/components/DefinitionList/DefinitionList.scss @@ -15,19 +15,16 @@ $block: '.#{variables.$ns}definition-list'; align-items: baseline; gap: var(--g-spacing-1); - :where(& + &) { - margin-block-start: var( - --g-definition-list-item-block-start, - var(--_--item-block-start) - ); + & + & { + margin-block-start: var(--g-definition-list-item-gap, var(--_--item-block-start)); } } &__term-container { + display: flex; flex: 0 0 auto; width: var(--_--term-width); max-width: var(--_--term-width); - display: flex; align-items: baseline; overflow: hidden; @@ -35,37 +32,17 @@ $block: '.#{variables.$ns}definition-list'; } &__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; + min-width: 25px; 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; @@ -77,6 +54,18 @@ $block: '.#{variables.$ns}definition-list'; flex: 1 0 50%; } } + &_vertical { + --_--item-block-start: var(--g-spacing-3); + --_--term-width: auto; + + #{$block}__term-container { + flex: 1 0 auto; + } + #{$block}__item { + flex-direction: column; + gap: var(--g-spacing-half); + } + } &__copy-container { position: relative; @@ -104,16 +93,3 @@ $block: '.#{variables.$ns}definition-list'; } } } - -#{$block}_vertical { - --_--item-block-start: var(--g-spacing-3); - --_--term-width: auto; - - #{$block}__term-container { - flex: 1 0 auto; - } - #{$block}__item { - flex-direction: column; - gap: var(--g-spacing-half); - } -} diff --git a/src/components/DefinitionList/DefinitionList.tsx b/src/components/DefinitionList/DefinitionList.tsx index 5bc809a95d..a217f71eb1 100644 --- a/src/components/DefinitionList/DefinitionList.tsx +++ b/src/components/DefinitionList/DefinitionList.tsx @@ -1,82 +1,55 @@ import React from 'react'; -import {Definition} from './components/Definition'; -import {Term} from './components/Term'; +import {isOfType} from '../utils/isOfType'; + +import {DefinitionListProvider} from './components/DefinitionListContext'; +import {DefinitionListItem} from './components/DefinitionListItem'; import type {DefinitionListProps} from './types'; -import {b, getTitle, isUnbreakableOver} from './utils'; +import {b} from './utils'; import './DefinitionList.scss'; export function DefinitionList({ - items, responsive, direction = 'horizontal', nameMaxWidth, contentMaxWidth, className, + children, qa, }: DefinitionListProps) { - const keyStyle = nameMaxWidth ? {maxWidth: nameMaxWidth, width: nameMaxWidth} : {}; - - const valueStyle = - typeof contentMaxWidth === 'number' - ? {width: contentMaxWidth, maxWidth: contentMaxWidth} - : {}; - - const normalizedItems = React.useMemo(() => { - return items.map((value, index) => ({...value, key: index})); - }, [items]); - + const normalizedChildren = prepareChildren(children); return (
-
- {normalizedItems.map((item) => { - const { - name, - key, - content, - contentTitle, - nameTitle, - copyText, - note, - multilineName, - } = item; - - return ( -
-
- -
-
- -
-
- ); - })} -
+ +
{normalizedChildren}
+
); } + +const isDefinitionListItem = isOfType(DefinitionListItem); + +function prepareChildren(children: React.ReactNode) { + const items = React.Children.toArray(children); + + for (const item of items) { + const isItem = isDefinitionListItem(item); + if (!isItem) { + throw new Error( + 'Only components is allowed inside ', + ); + } + } + return children; +} + +DefinitionList.Item = DefinitionListItem; +DefinitionList.displayName = 'DefinitionList'; diff --git a/src/components/DefinitionList/README.md b/src/components/DefinitionList/README.md index 1e6cbbacf8..b95b0fa9d1 100644 --- a/src/components/DefinitionList/README.md +++ b/src/components/DefinitionList/README.md @@ -12,32 +12,20 @@ The component to display definition list with term and definition separated by d value with copy, - copyText: 'value', - }, - {name: 'Empty value with copy', copyText: 'nothing to copy'}, - ]} - nameMaxWidth={100} - contentMaxWidth={100} -/> + + + value with copy + + + `} > - value with copy, - copyText: 'value', - }, - {name: 'Empty value with copy', copyText: 'nothing to copy'}, - ]} - nameMaxWidth={100} - contentMaxWidth={100} - /> + + + value with copy + + +; LANDING_BLOCK--> @@ -45,49 +33,40 @@ LANDING_BLOCK--> ```tsx -value with copy, - copyText: 'value', - }, - {name: 'Empty value with copy', copyText: 'nothing to copy'}, - ]} - nameMaxWidth="100" - contentMaxWidth="100" -/> + + + value with copy + + + ``` ## Properties -| 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 | -| direction | `'horizontal'` \| `'vertical'` | | 'horizontal' | If set to `vertical` content will be located under name and list will take 100% width of its parent | -| nameMaxWidth | `number` | | | Maximum width of term | -| contentMaxWidth | `number` | | | Maximum width of definition | -| className | `string` | | | Class name for the definition list | +| Property | Type | Required | Default | Description | +| :----------------- | :----------------------------- | :------: | :----------- | :-------------------------------------------------------------------------------------------------- | +| [children](#items) | `React.ReactNode` | yes | | Items of the list | +| responsive | `boolean` | | | If set to `true` list will take 100% width of its parent | +| direction | `'horizontal'` \| `'vertical'` | | 'horizontal' | If set to `vertical` content will be located under name and list will take 100% width of its parent | +| nameMaxWidth | `number` | | | Maximum width of term | +| contentMaxWidth | `number` | | | Maximum width of definition | +| className | `string` | | | Class name for the definition list | ### Items -Configuration for list items +DefinitionList children should be components of type `DefinitionList.Item` with following properties: -| 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 \| HelpMarkProps` | | | If set, HelpMark will be shown next to term | +| Property | Type | Required | Default | Description | +| -------- | ------------------------- | -------- | ------- | ------------------------------------------------ | +| name | `ReactNode` | true | | Term | +| children | `ReactNode` | | | Definition | +| copyText | `string` | | | If set, it will be shown icon for copy this text | +| note | `string \| HelpMarkProps` | | | If set, HelpMark will be shown next to term | ## CSS API -| Name | Description | -| :------------------------------------- | :---------------------------------- | -| `--g-definition-list-item-block-start` | Space between definition list items | +| Name | Description | +| :----------------------------- | :---------------------------------- | +| `--g-definition-list-item-gap` | Space between definition list items | diff --git a/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx index 38423156eb..a281d1b2be 100644 --- a/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx +++ b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx @@ -6,10 +6,14 @@ import {Label} from '../../Label'; import {Link} from '../../Link'; import {User} from '../../User'; import {DefinitionList} from '../DefinitionList'; -import type {DefinitionListItem, DefinitionListProps} from '../types'; +import type {DefinitionListItem as DefinitionListItemProps, DefinitionListProps} from '../types'; -const items: DefinitionListItem[] = [ - {name: String value, content: 'value'}, +const items: DefinitionListItemProps[] = [ + { + name: String value, + children: 'value', + note: 'link', + }, { name: ( ), - content: 'value', - note: 'This is avatar', + children: 'value', }, - {name: 'Number value', content: 2}, - {name: 'Node value', content: value}, + {name: 'Number value', children: 2, note: 'This is value'}, + {name: 'Node value', children: value}, {name: 'Empty value'}, - {name: 'String value with copy', content: 'value', copyText: 'value'}, - {name: 'Number value with copy', content: 2, copyText: 'two'}, - {name: 'Node value with copy', content: value, copyText: 'value'}, + {name: 'String value with copy', children: 'value', copyText: 'value'}, + {name: 'Number value with copy', children: 2, copyText: 'two'}, + {name: 'Node value with copy', children: value, copyText: 'value'}, {name: 'Empty value with copy', copyText: 'nothing to copy'}, - {name: 'String value with custom title', content: 'value', contentTitle: "value's title"}, - {name: 'Number value with custom title', content: 2, contentTitle: "value's title"}, + {name: 'String value with custom title', children: 'value'}, + {name: 'Number value with custom title', children: 2}, { name: 'Node value with custom title', - content: value, - contentTitle: "value's title", + children: value, }, - {name: 'Empty value with custom title', contentTitle: "value's title"}, + {name: 'Empty value with custom title'}, { name: 'String long value', - content: + children: 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', }, { name: 'String long value with copy', - content: + children: 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', copyText: 'The HTML
element represents a description list. The element encloses a list of groups of terms (specified using the
element) and descriptions (provided by
elements). Common uses for this element are to implement a glossary or to display metadata (a list of key-value pairs)', @@ -56,11 +58,11 @@ const items: DefinitionListItem[] = [ { name: 'Number long value', // eslint-disable-next-line no-loss-of-precision - content: 12345678901234567890123456789012345678901234567890123456789012345678901234567890, + children: 12345678901234567890123456789012345678901234567890123456789012345678901234567890, }, { name: 'Node long value', - content: ( + children: ( The{' '} @@ -88,35 +90,34 @@ const items: DefinitionListItem[] = [ }, { name: 'String long value without whitespace', - content: + children: 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', }, { name: 'String long looooooooooooooong looooooooooooooong looooooooooooooong looooooooooooooong value without multiline and with copy icon', - multilineName: true, note: 'This is multiline value', - content: + children: 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', copyText: 'https://example.com/long-long/like/beyond/the/farthest/lands/long/path/to/handle?and=some&list=of&query=parameters&that=is&overcomplicated=maybe&with=some&token=inside¬=really&readable=but&sometimes=useful', }, { name: 'String value with tooltip', - content: 'value', + children: 'value', note: 'This is simple string value', }, { name: 'String value with very very very looooooooooooooong key', - content: 'value', + children: 'value', }, { name: 'String value with very very very looooooooooooooong key and tooltip', - content: 'value', + children: 'value', note: 'This is simple string value', }, { name: 'Avatar with tooltip', - content: ( + children: ( label, + children: , }, ]; +const definitionListItems = items.map(({children, ...rest}, index) => ( + + {children} + +)); export default { title: 'Components/Data Display/DefinitionList', component: DefinitionList, args: { - items, responsive: false, + children: definitionListItems, }, parameters: { a11y: { diff --git a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx index 8fd623c7c7..a8673eff35 100644 --- a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx +++ b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx @@ -2,22 +2,27 @@ import React from 'react'; import {render, screen} from '../../../../test-utils/utils'; import {DefinitionList} from '../DefinitionList'; +import type {DefinitionListItem, DefinitionListProps} from '../types'; import {b} from '../utils'; const qaAttribute = 'definition-list'; -const getComponent = (props = {}) => - render( - node value
}, - ]} - {...props} - />, +const defaultItems: DefinitionListItem[] = [ + {name: 'test1', children: 'value1'}, + {name: 'test2', children: 2}, + {name: 'test3', children:
node value
}, +]; + +const getComponent = (props?: Partial & {items?: DefinitionListItem[]}) => { + const {items = defaultItems} = props ?? {}; + return render( + + {items.map((item, index) => ( + + ))} + , ).container; +}; describe('components: DefinitionList', () => { it('should render', () => { @@ -30,26 +35,13 @@ describe('components: DefinitionList', () => { const component = screen.getByTestId(qaAttribute); expect(component).toHaveClass('testClassName'); }); - - it('should render passed content title', () => { - const items = [{name: 'test1', content: 'value1', contentTitle: 'contentTitle1'}]; - getComponent({items}); - const component = screen.getByText('value1'); - expect(component).toHaveAttribute('title', 'contentTitle1'); - }); - it('should render passed name title', () => { - const items = [{name: 'test1', nameTitle: 'nameTitle1'}]; - getComponent({items}); - const component = screen.getByText('test1'); - expect(component).toHaveAttribute('title', 'nameTitle1'); - }); it('should not render clipboard button by default', () => { getComponent(); const copyButton = screen.queryByRole('button'); expect(copyButton).toBeNull(); }); it('should render clipboard button', () => { - const items = [{name: 'test1', content: 'value1', copyText: 'value1'}]; + const items = [{name: 'test1', children: 'value1', copyText: 'value1'}]; getComponent({items}); const copyButton = screen.getByRole('button'); @@ -57,19 +49,12 @@ describe('components: DefinitionList', () => { expect(copyButton).toHaveClass(b('copy-button')); }); it('should render in responsive mode', () => { - const items = [{name: 'test1', content: 'value1', copyText: 'value1'}]; + const items = [{name: 'test1', children: 'value1', copyText: 'value1'}]; getComponent({items, responsive: true}); const component = screen.getByTestId(qaAttribute); expect(component).toHaveClass(b({responsive: true})); }); - it('should render with multiline term', () => { - const items = [{name: 'test1', content: 'value1', copyText: 'value1', multilineName: true}]; - getComponent({items}); - - const component = screen.getByRole('term'); - expect(component).toHaveClass(b('term-container', {multiline: true})); - }); it('should render vertical view', () => { getComponent({direction: 'vertical'}); const component = screen.getByTestId(qaAttribute); diff --git a/src/components/DefinitionList/components/Definition.tsx b/src/components/DefinitionList/components/Definition.tsx index a1e16ebb6e..31c73846dc 100644 --- a/src/components/DefinitionList/components/Definition.tsx +++ b/src/components/DefinitionList/components/Definition.tsx @@ -4,10 +4,10 @@ import {ClipboardButton} from '../../ClipboardButton'; import type {DefinitionListItem} from '../types'; import {b} from '../utils'; -interface DefinitionProps extends Pick {} +interface DefinitionProps extends Pick {} -export function Definition({copyText, content}: DefinitionProps) { - const definitionContent = content ?? '—'; +export function Definition({copyText, children}: DefinitionProps) { + const definitionContent = children ?? '—'; return copyText ? (
diff --git a/src/components/DefinitionList/components/DefinitionListContext.tsx b/src/components/DefinitionList/components/DefinitionListContext.tsx new file mode 100644 index 0000000000..31073298a4 --- /dev/null +++ b/src/components/DefinitionList/components/DefinitionListContext.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +import type {DefinitionListProps} from '../types'; + +interface DefinitionListProviderProps + extends Pick { + children?: React.ReactNode; +} + +export const DefinitionListAttributesContext = React.createContext< + | (Pick & { + keyStyle?: React.CSSProperties; + valueStyle?: React.CSSProperties; + }) + | undefined +>(undefined); + +export function DefinitionListProvider({ + direction, + contentMaxWidth, + nameMaxWidth, + children, +}: DefinitionListProviderProps) { + const keyStyle = nameMaxWidth ? {maxWidth: nameMaxWidth, width: nameMaxWidth} : {}; + + const valueStyle = + typeof contentMaxWidth === 'number' + ? {width: contentMaxWidth, maxWidth: contentMaxWidth} + : {}; + + return ( + + {children} + + ); +} + +export function useDefinitionListAttributes() { + const state = React.useContext(DefinitionListAttributesContext); + + if (state === undefined) { + throw new Error('useDefinitionListAttributes must be used within DefinitionListProvider'); + } + + return state; +} diff --git a/src/components/DefinitionList/components/DefinitionListItem.tsx b/src/components/DefinitionList/components/DefinitionListItem.tsx new file mode 100644 index 0000000000..34c1795d80 --- /dev/null +++ b/src/components/DefinitionList/components/DefinitionListItem.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import type {DefinitionListItem as DefinitionListItemProps} from '../types'; +import {b, getTitle, isUnbreakableOver} from '../utils'; + +import {Definition} from './Definition'; +import {useDefinitionListAttributes} from './DefinitionListContext'; +import {Term} from './Term'; + +export function DefinitionListItem({name, children, copyText, note}: DefinitionListItemProps) { + const {direction, keyStyle, valueStyle} = useDefinitionListAttributes(); + return ( +
+
+ +
+
+ {children} +
+ + ); +} + +DefinitionListItem.displayName = 'DefinitionListItemin'; diff --git a/src/components/DefinitionList/components/Term.tsx b/src/components/DefinitionList/components/Term.tsx index eb85645bd7..f75f589b07 100644 --- a/src/components/DefinitionList/components/Term.tsx +++ b/src/components/DefinitionList/components/Term.tsx @@ -18,12 +18,13 @@ function NoteElement({note}: NoteElementsProps) { return ( + > + {note} + ); } @@ -45,28 +46,24 @@ function NoteElement({note}: NoteElementsProps) { return null; } -interface TermProps - extends Pick { +interface TermProps extends Pick { direction?: DefinitionListDirection; } -export function Term({note, name, nameTitle, multilineName, direction}: TermProps) { - const noteElement = ( +export function Term({note, name, direction}: TermProps) { + const noteElement = note ? (   - ); + ) : null; return (
- {name} - {multilineName && noteElement} + {name} + {noteElement}
- {!multilineName && noteElement} - {direction === 'horizontal' && ( -
- )} + {direction === 'horizontal' &&
} ); } diff --git a/src/components/DefinitionList/types.ts b/src/components/DefinitionList/types.ts index 589b8fb340..0522c49c55 100644 --- a/src/components/DefinitionList/types.ts +++ b/src/components/DefinitionList/types.ts @@ -6,23 +6,19 @@ export type DefinitionListItemNote = string | HelpMarkProps; export interface DefinitionListItem { name: React.ReactNode; - content?: React.ReactNode; - contentTitle?: string; - nameTitle?: string; + children?: React.ReactNode; copyText?: string; note?: DefinitionListItemNote; - multilineName?: boolean; } export type DefinitionListDirection = 'vertical' | 'horizontal'; export interface DefinitionListProps extends QAProps { - items: DefinitionListItem[]; responsive?: boolean; direction?: DefinitionListDirection; nameMaxWidth?: number; contentMaxWidth?: number; className?: string; - itemClassName?: string; groupLabelClassName?: string; + children: React.ReactNode; } diff --git a/src/components/DefinitionList/utils.ts b/src/components/DefinitionList/utils.ts index 8004627f0a..4a88e43866 100644 --- a/src/components/DefinitionList/utils.ts +++ b/src/components/DefinitionList/utils.ts @@ -12,11 +12,7 @@ export function isUnbreakableOver(limit: number) { }; } -export function getTitle(title?: string, content?: React.ReactNode) { - if (title) { - return title; - } - +export function getTitle(content?: React.ReactNode) { if (typeof content === 'string' || typeof content === 'number') { return String(content); } diff --git a/src/components/HelpMark/HelpMark.tsx b/src/components/HelpMark/HelpMark.tsx index d9d7a5cc35..b7528d017b 100644 --- a/src/components/HelpMark/HelpMark.tsx +++ b/src/components/HelpMark/HelpMark.tsx @@ -3,8 +3,8 @@ import React from 'react'; import {CircleQuestion} from '@gravity-ui/icons'; import {Icon} from '../Icon'; -import type {PopoverProps} from '../Popover'; import {Popover} from '../Popover'; +import type {PopupPlacement} from '../Popup'; import type {QAProps} from '../types'; import {block} from '../utils/cn'; @@ -13,19 +13,35 @@ import './HelpMark.scss'; const b = block('help-mark'); const ICON_SIZE = 16; -export interface HelpMarkProps extends Omit, QAProps { +export interface HelpMarkProps extends QAProps { buttonProps?: React.ButtonHTMLAttributes; buttonRef?: React.RefObject; + delayClosing?: number; + placement?: PopupPlacement; + className?: string; + children?: React.ReactNode; } -export function HelpMark(props: HelpMarkProps) { +export function HelpMark({ + buttonRef, + buttonProps = {}, + children, + className, + delayClosing = 300, + ...rest +}: HelpMarkProps) { return ( - + diff --git a/src/components/HelpMark/README.md b/src/components/HelpMark/README.md index 6f40f4f33c..93e8554ef9 100644 --- a/src/components/HelpMark/README.md +++ b/src/components/HelpMark/README.md @@ -18,60 +18,34 @@ Component with rendered raw html (use `content` for plain text) and close on mou alert('Lorem ipsum onClick'), - }, - ]} - to={['right', 'bottom']} - title="Simple tooltip" - htmlContent={ - 'Lorem ipsum dolor sit amet, at scelerisque suspendisse' - } - tooltipButton={{ - text: 'Actions', - onClick: () => console.log('just action happened'), - }} - /> + console.log('just action happened'), + }} +> + Lorem ipsum dolor sit{' '} + + amet + + , at scelerisque suspendisse + `} > - alert('Lorem ipsum onClick'), - }, - ]} - to={['right', 'bottom']} - title="Simple tooltip" - htmlContent={ - 'Lorem ipsum dolor sit amet, at scelerisque suspendisse' - } - tooltipButton={{ - text: 'Actions', + console.log('just action happened'), - }} - /> + }} +> + Lorem ipsum dolor sit{' '} + + amet + + , at scelerisque suspendisse + LANDING_BLOCK--> @@ -80,31 +54,18 @@ LANDING_BLOCK--> ```tsx alert('Lorem ipsum onClick'), - }, - ]} - to={['right', 'bottom']} - title="Simple tooltip" - htmlContent={ - 'Lorem ipsum dolor sit amet, at scelerisque suspendisse' - } - tooltipButton={{ - text: 'Actions', + placement={['right', 'bottom']} + buttonProps={{ onClick: () => console.log('just action happened'), }} -/> +> + Lorem ipsum dolor sit{' '} + + amet + + , at scelerisque suspendisse + ``` @@ -133,18 +94,11 @@ LANDING_BLOCK--> ## Properties -| Property | Type | Required | Default | Description | -| :------------ | :---------------------------------------------- | :------- | :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | -| theme | `info` `special` | | `info` | Appearance | -| className | `String` | | | Control class name | -| placement | `Array` | | [`right`, `bottom`] | Allowed popover positions | -| autoclosable | `Boolean` | | `true` | Close popover when pointer is outside of control | -| delayClosing | `Number` | | `300` | Timeout before closing popover (see `autoclosable`) | -| title | `String` | | | Popover title | -| content | `ReactNode` | | | Popover content | -| htmlContent | `String` | | | Render HTML via `dangerouslySetInnerHTML` | -| links | `Array` | | [] | Links below content, could be
`{ text: 'Link 1', href: 'https://example.com'}` or
`{ text: 'Link 2', onClick: () => onLinkClick() }` | -| tooltipButton | `Object` | | | Render button with this value
`{ text: 'Button', onClick: () => onClick() }` | -| offset | `Object` | | `{ left: 4 }` | Control popup toggle position offset
`{ top: 0, left: 0 }` | -| buttonProps | `React.ButtonHTMLAttributes` | | | Set attributes to the underlying button element | -| buttonRef | `React.RefObject` | | | Ref to the underlying button element | +| Property | Type | Required | Default | Description | +| :----------- | :---------------------------------------------- | :------- | :------------------ | :---------------------------------------------- | +| className | `String` | | | Control class name | +| placement | `Array` | | [`right`, `bottom`] | Allowed popover positions | +| delayClosing | `Number` | | `300` | Timeout before closing popover | +| children | `ReactNode` | | | Popover content | +| buttonProps | `React.ButtonHTMLAttributes` | | | Set attributes to the underlying button element | +| buttonRef | `React.RefObject` | | | Ref to the underlying button element | diff --git a/src/components/HelpMark/__stories__/HelpMark.stories.tsx b/src/components/HelpMark/__stories__/HelpMark.stories.tsx index e3bc0b646e..72a54ee98c 100644 --- a/src/components/HelpMark/__stories__/HelpMark.stories.tsx +++ b/src/components/HelpMark/__stories__/HelpMark.stories.tsx @@ -2,14 +2,9 @@ import React from 'react'; import type {Meta, StoryFn} from '@storybook/react'; -import {cn} from '../../utils/cn'; import {HelpMark} from '../HelpMark'; import type {HelpMarkProps} from '../HelpMark'; -import './HelpMarkShowcase.scss'; - -const b = cn('help-mark-showcase'); - export default { title: 'Components/Utils/HelpMark', id: 'components/utils/HelpMark', @@ -39,47 +34,5 @@ export default { const DefaultTemplate: StoryFn = (args) => ; export const Default = DefaultTemplate.bind({}); Default.args = { - content: 'Some content', -}; - -export const Accessible: StoryFn = (args) => { - const helpMarkWithoutActionsId = 'helpMarkWithoutActionsId'; - const helpMarkWithActionsId = 'helpMarkWithActionsId'; - const [openPopover, setOpenPopover] = React.useState(false); - const ref = React.useRef(null); - return ( -
-
- Without actions: - -
-
- With actions: - Some link} - openOnHover={false} - onOpenChange={setOpenPopover} - focusTrap - autoFocus - restoreFocusRef={ref} - buttonProps={{ - 'aria-expanded': openPopover, - 'aria-controls': helpMarkWithActionsId, - 'aria-label': 'More info', - }} - buttonRef={ref} - /> -
-
- ); + children: 'Some content', }; diff --git a/src/components/HelpMark/__stories__/HelpMarkShowcase.scss b/src/components/HelpMark/__stories__/HelpMarkShowcase.scss deleted file mode 100644 index da034acf54..0000000000 --- a/src/components/HelpMark/__stories__/HelpMarkShowcase.scss +++ /dev/null @@ -1,9 +0,0 @@ -.help-mark-showcase { - &__container { - display: flex; - margin-block-end: 10px; - } - &__container-title { - margin-inline-end: 5px; - } -} diff --git a/src/components/HelpMark/__tests__/HelpMark.test.tsx b/src/components/HelpMark/__tests__/HelpMark.test.tsx index 104dd6fe85..00e451fa3a 100644 --- a/src/components/HelpMark/__tests__/HelpMark.test.tsx +++ b/src/components/HelpMark/__tests__/HelpMark.test.tsx @@ -18,7 +18,7 @@ describe('HelpMark', () => { test('render popup when hover help icon', async () => { const title = 'HelpMark title'; - render(); + render({title}); const icon = screen.getByTestId(qaId); expect(icon).toBeVisible(); From fe51f4fbfaac51795555c3dab86940802c53adb6 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 11 Oct 2024 17:24:53 +0300 Subject: [PATCH 6/7] fix: review --- CODEOWNERS | 121 +++++++++--------- .../DefinitionList/DefinitionList.scss | 4 +- .../DefinitionList/DefinitionList.tsx | 34 ++--- src/components/DefinitionList/README.md | 6 +- .../__stories__/DefinitionList.stories.tsx | 24 +--- .../__tests__/DefinitionList.test.tsx | 12 +- .../DefinitionList/components/Definition.tsx | 6 +- .../components/DefinitionListItem.tsx | 7 +- .../DefinitionList/components/Term.tsx | 11 +- src/components/DefinitionList/constants.ts | 3 + src/components/DefinitionList/index.ts | 2 +- src/components/DefinitionList/types.ts | 2 +- src/components/DefinitionList/utils.ts | 4 - src/components/HelpMark/HelpMark.tsx | 27 ++-- src/components/HelpMark/README.md | 42 ++---- .../HelpMark/__stories__/HelpMark.stories.tsx | 17 +-- 16 files changed, 142 insertions(+), 180 deletions(-) create mode 100644 src/components/DefinitionList/constants.ts diff --git a/CODEOWNERS b/CODEOWNERS index bf4622efbb..dd3fceac5c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,63 +1,64 @@ -- @amje @ValeraS @korvin89 - /src/components/ActionTooltip @amje - /src/components/Alert @IsaevAlexandr - /src/components/ArrowToggle @Marginy605 - /src/components/Avatar @DakEnviy - /src/components/AvatarStack @ogonkov - #/src/components/Breadcrumbs - /src/components/Button @amje - /src/components/Card @Lunory - /src/components/Checkbox @zamkovskaya - /src/components/ClipboardButton @Raubzeug - /src/components/ClipboardIcon @Raubzeug - /src/components/ControlLabel @korvin89 - /src/components/CopyToClipboard @SeqviriouM - /src/components/DefinitionList @Raubzeug - #/src/components/Dialog - /src/components/Disclosure @Raubzeug - /src/components/Divider @v4dyar4 - /src/components/DropdownMenu @axtk - /src/components/HelpMark @Raubzeug - /src/components/Hotkey @d3m1d0v - /src/components/Icon @amje - /src/components/Label @goshander - /src/components/Link @Estasie - /src/components/List @korvin89 - /src/components/Loader @SeqviriouM - /src/components/Menu @NikitaCG - /src/components/Modal @amje - /src/components/Overlay @Vladeeg - /src/components/Pagination @jhoncool - /src/components/Palette @Ruminat - /src/components/PinInput @amje - /src/components/PlaceholderContainer @Marginy605 - /src/components/Popover @kseniya57 - /src/components/Popup @amje - /src/components/Portal @amje - /src/components/Progress @Lunory - /src/components/Radio @zamkovskaya - /src/components/RadioButton @zamkovskaya - /src/components/RadioGroup @zamkovskaya - /src/components/User @DakEnviy - /src/components/UserLabel @DakEnviy - /src/components/useList @IsaevAlexandr - /src/components/Select @korvin89 - /src/components/Sheet @mournfulCoroner - /src/components/Skeleton @SeqviriouM - /src/components/Slider @Arucard89 - /src/components/Spin @SeqviriouM - /src/components/Stories @DarkGenius - /src/components/Switch @zamkovskaya - /src/components/Table @Raubzeug - /src/components/Tabs @sofiushko - /src/components/Text @IsaevAlexandr - /src/components/TreeList @IsaevAlexandr - /src/components/TreeSelect @IsaevAlexandr - /src/components/controls/TextArea @korvin89 - /src/components/controls/TextInput @korvin89 - /src/components/Toaster @ogonkov - /src/components/Tooltip @amje - /src/components/layout @IsaevAlexandr +* @amje @ValeraS @korvin89 +/src/components/ActionsPanel @jhoncool +/src/components/ActionTooltip @amje +/src/components/Alert @IsaevAlexandr +/src/components/ArrowToggle @Marginy605 +/src/components/Avatar @DakEnviy +/src/components/AvatarStack @ogonkov +#/src/components/Breadcrumbs +/src/components/Button @amje +/src/components/Card @Lunory +/src/components/Checkbox @zamkovskaya +/src/components/ClipboardButton @Raubzeug +/src/components/ClipboardIcon @Raubzeug +/src/components/ControlLabel @korvin89 +/src/components/CopyToClipboard @SeqviriouM +/src/components/DefinitionList @Raubzeug +#/src/components/Dialog +/src/components/Disclosure @Raubzeug +/src/components/Divider @v4dyar4 +/src/components/DropdownMenu @axtk +/src/components/HelpMark @Raubzeug +/src/components/Hotkey @d3m1d0v +/src/components/Icon @amje +/src/components/Label @goshander +/src/components/Link @Estasie +/src/components/List @korvin89 +/src/components/Loader @SeqviriouM +/src/components/Menu @NikitaCG +/src/components/Modal @amje +/src/components/Overlay @Vladeeg +/src/components/Pagination @jhoncool +/src/components/Palette @Ruminat +/src/components/PinInput @amje +/src/components/PlaceholderContainer @Marginy605 +/src/components/Popover @kseniya57 +/src/components/Popup @amje +/src/components/Portal @amje +/src/components/Progress @Lunory +/src/components/Radio @zamkovskaya +/src/components/RadioButton @zamkovskaya +/src/components/RadioGroup @zamkovskaya +/src/components/User @DakEnviy +/src/components/UserLabel @DakEnviy +/src/components/useList @IsaevAlexandr +/src/components/Select @korvin89 +/src/components/Sheet @mournfulCoroner +/src/components/Skeleton @SeqviriouM +/src/components/Slider @Arucard89 +/src/components/Spin @SeqviriouM +/src/components/Stories @DarkGenius +/src/components/Switch @zamkovskaya +/src/components/Table @Raubzeug +/src/components/Tabs @sofiushko +/src/components/Text @IsaevAlexandr +/src/components/TreeList @IsaevAlexandr +/src/components/TreeSelect @IsaevAlexandr +/src/components/controls/TextArea @korvin89 +/src/components/controls/TextInput @korvin89 +/src/components/Toaster @ogonkov +/src/components/Tooltip @amje +/src/components/layout @IsaevAlexandr /src/hooks/useActionHandlers @ogonkov /src/hooks/useFileInput @korvin89 diff --git a/src/components/DefinitionList/DefinitionList.scss b/src/components/DefinitionList/DefinitionList.scss index 2454d572bd..95736e0e26 100644 --- a/src/components/DefinitionList/DefinitionList.scss +++ b/src/components/DefinitionList/DefinitionList.scss @@ -6,9 +6,7 @@ $block: '.#{variables.$ns}definition-list'; #{$block} { --_--item-block-start: var(--g-spacing-4); --_--term-width: 300px; - &__list { - margin: 0; - } + margin: 0; &__item { display: flex; diff --git a/src/components/DefinitionList/DefinitionList.tsx b/src/components/DefinitionList/DefinitionList.tsx index a217f71eb1..531337ec34 100644 --- a/src/components/DefinitionList/DefinitionList.tsx +++ b/src/components/DefinitionList/DefinitionList.tsx @@ -1,11 +1,12 @@ import React from 'react'; import {isOfType} from '../utils/isOfType'; +import {warnOnce} from '../utils/warn'; import {DefinitionListProvider} from './components/DefinitionListContext'; import {DefinitionListItem} from './components/DefinitionListItem'; +import {b} from './constants'; import type {DefinitionListProps} from './types'; -import {b} from './utils'; import './DefinitionList.scss'; @@ -20,18 +21,18 @@ export function DefinitionList({ }: DefinitionListProps) { const normalizedChildren = prepareChildren(children); return ( -
- -
{normalizedChildren}
-
-
+ {normalizedChildren} +
+ ); } @@ -40,15 +41,16 @@ const isDefinitionListItem = isOfType(DefinitionListItem); function prepareChildren(children: React.ReactNode) { const items = React.Children.toArray(children); + const normalizedItems = []; for (const item of items) { const isItem = isDefinitionListItem(item); - if (!isItem) { - throw new Error( - 'Only components is allowed inside ', - ); + if (isItem) { + normalizedItems.push(item); + } else { + warnOnce('Only components is allowed inside '); } } - return children; + return normalizedItems; } DefinitionList.Item = DefinitionListItem; diff --git a/src/components/DefinitionList/README.md b/src/components/DefinitionList/README.md index b95b0fa9d1..9c65b67f4f 100644 --- a/src/components/DefinitionList/README.md +++ b/src/components/DefinitionList/README.md @@ -21,10 +21,10 @@ The component to display definition list with term and definition separated by d `} > - + value with copy - - + + ; diff --git a/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx index a281d1b2be..a13fc3575b 100644 --- a/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx +++ b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import type {Meta, StoryFn} from '@storybook/react'; +import type {Meta, StoryObj} from '@storybook/react'; import {Label} from '../../Label'; import {Link} from '../../Link'; import {User} from '../../User'; import {DefinitionList} from '../DefinitionList'; -import type {DefinitionListItem as DefinitionListItemProps, DefinitionListProps} from '../types'; +import type {DefinitionListItemProps} from '../types'; const items: DefinitionListItemProps[] = [ { @@ -166,20 +166,10 @@ export default { }, } as Meta; -const DefaultTemplate: StoryFn = (args) => ; -export const Default = DefaultTemplate.bind({}); -Default.args = {contentMaxWidth: 480}; +type Story = StoryObj; -const TemplateResponsive: StoryFn = (args) => ; -export const ResponsiveList = TemplateResponsive.bind({}); -ResponsiveList.args = { - responsive: true, -}; +export const Default: Story = {args: {contentMaxWidth: 480}}; -const TemplateVertical: StoryFn = (args) => { - return ; -}; -export const VerticalList = TemplateVertical.bind({}); -VerticalList.args = { - direction: 'vertical', -}; +export const ResponsiveList: Story = {args: {responsive: true}}; + +export const VerticalList: Story = {args: {direction: 'vertical'}}; diff --git a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx index a8673eff35..c72e606eb5 100644 --- a/src/components/DefinitionList/__tests__/DefinitionList.test.tsx +++ b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx @@ -2,18 +2,20 @@ import React from 'react'; import {render, screen} from '../../../../test-utils/utils'; import {DefinitionList} from '../DefinitionList'; -import type {DefinitionListItem, DefinitionListProps} from '../types'; -import {b} from '../utils'; +import {b} from '../constants'; +import type {DefinitionListItemProps, DefinitionListProps} from '../types'; const qaAttribute = 'definition-list'; -const defaultItems: DefinitionListItem[] = [ +const defaultItems: DefinitionListItemProps[] = [ {name: 'test1', children: 'value1'}, {name: 'test2', children: 2}, {name: 'test3', children:
node value
}, ]; -const getComponent = (props?: Partial & {items?: DefinitionListItem[]}) => { +const getComponent = ( + props?: Partial & {items?: DefinitionListItemProps[]}, +) => { const {items = defaultItems} = props ?? {}; return render( @@ -24,7 +26,7 @@ const getComponent = (props?: Partial & {items?: Definition ).container; }; -describe('components: DefinitionList', () => { +describe('DefinitionList', () => { it('should render', () => { getComponent(); const component = screen.getByTestId(qaAttribute); diff --git a/src/components/DefinitionList/components/Definition.tsx b/src/components/DefinitionList/components/Definition.tsx index 31c73846dc..ad86fd216a 100644 --- a/src/components/DefinitionList/components/Definition.tsx +++ b/src/components/DefinitionList/components/Definition.tsx @@ -1,10 +1,10 @@ import React from 'react'; import {ClipboardButton} from '../../ClipboardButton'; -import type {DefinitionListItem} from '../types'; -import {b} from '../utils'; +import {b} from '../constants'; +import type {DefinitionListItemProps} from '../types'; -interface DefinitionProps extends Pick {} +interface DefinitionProps extends Pick {} export function Definition({copyText, children}: DefinitionProps) { const definitionContent = children ?? '—'; diff --git a/src/components/DefinitionList/components/DefinitionListItem.tsx b/src/components/DefinitionList/components/DefinitionListItem.tsx index 34c1795d80..238ceb33db 100644 --- a/src/components/DefinitionList/components/DefinitionListItem.tsx +++ b/src/components/DefinitionList/components/DefinitionListItem.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import type {DefinitionListItem as DefinitionListItemProps} from '../types'; -import {b, getTitle, isUnbreakableOver} from '../utils'; +import {b} from '../constants'; +import type {DefinitionListItemProps} from '../types'; +import {getTitle, isUnbreakableOver} from '../utils'; import {Definition} from './Definition'; import {useDefinitionListAttributes} from './DefinitionListContext'; @@ -31,4 +32,4 @@ export function DefinitionListItem({name, children, copyText, note}: DefinitionL ); } -DefinitionListItem.displayName = 'DefinitionListItemin'; +DefinitionListItem.displayName = 'DefinitionListItem'; diff --git a/src/components/DefinitionList/components/Term.tsx b/src/components/DefinitionList/components/Term.tsx index f75f589b07..7a68fa1847 100644 --- a/src/components/DefinitionList/components/Term.tsx +++ b/src/components/DefinitionList/components/Term.tsx @@ -1,9 +1,14 @@ import React from 'react'; import {HelpMark} from '../../HelpMark'; +import {b} from '../constants'; import i18n from '../i18n'; -import type {DefinitionListDirection, DefinitionListItem, DefinitionListItemNote} from '../types'; -import {b, getTitle} from '../utils'; +import type { + DefinitionListDirection, + DefinitionListItemNote, + DefinitionListItemProps, +} from '../types'; +import {getTitle} from '../utils'; interface NoteElementsProps { note?: DefinitionListItemNote; @@ -46,7 +51,7 @@ function NoteElement({note}: NoteElementsProps) { return null; } -interface TermProps extends Pick { +interface TermProps extends Pick { direction?: DefinitionListDirection; } diff --git a/src/components/DefinitionList/constants.ts b/src/components/DefinitionList/constants.ts new file mode 100644 index 0000000000..cb71c486a3 --- /dev/null +++ b/src/components/DefinitionList/constants.ts @@ -0,0 +1,3 @@ +import {block} from '../utils/cn'; + +export const b = block('definition-list'); diff --git a/src/components/DefinitionList/index.ts b/src/components/DefinitionList/index.ts index cfe7f5f06f..8a35d5a7e3 100644 --- a/src/components/DefinitionList/index.ts +++ b/src/components/DefinitionList/index.ts @@ -1,2 +1,2 @@ export {DefinitionList} from './DefinitionList'; -export type {DefinitionListProps, DefinitionListItem} from './types'; +export type {DefinitionListProps, DefinitionListItemProps} from './types'; diff --git a/src/components/DefinitionList/types.ts b/src/components/DefinitionList/types.ts index 0522c49c55..4cda2b3267 100644 --- a/src/components/DefinitionList/types.ts +++ b/src/components/DefinitionList/types.ts @@ -4,7 +4,7 @@ import type {HelpMarkProps} from '../HelpMark'; import type {QAProps} from '../types'; export type DefinitionListItemNote = string | HelpMarkProps; -export interface DefinitionListItem { +export interface DefinitionListItemProps { name: React.ReactNode; children?: React.ReactNode; copyText?: string; diff --git a/src/components/DefinitionList/utils.ts b/src/components/DefinitionList/utils.ts index 4a88e43866..e5999334db 100644 --- a/src/components/DefinitionList/utils.ts +++ b/src/components/DefinitionList/utils.ts @@ -1,9 +1,5 @@ import type React from 'react'; -import {block} from '../utils/cn'; - -export const b = block('definition-list'); - export function isUnbreakableOver(limit: number) { return function (value: string): boolean { const posibleLines = value.split(/\s+/); diff --git a/src/components/HelpMark/HelpMark.tsx b/src/components/HelpMark/HelpMark.tsx index b7528d017b..2dd4017167 100644 --- a/src/components/HelpMark/HelpMark.tsx +++ b/src/components/HelpMark/HelpMark.tsx @@ -16,7 +16,6 @@ const ICON_SIZE = 16; export interface HelpMarkProps extends QAProps { buttonProps?: React.ButtonHTMLAttributes; buttonRef?: React.RefObject; - delayClosing?: number; placement?: PopupPlacement; className?: string; children?: React.ReactNode; @@ -27,24 +26,20 @@ export function HelpMark({ buttonProps = {}, children, className, - delayClosing = 300, ...rest }: HelpMarkProps) { return ( - - + + {() => ( + + )} ); } diff --git a/src/components/HelpMark/README.md b/src/components/HelpMark/README.md index 93e8554ef9..3d5b3ab5ac 100644 --- a/src/components/HelpMark/README.md +++ b/src/components/HelpMark/README.md @@ -12,14 +12,13 @@ Component to display help icon with popover ## Examples -Component with rendered raw html (use `content` for plain text) and close on mouse leave after timeout set by `delayClosing`: +Component with rendered raw html and close on mouse leave: ```tsx console.log('just action happened'), @@ -70,35 +67,12 @@ LANDING_BLOCK--> -Popover with JSX component as content: - - - - - -```tsx -} /> -``` - - - ## Properties -| Property | Type | Required | Default | Description | -| :----------- | :---------------------------------------------- | :------- | :------------------ | :---------------------------------------------- | -| className | `String` | | | Control class name | -| placement | `Array` | | [`right`, `bottom`] | Allowed popover positions | -| delayClosing | `Number` | | `300` | Timeout before closing popover | -| children | `ReactNode` | | | Popover content | -| buttonProps | `React.ButtonHTMLAttributes` | | | Set attributes to the underlying button element | -| buttonRef | `React.RefObject` | | | Ref to the underlying button element | +| Property | Type | Required | Default | Description | +| :---------- | :---------------------------------------------- | :------- | :------------------ | :---------------------------------------------- | +| className | `String` | | | Control class name | +| placement | `Array` | | [`right`, `bottom`] | Allowed popover positions | +| children | `ReactNode` | | | Popover content | +| buttonProps | `React.ButtonHTMLAttributes` | | | Set attributes to the underlying button element | +| buttonRef | `React.RefObject` | | | Ref to the underlying button element | diff --git a/src/components/HelpMark/__stories__/HelpMark.stories.tsx b/src/components/HelpMark/__stories__/HelpMark.stories.tsx index 72a54ee98c..5f070d1511 100644 --- a/src/components/HelpMark/__stories__/HelpMark.stories.tsx +++ b/src/components/HelpMark/__stories__/HelpMark.stories.tsx @@ -1,9 +1,6 @@ -import React from 'react'; - -import type {Meta, StoryFn} from '@storybook/react'; +import type {Meta, StoryObj} from '@storybook/react'; import {HelpMark} from '../HelpMark'; -import type {HelpMarkProps} from '../HelpMark'; export default { title: 'Components/Utils/HelpMark', @@ -20,7 +17,7 @@ export default { config: { rules: [ { - id: 'button-name', + id: 'help-mark', enabled: false, // aria-labelledby id is valid after tooltip content is rendered selector: 'button[aria-labelledby="helpMarkWithoutActionsId"]', @@ -29,10 +26,8 @@ export default { }, }, }, -} as Meta; +} as Meta; + +type Story = StoryObj; -const DefaultTemplate: StoryFn = (args) => ; -export const Default = DefaultTemplate.bind({}); -Default.args = { - children: 'Some content', -}; +export const Default: Story = {args: {children: 'Some content'}}; From b27fb4647e4fdb3b2a43102ec4759854710daefe Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Tue, 15 Oct 2024 14:00:57 +0300 Subject: [PATCH 7/7] fix: code-review --- .../DefinitionList/DefinitionList.tsx | 4 ++- src/components/DefinitionList/README.md | 28 +++++++++---------- src/components/DefinitionList/index.ts | 1 + src/components/HelpMark/README.md | 14 +++++----- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/DefinitionList/DefinitionList.tsx b/src/components/DefinitionList/DefinitionList.tsx index 531337ec34..7eeaecce78 100644 --- a/src/components/DefinitionList/DefinitionList.tsx +++ b/src/components/DefinitionList/DefinitionList.tsx @@ -47,7 +47,9 @@ function prepareChildren(children: React.ReactNode) { if (isItem) { normalizedItems.push(item); } else { - warnOnce('Only components is allowed inside '); + warnOnce( + '[DefinitionList] Only components is allowed as children', + ); } } return normalizedItems; diff --git a/src/components/DefinitionList/README.md b/src/components/DefinitionList/README.md index 9c65b67f4f..5e4ed7621b 100644 --- a/src/components/DefinitionList/README.md +++ b/src/components/DefinitionList/README.md @@ -45,25 +45,25 @@ LANDING_BLOCK--> ## Properties -| Property | Type | Required | Default | Description | -| :----------------- | :----------------------------- | :------: | :----------- | :-------------------------------------------------------------------------------------------------- | -| [children](#items) | `React.ReactNode` | yes | | Items of the list | -| responsive | `boolean` | | | If set to `true` list will take 100% width of its parent | -| direction | `'horizontal'` \| `'vertical'` | | 'horizontal' | If set to `vertical` content will be located under name and list will take 100% width of its parent | -| nameMaxWidth | `number` | | | Maximum width of term | -| contentMaxWidth | `number` | | | Maximum width of definition | -| className | `string` | | | Class name for the definition list | +| Name | Description | Type | Default | +| :----------------- | :-------------------------------------------------------------------------------------------------- | :----------------------------: | :----------: | +| [children](#items) | Items of the list | `React.ReactNode` | | +| responsive | If set to `true` list will take 100% width of its parent | `boolean` | | +| direction | If set to `vertical` content will be located under name and list will take 100% width of its parent | `'horizontal'` \| `'vertical'` | 'horizontal' | +| nameMaxWidth | Maximum width of term | `number` | | +| contentMaxWidth | Maximum width of definition | `number` | | +| className | Class name for the definition list | `string` | | ### Items DefinitionList children should be components of type `DefinitionList.Item` with following properties: -| Property | Type | Required | Default | Description | -| -------- | ------------------------- | -------- | ------- | ------------------------------------------------ | -| name | `ReactNode` | true | | Term | -| children | `ReactNode` | | | Definition | -| copyText | `string` | | | If set, it will be shown icon for copy this text | -| note | `string \| HelpMarkProps` | | | If set, HelpMark will be shown next to term | +| Name | Description | Type | Default | +| -------- | ------------------------------------------------ | :-----------------------: | :-----: | +| name | Term | `ReactNode` | | +| children | Definition | `ReactNode` | | +| copyText | If set, it will be shown icon for copy this text | `string` | | +| note | If set, HelpMark will be shown next to term | `string \| HelpMarkProps` | | ## CSS API diff --git a/src/components/DefinitionList/index.ts b/src/components/DefinitionList/index.ts index 8a35d5a7e3..76cd806ea1 100644 --- a/src/components/DefinitionList/index.ts +++ b/src/components/DefinitionList/index.ts @@ -1,2 +1,3 @@ export {DefinitionList} from './DefinitionList'; +export {DefinitionListItem} from './components/DefinitionListItem'; export type {DefinitionListProps, DefinitionListItemProps} from './types'; diff --git a/src/components/HelpMark/README.md b/src/components/HelpMark/README.md index 3d5b3ab5ac..13e2d3a447 100644 --- a/src/components/HelpMark/README.md +++ b/src/components/HelpMark/README.md @@ -69,10 +69,10 @@ LANDING_BLOCK--> ## Properties -| Property | Type | Required | Default | Description | -| :---------- | :---------------------------------------------- | :------- | :------------------ | :---------------------------------------------- | -| className | `String` | | | Control class name | -| placement | `Array` | | [`right`, `bottom`] | Allowed popover positions | -| children | `ReactNode` | | | Popover content | -| buttonProps | `React.ButtonHTMLAttributes` | | | Set attributes to the underlying button element | -| buttonRef | `React.RefObject` | | | Ref to the underlying button element | +| Name | Description | Type | Default | +| :---------- | :---------------------------------------------- | :---------------------------------------------: | :-----------------: | +| className | Control class name | `String` | | +| placement | Allowed popover positions | `Array` | [`right`, `bottom`] | +| children | Popover content | `ReactNode` | | +| buttonProps | Set attributes to the underlying button element | `React.ButtonHTMLAttributes` | | +| buttonRef | Ref to the underlying button element | `React.RefObject` | |