Skip to content

Commit aa50c78

Browse files
authored
Merge pull request #93 from abdel-17/fix-rtl
fix: support rtl for keyboard shortcuts
2 parents e7e3b25 + 07eb811 commit aa50c78

File tree

7 files changed

+226
-27
lines changed

7 files changed

+226
-27
lines changed

.changeset/tall-beans-attack.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-file-tree": patch
3+
---
4+
5+
fix: swap arrow left/right in rtl

packages/svelte-file-tree/src/lib/components/TreeItem.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@
3838
return;
3939
}
4040
41+
const isRtl = getComputedStyle(event.currentTarget).direction === "rtl";
42+
const arrowRight = isRtl ? "ArrowLeft" : "ArrowRight";
43+
const arrowLeft = isRtl ? "ArrowRight" : "ArrowLeft";
44+
4145
switch (event.key) {
42-
case "ArrowRight": {
46+
case arrowRight: {
4347
if (item.node.type === "file") {
4448
break;
4549
}
@@ -55,7 +59,7 @@
5559
}
5660
break;
5761
}
58-
case "ArrowLeft": {
62+
case arrowLeft: {
5963
if (item.node.type === "folder" && item.expanded) {
6064
treeContext.getExpandedIds().delete(item.node.id);
6165
break;

sites/preview/src/lib/Tree.svelte

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,78 @@
2222
type FileTreeNode,
2323
type TreeItemState,
2424
} from "./tree.svelte.js";
25+
import { arabicNumbers } from "./utils.js";
2526
2627
const sortCollator = new Intl.Collator();
2728
2829
function sortComparator(x: FileTreeNode, y: FileTreeNode) {
2930
return sortCollator.compare(x.name, y.name);
3031
}
3132
33+
const translations = {
34+
toast: {
35+
cannotMoveInsideItself: {
36+
en: (name: string) => `Cannot move "${name}" inside itself`,
37+
ar: (name: string) => `لا يمكن نقل "${name}" داخل نفسه`,
38+
},
39+
itemAlreadyExists: {
40+
en: (name: string) => `An item named "${name}" already exists in this location`,
41+
ar: (name: string) => `عنصر باسم "${name}" موجود بالفعل في هذا الموقع`,
42+
},
43+
failedToReadFiles: {
44+
en: "Failed to read uploaded files",
45+
ar: "فشل قراءة الملفات المرفوعة",
46+
},
47+
},
48+
dialog: {
49+
failedToCopyItems: {
50+
en: "Failed to copy items",
51+
ar: "فشل نسخ العناصر",
52+
},
53+
failedToMoveItems: {
54+
en: "Failed to move items",
55+
ar: "فشل نقل العناصر",
56+
},
57+
nameConflictDescription: {
58+
en: (name: string) =>
59+
`An item named "${name}" already exists in this location. Do you want to skip it or cancel the operation entirely?`,
60+
ar: (name: string) =>
61+
`عنصر باسم "${name}" موجود بالفعل في هذا الموقع. هل تريد تخطيه أو إلغاء العملية بالكامل؟`,
62+
},
63+
skip: {
64+
en: "Skip",
65+
ar: "تخطي",
66+
},
67+
deleteConfirmTitle: {
68+
en: (count: number) => `Are you sure you want to delete ${count} item(s)?`,
69+
ar: (count: number) =>
70+
`هل أنت متأكد أنك تريد حذف ${arabicNumbers(count.toString())} عناصر؟`,
71+
},
72+
deleteConfirmDescription: {
73+
en: "They will be permanently deleted. This action cannot be undone.",
74+
ar: "سيتم حذفها نهائياً. لا يمكن التراجع عن هذا الإجراء.",
75+
},
76+
confirm: {
77+
en: "Confirm",
78+
ar: "تأكيد",
79+
},
80+
cancel: {
81+
en: "Cancel",
82+
ar: "إلغاء",
83+
},
84+
},
85+
};
86+
3287
export type TreeProps = {
3388
children: Snippet<[args: TreeChildrenSnippetArgs<FileNode, FolderNode>]>;
3489
root: FileTree;
90+
lang?: "en" | "ar";
3591
class?: ClassValue;
3692
style?: string;
3793
};
3894
3995
export type TreeContext = {
96+
getLang: () => "en" | "ar";
4097
getSelectedIds: () => Set<string>;
4198
getExpandedIds: () => Set<string>;
4299
getDraggedId: () => string | undefined;
@@ -53,7 +110,7 @@
53110
</script>
54111

55112
<script lang="ts">
56-
const { children, root, class: className, style }: TreeProps = $props();
113+
const { children, root, lang = "en", class: className, style }: TreeProps = $props();
57114
58115
let tree: Tree<FileNode, FolderNode> | null = $state.raw(null);
59116
const selectedIds = new SvelteSet<string>();
@@ -66,6 +123,7 @@
66123
let dialogTitle = $state.raw("");
67124
let dialogDescription = $state.raw("");
68125
let dialogConfirmLabel = $state.raw("");
126+
let dialogCancelLabel = $state.raw("");
69127
let dialogTrigger: HTMLElement | null = null;
70128
let dialogDidConfirm = false;
71129
let dialogOnClose: (() => void) | undefined;
@@ -74,17 +132,20 @@
74132
title,
75133
description,
76134
confirmLabel,
135+
cancelLabel,
77136
onClose,
78137
}: {
79138
title: string;
80139
description: string;
81140
confirmLabel: string;
141+
cancelLabel: string;
82142
onClose: () => void;
83143
}) {
84144
dialogOpen = true;
85145
dialogTitle = title;
86146
dialogDescription = description;
87147
dialogConfirmLabel = confirmLabel;
148+
dialogCancelLabel = cancelLabel;
88149
dialogTrigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
89150
dialogDidConfirm = false;
90151
dialogOnClose = onClose;
@@ -95,6 +156,7 @@
95156
dialogTitle = "";
96157
dialogDescription = "";
97158
dialogConfirmLabel = "";
159+
dialogCancelLabel = "";
98160
dialogTrigger?.focus();
99161
dialogTrigger = null;
100162
dialogOnClose?.();
@@ -134,19 +196,20 @@
134196
let title;
135197
switch (operation) {
136198
case "copy": {
137-
title = "Failed to copy items";
199+
title = translations.dialog.failedToCopyItems[lang];
138200
break;
139201
}
140202
case "move": {
141-
title = "Failed to move items";
203+
title = translations.dialog.failedToMoveItems[lang];
142204
break;
143205
}
144206
}
145207
146208
showDialog({
147209
title,
148-
description: `An item named "${name}" already exists in this location. Do you want to skip it or cancel the operation entirely?`,
149-
confirmLabel: "Skip",
210+
description: translations.dialog.nameConflictDescription[lang](name),
211+
confirmLabel: translations.dialog.skip[lang],
212+
cancelLabel: translations.dialog.cancel[lang],
150213
onClose: () => {
151214
resolve(dialogDidConfirm ? "skip" : "cancel");
152215
},
@@ -155,7 +218,7 @@
155218
}
156219
157220
function onCircularReference({ source }: OnCircularReferenceArgs<FileNode, FolderNode>) {
158-
toast.error(`Cannot move "${source.node.name}" inside itself`);
221+
toast.error(translations.toast.cannotMoveInsideItself[lang](source.node.name));
159222
}
160223
161224
function onCopy({ destination }: OnCopyArgs<FileNode, FolderNode>) {
@@ -171,9 +234,10 @@
171234
function canRemove({ removed }: OnRemoveArgs<FileNode, FolderNode>) {
172235
return new Promise<boolean>((resolve) => {
173236
showDialog({
174-
title: `Are you sure you want to delete ${removed.length} item(s)?`,
175-
description: "They will be permanently deleted. This action cannot be undone.",
176-
confirmLabel: "Confirm",
237+
title: translations.dialog.deleteConfirmTitle[lang](removed.length),
238+
description: translations.dialog.deleteConfirmDescription[lang],
239+
confirmLabel: translations.dialog.confirm[lang],
240+
cancelLabel: translations.dialog.cancel[lang],
177241
onClose: () => {
178242
resolve(dialogDidConfirm);
179243
},
@@ -236,9 +300,9 @@
236300
continue;
237301
}
238302
239-
const firstSegment = entry.name.split("/")[0];
303+
const firstSegment = entry.name.split("/")[0]!;
240304
if (uniqueNames.has(firstSegment)) {
241-
toast.error(`An item named "${firstSegment}" already exists in this location`);
305+
toast.error(translations.toast.itemAlreadyExists[lang](firstSegment));
242306
return;
243307
}
244308
@@ -274,7 +338,7 @@
274338
await Promise.all(entries.map(readEntry));
275339
} catch (error) {
276340
console.error(error);
277-
toast.error("Failed to read uploaded files");
341+
toast.error(translations.toast.failedToReadFiles[lang]);
278342
return;
279343
}
280344
@@ -336,6 +400,7 @@
336400
};
337401
338402
const context: TreeContext = {
403+
getLang: () => lang,
339404
getSelectedIds: () => selectedIds,
340405
getExpandedIds: () => expandedIds,
341406
getDraggedId: () => draggedId,
@@ -362,6 +427,8 @@
362427
{onCopy}
363428
{onMove}
364429
{canRemove}
430+
{lang}
431+
dir="auto"
365432
class={className}
366433
{style}
367434
ondragenter={handleDragEnterOrOver}
@@ -405,7 +472,7 @@
405472
<AlertDialog.Cancel
406473
class="inline-flex h-10 items-center justify-center rounded bg-gray-200 px-6 text-sm font-medium hover:bg-gray-300 focus-visible:outline-2 focus-visible:outline-current active:scale-95"
407474
>
408-
Cancel
475+
{dialogCancelLabel}
409476
</AlertDialog.Cancel>
410477
</div>
411478
</div>

sites/preview/src/lib/TreeItem.svelte

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { TreeItem } from "svelte-file-tree";
55
import { getTreeContext } from "./Tree.svelte";
66
import type { TreeItemState } from "./tree.svelte.js";
7+
import { arabicNumbers } from "./utils.js";
78
89
export type TreeItemProps = {
910
item: TreeItemState;
@@ -12,27 +13,39 @@
1213
style?: string;
1314
};
1415
15-
const UNITS = ["B", "KB", "MB", "GB", "TB"];
16+
const UNITS = [
17+
{ en: "B", ar: "ب" },
18+
{ en: "KB", ar: "ك.ب" },
19+
{ en: "MB", ar: "م.ب" },
20+
{ en: "GB", ar: "ج.ب" },
21+
{ en: "TB", ar: "ت.ب" },
22+
];
1623
17-
const formatter = new Intl.NumberFormat(undefined, {
24+
const sizeFormatter = new Intl.NumberFormat("en", {
1825
maximumFractionDigits: 2,
1926
});
20-
21-
function formatSize(size: number) {
22-
let unit = UNITS[0];
23-
for (let i = 1; i < UNITS.length && size >= 1024; i++) {
24-
unit = UNITS[i];
25-
size /= 1024;
26-
}
27-
return formatter.format(size) + " " + unit;
28-
}
2927
</script>
3028

3129
<script lang="ts">
3230
const treeContext = getTreeContext();
3331
3432
const { item, order, class: className, style }: TreeItemProps = $props();
3533
34+
function formatSize(size: number) {
35+
let unit = UNITS[0]!;
36+
for (let i = 1; i < UNITS.length && size >= 1024; i++) {
37+
unit = UNITS[i]!;
38+
size /= 1024;
39+
}
40+
41+
const lang = treeContext.getLang();
42+
let formattedSize = sizeFormatter.format(size);
43+
if (lang === "ar") {
44+
formattedSize = arabicNumbers(formattedSize);
45+
}
46+
return formattedSize + " " + unit[lang];
47+
}
48+
3649
const handleDragStart: EventHandler<DragEvent, HTMLDivElement> = (event) => {
3750
if (item.disabled) {
3851
event.preventDefault();
@@ -135,7 +148,7 @@
135148
<ChevronDownIcon
136149
role="presentation"
137150
data-invisible={item.node.type === "file" ? true : undefined}
138-
class="size-5 rounded-full transition-transform duration-200 group-aria-expanded:-rotate-90 hover:bg-current/8 active:bg-current/12 data-invisible:invisible"
151+
class="size-5 rounded-full transition-transform duration-200 group-aria-expanded:-rotate-90 hover:bg-current/8 active:bg-current/12 data-invisible:invisible group-aria-expanded:rtl:rotate-90"
139152
onclick={handleToggleClick}
140153
/>
141154

sites/preview/src/lib/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function arabicNumbers(text: string) {
2+
return text.replace(/[0-9]/g, (digit) => "٠١٢٣٤٥٦٧٨٩".charAt(+digit));
3+
}

sites/preview/src/routes/+page.svelte

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
AlignVerticalSpaceAroundIcon,
44
HandIcon,
55
KeyboardIcon,
6+
LanguagesIcon,
67
ScrollIcon,
78
UploadIcon,
89
ZapIcon,
@@ -197,6 +198,43 @@
197198
</GithubLink>
198199
</div>
199200
</section>
201+
202+
<section
203+
class="@container flex flex-col rounded-xl border border-slate-300 bg-slate-50 p-6 @min-4xl:col-span-2"
204+
>
205+
<h3 class="text-xl font-semibold text-slate-800">RTL Support</h3>
206+
207+
<p class="mt-3 text-slate-700">Right-to-left language support</p>
208+
209+
<div class="grow"></div>
210+
211+
<ul class="mt-6 space-y-3">
212+
<li class="flex items-center gap-2 text-sm text-slate-600">
213+
<LanguagesIcon role="presentation" class="size-4" />
214+
RTL Layout
215+
</li>
216+
217+
<li class="flex items-center gap-2 text-sm text-slate-600">
218+
<KeyboardIcon role="presentation" class="size-4" />
219+
Keyboard Navigation
220+
</li>
221+
</ul>
222+
223+
<div class="mt-6 grid gap-4 @min-sm:grid-cols-2">
224+
<a
225+
href="/rtl"
226+
class="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-700 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-700 active:scale-95"
227+
>
228+
View Example
229+
</a>
230+
231+
<GithubLink
232+
href="https://github.com/abdel-17/svelte-file-tree/tree/master/sites/preview/src/routes/rtl/+page.svelte"
233+
>
234+
View Code
235+
</GithubLink>
236+
</div>
237+
</section>
200238
</div>
201239
</section>
202240

0 commit comments

Comments
 (0)