@remit/web-client 0.0.309 → 0.0.310

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.309",
3
+ "version": "0.0.310",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -327,57 +327,67 @@ const WiredComposeHeader = ({
327
327
  // the keyboard down with it and started the cycle over.
328
328
  const [expandedFor, setExpandedFor] = useState<number | undefined>(undefined);
329
329
  const expanded = expandedFor === documentGeneration;
330
+ const [focusWithin, setFocusWithin] = useState(false);
330
331
 
331
332
  return (
332
- <ComposeHeader
333
- collapsed={!isDesktop && isKeyboardOpen && !expanded}
334
- onExpand={() => setExpandedFor(documentGeneration)}
335
- summary={composeHeaderSummary({
336
- to: toAddresses,
337
- cc: ccAddresses,
338
- bcc: bccAddresses,
339
- subject,
340
- })}
341
- from={
342
- <FromSelector
343
- selectedAccountId={selectedAccountId}
344
- onSelect={onAccountChange}
345
- />
346
- }
347
- to={
348
- <AddressField
349
- label="To"
350
- addresses={toAddresses}
351
- onChange={setToAddresses}
352
- placeholder="Recipients"
353
- onPendingChange={onToPendingChange}
354
- ref={fieldHandles.to}
355
- />
356
- }
357
- cc={
358
- showCc ? (
359
- <AddressField
360
- label="Cc"
361
- addresses={ccAddresses}
362
- onChange={setCcAddresses}
363
- ref={fieldHandles.cc}
333
+ <div
334
+ className="contents"
335
+ onFocusCapture={() => setFocusWithin(true)}
336
+ onBlurCapture={(event) => {
337
+ if (event.currentTarget.contains(event.relatedTarget)) return;
338
+ setFocusWithin(false);
339
+ }}
340
+ >
341
+ <ComposeHeader
342
+ collapsed={!isDesktop && isKeyboardOpen && !expanded && !focusWithin}
343
+ onExpand={() => setExpandedFor(documentGeneration)}
344
+ summary={composeHeaderSummary({
345
+ to: toAddresses,
346
+ cc: ccAddresses,
347
+ bcc: bccAddresses,
348
+ subject,
349
+ })}
350
+ from={
351
+ <FromSelector
352
+ selectedAccountId={selectedAccountId}
353
+ onSelect={onAccountChange}
364
354
  />
365
- ) : undefined
366
- }
367
- bcc={
368
- showBcc ? (
355
+ }
356
+ to={
369
357
  <AddressField
370
- label="Bcc"
371
- addresses={bccAddresses}
372
- onChange={setBccAddresses}
373
- ref={fieldHandles.bcc}
358
+ label="To"
359
+ addresses={toAddresses}
360
+ onChange={setToAddresses}
361
+ placeholder="Recipients"
362
+ onPendingChange={onToPendingChange}
363
+ ref={fieldHandles.to}
374
364
  />
375
- ) : undefined
376
- }
377
- subject={<ComposeSubjectField value={subject} onChange={setSubject} />}
378
- onShowCc={() => setShowCc(true)}
379
- onShowBcc={() => setShowBcc(true)}
380
- />
365
+ }
366
+ cc={
367
+ showCc ? (
368
+ <AddressField
369
+ label="Cc"
370
+ addresses={ccAddresses}
371
+ onChange={setCcAddresses}
372
+ ref={fieldHandles.cc}
373
+ />
374
+ ) : undefined
375
+ }
376
+ bcc={
377
+ showBcc ? (
378
+ <AddressField
379
+ label="Bcc"
380
+ addresses={bccAddresses}
381
+ onChange={setBccAddresses}
382
+ ref={fieldHandles.bcc}
383
+ />
384
+ ) : undefined
385
+ }
386
+ subject={<ComposeSubjectField value={subject} onChange={setSubject} />}
387
+ onShowCc={() => setShowCc(true)}
388
+ onShowBcc={() => setShowBcc(true)}
389
+ />
390
+ </div>
381
391
  );
382
392
  };
383
393
 
@@ -14,7 +14,10 @@
14
14
 
15
15
  import assert from "node:assert/strict";
16
16
  import { afterEach, describe, it } from "node:test";
17
- import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
17
+ import type {
18
+ RemitImapAccountResponse,
19
+ RemitImapDescribeMessageResponse,
20
+ } from "@remit/api-http-client/types.gen.ts";
18
21
  import {
19
22
  type AnyRouter,
20
23
  createMemoryHistory,
@@ -44,6 +47,20 @@ const account = {
44
47
  smtpEnabled: true,
45
48
  } as unknown as RemitImapAccountResponse;
46
49
 
50
+ const sourceMessage = {
51
+ message: { messageId: "msg-1" },
52
+ envelope: {
53
+ subject: "Lunch",
54
+ messageIdValue: "<m1@example.com>",
55
+ from: [{ normalizedEmail: "them@example.com", displayName: "Them" }],
56
+ replyTo: [],
57
+ to: [],
58
+ cc: [],
59
+ },
60
+ references: [],
61
+ bodyParts: [],
62
+ } as unknown as RemitImapDescribeMessageResponse;
63
+
47
64
  interface VisualViewportStub {
48
65
  height: number;
49
66
  addEventListener: (type: string, listener: () => void) => void;
@@ -97,15 +114,22 @@ const testRouter = (): AnyRouter =>
97
114
  history: createMemoryHistory({ initialEntries: ["/mail/mbx-1"] }),
98
115
  }) as unknown as AnyRouter;
99
116
 
100
- const Opened = () =>
117
+ const Opened = ({ mode }: { mode: "new" | "reply" }) =>
101
118
  createElement(ComposeForm, {
102
- mode: "new",
119
+ mode,
103
120
  account,
121
+ sourceMessage: mode === "reply" ? sourceMessage : undefined,
104
122
  onDraftCreated: () => {},
105
123
  onClose: () => {},
106
124
  });
107
125
 
108
- const mount = async (): Promise<void> => {
126
+ const mount = async ({
127
+ mode = "new",
128
+ keyboardOpen = true,
129
+ }: {
130
+ mode?: "new" | "reply";
131
+ keyboardOpen?: boolean;
132
+ } = {}): Promise<void> => {
109
133
  http = mockFetch(async (call) => {
110
134
  if (call.path.endsWith("/config")) return { accounts: [account] };
111
135
  return { items: [] };
@@ -115,7 +139,9 @@ const mount = async (): Promise<void> => {
115
139
  configurable: true,
116
140
  value: viewport,
117
141
  });
118
- viewport.height = globalThis.window.innerHeight - KEYBOARD_HEIGHT;
142
+ viewport.height = keyboardOpen
143
+ ? globalThis.window.innerHeight - KEYBOARD_HEIGHT
144
+ : globalThis.window.innerHeight;
119
145
 
120
146
  harness = createDomHarness(PHONE);
121
147
  harness.renderApp(
@@ -125,7 +151,7 @@ const mount = async (): Promise<void> => {
125
151
  children: createElement(
126
152
  ComposeProvider,
127
153
  null,
128
- createElement(Opened, null),
154
+ createElement(Opened, { mode }),
129
155
  ),
130
156
  }),
131
157
  );
@@ -171,4 +197,33 @@ describe("the phone compose header and the software keyboard", () => {
171
197
  "the header did not collapse over the field being typed into",
172
198
  );
173
199
  });
200
+
201
+ it("keeps To mounted when a reply brings the keyboard up over it", async () => {
202
+ await mount({ mode: "reply", keyboardOpen: false });
203
+
204
+ const input = recipientInput();
205
+ assert.ok(input, "the rows are there while the keyboard is down");
206
+
207
+ act(() => {
208
+ input.focus();
209
+ });
210
+ setKeyboard(true);
211
+ await harness?.flush();
212
+
213
+ assert.equal(
214
+ recipientInput(),
215
+ input,
216
+ "the recipient field stayed mounted under the keyboard",
217
+ );
218
+ assert.equal(
219
+ globalThis.document.activeElement,
220
+ input,
221
+ "the recipient field kept focus",
222
+ );
223
+ assert.equal(
224
+ collapsedBar(),
225
+ null,
226
+ "the header did not collapse over the field being typed into",
227
+ );
228
+ });
174
229
  });