@salesforce/design-system-react
Version:
Salesforce Lightning Design System for React
368 lines (328 loc) • 10.1 kB
JSX
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */
// # Carousel Component
// Implements the [Carousel design pattern](https://www.lightningdesignsystem.com/components/carousel/) in React.
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
// ### shortid
// [npmjs.com/package/shortid](https://www.npmjs.com/package/shortid)
// shortid is a short, non-sequential, url-friendly, unique id generator
import shortid from 'shortid';
import { CAROUSEL } from '../../utilities/constants';
import {
canUseDOM,
canUseEventListeners,
} from '../../utilities/execution-environment';
import CarouselIndicators from './private/carousel-indicators';
import PreviousNextCarouselNavigator from './private/previous-next-carousel-navigator';
import CarouselItem from './private/carousel-item';
import AutoPlayButton from './private/auto-play-button';
// ### Event Helpers
import KEYS from '../../utilities/key-code';
import EventUtil from '../../utilities/event';
// This component's `checkProps` which issues warnings to developers about properties when in development mode (similar to React's built in development tools)
import checkProps from './check-props';
import componentDoc from './docs.json';
/* eslint-disable jsx-a11y/no-static-element-interactions */
// ### Default Props
const defaultProps = {
assistiveText: {
autoPlayButton: 'Start / Stop auto-play',
nextPanel: 'Next Panel',
previousPanel: 'Previous Panel',
},
autoplayInterval: 4000,
hasAutoplay: false,
hasPreviousNextPanelNavigation: false,
isInfinite: false,
itemsPerPanel: 1,
};
/**
* A carousel allows multiple pieces of featured content to occupy an allocated amount of space.
* Currently panel index and auto play cannot be controlled by the app.
*/
class Carousel extends React.Component {
// ### Display Name
// Always use the canonical component name as the React display name.
static displayName = CAROUSEL;
// ### Prop Types
static propTypes = {
/**
* Description of the carousel items for screen-readers.
*/
assistiveText: PropTypes.object,
/**
* Interval for the autoplay iteration
*/
autoplayInterval: PropTypes.number,
/**
* CSS classes that are applied to the main 'slds-carousel' classed component container
*/
className: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
PropTypes.string,
]),
/**
* Boolean showing whether the autoplay feature is available or not
*/
hasAutoplay: PropTypes.bool,
/**
* Boolean for displaying the navigation indicators (left/right arrows) of the carousel
*/
hasPreviousNextPanelNavigation: PropTypes.bool,
/**
* Id of component, if desired. If not provided an id is automatically generated
*/
id: PropTypes.string,
/**
* CSS that is applied to carousel indicators
*/
indicatorStyles: PropTypes.object,
/**
* Boolean for infinite loop navigation
*/
isInfinite: PropTypes.bool,
/**
* * **Array of item objects used by the default carousel item renderer.**
* Each object can contain:
* * `id`: The id of the carousel item. [REQUIRED]
* * `heading`: Primary string that will be used as the heading
* * `description`: Secondary string that is used to describe the item
* * `buttonLabel`: If assigned a call to button action will be rendered with this text, if unassigned no button is rendered
* * `imageAssistiveText`: Image alt text, if not present heading will be used instead
* * `href`: Used for item link, if not provided 'javascript:void(0);' is used instead
* * `src`: Item image src value
*/
items: PropTypes.array.isRequired,
/**
* Number of items to be displayed at a time in the carousel
*/
itemsPerPanel: PropTypes.number,
/**
* Accepts a custom carousel item rendering function
*/
onRenderItem: PropTypes.func,
/**
* Handler for clicking on a carousel item
*/
onItemClick: PropTypes.func,
};
// ### Default Props
static defaultProps = defaultProps;
constructor(props) {
super(props);
const { items, itemsPerPanel } = this.props;
this.nrOfPanels = Math.ceil(items.length / itemsPerPanel);
this.stageItem = React.createRef();
this.state = {
translateX: -1000000,
currentPanel: 1,
isAutoPlayOn: this.props.hasAutoplay,
stageWidth: 0,
};
}
componentWillMount() {
this.generatedId = shortid.generate();
checkProps(CAROUSEL, componentDoc);
}
componentDidMount() {
this.setTranslationAmount(0);
if (this.props.hasAutoplay) {
this.startAutoplay();
}
if (
canUseDOM &&
this.stageItem !== undefined &&
this.stageItem.current !== undefined &&
this.stageItem.current.offsetWidth !== undefined
) {
this.stageWidth = this.stageItem.current.offsetWidth;
}
if (canUseEventListeners) {
window.addEventListener('resize', this.setDimensions, false);
}
}
componentWillUnmount() {
if (canUseEventListeners) {
window.removeEventListener('resize', this.setDimensions, false);
}
this.stopAutoplay();
}
onNextPanelHandler = () => {
const next = this.state.currentPanel % this.nrOfPanels + 1;
this.setCurrentPanel(next, this.changeTranslationAutomatically);
};
onPreviousPanelHandler = () => {
const prev =
(this.state.currentPanel + this.nrOfPanels - 1) % this.nrOfPanels;
this.setCurrentPanel(prev, this.changeTranslationAutomatically);
};
onIndicatorClickHandler = (panel) => {
this.setCurrentPanel(panel, this.changeTranslationAutomatically);
};
onAutoPlayBtnClick = () => {
const { isAutoPlayOn } = this.state;
const actionToTake = isAutoPlayOn ? this.stopAutoplay : this.startAutoplay;
this.setState({
isAutoPlayOn: !isAutoPlayOn,
});
actionToTake();
};
setDimensions = () => {
if (
canUseDOM &&
this.stageItem !== undefined &&
this.stageItem.current !== undefined &&
this.stageItem.current.offsetWidth !== undefined
) {
this.setState(
{ stageWidth: this.stageItem.current.offsetWidth },
this.changeTranslationAutomatically
);
}
};
setTranslationAmount = (amount, cb) => {
this.setState({ translateX: amount }, cb);
};
setCurrentPanel = (amount, cb) => {
this.setState({ currentPanel: amount }, cb);
};
startAutoplay = () => {
this.autoplayId = setInterval(() => {
if (this.canGoToNext()) {
this.onNextPanelHandler();
} else {
this.stopAutoplay();
}
}, this.props.autoplayInterval);
};
stopAutoplay = () => {
if (this.autoplayId) {
clearInterval(this.autoplayId);
}
this.setState({ isAutoPlayOn: false });
};
changeTranslationAutomatically = () => {
this.setTranslationAmount(
-(
(this.state.stageWidth || this.stageWidth) *
(this.state.currentPanel - 1)
)
);
};
canGoToNext = () => this.state.currentPanel < this.nrOfPanels;
canGoToPrevious = () => this.state.currentPanel > 1;
handleKeyDown = (event) => {
const keyDownCallbacks = {
[KEYS.LEFT]: () => {
if (this.canGoToPrevious()) {
this.onPreviousPanelHandler();
}
},
[KEYS.RIGHT]: () => {
if (this.canGoToNext()) {
this.onNextPanelHandler();
}
},
};
if (keyDownCallbacks[event.keyCode]) {
EventUtil.trapImmediate(event);
keyDownCallbacks[event.keyCode]();
}
};
render() {
const {
hasAutoplay,
hasPreviousNextPanelNavigation,
isInfinite,
} = this.props;
const id = this.props.id || this.generatedId;
const isPreviousBtnDisabled = !(isInfinite || this.canGoToPrevious());
const isNextBtnDisabled = !(isInfinite || this.canGoToNext());
const itemWidth =
(this.state.stageWidth || this.stageWidth) / this.props.itemsPerPanel;
return (
<div
className={classnames('slds-carousel', this.props.className)}
id={id}
onKeyDown={this.handleKeyDown}
>
<div className="slds-grid_vertical slds-col slds-path__scroller">
{hasAutoplay && (
<AutoPlayButton
assistiveText={this.props.assistiveText.autoPlayButton}
isAutoPlayOn={this.state.isAutoPlayOn}
onClick={this.onAutoPlayBtnClick}
/>
)}
<div
className="slds-is-relative"
style={{ marginLeft: '60px', marginRight: '60px' }}
>
{hasPreviousNextPanelNavigation && (
<PreviousNextCarouselNavigator
assistiveText={this.props.assistiveText.previousPanel}
iconName="left"
isDisabled={isPreviousBtnDisabled}
onClick={this.onPreviousPanelHandler}
inlineStyle={{ left: '-60px' }}
/>
)}
<div
ref={this.stageItem}
className="slds-carousel__stage slds-show"
>
<div
className="slds-carousel__panels slds-is-relative"
style={{
transform: `translateX(${this.state.translateX}px)`,
}}
>
{this.props.items.map((item, index) => (
<CarouselItem
onClick={(event) => {
this.props.onItemClick(event, { item });
}}
onRenderItem={this.props.onRenderItem}
{...item}
isInCurrentPanel={
index >=
(this.state.currentPanel - 1) *
this.props.itemsPerPanel &&
index <
(this.state.currentPanel - 1) *
this.props.itemsPerPanel +
this.props.itemsPerPanel
}
itemWidth={itemWidth}
key={item.id}
/>
))}
</div>
</div>
{hasPreviousNextPanelNavigation && (
<PreviousNextCarouselNavigator
assistiveText={this.props.assistiveText.nextPanel}
iconName="right"
isDisabled={isNextBtnDisabled}
onClick={this.onNextPanelHandler}
inlineStyle={{ right: '-60px' }}
/>
)}
</div>
<CarouselIndicators
style={this.props.indicatorStyles}
noOfIndicators={this.nrOfPanels}
currentIndex={this.state.currentPanel}
onClick={this.onIndicatorClickHandler}
items={this.props.items}
itemsPerPanel={this.props.itemsPerPanel}
/>
</div>
</div>
);
}
}
export default Carousel;