Skip to content

Commit

Permalink
Snackbar (#332)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wenche authored Jun 23, 2020
1 parent 462531c commit eb8a1bf
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 2 deletions.
120 changes: 120 additions & 0 deletions apps/storybook-react/stories/Snackbar.stories.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Wrapper>
<div>
<Button type="button" onClick={() => setOpen(true)}>
Show a simple snackbar for 5 seconds
</Button>
<Snackbar
open={open}
onClose={() => setOpen(false)}
autoHideDuration={5000}
leftAlignFrom="1500px"
>
Message goes here
</Snackbar>
</div>
<div>
<Button type="button" onClick={() => setWithActionOpen(true)}>
Show a snackbar with action for the default 7 seconds
</Button>
<Snackbar
open={withActionOpen}
onClose={() => setWithActionOpen(false)}
>
Your changes was saved
<SnackbarAction>
<StyledButton variant="ghost">Undo</StyledButton>
</SnackbarAction>
</Snackbar>
</div>
</Wrapper>
)
}

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 (
<Wrapper>
<div>
<Button onClick={() => setOpen(true)}>Trigger snackbar</Button>
</div>
{open && (
<Snackbar
open={open}
onClose={() => setOpen(false)}
autoHideDuration={duration}
>
{message}
{action}
{action && (
<SnackbarAction>
<Button variant="ghost">Undo</Button>
</SnackbarAction>
)}
</Snackbar>
)}
</Wrapper>
)
}

knobs.story = {
name: 'With knobs',
decorators: [withKnobs],
}
5 changes: 4 additions & 1 deletion libraries/core-react/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
Expand Down
83 changes: 83 additions & 0 deletions libraries/core-react/src/Snackbar/Snackbar.jsx
Original file line number Diff line number Diff line change
@@ -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 && (
<StyledSnackbar leftAlignFrom={leftAlignFrom} className={className}>
{children}
</StyledSnackbar>
)}
</>
)
}

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,
}
67 changes: 67 additions & 0 deletions libraries/core-react/src/Snackbar/Snackbar.test.jsx
Original file line number Diff line number Diff line change
@@ -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(
<StyledSnackbar open>{message}</StyledSnackbar>,
)
expect(container.firstChild).toHaveStyleRule('clip-path', 'unset')
})
it('Sets the message', () => {
const { getByText } = render(<Snackbar open>{message}</Snackbar>)
const snackbar = getByText(message)
expect(snackbar).toBeDefined()
})
it('Is only visible when the open prop is true', () => {
const { rerender, container } = render(
<StyledSnackbar open={false}>{message}</StyledSnackbar>,
)
expect(container.firstChild).toBe(null)
rerender(<StyledSnackbar open>{message}</StyledSnackbar>)
expect(container.firstChild).not.toBe(null)
})
it('Disappears automatically after a provided timeout', async () => {
const { queryByText } = render(
<Snackbar open autoHideDuration={1000}>
{message}
</Snackbar>,
)
expect(queryByText(message)).toBeDefined()
await waitForElementToBeRemoved(() => queryByText(message))
})
it('Can have one button attached', () => {
const buttonText = "I'm the button"
const { queryByText } = render(
<Snackbar open>
{message}
<SnackbarAction>
<button type="button">{buttonText}</button>
</SnackbarAction>
</Snackbar>,
)
expect(queryByText(buttonText)).toBeDefined()
})
})
39 changes: 39 additions & 0 deletions libraries/core-react/src/Snackbar/Snackbar.tokens.js
Original file line number Diff line number Diff line change
@@ -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',
}
20 changes: 20 additions & 0 deletions libraries/core-react/src/Snackbar/SnackbarAction.jsx
Original file line number Diff line number Diff line change
@@ -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 <StyledSnackbarAction>{Children.only(children)}</StyledSnackbarAction>
}

SnackbarAction.propTypes = {
/** @ignore */
children: PropTypes.node.isRequired,
}
8 changes: 8 additions & 0 deletions libraries/core-react/src/Snackbar/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SnackbarAction } from './SnackbarAction'
import { Snackbar } from './Snackbar'

Snackbar.SnackbarAction = SnackbarAction

export { Snackbar }

/* export { useSnackbars } from './snackbar-hook' */
4 changes: 3 additions & 1 deletion libraries/core-react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
export { Radio, Checkbox, Switch } from './SelectionControls'
/* eslint-enable */

0 comments on commit eb8a1bf

Please sign in to comment.