@@ -18,6 +18,8 @@ type DsarVerifyResult =
1818
1919type RequestDataResult = { message : string }
2020
21+ type RequestToastTone = 'success' | 'error' | 'info'
22+
2123const requestPreviewStates = [ 'loading' , 'success' , 'error' , 'validation' ] as const
2224
2325const requestPreviewQueryParams : Record < RequestFormType , string > = {
@@ -67,6 +69,13 @@ export class PrivacyFormElement extends LitElement {
6769 private deleteConfirmCheckbox ! : HTMLInputElement
6870 private deleteMessage ! : HTMLElement
6971
72+ private getPreviewToastElements ( requestType : RequestType ) : HTMLElement [ ] {
73+ const prefix = requestType === 'ACCESS' ? 'access' : 'delete'
74+ return [ 'success' , 'loading' , 'error' , 'validation' ]
75+ . map ( state => this . querySelector < HTMLElement > ( `#${ prefix } -preview-toast-${ state } ` ) )
76+ . filter ( ( el ) : el is HTMLElement => el !== null )
77+ }
78+
7079 override connectedCallback ( ) : void {
7180 super . connectedCallback ( )
7281
@@ -146,9 +155,20 @@ export class PrivacyFormElement extends LitElement {
146155 } )
147156 }
148157
149- private setRequestState ( form : HTMLFormElement , state : RequestPreviewState | 'idle' ) : void {
158+ private setRequestState (
159+ form : HTMLFormElement ,
160+ state : RequestPreviewState | 'idle' ,
161+ requestType ?: RequestType
162+ ) : void {
150163 form . dataset . privacyState = state
151164 form . setAttribute ( 'aria-busy' , String ( state === 'loading' ) )
165+
166+ if ( ! requestType ) {
167+ return
168+ }
169+
170+ const rootAttributeName = requestType === 'ACCESS' ? 'data-access-state' : 'data-delete-state'
171+ this . setAttribute ( rootAttributeName , state )
152172 }
153173
154174 private setSubmitLoading ( form : HTMLFormElement , loading : boolean ) : void {
@@ -168,20 +188,116 @@ export class PrivacyFormElement extends LitElement {
168188 this . deleteConfirmCheckbox . setAttribute ( 'aria-invalid' , String ( invalid ) )
169189 }
170190
191+ private hidePreviewToast ( requestType : RequestType ) : void {
192+ for ( const el of this . getPreviewToastElements ( requestType ) ) {
193+ el . classList . add ( 'hidden' )
194+ }
195+ }
196+
197+ private resolveRequestToastConfig (
198+ requestType : RequestType ,
199+ message : string ,
200+ type : RequestToastTone
201+ ) : {
202+ title : string
203+ icon : 'check-stylized' | 'warning' | 'spinner'
204+ containerClasses : string [ ]
205+ iconContainerClasses : string [ ]
206+ titleClasses : string [ ]
207+ } {
208+ if ( type === 'success' ) {
209+ return {
210+ title : requestType === 'ACCESS' ? 'Request Sent' : 'Deletion Request Sent' ,
211+ icon : 'check-stylized' ,
212+ containerClasses : [ 'border-success' , 'bg-success-inverse' , 'text-success' ] ,
213+ iconContainerClasses : [ 'bg-success' , 'text-content-inverse' ] ,
214+ titleClasses : [ 'text-success' ] ,
215+ }
216+ }
217+
218+ if ( type === 'info' ) {
219+ return {
220+ title : 'Sending Request' ,
221+ icon : 'spinner' ,
222+ containerClasses : [ 'border-info' , 'bg-info-inverse' , 'text-info' ] ,
223+ iconContainerClasses : [ 'bg-info' , 'text-content-inverse' ] ,
224+ titleClasses : [ 'text-info' ] ,
225+ }
226+ }
227+
228+ const isValidationMessage =
229+ message . includes ( 'valid email' ) || message . includes ( 'confirm you understand' ) || message . includes ( 'confirm the deletion request' )
230+
231+ return {
232+ title : isValidationMessage
233+ ? 'Check Your Details'
234+ : requestType === 'ACCESS'
235+ ? 'Request Failed'
236+ : 'Deletion Request Failed' ,
237+ icon : 'warning' ,
238+ containerClasses : [ 'border-danger' , 'bg-danger-inverse' , 'text-danger' ] ,
239+ iconContainerClasses : [ 'bg-danger' , 'text-content-inverse' ] ,
240+ titleClasses : [ 'text-danger' ] ,
241+ }
242+ }
243+
244+ private renderRequestToast ( target : HTMLElement , requestType : RequestType , message : string , type : RequestToastTone ) : void {
245+ const config = this . resolveRequestToastConfig ( requestType , message , type )
246+
247+ target . replaceChildren ( )
248+ target . classList . remove ( 'px-4' , 'py-3' , 'text-sm' , 'bg-danger-offset' )
249+ target . classList . add ( 'w-full' , 'rounded-xl' , 'border' )
250+ target . classList . add ( ...config . containerClasses )
251+
252+ const wrapper = document . createElement ( 'div' )
253+ wrapper . className = 'flex items-start gap-4 p-6'
254+
255+ const iconContainer = document . createElement ( 'div' )
256+ iconContainer . className = 'shrink-0 flex h-6 w-6 items-center justify-center rounded-full'
257+ iconContainer . classList . add ( ...config . iconContainerClasses )
258+
259+ const icon = document . createElement ( 'span' )
260+ icon . setAttribute ( 'aria-hidden' , 'true' )
261+ icon . textContent = config . icon === 'check-stylized' ? '✓' : config . icon === 'spinner' ? '◌' : '!'
262+ iconContainer . appendChild ( icon )
263+
264+ const content = document . createElement ( 'div' )
265+
266+ const title = document . createElement ( 'h3' )
267+ title . className = 'mb-2 text-lg font-semibold'
268+ title . classList . add ( ...config . titleClasses )
269+ title . textContent = config . title
270+
271+ const body = document . createElement ( 'p' )
272+ body . textContent = message
273+
274+ content . append ( title , body )
275+ wrapper . append ( iconContainer , content )
276+ target . appendChild ( wrapper )
277+ }
278+
171279 private resetMessage ( target : HTMLElement ) : void {
172- target . textContent = ''
280+ target . replaceChildren ( )
173281 target . classList . add ( 'hidden' )
174282 target . classList . remove (
283+ 'w-full' ,
284+ 'rounded-xl' ,
285+ 'border' ,
175286 'border-success' ,
176287 'bg-success-inverse' ,
177288 'text-success' ,
178289 'border-danger' ,
179290 'bg-danger-offset' ,
291+ 'bg-danger-inverse' ,
180292 'text-danger' ,
181293 'border-info' ,
182294 'bg-info-inverse' ,
183- 'text-info'
295+ 'text-info' ,
296+ 'px-4' ,
297+ 'py-3' ,
298+ 'text-sm'
184299 )
300+ target . classList . add ( 'rounded-xl' , 'border' , 'px-4' , 'py-3' , 'text-sm' )
185301 }
186302
187303 private resetRequestState ( requestType : RequestType ) : void {
@@ -190,8 +306,9 @@ export class PrivacyFormElement extends LitElement {
190306 const emailInput = isAccessRequest ? this . accessEmailInput : this . deleteEmailInput
191307 const message = isAccessRequest ? this . accessMessage : this . deleteMessage
192308
309+ this . hidePreviewToast ( requestType )
193310 this . resetMessage ( message )
194- this . setRequestState ( form , 'idle' )
311+ this . setRequestState ( form , 'idle' , requestType )
195312 this . setSubmitLoading ( form , false )
196313 this . setEmailInvalid ( emailInput , false )
197314
@@ -208,9 +325,23 @@ export class PrivacyFormElement extends LitElement {
208325 ) : void {
209326 target . setAttribute ( 'role' , type === 'error' ? 'alert' : 'status' )
210327 target . setAttribute ( 'aria-live' , type === 'error' ? 'assertive' : 'polite' )
211- target . textContent = message
212328 target . classList . remove ( 'hidden' )
213329
330+ const requestType =
331+ target . id === 'access-message' ? 'ACCESS' : target . id === 'delete-message' ? 'DELETE' : null
332+
333+ if ( requestType ) {
334+ this . renderRequestToast ( target , requestType , message , type )
335+
336+ if ( options . focus ?? true ) {
337+ target . focus ( )
338+ }
339+
340+ return
341+ }
342+
343+ target . textContent = message
344+
214345 const variantClasses = [
215346 'border-success' ,
216347 'bg-success-inverse' ,
@@ -263,61 +394,25 @@ export class PrivacyFormElement extends LitElement {
263394 requestType : RequestType ,
264395 previewState : RequestPreviewState
265396 ) : void {
266- this . resetRequestState ( requestType )
267-
268397 const isAccessRequest = requestType === 'ACCESS'
269398 const form = isAccessRequest ? this . accessForm : this . deleteForm
270399 const emailInput = isAccessRequest ? this . accessEmailInput : this . deleteEmailInput
271- const message = isAccessRequest ? this . accessMessage : this . deleteMessage
272-
273- switch ( previewState ) {
274- case 'loading' :
275- this . setRequestState ( form , 'loading' )
276- this . setSubmitLoading ( form , true )
277- this . setMessage ( message , 'Sending request...' , 'info' , { focus : false } )
278- return
400+ const prefix = isAccessRequest ? 'access' : 'delete'
279401
280- case 'success' :
281- this . setRequestState ( form , 'success' )
282- this . setMessage (
283- message ,
284- isAccessRequest
285- ? 'Data access request sent. Please check your inbox to verify the request.'
286- : 'Deletion request sent. Please check your inbox to verify the request.' ,
287- 'success' ,
288- { focus : false }
289- )
290- return
291-
292- case 'error' :
293- this . setRequestState ( form , 'error' )
294- this . setMessage (
295- message ,
296- isAccessRequest
297- ? 'Unable to submit your data request. Please try again.'
298- : 'Unable to submit your deletion request. Please try again.' ,
299- 'error' ,
300- { focus : false }
301- )
302- return
303-
304- case 'validation' :
305- this . setRequestState ( form , 'validation' )
306- this . setEmailInvalid ( emailInput , true )
402+ // Show only the matching static SSR preview toast; hide all others for this form.
403+ for ( const el of this . getPreviewToastElements ( requestType ) ) {
404+ el . classList . add ( 'hidden' )
405+ }
406+ this . querySelector < HTMLElement > ( `#${ prefix } -preview-toast-${ previewState } ` ) ?. classList . remove ( 'hidden' )
307407
308- if ( isAccessRequest ) {
309- this . setMessage ( message , 'Please enter a valid email address.' , 'error' , { focus : false } )
310- return
311- }
408+ this . setRequestState ( form , previewState , requestType )
409+ this . setSubmitLoading ( form , previewState === 'loading' )
312410
411+ if ( previewState === 'validation' ) {
412+ this . setEmailInvalid ( emailInput , true )
413+ if ( ! isAccessRequest ) {
313414 this . setDeleteConfirmationInvalid ( true )
314- this . setMessage (
315- message ,
316- 'Please enter a valid email address and confirm the deletion request.' ,
317- 'error' ,
318- { focus : false }
319- )
320- return
415+ }
321416 }
322417 }
323418
@@ -408,33 +503,33 @@ export class PrivacyFormElement extends LitElement {
408503 this . resetRequestState ( requestType )
409504
410505 if ( requestType === 'DELETE' && ! this . deleteConfirmCheckbox . checked ) {
411- this . setRequestState ( formEl , 'validation' )
506+ this . setRequestState ( formEl , 'validation' , requestType )
412507 this . setDeleteConfirmationInvalid ( true )
413508 this . setMessage ( messageEl , 'Please confirm you understand the deletion request.' , 'error' )
414509 return
415510 }
416511
417512 const email = emailInput . value
418- this . setRequestState ( formEl , 'loading' )
513+ this . setRequestState ( formEl , 'loading' , requestType )
419514 this . setSubmitLoading ( formEl , true )
420515 this . setMessage ( messageEl , 'Sending request...' , 'info' )
421516
422517 try {
423518 const { data, error } = await actions . gdpr . requestData ( { email, requestType } )
424519 if ( error || ! data ) {
425- this . setRequestState ( formEl , 'error' )
520+ this . setRequestState ( formEl , 'error' , requestType )
426521 this . setSubmitLoading ( formEl , false )
427522 this . setMessage ( messageEl , error ?. message || 'Request failed' , 'error' )
428523 return
429524 }
430525
431526 const resultData = data as RequestDataResult
432- this . setRequestState ( formEl , 'success' )
527+ this . setRequestState ( formEl , 'success' , requestType )
433528 this . setSubmitLoading ( formEl , false )
434529 this . setMessage ( messageEl , resultData . message , 'success' )
435530 formEl . reset ( )
436531 } catch ( error ) {
437- this . setRequestState ( formEl , 'error' )
532+ this . setRequestState ( formEl , 'error' , requestType )
438533 this . setSubmitLoading ( formEl , false )
439534 this . setMessage (
440535 messageEl ,
0 commit comments