{
}
private generateChatItems(chat: ChatData): JSX.Element[] {
- return generateChatProps(chat).map(({ itemType, ...props }, index) => {
- const ElementType = this.getElementType(itemType)
- const maybeAttributesForDivider =
- itemType === ChatItemTypes.divider
- ? {
- role: 'heading',
- 'aria-level': 3,
- }
- : {}
- return (
-
- {itemType === ChatItemTypes.message && (
-
- {this.getMessagePreviewForScreenReader(props)}
-
- )}
-
-
- )
- })
+ return generateChatProps(chat).map(
+ ({ mine, gutter, message: { itemType, ...props } }, index) => {
+ const ElementType = this.getElementType(itemType)
+ const maybeAttributesForDivider =
+ itemType === ChatItemTypes.divider
+ ? {
+ role: 'heading',
+ 'aria-level': 3,
+ }
+ : {}
+ return (
+ }}
+ message={{
+ content: (
+ <>
+ {itemType === ChatItemTypes.message && (
+
+ {this.getMessagePreviewForScreenReader(props)}
+
+ )}
+
+ >
+ ),
+ }}
+ />
+ )
+ },
+ )
}
private getElementType = (itemType: ChatItemTypes) => {
diff --git a/docs/src/prototypes/chatPane/services/messageFactoryMock.tsx b/docs/src/prototypes/chatPane/services/messageFactoryMock.tsx
index 7d50c4ed2d..b0f22acbd6 100644
--- a/docs/src/prototypes/chatPane/services/messageFactoryMock.tsx
+++ b/docs/src/prototypes/chatPane/services/messageFactoryMock.tsx
@@ -1,4 +1,11 @@
-import { Attachment, Popup, Button, Menu, popupFocusTrapBehavior } from '@stardust-ui/react'
+import {
+ Attachment,
+ Popup,
+ Button,
+ Menu,
+ popupFocusTrapBehavior,
+ AvatarProps,
+} from '@stardust-ui/react'
import * as React from 'react'
import * as _ from 'lodash'
import { ChatMessageProps } from 'src/components/Chat/ChatMessage'
@@ -12,18 +19,22 @@ export enum ChatItemTypes {
divider,
}
-interface ChatItem {
+interface ChatItemType {
itemType: ChatItemTypes
}
-interface ChatMessage extends ChatMessageProps, ChatItem {
+interface ChatMessage extends ChatMessageProps, ChatItemType {
tabIndex: number
'aria-labelledby': string
text: string
}
-interface Divider extends DividerProps, ChatItem {}
+interface Divider extends DividerProps, ChatItemType {}
-type ChatItemContentProps = ChatMessage | Divider
+type ChatItem = {
+ message?: ChatMessage | Divider
+ gutter?: AvatarProps
+ mine?: boolean
+}
type StatusPropsExtendable = Extendable
const statusMap: Map = new Map([
@@ -33,7 +44,7 @@ const statusMap: Map = new Map([
['Offline', { color: 'grey', title: 'Offline' }],
] as [UserStatus, StatusPropsExtendable][])
-function generateChatMsgProps(message: MessageData, fromUser: UserData): ChatMessage {
+function generateChatMsgProps(message: MessageData, fromUser: UserData): ChatItem {
const { content, mine } = message
const messageProps: ChatMessage = {
// aria-labelledby will need to by generated based on the needs. Currently just hardcoded.
@@ -53,12 +64,15 @@ function generateChatMsgProps(message: MessageData, fromUser: UserData): ChatMes
content: `${fromUser.firstName} ${fromUser.lastName} `,
id: `sender-${message.id}`,
},
- avatar: !message.mine && { image: fromUser.avatar, status: statusMap.get(fromUser.status) },
itemType: ChatItemTypes.message,
text: content,
}
- return messageProps
+ return {
+ mine,
+ message: messageProps,
+ gutter: !message.mine && { image: fromUser.avatar, status: statusMap.get(fromUser.status) },
+ }
}
function createMessageContent(message: MessageData): ShorthandValue {
@@ -120,20 +134,20 @@ function createMessageContentWithAttachments(content: string, messageId: string)
)
}
-function generateDividerProps(props: DividerProps): Divider {
+function generateDividerProps(props: DividerProps): ChatItem {
const { content, important, color = 'secondary' } = props
const dividerProps: Divider = { itemType: ChatItemTypes.divider, content, important, color }
- return dividerProps
+ return { message: dividerProps }
}
-export function generateChatProps(chat: ChatData): ChatItemContentProps[] {
+export function generateChatProps(chat: ChatData): ChatItem[] {
if (!chat || !chat.members || !chat.messages) {
return []
}
const { messages, members } = chat
- const chatProps: ChatItemContentProps[] = []
+ const chatProps: ChatItem[] = []
// First date divider
chatProps.push(generateDividerProps({ content: getFriendlyDateString(messages[0].date) }))
@@ -152,7 +166,7 @@ export function generateChatProps(chat: ChatData): ChatItemContentProps[] {
chatProps.push(generateChatMsgProps(lastMsg, members.get(lastMsg.from)))
// Last read divider
- const myLastMsgIndex = _.findLastIndex(chatProps, item => (item as ChatMessage).mine)
+ const myLastMsgIndex = _.findLastIndex(chatProps, item => item.mine)
if (myLastMsgIndex < chatProps.length - 1) {
chatProps.splice(
myLastMsgIndex + 1,
diff --git a/src/components/Chat/ChatItem.tsx b/src/components/Chat/ChatItem.tsx
index 308e012394..fee9a407bf 100644
--- a/src/components/Chat/ChatItem.tsx
+++ b/src/components/Chat/ChatItem.tsx
@@ -1,4 +1,5 @@
import * as React from 'react'
+import * as PropTypes from 'prop-types'
import { Extendable, ShorthandValue } from '../../../types/utils'
import {
@@ -8,58 +9,67 @@ import {
UIComponent,
UIComponentProps,
ChildrenComponentProps,
- ContentComponentProps,
commonPropTypes,
+ customPropTypes,
} from '../../lib'
import Slot from '../Slot/Slot'
+import { ComponentSlotStylesPrepared } from '../../themes/types'
-export interface ChatItemProps
- extends UIComponentProps,
- ChildrenComponentProps,
- ContentComponentProps {}
+export interface ChatItemProps extends UIComponentProps, ChildrenComponentProps {
+ /** Chat items can have a gutter. */
+ gutter?: ShorthandValue
+
+ /** Indicates whether the gutter is positioned at the start or the end. */
+ gutterPosition?: 'start' | 'end'
+
+ /** Chat items can have a message. */
+ message?: ShorthandValue
+}
/**
* A chat item represents a single event in a chat.
*/
class ChatItem extends UIComponent, any> {
static className = 'ui-chat__item'
-
static create: Function
-
static displayName = 'ChatItem'
static propTypes = {
- ...commonPropTypes.createCommon({
- content: 'shorthand',
- }),
+ ...commonPropTypes.createCommon({ content: false }),
+ gutter: customPropTypes.itemShorthand,
+ gutterPosition: PropTypes.oneOf(['start', 'end']),
+ message: customPropTypes.itemShorthand,
}
static defaultProps = {
as: 'li',
+ gutterPosition: 'start',
}
- renderComponent({
- ElementType,
- classes,
- styles,
- variables,
- rest,
- }: RenderResultConfig) {
- const { children, content } = this.props
+ renderComponent({ ElementType, classes, rest, styles }: RenderResultConfig) {
+ const { children } = this.props
return (
- {childrenExist(children)
- ? children
- : Slot.create(content, {
- styles: styles.content,
- variables: variables.content,
- })}
+ {childrenExist(children) ? children : this.renderChatItem(styles)}
)
}
+
+ private renderChatItem(styles: ComponentSlotStylesPrepared) {
+ const { message, gutter, gutterPosition } = this.props
+ const gutterElement = gutter && Slot.create(gutter, { defaultProps: { styles: styles.gutter } })
+
+ return (
+ <>
+ {gutterPosition === 'start' && gutterElement}
+ {Slot.create(message, { defaultProps: { styles: styles.message } })}
+ {gutterPosition === 'end' && gutterElement}
+ >
+ )
+ }
}
-ChatItem.create = createShorthandFactory(ChatItem, 'content')
+ChatItem.create = createShorthandFactory(ChatItem, 'message')
export default ChatItem
diff --git a/src/components/Chat/ChatMessage.tsx b/src/components/Chat/ChatMessage.tsx
index bd33694d03..97ae4d69a5 100644
--- a/src/components/Chat/ChatMessage.tsx
+++ b/src/components/Chat/ChatMessage.tsx
@@ -13,15 +13,10 @@ import {
ContentComponentProps,
commonPropTypes,
} from '../../lib'
-import {
- ComponentVariablesInput,
- ComponentSlotClasses,
- ComponentSlotStylesInput,
-} from '../../themes/types'
import { Extendable, ShorthandValue } from '../../../types/utils'
-import Avatar from '../Avatar/Avatar'
import { chatMessageBehavior } from '../../lib/accessibility'
import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types'
+
import Text from '../Text/Text'
import Slot from '../Slot/Slot'
@@ -38,9 +33,6 @@ export interface ChatMessageProps
/** Author of the message. */
author?: ShorthandValue
- /** Chat messages can have an avatar. */
- avatar?: ShorthandValue
-
/** Indicates whether message belongs to the current user. */
mine?: boolean
@@ -59,12 +51,9 @@ class ChatMessage extends UIComponent, any> {
static displayName = 'ChatMessage'
static propTypes = {
- ...commonPropTypes.createCommon({
- content: 'shorthand',
- }),
+ ...commonPropTypes.createCommon({ content: 'shorthand' }),
accessibility: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
author: customPropTypes.itemShorthand,
- avatar: customPropTypes.itemShorthand,
mine: PropTypes.bool,
timestamp: customPropTypes.itemShorthand,
}
@@ -87,9 +76,8 @@ class ChatMessage extends UIComponent, any> {
accessibility,
rest,
styles,
- variables,
}: RenderResultConfig) {
- const { children } = this.props
+ const { author, children, content, mine, timestamp } = this.props
const childrenPropExists = childrenExist(children)
const className = childrenPropExists ? cx(classes.root, classes.content) : classes.root
@@ -100,56 +88,21 @@ class ChatMessage extends UIComponent, any> {
{...rest}
className={className}
>
- {childrenPropExists ? children : this.renderContent(classes, styles, variables)}
+ {childrenPropExists ? (
+ children
+ ) : (
+ <>
+ {!mine &&
+ Text.create(author, { defaultProps: { size: 'small', styles: styles.author } })}
+ {Text.create(timestamp, {
+ defaultProps: { size: 'small', styles: styles.timestamp, timestamp: true },
+ })}
+ {Slot.create(content, { defaultProps: { styles: styles.content } })}
+ >
+ )}
)
}
-
- private renderContent = (
- classes: ComponentSlotClasses,
- styles: ComponentSlotStylesInput,
- variables: ComponentVariablesInput,
- ) => {
- const { author, avatar, content, mine, timestamp } = this.props
-
- const avatarElement = Avatar.create(avatar, {
- defaultProps: {
- styles: styles.avatar,
- variables: variables.avatar,
- },
- })
-
- const authorElement = Text.create(author, {
- defaultProps: {
- size: 'small',
- styles: styles.author,
- },
- })
-
- const timestampElement = Text.create(timestamp, {
- defaultProps: {
- size: 'small',
- styles: styles.timestamp,
- timestamp: true,
- },
- })
-
- const contentElement = Slot.create(content, {
- defaultProps: { styles: styles.content },
- })
-
- return (
- <>
- {!mine && avatarElement}
-
- {!mine && authorElement}
- {timestampElement}
- {contentElement}
-
- {mine && avatarElement}
- >
- )
- }
}
ChatMessage.create = createShorthandFactory(ChatMessage, 'content')
diff --git a/src/themes/teams/componentVariables.ts b/src/themes/teams/componentVariables.ts
index c21c3db458..a2b3261f0e 100644
--- a/src/themes/teams/componentVariables.ts
+++ b/src/themes/teams/componentVariables.ts
@@ -8,9 +8,7 @@ export { default as Button } from './components/Button/buttonVariables'
export { default as ButtonGroup } from './components/Button/buttonVariables'
export { default as Chat } from './components/Chat/chatVariables'
-
export { default as ChatItem } from './components/Chat/chatItemVariables'
-
export { default as ChatMessage } from './components/Chat/chatMessageVariables'
export { default as Divider } from './components/Divider/dividerVariables'
diff --git a/src/themes/teams/components/Chat/chatItemStyles.ts b/src/themes/teams/components/Chat/chatItemStyles.ts
index eba9eb8e2f..6f9458713c 100644
--- a/src/themes/teams/components/Chat/chatItemStyles.ts
+++ b/src/themes/teams/components/Chat/chatItemStyles.ts
@@ -1,7 +1,25 @@
-import { ICSSInJSStyle } from '../../../types'
+import { ICSSInJSStyle, ComponentSlotStylesInput } from '../../../types'
+import { ChatItemVariables } from './chatItemVariables'
+import { ChatItemProps } from '../../../../components/Chat/ChatItem'
-const chatItemStyles = {
- root: (): ICSSInJSStyle => ({}),
+const chatItemStyles: ComponentSlotStylesInput = {
+ root: ({ props: p, variables: v }): ICSSInJSStyle => ({
+ position: 'relative',
+ marginTop: v.margin,
+ marginBottom: v.margin,
+ }),
+
+ gutter: ({ props: p, variables: v }): ICSSInJSStyle => ({
+ position: 'absolute',
+ marginTop: v.gutterMargin,
+ [p.gutterPosition === 'end' ? 'right' : 'left']: 0,
+ }),
+
+ message: ({ props: p, variables: v }): ICSSInJSStyle => ({
+ position: 'relative',
+ marginLeft: v.messageMargin,
+ marginRight: v.messageMargin,
+ }),
}
export default chatItemStyles
diff --git a/src/themes/teams/components/Chat/chatItemVariables.ts b/src/themes/teams/components/Chat/chatItemVariables.ts
index c7770045c9..632d839aeb 100644
--- a/src/themes/teams/components/Chat/chatItemVariables.ts
+++ b/src/themes/teams/components/Chat/chatItemVariables.ts
@@ -1,15 +1,13 @@
+import { pxToRem } from '../../utils'
+
export interface ChatItemVariables {
- messageWidth: string
- messageColor: string
- messageColorMine: string
- avatar: { statusBorderColor: string }
+ margin: string
+ gutterMargin: string
+ messageMargin: string
}
-export default (siteVars): ChatItemVariables => ({
- messageWidth: '80%',
- messageColor: siteVars.white,
- messageColorMine: '#E0E0ED',
- avatar: {
- statusBorderColor: siteVars.gray10,
- },
+export default (): ChatItemVariables => ({
+ margin: pxToRem(8),
+ gutterMargin: pxToRem(10),
+ messageMargin: pxToRem(40),
})
diff --git a/src/themes/teams/components/Chat/chatMessageStyles.ts b/src/themes/teams/components/Chat/chatMessageStyles.ts
index ac3fc94955..bfd1cef48a 100644
--- a/src/themes/teams/components/Chat/chatMessageStyles.ts
+++ b/src/themes/teams/components/Chat/chatMessageStyles.ts
@@ -1,56 +1,33 @@
import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types'
import { ChatMessageProps } from '../../../../components/Chat/ChatMessage'
import { ChatMessageVariables } from './chatMessageVariables'
-import { pxToRem } from '../../utils'
-
-const px10asRem = pxToRem(10)
const chatMessageStyles: ComponentSlotStylesInput = {
root: ({ props: p, variables: v }): ICSSInJSStyle => ({
- display: 'inline-flex',
- position: 'relative',
- marginTop: '1rem',
- marginBottom: '1rem',
- ...(p.mine && {
- float: 'right',
- }),
- maxWidth: v.messageWidth,
+ display: 'inline-block',
+ padding: v.padding,
+ borderRadius: v.borderRadius,
+ color: v.color,
+ backgroundColor: p.mine ? v.backgroundColorMine : v.backgroundColor,
+ maxWidth: v.width,
wordBreak: 'break-word',
wordWrap: 'break-word',
+ ...(p.mine && { float: 'right' }),
':focus': {
- outline: 'none',
- '& .ui-chat__message__messageBody': {
- outline: `.2rem solid ${v.messageBody.focusOutlineColor}`,
- },
+ outline: `.2rem solid ${v.contentFocusOutlineColor}`,
},
}),
- avatar: ({ props: p }: { props: ChatMessageProps }): ICSSInJSStyle => ({
- flex: 'none',
- display: p.mine ? 'none' : undefined,
- marginTop: px10asRem,
- marginBottom: px10asRem,
- marginLeft: p.mine ? px10asRem : 0,
- marginRight: p.mine ? 0 : px10asRem,
- }),
-
- messageBody: ({ props: p, variables: v }): ICSSInJSStyle => ({
- padding: '1rem',
- color: 'rgb(64, 64, 64)',
- backgroundColor: p.mine ? v.messageColorMine : v.messageColor,
- borderRadius: '0.3rem',
- }),
-
- author: ({ props: p }): ICSSInJSStyle => ({
+ author: ({ props: p, variables: v }): ICSSInJSStyle => ({
display: p.mine ? 'none' : undefined,
- marginRight: px10asRem,
+ marginRight: v.authorMargin,
}),
content: ({ variables: v }): ICSSInJSStyle => ({
display: 'block',
'& a:focus': {
outline: 'none',
- color: v.messageBody.focusOutlineColor,
+ color: v.contentFocusOutlineColor,
textDecoration: 'underline',
},
}),
diff --git a/src/themes/teams/components/Chat/chatMessageVariables.ts b/src/themes/teams/components/Chat/chatMessageVariables.ts
index 988709a7ac..baef4681e0 100644
--- a/src/themes/teams/components/Chat/chatMessageVariables.ts
+++ b/src/themes/teams/components/Chat/chatMessageVariables.ts
@@ -1,17 +1,23 @@
+import { pxToRem } from '../../utils'
+
export interface ChatMessageVariables {
- messageWidth: string
- messageColor: string
- messageColorMine: string
- avatar: { statusBorderColor: string }
- messageBody: { focusOutlineColor: string }
+ width: string
+ backgroundColor: string
+ backgroundColorMine: string
+ borderRadius: string
+ color: string
+ padding: string
+ authorMargin: string
+ contentFocusOutlineColor: string
}
export default (siteVars): ChatMessageVariables => ({
- messageWidth: '80%',
- messageColor: siteVars.white,
- messageColorMine: '#E0E0ED',
- avatar: {
- statusBorderColor: siteVars.gray10,
- },
- messageBody: { focusOutlineColor: siteVars.brand },
+ width: '80%',
+ backgroundColor: siteVars.white,
+ backgroundColorMine: '#E0E0ED',
+ borderRadius: '0.3rem',
+ color: 'rgb(64, 64, 64)',
+ padding: pxToRem(14),
+ authorMargin: pxToRem(10),
+ contentFocusOutlineColor: siteVars.brand,
})
diff --git a/src/themes/teams/components/Chat/chatStyles.ts b/src/themes/teams/components/Chat/chatStyles.ts
index a046d1e8a9..aa5ad38ca4 100644
--- a/src/themes/teams/components/Chat/chatStyles.ts
+++ b/src/themes/teams/components/Chat/chatStyles.ts
@@ -1,9 +1,10 @@
-import { ICSSInJSStyle } from '../../../types'
+import { ICSSInJSStyle, ComponentSlotStylesInput } from '../../../types'
import { ChatVariables } from './chatVariables'
import { pxToRem } from '../../utils'
+import { ChatProps } from 'src/components/Chat/Chat'
-const chatStyles = {
- root: ({ variables: v }: { variables: ChatVariables }): ICSSInJSStyle => ({
+const chatStyles: ComponentSlotStylesInput = {
+ root: ({ variables: v }): ICSSInJSStyle => ({
backgroundColor: v.backgroundColor,
border: `1px solid ${v.backgroundColor}`,
display: 'flex',
diff --git a/test/specs/components/Chat/Chat-test.ts b/test/specs/components/Chat/Chat-test.ts
index bdff0c672d..0705f2e276 100644
--- a/test/specs/components/Chat/Chat-test.ts
+++ b/test/specs/components/Chat/Chat-test.ts
@@ -10,7 +10,7 @@ const chatImplementsCollectionShorthandProp = implementsCollectionShorthandProp(
describe('Chat', () => {
isConformant(Chat)
- chatImplementsCollectionShorthandProp('items', ChatItem)
+ chatImplementsCollectionShorthandProp('items', ChatItem, { mapsValueToProp: 'message' })
describe('accessibility', () => {
handlesAccessibility(Chat, {
diff --git a/test/specs/components/Chat/ChatItem-test.tsx b/test/specs/components/Chat/ChatItem-test.tsx
index dd6e78f310..c896daa242 100644
--- a/test/specs/components/Chat/ChatItem-test.tsx
+++ b/test/specs/components/Chat/ChatItem-test.tsx
@@ -1,7 +1,12 @@
-import { isConformant } from 'test/specs/commonTests'
-
+import { isConformant, implementsShorthandProp } from 'test/specs/commonTests'
import ChatItem from 'src/components/Chat/ChatItem'
+import Slot from 'src/components/Slot/Slot'
+
+const chatItemImplementsShorthandProp = implementsShorthandProp(ChatItem)
describe('ChatItem', () => {
isConformant(ChatItem)
+
+ chatItemImplementsShorthandProp('gutter', Slot, { mapsValueToProp: 'children' })
+ chatItemImplementsShorthandProp('message', Slot, { mapsValueToProp: 'children' })
})
diff --git a/test/specs/components/Chat/ChatMessage-test.tsx b/test/specs/components/Chat/ChatMessage-test.tsx
index 419e7a81f9..6c12ed8dbb 100644
--- a/test/specs/components/Chat/ChatMessage-test.tsx
+++ b/test/specs/components/Chat/ChatMessage-test.tsx
@@ -1,31 +1,23 @@
-import * as React from 'react'
import { handlesAccessibility, implementsShorthandProp, isConformant } from 'test/specs/commonTests'
-import { mountWithProvider } from 'test/utils'
import ChatMessage from 'src/components/Chat/ChatMessage'
-import Avatar from 'src/components/Avatar/Avatar'
import { chatMessageBehavior } from 'src/lib/accessibility'
import { AccessibilityDefinition } from 'src/lib/accessibility/types'
import Text from 'src/components/Text/Text'
+import Slot from 'src/components/Slot/Slot'
+
+const chatMessageImplementsShorthandProp = implementsShorthandProp(ChatMessage)
describe('ChatMessage', () => {
isConformant(ChatMessage)
- implementsShorthandProp(ChatMessage)('avatar', Avatar, { mapsValueToProp: 'name' })
- implementsShorthandProp(ChatMessage)('author', Text)
- implementsShorthandProp(ChatMessage)('timestamp', Text)
+
+ chatMessageImplementsShorthandProp('author', Text)
+ chatMessageImplementsShorthandProp('timestamp', Text)
+ chatMessageImplementsShorthandProp('content', Slot, { mapsValueToProp: 'children' })
describe('accessibility', () => {
handlesAccessibility(ChatMessage, {
focusZoneDefinition: (chatMessageBehavior as AccessibilityDefinition).focusZone,
})
})
-
- describe('avatar', () => {
- it('creates an Avatar component when the avatar shorthand is provided', () => {
- const name = 'John Doe'
- const chatMessage = mountWithProvider()
-
- expect(chatMessage.find('Avatar').prop('name')).toEqual(name)
- })
- })
})