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 ( + + ) +} + +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: