diff --git a/packages/uikit b/packages/uikit index 4fc061947..31e3110b0 160000 --- a/packages/uikit +++ b/packages/uikit @@ -1 +1 @@ -Subproject commit 4fc0619472906ca5a0a3a8992650ca8bcfdf54c0 +Subproject commit 31e3110b0ebd59ecae36899353f44406a016952c diff --git a/src/components/BotMessageWithBodyInput.tsx b/src/components/BotMessageWithBodyInput.tsx index 4a396247e..3666722ec 100644 --- a/src/components/BotMessageWithBodyInput.tsx +++ b/src/components/BotMessageWithBodyInput.tsx @@ -18,7 +18,7 @@ const Root = styled.span` const Sender = styled.div` padding: 0 0 4px 12px; - text-align: left; + text-align: start; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/src/components/CustomMessageBody.tsx b/src/components/CustomMessageBody.tsx index 3dba5db22..8fd350931 100644 --- a/src/components/CustomMessageBody.tsx +++ b/src/components/CustomMessageBody.tsx @@ -8,7 +8,7 @@ const Root = styled.div` const Text = styled.span` width: 100%; - text-align: left; + text-align: start; white-space: pre-line; word-break: break-word; line-height: 1.43; diff --git a/src/components/MessageDataContent.tsx b/src/components/MessageDataContent.tsx index f61a17264..a7c822b65 100644 --- a/src/components/MessageDataContent.tsx +++ b/src/components/MessageDataContent.tsx @@ -84,14 +84,14 @@ const Root = styled.div` display: flex; justify-content: flex-start; margin-top: 16px; - padding-left: 36px; + padding-inline-start: 36px; `; const SideBar = styled.div` width: 4px; border-radius: 100px; background-color: ${({ theme }) => theme.bgColor.messageDataContent.sidebar}; - margin-left: 8px; + margin-inline-start: 8px; `; const DataContainer = styled.div` @@ -100,7 +100,7 @@ const DataContainer = styled.div` justify-content: flex-start; align-items: flex-start; gap: 4px; - margin-left: 16px; + margin-inline-start: 16px; flex: 1; // Without this, Sidebar width is reduced. color: ${({ theme }) => theme.textColor.messageDataContent.default}; `; diff --git a/src/components/TokensBody.tsx b/src/components/TokensBody.tsx index 8c5838bd4..c77ccc9fb 100644 --- a/src/components/TokensBody.tsx +++ b/src/components/TokensBody.tsx @@ -34,7 +34,7 @@ const MultipleTokenTypeContainer = styled.div` export const TextContainer = styled.div` width: inherit; - text-align: left; + text-align: start; word-break: break-word; padding: 8px 12px; gap: 12px; diff --git a/src/components/UserMessageWithBodyInput.tsx b/src/components/UserMessageWithBodyInput.tsx index 726bf56a7..e1e20741b 100644 --- a/src/components/UserMessageWithBodyInput.tsx +++ b/src/components/UserMessageWithBodyInput.tsx @@ -21,7 +21,7 @@ const Root = styled.div` const Sender = styled(Label)` margin: 0 0 4px 12px; - text-align: left; + text-align: start; `; interface BodyContainerProps { diff --git a/src/components/chat/ui/ChatHeader.tsx b/src/components/chat/ui/ChatHeader.tsx index aa405f01c..e09d4c98b 100644 --- a/src/components/chat/ui/ChatHeader.tsx +++ b/src/components/chat/ui/ChatHeader.tsx @@ -41,7 +41,7 @@ export const ChatHeader = ({ fullscreen }: Props) => { return (
-
+
diff --git a/src/components/chat/ui/ChatInput.tsx b/src/components/chat/ui/ChatInput.tsx index df1062d55..d3ffe7151 100644 --- a/src/components/chat/ui/ChatInput.tsx +++ b/src/components/chat/ui/ChatInput.tsx @@ -116,6 +116,9 @@ const container = css` right: unset; bottom: unset; background-color: transparent; + [dir='rtl'] & { + transform: scaleX(-1); + } } .sendbird-message-input--attach { right: unset; diff --git a/src/components/chat/ui/ChatMessageList.tsx b/src/components/chat/ui/ChatMessageList.tsx index 3db73c2d6..fd05608e3 100644 --- a/src/components/chat/ui/ChatMessageList.tsx +++ b/src/components/chat/ui/ChatMessageList.tsx @@ -149,5 +149,5 @@ const dateSeparatorMargin = css` const scrollBottomPosition = css` position: absolute; bottom: 20px; - right: 20px; + inset-inline-end: 20px; `; diff --git a/src/components/messages/CarouselMessage.tsx b/src/components/messages/CarouselMessage.tsx index baa294e46..7b78c85a6 100644 --- a/src/components/messages/CarouselMessage.tsx +++ b/src/components/messages/CarouselMessage.tsx @@ -11,7 +11,7 @@ import { SnapCarousel } from '../ui/SnapCarousel'; const listPadding = 16; const avatarSize = 28; const avatarMargin = 8; -const leftMargin = avatarSize + avatarMargin + listPadding; +const startMargin = avatarSize + avatarMargin + listPadding; const BodyWrapper = styled.div({ display: 'flex', @@ -49,7 +49,7 @@ const Image = styled.img` background-color: ${({ theme }) => theme.bgColor.carouselItem}; `; -const Button = styled.button<{ direction: 'left' | 'right' }>(({ theme, direction }) => ({ +const Button = styled.button<{ direction: 'start' | 'end' }>(({ theme, direction }) => ({ display: 'flex', justifyContent: 'center', alignItems: 'center', @@ -58,14 +58,23 @@ const Button = styled.button<{ direction: 'left' | 'right' }>(({ theme, directio transform: 'translateY(-50%)', border: 'none', cursor: 'pointer', - borderRadius: direction === 'right' ? '100px 0px 0px 100px' : '0px 100px 100px 0px', - padding: direction === 'right' ? '8px 8px 8px 12px' : '8px 12px 8px 8px', + borderStartStartRadius: direction === 'start' ? 0 : 100, + borderStartEndRadius: direction === 'end' ? 0 : 100, + borderEndStartRadius: direction === 'start' ? 0 : 100, + borderEndEndRadius: direction === 'end' ? 0 : 100, + paddingTop: 8, + paddingBottom: 8, + paddingInlineStart: direction === 'end' ? 12 : 8, + paddingInlineEnd: direction === 'start' ? 12 : 8, backgroundColor: theme.bgColor.carouselButton, boxShadow: '0px 8px 10px 1px rgba(13, 13, 13, 0.12), 0px 3px 14px 2px rgba(13, 13, 13, 0.08), 0px 3px 5px -3px rgba(13, 13, 13, 0.04)', '&:hover': { backgroundColor: theme.bgColor.hover.carouselButton, }, + '[dir=rtl] & svg': { + transform: 'scaleX(-1)', + }, })); type Props = { @@ -85,20 +94,20 @@ export const CarouselMessage = ({ streaming, textBody, streamingBody, items }: P return ( shouldRenderButtons && ( <> {activeIndex !== 0 && ( - )} {activeIndex !== items.length - 1 && ( - )} diff --git a/src/components/FormInput.tsx b/src/components/messages/FormMessage/FormInput.tsx similarity index 98% rename from src/components/FormInput.tsx rename to src/components/messages/FormMessage/FormInput.tsx index 8d82a6a87..78ab63af9 100644 --- a/src/components/FormInput.tsx +++ b/src/components/messages/FormMessage/FormInput.tsx @@ -2,8 +2,8 @@ import { MessageFormItemStyle } from '@sendbird/chat/message'; import { ReactElement, ReactNode } from 'react'; import styled from 'styled-components'; -import { Icon } from '../foundation/components/Icon'; -import { Label as UILabel } from '../foundation/components/Label'; +import { Icon } from '../../../foundation/components/Icon'; +import { Label as UILabel } from '../../../foundation/components/Label'; export interface InputLabelProps { children: ReactNode; @@ -17,7 +17,7 @@ const Label = styled(UILabel)` export const InputLabel = ({ children }: InputLabelProps): ReactElement => (
-
@@ -79,7 +79,7 @@ const Placeholder = styled.div` position: absolute; pointer-events: none; top: 8px; - left: 13px; + inset-inline-start: 13px; font-size: 14px; line-height: 1.43; color: ${({ theme }) => theme.textColor.placeholder}; @@ -261,7 +261,7 @@ const CheckIconContainer = styled.div` `; const CheckIconForChip = styled(Icon)` - margin-left: 4px; + margin-inline-start: 4px; `; const InputContainer = styled.div` diff --git a/src/components/messages/FormMessage.tsx b/src/components/messages/FormMessage/index.tsx similarity index 94% rename from src/components/messages/FormMessage.tsx rename to src/components/messages/FormMessage/index.tsx index c0cbbb67f..35b9e0e85 100644 --- a/src/components/messages/FormMessage.tsx +++ b/src/components/messages/FormMessage/index.tsx @@ -5,12 +5,12 @@ import styled from 'styled-components'; import { isFormVersionCompatible } from '@uikit/modules/GroupChannel/context/utils'; import Button from '@uikit/ui/Button'; -import FallbackUserMessage from './FallbackUserMessage'; -import { widgetStringSet } from '../../const'; -import { useConstantState } from '../../context/ConstantContext'; -import { Label } from '../../foundation/components/Label'; -import FormInput from '../FormInput'; -import { AlertModal } from '../ui/AlertModal'; +import FallbackUserMessage from './../FallbackUserMessage'; +import FormInput from './FormInput'; +import { widgetStringSet } from '../../../const'; +import { useConstantState } from '../../../context/ConstantContext'; +import { Label } from '../../../foundation/components/Label'; +import { AlertModal } from '../../ui/AlertModal'; interface Props { message: BaseMessage; diff --git a/src/components/ui/CodeBlock.tsx b/src/components/ui/CodeBlock.tsx index 833bb5919..949db652a 100644 --- a/src/components/ui/CodeBlock.tsx +++ b/src/components/ui/CodeBlock.tsx @@ -22,8 +22,8 @@ const Line = styled.div` const LineNumber = styled.span` display: table-cell; - text-align: right; - padding-right: 10px; + text-align: end; + padding-inline-end: 10px; user-select: none; opacity: 0.5; `; @@ -35,7 +35,7 @@ const LineContent = styled.span` const CopyButton = styled.button` position: absolute; top: 8px; - right: 12px; + inset-inline-end: 12px; display: flex; flex-wrap: wrap; justify-content: center; diff --git a/src/components/ui/PoweredByBanner.tsx b/src/components/ui/PoweredByBanner.tsx index fdea2a311..61acbe782 100644 --- a/src/components/ui/PoweredByBanner.tsx +++ b/src/components/ui/PoweredByBanner.tsx @@ -48,7 +48,7 @@ function Banner() { const { chatBottomContent } = useConstantState(); return ( - + {chatBottomContent?.text}   Powered by  { const ref = useRef(null); const [activeIndex, setActiveState] = useState(0); - const itemLength = React.Children.toArray(children).length; + const direction = (ref.current ? getComputedStyle(ref.current).direction : 'ltr') as 'rtl' | 'ltr'; + const itemLength = React.Children.toArray(children).length; const itemWidth = useMemo(() => { const total = ref.current?.scrollWidth ?? 0; return (total - (startPadding + endPadding + gap * (itemLength - 1))) / itemLength; }, [ref.current?.scrollWidth, itemLength, gap, startPadding, endPadding]); const onScroll = (e: React.UIEvent) => { - const idx = Math.round(e.currentTarget.scrollLeft / itemWidth); + const idx = Math.round(e.currentTarget.scrollLeft / itemWidth) * (direction === 'ltr' ? 1 : -1); if (idx !== activeIndex) setActiveState(idx); }; const scrollTo = (index: number) => { if (ref.current) { const nextIdx = Math.min(Math.max(0, index), itemLength - 1); - ref.current.scroll({ - left: nextIdx * itemWidth, - behavior: 'smooth', - }); + ref.current.scroll({ left: nextIdx * itemWidth * (direction === 'ltr' ? 1 : -1), behavior: 'smooth' }); } }; @@ -88,8 +86,8 @@ export const SnapCarousel = ({ style={{ gap, scrollPadding: startPadding, - paddingLeft: startPadding, - paddingRight: endPadding, + paddingInlineStart: startPadding, + paddingInlineEnd: endPadding, ...style, }} > diff --git a/src/components/ui/WidgetButton.tsx b/src/components/ui/WidgetButton.tsx index 819a5e9f2..07ebf17f1 100644 --- a/src/components/ui/WidgetButton.tsx +++ b/src/components/ui/WidgetButton.tsx @@ -117,6 +117,7 @@ export interface WidgetButtonProps { onClick?: () => void; className?: string; animated?: boolean; + dir?: 'ltr' | 'rtl'; } export const WidgetButton = ({ @@ -126,9 +127,11 @@ export const WidgetButton = ({ onClick, className, animated = true, + dir, }: WidgetButtonProps) => { return ( ` position: fixed; z-index: ${WIDGET_WINDOW_Z_INDEX}; top: 0; - left: 0; + inset-inline-start: 0; width: ${({ width }) => `${width}px`}; height: 100%; overflow: hidden; @@ -39,11 +40,13 @@ const DesktopComponent = () => { const MobileComponent = () => { const { isOpen, isVisible } = useWidgetState(); + const { dir } = useConstantState(); const { width: mobileContainerWidth } = useMobileView(); return ( <> ; export default function WidgetToggleButton() { const { botStyle } = useWidgetSetting(); - const { renderWidgetToggleButton } = useConstantState(); + const { dir, renderWidgetToggleButton } = useConstantState(); const { isOpen, setIsOpen } = useWidgetState(); - const toggleButtonProps: ToggleButtonProps = { + const toggleButtonProps: WidgetButtonProps = { + dir, isOpen, onClick: () => setIsOpen(!isOpen), accentColor: botStyle.accentColor, + imageUrl: botStyle.toggleButtonUrl, }; if (typeof renderWidgetToggleButton === 'function') { return renderWidgetToggleButton(toggleButtonProps); } - return ; + return ; } diff --git a/src/components/widget/WidgetWindow.tsx b/src/components/widget/WidgetWindow.tsx index 7b488c772..31655b9eb 100644 --- a/src/components/widget/WidgetWindow.tsx +++ b/src/components/widget/WidgetWindow.tsx @@ -1,6 +1,7 @@ import styled, { css } from 'styled-components'; import { elementIds, WIDGET_WINDOW_Z_INDEX } from '../../const'; +import { useConstantState } from '../../context/ConstantContext'; import { useWidgetState } from '../../context/WidgetStateContext'; const StyledWidgetWindowWrapper = styled.div<{ @@ -13,7 +14,7 @@ const StyledWidgetWindowWrapper = styled.div<{ -webkit-overflow-scrolling: auto; position: fixed; bottom: 84px; - right: 20px; + inset-inline-end: 20px; height: 640px; min-height: 80px; width: 400px; @@ -25,7 +26,6 @@ const StyledWidgetWindowWrapper = styled.div<{ 0px 6px 10px -5px rgba(33, 33, 33, 0.04); border-radius: 16px; overflow: hidden; - transform-origin: right bottom; transition: width 200ms ease 0s, height 200ms ease 0s, @@ -34,6 +34,11 @@ const StyledWidgetWindowWrapper = styled.div<{ opacity 83ms ease-out 0s; transform: scale(0.15); opacity: 0; + transform-origin: right bottom; + [dir='rtl'] &:not([dir='ltr']), + &[dir='rtl'] { + transform-origin: left bottom; + } ${({ isOpen }) => { return ( @@ -62,10 +67,16 @@ const StyledWidgetWindowWrapper = styled.div<{ `; const WidgetWindow = ({ children }: { children: React.ReactNode }) => { + const { dir } = useConstantState(); const { isVisible, isOpen, isExpanded } = useWidgetState(); return ( - + {children} ); diff --git a/src/const.ts b/src/const.ts index 8aeed23f0..57bce938e 100644 --- a/src/const.ts +++ b/src/const.ts @@ -7,7 +7,7 @@ import React from 'react'; import { StringSet } from '@uikit/ui/Label/stringSet'; -import type { ToggleButtonProps } from './components/widget/WidgetToggleButton'; +import type { WidgetButtonProps } from './components/ui/WidgetButton'; import { BotStyle } from './context/WidgetSettingContext'; import RefreshIcon from './icons/ic-refresh.svg'; import { FunctionCallAdapter, SendbirdChatAICallbacks, WidgetCarouselItem } from './types'; @@ -131,7 +131,7 @@ export interface OnWidgetOpenStateChangeParams { value: boolean; } -export interface Constant extends ConstantFeatureFlags, ConstantAIFeatures { +export interface Constant extends ConstantFeatureFlags, ConstantAIFeatures, ConstantStyles { /** * @public * @description User nickname to be used in the widget. @@ -241,7 +241,7 @@ export interface Constant extends ConstantFeatureFlags, ConstantAIFeatures { * @private * @description Custom widget toggle button renderer. */ - renderWidgetToggleButton?: (props: ToggleButtonProps) => React.ReactElement; + renderWidgetToggleButton?: (props: WidgetButtonProps) => React.ReactElement; /** * @private * @description Service name to be used in the widget. @@ -274,6 +274,14 @@ export interface Constant extends ConstantFeatureFlags, ConstantAIFeatures { localCacheEnabled?: boolean; } +interface ConstantStyles { + /** + * @public + * @description dir of the widget. + * */ + dir?: 'ltr' | 'rtl'; +} + interface ConstantAIFeatures { /** * @public diff --git a/src/context/ConstantContext.tsx b/src/context/ConstantContext.tsx index 757901d7d..5d334f029 100644 --- a/src/context/ConstantContext.tsx +++ b/src/context/ConstantContext.tsx @@ -52,31 +52,19 @@ export const ConstantStateProvider = (props: PropsWithChildren