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 9, 2024
1 parent e4caa1b commit ba73dd9
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 0 deletions.
94 changes: 94 additions & 0 deletions e2e/base/components/Banner.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { StoryBox } from '../../../.storybook/components/StoryBox'
import { Level } from '../../../src'
import { _Banner as BannerStory } from '../../../stories/components/Banner.stories'
import { mountAndWait } from '../utils'

context(`Story`, () => {
describe('default visibility', () => {
it('should not show the banner when isHiddenByDefault is true', () => {
mountAndWait(
<StoryBox>
<BannerStory
isClosable={false}
isCollapsible
isHiddenByDefault
level={Level.SUCCESS}
text="some text"
top="60px"
/>
</StoryBox>
)
cy.get('.banner').should('not.be.visible')
})
it('should show the banner when isHiddenByDefault is false', () => {
mountAndWait(
<StoryBox>
<BannerStory
isClosable={false}
isCollapsible
isHiddenByDefault={false}
level={Level.SUCCESS}
text="some text"
top="60px"
/>
</StoryBox>
)
cy.get('.banner').should('be.visible')
})
it('should show the banner when isHiddenByDefault is undefined', () => {
mountAndWait(
<StoryBox>
<BannerStory
isClosable={false}
isCollapsible
isHiddenByDefault={undefined}
level={Level.SUCCESS}
text="some text"
top="60px"
/>
</StoryBox>
)
cy.get('.banner').should('be.visible')
})
})
describe('closable version', () => {
it('should disappear when the action button is clicked', () => {
mountAndWait(
<StoryBox>
<BannerStory
isClosable
isCollapsible={false}
isHiddenByDefault={false}
level={Level.SUCCESS}
text="some text"
top="60px"
/>
</StoryBox>
)
cy.get('.banner-button').click()
cy.get('.banner').should('not.be.visible')
})
})
describe('collapsible version', () => {
it('should collapse when the action button is clicked', () => {
mountAndWait(
<StoryBox>
<BannerStory
isClosable={false}
isCollapsible
isHiddenByDefault={false}
level={Level.SUCCESS}
text="some text"
top="60px"
/>
</StoryBox>
)
// check it's fully opened
cy.get('.banner').invoke('outerHeight').should('be.gt', 10)
// click on the hide button
cy.get('.banner-button').click()
// check it's shrunk
cy.get('.banner').invoke('outerHeight').should('be.equal', 10)
})
})
})
28 changes: 28 additions & 0 deletions src/components/Banner/__tests__/getBannerPalette.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getBannerPalette } from '../utils'

Check failure on line 1 in src/components/Banner/__tests__/getBannerPalette.test.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups

Check failure on line 1 in src/components/Banner/__tests__/getBannerPalette.test.ts

View workflow job for this annotation

GitHub Actions / Lint

`../utils` import should occur after import of `../../../theme`
import { describe, expect, test } from '@jest/globals'

Check failure on line 2 in src/components/Banner/__tests__/getBannerPalette.test.ts

View workflow job for this annotation

GitHub Actions / Lint

There should be at least one empty line between import groups
import { Level } from '../../../constants'
import { THEME } from '../../../theme'

describe('getBannerPalette', () => {
test('should return a red palette for the Error level', () => {
expect(getBannerPalette(Level.ERROR)).toStrictEqual({
backgroundColor: THEME.color.maximumRed15,
borderColor: THEME.color.maximumRed,
color: THEME.color.maximumRed
})
})
test('should return a yellow palette for the Warning level', () => {
expect(getBannerPalette(Level.WARNING)).toStrictEqual({
backgroundColor: THEME.color.goldenPoppy25,
borderColor: THEME.color.goldenPoppy,
color: THEME.color.charcoal
})
})
test('should return a green palette for the success level', () => {
expect(getBannerPalette(Level.SUCCESS)).toStrictEqual({
backgroundColor: THEME.color.mediumSeaGreen25,
borderColor: THEME.color.mediumSeaGreen,
color: THEME.color.mediumSeaGreen
})
})
})
122 changes: 122 additions & 0 deletions src/components/Banner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Button } from '@elements/Button'
import { IconButton } from '@elements/IconButton'
import { isString } from 'lodash'
import { type ReactNode, useState } from 'react'
import styled from 'styled-components'

import { getBannerPalette } from './utils'
import { Accent, Level, Icon } from '../../constants'

export type BannerProps = {
isClosable: boolean
isCollapsible: boolean
isHiddenByDefault: boolean | undefined
level: Level
text: string | ReactNode
top: string
}

interface WrapperProps {
$isCollapsed: boolean
$isHidden: boolean
$level: Level
$top: string
}
const Wrapper = styled.div<WrapperProps>`
display: ${(p: WrapperProps) => (p.$isHidden ? 'none' : 'flex')};
flex-direction: row;
justify-content: space-between;
align-items: center;
position: absolute;
background-color: ${(p: WrapperProps) => getBannerPalette(p.$level).backgroundColor};
width: 100%;
min-width: 100%;
max-width: 100%;
padding: 0 2rem;
top: ${(p: WrapperProps) => `${p.$top}`};
z-index: 1000;
height: ${(p: WrapperProps) => (!p.$isHidden && p.$isCollapsed ? '10px' : '50px')};
border-bottom: 4px solid ${(p: WrapperProps) => getBannerPalette(p.$level).borderColor};
transition: height 0.3s ease;
`

interface ContentWrapperProps {
$level: Level
}
const ContentWrapper = styled.div<ContentWrapperProps>`
color: ${(p: ContentWrapperProps) => getBannerPalette(p.$level).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({ isClosable, isCollapsible, isHiddenByDefault, level, text, top }: Readonly<BannerProps>) {
const [isHidden, setIsHidden] = useState<boolean>(!!isHiddenByDefault)
const [isCollapsed, setIsCollapsed] = useState<boolean>(false)
const [isCollapsing, setIsCollapsing] = useState<boolean>(false)
const [hasCollapsed, setHasCollapsed] = useState<boolean>(false)

const enterHover = (): void => {
if (!isHidden && isCollapsed && !isCollapsing) {
setIsCollapsed(false)
}
setIsCollapsing(false)
}
const leaveHover = (): void => {
if (!isHidden && hasCollapsed) {
setIsCollapsed(true)
}
}

const onClickAction = (): void => {
if (isClosable) {
setIsHidden(true)
} else if (isCollapsible) {
setIsCollapsing(true)
setIsCollapsed(true)
setHasCollapsed(true)
}
}

return (
<Wrapper
$isCollapsed={isCollapsed}
$isHidden={isHidden}
$level={level}
$top={top}
className="banner"
onMouseEnter={enterHover}
onMouseLeave={leaveHover}
>
{!isHidden && !isCollapsed && (
<>
<ContentWrapper $level={level}>{isString(text) ? <p>{text}</p> : <>{text}</>}</ContentWrapper>
<ButtonWrapper
className="banner-button"
onClick={() => onClickAction()}
title={isClosable ? 'fermer' : 'masquer'}
>
{isClosable && (
<IconButton accent={Accent.TERTIARY} color={getBannerPalette(level).color} Icon={Icon.Close} />
)}
{!isClosable && isCollapsible && (
<Button accent={Accent.TERTIARY} color={getBannerPalette(level).color}>
Masquer
</Button>
)}
</ButtonWrapper>
</>
)}
</Wrapper>
)
}

Banner.displayName = 'Banner'

export { Banner }
25 changes: 25 additions & 0 deletions src/components/Banner/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Level } from '../../constants'
import { THEME } from '../../theme'

export const getBannerPalette = (level: Level) => {
if (level === Level.ERROR) {
return {
backgroundColor: THEME.color.maximumRed15,
borderColor: THEME.color.maximumRed,
color: THEME.color.maximumRed
}
}
if (level === Level.WARNING) {
return {
backgroundColor: THEME.color.goldenPoppy25,
borderColor: THEME.color.goldenPoppy,
color: THEME.color.charcoal
}
}

return {
backgroundColor: THEME.color.mediumSeaGreen25,
borderColor: THEME.color.mediumSeaGreen,
color: THEME.color.mediumSeaGreen
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { SideMenu } from './components/SideMenu'
export { SingleTag } from './elements/SingleTag'
export { MapMenuDialog } from './components/MapMenuDialog'
export { Message } from './components/Message'
export { Banner } from './components/Banner'

export { Button } from './elements/Button'
export { Field } from './elements/Field'
Expand Down
53 changes: 53 additions & 0 deletions stories/components/Banner.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ARG_TYPE, META_DEFAULTS } from '../../.storybook/constants'
import { generateStoryDecorator } from '../../.storybook/utils/generateStoryDecorator'
import { Level, THEME } from '../../src'
import { Banner } 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,
level: ARG_TYPE.OPTIONAL_LEVEL,
isCollapsible: ARG_TYPE.BOOLEAN,
isClosable: ARG_TYPE.BOOLEAN
},

args: {
isHiddenByDefault: false,
isCollapsible: true,
isClosable: false,
level: Level.SUCCESS,
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>
)
}

0 comments on commit ba73dd9

Please sign in to comment.