diff --git a/packages/components/src/CopyTypography/CopyTypography.stories.tsx b/packages/components/src/CopyTypography/CopyTypography.stories.tsx new file mode 100644 index 000000000..23f6666b4 --- /dev/null +++ b/packages/components/src/CopyTypography/CopyTypography.stories.tsx @@ -0,0 +1,94 @@ +import { type Meta, type StoryObj } from '@storybook/react'; + +import { Typography } from '../Typography'; +import { styled } from '../styles'; +import { OverflowTypography } from '../OverflowTypography'; + +import { CopyTypography } from './CopyTypography'; + +/** + * ### [Figma]() + * ### [Guide]() + * Компонент позволяет скопировать содержимое в буфер обмена + */ + +const meta: Meta = { + title: 'Components/Data Display/CopyTypography', + component: CopyTypography, +}; + +export default meta; + +type Story = StoryObj; + +export const Interaction: Story = { + args: { + children: Швецова М. Д., + copyText: 'Швецова М. Д.', + }, + parameters: { + docs: { + disable: true, + }, + }, +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(4)}; +`; + +const OverflowWrapper = styled.div` + width: 150px; +`; + +export const Example = () => { + return Швецова М. Д.; +}; + +/** + * prop `copyPosition` определяет расположение иконки(справа/слева от текста). По умолчанию справа + */ +export const CopyPosition = () => { + return ( + + Швецова М. Д. + Швецова М. Д. + + ); +}; + +/** + * prop `copyText` указывает какой текст необходимо скопировать в буфер обмена. + * Необходим для копирования текста вложенных компонентов или когда копируемое содержимое + * должно отличаться от представления. + */ +export const CopyText = () => { + return ( + + + Швецова М. Д. + + + Швецова М. Д. + + + ); +}; + +/** + * prop `isShowCopyText` показывает в тултипе текст, который будет скопирован. + * Необходимо отключать тултип у вложенных компонентов, при их наличии, для избежания их наложения + */ +export const IsShowCopyText = () => { + return ( + + + + Швецова Мария Дмитриевна + + + + ); +}; diff --git a/packages/components/src/CopyTypography/CopyTypography.test.tsx b/packages/components/src/CopyTypography/CopyTypography.test.tsx new file mode 100644 index 000000000..12fb041fa --- /dev/null +++ b/packages/components/src/CopyTypography/CopyTypography.test.tsx @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from 'vitest'; +import { renderWithTheme, screen, userEvents } from '@astral/tests'; + +import { CopyTypography } from './CopyTypography'; + +describe('CopyTypography', () => { + it('Значение копируется в буфер обмена, при клике на компонент', async () => { + const copyText = 'it was copied'; + + const writeTextSpy = vi.fn(() => Promise.resolve()); + + Object.assign(navigator, { clipboard: { writeText: writeTextSpy } }); + renderWithTheme({copyText}); + + const element = screen.getByText(copyText); + + await userEvents.click(element); + expect(writeTextSpy).toBeCalled(); + }); + + it('Значение копируется в буфер обмена, если children содержит ReactNode и заданном copyText', async () => { + const copyText = 'it was copied'; + + const writeTextSpy = vi.fn(() => Promise.resolve()); + + Object.assign(navigator, { clipboard: { writeText: writeTextSpy } }); + + renderWithTheme( + +
{copyText}
+
, + ); + + const element = screen.getByText(copyText); + + await userEvents.click(element); + expect(writeTextSpy).toBeCalled(); + }); +}); diff --git a/packages/components/src/CopyTypography/CopyTypography.tsx b/packages/components/src/CopyTypography/CopyTypography.tsx new file mode 100644 index 000000000..b02da3ac0 --- /dev/null +++ b/packages/components/src/CopyTypography/CopyTypography.tsx @@ -0,0 +1,55 @@ +import { type TypographyProps } from '../Typography'; +import { Tooltip } from '../Tooltip'; + +import { StyledCopyIcon, Wrapper } from './styles'; +import { useLogic } from './useLogic'; + +export type CopyTypographyProps = TypographyProps & { + /** + * Текст, который будет скопирован. Перекрывает обычное копирование если children является строкой + */ + copyText?: string; + /** + * Отображает иконку слева или справа от текста + * @default right + */ + copyPosition?: 'right' | 'left'; + /** + * Если `true`, в тултипе будет отображаться текст, который будет скопирован при нажатии + */ + isShowCopyText?: boolean; +}; + +export const CopyTypography = (props: CopyTypographyProps) => { + const { + children, + copyPosition = 'right', + copyText, + isShowCopyText, + color, + ...restProps + } = props; + + const renderIcon = () => ( + + ); + + const { handleMouseLeave, handleClick, tooltipTitle, isIconOnLeft } = + useLogic(props); + + return ( + + + {isIconOnLeft && renderIcon()} + {children} + {!isIconOnLeft && renderIcon()} + + + ); +}; diff --git a/packages/components/src/CopyTypography/enums.ts b/packages/components/src/CopyTypography/enums.ts new file mode 100644 index 000000000..78f33f15b --- /dev/null +++ b/packages/components/src/CopyTypography/enums.ts @@ -0,0 +1,5 @@ +export enum CopyStatus { + Copied = 'Скопировано', + Error = 'Ошибка копирования', + CanCopy = 'Скопировать', +} diff --git a/packages/components/src/CopyTypography/index.ts b/packages/components/src/CopyTypography/index.ts new file mode 100644 index 000000000..afc382601 --- /dev/null +++ b/packages/components/src/CopyTypography/index.ts @@ -0,0 +1 @@ +export * from './CopyTypography'; diff --git a/packages/components/src/CopyTypography/styles.ts b/packages/components/src/CopyTypography/styles.ts new file mode 100644 index 000000000..d45e1631b --- /dev/null +++ b/packages/components/src/CopyTypography/styles.ts @@ -0,0 +1,29 @@ +import { CopyOutlineSm } from '@astral/icons'; + +import { styled } from '../styles'; +import { Typography } from '../Typography'; + +export const Wrapper = styled(Typography)` + cursor: pointer; + + display: flex; + align-items: center; + + &:hover { + text-decoration: underline; + } +`; + +export const StyledCopyIcon = styled(CopyOutlineSm, { + shouldForwardProp: (prop) => !['$copyPosition'].includes(prop), +})<{ $copyPosition: 'left' | 'right' }>` + margin-right: ${({ $copyPosition, theme }) => + $copyPosition === 'left' ? theme.spacing(1) : ''}; + margin-left: ${({ $copyPosition, theme }) => + $copyPosition === 'right' ? theme.spacing(1) : ''}; + + /* Задаем размер иконки */ + font-size: 16px; + + fill: ${({ color }) => color}; +`; diff --git a/packages/components/src/CopyTypography/useLogic/index.ts b/packages/components/src/CopyTypography/useLogic/index.ts new file mode 100644 index 000000000..51786a09c --- /dev/null +++ b/packages/components/src/CopyTypography/useLogic/index.ts @@ -0,0 +1 @@ +export * from './useLogic'; diff --git a/packages/components/src/CopyTypography/useLogic/useLogic.ts b/packages/components/src/CopyTypography/useLogic/useLogic.ts new file mode 100644 index 000000000..735e8f723 --- /dev/null +++ b/packages/components/src/CopyTypography/useLogic/useLogic.ts @@ -0,0 +1,38 @@ +import { type SyntheticEvent, useState } from 'react'; + +import { type CopyTypographyProps } from '../CopyTypography'; +import { CopyStatus } from '../enums'; + +type UseLogicParams = CopyTypographyProps; + +export const useLogic = ({ + children, + copyText, + isShowCopyText, + copyPosition, +}: UseLogicParams) => { + const [status, setStatus] = useState(CopyStatus.CanCopy); + + const handleMouseLeave = () => { + if (status !== CopyStatus.CanCopy) { + setTimeout(() => { + setStatus(CopyStatus.CanCopy); + }, 100); + } + }; + + const handleClick = (event: SyntheticEvent) => { + event.stopPropagation(); + + navigator.clipboard + .writeText(copyText || (typeof children === 'string' ? children : '')) + .then(() => setStatus(CopyStatus.Copied)) + .catch(() => setStatus(CopyStatus.Error)); + }; + + const tooltipTitle = isShowCopyText ? `${status}: ${copyText}` : status; + + const isIconOnLeft = copyPosition === 'left'; + + return { handleMouseLeave, handleClick, tooltipTitle, isIconOnLeft }; +}; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 2882b9c25..b341fe1d3 100755 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -57,6 +57,8 @@ export * from './ConfirmDialog'; export * from './ContentState'; +export { CopyTypography, type CopyTypographyProps } from './CopyTypography'; + export * from './DashboardLayout'; export {