diff --git a/.storybook/main.ts b/.storybook/main.ts index 363c910fb0..61446090a5 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -19,6 +19,15 @@ export default { }, viteFinal: (config) => { return mergeConfig(config, { + css: { + postcss: { + plugins: [ + require('postcss-rtlcss')({ + mode: 'override', + }), + ], + }, + }, plugins: [svgr({ include: '**/*.svg' })], }); }, diff --git a/apps/testing/vite.config.ts b/apps/testing/vite.config.ts index 171c92915b..9a68d84e75 100644 --- a/apps/testing/vite.config.ts +++ b/apps/testing/vite.config.ts @@ -1,8 +1,14 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import vitePluginSvgr from 'vite-plugin-svgr'; +import postcssRtl from "postcss-rtlcss"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), vitePluginSvgr({ include: '**/*.svg' })], + css: { + postcss: { + plugins: [postcssRtl({ mode: 'override' })], + }, + }, }); diff --git a/package.json b/package.json index 5c06f687ed..e19b73ccf3 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "jsdom": "^20.0.0", "plop": "^2.5.3", "postcss": "^8.3.5", + "postcss-rtlcss": "^5.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", "rollup": "^4.9.2", diff --git a/rollup.config.mjs b/rollup.config.mjs index 494a9adcd7..cf528b735f 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -6,6 +6,7 @@ import scss from "rollup-plugin-scss"; import postcss from "rollup-plugin-postcss"; import replace from "@rollup/plugin-replace"; import autoprefixer from "autoprefixer"; +import postcssRtl from "postcss-rtlcss"; import copy from "rollup-plugin-copy"; import nodePolyfills from "rollup-plugin-polyfill-node"; import {visualizer} from "rollup-plugin-visualizer"; @@ -57,7 +58,7 @@ export default { const result = scss.renderSync({ file: id }); resolvecss({ code: result.css.toString() }); }), - plugins: [autoprefixer], + plugins: [autoprefixer, postcssRtl({ mode: 'override' })], sourceMap: false, extract: "dist/index.css", extensions: [".sass", ".scss", ".css"], diff --git a/src/modules/App/AppLayout.tsx b/src/modules/App/AppLayout.tsx index 941baca4ac..144249e551 100644 --- a/src/modules/App/AppLayout.tsx +++ b/src/modules/App/AppLayout.tsx @@ -9,6 +9,7 @@ import { MobileLayout } from './MobileLayout'; import useSendbirdStateContext from '../../hooks/useSendbirdStateContext'; import { SendableMessageType } from '../../utils'; import { getCaseResolvedReplyType } from '../../lib/utils/resolvedReplyType'; +import useApplyTextDirection from './hooks/useApplyTextDirection'; export const AppLayout = (props: AppLayoutProps) => { const { @@ -33,6 +34,8 @@ export const AppLayout = (props: AppLayoutProps) => { const [startingPoint, setStartingPoint] = useState(null); const { isMobile } = useMediaQueryContext(); + useApplyTextDirection(htmlTextDirection); + /** * Below configs can be set via Dashboard UIKit config setting but as a lower priority than App props. * So need to be have fallback value \w global configs even though each prop values are undefined @@ -62,7 +65,6 @@ export const AppLayout = (props: AppLayoutProps) => { threadTargetMessage={threadTargetMessage} setThreadTargetMessage={setThreadTargetMessage} enableLegacyChannelModules={enableLegacyChannelModules} - htmlTextDirection={htmlTextDirection} /> ) : ( @@ -89,7 +91,6 @@ export const AppLayout = (props: AppLayoutProps) => { startingPoint={startingPoint} setStartingPoint={setStartingPoint} enableLegacyChannelModules={enableLegacyChannelModules} - htmlTextDirection={htmlTextDirection} /> ) } diff --git a/src/modules/App/DesktopLayout.tsx b/src/modules/App/DesktopLayout.tsx index b869f286be..411055d1db 100644 --- a/src/modules/App/DesktopLayout.tsx +++ b/src/modules/App/DesktopLayout.tsx @@ -13,6 +13,7 @@ import MessageSearchPannel from '../MessageSearch'; import Thread from '../Thread'; import { SendableMessageType } from '../../utils'; import { classnames } from '../../utils/utils'; +import { APP_LAYOUT_ROOT } from './const'; export const DesktopLayout: React.FC = (props: DesktopLayoutProps) => { const { @@ -39,7 +40,6 @@ export const DesktopLayout: React.FC = (props: DesktopLayout threadTargetMessage, setThreadTargetMessage, enableLegacyChannelModules, - htmlTextDirection, } = props; const updateFocusedChannel = (channel: GroupChannelClass) => { @@ -110,7 +110,7 @@ export const DesktopLayout: React.FC = (props: DesktopLayout }; return ( -
+
{enableLegacyChannelModules ? : }
diff --git a/src/modules/App/MobileLayout.tsx b/src/modules/App/MobileLayout.tsx index f03bee6fbf..fae2ee3bc9 100644 --- a/src/modules/App/MobileLayout.tsx +++ b/src/modules/App/MobileLayout.tsx @@ -17,6 +17,7 @@ import useSendbirdStateContext from '../../hooks/useSendbirdStateContext'; import uuidv4 from '../../utils/uuid'; import { ALL, useVoicePlayerContext } from '../../hooks/VoicePlayer'; import { SendableMessageType } from '../../utils'; +import { APP_LAYOUT_ROOT } from './const'; enum PANELS { CHANNEL_LIST = 'CHANNEL_LIST', @@ -44,7 +45,6 @@ export const MobileLayout: React.FC = (props: MobileLayoutPro highlightedMessage, setHighlightedMessage, enableLegacyChannelModules, - htmlTextDirection, } = props; const [panel, setPanel] = useState(PANELS.CHANNEL_LIST); @@ -166,7 +166,7 @@ export const MobileLayout: React.FC = (props: MobileLayoutPro }; return ( -
+
{panel === PANELS.CHANNEL_LIST && (
{enableLegacyChannelModules ? : } diff --git a/src/modules/App/const.ts b/src/modules/App/const.ts new file mode 100644 index 0000000000..537f84c030 --- /dev/null +++ b/src/modules/App/const.ts @@ -0,0 +1 @@ +export const APP_LAYOUT_ROOT = 'sendbird-app__layout'; diff --git a/src/modules/App/hooks/useApplyTextDirection.ts b/src/modules/App/hooks/useApplyTextDirection.ts new file mode 100644 index 0000000000..e5717e6e2a --- /dev/null +++ b/src/modules/App/hooks/useApplyTextDirection.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; +import { MODAL_ROOT } from '../../../hooks/useModal'; +import { EMOJI_MENU_ROOT_ID, MENU_ROOT_ID } from '../../../ui/ContextMenu'; +import { HTMLTextDirection } from '../../../types'; +import { APP_LAYOUT_ROOT } from '../const'; + +const ELEMENT_IDS = [ + MODAL_ROOT, + EMOJI_MENU_ROOT_ID, + MENU_ROOT_ID, + APP_LAYOUT_ROOT, +]; + +/** + * This hook sets the direction (dir) attribute for specified elements. + * + * @param {HTMLTextDirection} direction - The direction to set ('ltr' or 'rtl'). + * + * Note: + * This is necessary because elements such as modal, emoji reaction list, and dropdown + * are at the same level as the Sendbird app root element. They need to have the 'dir' + * attribute set explicitly to ensure proper directionality based on the app's language setting. + */ +const useApplyTextDirection = (direction: HTMLTextDirection) => { + useEffect(() => { + ELEMENT_IDS.forEach((id) => { + const element = document.getElementById(id); + if (element) { + element.dir = direction; + } + }); + }, [direction]); +}; + +export default useApplyTextDirection; diff --git a/src/ui/ContextMenu/MenuItems.tsx b/src/ui/ContextMenu/MenuItems.tsx index 05618943d7..2bd324dbee 100644 --- a/src/ui/ContextMenu/MenuItems.tsx +++ b/src/ui/ContextMenu/MenuItems.tsx @@ -1,7 +1,7 @@ import React, { ReactElement } from 'react'; import { createPortal } from 'react-dom'; import { classnames } from '../../utils/utils'; -import { MENU_OBSERVING_CLASS_NAME } from '.'; +import { MENU_OBSERVING_CLASS_NAME, MENU_ROOT_ID } from '.'; interface MenuItemsProps { id?: string; @@ -105,7 +105,7 @@ export default class MenuItems extends React.Component; diff --git a/src/ui/ContextMenu/index.scss b/src/ui/ContextMenu/index.scss index ac5587c975..ab94c477f5 100644 --- a/src/ui/ContextMenu/index.scss +++ b/src/ui/ContextMenu/index.scss @@ -17,7 +17,6 @@ z-index: 99999; position: absolute; top: 100%; - left: 0; min-width: 140px; margin: 0px; padding: 8px 0px; diff --git a/src/ui/ContextMenu/index.tsx b/src/ui/ContextMenu/index.tsx index e9baa92efb..4d42b2d31d 100644 --- a/src/ui/ContextMenu/index.tsx +++ b/src/ui/ContextMenu/index.tsx @@ -69,7 +69,6 @@ export const MenuRoot = (): ReactElement => (
); -// For the test environment export const EMOJI_MENU_ROOT_ID = 'sendbird-emoji-list-portal'; export const EmojiReactionListRoot = (): ReactElement =>
; diff --git a/src/ui/FileViewer/index.scss b/src/ui/FileViewer/index.scss index 4701d574c2..8a8af3a888 100644 --- a/src/ui/FileViewer/index.scss +++ b/src/ui/FileViewer/index.scss @@ -127,6 +127,8 @@ $file-viewer-img-max-width: calc(100% - #{$file-viewer-slide-buttons-side-length top: calc(50% - 16px); } +// Fliping the arrow icons for RTL is not necessary +/*rtl:begin:ignore*/ .sendbird-file-viewer-arrow--left { left: 14px; } @@ -135,3 +137,4 @@ $file-viewer-img-max-width: calc(100% - #{$file-viewer-slide-buttons-side-length right: 14px; transform: rotate(180deg); } +/*rtl:end:ignore*/ diff --git a/src/ui/Toggle/index.scss b/src/ui/Toggle/index.scss index d96f116f02..ce5cd57270 100644 --- a/src/ui/Toggle/index.scss +++ b/src/ui/Toggle/index.scss @@ -4,22 +4,24 @@ position: relative; display: inline-flex; align-items: center; - box-sizing: border-box; cursor: pointer; } + .sendbird-input-toggle-button--checked { @include themed() { background-color: t(primary-3); border: 1px solid t(primary-3); } } + .sendbird-input-toggle-button--unchecked { @include themed() { background-color: t(bg-3); border: 1px solid t(bg-3); } } + .sendbird-input-toggle-button--disabled { cursor: not-allowed; @include themed() { @@ -43,49 +45,58 @@ } /* Manage animation and position by status */ -@keyframes sendbirdMoveToRight { +@keyframes sendbirdMoveToEnd { 0% { - right: 60%; + inset-inline-end: 60%; } 100% { - right: 10%; + inset-inline-end: 10%; } } -@keyframes sendbirdMoveToLeft { + +@keyframes sendbirdMoveToStart { 0% { - right: 10%; + inset-inline-end: 10%; } 100% { - right: 60%; + inset-inline-end: 60%; } } + // normal - animation .sendbird-input-toggle-button--turned-on .sendbird-input-toggle-button__inner-dot { - animation-name: sendbirdMoveToRight; + animation-name: sendbirdMoveToEnd; } + .sendbird-input-toggle-button--turned-off .sendbird-input-toggle-button__inner-dot { - animation-name: sendbirdMoveToLeft; + animation-name: sendbirdMoveToStart; } + // normal - position .sendbird-input-toggle-button--unchecked .sendbird-input-toggle-button__inner-dot { - right: 60%; + inset-inline-end: 60%; } + .sendbird-input-toggle-button--checked .sendbird-input-toggle-button__inner-dot { - right: 10%; + inset-inline-end: 10%; } + .sendbird-input-toggle-button--reversed { // reverse - animation .sendbird-input-toggle-button--turned-on .sendbird-input-toggle-button__inner-dot { - animation-name: sendbirdMoveToLeft; + animation-name: sendbirdMoveToStart; } + .sendbird-input-toggle-button--turned-off .sendbird-input-toggle-button__inner-dot { - animation-name: sendbirdMoveToRight; + animation-name: sendbirdMoveToEnd; } + // reverse - position &.sendbird-input-toggle-button--unchecked .sendbird-input-toggle-button__inner-dot { - right: 10%; + inset-inline-end: 10%; } + &.sendbird-input-toggle-button--checked .sendbird-input-toggle-button__inner-dot { - right: 60%; + inset-inline-end: 60%; } } diff --git a/yarn.lock b/yarn.lock index d78d599a1a..9e8a90b5b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2710,6 +2710,7 @@ __metadata: jsdom: ^20.0.0 plop: ^2.5.3 postcss: ^8.3.5 + postcss-rtlcss: ^5.3.0 react: ^18.2.0 react-dom: ^18.2.0 rollup: ^4.9.2 @@ -12966,6 +12967,17 @@ __metadata: languageName: node linkType: hard +"postcss-rtlcss@npm:^5.3.0": + version: 5.3.0 + resolution: "postcss-rtlcss@npm:5.3.0" + dependencies: + rtlcss: 4.1.1 + peerDependencies: + postcss: ^8.4.21 + checksum: bd9bd09dc3b45b65db83dc8a0189701d1039b712916484bcb6afb7465a8343eafb0c09464c4d9079f016847e699a5724454e75855fb21fe72aa1b2b415f888ac + languageName: node + linkType: hard + "postcss-safe-parser@npm:^4.0.2": version: 4.0.2 resolution: "postcss-safe-parser@npm:4.0.2" @@ -13063,7 +13075,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.3.5, postcss@npm:^8.4.38": +"postcss@npm:^8.3.5, postcss@npm:^8.4.21, postcss@npm:^8.4.38": version: 8.4.38 resolution: "postcss@npm:8.4.38" dependencies: @@ -14043,6 +14055,20 @@ __metadata: languageName: node linkType: hard +"rtlcss@npm:4.1.1": + version: 4.1.1 + resolution: "rtlcss@npm:4.1.1" + dependencies: + escalade: ^3.1.1 + picocolors: ^1.0.0 + postcss: ^8.4.21 + strip-json-comments: ^3.1.1 + bin: + rtlcss: bin/rtlcss.js + checksum: dcf37d76265b5c84d610488afa68a2506d008f95feac968b35ccae9aa49e7019ae0336a80363303f8f8bbf60df3ecdeb60413548b049114a24748319b68aefde + languageName: node + linkType: hard + "run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1"