-
Notifications
You must be signed in to change notification settings - Fork 203
[Clover-402][BpkCardList] Update BpkCardList scroll behaviour #3878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
36eaa5c
0b77061
6f9f6b3
dd1482c
97e4a01
4d28c49
ce423ef
09e0133
ba9c39d
2b3bcd7
fbb380b
13f1c47
1875386
9846376
9df07f2
63dd429
153ff48
9f8b1c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -129,7 +129,6 @@ const commonProps = { | |||||
), | ||||||
buttonContent: 'See more', | ||||||
buttonHref: 'https://www.skyscanner.net/', | ||||||
initiallyShownCardsMobile: 2, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You removed this value, but I saw there is an example of giving
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed it here is because I change mobile default But in
|
||||||
accessibilityLabels: { | ||||||
indicatorLabel: 'Go to slide', | ||||||
prevNavLabel: 'Previous slide', | ||||||
|
@@ -152,7 +151,6 @@ const snippetProps = { | |||||
}; | ||||||
|
||||||
const DestinationCard = (i: number) => ( | ||||||
// Usage Suggestion: define minWidth from consumer side by using className | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
<BpkCard | ||||||
key={`card-${i}`} | ||||||
className={STYLES['bpk-card']} | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,12 +17,17 @@ | |
*/ | ||
|
||
@use '../../unstable__bpk-mixins/tokens'; | ||
@use '../../unstable__bpk-mixins/breakpoints'; | ||
|
||
.bpk-card-list { | ||
display: flex; | ||
flex-direction: column; | ||
gap: tokens.bpk-spacing-lg(); | ||
|
||
@include breakpoints.bpk-breakpoint-mobile { | ||
gap: tokens.bpk-spacing-base(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on design, if mobile gap change to |
||
} | ||
|
||
&--card-list { | ||
display: flex; | ||
flex-direction: column; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,7 +34,8 @@ import STYLES from './BpkCardList.module.scss'; | |
|
||
const getClassName = cssModules(STYLES); | ||
|
||
const DEFAULT_ITEMS = 3; | ||
const DEFAULT_ITEMS_DESKTOP = 3; | ||
const DEFAULT_ITEMS_MOBILE = 2; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on design |
||
|
||
const BpkCardList = (props: CardListProps) => { | ||
const { | ||
|
@@ -47,8 +48,8 @@ const BpkCardList = (props: CardListProps) => { | |
chipGroup, | ||
description, | ||
expandText, | ||
initiallyShownCardsDesktop = DEFAULT_ITEMS, | ||
initiallyShownCardsMobile = DEFAULT_ITEMS, | ||
initiallyShownCardsDesktop = DEFAULT_ITEMS_DESKTOP, | ||
initiallyShownCardsMobile = DEFAULT_ITEMS_MOBILE, | ||
layoutDesktop, | ||
layoutMobile, | ||
onButtonClick, | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -17,27 +17,33 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
@use '../../../unstable__bpk-mixins/tokens'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
@use '../../../unstable__bpk-mixins/breakpoints'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
@use '../../../unstable__bpk-mixins/utils'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
.bpk-card-list-row-rail { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
&__row, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
&__rail { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
padding-top: tokens.bpk-spacing-sm(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
padding-bottom: tokens.bpk-spacing-sm(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
overflow-x: scroll; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
overflow-x: hidden; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In desktop, scroll card by wheel/touchpad will change currentIndex, And pagination will also change the currentIndex. It is rather difficult to keep the two in perfect synchronization, and the functionality is redundant with that of pagination so disable this feature. |
||||||||||||||||||||||||||||||||||||||||||||||||||||
box-sizing: border-box; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
gap: tokens.bpk-spacing-sm(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
scroll-snap-stop: always; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
scroll-snap-type: x mandatory; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
scrollbar-width: none; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
@include breakpoints.bpk-breakpoint-mobile { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
overflow-x: scroll; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nitpick] There are two separate
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
&::-webkit-scrollbar { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
display: none; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
&__card { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
position: relative; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
padding: tokens.bpk-spacing-md(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
padding: 0 tokens.bpk-spacing-md(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||||||||||||||||||||||||||||
flex: 0 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
calc( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -52,6 +58,30 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
overflow: visible; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
box-sizing: border-box; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
scroll-snap-align: start; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
@include breakpoints.bpk-breakpoint-mobile { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
// On mobile, if there is more than one card in the viewport, the last card only shows partially, so subtract 0.8 when do the calculation | ||||||||||||||||||||||||||||||||||||||||||||||||||||
flex: 0 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
calc( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
100% - | ||||||||||||||||||||||||||||||||||||||||||||||||||||
( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
tokens.bpk-spacing-md() * | ||||||||||||||||||||||||||||||||||||||||||||||||||||
(var(--initially-shown-cards, 3) - 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
) / | ||||||||||||||||||||||||||||||||||||||||||||||||||||
max(1, (var(--initially-shown-cards, 3) - 0.8)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain the key numbers in this code and provide a note to help others understand it for future iterations? 🙇♀️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
&:first-child { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
padding-left: tokens.bpk-spacing-sm(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
@include utils.bpk-rtl { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
padding-right: tokens.bpk-spacing-sm(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
padding-left: tokens.bpk-spacing-md(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,15 +16,23 @@ | |
* limitations under the License. | ||
*/ | ||
import type { CSSProperties } from 'react'; | ||
import { useRef, useState, useEffect, isValidElement, Children } from 'react'; | ||
import { | ||
useRef, | ||
useState, | ||
useEffect, | ||
isValidElement, | ||
Children, | ||
useMemo, | ||
} from 'react'; | ||
|
||
import throttle from 'lodash/throttle'; | ||
|
||
import { cssModules } from '../../../bpk-react-utils'; | ||
|
||
import { RENDER_BUFFER_SIZE } from './constants'; | ||
import { | ||
lockScroll, | ||
setA11yTabIndex, | ||
useUpdateCurrentIndexByVisibility, | ||
useScrollToCard, | ||
useIntersectionObserver, | ||
} from './utils'; | ||
|
@@ -44,7 +52,6 @@ const BpkCardListCarousel = (props: CardListCarouselProps) => { | |
initiallyShownCards, | ||
isMobile = false, | ||
layout, | ||
setCurrentIndex, | ||
slideLabel = (index: number, childrenLength: number) => | ||
`slide ${index + 1} of ${childrenLength}`, | ||
} = props; | ||
|
@@ -59,7 +66,11 @@ const BpkCardListCarousel = (props: CardListCarouselProps) => { | |
|
||
const childrenLength = Children.count(children); | ||
const [root, setRoot] = useState<HTMLElement | null>(null); | ||
const [, forceUpdate] = useState(0); | ||
const cardRefs = useRef<Array<HTMLDivElement | null>>([]); | ||
const hasBeenVisibleRef = useRef<Set<number>>(new Set()); | ||
const firstCardWidthRef = useRef<number | null>(null); | ||
const firstCardHeightRef = useRef<number | null>(null); | ||
|
||
const [visibilityList, setVisibilityList] = useState<number[]>( | ||
Array(childrenLength).fill(0), | ||
|
@@ -73,6 +84,56 @@ const BpkCardListCarousel = (props: CardListCarouselProps) => { | |
setVisibilityList, | ||
); | ||
|
||
useScrollToCard( | ||
currentIndex * initiallyShownCards, | ||
root, | ||
cardRefs, | ||
stateScrollingLockRef, | ||
); | ||
|
||
// Similar to Virtual Scrolling to improve performance | ||
const firstVisibleIndex = Math.max(0, visibilityList.indexOf(1)); | ||
const lastVisibleIndex = firstVisibleIndex + initiallyShownCards - 1; | ||
const renderList = useMemo( | ||
() => | ||
visibilityList.map((_, index) => | ||
index >= firstVisibleIndex - RENDER_BUFFER_SIZE && | ||
index <= lastVisibleIndex + RENDER_BUFFER_SIZE | ||
? 1 | ||
: 0, | ||
), | ||
[visibilityList, firstVisibleIndex, lastVisibleIndex], | ||
); | ||
|
||
const cardRefFns = useMemo( | ||
() => | ||
Array(childrenLength) | ||
.fill(null) | ||
.map((_, i) => (el: HTMLDivElement | null) => { | ||
cardRefs.current[i] = el; | ||
observerVisibility(el, i); | ||
setA11yTabIndex(el, i, visibilityList); | ||
// record the first card's width and height when it becomes visible | ||
if (el && visibilityList[i] === 0) { | ||
hasBeenVisibleRef.current.add(i); | ||
if (firstCardWidthRef.current == null && el.offsetWidth) { | ||
firstCardWidthRef.current = el.offsetWidth; | ||
} | ||
if (firstCardHeightRef.current == null && el.offsetHeight) { | ||
firstCardHeightRef.current = el.offsetHeight; | ||
} | ||
} | ||
}), | ||
[ | ||
childrenLength, | ||
observerVisibility, | ||
visibilityList, | ||
hasBeenVisibleRef, | ||
firstCardWidthRef, | ||
firstCardHeightRef, | ||
], | ||
); | ||
|
||
useEffect(() => { | ||
const container = root; | ||
if (isMobile || !container) return undefined; | ||
|
@@ -93,25 +154,25 @@ const BpkCardListCarousel = (props: CardListCarouselProps) => { | |
}; | ||
}, [root]); | ||
|
||
useUpdateCurrentIndexByVisibility( | ||
isMobile, | ||
visibilityList, | ||
setCurrentIndex, | ||
stateScrollingLockRef, | ||
openSetStateLockTimeoutRef, | ||
); | ||
useEffect(() => { | ||
// update hasBeenVisibleRef to include the range of cards that should be visible | ||
const start = currentIndex * initiallyShownCards; | ||
const end = start + initiallyShownCards + RENDER_BUFFER_SIZE; | ||
for (let i = start; i < end && i < childrenLength; i += 1) { | ||
hasBeenVisibleRef.current.add(i); | ||
} | ||
}, [currentIndex, initiallyShownCards, childrenLength]); | ||
|
||
useScrollToCard(currentIndex, root, cardRefs, stateScrollingLockRef); | ||
useEffect(() => { | ||
const handleResize = throttle(() => { | ||
firstCardWidthRef.current = null; | ||
firstCardHeightRef.current = null; | ||
forceUpdate((n) => n + 1); | ||
}, 200); | ||
|
||
// Similar to Virtual Scrolling to improve performance | ||
const firstVisibleIndex = Math.max(0, visibilityList.indexOf(1)); | ||
const lastVisibleIndex = firstVisibleIndex + initiallyShownCards - 1; | ||
const renderList = visibilityList.map((_, index) => | ||
index >= firstVisibleIndex - RENDER_BUFFER_SIZE && | ||
index <= lastVisibleIndex + RENDER_BUFFER_SIZE | ||
? 1 | ||
: 0, | ||
); | ||
window.addEventListener('resize', handleResize); | ||
return () => window.removeEventListener('resize', handleResize); | ||
}, []); | ||
|
||
return ( | ||
<div | ||
|
@@ -127,30 +188,43 @@ const BpkCardListCarousel = (props: CardListCarouselProps) => { | |
{children.map((card, index) => { | ||
if (!isValidElement(card)) return null; | ||
|
||
const cardRefCallback = (el: HTMLDivElement | null) => { | ||
cardRefs.current[index] = el; | ||
observerVisibility(el, index); | ||
setA11yTabIndex(el, index, visibilityList); | ||
}; | ||
|
||
const cardStyle: CSSProperties = isMobile | ||
? { | ||
...shownNumberStyle, | ||
visibility: renderList[index] === 1 ? 'visible' : 'hidden', // for mobile, {renderList[index] === 1 && card} will cause reflowing and bugs, implementing visibility improvement instead | ||
} | ||
: shownNumberStyle; | ||
const cardDimensionStyle: CSSProperties = {}; | ||
if (firstCardWidthRef.current) { | ||
cardDimensionStyle.width = `${firstCardWidthRef.current}px`; | ||
} | ||
if (firstCardHeightRef.current) { | ||
cardDimensionStyle.height = `${firstCardHeightRef.current}px`; | ||
} | ||
|
||
// Only render cards that are within the renderList range or have been visible before | ||
if (renderList[index] !== 1 && !hasBeenVisibleRef.current.has(index)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the current card has not entered the visible area, replace it with an empty element. The empty element should be consistent with the card width/height to avoid reflow. |
||
return ( | ||
<div | ||
key={`placeholder-${index.toString()}`} | ||
style={{ | ||
...shownNumberStyle, | ||
...cardDimensionStyle, | ||
flexShrink: 0, | ||
visibility: 'hidden', | ||
}} | ||
aria-hidden="true" | ||
/> | ||
); | ||
} | ||
|
||
return ( | ||
<div | ||
className={getClassName(`bpk-card-list-row-rail__${layout}__card`)} | ||
ref={cardRefCallback} | ||
style={cardStyle} | ||
ref={cardRefFns[index]} | ||
style={{ | ||
...shownNumberStyle, | ||
...cardDimensionStyle, | ||
}} | ||
key={`carousel-card-${index.toString()}`} | ||
role="group" | ||
aria-label={slideLabel(index, childrenLength)} | ||
aria-current={index === currentIndex ? 'true' : 'false'} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 why do we remove this? By the way, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since multiple cards can be displayed on one screen currently and these cards have no primary or secondary distinctions (for example, three cards are displayed on one screen and all three are equally important), the current aria-current is not accurate: it only marks the first one, but there are three "current" ones. If aria-current is added to all three actually displayed pictures, multiple "current" designations will also cause confusion. But love to discuss more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing this is ok, aria-current="page" can be used on the pagnation dots, but not on a single slide :D |
||
> | ||
{isMobile ? card : renderList[index] === 1 && card} | ||
{card} | ||
</div> | ||
); | ||
})} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍