diff --git a/.eslintrc b/.eslintrc
index cce1bf41..b1374b34 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -54,6 +54,7 @@
"no-restricted-imports": 0,
"class-methods-use-this": 0,
"no-use-before-define": 0,
+ "object-curly-newline": 0,
"react/destructuring-assignment": 0,
"react/no-access-state-in-setstate": "error",
"react/jsx-filename-extension": 0,
diff --git a/config/styleguide.config.json b/config/styleguide.config.json
index fc1bc999..867765b9 100644
--- a/config/styleguide.config.json
+++ b/config/styleguide.config.json
@@ -39,6 +39,7 @@
"description": "React components",
"components": [
"Avatar",
+ "AvatarStack",
"BannerNotification",
"BannerNotifications",
"Button",
diff --git a/source/components/Avatar/index.js b/source/components/Avatar/index.js
index 6918d1cb..38b687c1 100644
--- a/source/components/Avatar/index.js
+++ b/source/components/Avatar/index.js
@@ -2,16 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import AvatarImage from './AvatarImage';
import Badge from './Badge';
+import { getAvatarImage } from './utils';
import './styles.scss';
-const getAvatarImage = (href, alt, src) => {
- const avatarImage = ;
- return href ? {avatarImage} : avatarImage;
-};
-
const Avatar = ({
alt,
badge,
diff --git a/source/components/Avatar/utils.js b/source/components/Avatar/utils.js
new file mode 100644
index 00000000..ffd10468
--- /dev/null
+++ b/source/components/Avatar/utils.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+import AvatarImage from './AvatarImage';
+
+// eslint-disable-next-line
+export function getAvatarImage(href, alt, src) {
+ const avatarImage = ;
+ return href ? {avatarImage} : avatarImage;
+}
diff --git a/source/components/AvatarStack/README.md b/source/components/AvatarStack/README.md
new file mode 100644
index 00000000..4def761b
--- /dev/null
+++ b/source/components/AvatarStack/README.md
@@ -0,0 +1,35 @@
+Defaults:
+```js
+
+```
+
+With maxStackSize
+
+```js
+
+```
+
+With hidden overflow
+
+```js
+
+```
+
+With overridden counter
+
+```js
+
+```
diff --git a/source/components/AvatarStack/__snapshots__/index.spec.js.snap b/source/components/AvatarStack/__snapshots__/index.spec.js.snap
new file mode 100644
index 00000000..b74fc790
--- /dev/null
+++ b/source/components/AvatarStack/__snapshots__/index.spec.js.snap
@@ -0,0 +1,100 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AvatarStack renders when there are less avatars than specified by maxStackSize 1`] = `
+
+`;
+
+exports[`AvatarStack renders when there are more avatars than specified by maxStackSize 1`] = `
+
+`;
+
+exports[`AvatarStack renders with default props 1`] = `
+
+`;
+
+exports[`AvatarStack renders with hideOverflow set to true 1`] = `
+
+`;
+
+exports[`AvatarStack renders with overrideCount 1`] = `
+
+`;
diff --git a/source/components/AvatarStack/index.js b/source/components/AvatarStack/index.js
new file mode 100644
index 00000000..ca5117dc
--- /dev/null
+++ b/source/components/AvatarStack/index.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import Avatar from '../Avatar';
+
+import './styles.scss';
+
+/* eslint-disable react/no-array-index-key */
+const AvatarStack = ({ avatars, overrideCount, maxStackSize, hideOverflow, className }) => {
+ const count = overrideCount || avatars.length;
+ const overflow = !hideOverflow && (count > maxStackSize ? count - maxStackSize : 0);
+
+ return (
+
+ {
+ avatars
+ .slice(0, maxStackSize)
+ .map(({ src, alt, link, badge = '' }, index) => (
+
+ ))
+ }
+ {
+ overflow && (
+
+ {`+${overflow}`}
+
+ )
+ }
+
+ );
+};
+
+AvatarStack.propTypes = {
+ /* An array of `Avatar` props */
+ avatars: PropTypes.arrayOf(PropTypes.shape()).isRequired,
+ /* Additional class name */
+ className: PropTypes.string,
+ /* Flag to hide overflow */
+ hideOverflow: PropTypes.bool,
+ /* Max stack size */
+ maxStackSize: PropTypes.number,
+ /* If specified, it will be used instead of `avatars.length` */
+ overrideCount: PropTypes.number,
+};
+
+AvatarStack.defaultProps = {
+ className: '',
+ hideOverflow: false,
+ maxStackSize: 5,
+ overrideCount: 0,
+};
+
+export default AvatarStack;
diff --git a/source/components/AvatarStack/index.spec.js b/source/components/AvatarStack/index.spec.js
new file mode 100644
index 00000000..b3dfa08c
--- /dev/null
+++ b/source/components/AvatarStack/index.spec.js
@@ -0,0 +1,48 @@
+import { shallow } from 'enzyme';
+import React from 'react';
+import merge from 'lodash/merge';
+
+import AvatarStack from './index';
+
+const defaultProps = {
+ avatars: [
+ { src: 'some.url', alt: 'User', link: 'some.link', badge: 'admin' },
+ { src: 'some.other.url', alt: 'Other user', link: 'some.other.link' },
+ ],
+};
+
+function renderComponent(props) {
+ const computedProps = merge({}, defaultProps, props);
+
+ return shallow();
+}
+
+test('AvatarStack renders with default props', () => {
+ const wrapper = renderComponent();
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('AvatarStack renders with overrideCount', () => {
+ const wrapper = renderComponent({ overrideCount: 1 });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('AvatarStack renders when there are less avatars than specified by maxStackSize', () => {
+ const wrapper = renderComponent({ maxStackSize: 3 });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('AvatarStack renders when there are more avatars than specified by maxStackSize', () => {
+ const wrapper = renderComponent({ maxStackSize: 1 });
+
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('AvatarStack renders with hideOverflow set to true', () => {
+ const wrapper = renderComponent({ hideOverflow: true, maxStackSize: 1 });
+
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/source/components/AvatarStack/styles.scss b/source/components/AvatarStack/styles.scss
new file mode 100644
index 00000000..d877cec0
--- /dev/null
+++ b/source/components/AvatarStack/styles.scss
@@ -0,0 +1,3 @@
+@import "~design-system/dist/scss/wds-mixins/index.scss";
+@import "~design-system/dist/scss/wds-variables/index.scss";
+@import "~design-system/dist/scss/wds-components/_avatar-stack.scss";
diff --git a/source/components/BannerNotification/README.md b/source/components/BannerNotification/README.md
index 77960c80..6eebaa54 100644
--- a/source/components/BannerNotification/README.md
+++ b/source/components/BannerNotification/README.md
@@ -10,7 +10,7 @@ But it can be rendered with close buttton:
Or with extra HTML:
```js
- alert('Click')}>
+ alert('Click')}>
This is a text with a link
```
diff --git a/source/components/BannerNotification/index.js b/source/components/BannerNotification/index.js
index 828cf51a..2893eb7a 100644
--- a/source/components/BannerNotification/index.js
+++ b/source/components/BannerNotification/index.js
@@ -2,38 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import IconCloseTiny from '../../icons/IconCloseTiny';
-import IconAlertSmall from '../../icons/IconAlertSmall';
-import IconCheckmarkSmall from '../../icons/IconCheckmarkSmall';
-import IconErrorSmall from '../../icons/IconErrorSmall';
-import IconFlagSmall from '../../icons/IconFlagSmall';
-import './styles.scss';
-
-function getIcon(type) {
- switch (type) {
- case ('alert'):
- return ;
- case ('warning'):
- return ;
- case ('success'):
- return ;
- default:
- return ;
- }
-}
+import { getClassName, getIcon } from './utils';
-function getClassName(type) {
- switch (type) {
- case ('alert'):
- return 'wds-alert';
- case ('warning'):
- return 'wds-warning';
- case ('success'):
- return 'wds-success';
- default:
- return 'wds-message';
- }
-}
+import './styles.scss';
/**
* This is a single component used in `BannerNotifications` component.
@@ -48,6 +20,7 @@ const BannerNotification = ({
);
+
BannerNotification.propTypes = {
/** Children to display */
children: PropTypes.node,
diff --git a/source/components/BannerNotification/utils.js b/source/components/BannerNotification/utils.js
new file mode 100644
index 00000000..2fe18df8
--- /dev/null
+++ b/source/components/BannerNotification/utils.js
@@ -0,0 +1,32 @@
+import React from 'react';
+
+import IconAlertSmall from '../../icons/IconAlertSmall';
+import IconCheckmarkSmall from '../../icons/IconCheckmarkSmall';
+import IconErrorSmall from '../../icons/IconErrorSmall';
+import IconFlagSmall from '../../icons/IconFlagSmall';
+
+export function getIcon(type) {
+ switch (type) {
+ case ('alert'):
+ return ;
+ case ('warning'):
+ return ;
+ case ('success'):
+ return ;
+ default:
+ return ;
+ }
+}
+
+export function getClassName(type) {
+ switch (type) {
+ case ('alert'):
+ return 'wds-alert';
+ case ('warning'):
+ return 'wds-warning';
+ case ('success'):
+ return 'wds-success';
+ default:
+ return 'wds-message';
+ }
+}
diff --git a/source/components/index.js b/source/components/index.js
index 81081b37..c09b7c87 100644
--- a/source/components/index.js
+++ b/source/components/index.js
@@ -26,6 +26,7 @@ export { default as FandomContentWell } from './FandomContentWell';
export { default as List } from './List';
// Other UI
export { default as Avatar } from './Avatar';
+export { default as AvatarStack } from './AvatarStack';
export { default as ExpandableText } from './ExpandableText';
export { default as Switch } from './Switch';
export { default as Timeago } from './Timeago';