diff --git a/CODEOWNERS b/CODEOWNERS index 9724de97a6..dd3fceac5c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,10 +13,12 @@ /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 @@ -68,5 +70,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 new file mode 100644 index 0000000000..95736e0e26 --- /dev/null +++ b/src/components/DefinitionList/DefinitionList.scss @@ -0,0 +1,93 @@ +@use '../../../styles/mixins.scss'; +@use '../variables'; + +$block: '.#{variables.$ns}definition-list'; + +#{$block} { + --_--item-block-start: var(--g-spacing-4); + --_--term-width: 300px; + margin: 0; + + &__item { + display: flex; + align-items: baseline; + gap: var(--g-spacing-1); + + & + & { + 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); + align-items: baseline; + + overflow: hidden; + position: relative; + } + + &__term-wrapper { + color: var(--g-color-text-secondary); + } + + &__dots { + box-sizing: border-box; + flex: 1 0 auto; + min-width: 25px; + margin: 0 2px; + border-block-end: 1px dotted var(--g-color-line-generic-active); + } + + &__definition { + flex: 0 1 auto; + margin: 0; + } + + &_responsive { + #{$block}__term-container { + --_--term-width: auto; + 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; + 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-button { + position: absolute; + display: inline-block; + inset-inline-end: 0; + margin-inline-start: 10px; + opacity: 0; + &:focus-visible { + opacity: 1; + } + } +} diff --git a/src/components/DefinitionList/DefinitionList.tsx b/src/components/DefinitionList/DefinitionList.tsx new file mode 100644 index 0000000000..7eeaecce78 --- /dev/null +++ b/src/components/DefinitionList/DefinitionList.tsx @@ -0,0 +1,59 @@ +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 './DefinitionList.scss'; + +export function DefinitionList({ + responsive, + direction = 'horizontal', + nameMaxWidth, + contentMaxWidth, + className, + children, + qa, +}: DefinitionListProps) { + const normalizedChildren = prepareChildren(children); + return ( + +
+ {normalizedChildren} +
+
+ ); +} + +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) { + normalizedItems.push(item); + } else { + warnOnce( + '[DefinitionList] Only components is allowed as children', + ); + } + } + return normalizedItems; +} + +DefinitionList.Item = DefinitionListItem; +DefinitionList.displayName = 'DefinitionList'; diff --git a/src/components/DefinitionList/README.md b/src/components/DefinitionList/README.md new file mode 100644 index 0000000000..5e4ed7621b --- /dev/null +++ b/src/components/DefinitionList/README.md @@ -0,0 +1,72 @@ + + +# DefinitionList + + + +The component to display definition list with term and definition separated by dots. + +## Examples + + + + + +```tsx + + + value with copy + + + +``` + + + +## Properties + +| 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: + +| 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 + +| 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 new file mode 100644 index 0000000000..a13fc3575b --- /dev/null +++ b/src/components/DefinitionList/__stories__/DefinitionList.stories.tsx @@ -0,0 +1,175 @@ +import React from '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 {DefinitionListItemProps} from '../types'; + +const items: DefinitionListItemProps[] = [ + { + name: String value, + children: 'value', + note: 'link', + }, + { + name: ( + + ), + children: 'value', + }, + {name: 'Number value', children: 2, note: 'This is value'}, + {name: 'Node value', children: value}, + {name: 'Empty 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', children: 'value'}, + {name: 'Number value with custom title', children: 2}, + { + name: 'Node value with custom title', + children: value, + }, + {name: 'Empty value with custom title'}, + { + name: 'String long value', + 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', + 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)', + }, + { + name: 'Number long value', + // eslint-disable-next-line no-loss-of-precision + children: 12345678901234567890123456789012345678901234567890123456789012345678901234567890, + }, + { + name: 'Node long value', + children: ( + + 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', + 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', + note: 'This is multiline value', + 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', + children: 'value', + note: 'This is simple string value', + }, + { + name: 'String value with very very very looooooooooooooong key', + children: 'value', + }, + { + name: 'String value with very very very looooooooooooooong key and tooltip', + children: 'value', + note: 'This is simple string value', + }, + { + name: 'Avatar with tooltip', + children: ( + + ), + 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', + children: , + }, +]; +const definitionListItems = items.map(({children, ...rest}, index) => ( + + {children} + +)); + +export default { + title: 'Components/Data Display/DefinitionList', + component: DefinitionList, + args: { + responsive: false, + children: definitionListItems, + }, + 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; + +type Story = StoryObj; + +export const Default: Story = {args: {contentMaxWidth: 480}}; + +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 new file mode 100644 index 0000000000..c72e606eb5 --- /dev/null +++ b/src/components/DefinitionList/__tests__/DefinitionList.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import {render, screen} from '../../../../test-utils/utils'; +import {DefinitionList} from '../DefinitionList'; +import {b} from '../constants'; +import type {DefinitionListItemProps, DefinitionListProps} from '../types'; + +const qaAttribute = 'definition-list'; + +const defaultItems: DefinitionListItemProps[] = [ + {name: 'test1', children: 'value1'}, + {name: 'test2', children: 2}, + {name: 'test3', children:
node value
}, +]; + +const getComponent = ( + props?: Partial & {items?: DefinitionListItemProps[]}, +) => { + const {items = defaultItems} = props ?? {}; + return render( + + {items.map((item, index) => ( + + ))} + , + ).container; +}; + +describe('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 not render clipboard button by default', () => { + getComponent(); + const copyButton = screen.queryByRole('button'); + expect(copyButton).toBeNull(); + }); + it('should render clipboard button', () => { + const items = [{name: 'test1', children: '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', children: 'value1', copyText: 'value1'}]; + getComponent({items, responsive: true}); + + const component = screen.getByTestId(qaAttribute); + expect(component).toHaveClass(b({responsive: true})); + }); + 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..ad86fd216a --- /dev/null +++ b/src/components/DefinitionList/components/Definition.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import {ClipboardButton} from '../../ClipboardButton'; +import {b} from '../constants'; +import type {DefinitionListItemProps} from '../types'; + +interface DefinitionProps extends Pick {} + +export function Definition({copyText, children}: DefinitionProps) { + const definitionContent = children ?? '—'; + + return copyText ? ( +
+ {definitionContent} + +
+ ) : ( + definitionContent + ); +} 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..238ceb33db --- /dev/null +++ b/src/components/DefinitionList/components/DefinitionListItem.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import {b} from '../constants'; +import type {DefinitionListItemProps} from '../types'; +import {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 = 'DefinitionListItem'; diff --git a/src/components/DefinitionList/components/Term.tsx b/src/components/DefinitionList/components/Term.tsx new file mode 100644 index 0000000000..7a68fa1847 --- /dev/null +++ b/src/components/DefinitionList/components/Term.tsx @@ -0,0 +1,74 @@ +import React from 'react'; + +import {HelpMark} from '../../HelpMark'; +import {b} from '../constants'; +import i18n from '../i18n'; +import type { + DefinitionListDirection, + DefinitionListItemNote, + DefinitionListItemProps, +} from '../types'; +import {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 ( + + {note} + + ); + } + + if (typeof note === 'object') { + const {buttonProps, ...rest} = note; + + return ( + + ); + } + return null; +} + +interface TermProps extends Pick { + direction?: DefinitionListDirection; +} + +export function Term({note, name, direction}: TermProps) { + const noteElement = note ? ( + +   + + + ) : null; + return ( + +
+ {name} + {noteElement} +
+ {direction === 'horizontal' &&
} + + ); +} 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/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..e274729cd5 --- /dev/null +++ b/src/components/DefinitionList/i18n/index.ts @@ -0,0 +1,6 @@ +import {addComponentKeysets} from '../../../i18n'; + +import en from './en.json'; +import ru from './ru.json'; + +export default addComponentKeysets({en, ru}, 'DefinitionList'); 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..76cd806ea1 --- /dev/null +++ b/src/components/DefinitionList/index.ts @@ -0,0 +1,3 @@ +export {DefinitionList} from './DefinitionList'; +export {DefinitionListItem} from './components/DefinitionListItem'; +export type {DefinitionListProps, DefinitionListItemProps} from './types'; diff --git a/src/components/DefinitionList/types.ts b/src/components/DefinitionList/types.ts new file mode 100644 index 0000000000..4cda2b3267 --- /dev/null +++ b/src/components/DefinitionList/types.ts @@ -0,0 +1,24 @@ +import type React from 'react'; + +import type {HelpMarkProps} from '../HelpMark'; +import type {QAProps} from '../types'; +export type DefinitionListItemNote = string | HelpMarkProps; + +export interface DefinitionListItemProps { + name: React.ReactNode; + children?: React.ReactNode; + copyText?: string; + note?: DefinitionListItemNote; +} + +export type DefinitionListDirection = 'vertical' | 'horizontal'; + +export interface DefinitionListProps extends QAProps { + responsive?: boolean; + direction?: DefinitionListDirection; + nameMaxWidth?: number; + contentMaxWidth?: number; + className?: string; + groupLabelClassName?: string; + children: React.ReactNode; +} diff --git a/src/components/DefinitionList/utils.ts b/src/components/DefinitionList/utils.ts new file mode 100644 index 0000000000..e5999334db --- /dev/null +++ b/src/components/DefinitionList/utils.ts @@ -0,0 +1,17 @@ +import type React from 'react'; + +export function isUnbreakableOver(limit: number) { + return function (value: string): boolean { + const posibleLines = value.split(/\s+/); + + return posibleLines.some((line) => line.length > limit); + }; +} + +export function getTitle(content?: React.ReactNode) { + 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..2dd4017167 --- /dev/null +++ b/src/components/HelpMark/HelpMark.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import {CircleQuestion} from '@gravity-ui/icons'; + +import {Icon} from '../Icon'; +import {Popover} from '../Popover'; +import type {PopupPlacement} from '../Popup'; +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 QAProps { + buttonProps?: React.ButtonHTMLAttributes; + buttonRef?: React.RefObject; + placement?: PopupPlacement; + className?: string; + children?: React.ReactNode; +} + +export function HelpMark({ + buttonRef, + buttonProps = {}, + children, + className, + ...rest +}: HelpMarkProps) { + return ( + + {() => ( + + )} + + ); +} diff --git a/src/components/HelpMark/README.md b/src/components/HelpMark/README.md new file mode 100644 index 0000000000..13e2d3a447 --- /dev/null +++ b/src/components/HelpMark/README.md @@ -0,0 +1,78 @@ + + +# HelpMark + + + +```tsx +import {HelpMark} from '@gravity-ui/uikit'; +``` + +Component to display help icon with popover + +## Examples + +Component with rendered raw html and close on mouse leave: + + + + + +```tsx + console.log('just action happened'), + }} +> + Lorem ipsum dolor sit{' '} + + amet + + , at scelerisque suspendisse + +``` + + + +## Properties + +| 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` | | diff --git a/src/components/HelpMark/__stories__/HelpMark.stories.tsx b/src/components/HelpMark/__stories__/HelpMark.stories.tsx new file mode 100644 index 0000000000..5f070d1511 --- /dev/null +++ b/src/components/HelpMark/__stories__/HelpMark.stories.tsx @@ -0,0 +1,33 @@ +import type {Meta, StoryObj} from '@storybook/react'; + +import {HelpMark} from '../HelpMark'; + +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: 'help-mark', + enabled: false, + // aria-labelledby id is valid after tooltip content is rendered + selector: 'button[aria-labelledby="helpMarkWithoutActionsId"]', + }, + ], + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {args: {children: 'Some content'}}; diff --git a/src/components/HelpMark/__tests__/HelpMark.test.tsx b/src/components/HelpMark/__tests__/HelpMark.test.tsx new file mode 100644 index 0000000000..00e451fa3a --- /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({title}); + + 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 94391c3539..400a4dfdf1 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -14,6 +14,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';