Skip to content

Commit

Permalink
feat(components): introduce Banner component
Browse files Browse the repository at this point in the history
  • Loading branch information
lwih committed Apr 2, 2024
1 parent 2f49b9f commit 05b1241
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
80 changes: 80 additions & 0 deletions src/components/Banner/__test__/Banner.test.tsx
Original file line number Diff line number Diff line change
@@ -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: <div>button-placeholder</div>,
isHiddenByDefault: undefined,
text: defaultMessage,
top: '50px',
type,
version
})

describe('Banner', () => {
describe('default visibility', () => {
it('should be shown when isHiddenByDefault is undefined', () => {
render(<Banner {...props()} />)
expect(screen.getByText(defaultMessage)).toBeInTheDocument()

Check failure on line 26 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBeInTheDocument' does not exist on type 'Assertion'.
})
it('should be shown when isHiddenByDefault is false', () => {
render(<Banner {...{ ...props(), isHiddenByDefault: false }} />)
expect(screen.getByText(defaultMessage)).toBeInTheDocument()

Check failure on line 30 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBeInTheDocument' does not exist on type 'Assertion'.
})
it('should not be shown when isHiddenByDefault is true', () => {
render(<Banner {...{ ...props(), isHiddenByDefault: true }} />)
expect(screen.queryByText(defaultMessage)).toBeNull()

Check failure on line 34 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBeNull' does not exist on type 'Assertion'.
})
})
describe('closable version', () => {
it('should disappear when the action button is clicked', () => {
render(<Banner {...{ ...props(), type: BannerType.CLOSABLE }} />)
// check the banner is visible
expect(screen.getByText(defaultMessage)).toBeInTheDocument()

Check failure on line 41 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBeInTheDocument' does not exist on type 'Assertion'.
// click the button
const button = screen.getByText('button-placeholder')
fireEvent.click(button)
// check the banner has disappeared
expect(screen.queryByText(defaultMessage)).toBeNull()

Check failure on line 46 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBeNull' does not exist on type 'Assertion'.
})
})
describe('collapsible version', () => {
it('should collapse when the action button is clicked', () => {
const { container } = render(<Banner {...{ ...props() }} />)
// check the banner is visible
expect(getComputedStyle(container.firstChild as any).height).toBe('50px')

Check failure on line 53 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBe' does not exist on type 'Assertion'.
// 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')

Check failure on line 58 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBe' does not exist on type 'Assertion'.
})
it('should collapse/expand when hovering the banner', async () => {
const { container } = render(<Banner {...{ ...props() }} />)
// check the banner is visible
expect(screen.queryByText(defaultMessage)).not.toBeNull()

Check failure on line 63 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBeNull' does not exist on type 'Assertion'.
// click the button
const button = screen.getByText('button-placeholder')
fireEvent.click(button)
// check the banner has collapsed
expect(screen.queryByText(defaultMessage)).toBeNull()

Check failure on line 68 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBeNull' does not exist on type 'Assertion'.
// 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()

Check failure on line 73 in src/components/Banner/__test__/Banner.test.tsx

View workflow job for this annotation

GitHub Actions / Type

Property 'toBeInTheDocument' does not exist on type 'Assertion'.
})
// leave the banner and checks it's collapsed
fireEvent.mouseLeave(container.firstElementChild as Element)
expect(screen.queryByText(defaultMessage)).toBeNull()
})
})
})
129 changes: 129 additions & 0 deletions src/components/Banner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { isString } from 'lodash'
import { ReactNode, useState } from 'react'

Check failure on line 2 in src/components/Banner/index.tsx

View workflow job for this annotation

GitHub Actions / Release E2E Test

'ReactNode' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
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<BannerProps>) {
const [isHidden, setIsHidden] = useState<boolean>(isHiddenByDefault)

Check failure on line 81 in src/components/Banner/index.tsx

View workflow job for this annotation

GitHub Actions / Release E2E Test

Argument of type 'boolean | undefined' is not assignable to parameter of type 'boolean | (() => boolean)'.
const [isCollapsed, setIsCollapsed] = useState<boolean>(false)
const [isCollapsing, setIsCollapsing] = useState<boolean>(false)
const [hasCollapsed, setHasCollapsed] = useState<boolean>(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 (
<Wrapper
collapsed={isCollapsed}
hidden={isHidden}
onMouseEnter={enterHover}
onMouseLeave={leaveHover}
top={top}
version={version}
>
{!isHidden && !isCollapsed && (
<>
<ContentWrapper version={version}>{isString(text) ? <p>{text}</p> : <>{text}</>}</ContentWrapper>
<ButtonWrapper onClick={() => onClickAction()}>{actionButton}</ButtonWrapper>
</>
)}
</Wrapper>
)
}

Banner.displayName = 'Banner'

export { Banner }
52 changes: 52 additions & 0 deletions stories/components/Banner.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<BannerProps> = {
...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: <Button>configurable button</Button>,
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 (
<div>
<div
style={{
backgroundColor: THEME.color.charcoal,
height: '60px',
width: '100%'
}}
>
<h2 style={{ color: THEME.color.white }}>This is a header</h2>
</div>
<Banner {...props} />
</div>
)
}
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 05b1241

Please sign in to comment.