Skip to content

Commit 76a757b

Browse files
committed
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)
1 parent 031812e commit 76a757b

File tree

15 files changed

+53
-213
lines changed

15 files changed

+53
-213
lines changed

packages/javascript/src/models/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ export interface WithPreferences {
215215
export type Config<T = unknown> = BaseConfig<T>;
216216

217217
export interface ThemePreferences {
218+
/**
219+
* The text direction for the UI.
220+
* @default 'ltr'
221+
*/
222+
direction?: 'ltr' | 'rtl';
218223
/**
219224
* Inherit from Branding from WSO2 Identity Server or Asgardeo.
220225
*/

packages/javascript/src/theme/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ export interface ThemeConfig {
149149
small: string;
150150
};
151151
colors: ThemeColors;
152+
/**
153+
* The text direction for the UI.
154+
* @default 'ltr'
155+
*/
156+
direction?: 'ltr' | 'rtl';
152157
shadows: {
153158
large: string;
154159
medium: string;

packages/react/src/components/presentation/OrganizationList/OrganizationList.styles.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
4848
4949
&__loading-overlay {
5050
position: absolute;
51-
top: 0;
52-
left: 0;
53-
right: 0;
54-
bottom: 0;
51+
inset: 0;
5552
background-color: color-mix(in srgb, ${theme.vars.colors.background.surface} 80%, transparent);
5653
display: flex;
5754
align-items: center;
@@ -77,10 +74,7 @@ const useStyles = (theme: Theme, colorScheme: string) => {
7774
`,
7875
loadingOverlay: css`
7976
position: absolute;
80-
top: 0;
81-
left: 0;
82-
right: 0;
83-
bottom: 0;
77+
inset: 0;
8478
background-color: color-mix(in srgb, ${theme.vars.colors.background.surface} 80%, transparent);
8579
display: flex;
8680
align-items: center;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ vi.mock('../../../contexts/Theme/useTheme', () => ({
3838
},
3939
},
4040
colorScheme: 'light',
41+
direction: (document.documentElement.getAttribute('dir') as 'ltr' | 'rtl') || 'ltr',
4142
}),
4243
}));
4344

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ 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';
3837
import {Avatar} from '../../primitives/Avatar/Avatar';
3938
import Button from '../../primitives/Button/Button';
4039
import Building from '../../primitives/Icons/Building';
@@ -187,12 +186,12 @@ export const BaseOrganizationSwitcher: FC<BaseOrganizationSwitcherProps> = ({
187186
avatarSize = 24,
188187
fallback = null,
189188
}): ReactElement => {
190-
const {theme, colorScheme} = useTheme();
189+
const {theme, colorScheme, direction} = useTheme();
191190
const styles = useStyles(theme, colorScheme);
192191
const [isOpen, setIsOpen] = useState(false);
193192
const [hoveredItemIndex, setHoveredItemIndex] = useState<number | null>(null);
194193
const {t} = useTranslation();
195-
const {isRTL} = useRTL();
194+
const isRTL = direction === 'rtl';
196195

197196
const {refs, floatingStyles, context} = useFloating({
198197
open: isOpen,
@@ -310,7 +309,9 @@ export const BaseOrganizationSwitcher: FC<BaseOrganizationSwitcherProps> = ({
310309
)}
311310
</>
312311
)}
313-
<ChevronDown width="16" height="16" style={{transform: isRTL ? 'scaleX(-1)' : 'none'}} />
312+
<span style={{transform: isRTL ? 'scaleX(-1)' : 'none', display: 'inline-flex'}}>
313+
<ChevronDown width="16" height="16" />
314+
</span>
314315
</Button>
315316

316317
{isOpen && (

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ const useStyles = (
5454
height: 100%;
5555
min-height: calc(${theme.vars.spacing.unit} * 2);
5656
width: 1px;
57-
border-left: 1px ${borderStyle} ${baseColor};
58-
margin: 0 calc(${theme.vars.spacing.unit} * 1);
57+
border-inline-start: 1px ${borderStyle} ${baseColor};
58+
margin-block: 0;
59+
margin-inline: calc(${theme.vars.spacing.unit} * 1);
5960
`;
6061

6162
const horizontalDivider = css`

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ const useStyles = (
3939
hasEndIcon: boolean,
4040
) => {
4141
return useMemo(() => {
42-
const leftPadding = hasStartIcon
42+
const inlineStartPadding = hasStartIcon
4343
? `calc(${theme.vars.spacing.unit} * 5)`
4444
: `calc(${theme.vars.spacing.unit} * 1.5)`;
45-
const rightPadding = hasEndIcon ? `calc(${theme.vars.spacing.unit} * 5)` : `calc(${theme.vars.spacing.unit} * 1.5)`;
45+
const inlineEndPadding = hasEndIcon ? `calc(${theme.vars.spacing.unit} * 5)` : `calc(${theme.vars.spacing.unit} * 1.5)`;
4646

4747
const inputContainer = css`
4848
position: relative;
@@ -52,7 +52,9 @@ const useStyles = (
5252

5353
const input = css`
5454
width: 100%;
55-
padding: ${theme.vars.spacing.unit} ${rightPadding} ${theme.vars.spacing.unit} ${leftPadding};
55+
padding-block: ${theme.vars.spacing.unit};
56+
padding-inline-start: ${inlineStartPadding};
57+
padding-inline-end: ${inlineEndPadding};
5658
border: 1px solid ${hasError ? theme.vars.colors.error.main : theme.vars.colors.border};
5759
border-radius: ${theme.vars.components?.Field?.root?.borderRadius || theme.vars.borderRadius.medium};
5860
font-size: ${theme.vars.typography.fontSizes.md};
@@ -127,12 +129,12 @@ const useStyles = (
127129

128130
const startIcon = css`
129131
${icon};
130-
left: ${theme.vars.spacing.unit};
132+
inset-inline-start: ${theme.vars.spacing.unit};
131133
`;
132134

133135
const endIcon = css`
134136
${icon};
135-
right: ${theme.vars.spacing.unit};
137+
inset-inline-end: ${theme.vars.spacing.unit};
136138
`;
137139

138140
return {

packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,10 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
468468
>
469469
<ThemeProvider
470470
inheritFromBranding={preferences?.theme?.inheritFromBranding}
471-
theme={preferences?.theme?.overrides}
471+
theme={{
472+
...preferences?.theme?.overrides,
473+
direction: preferences?.theme?.direction,
474+
}}
472475
mode={getActiveTheme(preferences.theme.mode)}
473476
>
474477
<FlowProvider>

packages/react/src/contexts/Theme/ThemeContext.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import {Theme} from '@asgardeo/browser';
2222
export interface ThemeContextValue {
2323
theme: Theme;
2424
colorScheme: 'light' | 'dark';
25+
/**
26+
* The text direction for the UI.
27+
*/
28+
direction: 'ltr' | 'rtl';
2529
toggleTheme: () => void;
2630
/**
2731
* Whether branding theme is currently loading

packages/react/src/contexts/Theme/ThemeProvider.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,9 @@ const ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = ({
213213

214214
const theme = useMemo(() => createTheme(finalThemeConfig, colorScheme === 'dark'), [finalThemeConfig, colorScheme]);
215215

216+
// Get direction from theme config or default to 'ltr'
217+
const direction = (finalThemeConfig as any)?.direction || 'ltr';
218+
216219
const handleThemeChange = useCallback((isDark: boolean) => {
217220
setColorScheme(isDark ? 'dark' : 'light');
218221
}, []);
@@ -262,9 +265,17 @@ const ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>> = ({
262265
applyThemeToDOM(theme);
263266
}, [theme]);
264267

268+
// Apply direction to document
269+
useEffect(() => {
270+
if (typeof document !== 'undefined') {
271+
document.documentElement.dir = direction;
272+
}
273+
}, [direction]);
274+
265275
const value = {
266276
theme,
267277
colorScheme,
278+
direction,
268279
toggleTheme,
269280
isBrandingLoading,
270281
brandingError,

0 commit comments

Comments
 (0)