diff --git a/package-lock.json b/package-lock.json index 2c65db7f6..d538a0d8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.668", + "@defra/forms-model": "^3.0.673", "@defra/hapi-tracing": "^1.29.0", "@defra/interactive-map": "^0.0.22-alpha", "@elastic/ecs-pino-format": "^1.5.0", @@ -236,6 +236,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2081,6 +2082,7 @@ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -2223,6 +2225,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2262,6 +2265,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3513,9 +3517,9 @@ } }, "node_modules/@defra/forms-model": { - "version": "3.0.668", - "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.668.tgz", - "integrity": "sha512-H0FBwHZu+joIlBXdGOiqDguZMtVT8WDxIFUUw737wJNdFrqj0PxlpVQn/hPHc5Juvt0s19JUZhrNlI0sXoucZw==", + "version": "3.0.673", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.673.tgz", + "integrity": "sha512-CyPKSpnWYv+mBhZXNjc2L2mNxrzuGp3iToxHbzv3QU8ckmPUbocS6jgB7rXsgg98Fvza7hiG7CCCRG3wUwyCbg==", "license": "OGL-UK-3.0", "dependencies": { "@joi/date": "^2.1.1", @@ -4412,6 +4416,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.10.1.tgz", "integrity": "sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/babel": "3.10.1", "@docusaurus/bundler": "3.10.1", @@ -4586,6 +4591,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz", "integrity": "sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.10.1", "@docusaurus/logger": "3.10.1", @@ -4619,6 +4625,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.1.tgz", "integrity": "sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.10.1", "@docusaurus/module-type-aliases": "3.10.1", @@ -7358,6 +7365,7 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -11305,6 +11313,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -11501,6 +11510,7 @@ "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", @@ -11540,6 +11550,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -12344,6 +12355,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -12420,6 +12432,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13468,6 +13481,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -13826,6 +13840,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -17367,6 +17382,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -17554,6 +17570,7 @@ "integrity": "sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/types": "^8.35.0", "comment-parser": "^1.4.1", @@ -21347,6 +21364,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -22564,6 +22582,7 @@ "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -22630,6 +22649,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -27543,6 +27563,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -28939,6 +28960,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -29516,6 +29538,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -29525,6 +29548,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -29569,6 +29593,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -29597,6 +29622,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -31123,6 +31149,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -32375,6 +32402,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-syntax-patches-for-csstree": "^1.0.19", @@ -33124,6 +33152,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -33360,7 +33389,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.21.0", @@ -33368,6 +33398,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -33562,6 +33593,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -34238,6 +34270,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index 42a2515be..205d8827c 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ }, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.668", + "@defra/forms-model": "^3.0.673", "@defra/hapi-tracing": "^1.29.0", "@defra/interactive-map": "^0.0.22-alpha", "@elastic/ecs-pino-format": "^1.5.0", diff --git a/src/client/stylesheets/_prose.scss b/src/client/stylesheets/_prose.scss index a929dceba..ec7801fe7 100644 --- a/src/client/stylesheets/_prose.scss +++ b/src/client/stylesheets/_prose.scss @@ -26,6 +26,10 @@ @extend %govuk-body-m; } + > :last-child { + margin-bottom: 0; + } + strong, b { @include govuk-typography-weight-bold; diff --git a/src/server/forms/simple-form.yaml b/src/server/forms/simple-form.yaml index e43cabd66..af1f92f38 100644 --- a/src/server/forms/simple-form.yaml +++ b/src/server/forms/simple-form.yaml @@ -78,6 +78,25 @@ pages: id: '00738799-3489-4ab2-a57b-542eecb31bfa' next: [] id: da0fbdb4-a2de-4650-be16-9ba552af135f + - title: '' + path: '/notification-demo' + components: + - type: NotificationBanner + title: Important + name: notificationImportant + content: 'Contact us if you need [help with your application](https://www.defra.gov.uk).' + options: + heading: There may be a delay in processing your application. + id: ca6b6590-1608-49f4-82f9-9d92e38e66ea + - type: NotificationBanner + title: Success + name: notificationSuccess + content: 'Your application has been submitted.' + options: + type: success + id: b6960f4b-d771-4956-9c9b-17da5c603062 + next: [] + id: be4b4b86-514c-43bb-8cdc-f9aed6155924 - id: 449a45f6-4541-4a46-91bd-8b8931b07b50 title: '' path: '/summary' diff --git a/src/server/plugins/engine/components/NotificationBanner.test.ts b/src/server/plugins/engine/components/NotificationBanner.test.ts new file mode 100644 index 000000000..de8efd586 --- /dev/null +++ b/src/server/plugins/engine/components/NotificationBanner.test.ts @@ -0,0 +1,83 @@ +import { + ComponentType, + type NotificationBannerComponent +} from '@defra/forms-model' + +import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js' +import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js' +import definition from '~/test/form/definitions/basic.js' + +describe('NotificationBanner', () => { + let model: FormModel + + beforeEach(() => { + model = new FormModel(definition, { + basePath: 'test' + }) + }) + + it('sets Nunjucks component defaults', () => { + const def = { + title: 'Important', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'You have 30 days to [appeal this decision](/appeal).', + options: {} + } satisfies NotificationBannerComponent + + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() + + expect(viewModel).toEqual( + expect.objectContaining({ + attributes: {}, + titleHtml: def.title, + content: def.content + }) + ) + }) + + it('includes heading in view model', () => { + const def = { + title: 'Important', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'Contact us if you need help.', + options: { + heading: 'There may be a **delay** in processing your application.' + } + } satisfies NotificationBannerComponent + + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() + + expect(viewModel).toEqual( + expect.objectContaining({ + titleHtml: def.title, + content: def.content, + heading: def.options.heading + }) + ) + }) + + it('sets type: success for success variant', () => { + const def = { + title: 'Success', + name: 'myComponent', + type: ComponentType.NotificationBanner, + content: 'Your application has been submitted.', + options: { type: 'success' } + } satisfies NotificationBannerComponent + + const collection = new ComponentCollection([def], { model }) + const viewModel = collection.guidance[0].getViewModel() + + expect(viewModel).toEqual( + expect.objectContaining({ + titleHtml: def.title, + content: def.content, + type: 'success' + }) + ) + }) +}) diff --git a/src/server/plugins/engine/components/NotificationBanner.ts b/src/server/plugins/engine/components/NotificationBanner.ts new file mode 100644 index 000000000..2ee78cf3b --- /dev/null +++ b/src/server/plugins/engine/components/NotificationBanner.ts @@ -0,0 +1,32 @@ +import { type NotificationBannerComponent } from '@defra/forms-model' + +import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js' + +export class NotificationBanner extends ComponentBase { + declare options: NotificationBannerComponent['options'] + content: NotificationBannerComponent['content'] + + constructor( + def: NotificationBannerComponent, + props: ConstructorParameters[1] + ) { + super(def, props) + + const { content, options } = def + + this.content = content + this.options = options + } + + getViewModel() { + const { content, title, viewModel } = this + + return { + ...viewModel, + titleHtml: title, + content, + heading: this.options.heading, + type: this.options.type + } + } +} diff --git a/src/server/plugins/engine/components/helpers/components.ts b/src/server/plugins/engine/components/helpers/components.ts index 87e1cc264..fbda7be88 100644 --- a/src/server/plugins/engine/components/helpers/components.ts +++ b/src/server/plugins/engine/components/helpers/components.ts @@ -46,6 +46,7 @@ export type Guidance = | InstanceType | InstanceType | InstanceType + | InstanceType // List component instances only export type ListField = InstanceType< @@ -134,6 +135,10 @@ export function createComponent( component = new Components.Markdown(def, options) break + case ComponentType.NotificationBanner: + component = new Components.NotificationBanner(def, options) + break + case ComponentType.MultilineTextField: component = new Components.MultilineTextField(def, options) break diff --git a/src/server/plugins/engine/components/index.ts b/src/server/plugins/engine/components/index.ts index 7b59f6b11..8662e8ad2 100644 --- a/src/server/plugins/engine/components/index.ts +++ b/src/server/plugins/engine/components/index.ts @@ -15,6 +15,7 @@ export { Html } from '~/src/server/plugins/engine/components/Html.js' export { InsetText } from '~/src/server/plugins/engine/components/InsetText.js' export { List } from '~/src/server/plugins/engine/components/List.js' export { Markdown } from '~/src/server/plugins/engine/components/Markdown.js' +export { NotificationBanner } from '~/src/server/plugins/engine/components/NotificationBanner.js' export { MonthYearField } from '~/src/server/plugins/engine/components/MonthYearField.js' export { MultilineTextField } from '~/src/server/plugins/engine/components/MultilineTextField.js' export { NumberField } from '~/src/server/plugins/engine/components/NumberField.js' diff --git a/src/server/plugins/engine/views/components/notificationbanner.html b/src/server/plugins/engine/views/components/notificationbanner.html new file mode 100644 index 000000000..8aaa7fb78 --- /dev/null +++ b/src/server/plugins/engine/views/components/notificationbanner.html @@ -0,0 +1,15 @@ +{% from "govuk/components/notification-banner/macro.njk" import govukNotificationBanner %} + +{% macro NotificationBanner(component) %} + {%- set bannerHtml -%} +
+ {%- if component.model.heading %}{{ component.model.heading | markdown | safe }}{% endif -%} + {{ component.model.content | markdown | safe }} +
+ {%- endset -%} + {{ govukNotificationBanner({ + titleHtml: component.model.titleHtml, + html: bannerHtml, + type: component.model.type + }) }} +{% endmacro %}