From 2c098b9f1ddfa6c966313b91a011c908e9edd49a Mon Sep 17 00:00:00 2001 From: Alex Terentiev Date: Mon, 5 Oct 2020 16:58:43 -0700 Subject: [PATCH 1/2] Ability to display Carousel indicators in a separate block --- docs/documentation/docs/controls/Carousel.md | 11 +- src/controls/carousel/Carousel.module.scss | 515 +++++++++--------- src/controls/carousel/Carousel.tsx | 84 +-- src/controls/carousel/ICarouselProps.ts | 24 + .../controlsTest/components/ControlsTest.tsx | 7 +- 5 files changed, 358 insertions(+), 283 deletions(-) diff --git a/docs/documentation/docs/controls/Carousel.md b/docs/documentation/docs/controls/Carousel.md index 573fd0cd7..88698f988 100644 --- a/docs/documentation/docs/controls/Carousel.md +++ b/docs/documentation/docs/controls/Carousel.md @@ -128,7 +128,9 @@ The Carousel component can be configured with the following properties: | indicatorShape | CarouselIndicatorShape | no | Specifies indicators' shape. If onRenderIndicator is provided - this property is ignored | | indicatorClassName | string | no | Specifies additional class applied to slide position indicators | | indicatorStyle | React.CSSProperties | no | Specifies additional styles applied to slide position indicators | -| onRenderIndicator | (index: number, onClick: (e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>, selectedIndex: number) => void) => JSX.Element | no | Function to render indicator element | +| onRenderIndicator | (index: number, onClick: (e: React.MouseEvent<HTMLElement> \| React.TouchEvent<HTMLElement>, selectedIndex: number) => void) => JSX.Element | no | Function to render indicator element | +| indicatorsDisplay | CarouselIndicatorsDisplay | no | Specifies display mode of the indicators. Default value `overlap`. | +| rootStyles | ICssInput | no | Allows to specify own styles for root element | enum `CarouselButtonsLocation` @@ -160,6 +162,13 @@ Provides options for carousel indicators' shape. | square | Indicators displayed as squares | | rectangle | Indicators displayed as rectangles | +enum `CarouselIndicatorsDisplay` + +Provides options for carousel indicators display mode. +| Value | Description | +| overlap | Indicators are displayed on top of the carousel content | +| block | Reserves space for indicators | + Interface `ICarouselImageProps` Allows to easily render a set of `CarouselImage` components in the carousel diff --git a/src/controls/carousel/Carousel.module.scss b/src/controls/carousel/Carousel.module.scss index 427ed0743..861be2816 100644 --- a/src/controls/carousel/Carousel.module.scss +++ b/src/controls/carousel/Carousel.module.scss @@ -1,259 +1,288 @@ @import "~office-ui-fabric-react/dist/sass/References.scss"; @keyframes slideleft { - 0% { - left: 0; - right: 0; - } - 100% { - left: -100%; - right: 100%; - } + 0% { + left: 0; + right: 0; + } + 100% { + left: -100%; + right: 100%; + } } @keyframes slideright { - 0% { - left: 0; - right: 0; - } - 100% { - left: 100%; - right: -100%; - } + 0% { + left: 0; + right: 0; + } + 100% { + left: 100%; + right: -100%; + } } @keyframes slidefromright { - 0% { - left: 100%; - right: -100%; - } - 100% { - left: 0; - right: 0; - } + 0% { + left: 100%; + right: -100%; + } + 100% { + left: 0; + right: 0; + } } @keyframes slidefromleft { - 0% { - left: -100%; - right: 100%; - } - 100% { - left: 0; - right: 0; - } + 0% { + left: -100%; + right: 100%; + } + 100% { + left: 0; + right: 0; + } +} + +.root { + display: flex; + flex-direction: column; + flex-grow: 2; +} + +.indicatorsContainer { + align-items: center; + + .indicators { + @extend .indicators; + + & > li { + background-color: "[theme: black, default: #000]"; + } + } +} + +.indicators { + z-index: 10; + display: flex; + justify-content: center; + list-style: none; + padding-inline-start: 0; + margin-block-start: 0.5em; + margin-block-end: 0.5em; + + &.rectangle > li { + width: 25px; + height: 5px; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + } + + &.square > li { + width: 10px; + height: 10px; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + } + + &.circle > li { + width: 10px; + height: 10px; + border-radius: 50%; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + } + + & > li { + flex: 0 1 auto; + text-indent: -999px; + cursor: pointer; + opacity: 0.5; + transition: opacity 0.5 ease; + margin: 0 3px; + box-sizing: content-box; + background-clip: padding-box; + + &.active { + opacity: 1; + } + } } .container { - display: flex; - - // Styles for elements container - .contentContainer { - flex-grow: 2; - position: relative; - overflow: hidden; - - .slideWrapper { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - - &.left { - left: -100%; - right: 100%; - } - - &.right { - left: 100%; - right: -100%; - } - - &.slideLeft { - animation: slideleft linear 0.6s; - } - &.slideRight { - animation: slideright linear 0.6s; - } - &.slideFromRight { - animation: slidefromright linear 0.6s; - } - &.slideFromLeft { - animation: slidefromleft linear 0.6s; - } - } - - .carouselImage { - overflow: hidden; - width: 100%; - height: 100%; - position: relative; - - .image { - width: 100%; - height: 100%; - } - - &.staticDetails { - .details { - top: 60%; - } - } - - .details { - background-color: rgba(0, 0, 0, 0.8); - box-sizing: border-box; - color: white; - height: 40%; - left: 0; - padding: 15px; - position: absolute; - right: 0; - top: 100%; - transition: all 0.5s ease; - - .title { - display: block; - @include ms-fontSize-l; - padding-bottom: 5px; - } - } - } - - &:hover { - .carouselImage.dynamicDetails { - .details { - top: 60%; - } - } - } - } - - .indicators { - position: absolute; - right: 0; - bottom: 0; - left: 0; - z-index: 10; - display: flex; - justify-content: center; - list-style: none; - - &.rectangle > li { - width: 25px; - height: 5px; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - } - - &.square > li { - width: 10px; - height: 10px; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - } - - &.circle > li { - width: 10px; - height: 10px; - border-radius: 50%; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - } - - & > li { - flex: 0 1 auto; - text-indent: -999px; - cursor: pointer; - background-color: "[theme:white, default: #fff]"; - opacity: 0.5; - transition: opacity 0.5 ease; - margin: 0 3px; - box-sizing: content-box; - background-clip: padding-box; - - &.active { - opacity: 1; - } - } - } - - .loadingComponent { - margin: auto; - } - - // Bottons containers - .buttonLocations { - cursor: pointer; - flex-direction: column; - } - - .centralButtonsContainer { - @extend .buttonLocations; - justify-content: center; - } - .topButtonsContainer { - @extend .buttonLocations; - justify-content: left; - } - .bottomButtonsContainer { - @extend .buttonLocations; - justify-content: left; - flex-direction: column-reverse; - } - - // ButtonContainer display mode - .buttonsOnlyPrevButton { - position: absolute; - left: 0; - z-index: 1; - } - .buttonsOnlyPrevButton:hover { - cursor: pointer; - } - - // Buttons styles - .buttonsOnlyNextButton { - position: absolute; - left: -32px; - z-index: 1; - } - .buttonsOnlyNextButton:hover { - cursor: pointer; - } - - .buttonsContainer { - display: flex; - align-items: center; - background-color: transparent; - cursor: pointer; - } - .buttonsOnlyContainer { - @extend .buttonsContainer; - position: relative; - width: 0px; - } - - .blockButtonsContainer { - @extend .buttonsContainer; - min-height: 100%; - min-width: 32px; - } - .blockButtonsContainer:hover { - @extend .buttonsContainer; - background-color: #f4f4f4; - opacity: 0.5; - } - - .hiddenButtonsContainer { - @extend .buttonsContainer; - min-height: 100%; - min-width: 32px; - opacity: 0; - } - .hiddenButtonsContainer:hover { - opacity: 0.5; - background-color: #f4f4f4; - } + display: flex; + + // Styles for elements container + .contentContainer { + flex-grow: 2; + position: relative; + overflow: hidden; + + .slideWrapper { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + + &.left { + left: -100%; + right: 100%; + } + + &.right { + left: 100%; + right: -100%; + } + + &.slideLeft { + animation: slideleft linear 0.6s; + } + &.slideRight { + animation: slideright linear 0.6s; + } + &.slideFromRight { + animation: slidefromright linear 0.6s; + } + &.slideFromLeft { + animation: slidefromleft linear 0.6s; + } + } + + .carouselImage { + overflow: hidden; + width: 100%; + height: 100%; + position: relative; + + .image { + width: 100%; + height: 100%; + } + + &.staticDetails { + .details { + top: 60%; + } + } + + .details { + background-color: rgba(0, 0, 0, 0.8); + box-sizing: border-box; + color: white; + height: 40%; + left: 0; + padding: 15px; + position: absolute; + right: 0; + top: 100%; + transition: all 0.5s ease; + + .title { + display: block; + @include ms-fontSize-l; + padding-bottom: 5px; + } + } + } + + .indicators { + @extend .indicators; + + position: absolute; + right: 0; + bottom: 0; + left: 0; + + & > li { + background-color: "[theme:white, default: #fff]"; + } + } + + &:hover { + .carouselImage.dynamicDetails { + .details { + top: 60%; + } + } + } + } + + .loadingComponent { + margin: auto; + } + + // Bottons containers + .buttonLocations { + cursor: pointer; + flex-direction: column; + } + + .centralButtonsContainer { + @extend .buttonLocations; + justify-content: center; + } + .topButtonsContainer { + @extend .buttonLocations; + justify-content: left; + } + .bottomButtonsContainer { + @extend .buttonLocations; + justify-content: left; + flex-direction: column-reverse; + } + + // ButtonContainer display mode + .buttonsOnlyPrevButton { + position: absolute; + left: 0; + z-index: 1; + } + .buttonsOnlyPrevButton:hover { + cursor: pointer; + } + + // Buttons styles + .buttonsOnlyNextButton { + position: absolute; + left: -32px; + z-index: 1; + } + .buttonsOnlyNextButton:hover { + cursor: pointer; + } + + .buttonsContainer { + display: flex; + align-items: center; + background-color: transparent; + cursor: pointer; + } + .buttonsOnlyContainer { + @extend .buttonsContainer; + position: relative; + width: 0px; + } + + .blockButtonsContainer { + @extend .buttonsContainer; + min-height: 100%; + min-width: 32px; + } + .blockButtonsContainer:hover { + @extend .buttonsContainer; + background-color: #f4f4f4; + opacity: 0.5; + } + + .hiddenButtonsContainer { + @extend .buttonsContainer; + min-height: 100%; + min-width: 32px; + opacity: 0; + } + .hiddenButtonsContainer:hover { + opacity: 0.5; + background-color: #f4f4f4; + } } diff --git a/src/controls/carousel/Carousel.tsx b/src/controls/carousel/Carousel.tsx index 5c080c593..e9f908ca0 100644 --- a/src/controls/carousel/Carousel.tsx +++ b/src/controls/carousel/Carousel.tsx @@ -12,6 +12,7 @@ import { Spinner } from "office-ui-fabric-react/lib/Spinner"; import { isArray } from "@pnp/common"; import * as telemetry from '../../common/telemetry'; import CarouselImage from "./CarouselImage"; +import { CarouselIndicatorsDisplay } from "./ICarouselProps"; export class Carousel extends React.Component { private _intervalId: number | undefined; @@ -67,7 +68,9 @@ export class Carousel extends React.Component { nextButtonIconName = 'ChevronRight', loadingComponent = , pauseOnHover, - interval + interval, + indicatorsDisplay, + rootStyles } = this.props; const processing = processingState === ProcessingState.processing; @@ -78,43 +81,48 @@ export class Carousel extends React.Component { const element = this.getElementToDisplay(currentIndex); return ( -
-
{ if (!prevButtonDisabled) { this.onCarouselButtonClicked(false); } }} > - { this.onCarouselButtonClicked(false); }} /> -
- -
- { - processing && -
- {loadingComponent} -
- } - - { - !processing && this.renderSlide(element) - } - {this.getIndicatorsElement()} -
- -
{ if (!nextButtonDisabled) { this.onCarouselButtonClicked(true); } }}> - { this.onCarouselButtonClicked(true); }} /> +
+
+
{ if (!prevButtonDisabled) { this.onCarouselButtonClicked(false); } }} > + { this.onCarouselButtonClicked(false); }} /> +
+
+ { + processing && +
+ {loadingComponent} +
+ } + + { + !processing && this.renderSlide(element) + } + {indicatorsDisplay !== CarouselIndicatorsDisplay.block && this.getIndicatorsElement()} +
+ +
{ if (!nextButtonDisabled) { this.onCarouselButtonClicked(true); } }}> + { this.onCarouselButtonClicked(true); }} /> +
+ {indicatorsDisplay === CarouselIndicatorsDisplay.block && +
+ {this.getIndicatorsElement()} +
}
); } @@ -342,7 +350,7 @@ export class Carousel extends React.Component { if (nextButtonClicked && this.props.onMoveNextClicked) { this.props.onMoveNextClicked(nextIndex); } - else if(this.props.onMovePrevClicked) { + else if (this.props.onMovePrevClicked) { this.props.onMovePrevClicked(nextIndex); } } diff --git a/src/controls/carousel/ICarouselProps.ts b/src/controls/carousel/ICarouselProps.ts index 3d06a780d..3e1c57343 100644 --- a/src/controls/carousel/ICarouselProps.ts +++ b/src/controls/carousel/ICarouselProps.ts @@ -46,6 +46,20 @@ export enum CarouselIndicatorShape { rectangle } +/** + * Provides options for carousel indicators display mode + */ +export enum CarouselIndicatorsDisplay { + /** + * Indicators are displayed on top of the carousel content + */ + overlap = 1, + /** + * Reserves space for indicators + */ + block +} + export interface ICarouselProps { /** * Specifies the initial index of the element to be displayed. @@ -183,4 +197,14 @@ export interface ICarouselProps { */ onRenderIndicator?: (index: number, onClick: (e: React.MouseEvent | React.TouchEvent, selectedIndex: number) => void) => JSX.Element; + /** + * Specifies display mode of the indicators + */ + indicatorsDisplay?: CarouselIndicatorsDisplay; + + /** + * Allows to specify own styles for root element. + */ + rootStyles?: ICssInput; + } diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index 02d6d39dd..7e59e3bd7 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -33,7 +33,7 @@ import { TermLabelAction, TermActionsDisplayMode } from '../../../controls/taxon import { ListItemAttachments } from '../../../ListItemAttachments'; import { RichText } from '../../../RichText'; import { Link } from 'office-ui-fabric-react/lib/components/Link'; -import { Carousel, CarouselButtonsLocation, CarouselButtonsDisplay, CarouselIndicatorShape } from '../../../controls/carousel'; +import { Carousel, CarouselButtonsLocation, CarouselButtonsDisplay, CarouselIndicatorShape, CarouselIndicatorsDisplay } from '../../../controls/carousel'; import { TimeDisplayControlType } from '../../../controls/dateTimePicker/TimeDisplayControlType'; import { GridLayout } from '../../../GridLayout'; import { ComboBoxListItemPicker } from '../../../controls/listItemPicker/ComboBoxListItemPicker'; @@ -61,6 +61,7 @@ import { Pagination } from '../../../controls/pagination'; import CarouselImage from '../../../controls/carousel/CarouselImage'; import { FieldCollectionData, CustomCollectionFieldType } from '../../../FieldCollectionData'; import { Accordion } from '../../..'; +import { mergeStyles } from 'office-ui-fabric-react/lib/Styling'; /** * The sample data below was randomly generated (except for the title). It is used by the grid layout @@ -1175,6 +1176,7 @@ export default class ControlsTest extends React.Component { console.log(`Next button clicked: ${index}`); }} onMovePrevClicked={(index: number) => { console.log(`Prev button clicked: ${index}`); }} + rootStyles={mergeStyles({ + backgroundColor: '#C3C3C3' + })} />
From c79a19b8e253284572a39de4b129dc6193b28175 Mon Sep 17 00:00:00 2001 From: Alex Terentiev Date: Mon, 5 Oct 2020 17:04:02 -0700 Subject: [PATCH 2/2] indicatorsContainerStyles --- docs/documentation/docs/controls/Carousel.md | 1 + src/controls/carousel/Carousel.tsx | 5 +++-- src/controls/carousel/ICarouselProps.ts | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/documentation/docs/controls/Carousel.md b/docs/documentation/docs/controls/Carousel.md index 88698f988..9332937da 100644 --- a/docs/documentation/docs/controls/Carousel.md +++ b/docs/documentation/docs/controls/Carousel.md @@ -131,6 +131,7 @@ The Carousel component can be configured with the following properties: | onRenderIndicator | (index: number, onClick: (e: React.MouseEvent<HTMLElement> \| React.TouchEvent<HTMLElement>, selectedIndex: number) => void) => JSX.Element | no | Function to render indicator element | | indicatorsDisplay | CarouselIndicatorsDisplay | no | Specifies display mode of the indicators. Default value `overlap`. | | rootStyles | ICssInput | no | Allows to specify own styles for root element | +| indicatorsContainerStyles | ICssInput | no | Allows to specify own styles for indicators container when indicatorsDisplay is set to "block" | enum `CarouselButtonsLocation` diff --git a/src/controls/carousel/Carousel.tsx b/src/controls/carousel/Carousel.tsx index e9f908ca0..2f5436c61 100644 --- a/src/controls/carousel/Carousel.tsx +++ b/src/controls/carousel/Carousel.tsx @@ -70,7 +70,8 @@ export class Carousel extends React.Component { pauseOnHover, interval, indicatorsDisplay, - rootStyles + rootStyles, + indicatorsContainerStyles } = this.props; const processing = processingState === ProcessingState.processing; @@ -120,7 +121,7 @@ export class Carousel extends React.Component {
{indicatorsDisplay === CarouselIndicatorsDisplay.block && -
+
{this.getIndicatorsElement()}
}
diff --git a/src/controls/carousel/ICarouselProps.ts b/src/controls/carousel/ICarouselProps.ts index 3e1c57343..560721c9b 100644 --- a/src/controls/carousel/ICarouselProps.ts +++ b/src/controls/carousel/ICarouselProps.ts @@ -207,4 +207,9 @@ export interface ICarouselProps { */ rootStyles?: ICssInput; + /** + * Allows to specify own styles for indicators container when indicatorsDisplay is set to "block". + */ + indicatorsContainerStyles?: ICssInput; + }