Skip to content

Commit 1dc705f

Browse files
feat(ui): RTL support (#7718)
1 parent 4be764f commit 1dc705f

73 files changed

Lines changed: 269 additions & 123 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/cuddly-ends-wait.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@clerk/ui': minor
3+
---
4+
5+
Improve RTL support by converting physical CSS properties (margins, padding, text alignment, borders) to logical equivalents and adding direction-aware arrow icons
6+
7+
The changes included:
8+
9+
- Positioning (left → insetInlineStart)
10+
- Margins (marginLeft/Right → marginInlineStart/End)
11+
- Padding (paddingLeft/Right → paddingInlineStart/End)
12+
- Text alignment (left/right → start/end)
13+
- Border radius (borderTopLeftRadius → borderStartStartRadius)
14+
- Arrow icon flipping with scaleX(-1) in RTL
15+
- Animation direction adjustments

eslint.config.mjs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,83 @@ const noUnstableMethods = {
171171
},
172172
};
173173

174+
const noPhysicalCssProperties = {
175+
meta: {
176+
type: 'problem',
177+
docs: {
178+
description: 'Enforce use of CSS logical properties instead of physical properties for RTL support',
179+
recommended: false,
180+
},
181+
messages: {
182+
useLogicalProperty:
183+
'Use logical CSS property "{{logical}}" instead of physical property "{{physical}}" for RTL support.',
184+
useLogicalTextAlign:
185+
'Use logical textAlign value "{{logical}}" instead of physical value "{{physical}}" for RTL support.',
186+
},
187+
schema: [],
188+
},
189+
create(context) {
190+
// Mapping of physical properties to logical equivalents
191+
const propertyMap = {
192+
left: 'insetInlineStart',
193+
right: 'insetInlineEnd',
194+
marginLeft: 'marginInlineStart',
195+
marginRight: 'marginInlineEnd',
196+
paddingLeft: 'paddingInlineStart',
197+
paddingRight: 'paddingInlineEnd',
198+
borderLeft: 'borderInlineStart',
199+
borderRight: 'borderInlineEnd',
200+
borderLeftWidth: 'borderInlineStartWidth',
201+
borderRightWidth: 'borderInlineEndWidth',
202+
borderLeftStyle: 'borderInlineStartStyle',
203+
borderRightStyle: 'borderInlineEndStyle',
204+
borderLeftColor: 'borderInlineStartColor',
205+
borderRightColor: 'borderInlineEndColor',
206+
borderTopLeftRadius: 'borderStartStartRadius',
207+
borderTopRightRadius: 'borderStartEndRadius',
208+
borderBottomLeftRadius: 'borderEndStartRadius',
209+
borderBottomRightRadius: 'borderEndEndRadius',
210+
};
211+
212+
const checkProperty = (key, value) => {
213+
const keyName = key.type === 'Identifier' ? key.name : key.value;
214+
215+
// Check for physical property names
216+
if (propertyMap[keyName]) {
217+
context.report({
218+
node: key,
219+
messageId: 'useLogicalProperty',
220+
data: {
221+
physical: keyName,
222+
logical: propertyMap[keyName],
223+
},
224+
});
225+
}
226+
227+
// Check for textAlign with physical values
228+
if (keyName === 'textAlign' && value) {
229+
if (value.type === 'Literal' && (value.value === 'left' || value.value === 'right')) {
230+
const logicalValue = value.value === 'left' ? 'start' : 'end';
231+
context.report({
232+
node: value,
233+
messageId: 'useLogicalTextAlign',
234+
data: {
235+
physical: value.value,
236+
logical: logicalValue,
237+
},
238+
});
239+
}
240+
}
241+
};
242+
243+
return {
244+
Property(node) {
245+
checkProperty(node.key, node.value);
246+
},
247+
};
248+
},
249+
};
250+
174251
export default tseslint.config([
175252
{
176253
name: 'repo/ignores',
@@ -248,6 +325,7 @@ export default tseslint.config([
248325
rules: {
249326
'no-global-object': noGlobalObject,
250327
'no-unstable-methods': noUnstableMethods,
328+
'no-physical-css-properties': noPhysicalCssProperties,
251329
},
252330
},
253331
'simple-import-sort': pluginSimpleImportSort,
@@ -458,6 +536,13 @@ export default tseslint.config([
458536
'custom-rules/no-unstable-methods': 'error',
459537
},
460538
},
539+
{
540+
name: 'packages/ui',
541+
files: ['packages/ui/src/**/*'],
542+
rules: {
543+
'custom-rules/no-physical-css-properties': 'error',
544+
},
545+
},
461546
{
462547
name: 'packages - vitest',
463548
files: ['packages/*/src/**/*.test.{ts,tsx}'],

packages/clerk-js/sandbox/template.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<!doctype html>
2-
<html class="h-full">
2+
<html
3+
class="h-full"
4+
dir="rtl"
5+
>
36
<head>
47
<title>clerk-js Sandbox</title>
58
<meta charset="utf-8" />

packages/ui/src/common/InfiniteListSpinner.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const InfiniteListSpinner = forwardRef<HTMLDivElement>((_, ref) => {
1616
sx={{
1717
margin: 'auto',
1818
position: 'absolute',
19+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Centering with transform: translateX(-50%)
1920
left: '50%',
2021
top: '50%',
2122
transform: 'translateY(-50%) translateX(-50%)',

packages/ui/src/common/NotificationCountBadge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const NotificationCountBadge = (props: NotificationCountBadgeProps) => {
3232
as='span'
3333
sx={[
3434
t => ({
35-
marginLeft: t.space.$1x5,
35+
marginInlineStart: t.space.$1x5,
3636
}),
3737
containerSx,
3838
]}

packages/ui/src/common/PrintableComponent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const PrintableComponent = (props: UsePrintableReturn['printableProps'] &
2424
return (
2525
<div
2626
ref={ref}
27+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Off-screen hide for print functionality
2728
style={{ position: 'fixed', left: '-9999px', top: 0, display: 'none' }}
2829
>
2930
{children}

packages/ui/src/common/organizations/OrganizationPreview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const OrganizationPreviewSpinner = forwardRef<HTMLDivElement>((_, ref) =>
9696
sx={{
9797
margin: 'auto',
9898
position: 'absolute',
99+
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Centering with transform: translateX(-50%)
99100
left: '50%',
100101
top: '50%',
101102
transform: 'translateY(-50%) translateX(-50%)',

packages/ui/src/components/APIKeys/APIKeyModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const getScopedPortalContainerStyles = (modalRoot?: React.MutableRefObject<HTMLE
1818
modalRoot
1919
? t => ({
2020
position: 'absolute',
21-
right: 0,
21+
insetInlineEnd: 0,
2222
bottom: 0,
2323
backgroundColor: 'inherit',
2424
backdropFilter: `blur(${t.sizes.$2})`,

packages/ui/src/components/APIKeys/ApiKeysTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const APIKeysTable = ({
4242
<Tr>
4343
<Th>Name</Th>
4444
<Th>Last used</Th>
45-
{canManageAPIKeys && <Th sx={{ textAlign: 'right' }}>Actions</Th>}
45+
{canManageAPIKeys && <Th sx={{ textAlign: 'end' }}>Actions</Th>}
4646
</Tr>
4747
</Thead>
4848
<Tbody>
@@ -98,7 +98,7 @@ export const APIKeysTable = ({
9898
</Box>
9999
</Td>
100100
{canManageAPIKeys && (
101-
<Td sx={{ textAlign: 'right' }}>
101+
<Td sx={{ textAlign: 'end' }}>
102102
<ThreeDotsMenu
103103
actions={[
104104
{

packages/ui/src/components/APIKeys/CopyAPIKeyModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const CopyAPIKeyModal = ({
6363
>
6464
<Card.Content
6565
sx={t => ({
66-
textAlign: 'left',
66+
textAlign: 'start',
6767
padding: `${t.sizes.$4} ${t.sizes.$5} ${t.sizes.$4} ${t.sizes.$6}`,
6868
})}
6969
>

0 commit comments

Comments
 (0)