Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
'**lib/core/**/*.js',
'!**lib/components/**/*.mock.js',
'!**lib/components/**/*.story.js',
'!**lib/components/**/*.styles.js',
'!**lib/styles/**/*.js',
'!**/node_modules/**',
],
Expand Down
139 changes: 139 additions & 0 deletions lib/components/molecules/Popover/Popover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// @flow
/**
*
* Popover
*
*/
import React, { PureComponent, createRef } from 'react';
import styled from 'styled-components';
import classnames from 'classnames';

import styles from './Popover.style';
import type { Props, State } from './types';

class Popover extends PureComponent<Props, State> {
static defaultProps = {
isVisible: false,
hidePopoverCloseBtn: false,
};

/**
* showPopover: State to manage the visibility of Popover content
* default is false which can be changed by passing isVisible prop
*/
state: State = {
showPopover: false,
};

/**
* wrapperRef: It represents the wrapper containing the element around
* which popover needs to be wrapped and the popover content managed by
* showPopover state or isVisible prop
*/
wrapperRef: { current: null | HTMLDivElement } = createRef();

/**
* @componentDidMount
* Add event listener to handle click outside the wrapper to that popover
* gets closed and setting showPopover state on the basis of prop if passed
*/
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside, false);

const { isVisible } = this.props;
if (isVisible) {
this.setState({
showPopover: isVisible,
});
}
}

/**
* @componentWillUnmount
* Remove all event listener on component un-mounting
*/
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside, false);
}

/**
* @handleClickOutside
* @param {MouseEvent} e
* This function handles the outside click from the wrapper
* to close the popover if open
*/
handleClickOutside = (e: MouseEvent) => {
const el = e.target;
if (
this.wrapperRef &&
this.wrapperRef.current &&
el instanceof Node &&
!this.wrapperRef.current.contains(el)
) {
this.setState({
showPopover: false,
});
}
};

/**
* @handleClick
* This function handles the click of child element (like button/anchor etc)
* on which popover needs to be open
*/
handleClick = () => {
this.setState({
showPopover: true,
});
};

/**
* @close
* This function closes the popover on when cross button
* inside the popover gets clicked
*/
close = () => {
this.setState({
showPopover: false,
});
};

render() {
const { popOverHeader, children, hidePopoverCloseBtn, className, trigger } = this.props;
const { showPopover } = this.state;

/**
* Cloning of element needs to be done so that handleClick function can
* be wrapped around element without wrapping it around another element
* or creating an extra DOM node
*/
const element = trigger && React.cloneElement(trigger, { onClick: this.handleClick });

return (
<div
role="presentation"
ref={this.wrapperRef}
className={classnames('popover-wrap', className)}
>
{element}
{showPopover && (
<div role="dialog" className="popover">
{!hidePopoverCloseBtn && (
<button className="popover__close" onClick={this.close}>
X
</button>
)}
{popOverHeader && <h3 className="popover__header">{popOverHeader}</h3>}
<div className="popover__body">{children}</div>
</div>
)}
</div>
);
}
}

export default styled(Popover)`
${styles};
`;

export { Popover as PopoverVanilla };
29 changes: 29 additions & 0 deletions lib/components/molecules/Popover/Popover.mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const defaultVisiblePopover = {
className: 'example',
popOverHeader: `This is a popover header text`,
trigger: <button>Click to Open</button>,
isVisible: true,
};

const hiddenPopover = {
className: 'example',
popOverHeader: `This is a popover header text`,
trigger: <button>Click to Open</button>,
isVisible: false,
};

const ListPopover = {
className: 'example',
popOverHeader: `This is a popover header text`,
trigger: <button>Click to Open</button>,
};

const noCloseBtnPopover = {
className: 'example',
popOverHeader: `This is a popover header text`,
trigger: <button>Click to Open</button>,
isVisible: false,
hidePopoverCloseBtn: true,
};

export { defaultVisiblePopover, hiddenPopover, ListPopover, noCloseBtnPopover };
83 changes: 83 additions & 0 deletions lib/components/molecules/Popover/Popover.story.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
// import { action } from '@storybook/addon-actions';
import List from '../List';
import {
defaultVisiblePopover,
hiddenPopover,
ListPopover,
noCloseBtnPopover,
} from './Popover.mock';

// Import Styled Component to showcase variations
import Popover, { PopoverVanilla } from '.';

storiesOf('Molecules', module).addWithChapters('Popover', {
chapters: [
{
sections: [
{
sectionFn: () => (
<PopoverVanilla
{...hiddenPopover}
className={`hide-default-sample ${hiddenPopover.className}`}
/>
),
options: {
showSource: true,
allowSourceToggling: true,
showPropTables: true,
allowPropTablesToggling: true,
},
},
],
},
{
title: 'Popover Variations',
sections: [
{
title: 'By default Visible variation',
sectionFn: () => (
<Popover {...defaultVisiblePopover}>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
</Popover>
),
},
{
title: 'By default Invisible variation',
sectionFn: () => (
<Popover {...hiddenPopover}>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
</Popover>
),
},
{
title: 'List Component as child variation',
sectionFn: () => (
<Popover {...ListPopover}>
<div>
<p>This text is a random text</p>
<List ListType="ol">
<li>This is ordered list item, content of tab 1.</li>
<li>This is ordered list item, content of tab 1.</li>
</List>
<p>This is another random text</p>
</div>
</Popover>
),
},
{
title: 'Optional close button variation',
sectionFn: () => (
<Popover {...noCloseBtnPopover}>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
</Popover>
),
},
],
},
],
});
15 changes: 15 additions & 0 deletions lib/components/molecules/Popover/Popover.style.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { css } from 'styled-components';

export default css`
.popover {
border: 1px solid #000;

&__close {
border: none;
cursor: pointer;
float: right;
}
}

${props => (props.inheritedStyles ? props.inheritedStyles : '')};
`;
2 changes: 2 additions & 0 deletions lib/components/molecules/Popover/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './Popover';
export { PopoverVanilla } from './Popover';
63 changes: 63 additions & 0 deletions lib/components/molecules/Popover/tests/Popover.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { shallow, mount } from 'enzyme';
import { PopoverVanilla } from '../index';

const interactivePopover = {
className: 'example',
popOverHeader: 'This is a popover header text',
trigger: <button id="popover-trigger">Click to Open</button>,
};

describe('<Popover /> Rendering', () => {
let PopoverComponent;
beforeEach(() => {
PopoverComponent = shallow(<PopoverVanilla>Test</PopoverVanilla>);
});

test('should render correctly', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test cases needs to improve, you have a open/close functionality too. Test it here!

expect(PopoverComponent).toMatchSnapshot();
});
});

describe('Popover functional behavior', () => {
let PopoverComponent;

test('Should have Close Button By Default and can be closed', () => {
PopoverComponent = mount(
<PopoverVanilla {...interactivePopover} isVisible>
Test
</PopoverVanilla>
);
expect(PopoverComponent.find('.popover__close').length).toEqual(1);
PopoverComponent = mount(
<PopoverVanilla {...interactivePopover} hidePopoverCloseBtn>
Test
</PopoverVanilla>
);
expect(PopoverComponent.find('.popover__close').length).toEqual(0);
});

test('Should open By Default when isVisible set to true', () => {
PopoverComponent = mount(
<PopoverVanilla {...interactivePopover} isVisible>
Test
</PopoverVanilla>
);
expect(PopoverComponent.find('.popover__header').length).toEqual(1);
});

test('Should open and close on Trigger and X button click', () => {
PopoverComponent = mount(
<div>
<p className="test">Test Content</p>
<PopoverVanilla {...interactivePopover}>Test</PopoverVanilla>
</div>
);
// Test Default Open through trigger
PopoverComponent.find('#popover-trigger').simulate('click');
expect(PopoverComponent.find('.popover__header').length).toEqual(1);
// Test Close Popover when clicking outside through trigger the popover
// Test Close through X button
PopoverComponent.find('.popover__close').simulate('click');
expect(PopoverComponent.find('.popover__header').length).toEqual(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Popover /> Rendering should render correctly 1`] = `
<div
className="popover-wrap"
role="presentation"
/>
`;
15 changes: 15 additions & 0 deletions lib/components/molecules/Popover/types/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @flow
import type { Node } from 'react';

export type Props = {
popOverHeader?: Node | string,
children: Node | string,
isVisible?: boolean,
hidePopoverCloseBtn?: boolean,
className?: string,
trigger: React$Element<*>,
};

export type State = {
showPopover: boolean,
};
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Textarea, { TextareaVanilla } from './components/atoms/Textarea';
import Form from './components/molecules/Form';
import InputChoice, { InputChoiceVanilla } from './components/molecules/InputChoice';
import Modal, { ModalVanilla } from './components/molecules/Modal';
import Popover, { PopoverVanilla } from './components/molecules/Popover';

/**
* Theme
Expand Down Expand Up @@ -50,5 +51,7 @@ export {
InputChoiceVanilla,
Modal,
ModalVanilla,
Popover,
PopoverVanilla,
Theme,
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"scripts": {
"test": "cross-env NODE_ENV=test jest --config ./config/jest.config.js",
"test:updateSnapshot": "yarn run test --updateSnapshot",
"test:watch": "yarn run test --watch",
"test:watch": "yarn run test --watch --verbose",
"audit": "yarn run build -- --report",
"clean:dist": "rimraf dist",
"build": "yarn run clean:dist && cross-env BABEL_ENV=production rollup --config rollup.config.js",
Expand Down