Skip to content

Commit 0801b02

Browse files
fix(clerk-js): Ensure RTL is supported within Drawer component (#5906)
1 parent 80bdc58 commit 0801b02

8 files changed

Lines changed: 158 additions & 8 deletions

File tree

.changeset/slick-pets-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Ensure Checkout drawer animation and content respects RTL usage.

packages/clerk-js/bundlewatch.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"files": [
3-
{ "path": "./dist/clerk.js", "maxSize": "594kB" },
3+
{ "path": "./dist/clerk.js", "maxSize": "594.1kB" },
44
{ "path": "./dist/clerk.browser.js", "maxSize": "68.3KB" },
55
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" },
66
{ "path": "./dist/clerk.headless*.js", "maxSize": "52KB" },
7-
{ "path": "./dist/ui-common*.js", "maxSize": "104.05KB" },
7+
{ "path": "./dist/ui-common*.js", "maxSize": "104.4KB" },
88
{ "path": "./dist/vendors*.js", "maxSize": "39.5KB" },
99
{ "path": "./dist/coinbase*.js", "maxSize": "38KB" },
1010
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },

packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ const Header = React.forwardRef<HTMLDivElement, HeaderProps>((props, ref) => {
360360
sx={t => ({
361361
position: 'absolute',
362362
top: t.space.$2,
363-
right: t.space.$2,
363+
insetInlineEnd: t.space.$2,
364364
})}
365365
>
366366
{closeSlot}
@@ -382,7 +382,7 @@ const Header = React.forwardRef<HTMLDivElement, HeaderProps>((props, ref) => {
382382
) : null}
383383
<Box
384384
sx={t => ({
385-
paddingRight: t.space.$10,
385+
paddingBlockEnd: t.space.$10,
386386
})}
387387
>
388388
<Flex

packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>((props, ref
349349
elementDescriptor={descriptors.pricingTableCardDescription}
350350
variant='subtitle'
351351
colorScheme='secondary'
352+
sx={{
353+
justifySelf: 'flex-start',
354+
}}
352355
>
353356
{plan.description}
354357
</Text>
@@ -412,6 +415,7 @@ const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>((props, ref
412415
plan.isDefault ? localizationKeys('commerce.alwaysFree') : localizationKeys('commerce.billedMonthlyOnly')
413416
}
414417
sx={{
418+
justifySelf: 'flex-start',
415419
alignSelf: 'center',
416420
}}
417421
/>

packages/clerk-js/src/ui/elements/Drawer.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as React from 'react';
1616
import { transitionDurationValues, transitionTiming } from '../../ui/foundations/transitions';
1717
import type { LocalizationKey } from '../customizables';
1818
import { Box, descriptors, Flex, Heading, Icon, Span, useAppearance } from '../customizables';
19-
import { usePrefersReducedMotion } from '../hooks';
19+
import { useDirection, usePrefersReducedMotion } from '../hooks';
2020
import { useScrollLock } from '../hooks/useScrollLock';
2121
import { Close as CloseIcon } from '../icons';
2222
import type { ThemableCssProp } from '../styledSystem';
@@ -38,6 +38,7 @@ interface DrawerContext {
3838
context: ReturnType<typeof useFloating>['context'];
3939
getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps'];
4040
portalProps: FloatingPortalProps;
41+
direction: ReturnType<typeof useDirection>;
4142
}
4243

4344
const DrawerContext = React.createContext<DrawerContext | null>(null);
@@ -87,12 +88,14 @@ function Root({
8788
portalProps,
8889
dismissProps,
8990
}: RootProps) {
91+
const direction = useDirection();
92+
9093
const { refs, context } = useFloating({
9194
open,
9295
onOpenChange,
9396
transform: false,
9497
strategy,
95-
placement: 'right',
98+
placement: direction === 'ltr' ? 'right' : 'left',
9699
...floatingProps,
97100
});
98101

@@ -112,6 +115,7 @@ function Root({
112115
refs,
113116
context,
114117
getFloatingProps,
118+
direction,
115119
}}
116120
>
117121
<FloatingPortal {...portalProps}>{children}</FloatingPortal>
@@ -195,7 +199,7 @@ const Content = React.forwardRef<HTMLDivElement, ContentProps>(({ children }, re
195199
const prefersReducedMotion = usePrefersReducedMotion();
196200
const { animations: layoutAnimations } = useAppearance().parsedLayout;
197201
const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
198-
const { strategy, refs, context, getFloatingProps } = useDrawerContext();
202+
const { strategy, refs, context, getFloatingProps, direction } = useDrawerContext();
199203
const mergedRefs = useMergeRefs([ref, refs.setFloating]);
200204

201205
const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
@@ -236,7 +240,9 @@ const Content = React.forwardRef<HTMLDivElement, ContentProps>(({ children }, re
236240
// Apply the conditional right offset + the spread of the
237241
// box shadow to ensure it is fully offscreen before unmounting
238242
'--transform-offset':
239-
strategy === 'fixed' ? `calc(100% + ${t.space.$3} + ${t.space.$8x75})` : `calc(100% + ${t.space.$8x75})`,
243+
strategy === 'fixed'
244+
? `calc((100% + ${t.space.$3} + ${t.space.$8x75}) * ${direction === 'rtl' ? -1 : 1})`
245+
: `calc((100% + ${t.space.$8x75}) * ${direction === 'rtl' ? -1 : 1})`,
240246
willChange: 'transform',
241247
position: strategy,
242248
insetBlock: strategy === 'fixed' ? t.space.$3 : 0,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { renderHook } from '@testing-library/react';
2+
3+
import { useDirection } from '../useDirection';
4+
5+
describe('useDirection', () => {
6+
const originalWindow = window;
7+
const mockGetComputedStyle = jest.fn();
8+
9+
beforeEach(() => {
10+
// Mock window.getComputedStyle
11+
mockGetComputedStyle.mockReset();
12+
Object.defineProperty(window, 'getComputedStyle', {
13+
value: mockGetComputedStyle,
14+
writable: true,
15+
});
16+
});
17+
18+
afterEach(() => {
19+
// Restore window
20+
Object.defineProperty(global, 'window', {
21+
value: originalWindow,
22+
writable: true,
23+
});
24+
});
25+
26+
describe('SSR environment', () => {
27+
const originalWindow = global.window;
28+
29+
beforeEach(() => {
30+
// @ts-ignore - Intentionally removing window for SSR test
31+
delete global.window;
32+
});
33+
34+
afterEach(() => {
35+
global.window = originalWindow;
36+
});
37+
38+
it('returns ltr when window is undefined', () => {
39+
expect(useDirection()).toBe('ltr');
40+
});
41+
});
42+
43+
describe('Browser environment', () => {
44+
it('returns rtl when element has dir="rtl"', () => {
45+
const element = document.createElement('div');
46+
element.dir = 'rtl';
47+
48+
const { result } = renderHook(() => useDirection(element));
49+
expect(result.current).toBe('rtl');
50+
});
51+
52+
it('returns ltr when element has dir="ltr"', () => {
53+
const element = document.createElement('div');
54+
element.dir = 'ltr';
55+
56+
const { result } = renderHook(() => useDirection(element));
57+
expect(result.current).toBe('ltr');
58+
});
59+
60+
it('returns rtl when element has computed direction rtl', () => {
61+
const element = document.createElement('div');
62+
element.dir = 'auto';
63+
mockGetComputedStyle.mockReturnValue({ direction: 'rtl' });
64+
65+
const { result } = renderHook(() => useDirection(element));
66+
expect(result.current).toBe('rtl');
67+
});
68+
69+
it('returns ltr when element has no dir attribute', () => {
70+
const element = document.createElement('div');
71+
mockGetComputedStyle.mockReturnValue({ direction: 'ltr' });
72+
73+
const { result } = renderHook(() => useDirection(element));
74+
expect(result.current).toBe('ltr');
75+
});
76+
77+
it('returns ltr when element has invalid dir attribute value', () => {
78+
const element = document.createElement('div');
79+
element.dir = 'test';
80+
mockGetComputedStyle.mockReturnValue({ direction: 'ltr' });
81+
82+
const { result } = renderHook(() => useDirection(element));
83+
expect(result.current).toBe('ltr');
84+
});
85+
86+
it('uses document.documentElement when no element is provided', () => {
87+
document.documentElement.dir = 'rtl';
88+
89+
const { result } = renderHook(() => useDirection());
90+
expect(result.current).toBe('rtl');
91+
});
92+
93+
it('prioritizes element direction over document direction', () => {
94+
document.documentElement.dir = 'rtl';
95+
const element = document.createElement('div');
96+
element.dir = 'ltr';
97+
98+
const { result } = renderHook(() => useDirection(element));
99+
expect(result.current).toBe('ltr');
100+
});
101+
});
102+
});

packages/clerk-js/src/ui/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './useClerkModalStateParams';
22
export * from './useClipboard';
33
export * from './useDebounce';
44
export * from './useDelayedVisibility';
5+
export * from './useDirection';
56
export * from './useEmailLink';
67
export * from './useEnabledThirdPartyProviders';
78
export * from './useEnterpriseSSOLink';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
function getDirectionFromElement(element: HTMLElement): 'ltr' | 'rtl' {
2+
const dir = element.dir;
3+
4+
if (dir === 'rtl') {
5+
return 'rtl';
6+
}
7+
8+
if (dir === 'ltr') {
9+
return 'ltr';
10+
}
11+
12+
if (dir === 'auto' || !dir) {
13+
const computedDirection = window.getComputedStyle(element).direction;
14+
if (computedDirection === 'rtl') {
15+
return 'rtl';
16+
}
17+
}
18+
19+
return 'ltr';
20+
}
21+
22+
export function useDirection(element?: HTMLElement) {
23+
if (typeof window === 'undefined') {
24+
return 'ltr';
25+
}
26+
27+
if (element) {
28+
return getDirectionFromElement(element);
29+
}
30+
31+
return getDirectionFromElement(document.documentElement);
32+
}

0 commit comments

Comments
 (0)