Skip to content

Commit d87d9bb

Browse files
author
Piotr Siatka
committed
#227 Implement carousel control.
1 parent c198454 commit d87d9bb

File tree

5 files changed

+496
-0
lines changed

5 files changed

+496
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
.container {
2+
display: flex;
3+
4+
// Styles for elements container
5+
.contentContainer {
6+
flex-grow: 2;
7+
}
8+
9+
.loadingComponent {
10+
margin: auto;
11+
}
12+
13+
// Bottons containers
14+
.buttonLocations {
15+
cursor: pointer;
16+
flex-direction: column;
17+
}
18+
19+
.centralButtonsContainer {
20+
@extend .buttonLocations;
21+
justify-content: center;
22+
}
23+
.topButtonsContainer {
24+
@extend .buttonLocations;
25+
justify-content: left;
26+
}
27+
.bottomButtonsContainer {
28+
@extend .buttonLocations;
29+
justify-content: left;
30+
flex-direction: column-reverse;
31+
}
32+
33+
34+
// ButtonContainer display mode
35+
.buttonsOnlyPrevButton {
36+
position: absolute;
37+
}
38+
.buttonsOnlyPrevButton:hover {
39+
cursor: pointer;
40+
}
41+
42+
// Buttons styles
43+
.buttonsOnlyNextButton {
44+
position: absolute;
45+
left: -32px;
46+
}
47+
.buttonsOnlyNextButton:hover {
48+
cursor: pointer;
49+
}
50+
51+
.buttonsContainer {
52+
display: flex;
53+
background-color: transparent;
54+
}
55+
.buttonsOnlyContainer {
56+
@extend .buttonsContainer;
57+
position: relative;
58+
width: 0px;
59+
}
60+
61+
.blockButtonsContainer {
62+
@extend .buttonsContainer;
63+
height: 100%;
64+
min-width: 32px;
65+
}
66+
.blockButtonsContainer:hover {
67+
@extend .buttonsContainer;
68+
background-color: #f4f4f4;
69+
opacity: 0.5;
70+
}
71+
72+
.hiddenButtonsContainer {
73+
@extend .buttonsContainer;
74+
height: 100%;
75+
min-width: 32px;
76+
opacity: 0;
77+
}
78+
.hiddenButtonsContainer:hover {
79+
opacity: 0.5;
80+
background-color: #f4f4f4;
81+
}
82+
}

src/controls/carousel/Carousel.tsx

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
2+
import { IconButton } from "office-ui-fabric-react/lib/Button";
3+
import { initializeIcons } from '@uifabric/icons';
4+
initializeIcons();
5+
6+
import * as React from "react";
7+
// import { ICarouselProps, ICarouselState } from ".";
8+
import styles from "./Carousel.module.scss";
9+
import { ICarouselProps, ICarouselState, CarouselButtonsDisplay } from ".";
10+
import { css, ICssInput } from "@uifabric/utilities/lib";
11+
import { CarouselButtonsLocation } from "../../../lib/controls/carousel";
12+
import { ProcessingState } from "./ICarouselState";
13+
import { Spinner } from "office-ui-fabric-react/lib/Spinner";
14+
import { isArray } from "@pnp/common";
15+
16+
export class Carousel extends React.Component<ICarouselProps, ICarouselState> {
17+
constructor(props: ICarouselProps) {
18+
super(props);
19+
20+
const currentIndex = props.startIndex ? props.startIndex : 0;
21+
22+
this.state = {
23+
currentIndex,
24+
processingState: ProcessingState.idle
25+
};
26+
}
27+
28+
/**
29+
* Handles component update lifecycle method.
30+
* @param prevProps
31+
*/
32+
public componentDidUpdate(prevProps: ICarouselProps) {
33+
const currProps = this.props;
34+
35+
const prevPropsElementKey = prevProps.triggerPageEvent && prevProps.element ? (prevProps.element as JSX.Element).key : null;
36+
const nextPropsElementKey = currProps.triggerPageEvent && currProps.element ? (currProps.element as JSX.Element).key : null;
37+
38+
// Checking if component is in processing state and the key of the current element has been changed
39+
if (this.state.processingState === ProcessingState.processing && nextPropsElementKey != null && prevPropsElementKey != nextPropsElementKey) {
40+
this.setState({
41+
processingState: ProcessingState.idle
42+
});
43+
}
44+
}
45+
46+
47+
public render(): React.ReactElement<ICarouselProps> {
48+
const { currentIndex, processingState } = this.state;
49+
const { containerStyles, contentContainerStyles, containerButtonsStyles, prevButtonStyles, nextButtonStyles, loadingComponentContainerStyles } = this.props;
50+
51+
const prevButtonIconName = this.props.prevButtonIconName ? this.props.prevButtonIconName : "ChevronLeft";
52+
const nextButtonIconName = this.props.nextButtonIconName ? this.props.nextButtonIconName : "ChevronRight";
53+
const processing = processingState === ProcessingState.processing;
54+
55+
const prevButtonDisabled = processing || this.isCarouselButtonDisabled(false);
56+
const nextButtonDisabled = processing || this.isCarouselButtonDisabled(true);
57+
58+
const loadingComponent = this.props.loadingComponent ? this.props.loadingComponent : <Spinner />;
59+
const element = this.getElementToDisplay();
60+
61+
return (
62+
<div className={this.getMergedStyles(styles.container, containerStyles)}>
63+
<div className={this.getMergedStyles(this.getButtonContainerStyles(), containerButtonsStyles)} onClick={() => { if (prevButtonDisabled) { this.onCarouselButtonClicked(false); } }} >
64+
<IconButton
65+
className={this.getMergedStyles(this.getButtonStyles(false), prevButtonStyles)}
66+
iconProps={{ iconName: prevButtonIconName }}
67+
disabled={prevButtonDisabled}
68+
onClick={() => { this.onCarouselButtonClicked(false); }} />
69+
</div>
70+
71+
<div className={this.getMergedStyles(styles.contentContainer, contentContainerStyles)}>
72+
{
73+
processing &&
74+
<div className={this.getMergedStyles(styles.loadingComponent, loadingComponentContainerStyles)}>
75+
{loadingComponent}
76+
</div>
77+
}
78+
79+
{
80+
!processing && element &&
81+
element
82+
}
83+
84+
{
85+
!processing && !element && this.props.children && React.Children.count(this.props.children) > 0 &&
86+
this.props.children[currentIndex]
87+
}
88+
</div>
89+
90+
<div className={this.getMergedStyles(this.getButtonContainerStyles(), containerButtonsStyles)} onClick={() => { if (nextButtonDisabled) { this.onCarouselButtonClicked(true); } }}>
91+
<IconButton
92+
className={this.getMergedStyles(this.getButtonStyles(true), nextButtonStyles)}
93+
iconProps={{ iconName: nextButtonIconName }}
94+
disabled={nextButtonDisabled}
95+
onClick={() => { this.onCarouselButtonClicked(true); }} />
96+
</div>
97+
</div>
98+
);
99+
}
100+
101+
/**
102+
* Return merged styles for Button containers.
103+
*/
104+
private getButtonContainerStyles = (): string => {
105+
const buttonsDisplayMode = this.props.buttonsDisplay ? this.props.buttonsDisplay : CarouselButtonsDisplay.block;
106+
let buttonDisplayModeCss = "";
107+
switch (buttonsDisplayMode) {
108+
case CarouselButtonsDisplay.block:
109+
buttonDisplayModeCss = styles.blockButtonsContainer;
110+
break;
111+
case CarouselButtonsDisplay.buttonsOnly:
112+
buttonDisplayModeCss = styles.buttonsOnlyContainer;
113+
break;
114+
case CarouselButtonsDisplay.hidden:
115+
buttonDisplayModeCss = styles.hiddenButtonsContainer;
116+
break;
117+
default:
118+
return "";
119+
}
120+
121+
const buttonsLocation = this.props.buttonsLocation ? this.props.buttonsLocation : CarouselButtonsLocation.top;
122+
let buttonsLocationCss = "";
123+
switch (buttonsLocation) {
124+
case CarouselButtonsLocation.top:
125+
buttonsLocationCss = styles.blockButtonsContainer;
126+
break;
127+
case CarouselButtonsLocation.center:
128+
buttonsLocationCss = styles.centralButtonsContainer;
129+
break;
130+
case CarouselButtonsLocation.bottom:
131+
buttonsLocationCss = styles.bottomButtonsContainer;
132+
break;
133+
default:
134+
return "";
135+
}
136+
137+
const result = css(buttonDisplayModeCss, buttonsLocationCss);
138+
return result;
139+
}
140+
141+
/**
142+
* Return merged styles for Buttons.
143+
* @param nextButton
144+
*/
145+
private getButtonStyles(nextButton: boolean) {
146+
const buttonsDisplayMode = this.props.buttonsDisplay ? this.props.buttonsDisplay : CarouselButtonsDisplay.block;
147+
let result = "";
148+
if (buttonsDisplayMode === CarouselButtonsDisplay.buttonsOnly) {
149+
result = nextButton ? styles.buttonsOnlyNextButton : styles.buttonsOnlyPrevButton;
150+
}
151+
152+
return css(result);
153+
}
154+
155+
/**
156+
* Merges the styles of the components.
157+
*/
158+
private getMergedStyles = (componentStyles: string, userStyles?: ICssInput): string => {
159+
const mergedStyles = userStyles ? css(componentStyles, userStyles) : css(componentStyles);
160+
return mergedStyles;
161+
}
162+
163+
/**
164+
* Determines if the carousel button can be clicked.
165+
*/
166+
private isCarouselButtonDisabled = (nextButton: boolean): boolean => {
167+
// false by default
168+
const isInfinite = this.props.isInfinite != undefined ? this.props.isInfinite : false;
169+
const currentIndex = this.state.currentIndex;
170+
let result = false;
171+
172+
// Use validation from parent control or calcualte it based on the current index
173+
if (nextButton) {
174+
result = this.props.canMoveNext != undefined ?
175+
!this.props.canMoveNext :
176+
(currentIndex === React.Children.count(this.props.children) - 1) && !isInfinite;
177+
} else {
178+
result = this.props.canMovePrev != undefined ?
179+
!this.props.canMovePrev :
180+
(0 === currentIndex) && !isInfinite;
181+
}
182+
183+
return result;
184+
}
185+
186+
/**
187+
* Handles carousel button click.
188+
*/
189+
private onCarouselButtonClicked = (nextButtonClicked: boolean): void => {
190+
const currentIndex = this.state.currentIndex;
191+
192+
let nextIndex = this.state.currentIndex;
193+
let processingState = ProcessingState.processing;
194+
195+
// Trigger parent control to update provided element
196+
if (this.props.triggerPageEvent) {
197+
// Index validation needs to be done by the parent control specyfing canMove Next|Prev
198+
nextIndex = nextButtonClicked ? (currentIndex + 1) : (currentIndex - 1);
199+
// Trigger parent to provide new data
200+
this.props.triggerPageEvent(nextIndex);
201+
processingState = ProcessingState.processing;
202+
203+
} else {
204+
nextIndex = this.getNextIndex(nextButtonClicked);
205+
const canMoveNext = this.props.canMoveNext != undefined ? this.props.canMoveNext : true;
206+
const canMovePrev = this.props.canMovePrev != undefined ? this.props.canMovePrev : true;
207+
208+
if (canMoveNext && nextButtonClicked && this.props.onMoveNextClicked) {
209+
this.props.onMoveNextClicked(nextIndex);
210+
}
211+
else if (canMovePrev && !nextButtonClicked && this.props.onMovePrevClicked) {
212+
this.props.onMovePrevClicked(nextIndex);
213+
}
214+
215+
processingState = ProcessingState.idle;
216+
}
217+
218+
this.setState({
219+
currentIndex: nextIndex,
220+
processingState
221+
});
222+
}
223+
224+
/**
225+
* Returns next index after carousel button is clicked.
226+
*/
227+
private getNextIndex = (nextButtonClicked: boolean): number => {
228+
const currentIndex = this.state.currentIndex;
229+
let nextIndex = currentIndex;
230+
231+
const isInfinite = this.props.isInfinite !== undefined ? this.props.isInfinite : false;
232+
const length = (this.props.element as JSX.Element[]).length;
233+
234+
// Next Button clicked
235+
if (nextButtonClicked) {
236+
// If there is next element
237+
if (currentIndex < length - 1) {
238+
nextIndex = currentIndex + 1;
239+
}
240+
// In no more elements are available but it isInfiniteLoop -> reset index to the first element
241+
else if (isInfinite) {
242+
nextIndex = 0;
243+
}
244+
}
245+
// Prev Button clicked
246+
else {
247+
if (currentIndex - 1 >= 0) {
248+
// If there is previous element
249+
nextIndex = currentIndex - 1;
250+
} else if (isInfinite) {
251+
// If there is no previous element but isInfitineLoop -> reset index to the last element
252+
nextIndex = length - 1;
253+
}
254+
}
255+
256+
return nextIndex;
257+
}
258+
259+
/**
260+
* Returns current element to be displayed.
261+
*/
262+
private getElementToDisplay = (): JSX.Element => {
263+
const { element } = this.props;
264+
const currentIndex = this.state.currentIndex;
265+
let result : JSX.Element = null;
266+
267+
// If no element has been provided.
268+
if (!element) {
269+
result = null;
270+
}
271+
// Retrieve proper element from the array
272+
else if (isArray(element) && currentIndex >= 0 && (element as JSX.Element[]).length > currentIndex) {
273+
result = element[currentIndex];
274+
}
275+
else {
276+
result = element as JSX.Element;
277+
}
278+
279+
return result;
280+
}
281+
}

0 commit comments

Comments
 (0)