From eb8a1bfb6e1b2333d56a1d070b8a033ca830a023 Mon Sep 17 00:00:00 2001 From: Wenche Tollevsen Date: Tue, 23 Jun 2020 09:50:54 +0200 Subject: [PATCH] Snackbar (#332) * First commit * Simple version of snackbar * Add support for async/await in tests https://github.com/facebook/jest/issues/3126\#issuecomment-483320742 * Tests for snackbar * Add better button labels, add knobs for storybook * Add version with Moss green 34 as action button color * Remove button color because of lack of contrast on hover * Wider query for 'larger screen' -> 1200px. Swap center and left, had this mixed up * Add lighter button color example --- .../stories/Snackbar.stories.jsx | 120 ++++++++++++++++++ libraries/core-react/babel.config.js | 5 +- .../core-react/src/Snackbar/Snackbar.jsx | 83 ++++++++++++ .../core-react/src/Snackbar/Snackbar.test.jsx | 67 ++++++++++ .../src/Snackbar/Snackbar.tokens.js | 39 ++++++ .../src/Snackbar/SnackbarAction.jsx | 20 +++ libraries/core-react/src/Snackbar/index.js | 8 ++ libraries/core-react/src/index.js | 4 +- 8 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 apps/storybook-react/stories/Snackbar.stories.jsx create mode 100644 libraries/core-react/src/Snackbar/Snackbar.jsx create mode 100644 libraries/core-react/src/Snackbar/Snackbar.test.jsx create mode 100644 libraries/core-react/src/Snackbar/Snackbar.tokens.js create mode 100644 libraries/core-react/src/Snackbar/SnackbarAction.jsx create mode 100644 libraries/core-react/src/Snackbar/index.js diff --git a/apps/storybook-react/stories/Snackbar.stories.jsx b/apps/storybook-react/stories/Snackbar.stories.jsx new file mode 100644 index 0000000000..0c7603e4f4 --- /dev/null +++ b/apps/storybook-react/stories/Snackbar.stories.jsx @@ -0,0 +1,120 @@ +import React, { Fragment, useState } from 'react' +import { withKnobs, select, text } from '@storybook/addon-knobs' +import { Snackbar, Button, Typography } from '@equinor/eds-core-react' + +const { SnackbarAction } = Snackbar +import styled from 'styled-components' +import { tokens } from '@equinor/eds-tokens' + +const { + colors: { + infographic: { + primary__moss_green_34: { rgba: buttonColor }, + }, + }, +} = tokens + +const Wrapper = styled.div` + display: grid; + grid-template-rows: min-width; + padding: 32px 0; + padding-bottom: 8rem; + grid-gap: 2rem; +` +// At the moment you'll have to override the default ghost button with this slightly lighter color for better contrast on dark background +const StyledButton = styled(Button)` + :not(:hover) { + color: ${buttonColor}; + } +` + +export default { + title: 'Components|Snackbar', + component: Snackbar, +} + +export const Example = () => { + const [open, setOpen] = useState(false) + const [withActionOpen, setWithActionOpen] = useState(false) + + return ( + +
+ + setOpen(false)} + autoHideDuration={5000} + leftAlignFrom="1500px" + > + Message goes here + +
+
+ + setWithActionOpen(false)} + > + Your changes was saved + + Undo + + +
+
+ ) +} + +const autoHideDurationOptions = { + Five: 5000, + Six: 6000, + Seven: 7000, + Eight: 8000, + Nine: 9000, + Ten: 10000, +} + +const actionOptions = { + none: null, + undoButton: 'button', +} + +export const knobs = () => { + const [open, setOpen] = useState(false) + const message = text('Message', 'Message goes here') + const duration = select('Duration in seconds', autoHideDurationOptions) + const action = select('Action', actionOptions) + + return ( + +
+ +
+ {open && ( + setOpen(false)} + autoHideDuration={duration} + > + {message} + {action} + {action && ( + + + + )} + + )} +
+ ) +} + +knobs.story = { + name: 'With knobs', + decorators: [withKnobs], +} diff --git a/libraries/core-react/babel.config.js b/libraries/core-react/babel.config.js index ce93e2de61..759b5178c3 100644 --- a/libraries/core-react/babel.config.js +++ b/libraries/core-react/babel.config.js @@ -7,7 +7,10 @@ module.exports = function babelConfig(api) { const env = { test: { - presets: ['@babel/preset-env', '@babel/preset-react'], + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-react', + ], plugins, }, } diff --git a/libraries/core-react/src/Snackbar/Snackbar.jsx b/libraries/core-react/src/Snackbar/Snackbar.jsx new file mode 100644 index 0000000000..3c0d2d6d10 --- /dev/null +++ b/libraries/core-react/src/Snackbar/Snackbar.jsx @@ -0,0 +1,83 @@ +import React, { useState, useEffect } from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { snackbar as tokens } from './Snackbar.tokens' +import { typographyTemplate } from '../_common/templates' + +const StyledSnackbar = styled.div.attrs(() => ({ + role: 'alert', +}))` + + position: fixed; + left: ${tokens.spacings.left}; + bottom: ${tokens.spacings.bottom}; + background-color: ${tokens.background}; + padding: ${tokens.spacings.padding}; + border-radius: ${tokens.borderRadius}; + ${typographyTemplate(tokens.text.typography)} + color: ${tokens.text.color}; + box-shadow: ${tokens.boxShadow}; + min-height: ${tokens.minHeight}; + box-sizing: border-box; + left: 50%; + transform: translateX(-50%); + @media (min-width: ${({ leftAlignFrom }) => leftAlignFrom}) { + left: auto; + transform: none; + } +` + +export const Snackbar = ({ + open, + autoHideDuration, + onClose, + leftAlignFrom, + children, + className, +}) => { + const [visible, setVisible] = useState(open) + useEffect(() => { + setVisible(open) + const timer = setTimeout(() => { + setVisible(false) + if (onClose) { + onClose() + } + }, autoHideDuration) + return () => clearTimeout(timer) + }, [open]) + return ( + <> + {visible && ( + + {children} + + )} + + ) +} + +Snackbar.displayName = 'eds-snackbar' + +Snackbar.propTypes = { + /** Controls the visibility of the snackbar */ + open: PropTypes.bool, + /** How long will the message be visible in milliseconds */ + autoHideDuration: PropTypes.number, + /** @ignore */ + children: PropTypes.node.isRequired, + /** @ignore */ + className: PropTypes.string, + /** Callback fired when the snackbar is closed by auto hide duration timeout */ + onClose: PropTypes.func, + /** Media query from which the snackbar will be horizontal centered */ + centerAlignFrom: PropTypes.string, +} + +Snackbar.defaultProps = { + autoHideDuration: 7000, + onClose: undefined, + open: false, + leftAlignFrom: '1200px', + className: undefined, +} diff --git a/libraries/core-react/src/Snackbar/Snackbar.test.jsx b/libraries/core-react/src/Snackbar/Snackbar.test.jsx new file mode 100644 index 0000000000..338decb31f --- /dev/null +++ b/libraries/core-react/src/Snackbar/Snackbar.test.jsx @@ -0,0 +1,67 @@ +/* eslint-disable no-undef */ + +import React from 'react' +import { + render, + cleanup, + waitForElementToBeRemoved, +} from '@testing-library/react' + +import '@testing-library/jest-dom' + +import 'jest-styled-components' +import styled from 'styled-components' + +import { Snackbar } from './index' + +const { SnackbarAction } = Snackbar + +afterEach(cleanup) + +const StyledSnackbar = styled(Snackbar)` + clip-path: unset; +` +const message = "Hi, I'm the snackbar" + +describe('Snackbar', () => { + it('Can extend the css for the component', () => { + const { container } = render( + {message}, + ) + expect(container.firstChild).toHaveStyleRule('clip-path', 'unset') + }) + it('Sets the message', () => { + const { getByText } = render({message}) + const snackbar = getByText(message) + expect(snackbar).toBeDefined() + }) + it('Is only visible when the open prop is true', () => { + const { rerender, container } = render( + {message}, + ) + expect(container.firstChild).toBe(null) + rerender({message}) + expect(container.firstChild).not.toBe(null) + }) + it('Disappears automatically after a provided timeout', async () => { + const { queryByText } = render( + + {message} + , + ) + expect(queryByText(message)).toBeDefined() + await waitForElementToBeRemoved(() => queryByText(message)) + }) + it('Can have one button attached', () => { + const buttonText = "I'm the button" + const { queryByText } = render( + + {message} + + + + , + ) + expect(queryByText(buttonText)).toBeDefined() + }) +}) diff --git a/libraries/core-react/src/Snackbar/Snackbar.tokens.js b/libraries/core-react/src/Snackbar/Snackbar.tokens.js new file mode 100644 index 0000000000..0ac8292757 --- /dev/null +++ b/libraries/core-react/src/Snackbar/Snackbar.tokens.js @@ -0,0 +1,39 @@ +import { tokens } from '@equinor/eds-tokens' + +const { + typography: { + ui: { snackbar: typography }, + }, + colors: { + ui: { + background__overlay: { rgba: background }, + }, + text: { + static_icons__primary_white: { hex: color }, + }, + }, + spacings: { + comfortable: { medium: spacingMedium }, + }, + elevation: { overlay: boxShadow }, + clickbounds: { default__base: clickbounds }, +} = tokens + +export const snackbar = { + background, + boxShadow, + minHeight: clickbounds, + spacings: { + left: spacingMedium, + bottom: spacingMedium, + padding: spacingMedium, + actionSpace: '32px', + }, + text: { + color, + typography: { + ...typography, + }, + }, + borderRadius: '4px', +} diff --git a/libraries/core-react/src/Snackbar/SnackbarAction.jsx b/libraries/core-react/src/Snackbar/SnackbarAction.jsx new file mode 100644 index 0000000000..be86023892 --- /dev/null +++ b/libraries/core-react/src/Snackbar/SnackbarAction.jsx @@ -0,0 +1,20 @@ +import React, { Children } from 'react' +import PropTypes from 'prop-types' +import styled from 'styled-components' +import { snackbar as tokens } from './Snackbar.tokens' + +const StyledSnackbarAction = styled.div` + display: inline-flex; + margin-left: ${tokens.spacings.actionSpace}; + margin-top: -10px; + margin-bottom: -10px; +` + +export const SnackbarAction = ({ children }) => { + return {Children.only(children)} +} + +SnackbarAction.propTypes = { + /** @ignore */ + children: PropTypes.node.isRequired, +} diff --git a/libraries/core-react/src/Snackbar/index.js b/libraries/core-react/src/Snackbar/index.js new file mode 100644 index 0000000000..09eda23dfb --- /dev/null +++ b/libraries/core-react/src/Snackbar/index.js @@ -0,0 +1,8 @@ +import { SnackbarAction } from './SnackbarAction' +import { Snackbar } from './Snackbar' + +Snackbar.SnackbarAction = SnackbarAction + +export { Snackbar } + +/* export { useSnackbars } from './snackbar-hook' */ diff --git a/libraries/core-react/src/index.js b/libraries/core-react/src/index.js index e09e8e9eb0..9ba64ae542 100644 --- a/libraries/core-react/src/index.js +++ b/libraries/core-react/src/index.js @@ -19,6 +19,8 @@ export { Avatar } from './Avatar' export { Search } from './Search' export { Slider } from './Slider' export { Tooltip } from './Tooltip' +export { Snackbar } from './Snackbar' export { Popover } from './Popover' export { Banner } from './Banner' -export { Radio, Checkbox, Switch } from './SelectionControls' \ No newline at end of file +export { Radio, Checkbox, Switch } from './SelectionControls' +/* eslint-enable */