@remit/ui 0.0.66 → 0.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -11,10 +11,14 @@ import {
|
|
|
11
11
|
const {
|
|
12
12
|
orderFolderNodes,
|
|
13
13
|
filterFolderTree,
|
|
14
|
+
collapseFolderTree,
|
|
15
|
+
queryExpandedPaths,
|
|
16
|
+
withCreateRows,
|
|
14
17
|
matchesQuery,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
findFirstFocusable,
|
|
19
|
+
findLastFocusable,
|
|
20
|
+
findNextFocusable,
|
|
21
|
+
findParentRow,
|
|
18
22
|
} = folderTreePickerInternals;
|
|
19
23
|
|
|
20
24
|
const node = (
|
|
@@ -33,6 +37,8 @@ const folders: FolderTreeNode[] = [
|
|
|
33
37
|
node("trash", "Trash", "Deleted Messages"),
|
|
34
38
|
];
|
|
35
39
|
|
|
40
|
+
const ordered = orderFolderNodes(folders, "/");
|
|
41
|
+
|
|
36
42
|
const paths = (rows: readonly { folder: FolderTreeNode }[]): string[] =>
|
|
37
43
|
rows.map((row) => row.folder.path);
|
|
38
44
|
|
|
@@ -82,9 +88,72 @@ describe("folder ordering", () => {
|
|
|
82
88
|
});
|
|
83
89
|
});
|
|
84
90
|
|
|
85
|
-
describe("
|
|
86
|
-
|
|
91
|
+
describe("collapsing", () => {
|
|
92
|
+
it("shows the top level and nothing under it", () => {
|
|
93
|
+
const rows = collapseFolderTree(ordered, new Set(), "/");
|
|
94
|
+
assert.deepEqual(paths(rows), [
|
|
95
|
+
"INBOX",
|
|
96
|
+
"Archive",
|
|
97
|
+
"Travel",
|
|
98
|
+
"Deleted Messages",
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
87
101
|
|
|
102
|
+
it("reveals only the children of what is open", () => {
|
|
103
|
+
const rows = collapseFolderTree(ordered, new Set(["Travel"]), "/");
|
|
104
|
+
assert.deepEqual(paths(rows), [
|
|
105
|
+
"INBOX",
|
|
106
|
+
"Archive",
|
|
107
|
+
"Travel",
|
|
108
|
+
"Travel/Hotels",
|
|
109
|
+
"Deleted Messages",
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("needs every ancestor open to reach a grandchild", () => {
|
|
114
|
+
assert.deepEqual(
|
|
115
|
+
paths(collapseFolderTree(ordered, new Set(["Travel/Hotels"]), "/")),
|
|
116
|
+
["INBOX", "Archive", "Travel", "Deleted Messages"],
|
|
117
|
+
);
|
|
118
|
+
assert.deepEqual(
|
|
119
|
+
paths(
|
|
120
|
+
collapseFolderTree(ordered, new Set(["Travel", "Travel/Hotels"]), "/"),
|
|
121
|
+
),
|
|
122
|
+
[
|
|
123
|
+
"INBOX",
|
|
124
|
+
"Archive",
|
|
125
|
+
"Travel",
|
|
126
|
+
"Travel/Hotels",
|
|
127
|
+
"Travel/Hotels/Receipts",
|
|
128
|
+
"Deleted Messages",
|
|
129
|
+
],
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("marks what is open on the row", () => {
|
|
134
|
+
const rows = collapseFolderTree(ordered, new Set(["Travel"]), "/");
|
|
135
|
+
assert.deepEqual(
|
|
136
|
+
rows.map((row) => row.expanded),
|
|
137
|
+
[false, false, true, false, false],
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("shows a folder whose parent is absent from the list", () => {
|
|
142
|
+
const orphaned = orderFolderNodes(
|
|
143
|
+
[
|
|
144
|
+
node("archive", "Archive", "Archive"),
|
|
145
|
+
node("apollo", "Apollo", "Work/Projects/Apollo"),
|
|
146
|
+
],
|
|
147
|
+
"/",
|
|
148
|
+
);
|
|
149
|
+
assert.deepEqual(paths(collapseFolderTree(orphaned, new Set(), "/")), [
|
|
150
|
+
"Archive",
|
|
151
|
+
"Work/Projects/Apollo",
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("filtering", () => {
|
|
88
157
|
it("matches on the label", () => {
|
|
89
158
|
assert.equal(matchesQuery(node("a", "Archive", "Archive"), "arch"), true);
|
|
90
159
|
});
|
|
@@ -121,6 +190,17 @@ describe("filtering", () => {
|
|
|
121
190
|
);
|
|
122
191
|
});
|
|
123
192
|
|
|
193
|
+
it("opens the ancestors a match hides behind", () => {
|
|
194
|
+
assert.deepEqual([...queryExpandedPaths(ordered, "receipts", "/")].sort(), [
|
|
195
|
+
"Travel",
|
|
196
|
+
"Travel/Hotels",
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("opens nothing on an empty query, so the list stays as it was", () => {
|
|
201
|
+
assert.equal(queryExpandedPaths(ordered, "", "/").size, 0);
|
|
202
|
+
});
|
|
203
|
+
|
|
124
204
|
it("carries the depth the row indents by", () => {
|
|
125
205
|
const rows = filterFolderTree(ordered, "", "/");
|
|
126
206
|
assert.deepEqual(
|
|
@@ -134,38 +214,99 @@ describe("filtering", () => {
|
|
|
134
214
|
});
|
|
135
215
|
});
|
|
136
216
|
|
|
137
|
-
describe("
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
217
|
+
describe("create rows", () => {
|
|
218
|
+
const kinds = (expanded: string[]) =>
|
|
219
|
+
withCreateRows(
|
|
220
|
+
collapseFolderTree(ordered, new Set(expanded), "/"),
|
|
221
|
+
"/",
|
|
222
|
+
).map((entry) =>
|
|
223
|
+
entry.kind === "create"
|
|
224
|
+
? `new inside ${entry.parent.path} @${entry.depth}`
|
|
225
|
+
: entry.row.folder.path,
|
|
226
|
+
);
|
|
143
227
|
|
|
144
|
-
it("
|
|
145
|
-
assert.
|
|
228
|
+
it("offers none while everything is closed", () => {
|
|
229
|
+
assert.deepEqual(kinds([]), [
|
|
230
|
+
"INBOX",
|
|
231
|
+
"Archive",
|
|
232
|
+
"Travel",
|
|
233
|
+
"Deleted Messages",
|
|
234
|
+
]);
|
|
146
235
|
});
|
|
147
236
|
|
|
148
|
-
it("
|
|
149
|
-
assert.
|
|
237
|
+
it("puts the action after the children of the folder it belongs to", () => {
|
|
238
|
+
assert.deepEqual(kinds(["Travel"]), [
|
|
239
|
+
"INBOX",
|
|
240
|
+
"Archive",
|
|
241
|
+
"Travel",
|
|
242
|
+
"Travel/Hotels",
|
|
243
|
+
"new inside Travel @1",
|
|
244
|
+
"Deleted Messages",
|
|
245
|
+
]);
|
|
150
246
|
});
|
|
151
247
|
|
|
152
|
-
it("
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
248
|
+
it("closes the deepest branch first when several are open", () => {
|
|
249
|
+
assert.deepEqual(kinds(["Travel", "Travel/Hotels"]), [
|
|
250
|
+
"INBOX",
|
|
251
|
+
"Archive",
|
|
252
|
+
"Travel",
|
|
253
|
+
"Travel/Hotels",
|
|
254
|
+
"Travel/Hotels/Receipts",
|
|
255
|
+
"new inside Travel/Hotels @2",
|
|
256
|
+
"new inside Travel @1",
|
|
257
|
+
"Deleted Messages",
|
|
258
|
+
]);
|
|
157
259
|
});
|
|
158
260
|
|
|
159
|
-
it("
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"",
|
|
163
|
-
"
|
|
261
|
+
it("offers the action on an open folder with no children", () => {
|
|
262
|
+
assert.deepEqual(kinds(["Archive"]), [
|
|
263
|
+
"INBOX",
|
|
264
|
+
"Archive",
|
|
265
|
+
"new inside Archive @1",
|
|
266
|
+
"Travel",
|
|
267
|
+
"Deleted Messages",
|
|
268
|
+
]);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("roving focus", () => {
|
|
273
|
+
const rows = filterFolderTree(ordered, "receipts", "/");
|
|
274
|
+
|
|
275
|
+
it("skips context rows, which are branches rather than folders to open", () => {
|
|
276
|
+
assert.equal(findFirstFocusable(rows), 2);
|
|
277
|
+
assert.equal(findLastFocusable(rows), 2);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("reaches the current folder, which cannot be picked but can be opened", () => {
|
|
281
|
+
const all = collapseFolderTree(ordered, new Set(), "/");
|
|
282
|
+
assert.equal(findFirstFocusable(all), 0);
|
|
283
|
+
assert.equal(findNextFocusable(all, 3, 1), 0);
|
|
284
|
+
assert.equal(findNextFocusable(all, 0, -1), 3);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("walks only what is on screen, so closed children are skipped", () => {
|
|
288
|
+
const closed = collapseFolderTree(ordered, new Set(), "/");
|
|
289
|
+
assert.equal(
|
|
290
|
+
closed[findNextFocusable(closed, 2, 1)]?.folder.path,
|
|
291
|
+
"Deleted Messages",
|
|
164
292
|
);
|
|
165
|
-
|
|
166
|
-
assert.equal(
|
|
167
|
-
|
|
168
|
-
|
|
293
|
+
const open = collapseFolderTree(ordered, new Set(["Travel"]), "/");
|
|
294
|
+
assert.equal(
|
|
295
|
+
open[findNextFocusable(open, 2, 1)]?.folder.path,
|
|
296
|
+
"Travel/Hotels",
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("finds the parent to move focus to when a row closes", () => {
|
|
301
|
+
const open = collapseFolderTree(ordered, new Set(["Travel"]), "/");
|
|
302
|
+
assert.equal(findParentRow(open, 3, "/"), 2);
|
|
303
|
+
assert.equal(findParentRow(open, 2, "/"), -1);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns -1 when there is nothing to walk", () => {
|
|
307
|
+
assert.equal(findFirstFocusable([]), -1);
|
|
308
|
+
assert.equal(findLastFocusable([]), -1);
|
|
309
|
+
assert.equal(findNextFocusable([], 0, 1), -1);
|
|
169
310
|
});
|
|
170
311
|
});
|
|
171
312
|
|
|
@@ -179,17 +320,15 @@ describe("FolderTreePicker render", () => {
|
|
|
179
320
|
}),
|
|
180
321
|
);
|
|
181
322
|
|
|
182
|
-
it("
|
|
323
|
+
it("starts at the top level, with every row closed and openable", () => {
|
|
183
324
|
const html = render({});
|
|
184
325
|
assert.match(html, /role="tree"/);
|
|
185
|
-
assert.equal(html.match(/role="treeitem"/g)?.length,
|
|
326
|
+
assert.equal(html.match(/role="treeitem"/g)?.length, 4);
|
|
327
|
+
assert.equal(html.match(/aria-expanded="false"/g)?.length, 4);
|
|
328
|
+
assert.doesNotMatch(html, /aria-label="Move to Hotels"/);
|
|
186
329
|
assert.match(
|
|
187
|
-
tagWith(html, 'aria-label="Move to
|
|
188
|
-
/aria-level="
|
|
189
|
-
);
|
|
190
|
-
assert.match(
|
|
191
|
-
tagWith(html, 'aria-label="Move to Receipts"'),
|
|
192
|
-
/aria-level="3"/,
|
|
330
|
+
tagWith(html, 'aria-label="Move to Travel"'),
|
|
331
|
+
/aria-level="1"/,
|
|
193
332
|
);
|
|
194
333
|
});
|
|
195
334
|
|
|
@@ -217,14 +356,13 @@ describe("FolderTreePicker render", () => {
|
|
|
217
356
|
assert.doesNotMatch(html, /New folder/);
|
|
218
357
|
});
|
|
219
358
|
|
|
220
|
-
it("
|
|
359
|
+
it("pins one create action above the tree and none inside a closed list", () => {
|
|
221
360
|
const html = render({
|
|
222
361
|
onCreateFolder: () =>
|
|
223
362
|
Promise.resolve(node("made", "Made", "Travel/Made")),
|
|
224
363
|
});
|
|
225
|
-
assert.match(
|
|
226
|
-
assert.
|
|
227
|
-
assert.match(html, /aria-label="New folder inside Inbox"/);
|
|
364
|
+
assert.equal(html.match(/aria-label="New folder"/g)?.length, 1);
|
|
365
|
+
assert.doesNotMatch(html, /aria-label="New folder inside/);
|
|
228
366
|
});
|
|
229
367
|
|
|
230
368
|
it("renders the empty state when no folder matches", () => {
|
|
@@ -127,8 +127,9 @@ function Picker({
|
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** The list at rest: top level only, one tap deep into anything. */
|
|
130
131
|
export const Default: Story = {
|
|
131
|
-
name: "Default (
|
|
132
|
+
name: "Default (collapsed to top level)",
|
|
132
133
|
render: () => <Picker />,
|
|
133
134
|
};
|
|
134
135
|
|
|
@@ -198,12 +199,40 @@ const typeFolderName = (canvasElement: HTMLElement, name: string) => {
|
|
|
198
199
|
|
|
199
200
|
const tick = () => new Promise((resolve) => setTimeout(resolve, 60));
|
|
200
201
|
|
|
202
|
+
const expand = async (canvasElement: HTMLElement, ...labels: string[]) => {
|
|
203
|
+
for (const label of labels) {
|
|
204
|
+
clickAriaLabel(canvasElement, `Move to ${label}`);
|
|
205
|
+
await tick();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* One tap picks the destination and opens it. The children arrive indented,
|
|
211
|
+
* with "New folder" sitting at the end of them as the last folder inside.
|
|
212
|
+
*/
|
|
213
|
+
export const Expanded: Story = {
|
|
214
|
+
name: "A folder opened (children and its New folder)",
|
|
215
|
+
render: () => <Picker />,
|
|
216
|
+
play: async ({ canvasElement }) => {
|
|
217
|
+
await expand(canvasElement, "Travel");
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/** Three levels open at once, each with its own place to add a folder. */
|
|
222
|
+
export const DeepExpansion: Story = {
|
|
223
|
+
name: "Opened three levels deep",
|
|
224
|
+
render: () => <Picker />,
|
|
225
|
+
play: async ({ canvasElement }) => {
|
|
226
|
+
await expand(canvasElement, "Travel", "Hotels", "Receipts");
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
201
230
|
/**
|
|
202
|
-
* "rec" matches two
|
|
203
|
-
*
|
|
231
|
+
* "rec" matches two nested folders. Their parents open to put them on screen
|
|
232
|
+
* and read as the branches they are, not as an answer to what was typed.
|
|
204
233
|
*/
|
|
205
234
|
export const Filtered: Story = {
|
|
206
|
-
name: "Filtered (ancestors
|
|
235
|
+
name: "Filtered (ancestors opened for context)",
|
|
207
236
|
render: () => <Picker />,
|
|
208
237
|
play: async ({ canvasElement }) => {
|
|
209
238
|
typeFilter(canvasElement, "rec");
|
|
@@ -214,20 +243,22 @@ export const CreateTopLevel: Story = {
|
|
|
214
243
|
name: "Create a top-level folder",
|
|
215
244
|
render: () => <Picker />,
|
|
216
245
|
play: async ({ canvasElement }) => {
|
|
217
|
-
|
|
246
|
+
clickAriaLabel(canvasElement, "New folder");
|
|
218
247
|
await tick();
|
|
219
248
|
typeFolderName(canvasElement, "Insurance");
|
|
220
249
|
},
|
|
221
250
|
};
|
|
222
251
|
|
|
223
252
|
/**
|
|
224
|
-
* The
|
|
225
|
-
* and states the parent as text, so there is no second
|
|
253
|
+
* The folder you opened is the answer to "inside where": the action sits among
|
|
254
|
+
* its children and the form states the parent as text, so there is no second
|
|
255
|
+
* choice to make.
|
|
226
256
|
*/
|
|
227
257
|
export const CreateSubfolder: Story = {
|
|
228
|
-
name: "Create a
|
|
258
|
+
name: "Create a folder inside an opened one",
|
|
229
259
|
render: () => <Picker />,
|
|
230
260
|
play: async ({ canvasElement }) => {
|
|
261
|
+
await expand(canvasElement, "Travel");
|
|
231
262
|
clickAriaLabel(canvasElement, "New folder inside Travel");
|
|
232
263
|
await tick();
|
|
233
264
|
typeFolderName(canvasElement, "Car hire");
|
|
@@ -242,6 +273,7 @@ export const CreatePending: Story = {
|
|
|
242
273
|
name: "Create — waiting for the server",
|
|
243
274
|
render: () => <Picker onCreateFolder={neverResolves} />,
|
|
244
275
|
play: async ({ canvasElement }) => {
|
|
276
|
+
await expand(canvasElement, "Finance");
|
|
245
277
|
clickAriaLabel(canvasElement, "New folder inside Finance");
|
|
246
278
|
await tick();
|
|
247
279
|
typeFolderName(canvasElement, "Mortgage");
|
|
@@ -261,6 +293,7 @@ export const CreateFailed: Story = {
|
|
|
261
293
|
/>
|
|
262
294
|
),
|
|
263
295
|
play: async ({ canvasElement }) => {
|
|
296
|
+
await expand(canvasElement, "Finance");
|
|
264
297
|
clickAriaLabel(canvasElement, "New folder inside Finance");
|
|
265
298
|
await tick();
|
|
266
299
|
typeFolderName(canvasElement, "Mortgage");
|
|
@@ -296,7 +329,7 @@ function WizardFolderStep() {
|
|
|
296
329
|
<p className="px-1 text-xs text-fg-muted">
|
|
297
330
|
{chosen
|
|
298
331
|
? `Moving to ${chosen.label}.`
|
|
299
|
-
: "
|
|
332
|
+
: "Tap a folder to open it, or make a new one where you want it."}
|
|
300
333
|
</p>
|
|
301
334
|
<div className="flex min-h-0 flex-1 overflow-hidden rounded-lg border border-line bg-surface">
|
|
302
335
|
<FolderTreePicker
|