Skip to content

Commit 031812e

Browse files
committed
Add RTL support and useRTL hook to React package
Introduces a new useRTL hook for detecting right-to-left (RTL) text direction and updates components and styles to use logical CSS properties for RTL compatibility. Adds tests for the useRTL hook and RTL support in BaseOrganizationSwitcher. Updates exports to include useRTL.
1 parent 0a1610d commit 031812e

File tree

10 files changed

+394
-12
lines changed

10 files changed

+394
-12
lines changed

packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
122122

123123
const manageButton = css`
124124
min-width: auto;
125-
margin-left: auto;
125+
margin-inline-start: auto;
126126
`;
127127

128128
const menu = css`
@@ -144,7 +144,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
144144
background-color: transparent;
145145
cursor: pointer;
146146
font-size: 0.875rem;
147-
text-align: left;
147+
text-align: start;
148148
border-radius: ${theme.vars.borderRadius.medium};
149149
transition: background-color 0.15s ease-in-out;
150150
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import {render, screen, waitFor} from '@testing-library/react';
20+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
21+
import {BaseOrganizationSwitcher, Organization} from './BaseOrganizationSwitcher';
22+
import React from 'react';
23+
24+
// Mock the dependencies
25+
vi.mock('../../../contexts/Theme/useTheme', () => ({
26+
default: () => ({
27+
theme: {
28+
vars: {
29+
colors: {
30+
text: {primary: '#000', secondary: '#666'},
31+
background: {surface: '#fff'},
32+
border: '#ccc',
33+
action: {hover: '#f0f0f0'},
34+
},
35+
spacing: {unit: '8px'},
36+
borderRadius: {medium: '4px', large: '8px'},
37+
shadows: {medium: '0 2px 4px rgba(0,0,0,0.1)'},
38+
},
39+
},
40+
colorScheme: 'light',
41+
}),
42+
}));
43+
44+
vi.mock('../../../hooks/useTranslation', () => ({
45+
default: () => ({
46+
t: (key: string) => key,
47+
currentLanguage: 'en',
48+
setLanguage: vi.fn(),
49+
availableLanguages: ['en'],
50+
}),
51+
}));
52+
53+
const mockOrganizations: Organization[] = [
54+
{
55+
id: '1',
56+
name: 'Organization 1',
57+
avatar: 'https://example.com/avatar1.jpg',
58+
memberCount: 10,
59+
role: 'admin',
60+
},
61+
{
62+
id: '2',
63+
name: 'Organization 2',
64+
avatar: 'https://example.com/avatar2.jpg',
65+
memberCount: 5,
66+
role: 'member',
67+
},
68+
];
69+
70+
describe('BaseOrganizationSwitcher RTL Support', () => {
71+
beforeEach(() => {
72+
document.documentElement.removeAttribute('dir');
73+
});
74+
75+
afterEach(() => {
76+
document.documentElement.removeAttribute('dir');
77+
});
78+
79+
it('should render correctly in LTR mode', () => {
80+
document.documentElement.setAttribute('dir', 'ltr');
81+
const handleSwitch = vi.fn();
82+
83+
render(
84+
<BaseOrganizationSwitcher
85+
organizations={mockOrganizations}
86+
currentOrganization={mockOrganizations[0]}
87+
onOrganizationSwitch={handleSwitch}
88+
/>,
89+
);
90+
91+
expect(screen.getByText('Organization 1')).toBeInTheDocument();
92+
});
93+
94+
it('should render correctly in RTL mode', () => {
95+
document.documentElement.setAttribute('dir', 'rtl');
96+
const handleSwitch = vi.fn();
97+
98+
render(
99+
<BaseOrganizationSwitcher
100+
organizations={mockOrganizations}
101+
currentOrganization={mockOrganizations[0]}
102+
onOrganizationSwitch={handleSwitch}
103+
/>,
104+
);
105+
106+
expect(screen.getByText('Organization 1')).toBeInTheDocument();
107+
});
108+
109+
it('should flip chevron icon in RTL mode', async () => {
110+
document.documentElement.setAttribute('dir', 'rtl');
111+
const handleSwitch = vi.fn();
112+
113+
const {container} = render(
114+
<BaseOrganizationSwitcher
115+
organizations={mockOrganizations}
116+
currentOrganization={mockOrganizations[0]}
117+
onOrganizationSwitch={handleSwitch}
118+
/>,
119+
);
120+
121+
await waitFor(() => {
122+
const chevronIcon = container.querySelector('svg');
123+
expect(chevronIcon).toBeTruthy();
124+
if (chevronIcon) {
125+
const style = window.getComputedStyle(chevronIcon);
126+
// In RTL mode, the transform should be scaleX(-1)
127+
expect(chevronIcon.style.transform).toContain('scaleX(-1)');
128+
}
129+
});
130+
});
131+
132+
it('should not flip chevron icon in LTR mode', async () => {
133+
document.documentElement.setAttribute('dir', 'ltr');
134+
const handleSwitch = vi.fn();
135+
136+
const {container} = render(
137+
<BaseOrganizationSwitcher
138+
organizations={mockOrganizations}
139+
currentOrganization={mockOrganizations[0]}
140+
onOrganizationSwitch={handleSwitch}
141+
/>,
142+
);
143+
144+
await waitFor(() => {
145+
const chevronIcon = container.querySelector('svg');
146+
expect(chevronIcon).toBeTruthy();
147+
if (chevronIcon) {
148+
// In LTR mode, the transform should be none
149+
expect(chevronIcon.style.transform).toBe('none');
150+
}
151+
});
152+
});
153+
154+
it('should update icon flip when direction changes', async () => {
155+
document.documentElement.setAttribute('dir', 'ltr');
156+
const handleSwitch = vi.fn();
157+
158+
const {container, rerender} = render(
159+
<BaseOrganizationSwitcher
160+
organizations={mockOrganizations}
161+
currentOrganization={mockOrganizations[0]}
162+
onOrganizationSwitch={handleSwitch}
163+
/>,
164+
);
165+
166+
// Initially LTR
167+
let chevronIcon = container.querySelector('svg');
168+
expect(chevronIcon?.style.transform).toBe('none');
169+
170+
// Change to RTL
171+
document.documentElement.setAttribute('dir', 'rtl');
172+
173+
// Force re-render
174+
rerender(
175+
<BaseOrganizationSwitcher
176+
organizations={mockOrganizations}
177+
currentOrganization={mockOrganizations[0]}
178+
onOrganizationSwitch={handleSwitch}
179+
/>,
180+
);
181+
182+
await waitFor(() => {
183+
chevronIcon = container.querySelector('svg');
184+
expect(chevronIcon?.style.transform).toContain('scaleX(-1)');
185+
});
186+
});
187+
});

packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {cx} from '@emotion/css';
3434
import {FC, ReactElement, ReactNode, useState} from 'react';
3535
import useTheme from '../../../contexts/Theme/useTheme';
3636
import useTranslation from '../../../hooks/useTranslation';
37+
import useRTL from '../../../hooks/useRTL';
3738
import {Avatar} from '../../primitives/Avatar/Avatar';
3839
import Button from '../../primitives/Button/Button';
3940
import Building from '../../primitives/Icons/Building';
@@ -191,6 +192,7 @@ export const BaseOrganizationSwitcher: FC<BaseOrganizationSwitcherProps> = ({
191192
const [isOpen, setIsOpen] = useState(false);
192193
const [hoveredItemIndex, setHoveredItemIndex] = useState<number | null>(null);
193194
const {t} = useTranslation();
195+
const {isRTL} = useRTL();
194196

195197
const {refs, floatingStyles, context} = useFloating({
196198
open: isOpen,
@@ -308,7 +310,7 @@ export const BaseOrganizationSwitcher: FC<BaseOrganizationSwitcherProps> = ({
308310
)}
309311
</>
310312
)}
311-
<ChevronDown width="16" height="16" />
313+
<ChevronDown width="16" height="16" style={{transform: isRTL ? 'scaleX(-1)' : 'none'}} />
312314
</Button>
313315

314316
{isOpen && (

packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
9595
border: none;
9696
cursor: pointer;
9797
font-size: 0.875rem;
98-
text-align: left;
98+
text-align: start;
9999
border-radius: ${theme.vars.borderRadius.medium};
100100
transition: none;
101101
box-shadow: none;
@@ -125,7 +125,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
125125
background: none;
126126
cursor: pointer;
127127
font-size: 0.875rem;
128-
text-align: left;
128+
text-align: start;
129129
border-radius: ${theme.vars.borderRadius.medium};
130130
transition: background-color 0.15s ease-in-out;
131131

packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
5555
display: flex;
5656
gap: calc(${theme.vars.spacing.unit} / 2);
5757
align-items: center;
58-
margin-left: calc(${theme.vars.spacing.unit} * 4);
58+
margin-inline-start: calc(${theme.vars.spacing.unit} * 4);
5959
`;
6060

6161
const complexTextarea = css`
@@ -135,7 +135,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
135135
width: 120px;
136136
flex-shrink: 0;
137137
line-height: 28px;
138-
text-align: left;
138+
text-align: start;
139139
`;
140140

141141
const value = css`
@@ -151,7 +151,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
151151
text-overflow: ellipsis;
152152
white-space: nowrap;
153153
max-width: 350px;
154-
text-align: left;
154+
text-align: start;
155155
156156
.${withVendorCSSClassPrefix('form-control')} {
157157
margin-bottom: 0;

packages/react/src/components/primitives/Checkbox/Checkbox.styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const useStyles = (theme: Theme, colorScheme: string, hasError: boolean, require
3838
const inputStyles = css`
3939
width: calc(${theme.vars.spacing.unit} * 2.5);
4040
height: calc(${theme.vars.spacing.unit} * 2.5);
41-
margin-right: ${theme.vars.spacing.unit};
41+
margin-inline-end: ${theme.vars.spacing.unit};
4242
accent-color: ${theme.vars.colors.primary.main};
4343
cursor: pointer;
4444

packages/react/src/components/primitives/FormControl/FormControl.styles.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ const useStyles = (
4040
) => {
4141
return useMemo(() => {
4242
const formControl = css`
43-
text-align: left;
43+
text-align: start;
4444
margin-bottom: calc(${theme.vars.spacing.unit} * 2);
4545
`;
4646

4747
const helperText = css`
4848
margin-top: calc(${theme.vars.spacing.unit} / 2);
49-
text-align: ${helperTextAlign};
50-
${helperTextMarginLeft && `margin-left: ${helperTextMarginLeft};`}
49+
text-align: ${helperTextAlign === 'left' ? 'start' : helperTextAlign};
50+
${helperTextMarginLeft && `margin-inline-start: ${helperTextMarginLeft};`}
5151
`;
5252

5353
const helperTextError = css`

0 commit comments

Comments
 (0)