Skip to content

Commit e59ff54

Browse files
authored
CIF-2706: Improve Carousel to support margins and other layout gaps (#830)
* improve carousel frontend logic Instead of navigating based on the fixed width of the cards, the transitions are now between the left upper corners of them. This does also not require to calculate the full width of the content anymore as a large horizontal rail is given by css * support for rtl in the carousel * add tests for rtl, fixed contentWidth calculation * better handle fractional scrolling * use only absolute max-widths for demo components * imporve responsive styling of the carousel in the examples * adapt ui tests to new responsive styling of the carousel
1 parent dc8c4f4 commit e59ff54

10 files changed

Lines changed: 415 additions & 108 deletions

File tree

examples/ui.apps/src/main/content/jcr_root/apps/cif-components-examples/clientlibs/venia-theme/cif-demo.css

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,46 @@
1414
~ limitations under the License.
1515
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
1616
.cif-demo .productteaser {
17-
width: 50%;
17+
max-width: 360px;
1818
margin: 0 auto;
1919
}
2020

2121
.cif-demo .productteaser .item__image {
2222
border: none !important;
2323
}
2424

25-
.cif-demo .productcarousel__container {
26-
max-width: 50%;
25+
.cif-demo .productcarousel__container,
26+
.cif-demo .carousel__container {
27+
/* 3x the item width, 4 items */
28+
max-width: 720px;
29+
width: 100%;
2730
}
2831

29-
.cif-demo .carousel__container {
30-
max-width: 50%;
32+
/*
33+
* there is a layout gap for 32px left and right of the container
34+
* and for a viewport >= 1024px there is the navigation of 280px width
35+
*/
36+
37+
/* 720 + 64 + 280 */
38+
@media screen and (max-width:1064px) {
39+
.cif-demo .productcarousel__container {
40+
max-width: 480px;
41+
}
42+
}
43+
44+
/* 480 + 64 */
45+
@media screen and (max-width:544px) {
46+
.cif-demo .productcarousel__container {
47+
max-width: 240px;
48+
}
49+
}
50+
51+
/* undo the styles from the core components */
52+
53+
@media screen and (min-width:790px) {
54+
.cif-demo .productcarousel__container {
55+
width: 100%;
56+
}
3157
}
3258

3359
.cif-demo .searchBar__root {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#base=css
22

3-
carousel.css
3+
carousel.css
4+
rtl.css

ui.apps/src/main/content/jcr_root/apps/core/cif/components/commerce/carousel/v1/carousel/clientlibs/css/carousel.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@
5959

6060
.carousel__cardscontainer {
6161
margin: auto;
62+
/*
63+
* the cardscontainer is within the cardsroot with overflow hidden.
64+
* in the default style with a card width of 240px the 12000px width
65+
* give enough space for 41 cards, this should be sufficient for all
66+
* practical cases.
67+
*/
68+
width: 12000px;
69+
transition-property: margin-left, margin-right;
70+
transition-duration: 300ms;
71+
transition-timing-function: linear;
6272
}
6373

6474
.carousel__cardsroot {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2+
~ Copyright 2022 Adobe
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
16+
17+
[dir='rtl'] .carousel__container {
18+
direction: rtl;
19+
}
20+
21+
[dir='rtl'] .carousel__btn--prev {
22+
-moz-transform: rotate(225deg);
23+
-webkit-transform: rotate(225deg);
24+
transform: rotate(225deg);
25+
right: 0;
26+
left: unset;
27+
}
28+
29+
[dir='rtl'] .carousel__btn--next {
30+
-moz-transform: rotate(135deg);
31+
-webkit-transform: rotate(135deg);
32+
transform: rotate(135deg);
33+
left: 0;
34+
right: unset;
35+
}

ui.apps/src/main/content/jcr_root/apps/core/cif/components/commerce/carousel/v1/carousel/clientlibs/js/carousel.js

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,27 @@
2020
*/
2121
class Carousel {
2222
constructor(rootElement, selectors = Carousel.selectors) {
23-
this._cardsContainer = rootElement.querySelector(selectors.container);
24-
if (!this._cardsContainer) {
23+
const cardsContainer = rootElement.querySelector(selectors.container);
24+
if (!cardsContainer) {
2525
// the carousel is empty
2626
return;
2727
}
2828

29+
this._cardsContainer = cardsContainer;
30+
2931
// Re-calculate carousel state when the window size changes
3032
this._calculate = this._calculate.bind(this);
3133
window.addEventListener('resize', this._calculate);
3234

33-
this._speed = 300;
34-
this._delay = 0;
35-
this._effect = 'linear';
36-
this._carousel_root = rootElement.querySelector(selectors.root);
35+
this._currentPos = 0;
36+
this._currentOffset = 0;
3737
this._currentRootWidth = 0;
38+
this._carousel_root = rootElement.querySelector(selectors.root);
3839
this._carousel_parent = rootElement.querySelector(selectors.parent);
39-
this._cards = this._cardsContainer.querySelectorAll(selectors.card);
40+
this._cards = cardsContainer.querySelectorAll(selectors.card);
4041
this._btnPrev = rootElement.querySelector(selectors.btnPrev);
4142
this._btnNext = rootElement.querySelector(selectors.btnNext);
42-
this._currentPos = 0;
43+
this._direction = getComputedStyle(cardsContainer).direction;
4344

4445
this._calculate();
4546

@@ -49,80 +50,122 @@ class Carousel {
4950

5051
_calculate() {
5152
// Only re-calculate when one of the screen size breakpoints changes the size of the component
52-
if (this._carousel_root.offsetWidth == this._currentRootWidth) {
53+
if (this._cards.length === 0 || this._carousel_root.offsetWidth == this._currentRootWidth) {
5354
return;
5455
}
5556

56-
this._minPos = this._carousel_parent.offsetWidth - this._cards.length * this._cards[0].offsetWidth;
57-
this._cardsContainer.style.width = this._cards[0].offsetWidth * this._cards.length + 'px';
58-
this._maxPosIndex =
59-
(this._cardsContainer.offsetWidth - this._carousel_parent.offsetWidth) / this._cards[0].offsetWidth;
60-
61-
if (this._minPos >= 0) {
57+
const lastCard = this._cards[this._cards.length - 1];
58+
const lastCardBB = lastCard.getBoundingClientRect();
59+
const lastCardStyle = getComputedStyle(lastCard);
60+
const firstCard = this._cards[0];
61+
const firstCardBB = firstCard.getBoundingClientRect();
62+
const firstCardStyle = getComputedStyle(firstCard);
63+
const contentWidth =
64+
this._direction === 'ltr'
65+
? lastCardBB.right +
66+
parseInt(lastCardStyle.marginRight) -
67+
(firstCardBB.left + parseInt(firstCardStyle.marginLeft))
68+
: firstCardBB.right +
69+
parseInt(firstCardStyle.marginRight) -
70+
(lastCardBB.left + parseInt(lastCardStyle.marginLeft));
71+
72+
if (this._carousel_parent.offsetWidth >= contentWidth) {
6273
// Hide buttons if all fit on the screen
6374
this._btnNext.style.display = 'none';
6475
this._btnPrev.style.display = 'none';
6576
} else {
6677
this._btnNext.style.display = 'block';
6778
this._btnPrev.style.display = 'block';
68-
this._btnPrev.disabled = true;
6979
}
7080

7181
// Reset carousel to first item
72-
this._goToCard(0, 'reset');
82+
this._goToCard(0);
7383
this._currentRootWidth = this._carousel_root.offsetWidth;
7484
}
7585

7686
// click event handler for Next Button
7787
_goToNextCard() {
7888
if (this._btnNext.disabled === false) {
79-
var newCurrentPos = 0;
80-
newCurrentPos = this._currentPos + 1;
81-
this._goToCard(newCurrentPos, 'next');
89+
this._goToCard(this._currentPos + 1);
8290
}
8391
}
8492

8593
// Click event handler for Prev Button
8694
_goToPrevCard() {
8795
if (this._btnPrev.disabled === false) {
88-
var newCurrentPos = 0;
89-
newCurrentPos = this._currentPos - 1;
90-
this._goToCard(newCurrentPos, 'prev');
96+
this._goToCard(this._currentPos - 1);
9197
}
9298
}
9399

94100
// create card and transition
95-
_goToCard(n, dir) {
96-
var cardwidth = this._cards[0].offsetWidth,
97-
currentPos = Math.max(-cardwidth * this._currentPos, this._minPos),
98-
scrollWidth = cardwidth,
99-
newPos;
100-
101-
if (dir === 'next') {
102-
newPos = Math.max(this._minPos, currentPos - scrollWidth);
103-
} else if (dir === 'prev') {
104-
newPos = Math.min(0, currentPos + scrollWidth);
105-
} else {
106-
newPos = 0;
101+
_goToCard(nextPos) {
102+
if (nextPos < 0) {
103+
// index out of bounds
104+
return;
105+
}
106+
107+
const lastCard = this._cards[this._cards.length - 1];
108+
const lastCardBB = lastCard.getBoundingClientRect();
109+
const carouselParentBB = this._carousel_parent.getBoundingClientRect();
110+
111+
if (
112+
nextPos > this._currentPos &&
113+
((this._direction === 'ltr' && lastCardBB.right <= carouselParentBB.right) ||
114+
(this._direction === 'rtl' && lastCardBB.left >= carouselParentBB.left))
115+
) {
116+
return;
107117
}
108118

109-
this._cardsContainer.style.transition =
110-
'margin-left ' + this._speed + 'ms' + ' ' + this._effect + ' ' + this._delay + 'ms';
111-
this._cardsContainer.style.marginLeft = newPos == 0 && this._minPos >= 0 ? 'auto' : newPos + 'px';
112-
this._currentPos = n;
119+
const firstCard = this._cards[0];
120+
const firstCardBB = firstCard.getBoundingClientRect();
121+
const currentCard = this._cards[this._currentPos];
122+
const currentCardBB = currentCard.getBoundingClientRect();
123+
const targetCard = this._cards[nextPos];
124+
const targetCardBB = targetCard.getBoundingClientRect();
125+
126+
// difference that needs to be added on the margin-left
127+
let offsetDiff;
128+
let newOffset;
129+
let diffCarouselToLast;
130+
let diffCarouselToFirst;
131+
132+
if (this._direction === 'rtl') {
133+
offsetDiff = currentCardBB.right - targetCardBB.right;
134+
diffCarouselToLast = carouselParentBB.left - lastCardBB.left;
135+
diffCarouselToFirst = carouselParentBB.right - firstCardBB.right;
136+
} else {
137+
offsetDiff = currentCardBB.left - targetCardBB.left;
138+
diffCarouselToLast = carouselParentBB.right - lastCardBB.right;
139+
diffCarouselToFirst = carouselParentBB.left - firstCardBB.left;
140+
}
113141

114-
this._btnNext.disabled = false;
115-
this._btnPrev.disabled = false;
142+
if (nextPos > this._currentPos && Math.abs(diffCarouselToLast) < Math.abs(offsetDiff)) {
143+
// navigating forward to the last card (fractional)
144+
offsetDiff = diffCarouselToLast;
145+
} else if (nextPos < this._currentPos && Math.abs(diffCarouselToFirst) < Math.abs(offsetDiff)) {
146+
// navigating backward to the second-last card (fractional)
147+
offsetDiff = diffCarouselToFirst;
148+
}
116149

117-
if (this._currentPos >= this._maxPosIndex) {
118-
this._btnNext.disabled = true;
119-
this._btnPrev.disabled = false;
150+
if (this._direction === 'ltr') {
151+
newOffset = this._currentOffset + offsetDiff;
152+
this._cardsContainer.style.marginLeft = newOffset + 'px';
153+
} else {
154+
newOffset = this._currentOffset - offsetDiff;
155+
this._cardsContainer.style.marginRight = newOffset + 'px';
120156
}
121157

122-
if (this._currentPos <= 0) {
123-
this._btnPrev.disabled = true;
124-
this._btnNext.disabled = false;
158+
this._currentPos = nextPos;
159+
this._currentOffset = newOffset;
160+
161+
// disable _btnNext when the last card is in the carousel parent
162+
if (this._direction === 'ltr') {
163+
this._btnNext.disabled = lastCardBB.right <= carouselParentBB.right - offsetDiff;
164+
} else {
165+
this._btnNext.disabled = lastCardBB.left >= carouselParentBB.left - offsetDiff;
125166
}
167+
// disable _btnPrev when the we are at the first card
168+
this._btnPrev.disabled = this._currentPos == 0;
126169
}
127170
}
128171

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#base=css
22

3-
productcarousel.css
3+
productcarousel.css
4+
rtl.css

ui.apps/src/main/content/jcr_root/apps/core/cif/components/commerce/productcarousel/v1/productcarousel/clientlibs/css/productcarousel.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@
6565

6666
.productcarousel__cardscontainer {
6767
margin: auto;
68+
width: 12000px;
69+
transition-property: margin-left, margin-right;
70+
transition-duration: 300ms;
71+
transition-timing-function: linear;
6872
}
6973

7074
.productcarousel__container {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2+
~ Copyright 2022 Adobe
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
16+
17+
[dir='rtl'] .productcarousel__container {
18+
direction: rtl;
19+
}
20+
21+
[dir='rtl'] .productcarousel__btn--prev {
22+
-moz-transform: rotate(225deg);
23+
-webkit-transform: rotate(225deg);
24+
transform: rotate(225deg);
25+
right: 0;
26+
left: unset;
27+
}
28+
29+
[dir='rtl'] .productcarousel__btn--next {
30+
-moz-transform: rotate(135deg);
31+
-webkit-transform: rotate(135deg);
32+
transform: rotate(135deg);
33+
left: 0;
34+
right: unset;
35+
}

0 commit comments

Comments
 (0)