Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.

Commit da42c3b

Browse files
authored
Fix circular dependency in paragraph renderer (#1890)
1 parent 70b559e commit da42c3b

20 files changed

Lines changed: 335 additions & 157 deletions

File tree

src/app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import '@/assets/styles/globals.css'
55
import { ClientMetrics } from './ClientMetrics'
66
import { CustomElements } from './CustomElements'
77
import { PolyfillsScript } from './PolyfillsScript'
8+
import { ParagraphRendererProvider } from '@/components/paragraph/ParagraphRendererProvider'
89

910
const ASSETS_URL = process.env.NEXT_PUBLIC_ASSETS_URL || '/generated/'
1011
const nonce = '**CSP_NONCE**'
@@ -119,7 +120,9 @@ export default function RootLayout({
119120
/>
120121
</noscript>
121122
<CustomElements />
122-
<ClientMetrics>{children}</ClientMetrics>
123+
<ParagraphRendererProvider>
124+
<ClientMetrics>{children}</ClientMetrics>
125+
</ParagraphRendererProvider>
123126
</body>
124127
</html>
125128
)

src/assets/styles/globals.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ va-button#mdFormButton::part(button) {
8181
max-width: 40px !important;
8282
}
8383

84-
/* Alert block styles */
85-
.alert-with-additional-info {
84+
/* Alert block: tighten margins for expandable body inside va-alert */
85+
va-alert va-additional-info {
8686
*:first-child,
8787
*:last-child {
8888
margin-bottom: 0 !important;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react'
2+
import type { AlertType } from '@/components/alert/formatted-type'
3+
4+
export type AlertShellProps = {
5+
status: AlertType
6+
id?: string
7+
dataTemplate?: string
8+
dataParagraphType?: string
9+
dataEntityId?: string | number | null
10+
headline: React.ReactNode
11+
headlineClassName?: string
12+
headlineId?: string
13+
children?: React.ReactNode
14+
}
15+
16+
/**
17+
* Shared `va-alert` chrome for inline alerts and reusable alert blocks.
18+
*/
19+
export function AlertShell({
20+
status,
21+
id,
22+
dataTemplate,
23+
dataParagraphType,
24+
dataEntityId,
25+
headline,
26+
headlineClassName = 'vads-u-font-size--h3',
27+
headlineId,
28+
children,
29+
}: AlertShellProps) {
30+
return (
31+
<va-alert
32+
{...(id ? { id } : {})}
33+
{...(dataTemplate ? { 'data-template': dataTemplate } : {})}
34+
{...(dataParagraphType
35+
? { 'data-paragraph-type': dataParagraphType }
36+
: {})}
37+
{...(dataEntityId != null && dataEntityId !== ''
38+
? { 'data-entity-id': String(dataEntityId) }
39+
: {})}
40+
status={status}
41+
class="vads-u-margin-top--3"
42+
uswds
43+
>
44+
<h2
45+
slot="headline"
46+
className={headlineClassName}
47+
{...(headlineId ? { id: headlineId } : {})}
48+
>
49+
{headline}
50+
</h2>
51+
{children}
52+
</va-alert>
53+
)
54+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react'
2+
import type { AlertType } from '@/components/alert/formatted-type'
3+
import { AlertShell } from '@/components/alert/AlertShell'
4+
5+
export type InlineAlertProps = {
6+
entityId?: string | number | null
7+
alertType: AlertType
8+
heading: string
9+
children?: React.ReactNode
10+
}
11+
12+
/**
13+
* Inline alert body (`paragraph--alert` or non-reusable `alert_single` / `non_reusable_alert` UI).
14+
* Nested paragraph entities are passed as `children`.
15+
*/
16+
export function InlineAlert({
17+
entityId,
18+
alertType,
19+
heading,
20+
children,
21+
}: InlineAlertProps) {
22+
return (
23+
<AlertShell
24+
status={alertType}
25+
dataTemplate="paragraphs/alert"
26+
dataParagraphType="paragraph--alert"
27+
dataEntityId={entityId}
28+
headline={heading}
29+
>
30+
{children}
31+
</AlertShell>
32+
)
33+
}

src/components/alert/template.test.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react'
12
import { render, screen } from '@testing-library/react'
23
import { axe } from '@/test-utils'
34
import { Alert } from './template'
@@ -6,6 +7,7 @@ import { Alert as FormattedAlert } from '@/components/alert/formatted-type'
67
import { AlertType } from '@/components/alert/formatted-type'
78
import { AccordionItem as FormattedAccordion } from '@/components/accordion/formatted-type'
89
import { Wysiwyg } from '@/components/wysiwyg/formatted-type'
10+
import { ParagraphRendererProvider } from '@/components/paragraph/ParagraphRendererProvider'
911

1012
const mockParagraphs: FormattedParagraph[] = [
1113
{
@@ -32,6 +34,9 @@ const mockAlert: FormattedAlert = {
3234
}
3335

3436
describe('<Alert> Component', () => {
37+
const renderWithParagraphProvider = (ui: React.ReactElement) =>
38+
render(<ParagraphRendererProvider>{ui}</ParagraphRendererProvider>)
39+
3540
it('renders AlertBlock when blockReference is provided', async () => {
3641
const mockAlertWithBlockReference = {
3742
...mockAlert,
@@ -42,7 +47,9 @@ describe('<Alert> Component', () => {
4247
content: wysiwyg,
4348
},
4449
}
45-
const { container } = render(<Alert {...mockAlertWithBlockReference} />)
50+
const { container } = renderWithParagraphProvider(
51+
<Alert {...mockAlertWithBlockReference} />
52+
)
4653
const alertBlock = container.querySelector('va-alert')
4754
expect(alertBlock).toBeInTheDocument()
4855
expect(screen.getByText('Block Reference Title')).toBeInTheDocument()
@@ -52,7 +59,7 @@ describe('<Alert> Component', () => {
5259
})
5360

5461
it('renders custom alert with paragraphs when blockReference is not provided', async () => {
55-
const { container } = render(<Alert {...mockAlert} />)
62+
const { container } = renderWithParagraphProvider(<Alert {...mockAlert} />)
5663
const customAlert = container.querySelector('va-alert')
5764
expect(customAlert).toHaveAttribute(
5865
'data-paragraph-type',

src/components/alert/template.tsx

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import React from 'react'
22
import { Alert as FormattedAlert } from '@/components/alert/formatted-type'
33
import { ParagraphComponent } from '@/components/paragraph/formatted-type'
44
import { AlertBlock } from '@/components/alertBlock/template'
5-
import { Paragraph } from '@/components/paragraph/template'
5+
import { InlineAlert } from '@/components/alert/InlineAlert'
6+
import { useRenderParagraphs } from '@/components/paragraph/ParagraphRenderContext'
67

78
export function Alert(alert: ParagraphComponent<FormattedAlert>) {
9+
const renderParagraphs = useRenderParagraphs()
10+
811
// Checking for empty object because this component expects spread props like
912
// <Alert {...alert} /> so the `alert` param will always be at least `{}`.
1013
if (!alert || !Object.keys(alert).length) {
@@ -18,22 +21,9 @@ export function Alert(alert: ParagraphComponent<FormattedAlert>) {
1821
}
1922

2023
return (
21-
<va-alert
22-
data-template="paragraphs/alert"
23-
data-paragraph-type="paragraph--alert"
24-
data-entity-id={entityId}
25-
status={alertType}
26-
class="vads-u-margin-top--3"
27-
uswds
28-
>
29-
<h2 slot="headline" className="vads-u-font-size--h3">
30-
{heading}
31-
</h2>
32-
33-
{paragraphs?.map((paragraph) => (
34-
<Paragraph key={paragraph.id || paragraph.entityId} {...paragraph} />
35-
))}
36-
</va-alert>
24+
<InlineAlert entityId={entityId} alertType={alertType} heading={heading}>
25+
{renderParagraphs(paragraphs)}
26+
</InlineAlert>
3727
)
3828
}
3929

src/components/alertBlock/template.test.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AlertBlock } from './template'
44
import { AlertBlock as FormattedAlertBlock } from '@/components/alert/formatted-type'
55
import { ExpandableText } from '@/components/expandableText/formatted-type'
66
import { Wysiwyg } from '@/components/wysiwyg/formatted-type'
7+
import { ParagraphRendererProvider } from '@/components/paragraph/ParagraphRendererProvider'
78

89
const expandableText: ExpandableText = {
910
id: 'et-1',
@@ -31,8 +32,15 @@ describe('<Alert> with valid data and with expandable text', () => {
3132
content: expandableText,
3233
}
3334

35+
const renderAlertBlock = (props: FormattedAlertBlock) =>
36+
render(
37+
<ParagraphRendererProvider>
38+
<AlertBlock {...props} />
39+
</ParagraphRendererProvider>
40+
)
41+
3442
test('renders info <Alert> component', async () => {
35-
const { container } = render(<AlertBlock {...blockContentExpandable} />)
43+
const { container } = renderAlertBlock(blockContentExpandable)
3644

3745
const vaAdditionalInfo = container.querySelector('va-additional-info')
3846
expect(vaAdditionalInfo).toHaveAttribute('trigger', 'Learn how to sign in')
@@ -53,7 +61,7 @@ describe('<Alert> with valid data and with expandable text', () => {
5361

5462
test('renders warning <Alert> component', async () => {
5563
blockContentExpandable.alertType = 'warning'
56-
const { container } = render(<AlertBlock {...blockContentExpandable} />)
64+
const { container } = renderAlertBlock(blockContentExpandable)
5765

5866
const vaAdditionalInfo = container.querySelector('va-additional-info')
5967
expect(vaAdditionalInfo).toHaveAttribute('trigger', 'Learn how to sign in')
@@ -79,8 +87,15 @@ describe('<Alert> with valid data and wysiwyg', () => {
7987
content: wysiwyg,
8088
}
8189

90+
const renderAlertBlock = (props: FormattedAlertBlock) =>
91+
render(
92+
<ParagraphRendererProvider>
93+
<AlertBlock {...props} />
94+
</ParagraphRendererProvider>
95+
)
96+
8297
test('renders info <Alert> component', async () => {
83-
const { container } = render(<AlertBlock {...blockContentWysiwyg} />)
98+
const { container } = renderAlertBlock(blockContentWysiwyg)
8499

85100
expect(
86101
container.querySelector('va-additional-info')
@@ -99,7 +114,7 @@ describe('<Alert> with valid data and wysiwyg', () => {
99114

100115
test('renders warning <Alert> component', async () => {
101116
blockContentWysiwyg.alertType = 'warning'
102-
const { container } = render(<AlertBlock {...blockContentWysiwyg} />)
117+
const { container } = renderAlertBlock(blockContentWysiwyg)
103118

104119
expect(
105120
container.querySelector('va-additional-info')
Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,26 @@
1-
import { VaAlert } from '@department-of-veterans-affairs/component-library/dist/react-bindings'
1+
'use client'
2+
23
import { AlertBlock as FormattedAlertBlock } from '@/components/alert/formatted-type'
3-
import { PARAGRAPH_RESOURCE_TYPES } from '@/lib/constants/resourceTypes'
4-
import { Wysiwyg } from '@/components/wysiwyg/template'
54
import { slugifyString } from '@/lib/utils/slug'
5+
import { AlertShell } from '@/components/alert/AlertShell'
6+
import { useRenderParagraph } from '@/components/paragraph/ParagraphRenderContext'
67

78
export function AlertBlock(alertBlock: FormattedAlertBlock) {
9+
const renderParagraph = useRenderParagraph()
10+
811
if (!alertBlock || !Object.keys(alertBlock).length) {
912
return null
1013
}
1114

1215
const { alertType, id, title, content } = alertBlock
1316
return (
14-
<VaAlert id={id} status={alertType} class="vads-u-margin-top--3" uswds>
15-
<h2
16-
slot="headline"
17-
className="vads-u-font-size--h3"
18-
id={slugifyString(title)}
19-
>
20-
{title}
21-
</h2>
22-
23-
{content.type === PARAGRAPH_RESOURCE_TYPES.WYSIWYG && (
24-
<Wysiwyg {...content} />
25-
)}
26-
27-
{content.type === PARAGRAPH_RESOURCE_TYPES.EXPANDABLE_TEXT && (
28-
<va-additional-info
29-
id={`alert-with-additional-info-${id}`}
30-
trigger={content.header}
31-
disable-border="true"
32-
className="alert-with-additional-info"
33-
>
34-
{content.text && (
35-
<div
36-
dangerouslySetInnerHTML={{
37-
__html: content.text,
38-
}}
39-
/>
40-
)}
41-
</va-additional-info>
42-
)}
43-
</VaAlert>
17+
<AlertShell
18+
id={id}
19+
status={alertType}
20+
headline={title}
21+
headlineId={slugifyString(title)}
22+
>
23+
{renderParagraph(content)}
24+
</AlertShell>
4425
)
4526
}

src/components/alertNonReusable/template.test.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/components/alertNonReusable/template.tsx

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)