diff --git a/.changeset/mosaic-direction-provider.md b/.changeset/mosaic-direction-provider.md new file mode 100644 index 00000000000..d1559d55734 --- /dev/null +++ b/.changeset/mosaic-direction-provider.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': patch +--- + +Add a `direction` prop to `MosaicProvider` (`'ltr' | 'rtl'`, default `'ltr'`). Arrow-key navigation in tabs, segmented controls, one-time code inputs, tag inputs, and nested menus now follows this prop instead of the nearest `dir` attribute. Pages that render right-to-left should pass `direction="rtl"`. diff --git a/packages/mosaic/ARCHITECTURE.md b/packages/mosaic/ARCHITECTURE.md index c070b80702b..4e892d1270e 100644 --- a/packages/mosaic/ARCHITECTURE.md +++ b/packages/mosaic/ARCHITECTURE.md @@ -74,7 +74,7 @@ State styling uses real class + attribute-selector specificity — no `&&` boost ## MosaicProvider -Mosaic components need no provider to render or to be styled — the stylesheet and the `--cl-*` tokens do that work. `MosaicProvider` exists for three things: per-name icon glyph overrides, localization, and the toast region (it mounts `ToastProvider`). +Mosaic components need no provider to render or to be styled — the stylesheet and the `--cl-*` tokens do that work. `MosaicProvider` exists for four things: per-name icon glyph overrides, localization, the reading direction keyboard navigation follows (it mounts `DirectionProvider`), and the toast region (it mounts `ToastProvider`). ```tsx import { MosaicProvider } from './mosaic-provider'; diff --git a/packages/mosaic/docs/headless.md b/packages/mosaic/docs/headless.md index 28d2e893008..1566e746658 100644 --- a/packages/mosaic/docs/headless.md +++ b/packages/mosaic/docs/headless.md @@ -133,6 +133,14 @@ Any new part with an exit transition needs one of the two; animated element). Add `cssVars()` to floating-ui middleware **after** `arrow()`, or the transform origin won't follow the arrow. +## Reading direction + +Read the direction with `useDirection()` from `primitives/direction-provider`, never from the DOM +`dir` attribute or computed style. It returns `ltr` outside a `DirectionProvider`. Use +`readingDirectionKeys(direction)` for the previous and next arrow keys, or pass +`rtl: direction === 'rtl'` to floating-ui's `Composite` or `useListNavigation` +(`tabs/tabs-list.tsx`). `MosaicProvider` renders the `DirectionProvider` from its `direction` prop. + ## Testing Primitive tests run on **happy-dom** (the `primitives` project in Mosaic's diff --git a/packages/mosaic/src/__tests__/mosaic-provider.test.tsx b/packages/mosaic/src/__tests__/mosaic-provider.test.tsx index a6b1b81c2d1..dcbd1f61531 100644 --- a/packages/mosaic/src/__tests__/mosaic-provider.test.tsx +++ b/packages/mosaic/src/__tests__/mosaic-provider.test.tsx @@ -6,6 +6,7 @@ import type { MosaicIconOverrides } from '../icons/overrides'; import { useMosaicIcons } from '../icons/overrides'; import { useLocale, useMessages } from '../localization'; import { MosaicProvider } from '../mosaic-provider'; +import { useDirection } from '../primitives/direction-provider'; describe('MosaicProvider icons', () => { it('exposes the icon overrides via useMosaicIcons', () => { @@ -49,3 +50,19 @@ describe('MosaicProvider localization', () => { expect(result.current).toBe('de'); }); }); + +describe('MosaicProvider direction', () => { + it('passes the direction prop to useDirection', () => { + const { result } = renderHook(() => useDirection(), { + wrapper: ({ children }) => React.createElement(MosaicProvider, { direction: 'rtl' }, children), + }); + expect(result.current).toBe('rtl'); + }); + + it('defaults to ltr', () => { + const { result } = renderHook(() => useDirection(), { + wrapper: ({ children }) => React.createElement(MosaicProvider, {}, children), + }); + expect(result.current).toBe('ltr'); + }); +}); diff --git a/packages/mosaic/src/components/segmented-control/segmented-control.test.tsx b/packages/mosaic/src/components/segmented-control/segmented-control.test.tsx index fb5611ae43c..b16df7a9750 100644 --- a/packages/mosaic/src/components/segmented-control/segmented-control.test.tsx +++ b/packages/mosaic/src/components/segmented-control/segmented-control.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { describe, expect, it, vi } from 'vitest'; +import { DirectionProvider } from '../../primitives/direction-provider'; import { SegmentedControl } from './segmented-control'; function BillingPeriod(props: Partial>) { @@ -58,9 +59,9 @@ describe('Mosaic SegmentedControl', () => { it('flips the arrow keys in a right-to-left layout', async () => { render( -
+ -
, + , ); await userEvent.tab(); diff --git a/packages/mosaic/src/mosaic-provider.tsx b/packages/mosaic/src/mosaic-provider.tsx index 78e30da3b48..7eb3f61e87f 100644 --- a/packages/mosaic/src/mosaic-provider.tsx +++ b/packages/mosaic/src/mosaic-provider.tsx @@ -8,9 +8,13 @@ import type { MosaicLocalization } from './localization'; import { MosaicLocalizationProvider, resolveLocalization } from './localization'; import type { ModalSurfaces } from './modals/modal.types'; import { ModalHost } from './modals/modal-host'; +import type { TextDirection } from './primitives/direction-provider'; +import { DirectionProvider } from './primitives/direction-provider'; export interface MosaicProviderProps { children: React.ReactNode; + /** Reading direction that arrow-key navigation follows. Set it to match the page's `dir`. Defaults to `ltr`. */ + direction?: TextDirection; /** Per-name icon glyph overrides, applied to every `` below this provider. */ icons?: MosaicIconOverrides; /** Locale and strings for everything Mosaic renders; see `MosaicLocalization`. */ @@ -20,21 +24,23 @@ export interface MosaicProviderProps { // Exported Mosaic components annotate a React return type on purpose. The package sets // `jsxImportSource: '@emotion/react'`, so an inferred return leaks Emotion's `JSX.Element` into the // published `.d.ts`, which React 19 consumers reject as a JSX element type. -export function MosaicProvider({ children, icons, localization }: MosaicProviderProps): React.ReactElement { +export function MosaicProvider({ children, direction, icons, localization }: MosaicProviderProps): React.ReactElement { const iconsValue = React.useMemo(() => icons ?? {}, [icons]); const localizationValue = React.useMemo(() => resolveLocalization(localization), [localization]); const [now] = React.useState(() => new Date()); return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ); } diff --git a/packages/mosaic/src/primitives/direction-provider/direction-provider.test.tsx b/packages/mosaic/src/primitives/direction-provider/direction-provider.test.tsx new file mode 100644 index 00000000000..038fffd87c1 --- /dev/null +++ b/packages/mosaic/src/primitives/direction-provider/direction-provider.test.tsx @@ -0,0 +1,37 @@ +import { renderHook } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; + +import { DirectionProvider, useDirection } from './index'; + +describe('DirectionProvider', () => { + it('defaults to ltr without a provider', () => { + const { result } = renderHook(() => useDirection()); + expect(result.current).toBe('ltr'); + }); + + it('defaults to ltr when no direction is given', () => { + const { result } = renderHook(() => useDirection(), { + wrapper: ({ children }) => {children}, + }); + expect(result.current).toBe('ltr'); + }); + + it('provides the direction to descendants', () => { + const { result } = renderHook(() => useDirection(), { + wrapper: ({ children }) => {children}, + }); + expect(result.current).toBe('rtl'); + }); + + it('lets a nested provider override an outer one', () => { + const { result } = renderHook(() => useDirection(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + expect(result.current).toBe('ltr'); + }); +}); diff --git a/packages/mosaic/src/primitives/direction-provider/direction-provider.tsx b/packages/mosaic/src/primitives/direction-provider/direction-provider.tsx new file mode 100644 index 00000000000..f8a01cca6c0 --- /dev/null +++ b/packages/mosaic/src/primitives/direction-provider/direction-provider.tsx @@ -0,0 +1,29 @@ +'use client'; + +import type { ReactNode } from 'react'; +import React from 'react'; + +export type TextDirection = 'ltr' | 'rtl'; + +export interface DirectionProviderProps { + /** The reading direction that keyboard navigation follows below this provider. Defaults to `ltr`. */ + direction?: TextDirection; + children: ReactNode; +} + +const DirectionContext = React.createContext(undefined); + +export function DirectionProvider({ direction = 'ltr', children }: DirectionProviderProps): React.ReactElement { + return {children}; +} + +export function useDirection(): TextDirection { + return React.useContext(DirectionContext) ?? 'ltr'; +} + +export function readingDirectionKeys(direction: TextDirection) { + return { + previousKey: direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft', + nextKey: direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight', + }; +} diff --git a/packages/mosaic/src/primitives/direction-provider/index.ts b/packages/mosaic/src/primitives/direction-provider/index.ts new file mode 100644 index 00000000000..b9151ff3399 --- /dev/null +++ b/packages/mosaic/src/primitives/direction-provider/index.ts @@ -0,0 +1,2 @@ +export { DirectionProvider, readingDirectionKeys, useDirection } from './direction-provider'; +export type { DirectionProviderProps, TextDirection } from './direction-provider'; diff --git a/packages/mosaic/src/primitives/menu/menu-root.tsx b/packages/mosaic/src/primitives/menu/menu-root.tsx index fa1fadab26c..621e57ce993 100644 --- a/packages/mosaic/src/primitives/menu/menu-root.tsx +++ b/packages/mosaic/src/primitives/menu/menu-root.tsx @@ -25,6 +25,7 @@ import { } from '@floating-ui/react'; import { type ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useDirection } from '../direction-provider'; import { useControllableState } from '../hooks/use-controllable-state'; import { useReturnFocus } from '../hooks/use-return-focus'; import { useTransition } from '../hooks/use-transition'; @@ -71,6 +72,7 @@ function MenuInner(props: MenuProps) { // has a parent node id too, and treating that as nesting makes it hover-open, side-placed, and // unclickable by mouse. const isNested = parentId != null && parentContext != null; + const direction = useDirection(); const [open, setOpen] = useControllableState(props.open, props.defaultOpen ?? false, props.onOpenChange); @@ -179,6 +181,7 @@ function MenuInner(props: MenuProps) { listRef: elementsRef, activeIndex, nested: isNested, + rtl: direction === 'rtl', onNavigate: setActiveIndex, }); const typeahead = useTypeahead(floatingContext, { diff --git a/packages/mosaic/src/primitives/menu/menu.test.tsx b/packages/mosaic/src/primitives/menu/menu.test.tsx index 08b9a70783b..469cd80790c 100644 --- a/packages/mosaic/src/primitives/menu/menu.test.tsx +++ b/packages/mosaic/src/primitives/menu/menu.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DirectionProvider } from '../direction-provider'; import { Popover } from '../popover'; import { axe } from '../test-utils/axe'; import { Menu } from './index'; @@ -736,6 +737,43 @@ describe('Menu', () => { expect(screen.getByText('Share')).toHaveAttribute('data-closed', ''); expect(screen.getByText('Actions')).toHaveAttribute('data-open', ''); }); + + it('opens and closes a submenu with the reading-direction arrow keys in RTL', async () => { + const user = userEvent.setup(); + render( + + + Actions + + + + Share + + + Email + + + + + + + , + ); + + await user.click(screen.getByText('Actions')); + await new Promise(r => requestAnimationFrame(r)); + await user.keyboard('{ArrowDown}'); + await user.keyboard('{ArrowRight}'); + expect(screen.getByText('Share')).not.toHaveAttribute('data-open'); + + await user.keyboard('{ArrowLeft}'); + expect(screen.getByText('Share')).toHaveAttribute('data-open', ''); + + await new Promise(r => requestAnimationFrame(r)); + await user.keyboard('{ArrowRight}'); + expect(screen.getByText('Share')).toHaveAttribute('data-closed', ''); + expect(screen.getByText('Actions')).toHaveAttribute('data-open', ''); + }); }); describe('inside a popover', () => { diff --git a/packages/mosaic/src/primitives/otp/README.md b/packages/mosaic/src/primitives/otp/README.md index e117aa6dd31..78395c04308 100644 --- a/packages/mosaic/src/primitives/otp/README.md +++ b/packages/mosaic/src/primitives/otp/README.md @@ -142,8 +142,7 @@ default element. Focus can't skip past the first empty slot: clicking, arrowing, or tabbing onto an empty slot beyond it snaps focus back to the first empty slot (so an empty field always focuses the first slot). -Arrow keys follow reading order. Under `dir="rtl"` (resolved from the nearest ancestor with an -explicit `dir`), `ArrowLeft` moves to the next slot and `ArrowRight` to the previous one; `Home`/`End` +Arrow keys follow reading order. Under ``, `ArrowLeft` moves to the next slot and `ArrowRight` to the previous one; `Home`/`End` and the `Ctrl`/`Cmd` boundary jumps stay logical (first / last-entered). ## Data Attributes diff --git a/packages/mosaic/src/primitives/otp/otp-input.tsx b/packages/mosaic/src/primitives/otp/otp-input.tsx index 0b0ab9ebd32..8d2c3567b01 100644 --- a/packages/mosaic/src/primitives/otp/otp-input.tsx +++ b/packages/mosaic/src/primitives/otp/otp-input.tsx @@ -2,6 +2,7 @@ import React, { useCallback } from 'react'; +import { readingDirectionKeys, useDirection } from '../direction-provider'; import { type ComponentProps, mergeProps, useRender } from '../utils'; import { useOtpContext } from './otp-context'; import { inputModeForPattern, removeAt, replaceAt, sanitize } from './otp-utils'; @@ -31,6 +32,7 @@ export const OtpInput = React.forwardRef(functi onSlotFocus, onSlotBlur, } = useOtpContext(); + const { previousKey, nextKey } = readingDirectionKeys(useDirection()); const char = value[index] ?? ''; @@ -126,13 +128,6 @@ export const OtpInput = React.forwardRef(functi // Ctrl/Cmd (without Alt) turns navigation and delete into boundary actions. const boundaryModifier = (event.ctrlKey || event.metaKey) && !event.altKey; - // Resolve direction from the nearest ancestor with an explicit `dir` so - // arrow keys follow reading order: in RTL, Left moves to the next slot and - // Right to the previous one. - const isRtl = (event.currentTarget.closest('[dir]')?.getAttribute('dir') ?? '').toLowerCase() === 'rtl'; - const previousKey = isRtl ? 'ArrowRight' : 'ArrowLeft'; - const nextKey = isRtl ? 'ArrowLeft' : 'ArrowRight'; - if (event.key === previousKey) { event.preventDefault(); focus(boundaryModifier ? 0 : index - 1); diff --git a/packages/mosaic/src/primitives/otp/otp.test.tsx b/packages/mosaic/src/primitives/otp/otp.test.tsx index 30511f8abeb..b3563584402 100644 --- a/packages/mosaic/src/primitives/otp/otp.test.tsx +++ b/packages/mosaic/src/primitives/otp/otp.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { DirectionProvider } from '../direction-provider'; import { axe } from '../test-utils/axe'; import { Otp } from './index'; @@ -239,15 +240,15 @@ describe('Otp', () => { expect(inputs()[0]).toHaveFocus(); }); - it('swaps ArrowLeft/ArrowRight under dir="rtl"', async () => { + it('swaps ArrowLeft/ArrowRight in RTL', async () => { const user = userEvent.setup(); render( -
+ -
, +
, ); await user.click(inputs()[1]); @@ -258,15 +259,15 @@ describe('Otp', () => { expect(inputs()[1]).toHaveFocus(); }); - it('swaps Ctrl/Cmd + arrows under dir="rtl"', async () => { + it('swaps Ctrl/Cmd + arrows in RTL', async () => { const user = userEvent.setup(); render( -
+ -
, + , ); await user.click(inputs()[1]); diff --git a/packages/mosaic/src/primitives/segmented-control/segmented-control-root.tsx b/packages/mosaic/src/primitives/segmented-control/segmented-control-root.tsx index ba3f839d43f..a642dfa3f26 100644 --- a/packages/mosaic/src/primitives/segmented-control/segmented-control-root.tsx +++ b/packages/mosaic/src/primitives/segmented-control/segmented-control-root.tsx @@ -3,6 +3,7 @@ import { Composite } from '@floating-ui/react'; import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useDirection } from '../direction-provider'; import { useControllableState } from '../hooks/use-controllable-state'; import { type ComponentProps, isRef, mergeProps, useRender } from '../utils'; import { @@ -25,7 +26,7 @@ export const SegmentedControlRoot = React.forwardRef('forward'); const [activeIndex, setActiveIndex] = useState(0); - const [rtl, setRtl] = useState(false); + const readingDirection = useDirection(); const rootRef = useRef(null); const selectedIndexRef = useRef(-1); const navigatedIndexRef = useRef(null); @@ -73,16 +74,13 @@ export const SegmentedControlRoot = React.forwardRef) => { const { onKeyDown: compositeKeyDown, ...otherCompositeProps } = compositeProps; const defaultProps: Record = { role: 'radiogroup' as const, - onFocus: (event: React.FocusEvent) => { - setRtl((event.currentTarget.closest('[dir]')?.getAttribute('dir') ?? '').toLowerCase() === 'rtl'); - }, onKeyDown: (event: React.KeyboardEvent) => { navigatedIndexRef.current = -1; if (event.key === 'Home' || event.key === 'End') { diff --git a/packages/mosaic/src/primitives/tabs/tabs-list.tsx b/packages/mosaic/src/primitives/tabs/tabs-list.tsx index ca249d80019..5b78a170364 100644 --- a/packages/mosaic/src/primitives/tabs/tabs-list.tsx +++ b/packages/mosaic/src/primitives/tabs/tabs-list.tsx @@ -1,8 +1,9 @@ 'use client'; import { Composite } from '@floating-ui/react'; -import React, { useState } from 'react'; +import React from 'react'; +import { useDirection } from '../direction-provider'; import { type ComponentProps, isRef, mergeProps, useRender } from '../utils'; import { useTabsContext } from './tabs-context'; @@ -11,18 +12,15 @@ export type TabsListProps = ComponentProps<'div'>; export function TabsList(props: TabsListProps) { const { render, children, ...otherProps } = props; const { orientation } = useTabsContext(); - const [rtl, setRtl] = useState(false); + const direction = useDirection(); return ( ) => { const defaultProps: Record = { role: 'tablist' as const, - onFocus: (event: React.FocusEvent) => { - setRtl((event.currentTarget.closest('[dir]')?.getAttribute('dir') ?? '').toLowerCase() === 'rtl'); - }, onKeyDown: (event: React.KeyboardEvent) => { if (event.key !== 'Home' && event.key !== 'End') { return; diff --git a/packages/mosaic/src/primitives/tabs/tabs.test.tsx b/packages/mosaic/src/primitives/tabs/tabs.test.tsx index a10554eccc6..33769ed538b 100644 --- a/packages/mosaic/src/primitives/tabs/tabs.test.tsx +++ b/packages/mosaic/src/primitives/tabs/tabs.test.tsx @@ -4,6 +4,7 @@ import { createRef, useState } from 'react'; import { renderToString } from 'react-dom/server'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { DirectionProvider } from '../direction-provider'; import { axe } from '../test-utils/axe'; import { Tabs } from './index'; @@ -209,7 +210,7 @@ describe('Tabs', () => { it('follows the visual order of arrow keys in RTL', async () => { const user = userEvent.setup(); render( -
+ Account @@ -220,7 +221,7 @@ describe('Tabs', () => { Settings content Billing content -
, + , ); await user.click(screen.getByText('Settings')); @@ -231,6 +232,28 @@ describe('Tabs', () => { expect(document.activeElement).toBe(screen.getByText('Settings')); }); + it('follows a direction change while a tab has focus', async () => { + const user = userEvent.setup(); + const tabs = (direction: 'ltr' | 'rtl') => ( + + + + Account + Settings + Billing + + + + ); + const { rerender } = render(tabs('ltr')); + + await user.click(screen.getByText('Settings')); + rerender(tabs('rtl')); + await user.keyboard('{ArrowLeft}'); + + expect(document.activeElement).toBe(screen.getByText('Billing')); + }); + it('uses ArrowDown/ArrowUp for vertical orientation', async () => { const user = userEvent.setup(); renderTabs({ orientation: 'vertical' }); diff --git a/packages/mosaic/src/primitives/tag-input/tag-input-input.tsx b/packages/mosaic/src/primitives/tag-input/tag-input-input.tsx index 003c46a5a52..1825da38a4c 100644 --- a/packages/mosaic/src/primitives/tag-input/tag-input-input.tsx +++ b/packages/mosaic/src/primitives/tag-input/tag-input-input.tsx @@ -2,9 +2,10 @@ import React, { useState } from 'react'; +import { readingDirectionKeys, useDirection } from '../direction-provider'; import { type ComponentProps, mergeProps, useRender } from '../utils'; import { useTagInputContext } from './tag-input-context'; -import { readingDirectionKeys, splitTags } from './tag-input-utils'; +import { splitTags } from './tag-input-utils'; export type TagInputInputProps = Omit, 'value' | 'defaultValue'>; @@ -13,6 +14,7 @@ export const TagInputInput = React.forwardRef { add([text]); @@ -54,7 +56,6 @@ export const TagInputInput = React.forwardRef 0) { event.preventDefault(); focusTag(value[value.length - 1]); diff --git a/packages/mosaic/src/primitives/tag-input/tag-input-tag.tsx b/packages/mosaic/src/primitives/tag-input/tag-input-tag.tsx index 61674a28076..3f4d7e26f11 100644 --- a/packages/mosaic/src/primitives/tag-input/tag-input-tag.tsx +++ b/packages/mosaic/src/primitives/tag-input/tag-input-tag.tsx @@ -3,10 +3,10 @@ import { inertProps } from '@clerk/shared/inert'; import React, { useCallback, useEffect, useRef } from 'react'; +import { readingDirectionKeys, useDirection } from '../direction-provider'; import { useTransition } from '../hooks/use-transition'; import { type ComponentProps, mergeProps, useRender } from '../utils'; import { TagContext, useTagInputContext } from './tag-input-context'; -import { readingDirectionKeys } from './tag-input-utils'; export interface TagInputTagProps extends ComponentProps<'div'> { value: string; @@ -16,6 +16,7 @@ export const TagInputTag = React.forwardRef( function TagInputTag(props, forwardedRef) { const { render, value: tagValue, ...otherProps } = props; const { value, tags, disabled, remove, focusInput, focusTag, registerTag, onTagExited } = useTagInputContext(); + const { previousKey, nextKey } = readingDirectionKeys(useDirection()); const present = value.includes(tagValue); const invalid = tags.find(tag => tag.value === tagValue)?.invalid ?? false; @@ -53,7 +54,6 @@ export const TagInputTag = React.forwardRef( return; } const index = value.indexOf(tagValue); - const { previousKey, nextKey } = readingDirectionKeys(event.currentTarget); switch (event.key) { case previousKey: diff --git a/packages/mosaic/src/primitives/tag-input/tag-input-utils.ts b/packages/mosaic/src/primitives/tag-input/tag-input-utils.ts index 925801749a0..da109da97cd 100644 --- a/packages/mosaic/src/primitives/tag-input/tag-input-utils.ts +++ b/packages/mosaic/src/primitives/tag-input/tag-input-utils.ts @@ -20,11 +20,3 @@ export function mergeRenderList(previous: string[], value: string[]): string[] { }); return result; } - -export function readingDirectionKeys(element: Element) { - const isRtl = (element.closest('[dir]')?.getAttribute('dir') ?? '').toLowerCase() === 'rtl'; - return { - previousKey: isRtl ? 'ArrowRight' : 'ArrowLeft', - nextKey: isRtl ? 'ArrowLeft' : 'ArrowRight', - }; -} diff --git a/packages/mosaic/src/primitives/tag-input/tag-input.test.tsx b/packages/mosaic/src/primitives/tag-input/tag-input.test.tsx index b54b6cb68ac..93914763d9b 100644 --- a/packages/mosaic/src/primitives/tag-input/tag-input.test.tsx +++ b/packages/mosaic/src/primitives/tag-input/tag-input.test.tsx @@ -4,6 +4,7 @@ import { createRef, useState } from 'react'; import { flushSync } from 'react-dom'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { DirectionProvider } from '../direction-provider'; import { axe } from '../test-utils/axe'; import { TagInput } from './index'; @@ -324,9 +325,9 @@ describe('TagInput', () => { it('follows reading order in RTL', async () => { const user = userEvent.setup(); render( -
+ -
, + , ); await user.click(input()); diff --git a/packages/swingset/src/components/ChaosProvider.tsx b/packages/swingset/src/components/ChaosProvider.tsx index 21c7aa2aa7e..ff0079cf1a4 100644 --- a/packages/swingset/src/components/ChaosProvider.tsx +++ b/packages/swingset/src/components/ChaosProvider.tsx @@ -5,6 +5,8 @@ import * as React from 'react'; import { chaosLocalization } from '@/lib/chaos'; +import { useDirection } from './DirectionProvider'; + interface ChaosContextValue { chaos: boolean; setChaos: (chaos: boolean) => void; @@ -29,9 +31,11 @@ export function useChaosFixture(data: T, toChaos: (data: T) => T): T { export function StoryMosaicProvider({ children }: { children: React.ReactNode }) { const { chaos } = useChaos(); + const { direction } = useDirection(); return ( {children}