From a6050f5ea6cc9ceba63db702663242b571e9f602 Mon Sep 17 00:00:00 2001 From: bestguy <7zark7@gmail.com> Date: Fri, 21 May 2021 08:31:34 -0700 Subject: [PATCH] feat(accordion): add `flush` support and `stayOpen` support --- docs/lib/Components/AccordionPage.js | 39 ++++++++++++++-- docs/lib/examples/Accordion.js | 6 +-- docs/lib/examples/AccordionFlush.js | 45 +++++++++++++++++++ docs/lib/examples/UncontrolledAccordion.js | 2 +- .../examples/UncontrolledAccordionStayOpen.js | 40 +++++++++++++++++ src/Accordion.js | 11 +++-- src/AccordionBody.js | 7 ++- src/AccordionHeader.js | 6 ++- src/Placeholder.js | 8 ++-- src/PlaceholderButton.js | 1 + src/UncontrolledAccordion.js | 16 ++++--- src/__tests__/Accordion.spec.js | 12 +++-- src/__tests__/AccordionBody.spec.js | 4 +- src/__tests__/AccordionHeader.spec.js | 4 +- src/__tests__/Placeholder.spec.js | 22 +++++++++ types/lib/Accordion.d.ts | 2 + types/lib/UncontrolledAccordion.d.ts | 9 ++++ types/lib/index.d.ts | 4 ++ 18 files changed, 207 insertions(+), 31 deletions(-) create mode 100644 docs/lib/examples/AccordionFlush.js create mode 100644 docs/lib/examples/UncontrolledAccordionStayOpen.js create mode 100644 types/lib/UncontrolledAccordion.d.ts diff --git a/docs/lib/Components/AccordionPage.js b/docs/lib/Components/AccordionPage.js index 715dcfae4..0e06e0523 100644 --- a/docs/lib/Components/AccordionPage.js +++ b/docs/lib/Components/AccordionPage.js @@ -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() { @@ -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, @@ -80,6 +85,20 @@ AccordionItem.propTypes = { `} + + Flush +

+ Add flush to remove the default background-color, some borders, + and some rounded corners to render Accordions edge-to-edge with their parent container. +

+
+ +
+
+          {AccordionFlushExampleSource}
+        
+ + Uncontrolled
@@ -88,6 +107,20 @@ AccordionItem.propTypes = { { UncontrolledAccordionExampleSource } + + Stay Open +

+ Add the stayOpen prop to make accordion items stay open when another item is opened. +

+
+ +
+
+          
+            { UncontrolledAccordionStayOpenExampleSource }
+          
+        
+ Properties
           
diff --git a/docs/lib/examples/Accordion.js b/docs/lib/examples/Accordion.js
index 4a323d2e0..db542235a 100644
--- a/docs/lib/examples/Accordion.js
+++ b/docs/lib/examples/Accordion.js
@@ -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 (
     
- + Accordion Item 1 diff --git a/docs/lib/examples/AccordionFlush.js b/docs/lib/examples/AccordionFlush.js new file mode 100644 index 000000000..37e1f743a --- /dev/null +++ b/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 ( +
+ + + + Accordion Item 1 + + + This is the first item's accordion body. + 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 .accordion-body, though the transition does limit overflow. + + + + + Accordion Item 2 + + + This is the second item's accordion body. + 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 .accordion-body, though the transition does limit overflow. + + + + + Accordion Item 3 + + + This is the third item's accordion body. + 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 .accordion-body, though the transition does limit overflow. + + + +
+ ); +}; + +export default Example; diff --git a/docs/lib/examples/UncontrolledAccordion.js b/docs/lib/examples/UncontrolledAccordion.js index e675b68c1..c2d4c70b4 100644 --- a/docs/lib/examples/UncontrolledAccordion.js +++ b/docs/lib/examples/UncontrolledAccordion.js @@ -4,7 +4,7 @@ import { UncontrolledAccordion, AccordionBody, AccordionHeader, AccordionItem } const Example = (props) => { return (
- + Accordion Item 1 diff --git a/docs/lib/examples/UncontrolledAccordionStayOpen.js b/docs/lib/examples/UncontrolledAccordionStayOpen.js new file mode 100644 index 000000000..6b4c53fc2 --- /dev/null +++ b/docs/lib/examples/UncontrolledAccordionStayOpen.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { UncontrolledAccordion, AccordionBody, AccordionHeader, AccordionItem } from 'reactstrap'; + +const Example = (props) => { + return ( +
+ + + + Accordion Item 1 + + + This is the first item's accordion body. + 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 .accordion-body, though the transition does limit overflow. + + + + + Accordion Item 2 + + + This is the second item's accordion body. + 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 .accordion-body, though the transition does limit overflow. + + + + + Accordion Item 3 + + + This is the third item's accordion body. + 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 .accordion-body, though the transition does limit overflow. + + + +
+ ); +}; + +export default Example; diff --git a/src/Accordion.js b/src/Accordion.js index 295bbb0f4..d5223a288 100644 --- a/src/Accordion.js +++ b/src/Accordion.js @@ -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, }; @@ -24,7 +25,8 @@ const defaultProps = { const Accordion = (props) => { const { - openId, + flush, + open, toggle, className, cssModule, @@ -35,10 +37,13 @@ const Accordion = (props) => { const classes = mapToCssModules(classNames( className, 'accordion', + { + 'accordion-flush': flush + } ), cssModule); const accordionContext = useMemo(() => ({ - openId, + open, toggle, })); diff --git a/src/AccordionBody.js b/src/AccordionBody.js index bf35876aa..50138f2f0 100644 --- a/src/AccordionBody.js +++ b/src/AccordionBody.js @@ -33,7 +33,7 @@ const AccordionItem = (props) => { ...attributes } = props; - const { openId } = useContext(AccordionContext); + const { open } = useContext(AccordionContext); const classes = mapToCssModules(classNames( className, @@ -41,7 +41,10 @@ const AccordionItem = (props) => { ), cssModule); return ( - + {children} ); diff --git a/src/AccordionHeader.js b/src/AccordionHeader.js index 4bdd95bf0..a43754c62 100644 --- a/src/AccordionHeader.js +++ b/src/AccordionHeader.js @@ -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, @@ -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 ( diff --git a/src/Placeholder.js b/src/Placeholder.js index 630658fe7..255539e18 100644 --- a/src/Placeholder.js +++ b/src/Placeholder.js @@ -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 = { @@ -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, diff --git a/src/PlaceholderButton.js b/src/PlaceholderButton.js index 1081e226c..c54e81fc2 100644 --- a/src/PlaceholderButton.js +++ b/src/PlaceholderButton.js @@ -21,6 +21,7 @@ const defaultProps = { const PlaceholderButton = (props) => { let { cssModule, + className, tag: Tag, ...attributes } = props; diff --git a/src/UncontrolledAccordion.js b/src/UncontrolledAccordion.js index 43eb094e9..5dfde3596 100644 --- a/src/UncontrolledAccordion.js +++ b/src/UncontrolledAccordion.js @@ -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 ; + return ; }; Accordion.propTypes = propTypes; Accordion.defaultProps = defaultProps; -export default UncntrolledAccordion; +export default UncontrolledAccordion; diff --git a/src/__tests__/Accordion.spec.js b/src/__tests__/Accordion.spec.js index 2a81c7242..a36b48768 100644 --- a/src/__tests__/Accordion.spec.js +++ b/src/__tests__/Accordion.spec.js @@ -4,19 +4,25 @@ import { Accordion } from '../'; describe('Accordion', () => { it('should render with "accordion" class', () => { - const wrapper = mount( {}} />); + const wrapper = mount( {}} />); expect(wrapper.find('.accordion').length).toBe(1); }); + it('should render with "accordion-flush" class', () => { + const wrapper = mount( {}} />); + + expect(wrapper.find('.accordion-flush').length).toBe(1); + }); + it('should render additional classes', () => { - const wrapper = mount( {}} />); + const wrapper = mount( {}} />); expect(wrapper.find('.accordion').hasClass('other')).toBe(true); }); it('should render custom tag', () => { - const wrapper = mount( {}} />); + const wrapper = mount( {}} />); expect(wrapper.find('main.accordion').length).toBe(1); }); diff --git a/src/__tests__/AccordionBody.spec.js b/src/__tests__/AccordionBody.spec.js index 24b7018ba..20b8f3c3e 100644 --- a/src/__tests__/AccordionBody.spec.js +++ b/src/__tests__/AccordionBody.spec.js @@ -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( - + ); diff --git a/src/__tests__/AccordionHeader.spec.js b/src/__tests__/AccordionHeader.spec.js index 6eef1572e..3f2e14945 100644 --- a/src/__tests__/AccordionHeader.spec.js +++ b/src/__tests__/AccordionHeader.spec.js @@ -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( - + ); diff --git a/src/__tests__/Placeholder.spec.js b/src/__tests__/Placeholder.spec.js index 2ee99e6e4..e374daccb 100644 --- a/src/__tests__/Placeholder.spec.js +++ b/src/__tests__/Placeholder.spec.js @@ -27,4 +27,26 @@ describe('Placeholder', () => { const wrapper = shallow(); expect(wrapper.hasClass('placeholder-lg')).toBe(true); }) + + it('should render different widths for different breakpoints', () => { + const wrapper = shallow(); + expect(wrapper.hasClass('col-lg-8')).toBe(true) + expect(wrapper.hasClass('col-12')).toBe(true) + }) + + it('should allow custom columns to be defined', () => { + const wrapper = shallow(); + expect(wrapper.hasClass('col-4')).toBe(true); + expect(wrapper.hasClass('col-jumbo-6')).toBe(true); + }); + + + it('should allow custom columns to be defined with objects', () => { + const wrapper = shallow(); + + expect(wrapper.hasClass('col-custom-1')).toBe(true); + expect(wrapper.hasClass('order-custom-2')).toBe(true); + expect(wrapper.hasClass('offset-custom-4')).toBe(true); + expect(wrapper.hasClass('col')).toBe(false); + }); }) diff --git a/types/lib/Accordion.d.ts b/types/lib/Accordion.d.ts index 7ea45265f..108203f98 100644 --- a/types/lib/Accordion.d.ts +++ b/types/lib/Accordion.d.ts @@ -4,7 +4,9 @@ import { CSSModule } from './index'; export interface AccordionProps extends React.HTMLAttributes { tag?: React.ElementType; cssModule?: CSSModule; + flush?: boolean; innerRef?: React.Ref; + open: string | string[]; } declare class Accordion extends React.Component {} diff --git a/types/lib/UncontrolledAccordion.d.ts b/types/lib/UncontrolledAccordion.d.ts new file mode 100644 index 000000000..004851611 --- /dev/null +++ b/types/lib/UncontrolledAccordion.d.ts @@ -0,0 +1,9 @@ +import { AccordionProps } from './Accordion'; + +export interface UncontrolledAccordionProps extends AccordionProps { + defaultOpen?: string | string[]; + stayOpen?: boolean; +} + +declare class UncontrolledAccordion extends React.Component {} +export default UncontrolledAccordion; diff --git a/types/lib/index.d.ts b/types/lib/index.d.ts index 31895b11f..2c00e2e1b 100644 --- a/types/lib/index.d.ts +++ b/types/lib/index.d.ts @@ -2,6 +2,8 @@ export interface CSSModule { [className: string]: string; } +export { default as Accordion } from './Accordion'; +export { AccordionProps } from './Accordion'; export { default as Alert } from './Alert'; export { AlertProps } from './Alert'; export { default as Badge } from './Badge'; @@ -154,6 +156,8 @@ export { default as ToastHeader } from './ToastHeader'; export { ToastHeaderProps } from './ToastHeader'; export { default as Tooltip } from './Tooltip'; export { TooltipProps } from './Tooltip'; +export { default as UncontrolledAccordion } from './UncontrolledAccordion'; +export { UncontrolledAccordionProps } from './UncontrolledAccordion'; export { UncontrolledAlert, UncontrolledButtonDropdown,