-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(blade): chat message component (#2513)
* feat/chat-bubble-api-decision * chore: api docs * chore: update is user message * fix: typo * feat: updated decisions * chore: more changes * chore: update decision and chatBubble * feat: done with right message * feat: response message * feat: api changes * chore: add motion * feat: add animation and update some props * chore: code refactor * chore: updated docs for chatbubble * chore: update width * chore: more refactor * fix: getStringFromReactText from utils * feat: add style props * feat: add message * chore: added more example * feat: add types * feat: added ref * feat: update types and example * chore: more ts and other changes * chore: update decision.md * chore: update ray icon * chore: update snops * chore: add more tests * feat: add sort and prompt icon [p0] (#2511) * feat: add sort and cherry pick icons * chore: add changeset * chore: change sort * fix: more lint changes * chore: few changes * chore: sort icon * chore: remove path * chore: update changeset * build(blade): update version (#2512) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(Avatar): add missing ref on default avatar (#2516) * fix(Avatar): add missing ref on default avatar * Create nasty-frogs-end.md * build(blade): update version (#2518) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * feat: add chatbubble * chore: minior review changes * chore: more review changes * chore: typescript gymnastics * chore: use Meta constans * chore: remove lazy motion * chore: added jsDoc comments * chore: added tokens * chore: update comments * chore: remove stringChildType and change export * chore: fix another error * chore: added wrapper component * chore: more changes * chrore: add footer actions * chore: update tests and snap * feat: more tests * fix: add should add padding * chore: add typing animation * chore: file name refactor , new animation * chore:sender type * chore: removed max width * chore: update docs * chore: chatMessage refactor * chore: update snaps * chore: fix ts * chore: more changes * feat: chatMessage * chore: docs update * chore: update snaps * chore: update example * chore: change styledPropsBlade to BaseBoxProps * fix: types * chore: added more types * chore: import type * chore: change types and footer spacing * chore: update snaps * chore: more changes * chore: reverse rfc change * chore: update test * chore: update snap and wrapper * chore: update web snap * chore: add make box props * feat: update snaps * chore: more changes * chore: update type and example --------- Co-authored-by: rzpcibot <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Saurabh Daware <[email protected]>
- Loading branch information
1 parent
4b6b43a
commit d906838
Showing
18 changed files
with
3,325 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@razorpay/blade': minor | ||
--- | ||
|
||
feat(blade): add Chat Message component |
15 changes: 15 additions & 0 deletions
15
packages/blade/src/components/ChatMessage/ChatMessage.native.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import React from 'react'; | ||
import type { ChatMessageProps } from './types'; | ||
import { Text } from '~components/Typography'; | ||
import { throwBladeError } from '~utils/logger'; | ||
|
||
const ChatMessage = (_prop: ChatMessageProps): React.ReactElement => { | ||
throwBladeError({ | ||
message: 'ChatMessage is not yet implemented for native', | ||
moduleName: 'ChatMessage', | ||
}); | ||
|
||
return <Text>ChatMessage is not available for Native mobile apps.</Text>; | ||
}; | ||
|
||
export { ChatMessage }; |
102 changes: 102 additions & 0 deletions
102
packages/blade/src/components/ChatMessage/ChatMessage.web.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import React from 'react'; | ||
import { SelfMessageBubble } from './SelfMessageBubble.web'; | ||
import { DefaultMessageBubble } from './DefaultMessageBubble.web'; | ||
import type { ChatMessageProps } from './types'; | ||
import { Text } from '~components/Typography'; | ||
import BaseBox from '~components/Box/BaseBox'; | ||
import { getStringFromReactText } from '~utils/getStringChildren'; | ||
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; | ||
import type { BladeElementRef } from '~utils/types'; | ||
import { MetaConstants, metaAttribute } from '~utils/metaAttribute'; | ||
import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute'; | ||
import { getStyledProps } from '~components/Box/styledProps'; | ||
|
||
const ButtonResetCss = { | ||
background: 'none', | ||
border: 'none', | ||
padding: undefined, | ||
cursor: 'pointer', | ||
color: 'inherit', | ||
font: 'inherit', | ||
textAlign: 'left' as const, | ||
outline: 'inherit', | ||
appearance: 'none', | ||
backgroundColor: 'inherit', | ||
}; | ||
|
||
const _ChatMessage: React.ForwardRefRenderFunction<BladeElementRef, ChatMessageProps> = ( | ||
{ | ||
messageType = 'default', | ||
senderType = 'self', | ||
isLoading = false, | ||
validationState = 'none', | ||
errorText, | ||
onClick, | ||
footerActions, | ||
children, | ||
leading, | ||
loadingText, | ||
wordBreak = 'break-word', | ||
maxWidth, | ||
...props | ||
}: ChatMessageProps, | ||
ref: React.Ref<BladeElementRef>, | ||
): React.ReactElement => { | ||
// since we can pass both string and Card component as children, we need to check if children is string or Card component | ||
// if children is string or array of string, we need to wrap it in Text component otherwise we will pass children as it is | ||
const shouldWrapInText = | ||
typeof children === 'string' || | ||
(Array.isArray(children) && children.every((child) => typeof child === 'string')) || | ||
isLoading; | ||
|
||
const finalChildren = shouldWrapInText ? ( | ||
<Text | ||
color={isLoading ? 'surface.text.gray.muted' : 'surface.text.gray.normal'} | ||
weight="regular" | ||
variant="body" | ||
size="medium" | ||
wordBreak={wordBreak} | ||
> | ||
{isLoading ? loadingText : getStringFromReactText(children as string | string[])} | ||
</Text> | ||
) : ( | ||
(children as React.ReactElement) | ||
); | ||
|
||
return ( | ||
<BaseBox | ||
onClick={onClick} | ||
{...(onClick ? { ...ButtonResetCss } : {})} | ||
{...metaAttribute({ name: MetaConstants.ChatMessage, testID: props.testID })} | ||
{...makeAnalyticsAttribute(props)} | ||
{...getStyledProps(props)} | ||
maxWidth={maxWidth} | ||
ref={ref as never} | ||
as={onClick ? 'button' : undefined} | ||
> | ||
{senderType === 'self' ? ( | ||
<SelfMessageBubble | ||
validationState={validationState} | ||
errorText={errorText} | ||
children={finalChildren} | ||
messageType={messageType} | ||
isChildText={shouldWrapInText} | ||
/> | ||
) : ( | ||
<DefaultMessageBubble | ||
children={finalChildren} | ||
leading={leading} | ||
isLoading={isLoading} | ||
footerActions={footerActions} | ||
isChildText={shouldWrapInText} | ||
/> | ||
)} | ||
</BaseBox> | ||
); | ||
}; | ||
|
||
const ChatMessage = assignWithoutSideEffects(React.forwardRef(_ChatMessage), { | ||
displayName: 'ChatMessage', | ||
componentId: MetaConstants.ChatMessage, | ||
}); | ||
export { ChatMessage }; |
41 changes: 41 additions & 0 deletions
41
packages/blade/src/components/ChatMessage/DefaultMessageBubble.web.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import React from 'react'; | ||
import Rotate from './Rotate.web'; | ||
import type { CommonChatMessageProps } from './types'; | ||
import BaseBox from '~components/Box/BaseBox'; | ||
|
||
const DefaultMessageBubble = ({ | ||
children, | ||
leading, | ||
isLoading, | ||
footerActions, | ||
isChildText, | ||
}: Pick<CommonChatMessageProps, 'children' | 'leading' | 'isLoading' | 'footerActions'> & { | ||
isChildText: boolean; | ||
}): React.ReactElement => { | ||
return ( | ||
<BaseBox> | ||
<BaseBox | ||
display="grid" | ||
gridTemplateColumns="auto 1fr" | ||
gridTemplateRows="auto auto" | ||
columnGap="spacing.4" | ||
> | ||
<BaseBox padding="spacing.2"> | ||
<Rotate animate={isLoading}>{leading as React.ReactElement}</Rotate> | ||
</BaseBox> | ||
|
||
<BaseBox | ||
display="flex" | ||
alignItems="center" | ||
paddingY={isChildText ? 'spacing.2' : 'spacing.0'} | ||
> | ||
{children} | ||
</BaseBox> | ||
|
||
<BaseBox gridColumn="2">{footerActions}</BaseBox> | ||
</BaseBox> | ||
</BaseBox> | ||
); | ||
}; | ||
|
||
export { DefaultMessageBubble }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import React from 'react'; | ||
import { m } from 'framer-motion'; | ||
import { castWebType } from '~utils'; | ||
import { useTheme } from '~components/BladeProvider'; | ||
import { msToSeconds } from '~utils/msToSeconds'; | ||
import { cssBezierToArray } from '~utils/cssBezierToArray'; | ||
|
||
const Rotate = ({ | ||
children, | ||
animate, | ||
}: { | ||
children: React.ReactElement; | ||
animate?: boolean; | ||
}): React.ReactElement => { | ||
const { theme } = useTheme(); | ||
|
||
if (!animate) { | ||
return children; | ||
} | ||
|
||
return ( | ||
<m.div | ||
style={{ | ||
display: 'flex', | ||
}} | ||
animate={{ rotate: 90 }} | ||
transition={{ | ||
duration: msToSeconds(theme.motion.duration.gentle), | ||
repeat: Infinity, | ||
ease: cssBezierToArray(castWebType(theme.motion.easing.emphasized)), | ||
delay: msToSeconds(theme.motion.delay.gentle), | ||
}} | ||
> | ||
{children} | ||
</m.div> | ||
); | ||
}; | ||
|
||
export default Rotate; |
48 changes: 48 additions & 0 deletions
48
packages/blade/src/components/ChatMessage/SelfMessageBubble.web.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import React from 'react'; | ||
import type { CommonChatMessageProps } from './types'; | ||
import { chatMessageToken } from './token'; | ||
import BaseBox from '~components/Box/BaseBox'; | ||
import { FormHint } from '~components/Form/FormHint'; | ||
|
||
const SelfMessageBubble = ({ | ||
children, | ||
validationState, | ||
errorText = 'Message not sent. Tap to retry.', | ||
messageType, | ||
isChildText, | ||
}: Pick<CommonChatMessageProps, 'children' | 'validationState' | 'errorText' | 'messageType'> & { | ||
// is child is text then only add padding otherwise no need to add padding | ||
isChildText: boolean; | ||
}): React.ReactElement => { | ||
const isError = validationState === 'error'; | ||
return ( | ||
<BaseBox display="flex" flexDirection="column"> | ||
<BaseBox | ||
backgroundColor={ | ||
isError | ||
? chatMessageToken.self.backgroundColor.error | ||
: chatMessageToken.self.backgroundColor.default | ||
} | ||
padding={isChildText ? 'spacing.4' : 'spacing.0'} | ||
borderTopLeftRadius={chatMessageToken.self.borderTopLeftRadius} | ||
borderTopRightRadius={chatMessageToken.self.borderTopRightRadius} | ||
borderBottomLeftRadius={chatMessageToken.self.borderBottomLeftRadius} | ||
borderBottomRightRadius={ | ||
messageType === 'last' | ||
? chatMessageToken.self.borderBottomRightRadiusForLastMessage | ||
: chatMessageToken.self.borderBottomRightRadius | ||
} | ||
width="fit-content" | ||
height="auto" | ||
alignSelf="flex-end" | ||
> | ||
{children} | ||
</BaseBox> | ||
<BaseBox alignSelf="flex-end"> | ||
{isError && <FormHint type="error" errorText={errorText} size="small" />} | ||
</BaseBox> | ||
</BaseBox> | ||
); | ||
}; | ||
|
||
export { SelfMessageBubble }; |
131 changes: 131 additions & 0 deletions
131
packages/blade/src/components/ChatMessage/__tests__/ChatMessage.ssr.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
// test case for ChatMessage component | ||
import { fireEvent } from '@testing-library/react'; | ||
import { ChatMessage } from '../ChatMessage'; | ||
import renderWithSSR from '~utils/testing/renderWithSSR.web'; | ||
import { RayIcon } from '~components/Icons'; | ||
import { Card, CardBody } from '~components/Card'; | ||
import { Box } from '~components/Box'; | ||
import { Text } from '~components/Typography'; | ||
import { Radio, RadioGroup } from '~components/Radio'; | ||
|
||
describe('<ChatMessage/>', () => { | ||
it('should render last message correctly', () => { | ||
const { container } = renderWithSSR( | ||
<ChatMessage senderType="self" messageType="last"> | ||
{' '} | ||
This is a demo message{' '} | ||
</ChatMessage>, | ||
); | ||
expect(container).toMatchSnapshot(); | ||
}); | ||
it('should render last message correctly', () => { | ||
const { container } = renderWithSSR( | ||
<ChatMessage messageType="default" senderType="self"> | ||
{' '} | ||
This is another demo message{' '} | ||
</ChatMessage>, | ||
); | ||
expect(container).toMatchSnapshot(); | ||
}); | ||
it('should render last message correctly', () => { | ||
const { container } = renderWithSSR( | ||
<ChatMessage | ||
senderType="other" | ||
leading={<RayIcon size="xlarge" color="surface.icon.onSea.onSubtle" />} | ||
> | ||
{' '} | ||
This is another demo message{' '} | ||
</ChatMessage>, | ||
); | ||
expect(container).toMatchSnapshot(); | ||
}); | ||
it('should render last message correctly', () => { | ||
const { container } = renderWithSSR( | ||
<ChatMessage | ||
senderType="other" | ||
leading={<RayIcon size="xlarge" color="surface.icon.onSea.onSubtle" />} | ||
loadingText="Analyzing your response..." | ||
> | ||
<Card> | ||
<CardBody> | ||
<Box display="flex" gap="8px" flexDirection="column"> | ||
<Text variant="body" size="medium"> | ||
Where do you want to collect payments? | ||
</Text> | ||
<RadioGroup> | ||
<Radio value="website">Website</Radio> | ||
<Radio value="android">Android App</Radio> | ||
<Radio value="ios">iOS App</Radio> | ||
</RadioGroup> | ||
</Box> | ||
</CardBody> | ||
</Card> | ||
</ChatMessage>, | ||
); | ||
expect(container).toMatchSnapshot(); | ||
}); | ||
it('it should fire onClick event when user clicks on message button', () => { | ||
const onClick = jest.fn(); | ||
const { getByText } = renderWithSSR( | ||
<ChatMessage | ||
senderType="self" | ||
messageType="last" | ||
validationState="error" | ||
errorText="Message not sent. Tap to retry." | ||
onClick={onClick} | ||
> | ||
Can you help me with the docs? | ||
</ChatMessage>, | ||
); | ||
const message = getByText('Can you help me with the docs?'); | ||
fireEvent.click(message); | ||
expect(onClick).toHaveBeenCalled(); | ||
}); | ||
it('should render loading message correctly when loadingText is passed as prop and children is Card component', () => { | ||
const { getByText } = renderWithSSR( | ||
<ChatMessage | ||
senderType="other" | ||
leading={<RayIcon size="xlarge" color="surface.icon.onSea.onSubtle" />} | ||
isLoading={true} | ||
loadingText="Analyzing your response..." | ||
> | ||
<Card> | ||
<CardBody> | ||
<Box display="flex" gap="8px" flexDirection="column"> | ||
<Text variant="body" size="medium"> | ||
Where do you want to collect payments? | ||
</Text> | ||
<RadioGroup> | ||
<Radio value="website">Website</Radio> | ||
<Radio value="android">Android App</Radio> | ||
<Radio value="ios">iOS App</Radio> | ||
</RadioGroup> | ||
</Box> | ||
</CardBody> | ||
</Card> | ||
</ChatMessage>, | ||
); | ||
const loadingTextElement = getByText('Analyzing your response...'); | ||
expect(loadingTextElement).toBeInTheDocument(); | ||
}); | ||
it('should render footer actions correctly when footerActions prop is passed', () => { | ||
const { getByText } = renderWithSSR( | ||
<ChatMessage | ||
senderType="other" | ||
footerActions={ | ||
<Box> | ||
<Box key={1}>Action 1</Box>,<Box key={2}>Action 2</Box>,<Box key={3}>Action 3</Box>, | ||
</Box> | ||
} | ||
> | ||
Can you help me with the docs? | ||
</ChatMessage>, | ||
); | ||
const action1 = getByText('Action 1'); | ||
const action2 = getByText('Action 2'); | ||
const action3 = getByText('Action 3'); | ||
expect(action1).toBeInTheDocument(); | ||
expect(action2).toBeInTheDocument(); | ||
expect(action3).toBeInTheDocument(); | ||
}); | ||
}); |
Oops, something went wrong.