Skip to content

Commit f43f795

Browse files
authored
[CLNP-4596] feat: add forceLeftToRightMessageLayout to enable LTR message layout display in RTL mode (#1184)
Addresses https://sendbird.atlassian.net/browse/CLNP-4596 This PR addresses the layout issues and CSS [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) improvements for right-to-left (RTL) text direction in the Sendbird UIKit React components. The background for adding the `forceLeftToRightMessageLayout` flag is that some of our users in the middle-east want to set `htmlTextDirection='rtl'` while keeping the message layout in left-to-right (LTR) format (outgoing messages on the right, incoming messages on the left). ### Todo - [x] The postcssRtl plugin options were modified. - [x] Layout adjustments were made to ensure proper alignment and behavior.
1 parent 07915a5 commit f43f795

File tree

18 files changed

+174
-9
lines changed

18 files changed

+174
-9
lines changed

.storybook/main.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { mergeConfig } from 'vite';
22
import svgr from 'vite-plugin-svgr';
33
import { dirname, join } from 'path';
4+
import postcssRTLOptions from "../postcssRtlOptions.mjs";
45

56
/**
67
* This function is used to resolve the absolute path of a package.
@@ -22,9 +23,7 @@ export default {
2223
css: {
2324
postcss: {
2425
plugins: [
25-
require('postcss-rtlcss')({
26-
mode: 'override',
27-
}),
26+
require('postcss-rtlcss')(postcssRTLOptions),
2827
],
2928
},
3029
},

apps/testing/src/utils/paramsBuilder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const useConfigParams = (initParams: InitialParams): ParamsAsProps => {
3030
isMultipleFilesMessageEnabled: parseValue(searchParams.get('enableMultipleFilesMessage')) ?? true,
3131
enableLegacyChannelModules: parseValue(searchParams.get('enableLegacyChannelModules')) ?? false,
3232
htmlTextDirection: parseValue(searchParams.get('htmlTextDirection')) ?? 'ltr',
33+
forceLeftToRightMessageLayout: parseValue(searchParams.get('forceLeftToRightMessageLayout')) ?? false,
3334
uikitOptions: {},
3435
} as ParamsAsProps;
3536

apps/testing/vite.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { defineConfig } from 'vite';
22
import react from '@vitejs/plugin-react';
33
import vitePluginSvgr from 'vite-plugin-svgr';
44
import postcssRtl from "postcss-rtlcss";
5+
// @ts-ignore
6+
import postcssRtlOptions from '../../postcssRtlOptions.mjs';
57

68
// https://vitejs.dev/config/
79
export default defineConfig({
810
plugins: [react(), vitePluginSvgr({ include: '**/*.svg' })],
911
css: {
1012
postcss: {
11-
plugins: [postcssRtl({ mode: 'override' })],
13+
plugins: [postcssRtl(postcssRtlOptions)],
1214
},
1315
},
1416
});

postcssRtlOptions.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
const CLASS_NAMES_TO_CHECK = [
2+
// Normal message list
3+
'sendbird-message-content',
4+
// Thread message list
5+
'sendbird-thread-list-item-content',
6+
'sendbird-parent-message-info'
7+
];
8+
9+
export default {
10+
prefixSelectorTransformer: (prefix, selector) => {
11+
// To increase specificity https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity
12+
// for certain classnames within .sendbird-conversation__messages context.
13+
// This only applies when the root dir is set as 'rtl' but forceLeftToRightMessageLayout is true.
14+
if (CLASS_NAMES_TO_CHECK.some(cls => selector.includes(cls))) {
15+
return `.sendbird-conversation__messages${prefix} ${selector}`;
16+
}
17+
return `${prefix} ${selector}`;
18+
},
19+
}
20+
21+
// Why we're doing this:
22+
// Let's say a root div element has `dir="rtl"` (with `htmlTextDirection={'rtl'}` prop setting).
23+
24+
/*
25+
<div class="sendbird-root" dir="rtl">
26+
...
27+
// Message list
28+
<div class="sendbird-conversation__messages" dir="ltr">
29+
<div class="emoji-container">
30+
<span class="emoji-inner">😀</span>
31+
</div>
32+
</div>
33+
</div>
34+
*/
35+
36+
// Even though the message list element has dir="ltr" attribute,
37+
// some children elements of the message list would have their styles overridden by the root one with the CSS settings below.
38+
// Why? postcss-rtlcss plugin generates the CSS settings in order of rtl -> ltr.
39+
40+
/*
41+
[dir="rtl"] .sendbird-message-content .emoji-container .emoji-inner {
42+
right: -84px; // Specificity (0.6.0)
43+
}
44+
[dir="ltr"] .sendbird-message-content .emoji-container .emoji-inner {
45+
left: -84px; // Specificity (0.6.0)
46+
}
47+
*/
48+
49+
// If both CSS settings have the same specificity, the one generated first (rtl) will be applied,
50+
// which is not the desired result since we want the `.emoji-inner` element to have the ltr setting.
51+
52+
// To increase the specificity of the ltr setting,
53+
// we can directly connect the classname of the element which has `dir='ltr'` to the children's selector in this way:
54+
55+
/*
56+
.sendbird-conversation__messages[dir="ltr"] .sendbird-message-content .emoji-container .emoji-inner {
57+
left: -84px; // Specificity (0.7.0), will be applied
58+
}
59+
60+
[dir="rtl"] .sendbird-message-content .emoji-container .emoji-inner {
61+
right: -84px; // Specificity (0.6.0), will be ignored
62+
}
63+
[dir="ltr"] .sendbird-message-content .emoji-container .emoji-inner {
64+
left: -84px; // Specificity (0.6.0), will be ignored
65+
}
66+
*/

rollup.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import ts2 from "rollup-plugin-typescript2"
1616
import pkg from "./package.json" assert {type: "json"};
1717
import inputs from "./rollup.module-exports.mjs";
1818
import { readFileSync, writeFileSync } from 'fs';
19+
import postcssRTLOptions from "./postcssRtlOptions.mjs";
1920

2021
const APP_VERSION_STRING = "__react_dev_mode__";
2122

@@ -59,7 +60,7 @@ export default {
5960
const result = scss.renderSync({ file: id });
6061
resolvecss({ code: result.css.toString() });
6162
}),
62-
plugins: [autoprefixer, postcssRtl({ mode: 'override' })],
63+
plugins: [autoprefixer, postcssRtl(postcssRTLOptions)],
6364
sourceMap: false,
6465
extract: "dist/index.css",
6566
extensions: [".sass", ".scss", ".css"],

src/hooks/useHTMLTextDirection.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,18 @@ const useHTMLTextDirection = (direction: HTMLTextDirection) => {
1515
}, [direction]);
1616
};
1717

18+
export const useMessageLayoutDirection = (direction: HTMLTextDirection, forceLeftToRightMessageLayout: boolean, loading: boolean) => {
19+
useEffect(() => {
20+
if (loading) return;
21+
const messageListElements = document.getElementsByClassName('sendbird-conversation__messages');
22+
if (messageListElements.length > 0) {
23+
Array.from(messageListElements).forEach((elem: HTMLElement) => {
24+
elem.dir = forceLeftToRightMessageLayout
25+
? 'ltr'
26+
: direction;
27+
});
28+
}
29+
}, [direction, forceLeftToRightMessageLayout, loading]);
30+
};
31+
1832
export default useHTMLTextDirection;

src/lib/Sendbird.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export interface SendbirdProviderProps extends CommonUIKitConfigProps, React.Pro
103103
disableMarkAsDelivered?: boolean;
104104
breakpoint?: string | boolean;
105105
htmlTextDirection?: HTMLTextDirection;
106+
forceLeftToRightMessageLayout?: boolean;
106107
renderUserProfile?: (props: RenderUserProfileProps) => React.ReactElement;
107108
onUserProfileMessage?: (channel: GroupChannel) => void;
108109
uikitOptions?: UIKitOptions;
@@ -178,6 +179,7 @@ const SendbirdSDK = ({
178179
isMultipleFilesMessageEnabled = false,
179180
eventHandlers,
180181
htmlTextDirection = 'ltr',
182+
forceLeftToRightMessageLayout = false,
181183
}: SendbirdProviderProps): React.ReactElement => {
182184
const { logLevel = '', userMention = {}, isREMUnitEnabled = false, pubSub: customPubSub } = config;
183185
const { isMobile } = useMediaQueryContext();
@@ -400,6 +402,7 @@ const SendbirdSDK = ({
400402
isMessageReceiptStatusEnabledOnChannelList: configs.groupChannel.channelList.enableMessageReceiptStatus,
401403
showSearchIcon: sdkInitialized && configsWithAppAttr(sdk).groupChannel.setting.enableMessageSearch,
402404
htmlTextDirection,
405+
forceLeftToRightMessageLayout,
403406
},
404407
eventHandlers,
405408
emojiManager,

src/lib/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
@import '../styles/light-theme';
44
@import '../styles/dark-theme';
55
@import '../styles/misc-colors';
6+
@import '../styles/postcss-rtl';

src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface SendBirdStateConfig {
7373
accessToken?: string;
7474
theme: string;
7575
htmlTextDirection: HTMLTextDirection;
76+
forceLeftToRightMessageLayout: boolean;
7677
pubSub: SBUGlobalPubSub;
7778
logger: Logger;
7879
setCurrentTheme: (theme: 'light' | 'dark') => void;

src/modules/App/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface AppProps {
4545
disableAutoSelect?: AppLayoutProps['disableAutoSelect'];
4646
onProfileEditSuccess?: AppLayoutProps['onProfileEditSuccess'];
4747
htmlTextDirection?: AppLayoutProps['htmlTextDirection'];
48+
forceLeftToRightMessageLayout?: AppLayoutProps['forceLeftToRightMessageLayout'];
4849

4950
/**
5051
* The default value is false.
@@ -102,6 +103,7 @@ export default function App(props: AppProps) {
102103
enableLegacyChannelModules = false,
103104
uikitOptions,
104105
htmlTextDirection = 'ltr',
106+
forceLeftToRightMessageLayout = false,
105107
// The below configs are duplicates of the Dashboard UIKit Configs.
106108
// Since their default values will be set in the Sendbird component,
107109
// we don't need to set them here.
@@ -158,6 +160,7 @@ export default function App(props: AppProps) {
158160
isMentionEnabled={isMentionEnabled}
159161
isVoiceMessageEnabled={isVoiceMessageEnabled}
160162
htmlTextDirection={htmlTextDirection}
163+
forceLeftToRightMessageLayout={forceLeftToRightMessageLayout}
161164
>
162165
<AppLayout
163166
isMessageGroupingEnabled={isMessageGroupingEnabled}

0 commit comments

Comments
 (0)