From eb56719ba1378a679841de4ae4dbfdcc64ae455a Mon Sep 17 00:00:00 2001 From: Ahyoung Ryu Date: Thu, 13 Jun 2024 14:58:11 +0900 Subject: [PATCH] [CLNP-3692] Apply postcss-rtlcss plugin for RTL support (#1134) Applied [postcss-rtlcss plugin](https://github.com/elchininet/postcss-rtlcss) for the better RTL support without tedious manual CSS style modifications. - [x] Added plugin configuration to storybook/main.ts - Allows testing of RTL support during development. - [x] Added plugin configuration to rollup.config.js - Ensures build artifacts support RTL languages. - [x] Created useHTMLTextDirection custom hook - Applies the dir attribute to modal and dropdown portal components for correct RTL rendering. postcss-rtlcss automates the conversion of LTR CSS to RTL, ensuring consistency and reducing manual errors. It automates CSS transformation by converting LTR styles to RTL, ensuring proper alignment and display in RTL mode. You can play around in here https://elchininet.github.io/postcss-rtlcss/ to see what kind of modifications could be made by this plugin. --- .storybook/main.ts | 9 ++++ apps/testing/vite.config.ts | 6 +++ package.json | 1 + rollup.config.mjs | 3 +- src/modules/App/AppLayout.tsx | 5 ++- src/modules/App/DesktopLayout.tsx | 4 +- src/modules/App/MobileLayout.tsx | 4 +- src/modules/App/const.ts | 1 + .../App/hooks/useApplyTextDirection.ts | 35 ++++++++++++++++ src/ui/ContextMenu/MenuItems.tsx | 4 +- src/ui/ContextMenu/index.scss | 1 - src/ui/ContextMenu/index.tsx | 1 - src/ui/FileViewer/index.scss | 3 ++ src/ui/Toggle/index.scss | 41 ++++++++++++------- yarn.lock | 28 ++++++++++++- 15 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 src/modules/App/const.ts create mode 100644 src/modules/App/hooks/useApplyTextDirection.ts 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"