Skip to content

Commit 8b8df5f

Browse files
refactor(mosaic): pass confirmation disabled state directly
1 parent ae30b88 commit 8b8df5f

10 files changed

Lines changed: 31 additions & 14 deletions

File tree

‎packages/mosaic/docs/controllers.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ Opening a dialog does not acquire the lock. The lock owns no errors or row IDs.
7171
`usePendingAction` keeps its inline errors and boolean result; refused runs
7272
return `false` without changing metadata. `useConfirmController` keeps removal
7373
errors in its dialog; refusal leaves the dialog and its error unchanged.
74-
Its `canConfirm` projection disables only the confirmation submit control.
75-
Admission still runs through the lock, not the rendered projection. Without an
74+
Pass the section's pending state to `Confirmation.disabled` to disable its submit
75+
control while another action runs. Admission still runs through the lock. Without an
7676
injected lock, each owner retains its existing independent behavior.
7777

7878
`actionLock` is a hook option, not a per-run option. Project its `isPending` to

‎packages/mosaic/src/blocks/confirm/confirm-shell.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export interface ConfirmStepProps<Payload> {
3434
actionLabel: string;
3535
cancelLabel: string;
3636
isPending: boolean;
37-
canConfirm: boolean;
3837
errorMessage: string | undefined;
3938
confirm: () => void;
4039
}
@@ -69,7 +68,6 @@ function ConfirmCard<Payload>({
6968
actionLabel: resolve(actionLabel, payload),
7069
cancelLabel,
7170
isPending: controller.isPending,
72-
canConfirm: controller.canConfirm,
7371
errorMessage: controller.errorMessage,
7472
confirm: () => controller.confirm(payload),
7573
});

‎packages/mosaic/src/blocks/confirm/confirm.controller.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ export interface ConfirmController<Payload = void> {
7474
isPending: boolean;
7575
/** Whether the dialog can close right now. False while the action is past the point of cancelling */
7676
canClose: boolean;
77-
canConfirm: boolean;
7877
errorMessage: string | undefined;
7978
/** Which card the dialog shows: the confirm step, or the reverification the action was interrupted by. */
8079
step: ConfirmStep;
@@ -124,7 +123,6 @@ export function useConfirmController<Payload = void>({
124123
isOpen: snapshot.value !== 'idle',
125124
isPending,
126125
canClose,
127-
canConfirm: snapshot.value === 'confirming' && !actionLock?.isPending,
128126
errorMessage: error ? errorText(error, errorFallback) : undefined,
129127
step: isPending && reverification?.visible ? 'verify' : 'confirm',
130128
reverification,

‎packages/mosaic/src/blocks/confirmation/__tests__/confirmation.feature.test.tsx‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ function Host<Payload>({
3838

3939
async function renderAccount(
4040
options: ConfirmControllerOptions<void>,
41-
{ localization, description }: { localization?: MosaicLocalization; description?: ReactNode } = {},
41+
{
42+
localization,
43+
description,
44+
disabled,
45+
}: { localization?: MosaicLocalization; description?: ReactNode; disabled?: boolean } = {},
4246
) {
4347
const ref: { current: ConfirmController | undefined } = { current: undefined };
4448
serveFapi({ client: fapiClient([session]) });
@@ -53,6 +57,7 @@ async function renderAccount(
5357
title={TITLE}
5458
description={description ?? 'Google will be removed from this account.'}
5559
actionLabel='Remove'
60+
disabled={disabled}
5661
/>
5762
);
5863
}}
@@ -70,6 +75,19 @@ async function renderAccount(
7075
const confirmButton = () => screen.getByRole('button', { name: 'Remove' });
7176

7277
describe('Confirmation', () => {
78+
it('disables confirmation while leaving Cancel usable', async () => {
79+
const onConfirm = vi.fn(() => Promise.resolve());
80+
const { user, open, closed } = await renderAccount({ onConfirm }, { disabled: true });
81+
await open();
82+
83+
expect(confirmButton()).toBeDisabled();
84+
await user.click(confirmButton());
85+
expect(onConfirm).not.toHaveBeenCalled();
86+
87+
await user.click(screen.getByRole('button', { name: 'Cancel' }));
88+
await closed();
89+
});
90+
7391
it('opens from its trigger, confirms, closes once the action lands, and hands focus back', async () => {
7492
const run = deferred<void>();
7593
const onConfirm = vi.fn(() => run.promise);

‎packages/mosaic/src/blocks/confirmation/confirmation.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type ConfirmationColor = 'negative' | 'primary';
1010
export interface ConfirmationProps<Payload = void> extends ConfirmBlockProps<Payload> {
1111
/** The weight the confirming button carries. An action that can be undone takes `primary` (default: `negative`) */
1212
color?: ConfirmationColor;
13+
disabled?: boolean;
1314
}
1415

1516
/**
@@ -36,10 +37,10 @@ export interface ConfirmationProps<Payload = void> extends ConfirmBlockProps<Pay
3637
* actionLabel='Remove'
3738
* />
3839
*/
39-
export function Confirmation<Payload = void>({ color = 'negative', ...props }: ConfirmationProps<Payload>) {
40+
export function Confirmation<Payload = void>({ color = 'negative', disabled, ...props }: ConfirmationProps<Payload>) {
4041
return (
4142
<ConfirmShell {...props}>
42-
{({ title, description, actionLabel, cancelLabel, isPending, canConfirm, errorMessage, confirm }) => (
43+
{({ title, description, actionLabel, cancelLabel, isPending, errorMessage, confirm }) => (
4344
<>
4445
<Card.Header>
4546
<Card.Title>{title}</Card.Title>
@@ -67,7 +68,7 @@ export function Confirmation<Payload = void>({ color = 'negative', ...props }: C
6768
fullWidth
6869
color={color}
6970
isPending={isPending}
70-
disabled={!canConfirm && !isPending}
71+
disabled={disabled && !isPending}
7172
onClick={confirm}
7273
>
7374
{actionLabel}

‎packages/mosaic/src/blocks/destructive/destructive.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ function DestructiveStep<Payload>({
3232
actionLabel,
3333
cancelLabel,
3434
isPending,
35-
canConfirm,
3635
errorMessage,
3736
confirm,
3837
fieldLabel,
@@ -99,7 +98,7 @@ function DestructiveStep<Payload>({
9998
fullWidth
10099
color='negative'
101100
isPending={isPending}
102-
disabled={!isConfirmed || !canConfirm}
101+
disabled={!isConfirmed}
103102
focusableWhenDisabled
104103
>
105104
{actionLabel}

‎packages/mosaic/src/features/user-profile/user-profile-email-section/user-profile-email-section.view.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function UserProfileEmailSectionView({
7979
{remove ? (
8080
<Confirmation
8181
controller={remove}
82+
disabled={isDisabled}
8283
title={m.email.removeDialog.title}
8384
description={email =>
8485
fill(email.isVerified ? m.email.removeDialog.verifiedDescription : m.email.removeDialog.description, {

‎packages/mosaic/src/features/user-profile/user-profile-phone-section/user-profile-phone-section.view.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export function UserProfilePhoneSectionView({
8181
{remove ? (
8282
<Confirmation
8383
controller={remove}
84+
disabled={isDisabled}
8485
title={m.phone.removeDialog.title}
8586
description={phone =>
8687
fill(phone.isVerified ? m.phone.removeDialog.verifiedDescription : m.phone.removeDialog.description, {

‎packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.view.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export function UserProfileWeb3WalletsSectionView({
7676
{remove ? (
7777
<Confirmation
7878
controller={remove}
79+
disabled={isBusy}
7980
title={m.removeDialog.title}
8081
description={wallet =>
8182
fill(wallet.isVerified ? m.removeDialog.verifiedDescription : m.removeDialog.description, {

‎packages/mosaic/src/hooks/__tests__/use-action-lock.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ describe('shared action admission', () => {
3434
expect(onConfirm).not.toHaveBeenCalled();
3535
expect(result.current.remove.isOpen).toBe(true);
3636
expect(result.current.remove.isPending).toBe(false);
37-
expect(result.current.remove.canConfirm).toBe(false);
37+
expect(result.current.actionLock.isPending).toBe(true);
3838
await act(() => pending.resolve());
39-
expect(result.current.remove.canConfirm).toBe(true);
39+
expect(result.current.actionLock.isPending).toBe(false);
4040
await act(() => result.current.remove.confirm());
4141
expect(onConfirm).toHaveBeenCalledOnce();
4242
expect(result.current.remove.isOpen).toBe(false);

0 commit comments

Comments
 (0)