Skip to content

Commit

Permalink
chore(chat-log): add ChatAttachment components
Browse files Browse the repository at this point in the history
chore(chat-log): adjust spacing

chore(chat-log): adjust color, icon color

chore(chat-log): use safelySpread for props
  • Loading branch information
gloriliale committed Jun 29, 2022
1 parent 26e0d23 commit 0aa5216
Show file tree
Hide file tree
Showing 13 changed files with 726 additions and 27 deletions.
11 changes: 11 additions & 0 deletions .changeset/twelve-seahorses-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@twilio-paste/chat-log': minor
'@twilio-paste/core': minor
---

[Chat log] Add several new components related to displaying attachments:

- ComposerAttachmentCard
- ChatAttachment
- ChatAttachmentLink
- ChatAttachmentDescription
4 changes: 4 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
"BreadcrumbItem": "@twilio-paste/core/breadcrumb",
"Button": "@twilio-paste/core/button",
"Card": "@twilio-paste/core/card",
"ChatAttachment": "@twilio-paste/core/chat-log",
"ChatAttachmentDescription": "@twilio-paste/core/chat-log",
"ChatAttachmentLink": "@twilio-paste/core/chat-log",
"ChatBubble": "@twilio-paste/core/chat-log",
"ChatMessage": "@twilio-paste/core/chat-log",
"ChatMessageMeta": "@twilio-paste/core/chat-log",
"ChatMessageMetaItem": "@twilio-paste/core/chat-log",
"ComposerAttachmentCard": "@twilio-paste/core/chat-log",
"Checkbox": "@twilio-paste/core/checkbox",
"CheckboxDisclaimer": "@twilio-paste/core/checkbox",
"CheckboxGroup": "@twilio-paste/core/checkbox",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import * as React from 'react';
import {screen, render} from '@testing-library/react';
// @ts-ignore typescript doesn't like js imports
import axe from '../../../../../.jest/axe-helper';
import {ChatAttachment, ChatAttachmentLink, ChatAttachmentDescription, ComposerAttachmentCard} from '../src';

import {CustomizationProvider} from '@twilio-paste/customization';
import type {PasteCustomCSS} from '@twilio-paste/customization';
import {Spinner} from '@twilio-paste/spinner';
import {DownloadIcon} from '@twilio-paste/icons/esm/DownloadIcon';
import {Theme} from '@twilio-paste/theme';

const customizedElements: {[key: string]: PasteCustomCSS} = {
COMPOSER_ATTACHMENT_CARD: {
padding: 'space100',
},
COMPOSER_ATTACHMENT_CARD_REMOVE_BUTTON: {
color: 'colorTextIconNeutral',
},
CHAT_ATTACHMENT: {
marginLeft: 'space50',
},
CHAT_ATTACHMENT_BODY: {
padding: 'space20',
},
CHAT_ATTACHMENT_LINK: {
fontSize: 'fontSize50',
},
CHAT_ATTACHMENT_DESCRIPTION: {
color: 'colorText',
},
};

const customizedMyElements: {[key: string]: PasteCustomCSS} = {
MY_COMPOSER_ATTACHMENT_CARD: {
padding: 'space100',
},
MY_COMPOSER_ATTACHMENT_CARD_REMOVE_BUTTON: {
color: 'colorTextIconNeutral',
},
MY_CHAT_ATTACHMENT: {
marginLeft: 'space50',
},
MY_CHAT_ATTACHMENT_BODY: {
padding: 'space20',
},
MY_CHAT_ATTACHMENT_LINK: {
fontSize: 'fontSize50',
},
MY_CHAT_ATTACHMENT_DESCRIPTION: {
color: 'colorText',
},
};

const CustomizationWrapper: React.FC<{elements?: {[key: string]: PasteCustomCSS}}> = ({children, elements}) => (
<CustomizationProvider baseTheme="default" theme={TestTheme} elements={elements || customizedElements}>
{children}
</CustomizationProvider>
);

const CustomizationMyWrapper: React.FC = ({children}) => (
<CustomizationWrapper elements={customizedMyElements}>{children}</CustomizationWrapper>
);

describe('ChatAttachment', () => {
it('should render an icon, anchor, and text', () => {
const {container} = render(
<ChatAttachment attachmentIcon={<DownloadIcon decorative />}>
<ChatAttachmentLink href="www.google.com">document</ChatAttachmentLink>
<ChatAttachmentDescription>description</ChatAttachmentDescription>
</ChatAttachment>
);
expect(container.querySelector('[data-paste-element="ICON"]')).toBeDefined();
expect(screen.getByRole('link')).toBeDefined();
expect(screen.getByText('description')).toBeDefined();
});
});

describe('ComposerAttachmentCard', () => {
it('should render a dismiss button if there is an onDismiss prop', () => {
render(
<Theme.Provider theme="default">
<ComposerAttachmentCard onDismiss={() => {}}>
<ChatAttachment attachmentIcon={<Spinner decorative={false} title="loading..." />}>
<ChatAttachmentLink href="www.google.com">Document-FINAL.doc</ChatAttachmentLink>
<ChatAttachmentDescription>123 MB</ChatAttachmentDescription>
</ChatAttachment>
</ComposerAttachmentCard>
</Theme.Provider>
);
expect(screen.getByRole('button')).toBeDefined();
});
it('should not render a dismiss button if there is no onDismiss prop', () => {
render(
<Theme.Provider theme="default">
<ComposerAttachmentCard>
<ChatAttachment attachmentIcon={<DownloadIcon decorative />}>
<ChatAttachmentLink href="www.google.com">Document-FINAL.doc</ChatAttachmentLink>
<ChatAttachmentDescription>123 MB</ChatAttachmentDescription>
</ChatAttachment>
</ComposerAttachmentCard>
</Theme.Provider>
);
expect(screen.queryByRole('button')).toBeNull();
});
});

describe('Customization', () => {
it('should set a default element data attribute', () => {
render(
<ComposerAttachmentCard data-testid="composer-attachment-card" onDismiss={() => {}}>
<ChatAttachment data-testid="chat-attachment" attachmentIcon={<DownloadIcon decorative />}>
<ChatAttachmentLink data-testid="chat-attachment-link" href="www.google.com">
Document-FINAL.doc
</ChatAttachmentLink>
<ChatAttachmentDescription data-testid="chat-attachment-description">123 MB</ChatAttachmentDescription>
</ChatAttachment>
</ComposerAttachmentCard>,
{wrapper: CustomizationWrapper}
);

const composerAttachmentCard = screen.getByTestId('composer-attachment-card');
const composerAttachmentCardRemoveButton = screen.getByRole('button');
const chatAttachment = screen.getByTestId('chat-attachment');
const chatAttachmentBody = chatAttachment.lastChild as HTMLElement;
const chatAttachmentLink = screen.getByTestId('chat-attachment-link');
const chatAttachmentDescription = screen.getByTestId('chat-attachment-description');

expect(composerAttachmentCard.getAttribute('data-paste-element')).toEqual('COMPOSER_ATTACHMENT_CARD');
expect(composerAttachmentCardRemoveButton.getAttribute('data-paste-element')).toEqual(
'COMPOSER_ATTACHMENT_CARD_REMOVE_BUTTON'
);
expect(chatAttachment.getAttribute('data-paste-element')).toEqual('CHAT_ATTACHMENT');
expect(chatAttachmentBody.getAttribute('data-paste-element')).toEqual('CHAT_ATTACHMENT_BODY');
expect(chatAttachmentLink.getAttribute('data-paste-element')).toEqual('CHAT_ATTACHMENT_LINK');
expect(chatAttachmentDescription.getAttribute('data-paste-element')).toEqual('CHAT_ATTACHMENT_DESCRIPTION');
});

it('should set a custom element data attribute', () => {
render(
<ComposerAttachmentCard
element="MY_COMPOSER_ATTACHMENT_CARD"
data-testid="composer-attachment-card"
onDismiss={() => {}}
>
<ChatAttachment
element="MY_CHAT_ATTACHMENT"
data-testid="chat-attachment"
attachmentIcon={<DownloadIcon decorative />}
>
<ChatAttachmentLink
element="MY_CHAT_ATTACHMENT_LINK"
data-testid="chat-attachment-link"
href="www.google.com"
>
Document-FINAL.doc
</ChatAttachmentLink>
<ChatAttachmentDescription element="MY_CHAT_ATTACHMENT_DESCRIPTION" data-testid="chat-attachment-description">
123 MB
</ChatAttachmentDescription>
</ChatAttachment>
</ComposerAttachmentCard>,
{wrapper: CustomizationWrapper}
);

const composerAttachmentCard = screen.getByTestId('composer-attachment-card');
const composerAttachmentCardRemoveButton = screen.getByRole('button');
const chatAttachment = screen.getByTestId('chat-attachment');
const chatAttachmentLink = screen.getByTestId('chat-attachment-link');
const chatAttachmentDescription = screen.getByTestId('chat-attachment-description');
const chatAttachmentBody = chatAttachment.lastChild as HTMLElement;

expect(composerAttachmentCard.getAttribute('data-paste-element')).toEqual('MY_COMPOSER_ATTACHMENT_CARD');
expect(composerAttachmentCardRemoveButton.getAttribute('data-paste-element')).toEqual(
'MY_COMPOSER_ATTACHMENT_CARD_REMOVE_BUTTON'
);
expect(chatAttachment.getAttribute('data-paste-element')).toEqual('MY_CHAT_ATTACHMENT');
expect(chatAttachmentBody.getAttribute('data-paste-element')).toEqual('MY_CHAT_ATTACHMENT_BODY');
expect(chatAttachmentLink.getAttribute('data-paste-element')).toEqual('MY_CHAT_ATTACHMENT_LINK');
expect(chatAttachmentDescription.getAttribute('data-paste-element')).toEqual('MY_CHAT_ATTACHMENT_DESCRIPTION');
});

it('should add custom styles to the component', () => {
render(
<ComposerAttachmentCard data-testid="composer-attachment-card" onDismiss={() => {}}>
<ChatAttachment data-testid="chat-attachment" attachmentIcon={<DownloadIcon decorative />}>
<ChatAttachmentLink data-testid="chat-attachment-link" href="www.google.com">
Document-FINAL.doc
</ChatAttachmentLink>
<ChatAttachmentDescription data-testid="chat-attachment-description">123 MB</ChatAttachmentDescription>
</ChatAttachment>
</ComposerAttachmentCard>,
{wrapper: CustomizationWrapper}
);

const composerAttachmentCard = screen.getByTestId('composer-attachment-card');
const composerAttachmentCardRemoveButton = screen.getByRole('button');
const chatAttachment = screen.getByTestId('chat-attachment');
const chatAttachmentBody = chatAttachment.lastChild as HTMLElement;
const chatAttachmentLink = screen.getByTestId('chat-attachment-link');
const chatAttachmentDescription = screen.getByTestId('chat-attachment-description');

expect(composerAttachmentCard).toHaveStyleRule('padding', '2.25rem');
expect(composerAttachmentCardRemoveButton).toHaveStyleRule('color', 'rgb(0, 20, 137)');
expect(chatAttachmentBody).toHaveStyleRule('padding', '0.25rem');
expect(chatAttachment).toHaveStyleRule('margin-left', '1rem');
expect(chatAttachmentLink).toHaveStyleRule('font-size', '1.125rem');
expect(chatAttachmentDescription).toHaveStyleRule('color', 'rgb(18, 28, 45)');
});

it('should add custom styles to the a custom element named component', () => {
render(
<ComposerAttachmentCard
element="MY_COMPOSER_ATTACHMENT_CARD"
data-testid="composer-attachment-card"
onDismiss={() => {}}
>
<ChatAttachment
element="MY_CHAT_ATTACHMENT"
data-testid="chat-attachment"
attachmentIcon={<DownloadIcon decorative />}
>
<ChatAttachmentLink
element="MY_CHAT_ATTACHMENT_LINK"
data-testid="chat-attachment-link"
href="www.google.com"
>
Document-FINAL.doc
</ChatAttachmentLink>
<ChatAttachmentDescription element="MY_CHAT_ATTACHMENT_DESCRIPTION" data-testid="chat-attachment-description">
123 MB
</ChatAttachmentDescription>
</ChatAttachment>
</ComposerAttachmentCard>,
{wrapper: CustomizationMyWrapper}
);

const composerAttachmentCard = screen.getByTestId('composer-attachment-card');
const composerAttachmentCardRemoveButton = screen.getByRole('button');
const chatAttachment = screen.getByTestId('chat-attachment');
const chatAttachmentBody = chatAttachment.lastChild as HTMLElement;
const chatAttachmentLink = screen.getByTestId('chat-attachment-link');
const chatAttachmentDescription = screen.getByTestId('chat-attachment-description');

expect(composerAttachmentCard).toHaveStyleRule('padding', '2.25rem');
expect(composerAttachmentCardRemoveButton).toHaveStyleRule('color', 'rgb(0, 20, 137)');
expect(chatAttachment).toHaveStyleRule('margin-left', '1rem');
expect(chatAttachmentBody).toHaveStyleRule('padding', '0.25rem');
expect(chatAttachmentLink).toHaveStyleRule('font-size', '1.125rem');
expect(chatAttachmentDescription).toHaveStyleRule('color', 'rgb(18, 28, 45)');
});
});

describe('Accessibility', () => {
it('Should have no accessibility violations', async () => {
const {container} = render(
<ComposerAttachmentCard>
<ChatAttachment attachmentIcon={<DownloadIcon decorative />}>
<ChatAttachmentLink href="www.google.com">Document-FINAL.doc</ChatAttachmentLink>
<ChatAttachmentDescription>123 MB</ChatAttachmentDescription>
</ChatAttachment>
</ComposerAttachmentCard>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
24 changes: 19 additions & 5 deletions packages/paste-core/components/chat-log/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,35 @@
"tsc": "tsc"
},
"peerDependencies": {
"@twilio-paste/anchor": "^8.0.0",
"@twilio-paste/box": "^6.0.0",
"@twilio-paste/button": "^10.0.0",
"@twilio-paste/customization": "^4.0.0",
"@twilio-paste/design-tokens": "^7.2.0",
"@twilio-paste/icons": "^8.0.0",
"@twilio-paste/media-object": "^6.0.0",
"@twilio-paste/screen-reader-only": "^9.0.0",
"@twilio-paste/style-props": "^5.0.0",
"@twilio-paste/text": "^6.0.0",
"@twilio-paste/theme": "^7.0.0",
"@twilio-paste/truncate": "^10.0.0",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@twilio-paste/box": "^6.0.1",
"@twilio-paste/customization": "^4.0.2",
"@twilio-paste/design-tokens": "^7.3.0",
"@twilio-paste/style-props": "^5.0.2",
"@twilio-paste/theme": "^7.0.1",
"@twilio-paste/anchor": "^8.0.0",
"@twilio-paste/box": "^6.0.0",
"@twilio-paste/button": "^10.0.0",
"@twilio-paste/customization": "^4.0.0",
"@twilio-paste/design-tokens": "^7.2.0",
"@twilio-paste/icons": "^8.0.0",
"@twilio-paste/media-object": "^6.0.0",
"@twilio-paste/screen-reader-only": "^9.0.0",
"@twilio-paste/style-props": "^5.0.0",
"@twilio-paste/text": "^6.0.0",
"@twilio-paste/theme": "^7.0.0",
"@twilio-paste/truncate": "^10.0.0",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
Expand Down
38 changes: 38 additions & 0 deletions packages/paste-core/components/chat-log/src/ChatAttachment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import type {BoxElementProps} from '@twilio-paste/box';
import {MediaObject, MediaFigure, MediaBody} from '@twilio-paste/media-object';
import {Stack} from '@twilio-paste/stack';

export interface ChatAttachmentProps {
children: NonNullable<React.ReactNode>;
element?: BoxElementProps['element'];
attachmentIcon: NonNullable<React.ReactNode>;
}

const ChatAttachment = React.forwardRef<HTMLDivElement, ChatAttachmentProps>(
({children, element = 'CHAT_ATTACHMENT', attachmentIcon, ...props}, ref) => {
return (
<MediaObject {...props} as="div" ref={ref} verticalAlign="center" element={element}>
<MediaFigure as="div" spacing="space30">
{attachmentIcon}
</MediaFigure>
<MediaBody as="div" element={`${element}_BODY`}>
<Stack orientation="vertical" spacing="space10">
{children}
</Stack>
</MediaBody>
</MediaObject>
);
}
);

ChatAttachment.displayName = 'ChatAttachment';

ChatAttachment.propTypes = {
children: PropTypes.node.isRequired,
element: PropTypes.string,
attachmentIcon: PropTypes.node.isRequired,
};

export {ChatAttachment};
Loading

0 comments on commit 0aa5216

Please sign in to comment.