Skip to content

Commit

Permalink
feat: Add notification component (#410)
Browse files Browse the repository at this point in the history
  • Loading branch information
frankieyan authored Dec 23, 2020
1 parent 106092a commit 1619fb6
Show file tree
Hide file tree
Showing 23 changed files with 465 additions and 11 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Reactist follows [semantic versioning](https://semver.org/) and doesn't introduce breaking changes (API-wise) in minor or patch releases. However, the appearance of a component might change in a minor or patch release so keep an eye on redesigns and make sure your app still looks and feels like you expect it.

## v7.2.0
- [Feature] A new `Notification` component has been added.

## v7.1.9

- [Fix] We're only rendering menu lists into the DOM when the menu is opened. This should result in measurable performance gains if you're rendering a lot menus (e.g. as part of a list).
Expand Down
2 changes: 1 addition & 1 deletion docs/iframe.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@
}</script><style>#root[hidden],
#docs-root[hidden] {
display: none !important;
}</style></head><body><div class="sb-nopreview sb-wrapper"><div class="sb-nopreview_main"><h1 class="sb-nopreview_heading sb-heading">No Preview</h1><p>Sorry, but you either have no stories or none are selected somehow.</p><ul><li>Please check the Storybook config.</li><li>Try reloading the page.</li></ul><p>If the problem persists, check the browser console, or the terminal you've run Storybook from.</p></div></div><div class="sb-errordisplay sb-wrapper"><pre id="error-message" class="sb-heading"></pre><pre class="sb-errordisplay_code"><code id="error-stack"></code></pre></div><div id="root"></div><div id="docs-root"></div><script src="runtime~main.2b6c7973ac2f266c7c5d.bundle.js"></script><script src="vendors~main.2b6c7973ac2f266c7c5d.bundle.js"></script><script src="main.2b6c7973ac2f266c7c5d.bundle.js"></script></body></html>
}</style></head><body><div class="sb-nopreview sb-wrapper"><div class="sb-nopreview_main"><h1 class="sb-nopreview_heading sb-heading">No Preview</h1><p>Sorry, but you either have no stories or none are selected somehow.</p><ul><li>Please check the Storybook config.</li><li>Try reloading the page.</li></ul><p>If the problem persists, check the browser console, or the terminal you've run Storybook from.</p></div></div><div class="sb-errordisplay sb-wrapper"><pre id="error-message" class="sb-heading"></pre><pre class="sb-errordisplay_code"><code id="error-stack"></code></pre></div><div id="root"></div><div id="docs-root"></div><script src="runtime~main.c44da9aa8ee4a5cb6f5e.bundle.js"></script><script src="vendors~main.c44da9aa8ee4a5cb6f5e.bundle.js"></script><script src="main.c44da9aa8ee4a5cb6f5e.bundle.js"></script></body></html>
2 changes: 0 additions & 2 deletions docs/main.2b6c7973ac2f266c7c5d.bundle.js

This file was deleted.

1 change: 0 additions & 1 deletion docs/main.2b6c7973ac2f266c7c5d.bundle.js.map

This file was deleted.

2 changes: 2 additions & 0 deletions docs/main.c44da9aa8ee4a5cb6f5e.bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/main.c44da9aa8ee4a5cb6f5e.bundle.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion docs/runtime~main.2b6c7973ac2f266c7c5d.bundle.js.map

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/runtime~main.c44da9aa8ee4a5cb6f5e.bundle.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions docs/vendors~main.2b6c7973ac2f266c7c5d.bundle.js

This file was deleted.

1 change: 0 additions & 1 deletion docs/vendors~main.2b6c7973ac2f266c7c5d.bundle.js.map

This file was deleted.

3 changes: 3 additions & 0 deletions docs/vendors~main.c44da9aa8ee4a5cb6f5e.bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/vendors~main.c44da9aa8ee4a5cb6f5e.bundle.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@doist/reactist",
"description": "Open source React components by Doist",
"author": "Henning Muszynski <[email protected]> (http://doist.com)",
"version": "7.1.9",
"version": "7.2.0",
"license": "MIT",
"homepage": "https://github.com/Doist/reactist#readme",
"repository": "git+https://github.com/Doist/reactist.git",
Expand Down
86 changes: 86 additions & 0 deletions src/components/notification/notification.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
@import '../styles/constants.less';

.container-style() {
box-sizing: border-box;
justify-content: space-between;
align-items: flex-start;
padding: 10px 14px;
border: 1px solid @color_border_grey;
border-radius: @default-radius;
box-shadow: @default-box-shadow;
background: #fff;
}

.reactist-notification {
display: flex;
position: relative;
font-size: @normal_font_size;

&:not(.reactist-notification--with-button) {
.container-style();
}
}

.reactist-notification__button,
.reactist-notification__close-button {
border: 0;
padding: 0;
margin: 0;
cursor: pointer;
transition-property: background-color;
transition-duration: @standard-timing;
background: #fff;

&:hover {
background: @color_grey_10;
}
}

.reactist-notification__button {
.container-style();
display: flex;
flex: 1;

&:hover + .reactist-notification__close-button {
background: @color_grey_10;
}
}

.reactist-notification__close-button {
position: absolute;
top: 8px;
right: 8px;
border-radius: @default-radius;

> * {
display: block;
}
}

.reactist-notification__icon-content-group {
display: flex;
flex: 1;
}

.reactist-notification--with-close-button {
.reactist-notification__icon-content-group {
padding-right: 24px;
}
}

.reactist-notification__content {
display: flex;
flex-direction: column;
align-items: flex-start;
}

.reactist-notification__title {
margin: 0 0 3px 0;
font-size: @normal_font_size;
text-align: left;
}

.reactist-notification__subtitle {
font-size: @smaller_font_size;
margin: 0;
}
79 changes: 79 additions & 0 deletions src/components/notification/notification.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'

import { Notification } from './notification'

describe('Notification', () => {
it('renders the provided title and subtitle', () => {
render(
<Notification id="notification-test" title="I'm a title" subtitle="I'm a subtitle" />,
)

expect(screen.getByText("I'm a title")).toBeVisible()
expect(screen.getByText("I'm a subtitle")).toBeVisible()
})

it("doesn't render a button or close button by default", () => {
render(
<Notification id="notification-test" title="I'm a title" subtitle="I'm a subtitle" />,
)

expect(screen.queryAllByRole('button')).toHaveLength(0)
})

it('renders a close button when onClose is provided', () => {
const onClose = jest.fn()

render(
<Notification
id="notification-test"
title="I'm a title"
onClose={onClose}
closeAltText="Close me"
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'Close me' }))

expect(onClose).toHaveBeenCalledTimes(1)
})

it('renders a button around the content when onClick is provided', () => {
const onClick = jest.fn()

render(<Notification id="notification-test" title="I'm a title" onClick={onClick} />)
fireEvent.click(screen.getByRole('button', { name: "I'm a title" }))

expect(onClick).toHaveBeenCalledTimes(1)
})

it('renders a provided custom icon', () => {
render(
<Notification
id="notification-test"
title="I'm a title"
icon={<div>I'm an icon</div>}
/>,
)

expect(screen.getByText("I'm an icon")).toBeVisible()
expect(screen.getByText("I'm a title")).toBeVisible()
})

it('renders children in place of the title and subtitle', () => {
render(
<Notification
id="notification-test"
title="I'm a title"
subtitle="I'm a subtitle"
icon={<div>I'm an icon</div>}
>
I'm what gets rendered instead
</Notification>,
)

expect(screen.queryByText("I'm a title")).not.toBeInTheDocument()
expect(screen.queryByText("I'm a subtitle")).not.toBeInTheDocument()
expect(screen.getByText("I'm an icon")).toBeVisible()
expect(screen.getByRole('dialog', { name: "I'm what gets rendered instead" })).toBeVisible()
})
})
103 changes: 103 additions & 0 deletions src/components/notification/notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react'
import classNames from 'classnames'
import CloseIcon from '../icons/CloseIcon.svg'
import './notification.less'

type NotificationProps = {
id: string
icon?: React.ReactNode
title?: React.ReactNode
subtitle?: React.ReactNode
children?: React.ReactNode
customCloseButton?: React.ReactNode
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
onClose?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
closeAltText?: string
className?: string
} & JSX.IntrinsicElements['div']

function Notification({
id,
icon,
title,
subtitle,
children,
customCloseButton,
onClick,
onClose,
closeAltText = 'Close',
className,
...rest
}: NotificationProps) {
const titleId = title ? `${id}-title` : null
const titleIdAttribute = titleId ? { id: titleId } : null
const subtitleId = subtitle ? `${id}-subtitle` : null
const subtitleIdAttribute = subtitleId ? { id: subtitleId } : null
const contentId = children ? `${id}-content` : null
const contentIdAttribute = children ? { id: `${id}-content` } : null
const ariaLabelledBy = contentId
? { 'aria-labelledby': contentId }
: titleId
? { 'aria-labelledby': titleId }
: null
const ariaDescribedBy = subtitleId && !children ? { 'aria-describedby': subtitleId } : null

const notificationContent = (
<div className="reactist-notification__content" {...contentIdAttribute}>
{children ?? (
<>
{title ? (
<h3 className="reactist-notification__title" {...titleIdAttribute}>
{title}
</h3>
) : null}
{subtitle ? (
<p className="reactist-notification__subtitle" {...subtitleIdAttribute}>
{subtitle}
</p>
) : null}
</>
)}
</div>
)
const notificationBody = (
<div className="reactist-notification__icon-content-group">
{icon ?? null}
{notificationContent}
</div>
)

return (
<div
id={id}
role="dialog"
className={classNames('reactist-notification', className, {
'reactist-notification--with-button': Boolean(onClick),
'reactist-notification--with-close-button': Boolean(onClose),
})}
{...ariaLabelledBy}
{...ariaDescribedBy}
{...rest}
>
{onClick ? (
<button className="reactist-notification__button" onClick={onClick}>
{notificationBody}
</button>
) : (
notificationBody
)}

{onClose ? (
<button
className="reactist-notification__close-button"
onClick={onClose}
aria-label={closeAltText}
>
{customCloseButton ?? <CloseIcon />}
</button>
) : null}
</div>
)
}

export { Notification }
9 changes: 9 additions & 0 deletions src/components/styles/constants.less
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,17 @@
line-height: 1.8;
}

// Border radius ////////////////////////////////////////////////////////////////////
@default-radius: 5px;

// Box shadow ////////////////////////////////////////////////////////////////////
@default-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);

// Z-Index ////////////////////////////////////////////////////////////////////
@z-index-tooltip: 1000;

// Break points ///////////////////////////////////////////////////////////////
@small_screen: ~'only screen and (max-width: 992px)';

// Transition timing ///////////////////////////////////////////////////////////////
@standard-timing: 300ms;
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export * from './components/Menu'

export { default as Modal } from './components/Modal'

export { Notification } from './components/notification/notification'

export { default as ProgressBar } from './components/ProgressBar'

export { default as RangeInput } from './components/RangeInput'
Expand Down
Loading

0 comments on commit 1619fb6

Please sign in to comment.