ValarSvelte

Direction / RTL

原生方向继承、显式交互方向与 Portal 的组合规则。

اتجاه موروث — inherited direction

القيمة / Value: 35

const direction = "ltr";
<script lang="ts">
	import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
	import * as Popover from "$lib/components/ui/popover/index.js";
	import * as Tabs from "$lib/components/ui/tabs/index.js";
	import { Button } from "$lib/components/ui/button/index.js";
	import { Slider } from "$lib/components/ui/slider/index.js";
 
	let direction = $state<"ltr" | "rtl">("rtl");
	let value = $state(35);
</script>
 
<div class="grid w-full max-w-lg gap-5" data-direction-demo>
	<label class="flex items-center gap-3 text-sm">
		Direction
		<select
			aria-label="Direction"
			bind:value={direction}
			class="rounded-md border bg-background p-2"
		>
			<option value="rtl">RTL — العربية</option>
			<option value="ltr">LTR — English</option>
		</select>
	</label>
	<div dir={direction} class="grid gap-5 rounded-lg border p-5">
		<Tabs.Root value="account">
			<Tabs.List aria-label="Direction tabs">
				<Tabs.Trigger value="account">الحساب / Account</Tabs.Trigger>
				<Tabs.Trigger value="security">الأمان / Security</Tabs.Trigger>
			</Tabs.List>
			<Tabs.Content value="account" class="text-sm">اتجاه موروث — inherited direction</Tabs.Content>
			<Tabs.Content value="security" class="text-sm"
				>مفاتيح معكوسة — mirrored arrow keys</Tabs.Content
			>
		</Tabs.Root>
		<div class="grid gap-3">
			<p class="text-sm">القيمة / Value: <output>{value}</output></p>
			<Slider type="single" dir={direction} bind:value aria-label="Direction value" max={100} />
		</div>
		<div class="flex flex-wrap gap-3">
			<Popover.Root>
				<Popover.Trigger>
					{#snippet child({ props })}<Button {...props} variant="outline">弹层 / Popover</Button
						>{/snippet}
				</Popover.Trigger>
				<Popover.Content dir={direction} align="start" class="w-64" aria-label="Direction popover">
					<p class="text-sm">محاذاة البداية / logical start</p>
					<p dir="ltr" class="mt-2 text-sm text-muted-foreground">
						Portal keeps context, not DOM inheritance.
					</p>
				</Popover.Content>
			</Popover.Root>
			<DropdownMenu.Root dir={direction}>
				<DropdownMenu.Trigger>
					{#snippet child({ props })}<Button {...props} variant="outline">菜单 / Menu</Button
						>{/snippet}
				</DropdownMenu.Trigger>
				<DropdownMenu.Content align="start">
					<DropdownMenu.Item>الحساب / Account</DropdownMenu.Item>
					<DropdownMenu.Sub>
						<DropdownMenu.SubTrigger>المزيد / More</DropdownMenu.SubTrigger>
						<DropdownMenu.SubContent side={direction === "rtl" ? "left" : "right"}>
							<DropdownMenu.Item>الإعدادات / Settings</DropdownMenu.Item>
						</DropdownMenu.SubContent>
					</DropdownMenu.Sub>
				</DropdownMenu.Content>
			</DropdownMenu.Root>
		</div>
		<pre dir="ltr" class="overflow-x-auto rounded-md bg-muted p-3 text-start text-xs"><code
				>const direction = "ltr";</code
			></pre>
	</div>
</div>

不需要第二套 Provider

Valar Svelte 使用原生 dir="rtl" / dir="ltr" 与逻辑样式。Bits UI 2.20.0 没有 DirectionProvider,BitsConfig 只配置 locale 与 Portal 目标,不会为所有组件统一设置方向。上面的示例可动态切换,默认 RTL,代码块局部固定 LTR。

交互方向必须分类处理

组件 方向来源 用法
Tabs、Stepper 键盘时读取实际 DOM 计算方向 父节点设置 dir 即可,局部 dir 可覆盖
Slider、ScrollArea 原语显式 dir,默认 ltr 向组件传入与容器一致的 dir;Slider 同时影响数值几何和方向键
DropdownMenu、ContextMenu、Menubar Root 的显式 dir 向 Root 传 dir;子菜单物理 side 单独设置 RTL left / LTR right
Popover、Select、HoverCard、Tooltip 浮动 Content 的方向与对齐 Content 显式传 dir,再用 align="start" / "end";不是只设置触发器的父节点

原生 dir 能继承,但 Portal 默认把 DOM 移到 body:Svelte context 保留,原父节点的 CSS 继承不保留。局部 RTL 的弹层必须显式指定方向,或通过既有 portalProps.to 移入正确方向的容器。不要把 align 的逻辑 start/end 与 side 的物理 left/right 混为一谈。

<div dir={direction}>
  <Slider dir={direction} type="single" bind:value />
  <Popover.Root>
    <Popover.Trigger>Open</Popover.Trigger>
    <Popover.Content dir={direction} align="start">Content</Popover.Content>
  </Popover.Root>
</div>

使用 ms/me、ps/pe、text-start/end 和 inset-inline-*。不是所有图标都应镜像:前进/后退/子菜单箭头应镜像,播放、商标、数字等不自动镜像。本轮修复 DropdownMenu、ContextMenu、Menubar 子菜单箭头的 ms-auto rtl:rotate-180;原有 cn-rtl-flip 标记本身并不产生镜像样式。现有逻辑弹层样式也不代表全库每个组合都已 RTL 验收。

范围与验收

本轮验收代表组合为继承方向的 Tabs、显式方向的 Slider、body Portal 的 Popover、嵌套菜单及局部 LTR 代码块,涵盖动态切换、键盘、逻辑对齐与 SSR 首屏。没有创建重复 Provider,也没有宣称现有所有业务区块、图表和第三方控件均已双向化。

固定版本源码依据、能力缺口和可重复结果见 standards/porting-records/direction-bits-ui-to-svelte.md。真实读屏、真实触摸设备及不同浏览器仍需集成项目按支持范围验证。