-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds octuple snackbar component (#81)
- Loading branch information
1 parent
3ea58e3
commit 6559c78
Showing
10 changed files
with
649 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import React from 'react'; | ||
import { | ||
Snackbar, | ||
SnackbarContainer, | ||
SnackbarPosition, | ||
SnackbarType, | ||
snack, | ||
} from './'; | ||
import { ButtonSize, DefaultButton } from '../Button'; | ||
|
||
export default { | ||
title: 'Snackbar', | ||
component: Snackbar, | ||
}; | ||
|
||
export const Default = () => { | ||
let snackIndex = 1; | ||
|
||
const snackbarPositions: SnackbarPosition[] = [ | ||
'top-center', | ||
'top-left', | ||
'top-right', | ||
'bottom-center', | ||
'bottom-left', | ||
'bottom-right', | ||
]; | ||
|
||
const snackbarTypes: SnackbarType[] = [ | ||
SnackbarType.disruptive, | ||
SnackbarType.warning, | ||
SnackbarType.positive, | ||
SnackbarType.neutral, | ||
]; | ||
|
||
const serveSnacks = (position: SnackbarPosition) => { | ||
const type: SnackbarType = | ||
snackbarTypes[Math.floor(Math.random() * snackbarTypes.length)]; | ||
snack.serve({ | ||
content: | ||
'Body 2 is used in this snackbar. This should be straight forward but can wrap up to two lines if needed.', | ||
type, | ||
position, | ||
closable: !Math.round(Math.random()), | ||
}); | ||
snackIndex += 1; | ||
}; | ||
|
||
return ( | ||
<div> | ||
<h1>Snackbar</h1> | ||
{snackbarPositions.map((position) => ( | ||
<DefaultButton | ||
key={position} | ||
text={`Serve snack ${position.split('-').join(' ')}`} | ||
onClick={() => serveSnacks(position)} | ||
size={ButtonSize.Small} | ||
/> | ||
))} | ||
<DefaultButton | ||
text={`Serve snack with action`} | ||
onClick={() => { | ||
snack.serve({ | ||
content: | ||
'Body 2 is used in this snackbar. This should be straight forward but can wrap up to two lines if needed.', | ||
actionButtonProps: { | ||
text: 'Action', | ||
onClick: () => { | ||
console.log('hi i was clicked'); | ||
}, | ||
}, | ||
}); | ||
}} | ||
size={ButtonSize.Small} | ||
/> | ||
<SnackbarContainer /> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import React from 'react'; | ||
import MatchMediaMock from 'jest-matchmedia-mock'; | ||
import { snack, SnackbarContainer } from './'; | ||
import { | ||
act, | ||
createEvent, | ||
fireEvent, | ||
render, | ||
RenderResult, | ||
} from '@testing-library/react'; | ||
import { IconName } from '../Icon'; | ||
|
||
let matchMedia: any; | ||
|
||
describe('Snackbar', () => { | ||
let wrapper: RenderResult; | ||
const content = 'This is a snackbar'; | ||
|
||
beforeAll(() => { | ||
matchMedia = new MatchMediaMock(); | ||
}); | ||
afterEach(() => { | ||
matchMedia.clear(); | ||
}); | ||
|
||
beforeEach(() => { | ||
wrapper = render(<SnackbarContainer />); | ||
}); | ||
|
||
test('test snack.serve', async () => { | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
await act(async () => { | ||
snack.serve({ | ||
content, | ||
}); | ||
await new Promise((r) => setTimeout(r, 300)); | ||
expect(wrapper.queryByText(content)).not.toBe(null); | ||
await new Promise((r) => setTimeout(r, 3200)); | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
}); | ||
}); | ||
|
||
test('test snack.serveNeutral', async () => { | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
await act(async () => { | ||
snack.serveNeutral({ | ||
content, | ||
}); | ||
await new Promise((r) => setTimeout(r, 300)); | ||
expect(wrapper.queryByText(content)).not.toBe(null); | ||
await new Promise((r) => setTimeout(r, 3200)); | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
}); | ||
}); | ||
|
||
test('test snack.serveDisruptive', async () => { | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
await act(async () => { | ||
snack.serveDisruptive({ | ||
content, | ||
}); | ||
await new Promise((r) => setTimeout(r, 300)); | ||
expect(wrapper.queryByText(content)).not.toBe(null); | ||
await new Promise((r) => setTimeout(r, 3200)); | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
}); | ||
}); | ||
|
||
test('test snack.servePositive', async () => { | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
await act(async () => { | ||
snack.servePositive({ | ||
content, | ||
}); | ||
await new Promise((r) => setTimeout(r, 300)); | ||
expect(wrapper.queryByText(content)).not.toBe(null); | ||
await new Promise((r) => setTimeout(r, 3200)); | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
}); | ||
}); | ||
|
||
test('test snack.serveWarning', async () => { | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
await act(async () => { | ||
snack.serveWarning({ | ||
content, | ||
}); | ||
}); | ||
}); | ||
|
||
test('test snack closable', async () => { | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
const onClose = jest.fn(); | ||
await act(async () => { | ||
snack.serveWarning({ | ||
content, | ||
closable: true, | ||
onClose, | ||
}); | ||
await new Promise((r) => setTimeout(r, 300)); | ||
expect(wrapper.queryByText(content)).not.toBe(null); | ||
await new Promise((r) => setTimeout(r, 3200)); | ||
expect(wrapper.queryByText(content)).not.toBe(null); | ||
const closeButton = wrapper.queryByRole('button'); | ||
const event = createEvent.click(closeButton); | ||
fireEvent(closeButton, event); | ||
expect(onClose).toBeCalled(); | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
}); | ||
}); | ||
|
||
test('test snack icon', async () => { | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
await act(async () => { | ||
snack.serve({ | ||
content, | ||
icon: IconName.mdiHomeCity, | ||
}); | ||
await new Promise((r) => setTimeout(r, 300)); | ||
expect(wrapper.queryByText(content)).not.toBe(null); | ||
await new Promise((r) => setTimeout(r, 3200)); | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
}); | ||
}); | ||
|
||
test('test snack action', async () => { | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
const onClick = jest.fn(); | ||
await act(async () => { | ||
snack.serve({ | ||
content, | ||
actionButtonProps: { | ||
text: 'Button', | ||
onClick, | ||
}, | ||
}); | ||
await new Promise((r) => setTimeout(r, 300)); | ||
const actionButton = wrapper.queryByRole('button'); | ||
console.log(actionButton); | ||
const event = createEvent.click(actionButton); | ||
fireEvent(actionButton, event); | ||
expect(onClick).toBeCalled(); | ||
await new Promise((r) => setTimeout(r, 3200)); | ||
expect(wrapper.queryByText(content)).toBe(null); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React, { FC } from 'react'; | ||
import { SnackbarProps, SnackbarType } from './Snackbar.types'; | ||
import { classNames } from '../../shared/utilities'; | ||
import { Icon, IconName } from '../Icon'; | ||
import { NeutralButton } from '../Button'; | ||
|
||
import styles from './snackbar.module.scss'; | ||
|
||
export const Snackbar: FC<SnackbarProps> = ({ | ||
content, | ||
icon, | ||
type = SnackbarType.neutral, | ||
closable, | ||
onClose, | ||
style, | ||
className, | ||
closeIcon = IconName.mdiClose, | ||
closeButtonProps, | ||
actionButtonProps, | ||
}) => { | ||
const snackbarClasses: string = classNames([ | ||
styles.snackbar, | ||
className, | ||
{ [styles.neutral]: type === SnackbarType.neutral }, | ||
{ [styles.positive]: type === SnackbarType.positive }, | ||
{ [styles.warning]: type === SnackbarType.warning }, | ||
{ [styles.disruptive]: type === SnackbarType.disruptive }, | ||
]); | ||
|
||
const messageClasses: string = classNames([styles.message, 'body2']); | ||
|
||
const getIconName = (): IconName => { | ||
if (icon) { | ||
return icon; | ||
} | ||
switch (type) { | ||
case SnackbarType.disruptive: | ||
case SnackbarType.neutral: | ||
return IconName.mdiInformation; | ||
case SnackbarType.positive: | ||
return IconName.mdiCheckCircle; | ||
case SnackbarType.warning: | ||
return IconName.mdiAlert; | ||
} | ||
}; | ||
|
||
return ( | ||
<div className={snackbarClasses} style={style} role="alert"> | ||
<Icon path={getIconName()} className={styles.icon} /> | ||
<div className={messageClasses}>{content}</div> | ||
{actionButtonProps && <NeutralButton {...actionButtonProps} />} | ||
{closable && ( | ||
<NeutralButton | ||
icon={closeIcon} | ||
ariaLabel={'Close'} | ||
onClick={onClose} | ||
{...closeButtonProps} | ||
/> | ||
)} | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import React from 'react'; | ||
import { IconName } from '../Icon'; | ||
import { ButtonProps } from '../Button'; | ||
import { | ||
serve, | ||
serveDisruptive, | ||
serveNeutral, | ||
servePositive, | ||
serveWarning, | ||
} from './snack'; | ||
|
||
export enum SnackbarType { | ||
neutral = 'neutral', | ||
positive = 'positive', | ||
warning = 'warning', | ||
disruptive = 'disruptive', | ||
} | ||
|
||
export type CloseButtonProps = Omit<ButtonProps, 'onClick' | 'icon'>; | ||
|
||
export type SnackbarPosition = | ||
| 'top-center' | ||
| 'top-left' | ||
| 'top-right' | ||
| 'bottom-center' | ||
| 'bottom-left' | ||
| 'bottom-right'; | ||
|
||
export interface SnackbarProps { | ||
/** | ||
* Content of the snackbar | ||
*/ | ||
content: string; | ||
/** | ||
* Unique id of the snackbar | ||
*/ | ||
id?: string; | ||
/** | ||
* Type of the snackbar | ||
* @default SnackbarType.neutral | ||
*/ | ||
type?: SnackbarType; | ||
/** | ||
* Custom icon for the snackbar | ||
* @default IconName.mdiInformation | IconName.mdiCheckCircle | IconName.mdiAlert | ||
*/ | ||
icon?: IconName; | ||
/** | ||
* Custom style for the snackbar | ||
*/ | ||
style?: React.CSSProperties; | ||
/** | ||
* Custom classname for the snackbar | ||
*/ | ||
className?: string; | ||
/** | ||
* If the snackbar is closable or not | ||
*/ | ||
closable?: boolean; | ||
/** | ||
* Callback fired on close of the snackbar | ||
*/ | ||
onClose?: () => void; | ||
/** | ||
* Icon for the close button | ||
* @default IconName.mdiClose | ||
*/ | ||
closeIcon?: IconName; | ||
/** | ||
* Custom props for the close button | ||
*/ | ||
closeButtonProps?: CloseButtonProps; | ||
/** | ||
* | ||
*/ | ||
actionButtonProps?: ButtonProps; | ||
/** | ||
* Duration for which the snackbar is shown | ||
* @default 3000 | ||
*/ | ||
duration?: number; | ||
/** | ||
* Position of the snackbar | ||
* @default top-center | ||
*/ | ||
position?: SnackbarPosition; | ||
} | ||
|
||
export interface SnackbarContainerProps { | ||
/** | ||
* Parent container on which the snackbar need to | ||
* be rendered on | ||
*/ | ||
parent?: HTMLElement; | ||
} | ||
|
||
export interface ISnack { | ||
serve: (props: SnackbarProps) => void; | ||
serveNeutral: (props: SnackbarProps) => void; | ||
servePositive: (props: SnackbarProps) => void; | ||
serveWarning: (props: SnackbarProps) => void; | ||
serveDisruptive: (props: SnackbarProps) => void; | ||
} |
Oops, something went wrong.