From 84f31df469ea8fad8d37b5e3cfac666cf9ebe1f6 Mon Sep 17 00:00:00 2001 From: "Mr.Dr.Professor Patrick" Date: Thu, 10 Aug 2023 11:02:39 +0200 Subject: [PATCH] feat(Persona): extend text property (#876) --- src/components/Persona/Persona.tsx | 51 +++++++--------- .../Persona/__tests__/Persona.test.tsx | 60 +++++++++++++++++++ src/components/Persona/getTwoLetters.ts | 6 -- src/components/Persona/i18n/en.json | 3 + src/components/Persona/i18n/index.ts | 7 +++ src/components/Persona/i18n/ru.json | 3 + src/components/Persona/index.ts | 3 +- src/components/Persona/types.ts | 26 ++++++++ src/components/Persona/utils.ts | 24 ++++++++ src/components/PersonaWrap/PersonaWrap.tsx | 4 +- 10 files changed, 149 insertions(+), 38 deletions(-) create mode 100644 src/components/Persona/__tests__/Persona.test.tsx delete mode 100644 src/components/Persona/getTwoLetters.ts create mode 100644 src/components/Persona/i18n/en.json create mode 100644 src/components/Persona/i18n/index.ts create mode 100644 src/components/Persona/i18n/ru.json create mode 100644 src/components/Persona/types.ts create mode 100644 src/components/Persona/utils.ts diff --git a/src/components/Persona/Persona.tsx b/src/components/Persona/Persona.tsx index 405a6f50ed..2fdcaa5900 100644 --- a/src/components/Persona/Persona.tsx +++ b/src/components/Persona/Persona.tsx @@ -5,32 +5,9 @@ import {Envelope} from '@gravity-ui/icons'; import {Icon} from '../Icon'; import {PersonaWrap} from '../PersonaWrap'; -import {getTwoLetters} from './getTwoLetters'; - -export interface PersonaProps { - /** Visible text */ - text: string; - /** Image source */ - image?: string; - /** - * Visual appearance (with or without border) - * @deprecated Use `hasBorder` prop instead - */ - theme?: 'default' | 'clear'; - /** Display border */ - hasBorder?: boolean; - /** Avatar appearance */ - type?: 'person' | 'email' | 'empty'; - /** Text size */ - size?: 's' | 'n'; - /** Handle click on button with cross */ - onClose?: (text: string) => void; - /** Handle click on component itself */ - onClick?: (text: string) => void; - /** Custom CSS class for root element */ - className?: string; - style?: React.CSSProperties; -} +import i18n from './i18n'; +import type {PersonaProps} from './types'; +import {extractTextValue, extractTextView, getTwoLetters} from './utils'; export function Persona({ size = 's', @@ -44,11 +21,16 @@ export function Persona({ className, style, }: PersonaProps) { + const textValue = extractTextValue(text); + const textView = extractTextView(text); + const closeButtonAriaAttributes: React.AriaAttributes = { + 'aria-label': i18n('label_remove-button', {persona: textValue}), + }; let avatar: React.ReactNode | null; switch (type) { case 'person': - avatar = image ? {''} : {getTwoLetters(text)}; + avatar = image ? {''} : {getTwoLetters(textValue)}; break; case 'email': avatar = ; @@ -58,18 +40,27 @@ export function Persona({ break; } + const handleClick = React.useCallback(() => { + onClick?.(textValue); + }, [textValue, onClick]); + + const handleClose = React.useCallback(() => { + onClose?.(textValue); + }, [textValue, onClose]); + return ( - {text} + {textView} ); } diff --git a/src/components/Persona/__tests__/Persona.test.tsx b/src/components/Persona/__tests__/Persona.test.tsx new file mode 100644 index 0000000000..5f81b10a96 --- /dev/null +++ b/src/components/Persona/__tests__/Persona.test.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import {queryByAttribute, render, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import {Persona} from '../Persona'; +import i18n from '../i18n'; +import type {PersonaProps} from '../types'; +import {extractTextValue, getTwoLetters} from '../utils'; + +const MOCKED_TEXT = 'text'; +const MOCKED_TEXT_NODE_CONTENT_VALUE = 'Some view'; +const MOCKED_TEXT_NODE: PersonaProps['text'] = { + value: MOCKED_TEXT, + content:
{MOCKED_TEXT_NODE_CONTENT_VALUE}
, +}; + +describe('Persona', () => { + describe('text property', () => { + test.each([MOCKED_TEXT, MOCKED_TEXT_NODE])( + 'should return text value as onClick argument', + async (text) => { + const onClick = jest.fn(); + render(); + const user = userEvent.setup(); + const textValue = extractTextValue(text); + const twoLetters = getTwoLetters(textValue); + const personaNode = screen.getByText(twoLetters); + await user.click(personaNode); + expect(onClick).toBeCalledWith(textValue); + }, + ); + test.each([MOCKED_TEXT, MOCKED_TEXT_NODE])( + 'should return text value as onClose argument', + async (text) => { + const onClose = jest.fn(); + const {container} = render(); + const user = userEvent.setup(); + const textValue = extractTextValue(text); + const ariaLabelValue = i18n('label_remove-button', {persona: textValue}); + const closeButtonNode = queryByAttribute('aria-label', container, ariaLabelValue); + + if (!closeButtonNode) { + throw new Error('There is no close button in dom'); + } + + await user.click(closeButtonNode); + expect(onClose).toBeCalledWith(textValue); + }, + ); + test('should render text as string', () => { + render(); + screen.getByText(MOCKED_TEXT); + }); + test('should render text as react node', () => { + render(); + screen.getByText(MOCKED_TEXT_NODE_CONTENT_VALUE); + }); + }); +}); diff --git a/src/components/Persona/getTwoLetters.ts b/src/components/Persona/getTwoLetters.ts deleted file mode 100644 index 5c93fafb99..0000000000 --- a/src/components/Persona/getTwoLetters.ts +++ /dev/null @@ -1,6 +0,0 @@ -import _ from 'lodash'; - -export function getTwoLetters(text: string) { - const words = text.split(' '); - return [_.get(words, '[0][0]'), _.get(words, '[1][0]')].filter(Boolean).join(''); -} diff --git a/src/components/Persona/i18n/en.json b/src/components/Persona/i18n/en.json new file mode 100644 index 0000000000..cd6d43cd47 --- /dev/null +++ b/src/components/Persona/i18n/en.json @@ -0,0 +1,3 @@ +{ + "label_remove-button": "Remove {{persona}}" +} diff --git a/src/components/Persona/i18n/index.ts b/src/components/Persona/i18n/index.ts new file mode 100644 index 0000000000..0a7c69da10 --- /dev/null +++ b/src/components/Persona/i18n/index.ts @@ -0,0 +1,7 @@ +import {addComponentKeysets} from '../../utils/addComponentKeysets'; +import {NAMESPACE_NEW} from '../../utils/cn'; + +import en from './en.json'; +import ru from './ru.json'; + +export default addComponentKeysets({en, ru}, `${NAMESPACE_NEW}persona-remove-button`); diff --git a/src/components/Persona/i18n/ru.json b/src/components/Persona/i18n/ru.json new file mode 100644 index 0000000000..120e2bed9b --- /dev/null +++ b/src/components/Persona/i18n/ru.json @@ -0,0 +1,3 @@ +{ + "label_remove-button": "Удалить {{persona}}" +} diff --git a/src/components/Persona/index.ts b/src/components/Persona/index.ts index 16a07b523c..ebddc01408 100644 --- a/src/components/Persona/index.ts +++ b/src/components/Persona/index.ts @@ -1 +1,2 @@ -export * from './Persona'; +export {Persona} from './Persona'; +export type {PersonaProps} from './types'; diff --git a/src/components/Persona/types.ts b/src/components/Persona/types.ts new file mode 100644 index 0000000000..64364489b9 --- /dev/null +++ b/src/components/Persona/types.ts @@ -0,0 +1,26 @@ +export type PersonaText = string | {value: string; content: React.ReactNode}; + +export type PersonaProps = { + /** Visible text node */ + text: PersonaText; + /** Image source */ + image?: string; + /** + * Visual appearance (with or without border) + * @deprecated Use `hasBorder` prop instead + */ + theme?: 'default' | 'clear'; + /** Display border */ + hasBorder?: boolean; + /** Avatar appearance */ + type?: 'person' | 'email' | 'empty'; + /** Text size */ + size?: 's' | 'n'; + /** Handle click on button with cross */ + onClose?: (text: string) => void; + /** Handle click on component itself */ + onClick?: (text: string) => void; + /** Custom CSS class for root element */ + className?: string; + style?: React.CSSProperties; +}; diff --git a/src/components/Persona/utils.ts b/src/components/Persona/utils.ts new file mode 100644 index 0000000000..0fed4758e6 --- /dev/null +++ b/src/components/Persona/utils.ts @@ -0,0 +1,24 @@ +import _ from 'lodash'; + +import type {PersonaText} from './types'; + +export const extractTextValue = (text: PersonaText = '') => { + if (text && typeof text === 'object') { + return text.value; + } + + return text; +}; + +export const extractTextView = (text: PersonaText = '') => { + if (text && typeof text === 'object') { + return text.content; + } + + return text; +}; + +export function getTwoLetters(text: string) { + const words = text.split(' '); + return [_.get(words, '[0][0]'), _.get(words, '[1][0]')].filter(Boolean).join(''); +} diff --git a/src/components/PersonaWrap/PersonaWrap.tsx b/src/components/PersonaWrap/PersonaWrap.tsx index e0ca2b2999..2ef513156d 100644 --- a/src/components/PersonaWrap/PersonaWrap.tsx +++ b/src/components/PersonaWrap/PersonaWrap.tsx @@ -19,6 +19,7 @@ export interface PersonaWrapProps { onClick?: (event: React.MouseEvent) => void; className?: string; style?: React.CSSProperties; + closeButtonAriaAttributes?: React.AriaAttributes; } export function PersonaWrap({ @@ -31,6 +32,7 @@ export function PersonaWrap({ avatar, children, style, + closeButtonAriaAttributes, }: PersonaWrapProps) { const clickable = Boolean(onClick); const closeable = Boolean(onClose); @@ -46,7 +48,7 @@ export function PersonaWrap({ {onClose && ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
+
)}