Skip to content

Commit

Permalink
feat(UncontrolledCollapse): add UncontrolledCollapse (#1009)
Browse files Browse the repository at this point in the history
  • Loading branch information
sscaff1 authored and TheSharpieOne committed May 14, 2018
1 parent 459df03 commit 355d2b8
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 55 deletions.
23 changes: 14 additions & 9 deletions docs/lib/Components/CollapsePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import PageTitle from '../UI/PageTitle';
import SectionTitle from '../UI/SectionTitle';

import CollapseExample from '../examples/Collapse';
const CollapseExampleSource = require('!!raw!../examples/Collapse');

import CollapseEventsExample from '../examples/CollapseEvents';

const CollapseExampleSource = require('!!raw!../examples/Collapse');
const CollapseEventsExampleSource = require('!!raw!../examples/CollapseEvents');

export default class CollapsePage extends React.Component {
Expand All @@ -19,9 +20,7 @@ export default class CollapsePage extends React.Component {
<CollapseExample />
</div>
<pre>
<PrismCode className="language-jsx">
{CollapseExampleSource}
</PrismCode>
<PrismCode className="language-jsx">{CollapseExampleSource}</PrismCode>
</pre>

<SectionTitle>Properties</SectionTitle>
Expand All @@ -44,17 +43,23 @@ export default class CollapsePage extends React.Component {

<SectionTitle>Events</SectionTitle>
<p>
Use the <code>onEnter</code>, onEntering, onEntered, onExiting and onExited props for callbacks when the
Collapse has finished opening (entering) or closing (exiting).
Use the <code>onEnter</code>, onEntering, onEntered, onExiting and onExited props for
callbacks when the Collapse has finished opening (entering) or closing (exiting).
</p>
<div className="docs-example">
<CollapseEventsExample />
</div>
<pre>
<PrismCode className="language-jsx">
{CollapseEventsExampleSource}
</PrismCode>
<PrismCode className="language-jsx">{CollapseEventsExampleSource}</PrismCode>
</pre>
<SectionTitle>Uncontrolled Collapse</SectionTitle>
<p>
For the most basic use-case, an uncontrolled component can provide the functionality
wanted without the need to manage/control the state of the component.{' '}
<code>UncontrolledCollapse</code> does not require an <code>isOpen</code> prop. Instead
pass a <code>toggler</code> prop. The <code>toggler</code> prop is a string which will run
querySelectorAll to find dom elements which will trigger toggle.
</p>
</div>
);
}
Expand Down
21 changes: 21 additions & 0 deletions docs/lib/examples/CollapseUncontrolled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { UncontrolledCollapse, Button, CardBody, Card } from 'reactstrap';

const Example = () => (
<div>
<Button color="primary" id="toggler" style={{ marginBottom: '1rem' }}>
Toggle
</Button>
<UncontrolledCollapse toggler="#toggler">
<Card>
<CardBody>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nesciunt magni, voluptas debitis
similique porro a molestias consequuntur earum odio officiis natus, amet hic, iste sed
dignissimos esse fuga! Minus, alias.
</CardBody>
</Card>
</UncontrolledCollapse>
</div>
);

export default Example;
18 changes: 7 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
"report-coverage": "coveralls < ./coverage/lcov.info",
"test": "cross-env BABEL_ENV=test react-scripts test --env=jsdom",
"cover": "npm test -- --coverage",
"start": "cross-env BABEL_ENV=webpack webpack-dev-server --config ./webpack.dev.config.js --watch",
"build-docs": "cross-env BABEL_ENV=webpack WEBPACK_BUILD=production webpack --config ./webpack.dev.config.js --progress --colors",
"start":
"cross-env BABEL_ENV=webpack webpack-dev-server --config ./webpack.dev.config.js --watch",
"build-docs":
"cross-env BABEL_ENV=webpack WEBPACK_BUILD=production webpack --config ./webpack.dev.config.js --progress --colors",
"build": "rollup -c",
"prebuild": "cross-env BABEL_ENV=lib-dir babel src --out-dir lib --ignore src/__tests__/",
"postbuild": "node ./scripts/postbuild.js",
Expand All @@ -27,14 +29,7 @@
"type": "git",
"url": "git+ssh://git@github.com/reactstrap/reactstrap.git"
},
"files": [
"LICENSE",
"README.md",
"CHANGELOG.md",
"lib",
"dist",
"src"
],
"files": ["LICENSE", "README.md", "CHANGELOG.md", "lib", "dist", "src"],
"keywords": [
"reactstrap",
"bootstrap",
Expand Down Expand Up @@ -86,7 +81,8 @@
"edgji (https://github.com/edgji)",
"nlrowe (https://github.com/nlrowe)",
"npm-to-cdn-bot (by Forbes Lindesay) (https://github.com/npmcdn-to-unpkg-bot)",
"polmauri (https://github.com/polmauri)"
"polmauri (https://github.com/polmauri)",
"Steven Scaffidi (https://github.com/sscaff1)"
],
"license": "MIT",
"bugs": {
Expand Down
58 changes: 58 additions & 0 deletions src/UncontrolledCollapse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Collapse from './Collapse';
import { findDOMElements, defaultToggleEvents, addMultipleEventListeners } from './utils';

const propTypes = {
toggler: PropTypes.string.isRequired,
toggleEvents: PropTypes.arrayOf(PropTypes.string)
};

const defaultProps = {
toggleEvents: defaultToggleEvents
};

class UncontrolledCollapse extends Component {
constructor(props) {
super(props);

this.togglers = null;
this.removeEventListeners = null;
this.toggle = this.toggle.bind(this);

this.state = {
isOpen: false
};
}

componentDidMount() {
this.togglers = findDOMElements(this.props.toggler);
if (this.togglers.length) {
this.removeEventListeners = addMultipleEventListeners(
this.togglers,
this.toggle,
this.props.toggleEvents
);
}
}

componentWillUnmount() {
if (this.togglers.length && this.removeEventListeners) {
this.removeEventListeners();
}
}

toggle() {
this.setState(({ isOpen }) => ({ isOpen: !isOpen }));
}

render() {
const { toggleEvents, ...rest } = this.props;
return <Collapse isOpen={this.state.isOpen} {...rest} />;
}
}

UncontrolledCollapse.propTypes = propTypes;
UncontrolledCollapse.defaultProps = defaultProps;

export default UncontrolledCollapse;
98 changes: 98 additions & 0 deletions src/__tests__/UncontrolledCollapse.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { Collapse, UncontrolledCollapse } from '../';

describe('UncontrolledCollapse', () => {
let toggler;
let togglers;

beforeEach(() => {
document.body.innerHTML = `
<div>
<button id="toggler">Click Me</button>
<button class="toggler">Toggler 1</button>
<button class="toggler">Toggler 2</button>
</div>`;
toggler = document.getElementById('toggler');
togglers = document.getElementsByClassName('toggler');
});

afterEach(() => {
if (jest.isMockFunction(UncontrolledCollapse.prototype.toggle)) {
UncontrolledCollapse.prototype.toggle.mockRestore();
}
document.body.innerHTML = '';
toggler = null;
togglers = null;
});

it('should be a Collapse', () => {
const collapse = shallow(<UncontrolledCollapse toggler="#toggler">Yo!</UncontrolledCollapse>);

expect(collapse.type()).toBe(Collapse);
});

it('should have isOpen default to false', () => {
const collapse = shallow(<UncontrolledCollapse toggler="#toggler">Yo!</UncontrolledCollapse>);

expect(collapse.prop('isOpen')).toBe(false);
});

it('should toggle isOpen when toggle is called', () => {
const collapse = shallow(<UncontrolledCollapse toggler="#toggler">Yo!</UncontrolledCollapse>);

toggler.click();
collapse.update();

expect(collapse.prop('isOpen')).toBe(true);
});

it('should call toggle when toggler is clicked', () => {
jest.spyOn(UncontrolledCollapse.prototype, 'toggle');
mount(<UncontrolledCollapse toggler="#toggler">Yo!</UncontrolledCollapse>);

expect(UncontrolledCollapse.prototype.toggle.mock.calls.length).toBe(0);

toggler.click();

expect(UncontrolledCollapse.prototype.toggle.mock.calls.length).toBe(1);
});

it('should toggle for multiple togglers', () => {
const collapse = shallow(<UncontrolledCollapse toggler=".toggler">Yo!</UncontrolledCollapse>);

expect(collapse.prop('isOpen')).toBe(false);

togglers[0].click();
collapse.update();

expect(collapse.prop('isOpen')).toBe(true);

togglers[1].click();
collapse.update();

expect(collapse.prop('isOpen')).toBe(false);
});

it('should remove eventListeners when unmounted', () => {
jest.spyOn(UncontrolledCollapse.prototype, 'componentWillUnmount');
jest.spyOn(UncontrolledCollapse.prototype, 'toggle');

const wrapper = mount(<UncontrolledCollapse toggler="#toggler">Yo!</UncontrolledCollapse>);

expect(UncontrolledCollapse.prototype.toggle.mock.calls.length).toBe(0);
expect(UncontrolledCollapse.prototype.componentWillUnmount.mock.calls.length).toBe(0);

toggler.click();

expect(UncontrolledCollapse.prototype.toggle.mock.calls.length).toBe(1);

wrapper.unmount();

expect(UncontrolledCollapse.prototype.componentWillUnmount.mock.calls.length).toBe(1);

toggler.click();

expect(UncontrolledCollapse.prototype.toggle.mock.calls.length).toBe(1);
});
});
12 changes: 6 additions & 6 deletions src/__tests__/utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,20 +120,20 @@ describe('Utils', () => {
const element = document.createElement('div');
element.className = 'thing';
document.body.appendChild(element);
jest.spyOn(document, 'querySelector');
jest.spyOn(document, 'querySelectorAll');
expect(Utils.getTarget('.thing')).toEqual(element);
expect(document.querySelector).toHaveBeenCalledWith('.thing');
document.querySelector.mockRestore();
expect(document.querySelectorAll).toHaveBeenCalledWith('.thing');
document.querySelectorAll.mockRestore();
});

it('should query the document for the id target if the target is a string and could not be found normally', () => {
const element = document.createElement('div');
element.setAttribute('id', 'thing');
document.body.appendChild(element);
jest.spyOn(document, 'querySelector');
jest.spyOn(document, 'querySelectorAll');
expect(Utils.getTarget('thing')).toEqual(element);
expect(document.querySelector).toHaveBeenCalledWith('#thing');
document.querySelector.mockRestore();
expect(document.querySelectorAll).toHaveBeenCalledWith('#thing');
document.querySelectorAll.mockRestore();
});

it('should return the input target if it is not a function nor a string', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import ListGroupItemHeading from './ListGroupItemHeading';
import ListGroupItemText from './ListGroupItemText';
import UncontrolledAlert from './UncontrolledAlert';
import UncontrolledButtonDropdown from './UncontrolledButtonDropdown';
import UncontrolledCollapse from './UncontrolledCollapse';
import UncontrolledDropdown from './UncontrolledDropdown';
import UncontrolledNavDropdown from './UncontrolledNavDropdown';
import UncontrolledTooltip from './UncontrolledTooltip';
Expand Down Expand Up @@ -170,6 +171,7 @@ export {
ListGroupItemHeading,
UncontrolledAlert,
UncontrolledButtonDropdown,
UncontrolledCollapse,
UncontrolledDropdown,
UncontrolledNavDropdown,
UncontrolledTooltip,
Expand Down
Loading

0 comments on commit 355d2b8

Please sign in to comment.