Skip to content

Commit

Permalink
feat(Persona): extend text property (#876)
Browse files Browse the repository at this point in the history
  • Loading branch information
korvin89 authored Aug 10, 2023
1 parent a329789 commit 84f31df
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 38 deletions.
51 changes: 21 additions & 30 deletions src/components/Persona/Persona.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 ? <img alt={''} src={image} /> : <span>{getTwoLetters(text)}</span>;
avatar = image ? <img alt={''} src={image} /> : <span>{getTwoLetters(textValue)}</span>;
break;
case 'email':
avatar = <Icon data={Envelope} size={14} />;
Expand All @@ -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 (
<PersonaWrap
size={size}
theme={hasBorder ? 'default' : 'clear'}
isEmpty={type === 'empty'}
onClick={onClick && onClick.bind(null, text)}
onClose={onClose && onClose.bind(null, text)}
onClick={onClick && handleClick}
onClose={onClose && handleClose}
avatar={avatar}
className={className}
style={style}
closeButtonAriaAttributes={closeButtonAriaAttributes}
>
{text}
{textView}
</PersonaWrap>
);
}
Expand Down
60 changes: 60 additions & 0 deletions src/components/Persona/__tests__/Persona.test.tsx
Original file line number Diff line number Diff line change
@@ -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: <div>{MOCKED_TEXT_NODE_CONTENT_VALUE}</div>,
};

describe('Persona', () => {
describe('text property', () => {
test.each<PersonaProps['text']>([MOCKED_TEXT, MOCKED_TEXT_NODE])(
'should return text value as onClick argument',
async (text) => {
const onClick = jest.fn();
render(<Persona text={text} onClick={onClick} />);
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<PersonaProps['text']>([MOCKED_TEXT, MOCKED_TEXT_NODE])(
'should return text value as onClose argument',
async (text) => {
const onClose = jest.fn();
const {container} = render(<Persona text={text} onClose={onClose} />);
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(<Persona text={MOCKED_TEXT} />);
screen.getByText(MOCKED_TEXT);
});
test('should render text as react node', () => {
render(<Persona text={MOCKED_TEXT_NODE} />);
screen.getByText(MOCKED_TEXT_NODE_CONTENT_VALUE);
});
});
});
6 changes: 0 additions & 6 deletions src/components/Persona/getTwoLetters.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/components/Persona/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label_remove-button": "Remove {{persona}}"
}
7 changes: 7 additions & 0 deletions src/components/Persona/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -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`);
3 changes: 3 additions & 0 deletions src/components/Persona/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label_remove-button": "Удалить {{persona}}"
}
3 changes: 2 additions & 1 deletion src/components/Persona/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Persona';
export {Persona} from './Persona';
export type {PersonaProps} from './types';
26 changes: 26 additions & 0 deletions src/components/Persona/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
24 changes: 24 additions & 0 deletions src/components/Persona/utils.ts
Original file line number Diff line number Diff line change
@@ -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('');
}
4 changes: 3 additions & 1 deletion src/components/PersonaWrap/PersonaWrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface PersonaWrapProps {
onClick?: (event: React.MouseEvent) => void;
className?: string;
style?: React.CSSProperties;
closeButtonAriaAttributes?: React.AriaAttributes;
}

export function PersonaWrap({
Expand All @@ -31,6 +32,7 @@ export function PersonaWrap({
avatar,
children,
style,
closeButtonAriaAttributes,
}: PersonaWrapProps) {
const clickable = Boolean(onClick);
const closeable = Boolean(onClose);
Expand All @@ -46,7 +48,7 @@ export function PersonaWrap({
</div>
{onClose && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={b('close')} onClick={onClose}>
<div className={b('close')} onClick={onClose} {...closeButtonAriaAttributes}>
<Icon data={Xmark} size={12} />
</div>
)}
Expand Down

0 comments on commit 84f31df

Please sign in to comment.