Skip to content

Commit 8aa750d

Browse files
authored
feat: make pages tree accessible from keyboard (#4132)
Added new implementation for tree component in design system which has out of the box keyboard support without tying with dnd. Now only home page is tabbable, all others are accessible with arrows. Right arrow can both expand folder and access "settings" button. Both left and right arrows are able to travel between folders/pages. https://github.com/user-attachments/assets/8d3264ca-1069-4168-97d9-afd7bdd4759e
1 parent c664b5b commit 8aa750d

File tree

12 files changed

+604
-373
lines changed

12 files changed

+604
-373
lines changed

apps/builder/app/builder/features/sidebar-left/panels/pages/page-settings.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ import {
100100
deletePageMutable,
101101
$pageRootScope,
102102
duplicatePage,
103-
toTreeData,
104103
isPathAvailable,
105104
} from "./page-utils";
106105
import { Form } from "./form";
@@ -773,8 +772,8 @@ const FormFields = ({
773772
}}
774773
color="subtle"
775774
>
776-
Move this page to the “{toTreeData(pages).root.name}” folder
777-
to set it as your home page
775+
Move this page to the “Root” folder to set it as your home
776+
page
778777
</Text>
779778
</>
780779
) : values.documentType === "xml" ? (

apps/builder/app/builder/features/sidebar-left/panels/pages/page-utils.test.ts

Lines changed: 0 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
isSlugAvailable,
1414
registerFolderChildMutable,
1515
reparentOrphansMutable,
16-
toTreeData,
1716
filterSelfAndChildren,
1817
getExistingRoutePaths,
1918
$pageRootScope,
@@ -97,162 +96,6 @@ const createPages = () => {
9796
const toMap = <T extends { id: string }>(list: T[]) =>
9897
new Map(list.map((item) => [item.id, item]));
9998

100-
describe("toTreeData", () => {
101-
test("initial pages always has home pages and a root folder", () => {
102-
const { pages } = createPages();
103-
const tree = toTreeData(pages);
104-
expect(tree.root).toEqual({
105-
id: ROOT_FOLDER_ID,
106-
name: "Root",
107-
slug: "",
108-
type: "folder",
109-
children: [
110-
{
111-
data: {
112-
id: "homePageId",
113-
meta: {},
114-
name: "Home",
115-
path: "",
116-
rootInstanceId: "rootInstanceId",
117-
systemDataSourceId: "systemDataSourceId",
118-
title: `"Home"`,
119-
},
120-
id: "homePageId",
121-
type: "page",
122-
},
123-
],
124-
});
125-
});
126-
127-
test("add empty folder", () => {
128-
const { pages, register, f } = createPages();
129-
register([f("folder", [])]);
130-
131-
const tree = toTreeData(pages);
132-
expect(tree.root).toEqual({
133-
id: ROOT_FOLDER_ID,
134-
name: "Root",
135-
slug: "",
136-
type: "folder",
137-
children: [
138-
{
139-
data: {
140-
id: "homePageId",
141-
meta: {},
142-
name: "Home",
143-
path: "",
144-
rootInstanceId: "rootInstanceId",
145-
systemDataSourceId: "systemDataSourceId",
146-
title: `"Home"`,
147-
},
148-
id: "homePageId",
149-
type: "page",
150-
},
151-
{
152-
type: "folder",
153-
id: "folder",
154-
name: "folder",
155-
slug: "folder",
156-
children: [],
157-
},
158-
],
159-
});
160-
});
161-
162-
test("add a page inside a folder", () => {
163-
const { pages, register, f, p } = createPages();
164-
register([f("folder", [p("page", "/page")])]);
165-
166-
const tree = toTreeData(pages);
167-
168-
expect(tree.root).toEqual({
169-
id: ROOT_FOLDER_ID,
170-
name: "Root",
171-
slug: "",
172-
type: "folder",
173-
children: [
174-
{
175-
data: {
176-
id: "homePageId",
177-
meta: {},
178-
name: "Home",
179-
path: "",
180-
rootInstanceId: "rootInstanceId",
181-
systemDataSourceId: "systemDataSourceId",
182-
title: `"Home"`,
183-
},
184-
id: "homePageId",
185-
type: "page",
186-
},
187-
{
188-
type: "folder",
189-
id: "folder",
190-
name: "folder",
191-
slug: "folder",
192-
children: [
193-
{
194-
type: "page",
195-
id: "page",
196-
data: {
197-
id: "page",
198-
meta: {},
199-
name: "page",
200-
path: "/page",
201-
rootInstanceId: "rootInstanceId",
202-
systemDataSourceId: "systemDataSourceId",
203-
title: `"page"`,
204-
},
205-
},
206-
],
207-
},
208-
],
209-
});
210-
});
211-
212-
test("nest a folder", () => {
213-
const { pages, register, f } = createPages();
214-
register([f("1", [f("1-1")])]);
215-
216-
const tree = toTreeData(pages);
217-
expect(tree.root).toEqual({
218-
type: "folder",
219-
id: ROOT_FOLDER_ID,
220-
name: "Root",
221-
slug: "",
222-
children: [
223-
{
224-
type: "page",
225-
id: "homePageId",
226-
data: {
227-
id: "homePageId",
228-
name: "Home",
229-
path: "",
230-
title: `"Home"`,
231-
meta: {},
232-
rootInstanceId: "rootInstanceId",
233-
systemDataSourceId: "systemDataSourceId",
234-
},
235-
},
236-
{
237-
type: "folder",
238-
id: "1",
239-
name: "1",
240-
slug: "1",
241-
children: [
242-
{
243-
type: "folder",
244-
id: "1-1",
245-
name: "1-1",
246-
slug: "1-1",
247-
children: [],
248-
},
249-
],
250-
},
251-
],
252-
});
253-
});
254-
});
255-
25699
describe("reparentOrphansMutable", () => {
257100
// We must deal with the fact there can be an orphaned folder or page in a collaborative mode,
258101
// because user A can add a page to a folder while user B deletes the folder without receiving the create page yet.

apps/builder/app/builder/features/sidebar-left/panels/pages/page-utils.ts

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -32,83 +32,6 @@ import {
3232
} from "~/shared/nano-states";
3333
import { insertPageCopyMutable } from "~/shared/page-utils";
3434

35-
type TreePage = {
36-
type: "page";
37-
id: string;
38-
data: Page;
39-
};
40-
41-
type TreeFolder = {
42-
// currently used only for root node
43-
type: "folder";
44-
id: Folder["id"];
45-
name: Folder["name"];
46-
slug: Folder["slug"];
47-
children: Array<TreeData>;
48-
};
49-
50-
export type TreeData = TreeFolder | TreePage;
51-
52-
type Index = Map<string, TreeData>;
53-
54-
/**
55-
* Return a nested tree structure from flat pages and folders.
56-
* To be used for rendering.
57-
*/
58-
export const toTreeData = (
59-
pages: Pages
60-
): { root: TreeFolder; index: Index } => {
61-
const pagesMap = new Map(pages.pages.map((page) => [page.id, page]));
62-
pagesMap.set(pages.homePage.id, pages.homePage);
63-
64-
const foldersMap = new Map(
65-
pages.folders.map((folder) => [folder.id, folder])
66-
);
67-
const index: Index = new Map();
68-
69-
const folderToTree = (folder: Folder) => {
70-
// Using map to ensure uniqueness of children.
71-
const children = new Map<string, TreeData>();
72-
for (const id of folder.children) {
73-
const folder = foldersMap.get(id);
74-
// It is a folder, not a page.
75-
if (folder) {
76-
const treeFolder = folderToTree(folder);
77-
children.set(treeFolder.id, treeFolder);
78-
index.set(folder.id, treeFolder);
79-
continue;
80-
}
81-
const page = pagesMap.get(id);
82-
if (page) {
83-
const treePage = {
84-
type: "page",
85-
id: page.id,
86-
data: page,
87-
} satisfies TreePage;
88-
children.set(treePage.id, treePage);
89-
index.set(page.id, treePage);
90-
continue;
91-
}
92-
}
93-
94-
return {
95-
type: "folder",
96-
id: folder.id,
97-
name: folder.name,
98-
slug: folder.slug,
99-
children: Array.from(children.values()),
100-
} satisfies TreeFolder;
101-
};
102-
const rootFolder = foldersMap.get(ROOT_FOLDER_ID);
103-
if (rootFolder === undefined) {
104-
throw new Error("Root folder not found");
105-
}
106-
return {
107-
root: folderToTree(rootFolder),
108-
index,
109-
};
110-
};
111-
11235
/**
11336
* When page or folder needs to be deleted or moved to a different parent,
11437
* we want to cleanup any existing reference to it in current folder.

0 commit comments

Comments
 (0)