Merge branch 'feat/v144-settings' into release/v1.44.0

# Conflicts:
#	src/components/settings/privacy-section.tsx
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 20:51:39 +02:00
commit e1b70537cc
129 files changed
+5138 -3169

No files matched your search

@@ -241,3 +241,35 @@ describe("<ConfirmDialog> — the trigger-less half", () => {
expect(carriers.map(nameOf)).toEqual(["ConfirmBody"]);
});
});
describe("ConfirmDialog with holdOpen — the request stays on screen", () => {
const onConfirm = vi.fn();
const body = walk(
ConfirmDialog({
open: true,
onOpenChange: () => {},
onConfirm,
pending: true,
holdOpen: true,
...COPY,
}) as ReactElement,
).find((el) => nameOf(el) === "ConfirmBody")!;
const rendered = walk(
(body.type as (p: unknown) => ReactElement)(body.props),
);
it("keeps the dialog open on confirm and still calls back", () => {
const action = rendered.find((el) => nameOf(el) === "AlertDialogAction")!;
const preventDefault = vi.fn();
(action.props as { onClick: (e: unknown) => void }).onClick({
preventDefault,
});
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(onConfirm).toHaveBeenCalledTimes(1);
});
it("holds Cancel while the request runs", () => {
const cancel = rendered.find((el) => nameOf(el) === "AlertDialogCancel")!;
expect((cancel.props as { disabled?: boolean }).disabled).toBe(true);
});
});
+35 -3
View File
@@ -52,6 +52,12 @@ interface ConfirmCopy {
pending?: boolean;
/** `data-slot` for tests and the design guards. */
slot?: string;
/**
* `data-testid` for the trigger; the dialog's action carries
* `<testId>-confirm`. For call sites whose tests already address the
* control by test id.
*/
testId?: string;
/** A control that belongs with the question, such as an opt-out checkbox. */
extra?: ReactNode;
/**
@@ -59,6 +65,16 @@ interface ConfirmCopy {
* data-list selection bar's confirm already does.
*/
destructive?: boolean;
/**
* The confirm keeps the dialog open: the caller closes it once the act
* settled, and Cancel waits while `pending`. For an act whose in-flight
* state must stay on screen (an irreversible delete), where a dismissal
* mid-request would read as cancelled. Only meaningful on `ConfirmDialog`,
* whose caller owns `open`.
*/
holdOpen?: boolean;
/** The confirm stays disabled, e.g. until a field in `extra` is valid. */
confirmDisabled?: boolean;
}
function ConfirmBody({
@@ -68,8 +84,11 @@ function ConfirmBody({
onConfirm,
pending = false,
slot,
testId,
extra,
destructive = false,
holdOpen = false,
confirmDisabled = false,
}: ConfirmCopy) {
const { t } = useTranslations();
@@ -81,13 +100,23 @@ function ConfirmBody({
</AlertDialogHeader>
{extra}
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogCancel disabled={holdOpen && pending}>
{t("common.cancel")}
</AlertDialogCancel>
<AlertDialogAction
variant={destructive ? "destructive" : "default"}
data-slot={slot ? `${slot}-confirm` : undefined}
disabled={pending}
data-testid={testId ? `${testId}-confirm` : undefined}
disabled={pending || confirmDisabled}
aria-busy={pending || undefined}
onClick={onConfirm}
onClick={
holdOpen
? (event) => {
event.preventDefault();
onConfirm();
}
: onConfirm
}
>
{pending && (
<Loader2 className="mr-1 size-3.5 animate-spin motion-reduce:animate-none" />
@@ -113,6 +142,7 @@ export function ConfirmButton({
icon,
ariaLabel,
slot,
testId,
extra,
destructive = false,
}: ConfirmCopy & {
@@ -136,6 +166,7 @@ export function ConfirmButton({
className={className}
aria-label={ariaLabel}
data-slot={slot}
data-testid={testId}
disabled={disabled || pending}
>
{pending ? (
@@ -156,6 +187,7 @@ export function ConfirmButton({
onConfirm={onConfirm}
pending={pending}
slot={slot}
testId={testId}
extra={extra}
destructive={destructive}
/>