Repository: react-materialize/react-materialize
Pull request #1081 (2020-03-29 11:09:46)

Carousel: refactor to stateless / use hooks / add type definition
Fixes https://github.com/react-materialize/react-materialize/issues/1078
Fixes https://github.com/react-materialize/react-materialize/issues/1055


Patch:
diff --git a/src/Carousel.js b/src/Carousel.js
index f69d5dc..e94c8c4 100644
--- a/src/Carousel.js
+++ b/src/Carousel.js
@@ -1,149 +1,121 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
 import cx from 'classnames';
 import PropTypes from 'prop-types';
+import idgen from './idgen';
 
-class Carousel extends React.Component {
-  constructor(props) {
-    super(props);
+const Carousel = ({
+  children,
+  className,
+  carouselId,
+  fixedItem,
+  images,
+  centerImages,
+  options
+}) => {
+  const carouselRef = useRef(null);
 
-    this.renderFixedItem = this.renderFixedItem.bind(this);
-  }
+  useEffect(() => {
+    const instance = M.Carousel.init(carouselRef.current, options);
 
-  componentDidMount() {
-    const { options } = this.props;
-    if (options && typeof M !== 'undefined') {
-      this.instance = M.Carousel.init(this._carousel, options);
-    }
-  }
+    return () => instance.destroy();
+  }, [options]);
 
-  componentWillUnmount() {
-    if (this.instance) {
-      this.instance.destroy();
-    }
-  }
+  const elemsToRender = children || images || [];
 
-  renderItems(child, centerImages) {
-    if (typeof child === 'string') {
-      return (
-        <a
-          className={cx('carousel-item', {
-            'valign-wrapper': centerImages
-          })}
-        >
-          <img src={child} alt="" />
-        </a>
-      );
-    }
+  const classes = cx(
+    'carousel',
+    {
+      'carousel-slider': options.fullWidth
+    },
+    className
+  );
 
-    return React.cloneElement(child, {
-      className: cx('carousel-item', child.props.className, {
-        'valign-wrapper': centerImages
-      })
-    });
-  }
+  return (
+    <div id={carouselId} ref={carouselRef} className={classes}>
+      {Boolean(fixedItem) && (
+        <div className="carousel-fixed-item center">{fixedItem}</div>
+      )}
+      {React.Children.map(elemsToRender, child => {
+        const classes = cx('carousel-item', { 'valign-wrapper': centerImages });
+        if (typeof child === 'string') {
+          return (
+            <a className={classes}>
+              <img src={child} alt={child} />
+            </a>
+          );
+        }
 
-  renderFixedItem(fixedItem) {
-    return <div className="carousel-fixed-item center">{fixedItem}</div>;
-  }
-
-  render() {
-    const {
-      children,
-      className,
-      carouselId,
-      fixedItem,
-      images,
-      centerImages,
-      options
-    } = this.props;
-    const elemsToRender = children || images || [];
-
-    return (
-      elemsToRender && (
-        <div
-          id={carouselId}
-          ref={el => {
-            this._carousel = el;
-          }}
-          className={cx(
-            'carousel',
-            { 'carousel-slider': options.fullWidth },
-            className
-          )}
-        >
-          {fixedItem && this.renderFixedItem(fixedItem)}
-          {React.Children.map(elemsToRender, child =>
-            this.renderItems(child, centerImages)
-          )}
-        </div>
-      )
-    );
-  }
-}
+        return React.cloneElement(child, {
+          className: cx(classes, child.props.className)
+        });
+      })}
+    </div>
+  );
+};
 
 Carousel.propTypes = {
-  /*
+  /**
    * Children to render as carousel elements
    */
   children: PropTypes.any,
-  /*
+  /**
    * Array of image url's
    */
   images: PropTypes.arrayOf(PropTypes.string),
-  /*
+  /**
    * Makes the images centered inside the carousel using '.valign-wrapper' CSS helper
    */
   centerImages: PropTypes.bool,
-  /*
+  /**
    * Fixed element on slider
    */
   fixedItem: PropTypes.node,
-  /*
+  /**
    * Additional classNames for carousel wrapper
    */
   className: PropTypes.string,
-  /*
+  /**
    * Carousel ID for jQuery manipulating
    */
   carouselId: PropTypes.string,
-  /*
+  /**
    * Carousel initialization options
    * <a href="http://materializecss.com/carousel.html">http://materializecss.com/carousel.html</a>
    */
   options: PropTypes.shape({
-    /*
+    /**
      * Transition duration in milliseconds. (Default: 200)
      */
     duration: PropTypes.number,
-    /*
+    /**
      * Perspective zoom. If 0, all items are the same size. (Default: -100)
      */
     dist: PropTypes.number,
-    /*
+    /**
      * Set the spacing of the center item. (Default: 0)
      */
     shift: PropTypes.number,
-    /*
+    /**
      * Set the padding between non center items. (Default: 0)
      */
     padding: PropTypes.number,
-    /*
+    /**
      * Set the number of visible items. (Default: 5)
      */
     numVisible: PropTypes.number,
-    /*
+    /**
      * Make the carousel a full width slider. (Default: false)
      */
     fullWidth: PropTypes.bool,
-    /*
+    /**
      * Set to true to show indicators. (Default: false)
      */
     indicators: PropTypes.bool,
-    /*
+    /**
      * Don't wrap around and cycle through items. (Default: false)
      */
     noWrap: PropTypes.bool,
-    /*
+    /**
      * Callback for when a new slide is cycled to. (Default: null)
      */
     onCycleTo: PropTypes.func
@@ -151,6 +123,7 @@ Carousel.propTypes = {
 };
 
 Carousel.defaultProps = {
+  carouselId: `Carousel-${idgen()}`,
   options: {
     duration: 200,
     dist: -100,
diff --git a/types/components/Carousel.ts b/types/components/Carousel.ts
index ea3aed6..e72b881 100644
--- a/types/components/Carousel.ts
+++ b/types/components/Carousel.ts
@@ -1,8 +1,29 @@
 import * as React from 'react';
 
-declare namespace Carousel {
+interface CarouselOptions {
+  duration?: number;
+  dist?: number;
+  shift?: number;
+  padding?: number;
+  numVisible?: number;
+  fullWidth?: boolean;
+  indicators?: boolean;
+  noWrap?: boolean;
+  onCycleTo?: (currentItem: React.ReactNode) => void;
 }
 
-declare class Carousel extends React.Component {}
+export interface CarouselProps {
+  options?: CarouselOptions;
+  images?: string[];
+  centerImages?: boolean;
+  fixedItem?: React.ReactNode;
+  className?: string;
+  carouselId?: string;
+}
+
+/**
+ * React Materialize: Carousel
+ */
+declare const Carousel: React.FC<CarouselProps>
 
-export = Carousel;
+export default Carousel;


Test patch:
diff --git a/test/Carousel.spec.js b/test/Carousel.spec.js
index 1e3dcf0..810312b 100644
--- a/test/Carousel.spec.js
+++ b/test/Carousel.spec.js
@@ -75,7 +75,7 @@ describe('<Carousel />', () => {
     });
 
     test('uses default options if none are given', () => {
-      wrapper = shallow(<Carousel />);
+      wrapper = mount(<Carousel />);
 
       expect(carouselInitMock).toHaveBeenCalledWith({
         duration: 200,
@@ -91,33 +91,16 @@ describe('<Carousel />', () => {
     });
 
     test('handles full width sliders', () => {
-      wrapper = shallow(
+      wrapper = mount(
         <Carousel images={images} options={{ fullWidth: true }} />
       );
-      expect(wrapper).toMatchSnapshot();
       expect(carouselInitMock).toHaveBeenCalledWith({ fullWidth: true });
     });
 
     test('more options', () => {
       const options = { padding: 12, fullWidth: true, indicators: false };
-      wrapper = shallow(<Carousel images={images} options={options} />);
+      wrapper = mount(<Carousel images={images} options={options} />);
       expect(carouselInitMock).toHaveBeenCalledWith(options);
     });
   });
-  describe('undefined M', () => {
-    let __M;
-    beforeEach(() => {
-      __M = global.M;
-      global.M = undefined;
-    });
-    afterEach(() => {
-      global.M = __M;
-    });
-    test('doesnt throw without M', () => {
-      const tooltip = {};
-      expect(() =>
-        mount(<Carousel options={{ fullWidth: true }} />)
-      ).not.toThrow();
-    });
-  });
 });
diff --git a/test/__snapshots__/Carousel.spec.js.snap b/test/__snapshots__/Carousel.spec.js.snap
index c8ddfe5..2faaec6 100644
--- a/test/__snapshots__/Carousel.spec.js.snap
+++ b/test/__snapshots__/Carousel.spec.js.snap
@@ -3,13 +3,14 @@
 exports[`<Carousel /> accepts className props 1`] = `
 <div
   className="carousel center"
+  id="Carousel-mockId"
 >
   <a
     className="carousel-item"
     key=".0"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/1"
       src="https://lorempixel.com/250/250/nature/1"
     />
   </a>
@@ -18,7 +19,7 @@ exports[`<Carousel /> accepts className props 1`] = `
     key=".1"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/2"
       src="https://lorempixel.com/250/250/nature/2"
     />
   </a>
@@ -27,7 +28,7 @@ exports[`<Carousel /> accepts className props 1`] = `
     key=".2"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/3"
       src="https://lorempixel.com/250/250/nature/3"
     />
   </a>
@@ -36,7 +37,7 @@ exports[`<Carousel /> accepts className props 1`] = `
     key=".3"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/4"
       src="https://lorempixel.com/250/250/nature/4"
     />
   </a>
@@ -53,7 +54,7 @@ exports[`<Carousel /> accepts external \`id\` value 1`] = `
     key=".0"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/1"
       src="https://lorempixel.com/250/250/nature/1"
     />
   </a>
@@ -62,7 +63,7 @@ exports[`<Carousel /> accepts external \`id\` value 1`] = `
     key=".1"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/2"
       src="https://lorempixel.com/250/250/nature/2"
     />
   </a>
@@ -71,7 +72,7 @@ exports[`<Carousel /> accepts external \`id\` value 1`] = `
     key=".2"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/3"
       src="https://lorempixel.com/250/250/nature/3"
     />
   </a>
@@ -80,7 +81,7 @@ exports[`<Carousel /> accepts external \`id\` value 1`] = `
     key=".3"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/4"
       src="https://lorempixel.com/250/250/nature/4"
     />
   </a>
@@ -90,6 +91,7 @@ exports[`<Carousel /> accepts external \`id\` value 1`] = `
 exports[`<Carousel /> handles content slides 1`] = `
 <div
   className="carousel"
+  id="Carousel-mockId"
 >
   <div
     className="carousel-item red"
@@ -105,59 +107,17 @@ exports[`<Carousel /> handles content slides 1`] = `
 </div>
 `;
 
-exports[`<Carousel /> initialises handles full width sliders 1`] = `
-<div
-  className="carousel carousel-slider"
->
-  <a
-    className="carousel-item"
-    key=".0"
-  >
-    <img
-      alt=""
-      src="https://lorempixel.com/250/250/nature/1"
-    />
-  </a>
-  <a
-    className="carousel-item"
-    key=".1"
-  >
-    <img
-      alt=""
-      src="https://lorempixel.com/250/250/nature/2"
-    />
-  </a>
-  <a
-    className="carousel-item"
-    key=".2"
-  >
-    <img
-      alt=""
-      src="https://lorempixel.com/250/250/nature/3"
-    />
-  </a>
-  <a
-    className="carousel-item"
-    key=".3"
-  >
-    <img
-      alt=""
-      src="https://lorempixel.com/250/250/nature/4"
-    />
-  </a>
-</div>
-`;
-
 exports[`<Carousel /> renders 1`] = `
 <div
   className="carousel"
+  id="Carousel-mockId"
 >
   <a
     className="carousel-item"
     key=".0"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/1"
       src="https://lorempixel.com/250/250/nature/1"
     />
   </a>
@@ -166,7 +126,7 @@ exports[`<Carousel /> renders 1`] = `
     key=".1"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/2"
       src="https://lorempixel.com/250/250/nature/2"
     />
   </a>
@@ -175,7 +135,7 @@ exports[`<Carousel /> renders 1`] = `
     key=".2"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/3"
       src="https://lorempixel.com/250/250/nature/3"
     />
   </a>
@@ -184,7 +144,7 @@ exports[`<Carousel /> renders 1`] = `
     key=".3"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/4"
       src="https://lorempixel.com/250/250/nature/4"
     />
   </a>
@@ -194,13 +154,14 @@ exports[`<Carousel /> renders 1`] = `
 exports[`<Carousel /> renders centered images 1`] = `
 <div
   className="carousel"
+  id="Carousel-mockId"
 >
   <a
     className="carousel-item valign-wrapper"
     key=".0"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/1"
       src="https://lorempixel.com/250/250/nature/1"
     />
   </a>
@@ -209,7 +170,7 @@ exports[`<Carousel /> renders centered images 1`] = `
     key=".1"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/2"
       src="https://lorempixel.com/250/250/nature/2"
     />
   </a>
@@ -218,7 +179,7 @@ exports[`<Carousel /> renders centered images 1`] = `
     key=".2"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/3"
       src="https://lorempixel.com/250/250/nature/3"
     />
   </a>
@@ -227,7 +188,7 @@ exports[`<Carousel /> renders centered images 1`] = `
     key=".3"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/4"
       src="https://lorempixel.com/250/250/nature/4"
     />
   </a>
@@ -237,6 +198,7 @@ exports[`<Carousel /> renders centered images 1`] = `
 exports[`<Carousel /> renders fixed items 1`] = `
 <div
   className="carousel"
+  id="Carousel-mockId"
 >
   <div
     className="carousel-fixed-item center"
@@ -250,7 +212,7 @@ exports[`<Carousel /> renders fixed items 1`] = `
     key=".0"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/1"
       src="https://lorempixel.com/250/250/nature/1"
     />
   </a>
@@ -259,7 +221,7 @@ exports[`<Carousel /> renders fixed items 1`] = `
     key=".1"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/2"
       src="https://lorempixel.com/250/250/nature/2"
     />
   </a>
@@ -268,7 +230,7 @@ exports[`<Carousel /> renders fixed items 1`] = `
     key=".2"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/3"
       src="https://lorempixel.com/250/250/nature/3"
     />
   </a>
@@ -277,7 +239,7 @@ exports[`<Carousel /> renders fixed items 1`] = `
     key=".3"
   >
     <img
-      alt=""
+      alt="https://lorempixel.com/250/250/nature/4"
       src="https://lorempixel.com/250/250/nature/4"
     />
   </a>
diff --git a/types/sample.test.tsx b/types/sample.test.tsx
index 941d393..850610e 100644
--- a/types/sample.test.tsx
+++ b/types/sample.test.tsx
@@ -4,7 +4,8 @@ import {
   Autocomplete,
   Tabs,
   Tab,
-  Section
+  Section,
+  Carousel
 } from 'react-materialize';
 
 <>
@@ -12,4 +13,5 @@ import {
   <Tabs className="hello" defaultValue="what" options={{ duration: 32, onShow: () => {}, swipeable: true, responsiveThreshold: 32 }} onChange={() => {}} />
   <Tab idx="as" title="Hello" />
   <Section node="div" className="foo" id="bar" />
+  <Carousel images={['one.jpg']} options={{ numVisible: 32, onCycleTo: () => {} }} centerImages={true} />
 </>