Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/javascript/src/models/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ export interface WithPreferences {
export type Config<T = unknown> = BaseConfig<T>;

export interface ThemePreferences {
/**
* The text direction for the UI.
* @default 'ltr'
*/
direction?: 'ltr' | 'rtl';
/**
* Inherit from Branding from WSO2 Identity Server or Asgardeo.
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/javascript/src/theme/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* 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',
direction: (document.documentElement.getAttribute('dir') as 'ltr' | 'rtl') || 'ltr',
}),
}));

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(
<BaseOrganizationSwitcher
organizations={mockOrganizations}
currentOrganization={mockOrganizations[0]}
onOrganizationSwitch={handleSwitch}
/>,
);

expect(screen.getByText('Organization 1')).toBeInTheDocument();
});

it('should render correctly in RTL mode', () => {
document.documentElement.setAttribute('dir', 'rtl');
const handleSwitch = vi.fn();

render(
<BaseOrganizationSwitcher
organizations={mockOrganizations}
currentOrganization={mockOrganizations[0]}
onOrganizationSwitch={handleSwitch}
/>,
);

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(
<BaseOrganizationSwitcher
organizations={mockOrganizations}
currentOrganization={mockOrganizations[0]}
onOrganizationSwitch={handleSwitch}
/>,
);

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(
<BaseOrganizationSwitcher
organizations={mockOrganizations}
currentOrganization={mockOrganizations[0]}
onOrganizationSwitch={handleSwitch}
/>,
);

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(
<BaseOrganizationSwitcher
organizations={mockOrganizations}
currentOrganization={mockOrganizations[0]}
onOrganizationSwitch={handleSwitch}
/>,
);

// 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(
<BaseOrganizationSwitcher
organizations={mockOrganizations}
currentOrganization={mockOrganizations[0]}
onOrganizationSwitch={handleSwitch}
/>,
);

await waitFor(() => {
chevronIcon = container.querySelector('svg');
expect(chevronIcon?.style.transform).toContain('scaleX(-1)');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,12 @@ export const BaseOrganizationSwitcher: FC<BaseOrganizationSwitcherProps> = ({
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<number | null>(null);
const {t} = useTranslation();
const isRTL = direction === 'rtl';

const {refs, floatingStyles, context} = useFloating({
open: isOpen,
Expand Down Expand Up @@ -308,7 +309,9 @@ export const BaseOrganizationSwitcher: FC<BaseOrganizationSwitcherProps> = ({
)}
</>
)}
<ChevronDown width="16" height="16" />
<span style={{transform: isRTL ? 'scaleX(-1)' : 'none', display: 'inline-flex'}}>
<ChevronDown width="16" height="16" />
</span>
</Button>

{isOpen && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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`
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading
Loading