Skip to content

Accordian component #327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
158 changes: 158 additions & 0 deletions lib/accordion/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React from 'react';
import PropTypes from 'prop-types';
import { themr } from 'react-css-themr';
import { FaAngleRight } from 'react-icons/fa';
import classnames from 'classnames';
import defaultTheme from './theme.module.scss';
import '../globals/fonts.scss';

class Accordion extends React.Component {
constructor(props) {
super(props);
this.state = {
expandedKeys: [],
};
this.expandedContentRef = {};
}

toggle = (keyValue) => {
const { expandedKeys } = this.state;
const { allowMultipleExpanded } = this.props;
let updatedExpandedKeys = [...expandedKeys];
if (allowMultipleExpanded) {
if (updatedExpandedKeys.includes(keyValue)) {
updatedExpandedKeys = updatedExpandedKeys.filter(key => key !== keyValue);
} else {
updatedExpandedKeys.push(keyValue);
}
} else {
updatedExpandedKeys = expandedKeys.includes(keyValue) ? [] : [keyValue];
}
this.setState({ expandedKeys: updatedExpandedKeys });

if (this.expandedContentRef[keyValue].clientHeight) {
this.expandedContentRef[keyValue].style.height = 0;
} else {
const wrapper = this.expandedContentRef[keyValue].firstChild;
this.expandedContentRef[keyValue].style.height = `${
wrapper.clientHeight
}px`;
}
};

renderContent = () => {
const {
theme,
headerKey,
contentKey,
borderless,
headerClassName,
contentClassName,
data,
} = this.props;
const { expandedKeys } = this.state;
const iconProps = {
className: classnames(theme.icon, theme['expand-icon'], theme.open),
};
const expandedKeysIncludesItemKey = key => expandedKeys.includes(key);
return (
<div>
{data
&& data.map(item => (
<div
id="accordion-list"
key={item.key}
className={classnames(theme.itemWrapper, {
[theme.noBorder]: borderless,
})}
>
<div className={classnames(theme.item)}>
{/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */}
<div
id="accordion-heading"
className={classnames(
theme.heading,
{ [theme.headerActive]: expandedKeysIncludesItemKey(item.key) },
headerClassName,
)}
onClick={() => this.toggle(item.key)}
>
<FaAngleRight
{...iconProps}
className={classnames(
expandedKeysIncludesItemKey(item.key) ? theme.open : theme.close,
)}
/>

<p className={classnames(theme.headingText)}>{item[headerKey]}</p>
</div>
<div
id={expandedKeysIncludesItemKey(item.key) ? 'content-element' : 'hidden-content-element'}
ref={(el) => {
this.expandedContentRef[item.key] = el;
}}
className={classnames(
{ [theme.hideContent]: !expandedKeysIncludesItemKey(item.key) },
theme.content,
)}
>
<p
className={classnames(
expandedKeysIncludesItemKey(item.key)
? [theme.contentTextOpen]
: [theme.contentTextClose],
contentClassName,
theme.contentPadding,
)}
>
{item[contentKey]}
</p>
</div>
</div>
</div>
))}
</div>
);
};

render() {
const { theme, borderless, className } = this.props;
return (
<div
className={classnames(
theme.accordionBody,
{ [theme.noBorder]: borderless },
className,
)}
>
{this.renderContent()}
</div>
);
}
}

Accordion.propTypes = {
data: PropTypes.oneOfType([PropTypes.array]).isRequired,
headerKey: PropTypes.string,
contentKey: PropTypes.string,
allowMultipleExpanded: PropTypes.bool,
className: PropTypes.string,
headerClassName: PropTypes.string,
contentClassName: PropTypes.string,
borderless: PropTypes.bool,
theme: PropTypes.shape({
accordionBody: PropTypes.string,
}),
};
Accordion.defaultProps = {
theme: defaultTheme,
headerKey: 'header',
contentKey: 'content',
allowMultipleExpanded: false,
className: '',
headerClassName: '',
contentClassName: '',
borderless: false,
};
export default themr('CBAccordion', defaultTheme)(Accordion);
50 changes: 50 additions & 0 deletions lib/accordion/readMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Accordion

A basic accordion component.

### Properties
| Name | Type | Default | Description |
|:-----|:-----|:-----|:-----|
| `data` | `Array` | &nbsp; | An array of contents for accordion. ( `Required` ) |
| `headerKey` | `String` | `header` | specify the key to be used for header from data. |
| `contentKey` | `String` | `content` | specify the key to be used for content from data.|
| `borderless` | `Boolean` | `false` | Prop to render borderless accordion |
| `className` | `String` | &nbsp; | Set a class to style the Component|
| `headerClassName` | `String` | &nbsp;| Set a class for the header.|
| `contentClassName` | `String` | &nbsp;| Set a class for the content.|
| `allowMultipleExpanded` | `Boolean` | `false`| allows to expand more then one item.|

### Theme

| Name | Description|
|:---------|:-----------|
| `accordionBody` | Class used for the accordion body.|
| `itemWrapper` | Class used for wrapper element of header and content for each item.|
| `item` | Class used for the items in the accordion.|
| `heading` | Class used for the header of the accordion.|
| `content` | Class used for the content of the accordion.|
| `headerActive` | Class used for the header when the content is visible.|
| `contentText` | Class used for the text of the content.|
| `open` | Class used for icon when the item is opened.|
| `close` | Class used for icon when the item is closed.|
| `noBorder` | Class used for the items to remove the border.|




### Example Usage
```
class Demo extends React.Component {
render() {
const items = [
{header:'Lorem Ipsum is simply dummy text of ',content:'Lorem Ipsum&nbsp;is simply dummy text of the printing and typesetting industry',key:'1'},
{header:'printing and typesetting industry',content:'Lorem Ipsum&nbsp;is simply dummy text of the printing and typesetting industry',key:'2'},
];
return (
<div>
<Accordion data={items} />
</div>
)
}
}
```
63 changes: 63 additions & 0 deletions lib/accordion/tests/accessibility.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import { expect } from 'chai';
import Accordion from '..';

/* eslint-disable no-undef */
describe('Accordion Render tests', () => {
let wrappedComponent;
let expandedWrappedComponent;

const data = [
{
header: 'Lorem Ipsum is simply dummy text of ',
content:
'Lorem Ipsum&nbsp;is simply dummy text of the printing and typesetting industry',
key: '1',
},
{
header: 'printing and typesetting industry',
content:
'Lorem Ipsum&nbsp;is simply dummy text of the printing and typesetting industry',
key: '2',
},
];
const ACCCORDION_LIST_SELECTOR = '#accordion-list';
const ACCORDION_HEADING_SELECTOR = '#accordion-heading';
const HIDDEN_CONTENT_ELEMENT_SELECTOR = '#hidden-content-element';

beforeEach(() => {
wrappedComponent = mount(shallow(<Accordion data={data} />).get(0));
expandedWrappedComponent = mount(
shallow(<Accordion data={data} allowMultipleExpanded />).get(0),
);
});

afterEach(() => {
wrappedComponent.unmount();
expandedWrappedComponent.unmount();
});

it('onClick item is expended', () => {
const childrenComponent = wrappedComponent
.find(ACCCORDION_LIST_SELECTOR)
.children();
childrenComponent.forEach((child) => {
expect(child.exists(HIDDEN_CONTENT_ELEMENT_SELECTOR)).to.equal(true);
child.find(ACCORDION_HEADING_SELECTOR).simulate('click');
expect(wrappedComponent.state().expandedKeys.length).to.equals(1);
});
});
it('onClick item is expended in allowMultipleExpanded.', () => {
const count = data.length;
const childrenComponent = expandedWrappedComponent
.find(ACCCORDION_LIST_SELECTOR)
.children();
childrenComponent.forEach((child) => {
child.find(ACCORDION_HEADING_SELECTOR).simulate('click');
});
expect(expandedWrappedComponent.state().expandedKeys.length).to.equal(
count,
);
});
});
62 changes: 62 additions & 0 deletions lib/accordion/tests/render.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import { mount } from 'enzyme';
import { expect } from 'chai';
import Accordion from '..';

/* eslint-disable no-undef */
describe('Accordion Render tests', () => {
let wrappedComponent;

const data = [
{
header: 'Lorem Ipsum is simply dummy text of ',
content:
'Lorem Ipsum&nbsp;is simply dummy text of the printing and typesetting industry',
key: '1',
},
{
header: 'printing and typesetting industry',
content:
'Lorem Ipsum&nbsp;is simply dummy text of the printing and typesetting industry',
key: '2',
},
];

const ACCCORDION_LIST_SELECTOR = '#accordion-list';
const ACCORDION_HEADING_SELECTOR = '#accordion-heading';
const HIDDEN_CONTENT_ELEMENT_SELECTOR = '#hidden-content-element';
beforeEach(() => {
wrappedComponent = mount(<Accordion data={data} />);
});

afterEach(() => {
wrappedComponent.unmount();
});

it('Successfully renders Accordion component', () => {
const expectedValue = 1;
const simulatedValue = wrappedComponent.find(Accordion).length;
expect(simulatedValue).equal(expectedValue);
});
it('Successfully renders all the data item provided.', () => {
const expectedValue = data.length;
const simulatedValue = () => wrappedComponent.find(ACCCORDION_LIST_SELECTOR).children();
expect(simulatedValue()).to.have.lengthOf(expectedValue);
});
it('Successfully renders Accordion header.', () => {
wrappedComponent
.find(ACCCORDION_LIST_SELECTOR)
.children()
.forEach((child) => {
expect(child.exists(ACCORDION_HEADING_SELECTOR)).to.equal(true);
});
});
it('Successfully renders Accordion content.', () => {
wrappedComponent
.find(ACCCORDION_LIST_SELECTOR)
.children()
.forEach((child) => {
expect(child.exists(HIDDEN_CONTENT_ELEMENT_SELECTOR)).to.equal(true);
});
});
});
Loading