Skip to content

Commit

Permalink
typescript SnackbarProvider, SnackbarContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
iamhosseindhv committed Mar 8, 2020
1 parent 668d9c5 commit 8d5d13d
Show file tree
Hide file tree
Showing 14 changed files with 437 additions and 504 deletions.
24 changes: 21 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@
],
"max-len": 0,
"object-curly-newline": 0,
"import/extensions": 0,
"react/prop-types": 0,
"react/destructuring-assignment": 0,
"react/jsx-closing-bracket-location": 0,
"react/forbid-prop-types": 0,
"react/jsx-closing-bracket-location": 0,
"react/sort-comp": 0,
"react/require-default-props": 0,
"react/jsx-filename-extension": [
1,
{
"extensions": [
".js",
".jsx"
".jsx",
".ts",
".tsx"
]
}
],
Expand All @@ -60,7 +64,21 @@
"parserOptions": {
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"@typescript-eslint/ban-ts-ignore": "off"
}
}
],
"settings": {
"import/resolver": {
"node": {
"extensions": [
".js",
".ts",
".tsx"
]
}
}
]
}
}
538 changes: 217 additions & 321 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@
"@babel/preset-env": "^7.8.7",
"@babel/preset-react": "^7.8.3",
"@babel/preset-typescript": "^7.8.3",
"@material-ui/core": "^3.2.0",
"@material-ui/core": "^4.9.5",
"@types/classnames": "^2.2.10",
"@types/node": "^13.9.0",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"@types/react-is": "^16.7.1",
"@typescript-eslint/eslint-plugin": "^2.22.0",
"@typescript-eslint/parser": "^2.22.0",
"babel-eslint": "^8.2.6",
Expand Down
35 changes: 16 additions & 19 deletions src/SnackbarContainer.js → src/SnackbarContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import { makeStyles } from '@material-ui/core/styles';
import { SNACKBAR_INDENTS } from './utils/constants';
import { SnackbarProviderProps } from '.';

const styles = theme => ({
const useStyle = makeStyles(theme => ({
root: {
boxSizing: 'border-box',
display: 'flex',
Expand Down Expand Up @@ -43,12 +43,19 @@ const styles = theme => ({
transform: 'translateX(-50%)',
},
},
});
}));

const SnackbarContainer = React.memo((props) => {
const {
classes, className, anchorOrigin, dense, ...other
} = props;

type SnackbarContainerProps = Required<{
className: string;
children: JSX.Element | JSX.Element[];
dense: SnackbarProviderProps['dense'];
anchorOrigin: SnackbarProviderProps['anchorOrigin'];
}>

const SnackbarContainer: React.FC<SnackbarContainerProps> = (props) => {
const classes = useStyle();
const { className, anchorOrigin, dense, ...other } = props;

const combinedClassname = classNames(
classes.root,
Expand All @@ -63,16 +70,6 @@ const SnackbarContainer = React.memo((props) => {
return (
<div className={combinedClassname} {...other} />
);
});

SnackbarContainer.propTypes = {
classes: PropTypes.object.isRequired,
className: PropTypes.string,
dense: PropTypes.bool.isRequired,
anchorOrigin: PropTypes.shape({
horizontal: PropTypes.oneOf(['left', 'center', 'right']).isRequired,
vertical: PropTypes.oneOf(['top', 'bottom']).isRequired,
}),
};

export default withStyles(styles)(SnackbarContainer);
export default React.memo(SnackbarContainer);
3 changes: 0 additions & 3 deletions src/SnackbarContext.js

This file was deleted.

5 changes: 5 additions & 0 deletions src/SnackbarContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';
import { ProviderContext } from '.';

// @ts-ignore
export default React.createContext<ProviderContext>();
88 changes: 46 additions & 42 deletions src/SnackbarProvider.js → src/SnackbarProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,43 @@ import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';
import Slide from '@material-ui/core/Slide';
import SnackbarContext from './SnackbarContext';
import { MESSAGES, defaultIconVariant, originKeyExtractor, allClasses, REASONS } from './utils/constants';
import { MESSAGES, originKeyExtractor, allClasses, REASONS } from './utils/constants';
import SnackbarItem from './SnackbarItem';
import SnackbarContainer from './SnackbarContainer';
import warning from './utils/warning';
import defaultIconVariants from './utils/defaultIconVariants';
import { SnackbarProviderProps, SnackbarKey, SnackbarMessage, OptionsObject, RequiredBy, ProviderContext } from '.';


/**
* Omit SnackbarContainer class keys that are not needed for SnakcbarItem
* Omit SnackbarContainer class keys that are not needed for SnackbarItem
*/
const getClasses = classes => (
const getClasses = (classes: { [key: string]: string }): { [key: string]: string } => (
// @ts-ignore
Object.keys(classes).filter(key => !allClasses.container[key]).reduce((obj, key) => ({
...obj,
[key]: classes[key],
}), {})
);

class SnackbarProvider extends Component {
constructor(props) {
type Reducer = (state: State) => State
type SnacksByPosition = { [key: string]: Snack[] }

export interface Snack extends RequiredBy<OptionsObject, 'key' | 'variant' | 'anchorOrigin'> {
message: SnackbarMessage;
open: boolean;
entered: boolean;
requestClose: boolean;
}

interface State {
snacks: Snack[];
queue: Snack[];
contextValue: ProviderContext;
}

class SnackbarProvider extends Component<SnackbarProviderProps, State> {
constructor(props: SnackbarProviderProps) {
super(props);
this.state = {
snacks: [],
Expand All @@ -34,20 +53,12 @@ class SnackbarProvider extends Component {

/**
* Adds a new snackbar to the queue to be presented.
* @param {string} message - text of the notification
* @param {object} options - additional options for the snackbar we want to enqueue.
* We can pass Material-ui Snackbar props for individual customisation.
* @param {string} options.key
* @param {string} options.variant - type of the snackbar. default value is 'default'.
* can be: (default, success, error, warning, info)
* @param {bool} options.persist
* @param {bool} options.preventDuplicate
* @returns generated or user defined key referencing the new snackbar or null
*/
enqueueSnackbar = (message, { key, preventDuplicate, ...options } = {}) => {
const userSpecifiedKey = key || key === 0;
const id = userSpecifiedKey ? key : new Date().getTime() + Math.random();
const snack = {
* Returns generated or user defined key referencing the new snackbar or null
*/
enqueueSnackbar = (message: SnackbarMessage, { key, preventDuplicate, ...options }: OptionsObject = {}): SnackbarKey => {
const hasSpecifiedKey = key || key === 0;
const id = hasSpecifiedKey ? (key as SnackbarKey) : new Date().getTime() + Math.random();
const snack: Snack = {
key: id,
...options,
message,
Expand All @@ -64,8 +75,8 @@ class SnackbarProvider extends Component {

this.setState((state) => {
if ((preventDuplicate === undefined && this.props.preventDuplicate) || preventDuplicate) {
const compareFunction = item => (
userSpecifiedKey ? item.key === key : item.message === message
const compareFunction = (item: Snack): boolean => (
hasSpecifiedKey ? item.key === key : item.message === message
);

const inQueue = state.queue.findIndex(compareFunction) > -1;
Expand All @@ -88,7 +99,7 @@ class SnackbarProvider extends Component {
* Reducer: Display snack if there's space for it. Otherwise, immediately
* begin dismissing the oldest message to start showing the new one.
*/
handleDisplaySnack = (state) => {
handleDisplaySnack: Reducer = (state) => {
const { snacks } = state;
if (snacks.length >= this.props.maxSnack) {
return this.handleDismissOldest(state);
Expand All @@ -99,7 +110,7 @@ class SnackbarProvider extends Component {
/**
* Reducer: Display items (notifications) in the queue if there's space for them.
*/
processQueue = (state) => {
processQueue: Reducer = (state) => {
const { queue, snacks } = state;
if (queue.length > 0) {
return {
Expand All @@ -119,7 +130,7 @@ class SnackbarProvider extends Component {
* Note 2: If the oldest message has not yet entered the screen, only a request to close the
* snackbar is made. Once it entered the screen, it will be immediately dismissed.
*/
handleDismissOldest = (state) => {
handleDismissOldest: Reducer = (state) => {
if (state.snacks.some(item => !item.open || item.requestClose)) {
return state;
}
Expand Down Expand Up @@ -165,7 +176,7 @@ class SnackbarProvider extends Component {
/**
* Set the entered state of the snackbar with the given key.
*/
handleEnteredSnack = (node, isAppearing, key) => {
handleEnteredSnack: SnackbarProviderProps['onEntered'] = (node, isAppearing, key) => {
if (this.props.onEntered) {
this.props.onEntered(node, isAppearing, key);
}
Expand All @@ -179,11 +190,8 @@ class SnackbarProvider extends Component {

/**
* Hide a snackbar after its timeout.
* @param {object} event - The event source of the callback
* @param {string} reason - can be timeout, clickaway
* @param {number} key - id of the snackbar we want to hide
*/
handleCloseSnack = (event, reason, key) => {
handleCloseSnack: SnackbarProviderProps['onClose'] = (event, reason, key) => {
if (this.props.onClose) {
this.props.onClose(event, reason, key);
}
Expand All @@ -207,12 +215,11 @@ class SnackbarProvider extends Component {

/**
* Close snackbar with the given key
* @param {number} key - id of the snackbar we want to hide
*/
closeSnackbar = (key) => {
closeSnackbar: ProviderContext['closeSnackbar'] = (key) => {
// call individual snackbar onClose callback passed through options parameter
const toBeClosed = this.state.snacks.find(item => item.key === key);
if (toBeClosed && toBeClosed.onClose) {
if (key && toBeClosed && toBeClosed.onClose) {
toBeClosed.onClose(null, REASONS.INSTRUCTED, key);
}

Expand All @@ -225,10 +232,8 @@ class SnackbarProvider extends Component {
* gets called. We remove the hidden snackbar from state and then display notifications
* waiting in the queue (if any). If after this process the queue is not empty, the
* oldest message is dismissed.
* @param {number} key - id of the snackbar we want to remove
* @param {object} event - The event source of the callback
*/
handleExitedSnack = (event, key) => {
handleExitedSnack: SnackbarProviderProps['onExited'] = (event, key) => {
this.setState((state) => {
const newState = this.processQueue({
...state,
Expand All @@ -247,11 +252,11 @@ class SnackbarProvider extends Component {
}
};

render() {
const { classes, children, maxSnack, dense, variant, domRoot, ...props } = this.props;
render(): JSX.Element {
const { classes, children, dense, domRoot, maxSnack, variant, ...props } = this.props;
const { contextValue } = this.state;

const categ = this.state.snacks.reduce((acc, current) => {
const categ = this.state.snacks.reduce<SnacksByPosition>((acc, current) => {
const category = originKeyExtractor(current.anchorOrigin);
const existingOfCategory = acc[category] || [];
return {
Expand All @@ -261,7 +266,7 @@ class SnackbarProvider extends Component {
}, {});

const iconVariant = {
...defaultIconVariant,
...defaultIconVariants,
...this.props.iconVariant,
};

Expand Down Expand Up @@ -301,6 +306,7 @@ class SnackbarProvider extends Component {
// eslint-disable-next-line
const Element = typeof Element === 'undefined' ? function () { } : Element;


SnackbarProvider.propTypes = {
/**
* Most of the time, this is your App. every component from this point onward
Expand All @@ -323,9 +329,7 @@ SnackbarProvider.propTypes = {
* Used to easily display different variant of snackbars. When passed to `SnackbarProvider`
* all snackbars inherit the `variant`, unless you override it in `enqueueSnackbar` options.
*/
variant: PropTypes.oneOf(
['default', 'error', 'success', 'warning', 'info'],
),
variant: PropTypes.oneOf(['default', 'error', 'success', 'warning', 'info']),
/**
* Ignores displaying multiple snackbars with the same `message`
*/
Expand Down
Loading

0 comments on commit 8d5d13d

Please sign in to comment.