Skip to content
Open
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
15 changes: 15 additions & 0 deletions .changeset/cuddly-ends-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@clerk/ui': minor
---

Improve RTL support by converting physical CSS properties (margins, padding, text alignment, borders) to logical equivalents and adding direction-aware arrow icons

The changes included:

- Positioning (left → insetInlineStart)
- Margins (marginLeft/Right → marginInlineStart/End)
- Padding (paddingLeft/Right → paddingInlineStart/End)
- Text alignment (left/right → start/end)
- Border radius (borderTopLeftRadius → borderStartStartRadius)
- Arrow icon flipping with scaleX(-1) in RTL
- Animation direction adjustments
85 changes: 85 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,83 @@ const noUnstableMethods = {
},
};

const noPhysicalCssProperties = {
meta: {
type: 'problem',
docs: {
description: 'Enforce use of CSS logical properties instead of physical properties for RTL support',
recommended: false,
},
messages: {
useLogicalProperty:
'Use logical CSS property "{{logical}}" instead of physical property "{{physical}}" for RTL support.',
useLogicalTextAlign:
'Use logical textAlign value "{{logical}}" instead of physical value "{{physical}}" for RTL support.',
},
schema: [],
},
create(context) {
// Mapping of physical properties to logical equivalents
const propertyMap = {
left: 'insetInlineStart',
right: 'insetInlineEnd',
marginLeft: 'marginInlineStart',
marginRight: 'marginInlineEnd',
paddingLeft: 'paddingInlineStart',
paddingRight: 'paddingInlineEnd',
borderLeft: 'borderInlineStart',
borderRight: 'borderInlineEnd',
borderLeftWidth: 'borderInlineStartWidth',
borderRightWidth: 'borderInlineEndWidth',
borderLeftStyle: 'borderInlineStartStyle',
borderRightStyle: 'borderInlineEndStyle',
borderLeftColor: 'borderInlineStartColor',
borderRightColor: 'borderInlineEndColor',
borderTopLeftRadius: 'borderStartStartRadius',
borderTopRightRadius: 'borderStartEndRadius',
borderBottomLeftRadius: 'borderEndStartRadius',
borderBottomRightRadius: 'borderEndEndRadius',
};

const checkProperty = (key, value) => {
const keyName = key.type === 'Identifier' ? key.name : key.value;

// Check for physical property names
if (propertyMap[keyName]) {
context.report({
node: key,
messageId: 'useLogicalProperty',
data: {
physical: keyName,
logical: propertyMap[keyName],
},
});
}

// Check for textAlign with physical values
if (keyName === 'textAlign' && value) {
if (value.type === 'Literal' && (value.value === 'left' || value.value === 'right')) {
const logicalValue = value.value === 'left' ? 'start' : 'end';
context.report({
node: value,
messageId: 'useLogicalTextAlign',
data: {
physical: value.value,
logical: logicalValue,
},
});
}
}
};

return {
Property(node) {
checkProperty(node.key, node.value);
},
};
},
};

export default tseslint.config([
{
name: 'repo/ignores',
Expand Down Expand Up @@ -248,6 +325,7 @@ export default tseslint.config([
rules: {
'no-global-object': noGlobalObject,
'no-unstable-methods': noUnstableMethods,
'no-physical-css-properties': noPhysicalCssProperties,
},
},
'simple-import-sort': pluginSimpleImportSort,
Expand Down Expand Up @@ -458,6 +536,13 @@ export default tseslint.config([
'custom-rules/no-unstable-methods': 'error',
},
},
{
name: 'packages/ui',
files: ['packages/ui/src/**/*'],
rules: {
'custom-rules/no-physical-css-properties': 'error',
},
},
{
name: 'packages - vitest',
files: ['packages/*/src/**/*.test.{ts,tsx}'],
Expand Down
5 changes: 4 additions & 1 deletion packages/clerk-js/sandbox/template.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<!doctype html>
<html class="h-full">
<html
class="h-full"
dir="rtl"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For demo purposes, will revert before merging.

>
<head>
<title>clerk-js Sandbox</title>
<meta charset="utf-8" />
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/common/InfiniteListSpinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const InfiniteListSpinner = forwardRef<HTMLDivElement>((_, ref) => {
sx={{
margin: 'auto',
position: 'absolute',
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Centering with transform: translateX(-50%)
left: '50%',
top: '50%',
transform: 'translateY(-50%) translateX(-50%)',
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/common/NotificationCountBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const NotificationCountBadge = (props: NotificationCountBadgeProps) => {
as='span'
sx={[
t => ({
marginLeft: t.space.$1x5,
marginInlineStart: t.space.$1x5,
}),
containerSx,
]}
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/common/PrintableComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const PrintableComponent = (props: UsePrintableReturn['printableProps'] &
return (
<div
ref={ref}
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Off-screen hide for print functionality
style={{ position: 'fixed', left: '-9999px', top: 0, display: 'none' }}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const OrganizationPreviewSpinner = forwardRef<HTMLDivElement>((_, ref) =>
sx={{
margin: 'auto',
position: 'absolute',
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Centering with transform: translateX(-50%)
left: '50%',
top: '50%',
transform: 'translateY(-50%) translateX(-50%)',
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/APIKeys/APIKeyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const getScopedPortalContainerStyles = (modalRoot?: React.MutableRefObject<HTMLE
modalRoot
? t => ({
position: 'absolute',
right: 0,
insetInlineEnd: 0,
bottom: 0,
backgroundColor: 'inherit',
backdropFilter: `blur(${t.sizes.$2})`,
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/APIKeys/ApiKeysTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const APIKeysTable = ({
<Tr>
<Th>Name</Th>
<Th>Last used</Th>
{canManageAPIKeys && <Th sx={{ textAlign: 'right' }}>Actions</Th>}
{canManageAPIKeys && <Th sx={{ textAlign: 'end' }}>Actions</Th>}
</Tr>
</Thead>
<Tbody>
Expand Down Expand Up @@ -98,7 +98,7 @@ export const APIKeysTable = ({
</Box>
</Td>
{canManageAPIKeys && (
<Td sx={{ textAlign: 'right' }}>
<Td sx={{ textAlign: 'end' }}>
<ThreeDotsMenu
actions={[
{
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/APIKeys/CopyAPIKeyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const CopyAPIKeyModal = ({
>
<Card.Content
sx={t => ({
textAlign: 'left',
textAlign: 'start',
padding: `${t.sizes.$4} ${t.sizes.$5} ${t.sizes.$4} ${t.sizes.$6}`,
})}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const RevokeAPIKeyConfirmationModal = ({
>
<Card.Content
sx={t => ({
textAlign: 'left',
textAlign: 'start',
padding: `${t.sizes.$4} ${t.sizes.$5} ${t.sizes.$4} ${t.sizes.$6}`,
})}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
headerSubtitle={props?.startPage?.headerSubtitle}
headerTitleTextVariant={headerTitleTextVariant}
headerSubtitleTextVariant={headerSubtitleTextVariant}
sx={t => ({ minHeight: t.sizes.$60, gap: t.space.$6, textAlign: 'left' })}
sx={t => ({ minHeight: t.sizes.$60, gap: t.space.$6, textAlign: 'start' })}
>
<Form.Root
onSubmit={onSubmit}
Expand Down Expand Up @@ -224,7 +224,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
headerTitle={localizationKeys('organizationProfile.invitePage.title')}
headerTitleTextVariant={headerTitleTextVariant}
headerSubtitleTextVariant={headerSubtitleTextVariant}
sx={t => ({ minHeight: t.sizes.$60, textAlign: 'left' })}
sx={t => ({ minHeight: t.sizes.$60, textAlign: 'start' })}
>
{organization && (
<InviteMembersForm
Expand All @@ -239,7 +239,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
<Header.Root>
<Header.Title
localizationKey={localizationKeys('organizationProfile.invitePage.title')}
sx={{ textAlign: 'left' }}
sx={{ textAlign: 'start' }}
/>
</Header.Root>
<SuccessPage
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/components/ImpersonationFab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ const FabContent = ({ title, signOutText }: FabContentProps) => {
<Col
sx={t => ({
width: '100%',
paddingLeft: t.sizes.$4,
paddingRight: t.sizes.$6,
paddingInlineStart: t.sizes.$4,
paddingInlineEnd: t.sizes.$6,
whiteSpace: 'nowrap',
})}
>
Expand Down Expand Up @@ -199,6 +199,7 @@ const ImpersonationFabInternal = () => {
position: 'fixed',
overflow: 'hidden',
top: `var(${topProperty}, ${defaultTop}px)`,
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Complex JS-based positioning via CSS custom properties
right: `var(${rightProperty}, ${defaultRight}px)`,
zIndex: t.zIndices.$fab,
boxShadow: t.shadows.$fabShadow,
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/components/OAuthConsent/OAuthConsent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function OAuthConsentInternal() {
sx={t => ({
position: 'absolute',
bottom: `calc(${t.space.$3} * -1)`,
right: `calc(${t.space.$3} * -1)`,
insetInlineEnd: `calc(${t.space.$3} * -1)`,
})}
/>
</Box>
Expand Down Expand Up @@ -112,7 +112,7 @@ export function OAuthConsentInternal() {
</Header.Root>
<Box
sx={t => ({
textAlign: 'left',
textAlign: 'start',
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$solid,
borderColor: t.colors.$borderAlpha100,
Expand Down Expand Up @@ -157,7 +157,7 @@ export function OAuthConsentInternal() {
background: t.colors.$colorMutedForeground,
borderRadius: t.radii.$circle,
transform: 'translateY(-0.1875rem)',
marginRight: t.space.$2,
marginInlineEnd: t.space.$2,
flexShrink: 0,
},
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const MemberRow = (props: {
/>
</Protect>
</Td>
<Td sx={{ textAlign: 'right' }}>
<Td sx={{ textAlign: 'end' }}>
<Protect permission={'org:sys_memberships:manage'}>
<ThreeDotsMenu
actions={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export const DomainList = withProtect(
display: 'flex',
margin: 'auto',
position: 'absolute',
// eslint-disable-next-line custom-rules/no-physical-css-properties -- Centering with transform: translateX(-50%)
left: '50%',
top: '50%',
transform: 'translateY(-50%) translateX(-50%)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const InvitationRow = (props: {
localizationKey={localizeCustomRole(invitation.role) || unlocalizedRoleLabel}
/>
</Td>
<Td sx={{ textAlign: 'right' }}>
<Td sx={{ textAlign: 'end' }}>
<ThreeDotsMenu
actions={[
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const MembersActionsRow = ({ actionSlot }: MembersActionsRowProps) => {
justify={actionSlot ? 'between' : 'end'}
sx={t => ({
width: '100%',
marginLeft: 'auto',
marginInlineStart: 'auto',
padding: `${t.space.$none} ${t.space.$1}`,
})}
gap={actionSlot ? 2 : undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ const OrganizationDomainsSection = () => {
<Text
localizationKey={localizationKeys('organizationProfile.profilePage.domainSection.subtitle')}
sx={t => ({
paddingLeft: t.space.$9,
paddingInlineStart: t.space.$9,
})}
colorScheme='secondary'
/>
Expand Down Expand Up @@ -198,7 +198,7 @@ const OrganizationLeaveSection = () => {
sx={t => ({
paddingTop: 0,
paddingBottom: 0,
paddingLeft: t.space.$1,
paddingInlineStart: t.space.$1,
})}
id='organizationDanger'
>
Expand Down Expand Up @@ -252,7 +252,7 @@ const OrganizationDeleteSection = () => {
sx={t => ({
paddingTop: 0,
paddingBottom: 0,
paddingLeft: t.space.$1,
paddingInlineStart: t.space.$1,
})}
id={'organizationDanger'}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export const OrganizationMembersTabInvitations = withCardStateProvider(() => {
width: '100%',
gap: t.space.$8,
paddingBottom: t.space.$4,
paddingLeft: t.space.$1,
paddingRight: t.space.$1,
paddingInlineStart: t.space.$1,
paddingInlineEnd: t.space.$1,
borderBottomWidth: t.borderWidths.$normal,
borderBottomStyle: t.borderStyles.$solid,
borderBottomColor: t.colors.$borderAlpha100,
Expand Down Expand Up @@ -68,10 +68,10 @@ export const OrganizationMembersTabInvitations = withCardStateProvider(() => {
'organizationProfile.membersPage.invitationsTab.autoInvitations.headerSubtitle',
)}
sx={t => ({
paddingLeft: t.space.$10,
paddingInlineStart: t.space.$10,
color: t.colors.$colorMutedForeground,
[mqu.md]: {
paddingLeft: 0,
paddingInlineStart: 0,
},
})}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export const OrganizationMembersTabRequests = () => {
width: '100%',
gap: t.space.$8,
paddingBottom: t.space.$4,
paddingLeft: t.space.$1,
paddingRight: t.space.$1,
paddingInlineStart: t.space.$1,
paddingInlineEnd: t.space.$1,
borderBottomWidth: t.borderWidths.$normal,
borderBottomStyle: t.borderStyles.$solid,
borderBottomColor: t.colors.$borderAlpha100,
Expand Down Expand Up @@ -67,10 +67,10 @@ export const OrganizationMembersTabRequests = () => {
'organizationProfile.membersPage.requestsTab.autoSuggestions.headerSubtitle',
)}
sx={t => ({
paddingLeft: t.space.$10,
paddingInlineStart: t.space.$10,
color: t.colors.$colorMutedForeground,
[mqu.md]: {
paddingLeft: 0,
paddingInlineStart: 0,
},
})}
/>
Expand Down
Loading