@@ -3,16 +3,18 @@ import { useEffect, useId, useState } from 'react';
33
44import { Button , SubmitButton } from '../../components/button' ;
55import { Card } from '../../components/card' ;
6- import type { DialogTriggerProps } from '../../components/dialog' ;
6+ import type { DialogRootProps , DialogTriggerProps } from '../../components/dialog' ;
77import { Dialog } from '../../components/dialog' ;
88import { Field } from '../../components/field' ;
9+ import { Flow } from '../../components/flow' ;
910import { Input } from '../../components/input' ;
11+ import { Reverification , type ReverificationProps } from '../../features/reverification' ;
1012
1113export interface DestructiveProps {
1214 /** Whether the dialog is open */
1315 open : boolean ;
1416 /** Callback when open state changes */
15- onOpenChange : ( open : boolean ) => void ;
17+ onOpenChange : NonNullable < DialogRootProps [ 'onOpenChange' ] > ;
1618 /** Element that opens the dialog */
1719 trigger ?: DialogTriggerProps [ 'render' ] ;
1820 /** Dialog heading */
@@ -33,6 +35,7 @@ export interface DestructiveProps {
3335 isDeleting ?: boolean ;
3436 /** Error message to display if the delete action fails */
3537 errorMessage ?: string ;
38+ reverification ?: ReverificationProps ;
3639}
3740
3841/**
@@ -70,6 +73,7 @@ export function Destructive({
7073 onDelete,
7174 isDeleting = false ,
7275 errorMessage,
76+ reverification,
7377} : DestructiveProps ) {
7478 const formId = useId ( ) ;
7579 const [ typedValue , setTypedValue ] = useState ( '' ) ;
@@ -83,6 +87,7 @@ export function Destructive({
8387 } , [ open ] ) ;
8488
8589 const isConfirmed = typedValue === confirmationValue ;
90+ const step = reverification && reverification . phase !== 'inactive' ? 'verify' : 'confirm' ;
8691
8792 // The action sits in the footer, outside the form, so `form={formId}` associates the two.
8893 // That is what makes Enter in the field submit. Both guards are re-checked here because
@@ -95,65 +100,96 @@ export function Destructive({
95100 }
96101 } ;
97102
103+ const handleOpenChange : NonNullable < DialogRootProps [ 'onOpenChange' ] > = ( ...args ) => {
104+ const [ nextOpen ] = args ;
105+ if ( ! nextOpen && reverification ?. phase === 'active' ) {
106+ reverification . cancel ( ) ;
107+ }
108+ onOpenChange ( ...args ) ;
109+ } ;
110+
111+ const confirmation = (
112+ < >
113+ < Card . Header >
114+ < Card . Title > { title } </ Card . Title >
115+ < Card . Description > { description } </ Card . Description >
116+ </ Card . Header >
117+ < Card . Content >
118+ < form
119+ id = { formId }
120+ onSubmit = { handleSubmit }
121+ >
122+ < Field . Root invalid = { Boolean ( errorMessage ) } >
123+ < Field . Label > { fieldLabel } </ Field . Label >
124+ < Input
125+ // Not a credential, so 1Password is told to leave it alone rather than
126+ // cover it with an autofill overlay.
127+ data-1p-ignore
128+ placeholder = { confirmationValue }
129+ value = { typedValue }
130+ disabled = { isDeleting }
131+ onChange = { event => setTypedValue ( event . target . value ) }
132+ />
133+ < Field . Message >
134+ < Field . Error > { errorMessage } </ Field . Error >
135+ </ Field . Message >
136+ </ Field . Root >
137+ </ form >
138+ </ Card . Content >
139+ < Card . Footer >
140+ < Dialog . Close
141+ render = {
142+ < Button
143+ variant = 'outline'
144+ fullWidth
145+ >
146+ { cancelLabel }
147+ </ Button >
148+ }
149+ />
150+ < SubmitButton
151+ form = { formId }
152+ fullWidth
153+ color = 'negative'
154+ isPending = { isDeleting }
155+ disabled = { ! isConfirmed }
156+ focusableWhenDisabled
157+ >
158+ { actionLabel }
159+ </ SubmitButton >
160+ </ Card . Footer >
161+ </ >
162+ ) ;
163+
164+ const content = reverification ? (
165+ < Flow . Root
166+ value = { step }
167+ direction = { step === 'verify' ? 1 : - 1 }
168+ state = { step }
169+ >
170+ { ( ) => (
171+ < >
172+ < Flow . Step ids = { [ 'confirm' ] } > { confirmation } </ Flow . Step >
173+ < Flow . Step ids = { [ 'verify' ] } > { reverification ? < Reverification { ...reverification } /> : null } </ Flow . Step >
174+ </ >
175+ ) }
176+ </ Flow . Root >
177+ ) : (
178+ confirmation
179+ ) ;
180+
98181 return (
99182 < Dialog . Root
100183 open = { open }
101- onOpenChange = { onOpenChange }
184+ onOpenChange = { handleOpenChange }
102185 >
103186 { trigger ? < Dialog . Trigger render = { trigger } /> : null }
104187 < Dialog . Popup variant = 'card' >
105188 < Card . Root
106189 elevation = 'overlay'
107190 renderBranding = { false }
108191 >
109- < Card . Header >
110- < Card . Title > { title } </ Card . Title >
111- < Card . Description > { description } </ Card . Description >
112- </ Card . Header >
113- < Card . Content >
114- < form
115- id = { formId }
116- onSubmit = { handleSubmit }
117- >
118- < Field . Root invalid = { Boolean ( errorMessage ) } >
119- < Field . Label > { fieldLabel } </ Field . Label >
120- < Input
121- // Not a credential, so 1Password is told to leave it alone rather than
122- // cover it with an autofill overlay.
123- data-1p-ignore
124- placeholder = { confirmationValue }
125- value = { typedValue }
126- disabled = { isDeleting }
127- onChange = { event => setTypedValue ( event . target . value ) }
128- />
129- < Field . Message >
130- < Field . Error > { errorMessage } </ Field . Error >
131- </ Field . Message >
132- </ Field . Root >
133- </ form >
134- </ Card . Content >
135- < Card . Footer >
136- < Dialog . Close
137- render = {
138- < Button
139- variant = 'outline'
140- fullWidth
141- >
142- { cancelLabel }
143- </ Button >
144- }
145- />
146- < SubmitButton
147- form = { formId }
148- fullWidth
149- color = 'negative'
150- isPending = { isDeleting }
151- disabled = { ! isConfirmed }
152- focusableWhenDisabled
153- >
154- { actionLabel }
155- </ SubmitButton >
156- </ Card . Footer >
192+ { content }
157193 </ Card . Root >
158194 </ Dialog . Popup >
159195 </ Dialog . Root >
0 commit comments