@remit/ui 0.0.32 → 0.0.33
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
|
@@ -37,6 +37,32 @@ describe("ComfortableRow", () => {
|
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
describe("ComfortableRow selection slot", () => {
|
|
41
|
+
it("renders no checkbox without a selection (non-selectable mode)", () => {
|
|
42
|
+
const html = renderToString(
|
|
43
|
+
createElement(ComfortableRow, { thread: { ...base, isRead: true } }),
|
|
44
|
+
);
|
|
45
|
+
assert.doesNotMatch(html, /Select message/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders a checkbox reflecting the checked state", () => {
|
|
49
|
+
const unchecked = renderToString(
|
|
50
|
+
createElement(ComfortableRow, {
|
|
51
|
+
thread: { ...base, isRead: true },
|
|
52
|
+
selection: { checked: false, onToggle: () => undefined },
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
const checked = renderToString(
|
|
56
|
+
createElement(ComfortableRow, {
|
|
57
|
+
thread: { ...base, isRead: true },
|
|
58
|
+
selection: { checked: true, onToggle: () => undefined },
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
assert.match(unchecked, /Select message/);
|
|
62
|
+
assert.match(checked, /Deselect message/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
40
66
|
describe("CompactRow", () => {
|
|
41
67
|
it("renders fromName and subject", () => {
|
|
42
68
|
const html = renderToString(
|
|
@@ -112,10 +112,40 @@ export const Compact: Story = {
|
|
|
112
112
|
export const States: Story = {
|
|
113
113
|
render: () => (
|
|
114
114
|
<List>
|
|
115
|
-
<ComfortableRow thread={unread} />
|
|
116
|
-
<ComfortableRow thread={read} />
|
|
115
|
+
<ComfortableRow thread={unread} active />
|
|
116
|
+
<ComfortableRow thread={read} focused />
|
|
117
117
|
<ComfortableRow thread={starred} />
|
|
118
118
|
<ComfortableRow thread={suspicious} />
|
|
119
119
|
</List>
|
|
120
120
|
),
|
|
121
121
|
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Selectable rows. The checkbox layers over the avatar: hidden until hover
|
|
125
|
+
* while unchecked, pinned visible once checked or while the list is in
|
|
126
|
+
* multi-select mode. A row rendered without `selection` — the brief and
|
|
127
|
+
* Flagged before they gained selection — shows the avatar alone.
|
|
128
|
+
*/
|
|
129
|
+
export const Selectable: Story = {
|
|
130
|
+
render: () => (
|
|
131
|
+
<List>
|
|
132
|
+
<ComfortableRow
|
|
133
|
+
thread={unread}
|
|
134
|
+
selection={{ checked: true, onToggle: () => undefined }}
|
|
135
|
+
/>
|
|
136
|
+
<ComfortableRow
|
|
137
|
+
thread={read}
|
|
138
|
+
selection={{ checked: false, onToggle: () => undefined }}
|
|
139
|
+
/>
|
|
140
|
+
<ComfortableRow
|
|
141
|
+
thread={starred}
|
|
142
|
+
selection={{
|
|
143
|
+
checked: false,
|
|
144
|
+
alwaysVisible: true,
|
|
145
|
+
onToggle: () => undefined,
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
<ComfortableRow thread={withCategory} />
|
|
149
|
+
</List>
|
|
150
|
+
),
|
|
151
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Paperclip, ShieldAlert, Star } from "lucide-react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
1
|
+
import { Check, Paperclip, ShieldAlert, Star } from "lucide-react";
|
|
2
|
+
import type { ComponentType, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn.js";
|
|
4
4
|
import { LIST_ROW_ATTRIBUTE } from "../lib/roving-focus.js";
|
|
5
5
|
import { categoryTone, type ThreadRowData } from "./app-shell-types.js";
|
|
@@ -173,18 +173,89 @@ export function ComfortableRowTextContent({
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
176
|
+
* Checkbox state for a selectable row. Absent on a row that cannot be
|
|
177
|
+
* multi-selected (the brief and Flagged before they gained selection), which
|
|
178
|
+
* renders the avatar alone.
|
|
178
179
|
*/
|
|
179
|
-
export
|
|
180
|
+
export interface RowSelection {
|
|
181
|
+
checked: boolean;
|
|
182
|
+
/** Keep the checkbox visible instead of revealing it on hover (mobile). */
|
|
183
|
+
alwaysVisible?: boolean;
|
|
184
|
+
onToggle: (event: React.MouseEvent) => void;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Leading slot of a comfortable row: the avatar, with a checkbox layered over
|
|
189
|
+
* it when the row is selectable. Fixed 28px so the row never reflows as the
|
|
190
|
+
* checkbox appears.
|
|
191
|
+
*/
|
|
192
|
+
export function ComfortableRowLeading({
|
|
193
|
+
thread,
|
|
194
|
+
selection,
|
|
195
|
+
}: {
|
|
196
|
+
thread: ThreadRowData;
|
|
197
|
+
selection?: RowSelection;
|
|
198
|
+
}) {
|
|
199
|
+
if (!selection) {
|
|
200
|
+
return <Avatar name={thread.fromName} email={thread.fromEmail} size="sm" />;
|
|
201
|
+
}
|
|
202
|
+
const { checked, alwaysVisible } = selection;
|
|
203
|
+
return (
|
|
204
|
+
<span className="relative size-7 shrink-0">
|
|
205
|
+
<Avatar
|
|
206
|
+
name={thread.fromName}
|
|
207
|
+
email={thread.fromEmail}
|
|
208
|
+
size="sm"
|
|
209
|
+
className={cn(
|
|
210
|
+
"absolute inset-0 transition-opacity sm:group-hover:opacity-0",
|
|
211
|
+
(checked || alwaysVisible) && "opacity-0",
|
|
212
|
+
)}
|
|
213
|
+
/>
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
// Out of the tab order: the row is the list's single tab stop, and this
|
|
217
|
+
// control is `opacity-0` until hover, so a tabbable one would put focus
|
|
218
|
+
// on something invisible on every row.
|
|
219
|
+
tabIndex={-1}
|
|
220
|
+
onClick={selection.onToggle}
|
|
221
|
+
className={cn(
|
|
222
|
+
"absolute inset-0 size-7 items-center justify-center rounded-full border transition-opacity",
|
|
223
|
+
alwaysVisible ? "flex" : "hidden sm:flex",
|
|
224
|
+
checked
|
|
225
|
+
? "bg-accent border-accent text-accent-fg opacity-100"
|
|
226
|
+
: alwaysVisible
|
|
227
|
+
? "border-fg-subtle/40 bg-canvas opacity-100"
|
|
228
|
+
: "border-fg-subtle/40 bg-canvas opacity-0 group-hover:opacity-100",
|
|
229
|
+
)}
|
|
230
|
+
aria-label={checked ? "Deselect message" : "Select message"}
|
|
231
|
+
>
|
|
232
|
+
{checked && <Check className="size-3" />}
|
|
233
|
+
</button>
|
|
234
|
+
</span>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Full inner body of a comfortable row (unread dot + leading slot + text
|
|
240
|
+
* content). Place inside any wrapper element that uses `comfortableRowClass()`.
|
|
241
|
+
*/
|
|
242
|
+
export function ComfortableRowBody({
|
|
243
|
+
thread,
|
|
244
|
+
selection,
|
|
245
|
+
badge,
|
|
246
|
+
}: {
|
|
247
|
+
thread: ThreadRowData;
|
|
248
|
+
selection?: RowSelection;
|
|
249
|
+
badge?: ReactNode;
|
|
250
|
+
}) {
|
|
180
251
|
const unread = !thread.isRead;
|
|
181
252
|
return (
|
|
182
253
|
<>
|
|
183
254
|
{unread && (
|
|
184
255
|
<span className="absolute left-1.5 top-1/2 size-1.5 -translate-y-1/2 rounded-full bg-accent" />
|
|
185
256
|
)}
|
|
186
|
-
<
|
|
187
|
-
<ComfortableRowTextContent thread={thread} />
|
|
257
|
+
<ComfortableRowLeading thread={thread} selection={selection} />
|
|
258
|
+
<ComfortableRowTextContent thread={thread} badge={badge} />
|
|
188
259
|
</>
|
|
189
260
|
);
|
|
190
261
|
}
|
|
@@ -192,10 +263,12 @@ export function ComfortableRowBody({ thread }: { thread: ThreadRowData }) {
|
|
|
192
263
|
export function CompactRow({
|
|
193
264
|
thread,
|
|
194
265
|
active,
|
|
266
|
+
focused,
|
|
195
267
|
onClick,
|
|
196
268
|
}: {
|
|
197
269
|
thread: ThreadRowData;
|
|
198
270
|
active?: boolean;
|
|
271
|
+
focused?: boolean;
|
|
199
272
|
onClick?: () => void;
|
|
200
273
|
}) {
|
|
201
274
|
return (
|
|
@@ -203,7 +276,7 @@ export function CompactRow({
|
|
|
203
276
|
type="button"
|
|
204
277
|
{...LIST_ROW_ATTRIBUTE}
|
|
205
278
|
onClick={onClick}
|
|
206
|
-
className={compactRowClass({ active })}
|
|
279
|
+
className={compactRowClass({ active, focused })}
|
|
207
280
|
>
|
|
208
281
|
<CompactRowBody thread={thread} />
|
|
209
282
|
</button>
|
|
@@ -213,10 +286,14 @@ export function CompactRow({
|
|
|
213
286
|
export function ComfortableRow({
|
|
214
287
|
thread,
|
|
215
288
|
active,
|
|
289
|
+
focused,
|
|
290
|
+
selection,
|
|
216
291
|
onClick,
|
|
217
292
|
}: {
|
|
218
293
|
thread: ThreadRowData;
|
|
219
294
|
active?: boolean;
|
|
295
|
+
focused?: boolean;
|
|
296
|
+
selection?: RowSelection;
|
|
220
297
|
onClick?: () => void;
|
|
221
298
|
}) {
|
|
222
299
|
return (
|
|
@@ -224,17 +301,17 @@ export function ComfortableRow({
|
|
|
224
301
|
type="button"
|
|
225
302
|
{...LIST_ROW_ATTRIBUTE}
|
|
226
303
|
onClick={onClick}
|
|
227
|
-
className={comfortableRowClass({ active })}
|
|
304
|
+
className={cn("group", comfortableRowClass({ active, focused }))}
|
|
228
305
|
>
|
|
229
|
-
<ComfortableRowBody thread={thread} />
|
|
306
|
+
<ComfortableRowBody thread={thread} selection={selection} />
|
|
230
307
|
</button>
|
|
231
308
|
);
|
|
232
309
|
}
|
|
233
310
|
|
|
234
311
|
/** A row renderer the brief drives — Comfortable/Compact rows or a consumer's
|
|
235
312
|
* own (e.g. the web client's navigation-aware row) all satisfy this shape. */
|
|
236
|
-
export type BriefRowComponent =
|
|
313
|
+
export type BriefRowComponent = ComponentType<{
|
|
237
314
|
thread: ThreadRowData;
|
|
238
315
|
active?: boolean;
|
|
239
316
|
onClick?: () => void;
|
|
240
|
-
}
|
|
317
|
+
}>;
|
package/src/index.ts
CHANGED
|
@@ -203,11 +203,13 @@ export {
|
|
|
203
203
|
type BriefRowComponent,
|
|
204
204
|
ComfortableRow,
|
|
205
205
|
ComfortableRowBody,
|
|
206
|
+
ComfortableRowLeading,
|
|
206
207
|
ComfortableRowTextContent,
|
|
207
208
|
CompactRow,
|
|
208
209
|
CompactRowBody,
|
|
209
210
|
comfortableRowClass,
|
|
210
211
|
compactRowClass,
|
|
212
|
+
type RowSelection,
|
|
211
213
|
} from "./components/message-row.js";
|
|
212
214
|
export {
|
|
213
215
|
type MobileMessageAction,
|