diff --git a/README.md b/README.md index 6add15c8..e9889587 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,12 @@ ReactDOM.render(, document.querySelector('[data-mount]')); accordion CSS class(es) applied to the component + + activeItems + Array + [] + Indexes (or custom keys) to pre expand items. Can be changed dynamically. Doesn't have the priority against `AccordionItem - expanded` on first render. + @@ -128,6 +134,12 @@ ReactDOM.render(, document.querySelector('[data-mount]')); null Class name for hidden body state + + customKey + String + + Custom key to be used as a reference in `Accordion - activeItems` + diff --git a/demo/css/demo.css b/demo/css/demo.css index e4aff248..e9aa8cea 100644 --- a/demo/css/demo.css +++ b/demo/css/demo.css @@ -7,6 +7,10 @@ margin-top: 1.875rem; } +.u-margin-right { + margin-right: 1.875rem; +} + .block { padding: 1.25rem; } diff --git a/demo/js/demo.js b/demo/js/demo.js index 81ceeef0..25a0de80 100644 --- a/demo/js/demo.js +++ b/demo/js/demo.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { @@ -11,531 +11,598 @@ import { import '../css/demo.css'; import '../../src/react-accessible-accordion.css'; -const Example = () => ( -
-

Default settings

+class Example extends Component { + constructor(props) { + super(props); + this.state = { + activeItems: [0, 1], + }; + this.changeActiveItems = this._changeActiveItems.bind(this); + } - - - -

- Accessible Accordion -
-

-
- -

- Accessible Accordion component for React. Inspired by rc-collapse and react-sanfona. -

-
-
- - -

- Components -
-

-
See all the components from this package
-
- -
    -
  • Accordion
  • -
  • AccordionItem
  • -
  • AccordionItemTitle
  • -
  • AccordionItemBody
  • -
-
-
-
+ _changeActiveItems(activeItems) { + this.setState({ + activeItems: activeItems, + }); + } -

Allow multiple

+ render() { + return ( +
+

Default settings

- - - -

- Accordion -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
accordionBooleantrueOpen only one item at a time or not
onChangeFunction(keys)noopTriggered on change (open/close items)
classNameStringaccordionCSS class(es) applied to the component
-
-
- - -

- AccordionItem -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
expandedBooleanfalseExpands this item on first render
classNameStringaccordion__itemCSS class(es) applied to the component
-
-
- - -

- AccordionItemTitle -
-

-
- - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
classNameStringaccordion__titleCSS class(es) applied to the component
-
-
- - -

- AccordionItemBody -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
classNameStringaccordion__bodyCSS class(es) applied to the component
- hideBody - ClassName - Stringaccordion__body--hiddenClass name for hidden body state
-
-
-
+ + + +

+ Accessible Accordion +
+

+
+ +

+ Accessible Accordion component for React. Inspired by rc-collapse and react-sanfona. +

+
+
+ + +

+ Components +
+

+
See all the components from this package
+
+ +
    +
  • Accordion
  • +
  • AccordionItem
  • +
  • AccordionItemTitle
  • +
  • AccordionItemBody
  • +
+
+
+
-

Multi Accordion children

+

Allow multiple

- - - -

- Components API -
-

-
- - - - -

- Accordion -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
accordionBooleantrueOpen only one item at a time or not
onChangeFunction(keys)noopTriggered on change (open/close items)
classNameStringaccordionCSS class(es) applied to the component
-
-
- - -

- AccordionItem -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
expandedBooleanfalseExpands this item on first render
classNameStringaccordion__itemCSS class(es) applied to the component
-
-
- - -

- AccordionItemTitle -
-

-
- - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
classNameStringaccordion__titleCSS class(es) applied to the component
-
-
- - -

- AccordionItemBody -
-

-
- - - - - - - - - - - - - - - - - - - - - - - - -
nametypedefaultdescription
classNameStringaccordion__bodyCSS class(es) applied to the component
- hideBody - ClassName - Stringaccordion__body--hiddenClass name for hidden body state
-
-
-
-
-
- - -

- Development -
-

-
How to install the project
-
- -

- Clone the project on your computer, and install Node. This project also uses nvm. -

-

- nvm install
- # Then, install all project dependencies.
- npm install
- # Install the git hooks.
- ./.githooks/deploy
- # Set up a `.env` file with the appropriate secrets.
- touch .env -

-
-
-
+ + + +

+ Accordion +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
accordionBooleantrueOpen only one item at a time or not
onChangeFunction(keys)noopTriggered on change (open/close items)
classNameStringaccordionCSS class(es) applied to the component
+
+
+ + +

+ AccordionItem +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
expandedBooleanfalseExpands this item on first render
classNameStringaccordion__itemCSS class(es) applied to the component
+
+
+ + +

+ AccordionItemTitle +
+

+
+ + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
classNameStringaccordion__titleCSS class(es) applied to the component
+
+
+ + +

+ AccordionItemBody +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
classNameStringaccordion__bodyCSS class(es) applied to the component
+ hideBody + ClassName + Stringaccordion__body--hiddenClass name for hidden body state
+
+
+
-

Pre expanded children

+

Multi Accordion children

- - - -

- Working on the project -
-

-
- -

- Everything mentioned in the installation process should already be done. -

-

- # Make sure you use the right node version.
- nvm use
- # Start the the development tools in watch mode.
- npm run start
- # Runs linting.
- npm run lint
- # Runs tests.
- npm run test
- # View other available commands with:
- npm run -

-
-
- - -

- Run the demo -
-

-
To have an easy play around
-
- -

- Everything mentioned in the installation process should already be done. -

-

- # Make sure you use the right node version.
- nvm use
- # Start the server and the development tools.
- npm run start-demo -

-
-
-
+ + + +

+ Components API +
+

+
+ + + + +

+ Accordion +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
accordionBooleantrueOpen only one item at a time or not
onChangeFunction(keys)noopTriggered on change (open/close items)
classNameStringaccordionCSS class(es) applied to the component
+
+
+ + +

+ AccordionItem +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
expandedBooleanfalseExpands this item on first render
classNameStringaccordion__itemCSS class(es) applied to the component
+
+
+ + +

+ AccordionItemTitle +
+

+
+ + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
classNameStringaccordion__titleCSS class(es) applied to the component
+
+
+ + +

+ AccordionItemBody +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
nametypedefaultdescription
classNameStringaccordion__bodyCSS class(es) applied to the component
+ hideBody + ClassName + Stringaccordion__body--hiddenClass name for hidden body state
+
+
+
+
+
+ + +

+ Development +
+

+
How to install the project
+
+ +

+ Clone the project on your computer, and install Node. This project also uses nvm. +

+

+ nvm install
+ # Then, install all project dependencies.
+ npm install
+ # Install the git hooks.
+ ./.githooks/deploy
+ # Set up a `.env` file with the appropriate secrets.
+ touch .env +

+
+
+
-

With permanent blocks

+

Pre expanded children

- - - -

- Working on the project -
-

-
- -

- Everything mentioned in the installation process should already be done. -

-

- # Make sure you use the right node version.
- nvm use
- # Start the the development tools in watch mode.
- npm run start
- # Runs linting.
- npm run lint
- # Runs tests.
- npm run test
- # View other available commands with:
- npm run -

-
-
- Please feel free to contribute to this repository -
-
- - -

- Run the demo -
-

-
To have an easy play around
-
-
- This block fits in between the title and the body. -
- -

- Everything mentioned in the installation process should already be done. -

-

- # Make sure you use the right node version.
- nvm use
- # Start the server and the development tools.
- npm run start-demo -

-
-
-
+ + + +

+ Working on the project +
+

+
+ +

+ Everything mentioned in the installation process should already be done. +

+

+ # Make sure you use the right node version.
+ nvm use
+ # Start the the development tools in watch mode.
+ npm run start
+ # Runs linting.
+ npm run lint
+ # Runs tests.
+ npm run test
+ # View other available commands with:
+ npm run +

+
+
+ + +

+ Run the demo +
+

+
To have an easy play around
+
+ +

+ Everything mentioned in the installation process should already be done. +

+

+ # Make sure you use the right node version.
+ nvm use
+ # Start the server and the development tools.
+ npm run start-demo +

+
+
+
+ +

With permanent blocks

+ + + + +

+ Working on the project +
+

+
+ +

+ Everything mentioned in the installation process should already be done. +

+

+ # Make sure you use the right node version.
+ nvm use
+ # Start the the development tools in watch mode.
+ npm run start
+ # Runs linting.
+ npm run lint
+ # Runs tests.
+ npm run test
+ # View other available commands with:
+ npm run +

+
+
+ Please feel free to contribute to this repository +
+
+ + +

+ Run the demo +
+

+
To have an easy play around
+
+
+ This block fits in between the title and the body. +
+ +

+ Everything mentioned in the installation process should already be done. +

+

+ # Make sure you use the right node version.
+ nvm use
+ # Start the server and the development tools.
+ npm run start-demo +

+
+
+
-

A bit of animation on the arrow?

+

A bit of animation on the arrow?

- - - -

- Animated Accessible Accordion -
-

-
- -

- Did you notice the animation on the arrow? -

-
-
- - -

- How to? -
-

-
- -

- Check css/demo.css in the demo/ folder :) -

-
-
-
+ + + +

+ Animated Accessible Accordion +
+

+
+ +

+ Did you notice the animation on the arrow? +

+
+
+ + +

+ How to? +
+

+
+ +

+ Check css/demo.css in the demo/ folder :) +

+
+
+
-

Only one item

+

Only one item

+ + + + +

+ Single item +
+

+
+ +

+ Why would you need more than one? +

+
+
+
+ +

Change active items

+ +
+ + +
- - - -

- Single item -
-

-
- -

- Why would you need more than one? -

-
-
-
-
-); + + + +

+ First item +
+

+
+ +

+ This should be open by default. +

+
+
+ + +

+ Second item +
+

+
+ +

+ This shoud be open by default. +

+
+
+ + +

+ Third item +
+

+
+ +

+ This should open when clicking on the button. +

+
+
+
+
+ ); + } +} ReactDOM.render(, document.getElementById('app-root')); diff --git a/src/Accordion/__snapshots__/accordion.spec.js.snap b/src/Accordion/__snapshots__/accordion.spec.js.snap index aa214e65..2d644a71 100644 --- a/src/Accordion/__snapshots__/accordion.spec.js.snap +++ b/src/Accordion/__snapshots__/accordion.spec.js.snap @@ -1,5 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Accordion close accordions via accordion props props dynamicly 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + exports[`Accordion different className 1`] = `
`; +exports[`Accordion different className with the same activeItems prop 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + +exports[`Accordion expand accordion via accordion props dynamicly 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + +exports[`Accordion expand multiple accordions via accordion props props dynamicly 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + +exports[`Accordion expand multiple accordions via accordion props props dynamicly with default + expanded on accordion items 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + exports[`Accordion handleClick function accordion false 1`] = `
`; +exports[`Accordion pre expand accordion via accordion props 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + +exports[`Accordion pre expand accordion via accordion props vs accordion item props. Expanded only second item. 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + +exports[`Accordion pre expand accordion via accordion props with custom key 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + +exports[`Accordion pre expand multiple accordions via accordion props 1`] = ` +
+
+ Fake Child +
+
+ Fake Child +
+
+ Fake Child +
+
+`; + exports[`Accordion pre expanded accordion 1`] = `
{}, className: 'accordion', + activeItems: [], }; const propTypes = { @@ -13,6 +15,10 @@ const propTypes = { PropTypes.arrayOf(PropTypes.element), PropTypes.object, ]).isRequired, + activeItems: PropTypes.arrayOf(PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ])), className: PropTypes.string, onChange: PropTypes.func, }; @@ -28,17 +34,38 @@ class Accordion extends Component { this.renderItems = this.renderItems.bind(this); } + componentWillReceiveProps(nextProps) { + if (!isArraysEqualShallow(nextProps.activeItems, this.state.activeItems)) { + let newActiveItems; + if (nextProps.accordion) { + newActiveItems = nextProps.activeItems.length + ? [nextProps.activeItems[0]] + : []; + } else { + newActiveItems = nextProps.activeItems.slice(); + } + this.setState({ + activeItems: newActiveItems, + }); + + nextProps.onChange(nextProps.accordion ? newActiveItems[0] : newActiveItems); + } + } + preExpandedItems() { - const activeItems = []; + let activeItems = []; React.Children.map(this.props.children, (item, index) => { if (item.props.expanded) { if (this.props.accordion) { - if (activeItems.length === 0) activeItems.push(index); + if (activeItems.length === 0) activeItems.push(item.props.customKey || index); } else { - activeItems.push(index); + activeItems.push(item.props.customKey || index); } } }); + if (activeItems.length === 0 && this.props.activeItems.length !== 0) { + activeItems = this.props.accordion ? [this.props.activeItems[0]] : this.props.activeItems.slice(); + } return activeItems; } @@ -68,7 +95,7 @@ class Accordion extends Component { const { accordion, children } = this.props; return React.Children.map(children, (item, index) => { - const key = index; + const key = item.props.customKey || index; const expanded = (this.state.activeItems.indexOf(key) !== -1) && (!item.props.disabled); return React.cloneElement(item, { diff --git a/src/Accordion/accordion.spec.js b/src/Accordion/accordion.spec.js index d40f9448..534c3d93 100644 --- a/src/Accordion/accordion.spec.js +++ b/src/Accordion/accordion.spec.js @@ -114,4 +114,134 @@ describe('Accordion', () => { ).toJSON(); expect(tree).toMatchSnapshot(); }); + + it('pre expand accordion via accordion props', () => { + const tree = renderer.create( + + Fake Child + Fake Child + , + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('pre expand accordion via accordion props vs accordion item props. Expanded only second item.', () => { + const tree = renderer.create( + + Fake Child + Fake Child + , + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('pre expand multiple accordions via accordion props', () => { + const tree = renderer.create( + + Fake Child + Fake Child + Fake Child + , + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('pre expand accordion via accordion props with custom key', () => { + const tree = renderer.create( + + Fake Child + Fake Child + , + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('expand accordion via accordion props dynamicly', () => { + const wrapper = renderer.create( + + Fake Child + Fake Child + , + ); + wrapper.update( + + Fake Child + Fake Child + , + ); + expect(wrapper.getInstance().state.activeItems).toEqual([1]); + expect(wrapper).toMatchSnapshot(); + }); + + it('expand multiple accordions via accordion props props dynamicly', () => { + const wrapper = renderer.create( + + Fake Child + Fake Child + Fake Child + , + ); + wrapper.update( + + Fake Child + Fake Child + Fake Child + , + ); + expect(wrapper.getInstance().state.activeItems).toEqual([1, 2]); + expect(wrapper).toMatchSnapshot(); + }); + + it(`expand multiple accordions via accordion props props dynamicly with default + expanded on accordion items`, () => { + const wrapper = renderer.create( + + Fake Child + Fake Child + Fake Child + , + ); + wrapper.update( + + Fake Child + Fake Child + Fake Child + , + ); + expect(wrapper.getInstance().state.activeItems).toEqual([1, 2]); + expect(wrapper).toMatchSnapshot(); + }); + + it('close accordions via accordion props props dynamicly', () => { + const wrapper = renderer.create( + + Fake Child + Fake Child + , + ); + wrapper.update( + + Fake Child + Fake Child + , + ); + expect(wrapper.getInstance().state.activeItems).toEqual([]); + expect(wrapper).toMatchSnapshot(); + }); + + it('different className with the same activeItems prop', () => { + const wrapper = renderer.create( + + Fake Child + Fake Child + , + ); + wrapper.update( + + Fake Child + Fake Child + , + ); + expect(wrapper).toMatchSnapshot(); + }); }); diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 00000000..b6ffa881 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,19 @@ +// eslint-disable-next-line +export const isArraysEqualShallow = (array, other) => { + if (array === other) { + return true; + } + if (!Array.isArray(array) || !Array.isArray(other) || array.length !== other.length) { + return false; + } + + let result = true; + const arrLength = array.length; + for (let index = 0; index < arrLength; index += 1) { + if (array[index] !== other[index]) { + result = false; + break; + } + } + return result; +}; diff --git a/src/utils/index.spec.js b/src/utils/index.spec.js new file mode 100644 index 00000000..97dc6e3c --- /dev/null +++ b/src/utils/index.spec.js @@ -0,0 +1,47 @@ +import { isArraysEqualShallow } from './index'; + +describe('isArrayEqual', () => { + it('exists', () => { + expect(isArraysEqualShallow).toBeDefined(); + }); + + it('Arrays are equal but not same ref', () => { + const arrayA = [3, 4, 7]; + const arrayB = [3, 4, 7]; + expect(isArraysEqualShallow(arrayA, arrayB)).toBeTruthy(); + }); + + it('Arrays are same ref', () => { + const arrayA = [3, 4, 7]; + const arrayB = arrayA; + expect(isArraysEqualShallow(arrayA, arrayB)).toBeTruthy(); + }); + + it('Params are not arrays', () => { + let arrayA = [3, 4, 7]; + let arrayB = 'string'; + expect(isArraysEqualShallow(arrayA, arrayB)).toBeFalsy(); + + arrayA = 5; + arrayB = [1, 4]; + expect(isArraysEqualShallow(arrayA, arrayB)).toBeFalsy(); + }); + + it('Params are not same length', () => { + const arrayA = [3, 4, 7]; + const arrayB = [1]; + expect(isArraysEqualShallow(arrayA, arrayB)).toBeFalsy(); + }); + + it('Array are different', () => { + const arrayA = [3, 4, 7]; + const arrayB = [1, 6, 8]; + expect(isArraysEqualShallow(arrayA, arrayB)).toBeFalsy(); + }); + + it('Array are different', () => { + const arrayA = [3, 4, 7]; + const arrayB = [1, 6, 8]; + expect(isArraysEqualShallow(arrayA, arrayB)).toBeFalsy(); + }); +});