diff --git a/.changeset/free-crabs-watch.md b/.changeset/free-crabs-watch.md new file mode 100644 index 000000000..62759a335 --- /dev/null +++ b/.changeset/free-crabs-watch.md @@ -0,0 +1,5 @@ +--- +'@radix-ui/react-direction': patch +--- + +Respect document-level dir attribute in useDirection when no DirectionProvider is present diff --git a/packages/react/direction/src/direction.test.tsx b/packages/react/direction/src/direction.test.tsx new file mode 100644 index 000000000..f2a749f0d --- /dev/null +++ b/packages/react/direction/src/direction.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import type { RenderResult } from '@testing-library/react'; +import { cleanup, render } from '@testing-library/react'; +import { DirectionProvider, useDirection } from './direction'; +import { afterEach, describe, it, beforeEach, expect } from 'vitest'; + +// Helper component that renders the resolved direction as text +function DirectionDisplay({ localDir }: { localDir?: 'ltr' | 'rtl' }) { + const direction = useDirection(localDir); + return {direction}; +} + +describe('useDirection', () => { + let rendered: RenderResult; + + afterEach(() => { + cleanup(); + // Reset document dir after each test + document.documentElement.dir = ''; + }); + + describe('given no DirectionProvider and no local dir', () => { + describe('when document dir is not set', () => { + beforeEach(() => { + document.documentElement.dir = ''; + rendered = render(); + }); + + it('should default to ltr', () => { + expect(rendered.getByTestId('direction').textContent).toBe('ltr'); + }); + }); + + describe('when document dir is rtl', () => { + beforeEach(() => { + document.documentElement.dir = 'rtl'; + rendered = render(); + }); + + it('should return rtl from document', () => { + expect(rendered.getByTestId('direction').textContent).toBe('rtl'); + }); + }); + + describe('when document dir is ltr', () => { + beforeEach(() => { + document.documentElement.dir = 'ltr'; + rendered = render(); + }); + + it('should return ltr from document', () => { + expect(rendered.getByTestId('direction').textContent).toBe('ltr'); + }); + }); + }); + + describe('given a DirectionProvider with dir="rtl"', () => { + describe('when document dir is ltr', () => { + beforeEach(() => { + document.documentElement.dir = 'ltr'; + rendered = render( + + + , + ); + }); + + it('should return rtl from provider (overrides document)', () => { + expect(rendered.getByTestId('direction').textContent).toBe('rtl'); + }); + }); + }); + + describe('given a DirectionProvider with dir="ltr"', () => { + describe('when document dir is rtl', () => { + beforeEach(() => { + document.documentElement.dir = 'rtl'; + rendered = render( + + + , + ); + }); + + it('should return ltr from provider (overrides document)', () => { + expect(rendered.getByTestId('direction').textContent).toBe('ltr'); + }); + }); + }); + + describe('given a local dir prop', () => { + describe('when DirectionProvider is rtl and document is rtl', () => { + beforeEach(() => { + document.documentElement.dir = 'rtl'; + rendered = render( + + + , + ); + }); + + it('should return ltr from local dir (highest priority)', () => { + expect(rendered.getByTestId('direction').textContent).toBe('ltr'); + }); + }); + }); +}); diff --git a/packages/react/direction/src/direction.tsx b/packages/react/direction/src/direction.tsx index 9ab26b44a..307dabfd7 100644 --- a/packages/react/direction/src/direction.tsx +++ b/packages/react/direction/src/direction.tsx @@ -20,9 +20,16 @@ const DirectionProvider: React.FC = (props) => { function useDirection(localDir?: Direction) { const globalDir = React.useContext(DirectionContext); - return localDir || globalDir || 'ltr'; + const documentDir = getDocumentDirection(); + return localDir || globalDir || documentDir; } +function getDocumentDirection(): Direction { + if (typeof document === 'undefined') return 'ltr'; + return document.documentElement.dir === 'rtl' ? 'rtl' : 'ltr'; +} + + const Provider = DirectionProvider; export {