From 82422095be1d570534951100a271a59ed12a7fee Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 12 May 2025 10:55:15 -0400 Subject: [PATCH 1/6] fix(clerk-js): Ensure RTL is supported within Drawer component --- .../src/ui/components/Plans/PlanDetails.tsx | 4 +- packages/clerk-js/src/ui/elements/Drawer.tsx | 14 ++- .../ui/hooks/__tests__/useDirection.test.ts | 93 +++++++++++++++++++ packages/clerk-js/src/ui/hooks/index.ts | 1 + .../clerk-js/src/ui/hooks/useDirection.ts | 32 +++++++ 5 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 packages/clerk-js/src/ui/hooks/__tests__/useDirection.test.ts create mode 100644 packages/clerk-js/src/ui/hooks/useDirection.ts diff --git a/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx b/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx index c513cf54c5e..d2393f6af18 100644 --- a/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx +++ b/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx @@ -360,7 +360,7 @@ const Header = React.forwardRef((props, ref) => { sx={t => ({ position: 'absolute', top: t.space.$2, - right: t.space.$2, + insetInlineEnd: t.space.$2, })} > {closeSlot} @@ -382,7 +382,7 @@ const Header = React.forwardRef((props, ref) => { ) : null} ({ - paddingRight: t.space.$10, + paddingBlockEnd: t.space.$10, })} > ['context']; getFloatingProps: ReturnType['getFloatingProps']; portalProps: FloatingPortalProps; + direction: 'ltr' | 'rtl'; } const DrawerContext = React.createContext(null); @@ -87,12 +88,14 @@ function Root({ portalProps, dismissProps, }: RootProps) { + const direction = useDirection(); + const { refs, context } = useFloating({ open, onOpenChange, transform: false, strategy, - placement: 'right', + placement: direction === 'ltr' ? 'right' : 'left', ...floatingProps, }); @@ -112,6 +115,7 @@ function Root({ refs, context, getFloatingProps, + direction, }} > {children} @@ -195,7 +199,7 @@ const Content = React.forwardRef(({ children }, re const prefersReducedMotion = usePrefersReducedMotion(); const { animations: layoutAnimations } = useAppearance().parsedLayout; const isMotionSafe = !prefersReducedMotion && layoutAnimations === true; - const { strategy, refs, context, getFloatingProps } = useDrawerContext(); + const { strategy, refs, context, getFloatingProps, direction } = useDrawerContext(); const mergedRefs = useMergeRefs([ref, refs.setFloating]); const { isMounted, styles: transitionStyles } = useTransitionStyles(context, { @@ -236,7 +240,9 @@ const Content = React.forwardRef(({ children }, re // Apply the conditional right offset + the spread of the // box shadow to ensure it is fully offscreen before unmounting '--transform-offset': - strategy === 'fixed' ? `calc(100% + ${t.space.$3} + ${t.space.$8x75})` : `calc(100% + ${t.space.$8x75})`, + strategy === 'fixed' + ? `calc((100% + ${t.space.$3} + ${t.space.$8x75}) * ${direction === 'rtl' ? -1 : 1})` + : `calc((100% + ${t.space.$8x75}) * ${direction === 'rtl' ? -1 : 1})`, willChange: 'transform', position: strategy, insetBlock: strategy === 'fixed' ? t.space.$3 : 0, diff --git a/packages/clerk-js/src/ui/hooks/__tests__/useDirection.test.ts b/packages/clerk-js/src/ui/hooks/__tests__/useDirection.test.ts new file mode 100644 index 00000000000..c4762f40138 --- /dev/null +++ b/packages/clerk-js/src/ui/hooks/__tests__/useDirection.test.ts @@ -0,0 +1,93 @@ +import { renderHook } from '@testing-library/react'; + +import { useDirection } from '../useDirection'; + +describe('useDirection', () => { + const originalWindow = window; + const mockGetComputedStyle = jest.fn(); + + beforeEach(() => { + // Mock window.getComputedStyle + mockGetComputedStyle.mockReset(); + Object.defineProperty(window, 'getComputedStyle', { + value: mockGetComputedStyle, + writable: true, + }); + }); + + afterEach(() => { + // Restore window + Object.defineProperty(global, 'window', { + value: originalWindow, + writable: true, + }); + }); + + describe('SSR environment', () => { + const originalWindow = global.window; + + beforeEach(() => { + // @ts-ignore - Intentionally removing window for SSR test + delete global.window; + }); + + afterEach(() => { + global.window = originalWindow; + }); + + it('returns ltr when window is undefined', () => { + expect(useDirection()).toBe('ltr'); + }); + }); + + describe('Browser environment', () => { + it('returns rtl when element has dir="rtl"', () => { + const element = document.createElement('div'); + element.dir = 'rtl'; + + const { result } = renderHook(() => useDirection(element)); + expect(result.current).toBe('rtl'); + }); + + it('returns ltr when element has dir="ltr"', () => { + const element = document.createElement('div'); + element.dir = 'ltr'; + + const { result } = renderHook(() => useDirection(element)); + expect(result.current).toBe('ltr'); + }); + + it('returns rtl when element has computed direction rtl', () => { + const element = document.createElement('div'); + element.dir = 'auto'; + mockGetComputedStyle.mockReturnValue({ direction: 'rtl' }); + + const { result } = renderHook(() => useDirection(element)); + expect(result.current).toBe('rtl'); + }); + + it('returns ltr when element has no dir attribute', () => { + const element = document.createElement('div'); + mockGetComputedStyle.mockReturnValue({ direction: 'ltr' }); + + const { result } = renderHook(() => useDirection(element)); + expect(result.current).toBe('ltr'); + }); + + it('uses document.documentElement when no element is provided', () => { + document.documentElement.dir = 'rtl'; + + const { result } = renderHook(() => useDirection()); + expect(result.current).toBe('rtl'); + }); + + it('prioritizes element direction over document direction', () => { + document.documentElement.dir = 'rtl'; + const element = document.createElement('div'); + element.dir = 'ltr'; + + const { result } = renderHook(() => useDirection(element)); + expect(result.current).toBe('ltr'); + }); + }); +}); diff --git a/packages/clerk-js/src/ui/hooks/index.ts b/packages/clerk-js/src/ui/hooks/index.ts index c60cc21955a..aef26d8c7c1 100644 --- a/packages/clerk-js/src/ui/hooks/index.ts +++ b/packages/clerk-js/src/ui/hooks/index.ts @@ -3,6 +3,7 @@ export * from './useClerkModalStateParams'; export * from './useClipboard'; export * from './useDebounce'; export * from './useDelayedVisibility'; +export * from './useDirection'; export * from './useEmailLink'; export * from './useEnabledThirdPartyProviders'; export * from './useEnterpriseSSOLink'; diff --git a/packages/clerk-js/src/ui/hooks/useDirection.ts b/packages/clerk-js/src/ui/hooks/useDirection.ts new file mode 100644 index 00000000000..75607ca54ad --- /dev/null +++ b/packages/clerk-js/src/ui/hooks/useDirection.ts @@ -0,0 +1,32 @@ +function getDirectionFromElement(element: HTMLElement): 'ltr' | 'rtl' { + const dir = element.dir; + + if (dir === 'rtl') { + return 'rtl'; + } + + if (dir === 'ltr') { + return 'ltr'; + } + + if (dir === 'auto' || !dir) { + const computedDirection = window.getComputedStyle(element).direction; + if (computedDirection === 'rtl') { + return 'rtl'; + } + } + + return 'ltr'; +} + +export function useDirection(element?: HTMLElement) { + if (typeof window === 'undefined') { + return 'ltr'; + } + + if (element) { + return getDirectionFromElement(element); + } + + return getDirectionFromElement(document.documentElement); +} From 2e5279fd6347f652f7769240a08df93ecdd9f59e Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 12 May 2025 13:01:49 -0400 Subject: [PATCH 2/6] fixes --- .../src/ui/components/PricingTable/PricingTableDefault.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx index a384d16b2d2..20294a884a8 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx @@ -334,6 +334,9 @@ const CardHeader = React.forwardRef((props, ref elementDescriptor={descriptors.pricingTableCardDescription} variant='subtitle' colorScheme='secondary' + sx={{ + justifySelf: 'flex-start', + }} > {plan.description} @@ -397,6 +400,7 @@ const CardHeader = React.forwardRef((props, ref plan.isDefault ? localizationKeys('commerce.alwaysFree') : localizationKeys('commerce.billedMonthlyOnly') } sx={{ + justifySelf: 'flex-start', alignSelf: 'center', }} /> From 3bba27db11a2f191176adf6c5c7c51edbc7a997e Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 12 May 2025 13:06:58 -0400 Subject: [PATCH 3/6] add changeset --- .changeset/slick-pets-love.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slick-pets-love.md diff --git a/.changeset/slick-pets-love.md b/.changeset/slick-pets-love.md new file mode 100644 index 00000000000..ed831c97fbd --- /dev/null +++ b/.changeset/slick-pets-love.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Ensure Checkout drawer animation and content respects RTL usage. From 8a74a115a44d4d7750b62e37a297edb3f7fc2cdd Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 12 May 2025 13:30:50 -0400 Subject: [PATCH 4/6] bump --- packages/clerk-js/bundlewatch.config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index c29df0179b4..03f755519df 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,10 +1,10 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "594kB" }, + { "path": "./dist/clerk.js", "maxSize": "594.1kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "68.3KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "52KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "104.1KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "104.4KB" }, { "path": "./dist/vendors*.js", "maxSize": "39.5KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, From ce9376f6e7a0e9f5e408727d82e3fc3342f02c7a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 13 May 2025 09:44:17 -0400 Subject: [PATCH 5/6] add test case --- .../clerk-js/src/ui/hooks/__tests__/useDirection.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/clerk-js/src/ui/hooks/__tests__/useDirection.test.ts b/packages/clerk-js/src/ui/hooks/__tests__/useDirection.test.ts index c4762f40138..63540b11d05 100644 --- a/packages/clerk-js/src/ui/hooks/__tests__/useDirection.test.ts +++ b/packages/clerk-js/src/ui/hooks/__tests__/useDirection.test.ts @@ -74,6 +74,15 @@ describe('useDirection', () => { expect(result.current).toBe('ltr'); }); + it('returns ltr when element has invalid dir attribute value', () => { + const element = document.createElement('div'); + element.dir = 'test'; + mockGetComputedStyle.mockReturnValue({ direction: 'ltr' }); + + const { result } = renderHook(() => useDirection(element)); + expect(result.current).toBe('ltr'); + }); + it('uses document.documentElement when no element is provided', () => { document.documentElement.dir = 'rtl'; From 84fbac474dc6099eca51157aaad9727a821eb075 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 13 May 2025 09:44:27 -0400 Subject: [PATCH 6/6] use ReturnType --- packages/clerk-js/src/ui/elements/Drawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/elements/Drawer.tsx b/packages/clerk-js/src/ui/elements/Drawer.tsx index 9222db3e946..cf542318381 100644 --- a/packages/clerk-js/src/ui/elements/Drawer.tsx +++ b/packages/clerk-js/src/ui/elements/Drawer.tsx @@ -38,7 +38,7 @@ interface DrawerContext { context: ReturnType['context']; getFloatingProps: ReturnType['getFloatingProps']; portalProps: FloatingPortalProps; - direction: 'ltr' | 'rtl'; + direction: ReturnType; } const DrawerContext = React.createContext(null);