Skip to content

Commit

Permalink
feat: adds octuple snackbar component (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
ychhabra-eightfold authored Apr 28, 2022
1 parent 3ea58e3 commit 6559c78
Show file tree
Hide file tree
Showing 10 changed files with 649 additions and 0 deletions.
78 changes: 78 additions & 0 deletions src/components/Snackbar/Snackbar.stories.tsx
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>
);
};
147 changes: 147 additions & 0 deletions src/components/Snackbar/Snackbar.test.tsx
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);
});
});
});
62 changes: 62 additions & 0 deletions src/components/Snackbar/Snackbar.tsx
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>
);
};
103 changes: 103 additions & 0 deletions src/components/Snackbar/Snackbar.types.ts
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;
}
Loading

0 comments on commit 6559c78

Please sign in to comment.