From 76e0fc616ca26a762a1085534dfaf2065c6b062b Mon Sep 17 00:00:00 2001
From: Tharun Devaraja <154892351+d3varaja@users.noreply.github.com>
Date: Wed, 15 Oct 2025 09:06:58 +0530
Subject: [PATCH 1/3] 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.
---
.../BaseOrganizationSwitcher.styles.ts | 4 +-
.../BaseOrganizationSwitcher.test.tsx | 187 ++++++++++++++++++
.../BaseOrganizationSwitcher.tsx | 4 +-
.../UserDropdown/BaseUserDropdown.styles.ts | 4 +-
.../UserProfile/BaseUserProfile.styles.ts | 6 +-
.../primitives/Checkbox/Checkbox.styles.ts | 2 +-
.../FormControl/FormControl.styles.ts | 6 +-
packages/react/src/hooks/useRTL.test.ts | 97 +++++++++
packages/react/src/hooks/useRTL.ts | 93 +++++++++
packages/react/src/index.ts | 3 +
10 files changed, 394 insertions(+), 12 deletions(-)
create mode 100644 packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx
create mode 100644 packages/react/src/hooks/useRTL.test.ts
create mode 100644 packages/react/src/hooks/useRTL.ts
diff --git a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.styles.ts b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.styles.ts
index c03ccd2da..81b296f16 100644
--- a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.styles.ts
+++ b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.styles.ts
@@ -122,7 +122,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
const manageButton = css`
min-width: auto;
- margin-left: auto;
+ margin-inline-start: auto;
`;
const menu = css`
@@ -144,7 +144,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
background-color: transparent;
cursor: pointer;
font-size: 0.875rem;
- text-align: left;
+ text-align: start;
border-radius: ${theme.vars.borderRadius.medium};
transition: background-color 0.15s ease-in-out;
diff --git a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx
new file mode 100644
index 000000000..e08f59194
--- /dev/null
+++ b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx
@@ -0,0 +1,187 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {render, screen, waitFor} from '@testing-library/react';
+import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
+import {BaseOrganizationSwitcher, Organization} from './BaseOrganizationSwitcher';
+import React from 'react';
+
+// Mock the dependencies
+vi.mock('../../../contexts/Theme/useTheme', () => ({
+ default: () => ({
+ theme: {
+ vars: {
+ colors: {
+ text: {primary: '#000', secondary: '#666'},
+ background: {surface: '#fff'},
+ border: '#ccc',
+ action: {hover: '#f0f0f0'},
+ },
+ spacing: {unit: '8px'},
+ borderRadius: {medium: '4px', large: '8px'},
+ shadows: {medium: '0 2px 4px rgba(0,0,0,0.1)'},
+ },
+ },
+ colorScheme: 'light',
+ }),
+}));
+
+vi.mock('../../../hooks/useTranslation', () => ({
+ default: () => ({
+ t: (key: string) => key,
+ currentLanguage: 'en',
+ setLanguage: vi.fn(),
+ availableLanguages: ['en'],
+ }),
+}));
+
+const mockOrganizations: Organization[] = [
+ {
+ id: '1',
+ name: 'Organization 1',
+ avatar: 'https://example.com/avatar1.jpg',
+ memberCount: 10,
+ role: 'admin',
+ },
+ {
+ id: '2',
+ name: 'Organization 2',
+ avatar: 'https://example.com/avatar2.jpg',
+ memberCount: 5,
+ role: 'member',
+ },
+];
+
+describe('BaseOrganizationSwitcher RTL Support', () => {
+ beforeEach(() => {
+ document.documentElement.removeAttribute('dir');
+ });
+
+ afterEach(() => {
+ document.documentElement.removeAttribute('dir');
+ });
+
+ it('should render correctly in LTR mode', () => {
+ document.documentElement.setAttribute('dir', 'ltr');
+ const handleSwitch = vi.fn();
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText('Organization 1')).toBeInTheDocument();
+ });
+
+ it('should render correctly in RTL mode', () => {
+ document.documentElement.setAttribute('dir', 'rtl');
+ const handleSwitch = vi.fn();
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText('Organization 1')).toBeInTheDocument();
+ });
+
+ it('should flip chevron icon in RTL mode', async () => {
+ document.documentElement.setAttribute('dir', 'rtl');
+ const handleSwitch = vi.fn();
+
+ const {container} = render(
+ ,
+ );
+
+ await waitFor(() => {
+ const chevronIcon = container.querySelector('svg');
+ expect(chevronIcon).toBeTruthy();
+ if (chevronIcon) {
+ const style = window.getComputedStyle(chevronIcon);
+ // In RTL mode, the transform should be scaleX(-1)
+ expect(chevronIcon.style.transform).toContain('scaleX(-1)');
+ }
+ });
+ });
+
+ it('should not flip chevron icon in LTR mode', async () => {
+ document.documentElement.setAttribute('dir', 'ltr');
+ const handleSwitch = vi.fn();
+
+ const {container} = render(
+ ,
+ );
+
+ await waitFor(() => {
+ const chevronIcon = container.querySelector('svg');
+ expect(chevronIcon).toBeTruthy();
+ if (chevronIcon) {
+ // In LTR mode, the transform should be none
+ expect(chevronIcon.style.transform).toBe('none');
+ }
+ });
+ });
+
+ it('should update icon flip when direction changes', async () => {
+ document.documentElement.setAttribute('dir', 'ltr');
+ const handleSwitch = vi.fn();
+
+ const {container, rerender} = render(
+ ,
+ );
+
+ // Initially LTR
+ let chevronIcon = container.querySelector('svg');
+ expect(chevronIcon?.style.transform).toBe('none');
+
+ // Change to RTL
+ document.documentElement.setAttribute('dir', 'rtl');
+
+ // Force re-render
+ rerender(
+ ,
+ );
+
+ await waitFor(() => {
+ chevronIcon = container.querySelector('svg');
+ expect(chevronIcon?.style.transform).toContain('scaleX(-1)');
+ });
+ });
+});
diff --git a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx
index 0a0a6357f..bd3924dd1 100644
--- a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx
+++ b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx
@@ -34,6 +34,7 @@ import {cx} from '@emotion/css';
import {FC, ReactElement, ReactNode, useState} from 'react';
import useTheme from '../../../contexts/Theme/useTheme';
import useTranslation from '../../../hooks/useTranslation';
+import useRTL from '../../../hooks/useRTL';
import {Avatar} from '../../primitives/Avatar/Avatar';
import Button from '../../primitives/Button/Button';
import Building from '../../primitives/Icons/Building';
@@ -191,6 +192,7 @@ export const BaseOrganizationSwitcher: FC = ({
const [isOpen, setIsOpen] = useState(false);
const [hoveredItemIndex, setHoveredItemIndex] = useState(null);
const {t} = useTranslation();
+ const {isRTL} = useRTL();
const {refs, floatingStyles, context} = useFloating({
open: isOpen,
@@ -308,7 +310,7 @@ export const BaseOrganizationSwitcher: FC = ({
)}
>
)}
-
+
{isOpen && (
diff --git a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts
index 858fd8b6a..b778412b1 100644
--- a/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts
+++ b/packages/react/src/components/presentation/UserDropdown/BaseUserDropdown.styles.ts
@@ -95,7 +95,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
border: none;
cursor: pointer;
font-size: 0.875rem;
- text-align: left;
+ text-align: start;
border-radius: ${theme.vars.borderRadius.medium};
transition: none;
box-shadow: none;
@@ -125,7 +125,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
background: none;
cursor: pointer;
font-size: 0.875rem;
- text-align: left;
+ text-align: start;
border-radius: ${theme.vars.borderRadius.medium};
transition: background-color 0.15s ease-in-out;
diff --git a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts
index 0d5c9efdc..a5c693853 100644
--- a/packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts
+++ b/packages/react/src/components/presentation/UserProfile/BaseUserProfile.styles.ts
@@ -55,7 +55,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
display: flex;
gap: calc(${theme.vars.spacing.unit} / 2);
align-items: center;
- margin-left: calc(${theme.vars.spacing.unit} * 4);
+ margin-inline-start: calc(${theme.vars.spacing.unit} * 4);
`;
const complexTextarea = css`
@@ -135,7 +135,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
width: 120px;
flex-shrink: 0;
line-height: 28px;
- text-align: left;
+ text-align: start;
`;
const value = css`
@@ -151,7 +151,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
text-overflow: ellipsis;
white-space: nowrap;
max-width: 350px;
- text-align: left;
+ text-align: start;
.${withVendorCSSClassPrefix('form-control')} {
margin-bottom: 0;
diff --git a/packages/react/src/components/primitives/Checkbox/Checkbox.styles.ts b/packages/react/src/components/primitives/Checkbox/Checkbox.styles.ts
index 4354e969e..66d12d7b5 100644
--- a/packages/react/src/components/primitives/Checkbox/Checkbox.styles.ts
+++ b/packages/react/src/components/primitives/Checkbox/Checkbox.styles.ts
@@ -38,7 +38,7 @@ const useStyles = (theme: Theme, colorScheme: string, hasError: boolean, require
const inputStyles = css`
width: calc(${theme.vars.spacing.unit} * 2.5);
height: calc(${theme.vars.spacing.unit} * 2.5);
- margin-right: ${theme.vars.spacing.unit};
+ margin-inline-end: ${theme.vars.spacing.unit};
accent-color: ${theme.vars.colors.primary.main};
cursor: pointer;
diff --git a/packages/react/src/components/primitives/FormControl/FormControl.styles.ts b/packages/react/src/components/primitives/FormControl/FormControl.styles.ts
index d5ef13a5b..fa78472e7 100644
--- a/packages/react/src/components/primitives/FormControl/FormControl.styles.ts
+++ b/packages/react/src/components/primitives/FormControl/FormControl.styles.ts
@@ -40,14 +40,14 @@ const useStyles = (
) => {
return useMemo(() => {
const formControl = css`
- text-align: left;
+ text-align: start;
margin-bottom: calc(${theme.vars.spacing.unit} * 2);
`;
const helperText = css`
margin-top: calc(${theme.vars.spacing.unit} / 2);
- text-align: ${helperTextAlign};
- ${helperTextMarginLeft && `margin-left: ${helperTextMarginLeft};`}
+ text-align: ${helperTextAlign === 'left' ? 'start' : helperTextAlign};
+ ${helperTextMarginLeft && `margin-inline-start: ${helperTextMarginLeft};`}
`;
const helperTextError = css`
diff --git a/packages/react/src/hooks/useRTL.test.ts b/packages/react/src/hooks/useRTL.test.ts
new file mode 100644
index 000000000..e24ec000d
--- /dev/null
+++ b/packages/react/src/hooks/useRTL.test.ts
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {renderHook, waitFor} from '@testing-library/react';
+import {describe, it, expect, beforeEach, afterEach} from 'vitest';
+import useRTL from './useRTL';
+
+describe('useRTL', () => {
+ beforeEach(() => {
+ // Reset the dir attribute before each test
+ document.documentElement.removeAttribute('dir');
+ });
+
+ afterEach(() => {
+ // Clean up after each test
+ document.documentElement.removeAttribute('dir');
+ });
+
+ it('should return LTR direction by default', () => {
+ const {result} = renderHook(() => useRTL());
+
+ expect(result.current.isRTL).toBe(false);
+ expect(result.current.direction).toBe('ltr');
+ });
+
+ it('should detect RTL when dir attribute is set to rtl', () => {
+ document.documentElement.setAttribute('dir', 'rtl');
+ const {result} = renderHook(() => useRTL());
+
+ expect(result.current.isRTL).toBe(true);
+ expect(result.current.direction).toBe('rtl');
+ });
+
+ it('should detect LTR when dir attribute is explicitly set to ltr', () => {
+ document.documentElement.setAttribute('dir', 'ltr');
+ const {result} = renderHook(() => useRTL());
+
+ expect(result.current.isRTL).toBe(false);
+ expect(result.current.direction).toBe('ltr');
+ });
+
+ it('should update direction when dir attribute changes from LTR to RTL', async () => {
+ document.documentElement.setAttribute('dir', 'ltr');
+ const {result} = renderHook(() => useRTL());
+
+ expect(result.current.direction).toBe('ltr');
+
+ // Change to RTL
+ document.documentElement.setAttribute('dir', 'rtl');
+
+ await waitFor(() => {
+ expect(result.current.isRTL).toBe(true);
+ expect(result.current.direction).toBe('rtl');
+ });
+ });
+
+ it('should update direction when dir attribute changes from RTL to LTR', async () => {
+ document.documentElement.setAttribute('dir', 'rtl');
+ const {result} = renderHook(() => useRTL());
+
+ expect(result.current.direction).toBe('rtl');
+
+ // Change to LTR
+ document.documentElement.setAttribute('dir', 'ltr');
+
+ await waitFor(() => {
+ expect(result.current.isRTL).toBe(false);
+ expect(result.current.direction).toBe('ltr');
+ });
+ });
+
+ it('should handle document.dir property', () => {
+ document.dir = 'rtl';
+ const {result} = renderHook(() => useRTL());
+
+ expect(result.current.isRTL).toBe(true);
+ expect(result.current.direction).toBe('rtl');
+
+ // Clean up
+ document.dir = '';
+ });
+});
diff --git a/packages/react/src/hooks/useRTL.ts b/packages/react/src/hooks/useRTL.ts
new file mode 100644
index 000000000..20077620d
--- /dev/null
+++ b/packages/react/src/hooks/useRTL.ts
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {useState, useEffect} from 'react';
+
+export interface UseRTL {
+ /**
+ * Whether the current direction is Right-to-Left
+ */
+ isRTL: boolean;
+
+ /**
+ * The current text direction ('ltr' or 'rtl')
+ */
+ direction: 'ltr' | 'rtl';
+}
+
+/**
+ * Hook for detecting Right-to-Left (RTL) text direction.
+ * Checks the document's dir attribute and updates on changes.
+ *
+ * @returns An object containing RTL state and direction
+ *
+ * @example
+ * ```tsx
+ * const { isRTL, direction } = useRTL();
+ *
+ * return (
+ *
+ * {children}
+ *
+ * );
+ * ```
+ */
+const useRTL = (): UseRTL => {
+ const getDirection = (): 'ltr' | 'rtl' => {
+ if (typeof document === 'undefined') {
+ return 'ltr';
+ }
+
+ const dir = document.dir || document.documentElement.dir;
+ return dir === 'rtl' ? 'rtl' : 'ltr';
+ };
+
+ const [direction, setDirection] = useState<'ltr' | 'rtl'>(getDirection);
+
+ useEffect(() => {
+ // Update direction if it changes
+ const updateDirection = () => {
+ setDirection(getDirection());
+ };
+
+ // Create a MutationObserver to watch for dir attribute changes
+ const observer = new MutationObserver(updateDirection);
+
+ // Observe the document element and html element for dir attribute changes
+ if (typeof document !== 'undefined') {
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['dir'],
+ });
+ }
+
+ // Initial check
+ updateDirection();
+
+ return () => {
+ observer.disconnect();
+ };
+ }, []);
+
+ return {
+ isRTL: direction === 'rtl',
+ direction,
+ };
+};
+
+export default useRTL;
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 64a9ff6df..5c7d6e666 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -91,6 +91,9 @@ export * from './hooks/useForm';
export {default as useBranding} from './hooks/useBranding';
export * from './hooks/useBranding';
+export {default as useRTL} from './hooks/useRTL';
+export * from './hooks/useRTL';
+
export {default as BaseSignInButton} from './components/actions/SignInButton/BaseSignInButton';
export * from './components/actions/SignInButton/BaseSignInButton';
From ff1f69822c21ebcaa4381c17d324edab660b765e Mon Sep 17 00:00:00 2001
From: Tharun Devaraja <154892351+d3varaja@users.noreply.github.com>
Date: Sun, 26 Oct 2025 03:10:39 +0530
Subject: [PATCH 2/3] feat: Add comprehensive RTL support with CSS logical
properties and icon flipping
- Add direction property to ThemePreferences interface (ltr/rtl)
- Convert component styles to CSS logical properties
- Implement icon flipping for RTL mode in OrganizationSwitcher
- Add comprehensive RTL tests
- Remove redundant useRTL hook (use useTheme instead)
---
packages/javascript/src/models/config.ts | 5 +
packages/javascript/src/theme/types.ts | 5 +
.../OrganizationList.styles.ts | 10 +-
.../BaseOrganizationSwitcher.test.tsx | 1 +
.../BaseOrganizationSwitcher.tsx | 9 +-
.../primitives/Divider/Divider.styles.ts | 5 +-
.../primitives/TextField/TextField.styles.ts | 12 ++-
.../contexts/Asgardeo/AsgardeoProvider.tsx | 5 +-
.../react/src/contexts/Theme/ThemeContext.ts | 4 +
.../src/contexts/Theme/ThemeProvider.tsx | 11 +++
packages/react/src/contexts/Theme/types.ts | 5 +
packages/react/src/hooks/useRTL.test.ts | 97 -------------------
packages/react/src/hooks/useRTL.ts | 93 ------------------
packages/react/src/index.ts | 3 -
samples/teamspace-react/src/main.tsx | 1 +
15 files changed, 53 insertions(+), 213 deletions(-)
delete mode 100644 packages/react/src/hooks/useRTL.test.ts
delete mode 100644 packages/react/src/hooks/useRTL.ts
diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts
index 297a6d4dd..08e661e69 100644
--- a/packages/javascript/src/models/config.ts
+++ b/packages/javascript/src/models/config.ts
@@ -223,6 +223,11 @@ export interface WithPreferences {
export type Config = BaseConfig;
export interface ThemePreferences {
+ /**
+ * The text direction for the UI.
+ * @default 'ltr'
+ */
+ direction?: 'ltr' | 'rtl';
/**
* Inherit from Branding from WSO2 Identity Server or Asgardeo.
*/
diff --git a/packages/javascript/src/theme/types.ts b/packages/javascript/src/theme/types.ts
index 1dd5ae7e5..9e12a5813 100644
--- a/packages/javascript/src/theme/types.ts
+++ b/packages/javascript/src/theme/types.ts
@@ -149,6 +149,11 @@ export interface ThemeConfig {
small: string;
};
colors: ThemeColors;
+ /**
+ * The text direction for the UI.
+ * @default 'ltr'
+ */
+ direction?: 'ltr' | 'rtl';
shadows: {
large: string;
medium: string;
diff --git a/packages/react/src/components/presentation/OrganizationList/OrganizationList.styles.ts b/packages/react/src/components/presentation/OrganizationList/OrganizationList.styles.ts
index e5757328e..e23270d01 100644
--- a/packages/react/src/components/presentation/OrganizationList/OrganizationList.styles.ts
+++ b/packages/react/src/components/presentation/OrganizationList/OrganizationList.styles.ts
@@ -48,10 +48,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
&__loading-overlay {
position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ inset: 0;
background-color: color-mix(in srgb, ${theme.vars.colors.background.surface} 80%, transparent);
display: flex;
align-items: center;
@@ -77,10 +74,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
`,
loadingOverlay: css`
position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ inset: 0;
background-color: color-mix(in srgb, ${theme.vars.colors.background.surface} 80%, transparent);
display: flex;
align-items: center;
diff --git a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx
index e08f59194..e9beff5dd 100644
--- a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx
+++ b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.test.tsx
@@ -38,6 +38,7 @@ vi.mock('../../../contexts/Theme/useTheme', () => ({
},
},
colorScheme: 'light',
+ direction: (document.documentElement.getAttribute('dir') as 'ltr' | 'rtl') || 'ltr',
}),
}));
diff --git a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx
index bd3924dd1..601121cdb 100644
--- a/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx
+++ b/packages/react/src/components/presentation/OrganizationSwitcher/BaseOrganizationSwitcher.tsx
@@ -34,7 +34,6 @@ import {cx} from '@emotion/css';
import {FC, ReactElement, ReactNode, useState} from 'react';
import useTheme from '../../../contexts/Theme/useTheme';
import useTranslation from '../../../hooks/useTranslation';
-import useRTL from '../../../hooks/useRTL';
import {Avatar} from '../../primitives/Avatar/Avatar';
import Button from '../../primitives/Button/Button';
import Building from '../../primitives/Icons/Building';
@@ -187,12 +186,12 @@ export const BaseOrganizationSwitcher: FC = ({
avatarSize = 24,
fallback = null,
}): ReactElement => {
- const {theme, colorScheme} = useTheme();
+ const {theme, colorScheme, direction} = useTheme();
const styles = useStyles(theme, colorScheme);
const [isOpen, setIsOpen] = useState(false);
const [hoveredItemIndex, setHoveredItemIndex] = useState(null);
const {t} = useTranslation();
- const {isRTL} = useRTL();
+ const isRTL = direction === 'rtl';
const {refs, floatingStyles, context} = useFloating({
open: isOpen,
@@ -310,7 +309,9 @@ export const BaseOrganizationSwitcher: FC = ({
)}
>
)}
-
+
+
+
{isOpen && (
diff --git a/packages/react/src/components/primitives/Divider/Divider.styles.ts b/packages/react/src/components/primitives/Divider/Divider.styles.ts
index 8aae54f4f..bb4813e2c 100644
--- a/packages/react/src/components/primitives/Divider/Divider.styles.ts
+++ b/packages/react/src/components/primitives/Divider/Divider.styles.ts
@@ -54,8 +54,9 @@ const useStyles = (
height: 100%;
min-height: calc(${theme.vars.spacing.unit} * 2);
width: 1px;
- border-left: 1px ${borderStyle} ${baseColor};
- margin: 0 calc(${theme.vars.spacing.unit} * 1);
+ border-inline-start: 1px ${borderStyle} ${baseColor};
+ margin-block: 0;
+ margin-inline: calc(${theme.vars.spacing.unit} * 1);
`;
const horizontalDivider = css`
diff --git a/packages/react/src/components/primitives/TextField/TextField.styles.ts b/packages/react/src/components/primitives/TextField/TextField.styles.ts
index 99225e0b0..2c9f83b23 100644
--- a/packages/react/src/components/primitives/TextField/TextField.styles.ts
+++ b/packages/react/src/components/primitives/TextField/TextField.styles.ts
@@ -39,10 +39,10 @@ const useStyles = (
hasEndIcon: boolean,
) => {
return useMemo(() => {
- const leftPadding = hasStartIcon
+ const inlineStartPadding = hasStartIcon
? `calc(${theme.vars.spacing.unit} * 5)`
: `calc(${theme.vars.spacing.unit} * 1.5)`;
- const rightPadding = hasEndIcon ? `calc(${theme.vars.spacing.unit} * 5)` : `calc(${theme.vars.spacing.unit} * 1.5)`;
+ const inlineEndPadding = hasEndIcon ? `calc(${theme.vars.spacing.unit} * 5)` : `calc(${theme.vars.spacing.unit} * 1.5)`;
const inputContainer = css`
position: relative;
@@ -52,7 +52,9 @@ const useStyles = (
const input = css`
width: 100%;
- padding: ${theme.vars.spacing.unit} ${rightPadding} ${theme.vars.spacing.unit} ${leftPadding};
+ padding-block: ${theme.vars.spacing.unit};
+ padding-inline-start: ${inlineStartPadding};
+ padding-inline-end: ${inlineEndPadding};
border: 1px solid ${hasError ? theme.vars.colors.error.main : theme.vars.colors.border};
border-radius: ${theme.vars.components?.Field?.root?.borderRadius || theme.vars.borderRadius.medium};
font-size: ${theme.vars.typography.fontSizes.md};
@@ -127,12 +129,12 @@ const useStyles = (
const startIcon = css`
${icon};
- left: ${theme.vars.spacing.unit};
+ inset-inline-start: ${theme.vars.spacing.unit};
`;
const endIcon = css`
${icon};
- right: ${theme.vars.spacing.unit};
+ inset-inline-end: ${theme.vars.spacing.unit};
`;
return {
diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
index aa33c9cae..4109e2484 100644
--- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
+++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
@@ -501,7 +501,10 @@ const AsgardeoProvider: FC> = ({
>
diff --git a/packages/react/src/contexts/Theme/ThemeContext.ts b/packages/react/src/contexts/Theme/ThemeContext.ts
index 599b29171..5c6688e74 100644
--- a/packages/react/src/contexts/Theme/ThemeContext.ts
+++ b/packages/react/src/contexts/Theme/ThemeContext.ts
@@ -22,6 +22,10 @@ import {Theme} from '@asgardeo/browser';
export interface ThemeContextValue {
theme: Theme;
colorScheme: 'light' | 'dark';
+ /**
+ * The text direction for the UI.
+ */
+ direction: 'ltr' | 'rtl';
toggleTheme: () => void;
/**
* Whether branding theme is currently loading
diff --git a/packages/react/src/contexts/Theme/ThemeProvider.tsx b/packages/react/src/contexts/Theme/ThemeProvider.tsx
index 752e307ab..2ec48309f 100644
--- a/packages/react/src/contexts/Theme/ThemeProvider.tsx
+++ b/packages/react/src/contexts/Theme/ThemeProvider.tsx
@@ -213,6 +213,9 @@ const ThemeProvider: FC> = ({
const theme = useMemo(() => createTheme(finalThemeConfig, colorScheme === 'dark'), [finalThemeConfig, colorScheme]);
+ // Get direction from theme config or default to 'ltr'
+ const direction = (finalThemeConfig as any)?.direction || 'ltr';
+
const handleThemeChange = useCallback((isDark: boolean) => {
setColorScheme(isDark ? 'dark' : 'light');
}, []);
@@ -262,9 +265,17 @@ const ThemeProvider: FC> = ({
applyThemeToDOM(theme);
}, [theme]);
+ // Apply direction to document
+ useEffect(() => {
+ if (typeof document !== 'undefined') {
+ document.documentElement.dir = direction;
+ }
+ }, [direction]);
+
const value = {
theme,
colorScheme,
+ direction,
toggleTheme,
isBrandingLoading,
brandingError,
diff --git a/packages/react/src/contexts/Theme/types.ts b/packages/react/src/contexts/Theme/types.ts
index d02caa39b..a278ae93e 100644
--- a/packages/react/src/contexts/Theme/types.ts
+++ b/packages/react/src/contexts/Theme/types.ts
@@ -58,6 +58,11 @@ export interface ThemeConfig {
small: string;
};
colors: ThemeColors;
+ /**
+ * The text direction for the UI.
+ * @default 'ltr'
+ */
+ direction?: 'ltr' | 'rtl';
shadows: {
large: string;
medium: string;
diff --git a/packages/react/src/hooks/useRTL.test.ts b/packages/react/src/hooks/useRTL.test.ts
deleted file mode 100644
index e24ec000d..000000000
--- a/packages/react/src/hooks/useRTL.test.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
- *
- * WSO2 LLC. licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import {renderHook, waitFor} from '@testing-library/react';
-import {describe, it, expect, beforeEach, afterEach} from 'vitest';
-import useRTL from './useRTL';
-
-describe('useRTL', () => {
- beforeEach(() => {
- // Reset the dir attribute before each test
- document.documentElement.removeAttribute('dir');
- });
-
- afterEach(() => {
- // Clean up after each test
- document.documentElement.removeAttribute('dir');
- });
-
- it('should return LTR direction by default', () => {
- const {result} = renderHook(() => useRTL());
-
- expect(result.current.isRTL).toBe(false);
- expect(result.current.direction).toBe('ltr');
- });
-
- it('should detect RTL when dir attribute is set to rtl', () => {
- document.documentElement.setAttribute('dir', 'rtl');
- const {result} = renderHook(() => useRTL());
-
- expect(result.current.isRTL).toBe(true);
- expect(result.current.direction).toBe('rtl');
- });
-
- it('should detect LTR when dir attribute is explicitly set to ltr', () => {
- document.documentElement.setAttribute('dir', 'ltr');
- const {result} = renderHook(() => useRTL());
-
- expect(result.current.isRTL).toBe(false);
- expect(result.current.direction).toBe('ltr');
- });
-
- it('should update direction when dir attribute changes from LTR to RTL', async () => {
- document.documentElement.setAttribute('dir', 'ltr');
- const {result} = renderHook(() => useRTL());
-
- expect(result.current.direction).toBe('ltr');
-
- // Change to RTL
- document.documentElement.setAttribute('dir', 'rtl');
-
- await waitFor(() => {
- expect(result.current.isRTL).toBe(true);
- expect(result.current.direction).toBe('rtl');
- });
- });
-
- it('should update direction when dir attribute changes from RTL to LTR', async () => {
- document.documentElement.setAttribute('dir', 'rtl');
- const {result} = renderHook(() => useRTL());
-
- expect(result.current.direction).toBe('rtl');
-
- // Change to LTR
- document.documentElement.setAttribute('dir', 'ltr');
-
- await waitFor(() => {
- expect(result.current.isRTL).toBe(false);
- expect(result.current.direction).toBe('ltr');
- });
- });
-
- it('should handle document.dir property', () => {
- document.dir = 'rtl';
- const {result} = renderHook(() => useRTL());
-
- expect(result.current.isRTL).toBe(true);
- expect(result.current.direction).toBe('rtl');
-
- // Clean up
- document.dir = '';
- });
-});
diff --git a/packages/react/src/hooks/useRTL.ts b/packages/react/src/hooks/useRTL.ts
deleted file mode 100644
index 20077620d..000000000
--- a/packages/react/src/hooks/useRTL.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
- *
- * WSO2 LLC. licenses this file to you under the Apache License,
- * Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import {useState, useEffect} from 'react';
-
-export interface UseRTL {
- /**
- * Whether the current direction is Right-to-Left
- */
- isRTL: boolean;
-
- /**
- * The current text direction ('ltr' or 'rtl')
- */
- direction: 'ltr' | 'rtl';
-}
-
-/**
- * Hook for detecting Right-to-Left (RTL) text direction.
- * Checks the document's dir attribute and updates on changes.
- *
- * @returns An object containing RTL state and direction
- *
- * @example
- * ```tsx
- * const { isRTL, direction } = useRTL();
- *
- * return (
- *
- * {children}
- *
- * );
- * ```
- */
-const useRTL = (): UseRTL => {
- const getDirection = (): 'ltr' | 'rtl' => {
- if (typeof document === 'undefined') {
- return 'ltr';
- }
-
- const dir = document.dir || document.documentElement.dir;
- return dir === 'rtl' ? 'rtl' : 'ltr';
- };
-
- const [direction, setDirection] = useState<'ltr' | 'rtl'>(getDirection);
-
- useEffect(() => {
- // Update direction if it changes
- const updateDirection = () => {
- setDirection(getDirection());
- };
-
- // Create a MutationObserver to watch for dir attribute changes
- const observer = new MutationObserver(updateDirection);
-
- // Observe the document element and html element for dir attribute changes
- if (typeof document !== 'undefined') {
- observer.observe(document.documentElement, {
- attributes: true,
- attributeFilter: ['dir'],
- });
- }
-
- // Initial check
- updateDirection();
-
- return () => {
- observer.disconnect();
- };
- }, []);
-
- return {
- isRTL: direction === 'rtl',
- direction,
- };
-};
-
-export default useRTL;
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 5c7d6e666..64a9ff6df 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -91,9 +91,6 @@ export * from './hooks/useForm';
export {default as useBranding} from './hooks/useBranding';
export * from './hooks/useBranding';
-export {default as useRTL} from './hooks/useRTL';
-export * from './hooks/useRTL';
-
export {default as BaseSignInButton} from './components/actions/SignInButton/BaseSignInButton';
export * from './components/actions/SignInButton/BaseSignInButton';
diff --git a/samples/teamspace-react/src/main.tsx b/samples/teamspace-react/src/main.tsx
index 6992792ea..7eeb1de0a 100644
--- a/samples/teamspace-react/src/main.tsx
+++ b/samples/teamspace-react/src/main.tsx
@@ -16,6 +16,7 @@ createRoot(document.getElementById('root')!).render(
scopes="openid address email profile user:email read:user internal_organization_create internal_organization_view internal_organization_update internal_organization_delete internal_org_organization_update internal_org_organization_create internal_org_organization_view internal_org_organization_delete"
preferences={{
theme: {
+ direction: 'rtl', // RTL (Right-to-Left) - Arabic/Hebrew support
// overrides: {
// colors: {
// primary: {
From cc25e1434dc778c12945ad4b9b89f5e95e4578d7 Mon Sep 17 00:00:00 2001
From: Brion Mario
Date: Mon, 27 Oct 2025 17:28:13 +0530
Subject: [PATCH 3/3] Update samples/teamspace-react/src/main.tsx
---
samples/teamspace-react/src/main.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/samples/teamspace-react/src/main.tsx b/samples/teamspace-react/src/main.tsx
index 7eeb1de0a..6992792ea 100644
--- a/samples/teamspace-react/src/main.tsx
+++ b/samples/teamspace-react/src/main.tsx
@@ -16,7 +16,6 @@ createRoot(document.getElementById('root')!).render(
scopes="openid address email profile user:email read:user internal_organization_create internal_organization_view internal_organization_update internal_organization_delete internal_org_organization_update internal_org_organization_create internal_org_organization_view internal_org_organization_delete"
preferences={{
theme: {
- direction: 'rtl', // RTL (Right-to-Left) - Arabic/Hebrew support
// overrides: {
// colors: {
// primary: {