diff --git a/package.json b/package.json
index 418bed063..85ae140d3 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
"@swc/jest": "0.2.36",
"@tanstack/react-table": "8.10.7",
"@tanstack/react-virtual": "beta",
+ "@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "14.2.1",
"@types/diacritics": "1.3.1",
"@types/jabber": "1.2.0",
diff --git a/src/components/Banner/__test__/Banner.test.tsx b/src/components/Banner/__test__/Banner.test.tsx
new file mode 100644
index 000000000..f5b5a37c2
--- /dev/null
+++ b/src/components/Banner/__test__/Banner.test.tsx
@@ -0,0 +1,80 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+
+import '@testing-library/jest-dom'
+import { Banner, BannerType, BannerVersion } from '../index'
+
+import type { BannerProps } from '../index'
+
+const defaultMessage = 'this is the message'
+
+const props = (
+ type: BannerType = BannerType.COLLAPSIBLE,
+ version: BannerVersion = BannerVersion.VALID
+): BannerProps => ({
+ actionButton:
button-placeholder
,
+ isHiddenByDefault: undefined,
+ text: defaultMessage,
+ top: '50px',
+ type,
+ version
+})
+
+describe('Banner', () => {
+ describe('default visibility', () => {
+ it('should be shown when isHiddenByDefault is undefined', () => {
+ render()
+ expect(screen.getByText(defaultMessage)).toBeInTheDocument()
+ })
+ it('should be shown when isHiddenByDefault is false', () => {
+ render()
+ expect(screen.getByText(defaultMessage)).toBeInTheDocument()
+ })
+ it('should not be shown when isHiddenByDefault is true', () => {
+ render()
+ expect(screen.queryByText(defaultMessage)).toBeNull()
+ })
+ })
+ describe('closable version', () => {
+ it('should disappear when the action button is clicked', () => {
+ render()
+ // check the banner is visible
+ expect(screen.getByText(defaultMessage)).toBeInTheDocument()
+ // click the button
+ const button = screen.getByText('button-placeholder')
+ fireEvent.click(button)
+ // check the banner has disappeared
+ expect(screen.queryByText(defaultMessage)).toBeNull()
+ })
+ })
+ describe('collapsible version', () => {
+ it('should collapse when the action button is clicked', () => {
+ const { container } = render()
+ // check the banner is visible
+ expect(getComputedStyle(container.firstChild as any).height).toBe('50px')
+ // click the button
+ const button = screen.getByText('button-placeholder')
+ fireEvent.click(button)
+ // check the banner has collapsed
+ expect(getComputedStyle(container.firstChild as any).height).toBe('10px')
+ })
+ it('should collapse/expand when hovering the banner', async () => {
+ const { container } = render()
+ // check the banner is visible
+ expect(screen.queryByText(defaultMessage)).not.toBeNull()
+ // click the button
+ const button = screen.getByText('button-placeholder')
+ fireEvent.click(button)
+ // check the banner has collapsed
+ expect(screen.queryByText(defaultMessage)).toBeNull()
+ // hover (twice) on the banner and checked it opened again
+ fireEvent.mouseEnter(container.firstElementChild as Element)
+ fireEvent.mouseEnter(container.firstElementChild as Element)
+ await waitFor(() => {
+ expect(screen.getByText(defaultMessage)).toBeInTheDocument()
+ })
+ // leave the banner and checks it's collapsed
+ fireEvent.mouseLeave(container.firstElementChild as Element)
+ expect(screen.queryByText(defaultMessage)).toBeNull()
+ })
+ })
+})
diff --git a/src/components/Banner/index.tsx b/src/components/Banner/index.tsx
new file mode 100644
index 000000000..de64b02dc
--- /dev/null
+++ b/src/components/Banner/index.tsx
@@ -0,0 +1,129 @@
+import { isString } from 'lodash'
+import { ReactNode, useState } from 'react'
+import styled from 'styled-components'
+
+import { THEME } from '../../theme'
+
+export enum BannerType {
+ 'CLOSABLE' = 'CLOSABLE',
+ 'COLLAPSIBLE' = 'COLLAPSIBLE'
+}
+export enum BannerVersion {
+ 'ERROR' = 'ERROR',
+ 'VALID' = 'VALID',
+ 'WARNING' = 'WARNING'
+}
+
+export type BannerProps = {
+ actionButton: ReactNode
+ isHiddenByDefault: boolean | undefined
+ text: string | ReactNode
+ top: string
+ type: BannerType
+ version: BannerVersion
+}
+
+const colorPalette = (version: BannerVersion) => {
+ let config = {
+ backgroundColor: THEME.color.mediumSeaGreen25,
+ borderColor: THEME.color.mediumSeaGreen,
+ color: THEME.color.mediumSeaGreen
+ }
+ if (version === BannerVersion.ERROR) {
+ config = {
+ backgroundColor: THEME.color.maximumRed15,
+ borderColor: THEME.color.maximumRed,
+ color: THEME.color.maximumRed
+ }
+ } else if (version === BannerVersion.WARNING) {
+ config = {
+ backgroundColor: THEME.color.goldenPoppy25,
+ borderColor: THEME.color.goldenPoppy,
+ color: THEME.color.charcoal
+ }
+ }
+
+ return config
+}
+
+const Wrapper = styled.div<{ collapsed: boolean; hidden: boolean; top: string; version: BannerVersion }>`
+ display: ${p => (p.hidden ? 'none' : 'flex')};
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ position: absolute;
+ background-color: ${p => colorPalette(p.version).backgroundColor};
+ width: 100%;
+ min-width: 100%;
+ max-width: 100%;
+ padding: 0 2rem;
+ top: ${p => `${p.top}`};
+ z-index: 100000000;
+ height: ${p => (!p.hidden && p.collapsed ? '10px' : '50px')};
+ border-bottom: 4px solid ${p => colorPalette(p.version).borderColor};
+ transition: height 0.3s ease;
+`
+
+const ContentWrapper = styled.div<{ version: BannerVersion }>`
+ color: ${p => colorPalette(p.version).color};
+ align-self: center;
+ flex-grow: 2;
+ text-align: center;
+ font-size: 16px;
+ font-weight: 500;
+`
+
+const ButtonWrapper = styled.div`
+ align-self: center;
+`
+
+function Banner({ actionButton, isHiddenByDefault, text, top, type, version }: Readonly) {
+ const [isHidden, setIsHidden] = useState(isHiddenByDefault)
+ const [isCollapsed, setIsCollapsed] = useState(false)
+ const [isCollapsing, setIsCollapsing] = useState(false)
+ const [hasCollapsed, setHasCollapsed] = useState(false)
+
+ const enterHover = () => {
+ if (!isHidden && isCollapsed && !isCollapsing) {
+ setIsCollapsed(false)
+ }
+ setIsCollapsing(false)
+ }
+ const leaveHover = () => {
+ if (!isHidden && hasCollapsed) {
+ setIsCollapsed(true)
+ }
+ }
+
+ const onClickAction = () => {
+ if (type === BannerType.CLOSABLE) {
+ setIsHidden(true)
+ } else {
+ setIsCollapsing(true)
+ setIsCollapsed(true)
+ setHasCollapsed(true)
+ }
+ }
+
+ return (
+
+ {!isHidden && !isCollapsed && (
+ <>
+ {isString(text) ? {text}
: <>{text}>}
+ onClickAction()}>{actionButton}
+ >
+ )}
+
+ )
+}
+
+Banner.displayName = 'Banner'
+
+export { Banner }
diff --git a/stories/components/Banner.stories.tsx b/stories/components/Banner.stories.tsx
new file mode 100644
index 000000000..aba5b862b
--- /dev/null
+++ b/stories/components/Banner.stories.tsx
@@ -0,0 +1,52 @@
+import { ARG_TYPE, META_DEFAULTS } from '../../.storybook/constants'
+import { generateStoryDecorator } from '../../.storybook/utils/generateStoryDecorator'
+import { Button, THEME } from '../../src'
+import { Banner, BannerType, BannerVersion } from '../../src/components/Banner'
+
+import type { BannerProps } from '../../src/components/Banner'
+import type { Meta } from '@storybook/react'
+
+/* eslint-disable sort-keys-fix/sort-keys-fix */
+const meta: Meta = {
+ ...META_DEFAULTS,
+
+ title: 'Components/Banner',
+ component: Banner,
+
+ argTypes: {
+ isHiddenByDefault: ARG_TYPE.OPTIONAL_BOOLEAN,
+ version: BannerVersion,
+ type: BannerType
+ },
+
+ args: {
+ isHiddenByDefault: false,
+ type: BannerType.COLLAPSIBLE,
+ version: BannerVersion.VALID,
+ actionButton: ,
+ text: 'This is the content of the banner',
+ top: '76px'
+ },
+
+ decorators: [generateStoryDecorator()]
+}
+/* eslint-enable sort-keys-fix/sort-keys-fix */
+
+export default meta
+
+export function _Banner(props: BannerProps) {
+ return (
+
+
+
This is a header
+
+
+
+ )
+}
diff --git a/yarn.lock b/yarn.lock
index cfda37f7a..30954be70 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2866,6 +2866,7 @@ __metadata:
"@swc/jest": "npm:0.2.36"
"@tanstack/react-table": "npm:8.10.7"
"@tanstack/react-virtual": "npm:beta"
+ "@testing-library/jest-dom": "npm:^6.4.2"
"@testing-library/react": "npm:14.2.1"
"@types/diacritics": "npm:1.3.1"
"@types/jabber": "npm:1.2.0"
@@ -5507,7 +5508,7 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/jest-dom@npm:^6.4.0":
+"@testing-library/jest-dom@npm:^6.4.0, @testing-library/jest-dom@npm:^6.4.2":
version: 6.4.2
resolution: "@testing-library/jest-dom@npm:6.4.2"
dependencies: