@remit/ui 0.0.128 → 0.0.130
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.
- package/package.json +2 -3
- package/src/components/brief-sections.keyboard.test.ts +8 -36
- package/src/components/compose-body.language.test.ts +8 -58
- package/src/components/compose-body.spellcheck.test.ts +8 -55
- package/src/components/event-editor.render.test.ts +2 -2
- package/src/components/filter-rule-editor.create.test.ts +10 -59
- package/src/components/folder-tree-picker.create.test.ts +18 -63
- package/src/components/mail-action-toolbar.tsx +17 -10
- package/src/components/message-list-pane.brief-filter.test.ts +4 -31
- package/src/components/nav-link-surface.interaction.test.ts +7 -34
- package/src/components/nav-sidebar.keyboard.test.ts +11 -40
- package/src/components/password-input.render.test.ts +8 -50
- package/src/components/plain-text-editor.mount.test.ts +11 -66
- package/src/components/reading-pane.tsx +10 -1
- package/src/components/rich-text-editor.mount.test.ts +9 -63
- package/src/components/rich-text-formatting.test.ts +27 -45
- package/src/components/rich-text-image-node.test.ts +60 -83
- package/src/components/rich-text-markdown.test.ts +3 -25
- package/src/components/rich-text-paste.test.ts +28 -42
- package/src/components/rich-text-spellcheck-menu.test.ts +28 -94
- package/src/components/rich-text-spellcheck.test.ts +11 -65
- package/src/components/swipeable-row.gesture.test.ts +8 -42
- package/src/components/touch-list.follows-sections.test.ts +4 -27
- package/src/lib/adopted-html.test.ts +3 -22
- package/src/lib/compose-language.test.ts +2 -7
- package/src/lib/roving-focus.test.ts +16 -43
- package/src/lib/use-list-cursor.test.ts +3 -27
- package/src/lib/use-list-keyboard.test.ts +6 -30
- package/src/lib/use-long-press.test.ts +12 -43
- package/src/lib/use-rendered-row-ids.test.ts +3 -24
- package/src/lib/use-selection.hook.test.ts +3 -26
- package/src/lib/use-suggest-list.test.ts +5 -29
- package/src/lib/use-triage-keyboard.test.ts +6 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.130",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -49,12 +49,11 @@
|
|
|
49
49
|
"@types/react-dom": "^19"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@remit/test-dom": "*",
|
|
52
53
|
"@storybook/react": "^9",
|
|
53
|
-
"@types/jsdom": "^28.0.3",
|
|
54
54
|
"dictionary-en": "4.0.0",
|
|
55
55
|
"dictionary-en-gb": "3.0.0",
|
|
56
56
|
"dictionary-nl": "2.0.0",
|
|
57
|
-
"jsdom": "^29.1.1",
|
|
58
57
|
"react": "^19",
|
|
59
58
|
"react-dom": "^19",
|
|
60
59
|
"typescript": "*"
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* BriefSections arrow-key traversal (#143) — mounted against jsdom rather than
|
|
3
3
|
* `renderToString`, since focus and keydown need a real `document`.
|
|
4
4
|
*/
|
|
5
|
+
import "@remit/test-dom";
|
|
5
6
|
import assert from "node:assert/strict";
|
|
6
|
-
import {
|
|
7
|
-
import type { JSDOM } from "jsdom";
|
|
7
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
8
8
|
import { act, createElement } from "react";
|
|
9
9
|
import { createRoot, type Root } from "react-dom/client";
|
|
10
10
|
import { LIST_ROW_SELECTOR } from "../lib/roving-focus.js";
|
|
@@ -61,39 +61,11 @@ const sections: ThreadSection[] = [
|
|
|
61
61
|
},
|
|
62
62
|
];
|
|
63
63
|
|
|
64
|
-
let dom: JSDOM;
|
|
65
64
|
let container: HTMLElement;
|
|
66
65
|
let root: Root;
|
|
67
66
|
|
|
68
|
-
before(async () => {
|
|
69
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
70
|
-
dom = new JSDOMCtor(
|
|
71
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
72
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
73
|
-
);
|
|
74
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
75
|
-
globalThis.document = dom.window.document;
|
|
76
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
77
|
-
globalThis.Element = dom.window.Element;
|
|
78
|
-
globalThis.KeyboardEvent = dom.window.KeyboardEvent;
|
|
79
|
-
globalThis.MutationObserver = dom.window.MutationObserver;
|
|
80
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
81
|
-
value: dom.window.navigator,
|
|
82
|
-
configurable: true,
|
|
83
|
-
});
|
|
84
|
-
(
|
|
85
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
86
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
after(() => {
|
|
90
|
-
dom.window.close();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
67
|
beforeEach(() => {
|
|
94
|
-
container =
|
|
95
|
-
"root",
|
|
96
|
-
) as unknown as HTMLElement;
|
|
68
|
+
container = document.getElementById("root") as unknown as HTMLElement;
|
|
97
69
|
container.innerHTML = "";
|
|
98
70
|
root = createRoot(container);
|
|
99
71
|
});
|
|
@@ -106,7 +78,7 @@ afterEach(() => {
|
|
|
106
78
|
|
|
107
79
|
function pressKey(target: Element, key: string, shiftKey = false) {
|
|
108
80
|
target.dispatchEvent(
|
|
109
|
-
new
|
|
81
|
+
new KeyboardEvent("keydown", { key, bubbles: true, shiftKey }),
|
|
110
82
|
);
|
|
111
83
|
}
|
|
112
84
|
|
|
@@ -147,7 +119,7 @@ describe("BriefSections arrow-key traversal", () => {
|
|
|
147
119
|
act(() => items[0]?.focus());
|
|
148
120
|
act(() => pressKey(items[0] as Element, "ArrowDown"));
|
|
149
121
|
act(() => pressKey(items[1] as Element, "ArrowDown"));
|
|
150
|
-
assert.equal(
|
|
122
|
+
assert.equal(document.activeElement, items[2]);
|
|
151
123
|
|
|
152
124
|
act(() => (items[2] as HTMLElement).click());
|
|
153
125
|
assert.equal(selected, "t3");
|
|
@@ -159,10 +131,10 @@ describe("BriefSections arrow-key traversal", () => {
|
|
|
159
131
|
|
|
160
132
|
act(() => items[2]?.focus());
|
|
161
133
|
act(() => pressKey(items[2] as Element, "ArrowUp"));
|
|
162
|
-
assert.equal(
|
|
134
|
+
assert.equal(document.activeElement, items[1]);
|
|
163
135
|
|
|
164
136
|
act(() => pressKey(items[1] as Element, "Home"));
|
|
165
|
-
assert.equal(
|
|
137
|
+
assert.equal(document.activeElement, items[0]);
|
|
166
138
|
});
|
|
167
139
|
|
|
168
140
|
it("steps over the section headers between rows", () => {
|
|
@@ -175,7 +147,7 @@ describe("BriefSections arrow-key traversal", () => {
|
|
|
175
147
|
|
|
176
148
|
act(() => items[1]?.focus());
|
|
177
149
|
act(() => pressKey(items[1] as Element, "ArrowDown"));
|
|
178
|
-
assert.equal(
|
|
150
|
+
assert.equal(document.activeElement, items[2]);
|
|
179
151
|
});
|
|
180
152
|
});
|
|
181
153
|
|
|
@@ -7,33 +7,22 @@
|
|
|
7
7
|
* and what it falls back on has to be a set detection can choose inside — a set
|
|
8
8
|
* of one is detection switched off, and the message then goes out tagged `en`
|
|
9
9
|
* with every Dutch word underlined.
|
|
10
|
-
*
|
|
11
|
-
* React is imported after the jsdom globals are installed so its DOM bindings
|
|
12
|
-
* bind to jsdom's prototypes.
|
|
13
10
|
*/
|
|
14
11
|
|
|
12
|
+
import "@remit/test-dom";
|
|
15
13
|
import assert from "node:assert/strict";
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import type
|
|
19
|
-
act as reactAct,
|
|
20
|
-
createElement as reactCreateElement,
|
|
21
|
-
} from "react";
|
|
22
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
14
|
+
import { afterEach, before, beforeEach, describe, it } from "node:test";
|
|
15
|
+
import { act, createElement } from "react";
|
|
16
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
23
17
|
import { defaultComposeLanguages } from "../lib/compose-language.js";
|
|
24
|
-
import
|
|
18
|
+
import { ComposeBody } from "./compose-body.js";
|
|
25
19
|
import type {
|
|
26
20
|
SpellcheckOptions,
|
|
27
21
|
SpellProvider,
|
|
28
22
|
} from "./rich-text-spellcheck.js";
|
|
29
23
|
|
|
30
|
-
let dom: JSDOM;
|
|
31
24
|
let container: HTMLElement;
|
|
32
25
|
let root: Root;
|
|
33
|
-
let act: typeof reactAct;
|
|
34
|
-
let createElement: typeof reactCreateElement;
|
|
35
|
-
let createRoot: typeof reactCreateRoot;
|
|
36
|
-
let ComposeBody: typeof ComposeBodyType;
|
|
37
26
|
|
|
38
27
|
/** What the published image stages, per `REMIT_SPELLCHECK_LANGUAGES`. */
|
|
39
28
|
const BUILT = ["en", "en-GB", "nl"];
|
|
@@ -117,35 +106,7 @@ const chip = (): HTMLElement => {
|
|
|
117
106
|
return control;
|
|
118
107
|
};
|
|
119
108
|
|
|
120
|
-
before(
|
|
121
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
122
|
-
dom = new JSDOMCtor(
|
|
123
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
124
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
125
|
-
);
|
|
126
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
127
|
-
globalThis.document = dom.window.document;
|
|
128
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
129
|
-
globalThis.Element = dom.window.Element;
|
|
130
|
-
globalThis.Node = dom.window.Node;
|
|
131
|
-
globalThis.Event = dom.window.Event;
|
|
132
|
-
globalThis.MouseEvent = dom.window.MouseEvent;
|
|
133
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
134
|
-
globalThis.MutationObserver = dom.window.MutationObserver;
|
|
135
|
-
globalThis.Range = dom.window.Range;
|
|
136
|
-
globalThis.AbortController = dom.window.AbortController;
|
|
137
|
-
globalThis.AbortSignal = dom.window.AbortSignal;
|
|
138
|
-
globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
|
|
139
|
-
globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(
|
|
140
|
-
dom.window,
|
|
141
|
-
);
|
|
142
|
-
globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(
|
|
143
|
-
dom.window,
|
|
144
|
-
);
|
|
145
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
146
|
-
value: dom.window.navigator,
|
|
147
|
-
configurable: true,
|
|
148
|
-
});
|
|
109
|
+
before(() => {
|
|
149
110
|
// The marks are drawn through the CSS Custom Highlight registry, which jsdom
|
|
150
111
|
// has neither half of, and without it no checker is opened at all.
|
|
151
112
|
Object.defineProperty(globalThis, "CSS", {
|
|
@@ -156,18 +117,11 @@ before(async () => {
|
|
|
156
117
|
value: Marks,
|
|
157
118
|
configurable: true,
|
|
158
119
|
});
|
|
159
|
-
(
|
|
160
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
161
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
162
|
-
|
|
163
|
-
({ act, createElement } = await import("react"));
|
|
164
|
-
({ createRoot } = await import("react-dom/client"));
|
|
165
|
-
({ ComposeBody } = await import("./compose-body.js"));
|
|
166
120
|
});
|
|
167
121
|
|
|
168
122
|
beforeEach(() => {
|
|
169
|
-
container =
|
|
170
|
-
|
|
123
|
+
container = document.createElement("div");
|
|
124
|
+
document.body.append(container);
|
|
171
125
|
});
|
|
172
126
|
|
|
173
127
|
afterEach(async () => {
|
|
@@ -177,10 +131,6 @@ afterEach(async () => {
|
|
|
177
131
|
container.remove();
|
|
178
132
|
});
|
|
179
133
|
|
|
180
|
-
after(() => {
|
|
181
|
-
dom.window.close();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
134
|
const mount = async (
|
|
185
135
|
text: string,
|
|
186
136
|
languages: readonly string[],
|
|
@@ -16,15 +16,12 @@
|
|
|
16
16
|
* way out so nothing is left underlined by a checker that is gone.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import "@remit/test-dom";
|
|
19
20
|
import assert from "node:assert/strict";
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
import type
|
|
23
|
-
|
|
24
|
-
createElement as reactCreateElement,
|
|
25
|
-
} from "react";
|
|
26
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
27
|
-
import type { ComposeBody as ComposeBodyType } from "./compose-body.js";
|
|
21
|
+
import { afterEach, before, beforeEach, describe, it } from "node:test";
|
|
22
|
+
import { act, createElement } from "react";
|
|
23
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
24
|
+
import { ComposeBody } from "./compose-body.js";
|
|
28
25
|
import type {
|
|
29
26
|
CheckRequest,
|
|
30
27
|
ProviderStatus,
|
|
@@ -33,13 +30,8 @@ import type {
|
|
|
33
30
|
SuggestRequest,
|
|
34
31
|
} from "./rich-text-spellcheck.js";
|
|
35
32
|
|
|
36
|
-
let dom: JSDOM;
|
|
37
33
|
let container: HTMLElement;
|
|
38
34
|
let root: Root;
|
|
39
|
-
let act: typeof reactAct;
|
|
40
|
-
let createElement: typeof reactCreateElement;
|
|
41
|
-
let createRoot: typeof reactCreateRoot;
|
|
42
|
-
let ComposeBody: typeof ComposeBodyType;
|
|
43
35
|
|
|
44
36
|
/** Short enough that detection declines and the chip stays on the account default. */
|
|
45
37
|
const DOCUMENT = "<p>Ths is redy.</p>";
|
|
@@ -215,35 +207,7 @@ const chooseLanguage = async (tag: string): Promise<void> => {
|
|
|
215
207
|
await settle();
|
|
216
208
|
};
|
|
217
209
|
|
|
218
|
-
before(
|
|
219
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
220
|
-
dom = new JSDOMCtor(
|
|
221
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
222
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
223
|
-
);
|
|
224
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
225
|
-
globalThis.document = dom.window.document;
|
|
226
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
227
|
-
globalThis.Element = dom.window.Element;
|
|
228
|
-
globalThis.Node = dom.window.Node;
|
|
229
|
-
globalThis.Event = dom.window.Event;
|
|
230
|
-
globalThis.MouseEvent = dom.window.MouseEvent;
|
|
231
|
-
globalThis.DOMParser = dom.window.DOMParser;
|
|
232
|
-
globalThis.MutationObserver = dom.window.MutationObserver;
|
|
233
|
-
globalThis.Range = dom.window.Range;
|
|
234
|
-
globalThis.AbortController = dom.window.AbortController;
|
|
235
|
-
globalThis.AbortSignal = dom.window.AbortSignal;
|
|
236
|
-
globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
|
|
237
|
-
globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(
|
|
238
|
-
dom.window,
|
|
239
|
-
);
|
|
240
|
-
globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(
|
|
241
|
-
dom.window,
|
|
242
|
-
);
|
|
243
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
244
|
-
value: dom.window.navigator,
|
|
245
|
-
configurable: true,
|
|
246
|
-
});
|
|
210
|
+
before(() => {
|
|
247
211
|
// The marks are drawn through the CSS Custom Highlight registry, which jsdom
|
|
248
212
|
// has neither half of. Without both, the editor draws nothing and never opens
|
|
249
213
|
// a provider at all — which is the browser this suite would be testing.
|
|
@@ -255,18 +219,11 @@ before(async () => {
|
|
|
255
219
|
value: Marks,
|
|
256
220
|
configurable: true,
|
|
257
221
|
});
|
|
258
|
-
(
|
|
259
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
260
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
261
|
-
|
|
262
|
-
({ act, createElement } = await import("react"));
|
|
263
|
-
({ createRoot } = await import("react-dom/client"));
|
|
264
|
-
({ ComposeBody } = await import("./compose-body.js"));
|
|
265
222
|
});
|
|
266
223
|
|
|
267
224
|
beforeEach(() => {
|
|
268
|
-
container =
|
|
269
|
-
|
|
225
|
+
container = document.createElement("div");
|
|
226
|
+
document.body.append(container);
|
|
270
227
|
});
|
|
271
228
|
|
|
272
229
|
afterEach(async () => {
|
|
@@ -276,10 +233,6 @@ afterEach(async () => {
|
|
|
276
233
|
container.remove();
|
|
277
234
|
});
|
|
278
235
|
|
|
279
|
-
after(() => {
|
|
280
|
-
dom.window.close();
|
|
281
|
-
});
|
|
282
|
-
|
|
283
236
|
const mount = async (options: SpellcheckOptions): Promise<void> => {
|
|
284
237
|
await act(async () => {
|
|
285
238
|
root = createRoot(container);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import "@remit/test-dom";
|
|
1
2
|
import assert from "node:assert/strict";
|
|
2
3
|
import { describe, it } from "node:test";
|
|
3
|
-
import { JSDOM } from "jsdom";
|
|
4
4
|
import { createElement } from "react";
|
|
5
5
|
import { renderToString } from "react-dom/server";
|
|
6
6
|
import type { CalendarDescriptor, EventDraft } from "./calendar-types.js";
|
|
@@ -52,7 +52,7 @@ const render = (props: Partial<Parameters<typeof EventEditor>[0]>) =>
|
|
|
52
52
|
|
|
53
53
|
/** The same render read as a document, so no assertion depends on attribute order. */
|
|
54
54
|
const parse = (props: Partial<Parameters<typeof EventEditor>[0]>) =>
|
|
55
|
-
|
|
55
|
+
document.createRange().createContextualFragment(render(props));
|
|
56
56
|
|
|
57
57
|
const field = (dom: DocumentFragment, label: string) =>
|
|
58
58
|
dom.querySelector<HTMLInputElement>(`input[aria-label="${label}"]`);
|
|
@@ -3,20 +3,15 @@
|
|
|
3
3
|
* folder is chosen, that a nested folder is told apart from a same-named
|
|
4
4
|
* sibling, and that a folder can be made inside another one from here. Mounted
|
|
5
5
|
* against jsdom for the tree interaction, the create form state and the async
|
|
6
|
-
* resolve.
|
|
7
|
-
* controlled value tracker binds to jsdom's prototypes.
|
|
6
|
+
* resolve.
|
|
8
7
|
*/
|
|
8
|
+
import "@remit/test-dom";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import type
|
|
13
|
-
act as reactAct,
|
|
14
|
-
createElement as reactCreateElement,
|
|
15
|
-
useState as reactUseState,
|
|
16
|
-
} from "react";
|
|
17
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
10
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
11
|
+
import { act, createElement, useState } from "react";
|
|
12
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
18
13
|
import type { FilterRule, PreviewCount } from "./filter-rule.js";
|
|
19
|
-
import
|
|
14
|
+
import { FilterRuleEditor } from "./filter-rule-editor.js";
|
|
20
15
|
import type { FolderTreeNode } from "./folder-tree-picker.js";
|
|
21
16
|
|
|
22
17
|
// `Prullenbak` labelled Trash is the account's own naming; two folders named
|
|
@@ -42,54 +37,11 @@ const rule: FilterRule = {
|
|
|
42
37
|
|
|
43
38
|
const preview: PreviewCount = { status: "ready", count: 3 };
|
|
44
39
|
|
|
45
|
-
let dom: JSDOM;
|
|
46
40
|
let container: HTMLElement;
|
|
47
41
|
let root: Root;
|
|
48
|
-
let act: typeof reactAct;
|
|
49
|
-
let createElement: typeof reactCreateElement;
|
|
50
|
-
let useState: typeof reactUseState;
|
|
51
|
-
let createRoot: typeof reactCreateRoot;
|
|
52
|
-
let FilterRuleEditor: typeof FilterRuleEditorType;
|
|
53
|
-
|
|
54
|
-
before(async () => {
|
|
55
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
56
|
-
dom = new JSDOMCtor(
|
|
57
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
58
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
59
|
-
);
|
|
60
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
61
|
-
globalThis.document = dom.window.document;
|
|
62
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
63
|
-
globalThis.Element = dom.window.Element;
|
|
64
|
-
globalThis.Event = dom.window.Event;
|
|
65
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
66
|
-
value: dom.window.navigator,
|
|
67
|
-
configurable: true,
|
|
68
|
-
});
|
|
69
|
-
(
|
|
70
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
71
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
72
|
-
Object.defineProperty(dom.window.HTMLElement.prototype, "scrollIntoView", {
|
|
73
|
-
configurable: true,
|
|
74
|
-
value: () => undefined,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const react = await import("react");
|
|
78
|
-
act = react.act;
|
|
79
|
-
createElement = react.createElement;
|
|
80
|
-
useState = react.useState;
|
|
81
|
-
({ createRoot } = await import("react-dom/client"));
|
|
82
|
-
({ FilterRuleEditor } = await import("./filter-rule-editor.js"));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
after(() => {
|
|
86
|
-
dom.window.close();
|
|
87
|
-
});
|
|
88
42
|
|
|
89
43
|
beforeEach(() => {
|
|
90
|
-
container =
|
|
91
|
-
"root",
|
|
92
|
-
) as unknown as HTMLElement;
|
|
44
|
+
container = document.getElementById("root") as unknown as HTMLElement;
|
|
93
45
|
container.innerHTML = "";
|
|
94
46
|
root = createRoot(container);
|
|
95
47
|
});
|
|
@@ -140,8 +92,7 @@ const mount = async ({
|
|
|
140
92
|
};
|
|
141
93
|
|
|
142
94
|
const click = async (element: Element | null | undefined) => {
|
|
143
|
-
if (!(element instanceof
|
|
144
|
-
assert.fail("control not rendered");
|
|
95
|
+
if (!(element instanceof HTMLElement)) assert.fail("control not rendered");
|
|
145
96
|
await act(async () => {
|
|
146
97
|
element.click();
|
|
147
98
|
});
|
|
@@ -187,10 +138,10 @@ const typeName = async (value: string) => {
|
|
|
187
138
|
assert.ok(input, "the folder name field is on screen");
|
|
188
139
|
await act(async () => {
|
|
189
140
|
Object.getOwnPropertyDescriptor(
|
|
190
|
-
|
|
141
|
+
HTMLInputElement.prototype,
|
|
191
142
|
"value",
|
|
192
143
|
)?.set?.call(input, value);
|
|
193
|
-
input.dispatchEvent(new
|
|
144
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
194
145
|
});
|
|
195
146
|
};
|
|
196
147
|
|
|
@@ -2,23 +2,14 @@
|
|
|
2
2
|
* Opening folders, and the inline create form that sits at the end of an opened
|
|
3
3
|
* folder's children: where it opens, what it says the parent is, and how it
|
|
4
4
|
* behaves while the mail server is confirming the folder. Mounted against jsdom
|
|
5
|
-
* for the anchoring, the field state and the async resolve.
|
|
6
|
-
* after the jsdom globals are installed so the controlled value tracker binds to
|
|
7
|
-
* jsdom's prototypes.
|
|
5
|
+
* for the anchoring, the field state and the async resolve.
|
|
8
6
|
*/
|
|
7
|
+
import "@remit/test-dom";
|
|
9
8
|
import assert from "node:assert/strict";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import type
|
|
13
|
-
|
|
14
|
-
createElement as reactCreateElement,
|
|
15
|
-
useState as reactUseState,
|
|
16
|
-
} from "react";
|
|
17
|
-
import type { Root, createRoot as reactCreateRoot } from "react-dom/client";
|
|
18
|
-
import type {
|
|
19
|
-
FolderTreeNode,
|
|
20
|
-
FolderTreePicker as FolderTreePickerType,
|
|
21
|
-
} from "./folder-tree-picker.js";
|
|
9
|
+
import { afterEach, before, beforeEach, describe, it } from "node:test";
|
|
10
|
+
import { act, createElement, useState } from "react";
|
|
11
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
12
|
+
import { type FolderTreeNode, FolderTreePicker } from "./folder-tree-picker.js";
|
|
22
13
|
|
|
23
14
|
const folders: FolderTreeNode[] = [
|
|
24
15
|
{ id: "inbox", label: "Inbox", path: "INBOX", isCurrent: true },
|
|
@@ -29,57 +20,23 @@ const folders: FolderTreeNode[] = [
|
|
|
29
20
|
{ id: "archive", label: "Archive", path: "Archive" },
|
|
30
21
|
];
|
|
31
22
|
|
|
32
|
-
let dom: JSDOM;
|
|
33
23
|
const scrolledIntoView: Element[] = [];
|
|
34
24
|
let container: HTMLElement;
|
|
35
25
|
let root: Root;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let createRoot: typeof reactCreateRoot;
|
|
40
|
-
let FolderTreePicker: typeof FolderTreePickerType;
|
|
41
|
-
|
|
42
|
-
before(async () => {
|
|
43
|
-
const { JSDOM: JSDOMCtor } = await import("jsdom");
|
|
44
|
-
dom = new JSDOMCtor(
|
|
45
|
-
"<!doctype html><html><body><div id=root></div></body></html>",
|
|
46
|
-
{ url: "http://localhost/", pretendToBeVisual: true },
|
|
47
|
-
);
|
|
48
|
-
globalThis.window = dom.window as unknown as typeof globalThis.window;
|
|
49
|
-
globalThis.document = dom.window.document;
|
|
50
|
-
globalThis.HTMLElement = dom.window.HTMLElement;
|
|
51
|
-
globalThis.Element = dom.window.Element;
|
|
52
|
-
globalThis.Event = dom.window.Event;
|
|
53
|
-
Object.defineProperty(globalThis, "navigator", {
|
|
54
|
-
value: dom.window.navigator,
|
|
55
|
-
configurable: true,
|
|
56
|
-
});
|
|
57
|
-
(
|
|
58
|
-
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
59
|
-
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
60
|
-
Object.defineProperty(dom.window.HTMLElement.prototype, "scrollIntoView", {
|
|
26
|
+
|
|
27
|
+
before(() => {
|
|
28
|
+
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
|
|
61
29
|
configurable: true,
|
|
62
30
|
value: function scrollIntoView(this: Element) {
|
|
63
31
|
scrolledIntoView.push(this);
|
|
64
32
|
},
|
|
65
33
|
});
|
|
66
|
-
|
|
67
|
-
const react = await import("react");
|
|
68
|
-
act = react.act;
|
|
69
|
-
createElement = react.createElement;
|
|
70
|
-
useState = react.useState;
|
|
71
|
-
({ createRoot } = await import("react-dom/client"));
|
|
72
|
-
({ FolderTreePicker } = await import("./folder-tree-picker.js"));
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
after(() => {
|
|
76
|
-
dom.window.close();
|
|
77
34
|
});
|
|
78
35
|
|
|
79
36
|
beforeEach(() => {
|
|
80
37
|
scrolledIntoView.length = 0;
|
|
81
|
-
container =
|
|
82
|
-
|
|
38
|
+
container = document.createElement("div");
|
|
39
|
+
document.body.append(container);
|
|
83
40
|
root = createRoot(container);
|
|
84
41
|
});
|
|
85
42
|
|
|
@@ -90,7 +47,7 @@ afterEach(async () => {
|
|
|
90
47
|
container.remove();
|
|
91
48
|
});
|
|
92
49
|
|
|
93
|
-
type PickerProps = Partial<Parameters<typeof
|
|
50
|
+
type PickerProps = Partial<Parameters<typeof FolderTreePicker>[0]>;
|
|
94
51
|
|
|
95
52
|
const mount = async (props: PickerProps = {}) => {
|
|
96
53
|
await act(async () => {
|
|
@@ -149,12 +106,12 @@ const typeName = async (value: string) => {
|
|
|
149
106
|
const input = nameField();
|
|
150
107
|
assert.ok(input, "name field not rendered");
|
|
151
108
|
const setter = Object.getOwnPropertyDescriptor(
|
|
152
|
-
|
|
109
|
+
HTMLInputElement.prototype,
|
|
153
110
|
"value",
|
|
154
111
|
)?.set;
|
|
155
112
|
await act(async () => {
|
|
156
113
|
setter?.call(input, value);
|
|
157
|
-
input.dispatchEvent(new
|
|
114
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
158
115
|
});
|
|
159
116
|
};
|
|
160
117
|
|
|
@@ -165,21 +122,19 @@ const typeFilter = async (value: string) => {
|
|
|
165
122
|
const input = filterField();
|
|
166
123
|
assert.ok(input, "filter field not rendered");
|
|
167
124
|
const setter = Object.getOwnPropertyDescriptor(
|
|
168
|
-
|
|
125
|
+
HTMLInputElement.prototype,
|
|
169
126
|
"value",
|
|
170
127
|
)?.set;
|
|
171
128
|
await act(async () => {
|
|
172
129
|
setter?.call(input, value);
|
|
173
|
-
input.dispatchEvent(new
|
|
130
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
174
131
|
});
|
|
175
132
|
};
|
|
176
133
|
|
|
177
134
|
const press = async (target: Element | null | undefined, key: string) => {
|
|
178
135
|
assert.ok(target, "control not rendered");
|
|
179
136
|
await act(async () => {
|
|
180
|
-
target.dispatchEvent(
|
|
181
|
-
new dom.window.KeyboardEvent("keydown", { key, bubbles: true }),
|
|
182
|
-
);
|
|
137
|
+
target.dispatchEvent(new KeyboardEvent("keydown", { key, bubbles: true }));
|
|
183
138
|
});
|
|
184
139
|
};
|
|
185
140
|
|
|
@@ -680,7 +635,7 @@ describe("a draft outlives the rows under it", () => {
|
|
|
680
635
|
|
|
681
636
|
describe("filter and keyboard", () => {
|
|
682
637
|
const focused = (): string | null =>
|
|
683
|
-
|
|
638
|
+
document.activeElement?.getAttribute("aria-label") ?? null;
|
|
684
639
|
|
|
685
640
|
it("opens the ancestors a match hides behind", async () => {
|
|
686
641
|
await mount({});
|
|
@@ -24,9 +24,10 @@ export interface MailActionToolbarProps {
|
|
|
24
24
|
/** Whether a thread is open. Drives the no-op-explain behaviour, never `disabled`. */
|
|
25
25
|
hasThread: boolean;
|
|
26
26
|
/**
|
|
27
|
-
* Fired when an action button is pressed
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Fired when an action button is pressed and there is nothing behind it —
|
|
28
|
+
* no thread open, or no handler wired for that verb. The host surfaces a
|
|
29
|
+
* one-line inline explanation — never a toast, never a disabled button
|
|
30
|
+
* (`doc/rules/ux.md`: never disable a control).
|
|
30
31
|
*/
|
|
31
32
|
onUnavailable?: (action: MailAction) => void;
|
|
32
33
|
/** A one-line inline notice rendered under the toolbar (e.g. "Open a message first"). */
|
|
@@ -59,10 +60,12 @@ export interface MailActionToolbarProps {
|
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* The reading-pane action toolbar, shared between the remit-ui AppShell
|
|
62
|
-
* reference and the live client. Buttons are always pressable:
|
|
63
|
-
* open
|
|
64
|
-
*
|
|
65
|
-
* the
|
|
63
|
+
* reference and the live client. Buttons are always pressable: where a press
|
|
64
|
+
* cannot act — no thread open, or no handler wired for that verb — it calls
|
|
65
|
+
* `onUnavailable(action)` so the host can explain why, rather than greying out
|
|
66
|
+
* (the never-disable tenet — a `disabled` button gives the user no path to
|
|
67
|
+
* learn what it does or what they must do first). Every press is answered; the
|
|
68
|
+
* bar has no branch that ends in silence.
|
|
66
69
|
*/
|
|
67
70
|
export function MailActionToolbar({
|
|
68
71
|
hasThread,
|
|
@@ -85,12 +88,16 @@ export function MailActionToolbar({
|
|
|
85
88
|
children,
|
|
86
89
|
className,
|
|
87
90
|
}: MailActionToolbarProps) {
|
|
91
|
+
// A verb with no handler behind it is unavailable in exactly the way one with
|
|
92
|
+
// no thread is, and says so the same way. Dropping the press instead left the
|
|
93
|
+
// three reply verbs pressable and mute for as long as a host took to wire
|
|
94
|
+
// them (#803).
|
|
88
95
|
const act = (action: MailAction, handler?: () => void) => () => {
|
|
89
|
-
if (!hasThread) {
|
|
96
|
+
if (!hasThread || !handler) {
|
|
90
97
|
onUnavailable?.(action);
|
|
91
98
|
return;
|
|
92
99
|
}
|
|
93
|
-
handler
|
|
100
|
+
handler();
|
|
94
101
|
};
|
|
95
102
|
|
|
96
103
|
return (
|
|
@@ -166,7 +173,7 @@ export function MailActionToolbar({
|
|
|
166
173
|
<div className="flex-1" />
|
|
167
174
|
{children}
|
|
168
175
|
</header>
|
|
169
|
-
{
|
|
176
|
+
{unavailableHint && (
|
|
170
177
|
// biome-ignore lint/a11y/useSemanticElements: <p> with role="status" preserves block layout; <output> is inline
|
|
171
178
|
<p
|
|
172
179
|
role="status"
|