Skip to content

Commit

Permalink
feat(accordion): add flush support and stayOpen support
Browse files Browse the repository at this point in the history
  • Loading branch information
bestguy authored and phwebi committed Oct 27, 2021
1 parent a698814 commit a6050f5
Show file tree
Hide file tree
Showing 18 changed files with 207 additions and 31 deletions.
39 changes: 36 additions & 3 deletions docs/lib/Components/AccordionPage.js
Expand Up @@ -3,12 +3,16 @@ import React from 'react';
import { PrismCode } from 'react-prism';
import PageTitle from '../UI/PageTitle';
import SectionTitle from '../UI/SectionTitle';

import AccordionExample from '../examples/Accordion';
const AccordionExampleSource = require('!!raw-loader!../examples/Accordion');

import AccordionFlushExample from '../examples/AccordionFlush';
import UncontrolledAccordionExample from '../examples/UncontrolledAccordion';
import UncontrolledAccordionStayOpenExample from '../examples/UncontrolledAccordionStayOpen';

const AccordionExampleSource = require('!!raw-loader!../examples/Accordion');
const AccordionFlushExampleSource = require('!!raw-loader!../examples/AccordionFlush');
const UncontrolledAccordionExampleSource = require('!!raw-loader!../examples/UncontrolledAccordion');
const UncontrolledAccordionStayOpenExampleSource =
require('!!raw-loader!../examples/UncontrolledAccordionStayOpen');

export default class AccordionPage extends React.Component {
render() {
Expand All @@ -31,6 +35,7 @@ export default class AccordionPage extends React.Component {
toggle: Proptypes.func.isRequired,
tag: tagPropType,
className: PropTypes.string,
flush: PropTypes.boolean,
cssModule: PropTypes.object,
innerRef: PropTypes.oneOfType([
PropTypes.object,
Expand Down Expand Up @@ -80,6 +85,20 @@ AccordionItem.propTypes = {
`}
</PrismCode>
</pre>

<SectionTitle>Flush</SectionTitle>
<p>
Add <code>flush</code> to remove the default background-color, some borders,
and some rounded corners to render Accordions edge-to-edge with their parent container.
</p>
<div className="docs-example">
<AccordionFlushExample />
</div>
<pre>
<PrismCode className="language-jsx">{AccordionFlushExampleSource}</PrismCode>
</pre>

<SectionTitle>Uncontrolled</SectionTitle>
<div className="docs-example">
<UncontrolledAccordionExample />
</div>
Expand All @@ -88,6 +107,20 @@ AccordionItem.propTypes = {
{ UncontrolledAccordionExampleSource }
</PrismCode>
</pre>

<SectionTitle>Stay Open</SectionTitle>
<p>
Add the <code>stayOpen</code> prop to make accordion items stay open when another item is opened.
</p>
<div className="docs-example">
<UncontrolledAccordionStayOpenExample />
</div>
<pre>
<PrismCode className="language-jsx">
{ UncontrolledAccordionStayOpenExampleSource }
</PrismCode>
</pre>

<SectionTitle>Properties</SectionTitle>
<pre>
<PrismCode className="language-jsx">
Expand Down
6 changes: 3 additions & 3 deletions docs/lib/examples/Accordion.js
Expand Up @@ -2,14 +2,14 @@ import React, { useState } from 'react';
import { Accordion, AccordionBody, AccordionHeader, AccordionItem } from 'reactstrap';

const Example = (props) => {
const [openId, setOpenId] = useState();
const [open, setOpen] = useState('1');
const toggle = (id) => {
openId === id ? setOpenId(undefined) : setOpenId(id);
open === id ? setOpen() : setOpen(id);
};

return (
<div>
<Accordion openId={openId} toggle={toggle}>
<Accordion open={open} toggle={toggle}>
<AccordionItem>
<AccordionHeader targetId="1">
Accordion Item 1
Expand Down
45 changes: 45 additions & 0 deletions docs/lib/examples/AccordionFlush.js
@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { Accordion, AccordionBody, AccordionHeader, AccordionItem } from 'reactstrap';

const Example = (props) => {
const [open, setOpen] = useState();
const toggle = (id) => {
open === id ? setOpen() : setOpen(id);
};

return (
<div>
<Accordion flush open={open} toggle={toggle}>
<AccordionItem>
<AccordionHeader targetId="1">
Accordion Item 1
</AccordionHeader>
<AccordionBody accordionId="1">
<strong>This is the first item's accordion body.</strong>
You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</AccordionBody>
</AccordionItem>
<AccordionItem>
<AccordionHeader targetId="2">
Accordion Item 2
</AccordionHeader>
<AccordionBody accordionId="2">
<strong>This is the second item's accordion body.</strong>
You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</AccordionBody>
</AccordionItem>
<AccordionItem>
<AccordionHeader targetId="3">
Accordion Item 3
</AccordionHeader>
<AccordionBody accordionId="3">
<strong>This is the third item's accordion body.</strong>
You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</AccordionBody>
</AccordionItem>
</Accordion>
</div>
);
};

export default Example;
2 changes: 1 addition & 1 deletion docs/lib/examples/UncontrolledAccordion.js
Expand Up @@ -4,7 +4,7 @@ import { UncontrolledAccordion, AccordionBody, AccordionHeader, AccordionItem }
const Example = (props) => {
return (
<div>
<UncontrolledAccordion>
<UncontrolledAccordion defaultOpen='1'>
<AccordionItem>
<AccordionHeader targetId="1">
Accordion Item 1
Expand Down
40 changes: 40 additions & 0 deletions docs/lib/examples/UncontrolledAccordionStayOpen.js
@@ -0,0 +1,40 @@
import React from 'react';
import { UncontrolledAccordion, AccordionBody, AccordionHeader, AccordionItem } from 'reactstrap';

const Example = (props) => {
return (
<div>
<UncontrolledAccordion defaultOpen={['1', '2']} stayOpen>
<AccordionItem>
<AccordionHeader targetId="1">
Accordion Item 1
</AccordionHeader>
<AccordionBody accordionId="1">
<strong>This is the first item's accordion body.</strong>
You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</AccordionBody>
</AccordionItem>
<AccordionItem>
<AccordionHeader targetId="2">
Accordion Item 2
</AccordionHeader>
<AccordionBody accordionId="2">
<strong>This is the second item's accordion body.</strong>
You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</AccordionBody>
</AccordionItem>
<AccordionItem>
<AccordionHeader targetId="3">
Accordion Item 3
</AccordionHeader>
<AccordionBody accordionId="3">
<strong>This is the third item's accordion body.</strong>
You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow.
</AccordionBody>
</AccordionItem>
</UncontrolledAccordion>
</div>
);
};

export default Example;
11 changes: 8 additions & 3 deletions src/Accordion.js
Expand Up @@ -14,7 +14,8 @@ const propTypes = {
PropTypes.func,
]),
children: PropTypes.node,
openId: PropTypes.string.isRequired,
flush: PropTypes.boolean,
open: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
toggle: PropTypes.func.isRequired,
};

Expand All @@ -24,7 +25,8 @@ const defaultProps = {

const Accordion = (props) => {
const {
openId,
flush,
open,
toggle,
className,
cssModule,
Expand All @@ -35,10 +37,13 @@ const Accordion = (props) => {
const classes = mapToCssModules(classNames(
className,
'accordion',
{
'accordion-flush': flush
}
), cssModule);

const accordionContext = useMemo(() => ({
openId,
open,
toggle,
}));

Expand Down
7 changes: 5 additions & 2 deletions src/AccordionBody.js
Expand Up @@ -33,15 +33,18 @@ const AccordionItem = (props) => {
...attributes
} = props;

const { openId } = useContext(AccordionContext);
const { open } = useContext(AccordionContext);

const classes = mapToCssModules(classNames(
className,
'accordion-collapse',
), cssModule);

return (
<Collapse {...attributes} className={classes} ref={innerRef} isOpen={openId === accordionId}>
<Collapse
{...attributes}
className={classes}
ref={innerRef} isOpen={Array.isArray(open) ? open.includes(accordionId) : open === accordionId }>
<Tag className="accordion-body">{children}</Tag>
</Collapse>
);
Expand Down
6 changes: 4 additions & 2 deletions src/AccordionHeader.js
Expand Up @@ -31,7 +31,7 @@ const AccordionHeader = (props) => {
targetId,
...attributes
} = props;
const { openId, toggle } = useContext(AccordionContext);
const { open, toggle } = useContext(AccordionContext);

const classes = mapToCssModules(classNames(
className,
Expand All @@ -40,7 +40,9 @@ const AccordionHeader = (props) => {

const buttonClasses = mapToCssModules(classNames(
'accordion-button',
{ collapsed: openId !== targetId },
{
collapsed: !(Array.isArray(open) ? open.includes(targetId) : open === targetId)
},
), cssModule);

return (
Expand Down
8 changes: 3 additions & 5 deletions src/Placeholder.js
Expand Up @@ -2,17 +2,15 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { mapToCssModules, tagPropType } from './utils';
import { getColumnClasses } from './Col';
import Col, { getColumnClasses } from './Col';

const propTypes = {
...Col.propTypes,
color: PropTypes.string,
tag: tagPropType,
animation: PropTypes.oneOf(['glow', 'wave']),
innerRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func, PropTypes.string]),
className: PropTypes.string,
cssModule: PropTypes.object,
size: PropTypes.oneOf(['lg', 'sm', 'xs']),
widths: PropTypes.array,
};

const defaultProps = {
Expand All @@ -32,7 +30,7 @@ const Placeholder = (props) => {
...attributes
} = props;

let { attributes: modifiedAttributes, colClasses } = getColumnClasses(attributes, cssModule)
let { attributes: modifiedAttributes, colClasses } = getColumnClasses(attributes, cssModule, widths)

const classes = mapToCssModules(classNames(
className,
Expand Down
1 change: 1 addition & 0 deletions src/PlaceholderButton.js
Expand Up @@ -21,6 +21,7 @@ const defaultProps = {
const PlaceholderButton = (props) => {
let {
cssModule,
className,
tag: Tag,
...attributes
} = props;
Expand Down
16 changes: 11 additions & 5 deletions src/UncontrolledAccordion.js
Expand Up @@ -13,22 +13,28 @@ const propTypes = {
PropTypes.func,
]),
children: PropTypes.node,
defaultOpen: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
stayOpen: PropTypes.bool,
};

const defaultProps = {
tag: 'div'
};

const UncntrolledAccordion = (props) => {
const [openId, setOpenId] = useState();
const UncontrolledAccordion = ({ defaultOpen, stayOpen, ...props }) => {
const [open, setOpen] = useState(defaultOpen || (stayOpen ? [] : undefined));
const toggle = (id) => {
openId === id ? setOpenId(undefined) : setOpenId(id);
if (stayOpen) {
open.includes(id) ? setOpen(open.filter(accordionId => accordionId !== id)) : setOpen([...open, id]);
} else {
open === id ? setOpen(undefined) : setOpen(id);
}
};

return <Accordion {...props} openId={openId} toggle={toggle} />;
return <Accordion {...props} open={open} toggle={toggle} />;
};

Accordion.propTypes = propTypes;
Accordion.defaultProps = defaultProps;

export default UncntrolledAccordion;
export default UncontrolledAccordion;
12 changes: 9 additions & 3 deletions src/__tests__/Accordion.spec.js
Expand Up @@ -4,19 +4,25 @@ import { Accordion } from '../';

describe('Accordion', () => {
it('should render with "accordion" class', () => {
const wrapper = mount(<Accordion openId="this accordion" togggle={() => {}} />);
const wrapper = mount(<Accordion open="this accordion" toggle={() => {}} />);

expect(wrapper.find('.accordion').length).toBe(1);
});

it('should render with "accordion-flush" class', () => {
const wrapper = mount(<Accordion flush open="this accordion" toggle={() => {}} />);

expect(wrapper.find('.accordion-flush').length).toBe(1);
});

it('should render additional classes', () => {
const wrapper = mount(<Accordion className="other" openId="this accordion" togggle={() => {}} />);
const wrapper = mount(<Accordion className="other" open="this accordion" toggle={() => {}} />);

expect(wrapper.find('.accordion').hasClass('other')).toBe(true);
});

it('should render custom tag', () => {
const wrapper = mount(<Accordion tag="main" openId="this accordion" togggle={() => {}} />);
const wrapper = mount(<Accordion tag="main" open="this accordion" toggle={() => {}} />);

expect(wrapper.find('main.accordion').length).toBe(1);
});
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/AccordionBody.spec.js
Expand Up @@ -24,9 +24,9 @@ describe('AccordionBody', () => {
expect(wrapper.find('.accordion-collapse.collapse').find('div.accordion-body').length).toBe(1);
});

it('should be open if openId == id', () => {
it('should be open if open == id', () => {
const wrapper = mount(
<AccordionContext.Provider value={{ openId: 'cool-accordion' }}>
<AccordionContext.Provider value={{ open: 'cool-accordion' }}>
<AccordionBody accordionId="cool-accordion" />
</AccordionContext.Provider>
);
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/AccordionHeader.spec.js
Expand Up @@ -21,9 +21,9 @@ describe('AccordionHeader', () => {
expect(wrapper.find('div.accordion-header').length).toBe(1);
});

it('should be open if openId == targetId', () => {
it('should be open if open == targetId', () => {
const wrapper = mount(
<AccordionContext.Provider value={{ openId: 'cool-accordion' }}>
<AccordionContext.Provider value={{ open: 'cool-accordion' }}>
<AccordionHeader targetId="cool-accordion" />
</AccordionContext.Provider>
);
Expand Down

0 comments on commit a6050f5

Please sign in to comment.