From 2f5eb9dfc79068f988b29ffc68aa59db90a48af3 Mon Sep 17 00:00:00 2001 From: Tyson Warner Date: Mon, 7 Oct 2019 11:57:46 -0400 Subject: [PATCH 1/5] fix(UncontrolledCarousel): use item.key instead of item.src as key prop in CarouselItems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See #1536 Resolves issue where if the UncontrolledCarousel contains CarouselItems with the same src, the following react error gets emitted: ``` Warning: Encountered two children with the same key, `https://xxx.png`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version. ``` --- docs/lib/examples/CarouselUncontrolled.js | 9 ++++-- src/UncontrolledCarousel.js | 7 ++++- src/__tests__/UncontrolledCarousel.spec.js | 34 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/docs/lib/examples/CarouselUncontrolled.js b/docs/lib/examples/CarouselUncontrolled.js index daa757bad..1d29eb8c4 100644 --- a/docs/lib/examples/CarouselUncontrolled.js +++ b/docs/lib/examples/CarouselUncontrolled.js @@ -6,19 +6,22 @@ const items = [ src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa1d%20text%20%7B%20fill%3A%23555%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa1d%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23777%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22285.921875%22%20y%3D%22218.3%22%3EFirst%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E', altText: 'Slide 1', caption: 'Slide 1', - header: 'Slide 1 Header' + header: 'Slide 1 Header', + key: '1' }, { src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa20%20text%20%7B%20fill%3A%23444%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa20%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23666%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22247.3203125%22%20y%3D%22218.3%22%3ESecond%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E', altText: 'Slide 2', caption: 'Slide 2', - header: 'Slide 2 Header' + header: 'Slide 2 Header', + key: '2' }, { src: 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22800%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20800%20400%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_15ba800aa21%20text%20%7B%20fill%3A%23333%3Bfont-weight%3Anormal%3Bfont-family%3AHelvetica%2C%20monospace%3Bfont-size%3A40pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_15ba800aa21%22%3E%3Crect%20width%3D%22800%22%20height%3D%22400%22%20fill%3D%22%23555%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22277%22%20y%3D%22218.3%22%3EThird%20slide%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E', altText: 'Slide 3', caption: 'Slide 3', - header: 'Slide 3 Header' + header: 'Slide 3 Header', + key: '3' } ]; diff --git a/src/UncontrolledCarousel.js b/src/UncontrolledCarousel.js index 8fcd8ba87..96fe2d189 100644 --- a/src/UncontrolledCarousel.js +++ b/src/UncontrolledCarousel.js @@ -5,6 +5,7 @@ import CarouselItem from './CarouselItem'; import CarouselControl from './CarouselControl'; import CarouselIndicators from './CarouselIndicators'; import CarouselCaption from './CarouselCaption'; +import { warnOnce } from './utils'; const propTypes = { items: PropTypes.array.isRequired, @@ -60,11 +61,15 @@ class UncontrolledCarousel extends Component { const { activeIndex } = this.state; const slides = items.map((item) => { + const key = item.key || item.src; + if (!item.key) { + warnOnce('Item in UncontrolledCarousel is missing a key. Please provide a unique key to the item to avoid rendering react children with duplicate keys.'); + } return ( {item.altText} diff --git a/src/__tests__/UncontrolledCarousel.spec.js b/src/__tests__/UncontrolledCarousel.spec.js index acdcb70e1..8e22137d3 100644 --- a/src/__tests__/UncontrolledCarousel.spec.js +++ b/src/__tests__/UncontrolledCarousel.spec.js @@ -3,12 +3,22 @@ import { shallow } from 'enzyme'; import { Carousel, UncontrolledCarousel } from '../'; const items = [ + { src: '', altText: 'a', caption: 'caption 1', key: '1' }, + { src: '', altText: 'b', caption: 'caption 2', key: '2' }, + { src: '', altText: 'c', caption: 'caption 3', key: '3' } +]; + +const itemsWithoutKeys = [ { src: '', altText: 'a', caption: 'caption 1' }, { src: '', altText: 'b', caption: 'caption 2' }, { src: '', altText: 'c', caption: 'caption 3' } ]; describe('UncontrolledCarousel', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should be an Carousel', () => { const carousel = shallow(); expect(carousel.type()).toBe(Carousel); @@ -134,4 +144,28 @@ describe('UncontrolledCarousel', () => { instance.onExited(); expect(instance.animating).toBe(false); }); + + it('should render carousel items with provided key', () => { + const carousel = shallow(); + const carouselItem1 = carousel.childAt(0); + const carouselItem2 = carousel.childAt(1); + const carouselItem3 = carousel.childAt(2); + expect(carouselItem1.key()).toBe('1'); + expect(carouselItem2.key()).toBe('2'); + expect(carouselItem3.key()).toBe('3'); + }); + + it('should warn user if item(s) with no key are provided', () => { + jest.spyOn(console, 'error'); + const carousel = shallow(); + const carouselItem1 = carousel.childAt(0); + const carouselItem2 = carousel.childAt(1); + const carouselItem3 = carousel.childAt(2); + expect(carouselItem1.key()).toBe(''); + expect(carouselItem2.key()).toBe(''); + expect(carouselItem3.key()).toBe(''); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error.mock.calls[0][0]).toBe('Item in UncontrolledCarousel is missing a key. Please provide a unique key to the item to avoid rendering react children with duplicate keys.'); + }); + }); From 81b6c5c2c9dfbedd0e483983976137ef18241b17 Mon Sep 17 00:00:00 2001 From: Tyson Warner Date: Mon, 7 Oct 2019 14:13:50 -0400 Subject: [PATCH 2/5] refactor(UncontrolledCarousel): remove warning message for undefined item key --- src/UncontrolledCarousel.js | 4 ---- src/__tests__/UncontrolledCarousel.spec.js | 24 ---------------------- 2 files changed, 28 deletions(-) diff --git a/src/UncontrolledCarousel.js b/src/UncontrolledCarousel.js index 96fe2d189..a2efcf3e5 100644 --- a/src/UncontrolledCarousel.js +++ b/src/UncontrolledCarousel.js @@ -5,7 +5,6 @@ import CarouselItem from './CarouselItem'; import CarouselControl from './CarouselControl'; import CarouselIndicators from './CarouselIndicators'; import CarouselCaption from './CarouselCaption'; -import { warnOnce } from './utils'; const propTypes = { items: PropTypes.array.isRequired, @@ -62,9 +61,6 @@ class UncontrolledCarousel extends Component { const slides = items.map((item) => { const key = item.key || item.src; - if (!item.key) { - warnOnce('Item in UncontrolledCarousel is missing a key. Please provide a unique key to the item to avoid rendering react children with duplicate keys.'); - } return ( { - afterEach(() => { - jest.clearAllMocks(); - }); - it('should be an Carousel', () => { const carousel = shallow(); expect(carousel.type()).toBe(Carousel); @@ -154,18 +144,4 @@ describe('UncontrolledCarousel', () => { expect(carouselItem2.key()).toBe('2'); expect(carouselItem3.key()).toBe('3'); }); - - it('should warn user if item(s) with no key are provided', () => { - jest.spyOn(console, 'error'); - const carousel = shallow(); - const carouselItem1 = carousel.childAt(0); - const carouselItem2 = carousel.childAt(1); - const carouselItem3 = carousel.childAt(2); - expect(carouselItem1.key()).toBe(''); - expect(carouselItem2.key()).toBe(''); - expect(carouselItem3.key()).toBe(''); - expect(console.error).toHaveBeenCalledTimes(1); - expect(console.error.mock.calls[0][0]).toBe('Item in UncontrolledCarousel is missing a key. Please provide a unique key to the item to avoid rendering react children with duplicate keys.'); - }); - }); From 6d5b4d0c6c3847cd9d4c31d86052e1d355bc1616 Mon Sep 17 00:00:00 2001 From: Tyson Warner Date: Wed, 9 Oct 2019 11:25:26 -0400 Subject: [PATCH 3/5] refactor(CarouselCaption): make captionText, captionHeader proptype a node --- docs/lib/Components/CarouselPage.js | 4 ++-- src/CarouselCaption.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/lib/Components/CarouselPage.js b/docs/lib/Components/CarouselPage.js index 13f6b21c9..d824405f2 100644 --- a/docs/lib/Components/CarouselPage.js +++ b/docs/lib/Components/CarouselPage.js @@ -103,8 +103,8 @@ export default class CarouselPage extends React.Component {
           
 {`CarouselCaption.propTypes = {
-  captionHeader: PropTypes.string,
-  captionText: PropTypes.string.isRequired,
+  captionHeader: PropTypes.node,
+  captionText: PropTypes.node.isRequired,
   cssModule: PropTypes.object
 };`}
           
diff --git a/src/CarouselCaption.js b/src/CarouselCaption.js
index 27ab49747..c90b31418 100644
--- a/src/CarouselCaption.js
+++ b/src/CarouselCaption.js
@@ -21,8 +21,8 @@ const CarouselCaption = (props) => {
 };
 
 CarouselCaption.propTypes = {
-  captionHeader: PropTypes.string,
-  captionText: PropTypes.string.isRequired,
+  captionHeader: PropTypes.node,
+  captionText: PropTypes.node.isRequired,
   cssModule: PropTypes.object,
   className: PropTypes.string,
 };

From 6f39aa39c2298c45c23c6e52a661320ba50e7ca8 Mon Sep 17 00:00:00 2001
From: Tyson Warner 
Date: Thu, 10 Oct 2019 11:03:17 -0400
Subject: [PATCH 4/5] fix(Collapse): add aria-expanded attribute

---
 src/Collapse.js                | 1 +
 src/__tests__/Collapse.spec.js | 8 ++++++++
 2 files changed, 9 insertions(+)

diff --git a/src/Collapse.js b/src/Collapse.js
index 9bac27e6d..af2146d20 100644
--- a/src/Collapse.js
+++ b/src/Collapse.js
@@ -127,6 +127,7 @@ class Collapse extends Component {
               style={{ ...childProps.style, ...style }}
               className={classes}
               ref={this.props.innerRef}
+              aria-expanded={isOpen ? 'true' : 'false'}
             >
               {children}
             
diff --git a/src/__tests__/Collapse.spec.js b/src/__tests__/Collapse.spec.js
index 3000ffad9..bc508860f 100644
--- a/src/__tests__/Collapse.spec.js
+++ b/src/__tests__/Collapse.spec.js
@@ -122,4 +122,12 @@ describe('Collapse', () => {
     expect(wrapper.state('height')).toBe(null);
     wrapper.unmount();
   });
+
+  it('should set aria-expanded', () => {
+    isOpen = false;
+    wrapper = mount();
+    expect(wrapper.find('div').prop('aria-expanded')).toBe('false');
+    toggle();
+    expect(wrapper.find('div').prop('aria-expanded')).toBe('true');
+  });
 });

From bc6faebf5a81a80082b400e45a3cac1eb8928d79 Mon Sep 17 00:00:00 2001
From: Tyson Warner 
Date: Thu, 10 Oct 2019 11:03:40 -0400
Subject: [PATCH 5/5] fix(UncontrolledCollapse): add aria-expanded attribute

---
 src/UncontrolledCollapse.js                |  2 +-
 src/__tests__/UncontrolledCollapse.spec.js | 10 ++++++++++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/UncontrolledCollapse.js b/src/UncontrolledCollapse.js
index c5c77a9a9..b893e7b08 100644
--- a/src/UncontrolledCollapse.js
+++ b/src/UncontrolledCollapse.js
@@ -49,7 +49,7 @@ class UncontrolledCollapse extends Component {
   }
 
   render() {
-    return ;
+    return ;
   }
 }
 
diff --git a/src/__tests__/UncontrolledCollapse.spec.js b/src/__tests__/UncontrolledCollapse.spec.js
index dcd5e102f..9e6e721a3 100644
--- a/src/__tests__/UncontrolledCollapse.spec.js
+++ b/src/__tests__/UncontrolledCollapse.spec.js
@@ -95,4 +95,14 @@ describe('UncontrolledCollapse', () => {
 
     expect(UncontrolledCollapse.prototype.toggle.mock.calls.length).toBe(1);
   });
+
+  it('should set aria-expanded', () => {
+    const collapse = shallow(Yo!);
+
+    expect(collapse.prop('aria-expanded')).toBe('false');
+    toggler.click();
+    collapse.update();
+
+    expect(collapse.prop('aria-expanded')).toBe('true');
+  });
 });