@skyscanner/backpack-web 38.19.1 → 38.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bpk-component-carousel/src/BpkCarousel.d.ts +1 -1
- package/bpk-component-carousel/src/BpkCarousel.js +16 -5
- package/bpk-component-carousel/src/BpkCarousel.module.css +1 -1
- package/bpk-component-carousel/src/BpkCarouselContainer.js +16 -7
- package/bpk-component-carousel/src/BpkCarouselContainer.module.css +1 -1
- package/bpk-component-carousel/src/types.d.ts +6 -0
- package/bpk-component-carousel/src/utils.d.ts +1 -0
- package/bpk-component-carousel/src/utils.js +42 -7
- package/package.json +1 -1
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Props } from './types';
|
|
2
|
-
declare const BpkCarousel: ({ bottom, images, initialImageIndex, onImageChanged, }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
declare const BpkCarousel: ({ accessibilityLabels, bottom, images, initialImageIndex, onImageChanged, }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
export default BpkCarousel;
|
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { useRef, useState } from 'react';
|
|
20
|
+
import { BREAKPOINTS, useMediaQuery } from "../../bpk-component-breakpoint";
|
|
20
21
|
import BpkPageIndicator, { VARIANT } from "../../bpk-component-page-indicator";
|
|
21
22
|
import { cssModules } from "../../bpk-react-utils";
|
|
22
23
|
import BpkCarouselContainer from "./BpkCarouselContainer";
|
|
23
|
-
import { useScrollToInitialImage } from "./utils";
|
|
24
|
+
import { scrollToIndex, useScrollToInitialImage } from "./utils";
|
|
24
25
|
import STYLES from "./BpkCarousel.module.css";
|
|
25
26
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
26
27
|
const getClassName = cssModules(STYLES);
|
|
27
28
|
const BpkCarousel = ({
|
|
29
|
+
accessibilityLabels = {},
|
|
28
30
|
bottom,
|
|
29
31
|
images,
|
|
30
32
|
initialImageIndex = 0,
|
|
@@ -32,6 +34,13 @@ const BpkCarousel = ({
|
|
|
32
34
|
}) => {
|
|
33
35
|
const [shownImageIndex, updateShownImageIndex] = useState(initialImageIndex);
|
|
34
36
|
const imagesRef = useRef([]);
|
|
37
|
+
const isDesktop = useMediaQuery(BREAKPOINTS.ABOVE_TABLET);
|
|
38
|
+
const handleIndicatorClick = (e, newIndex) => {
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
let target = newIndex;
|
|
41
|
+
if (newIndex === -1) target = images.length - 1;else if (newIndex === images.length) target = 0;
|
|
42
|
+
if (target !== shownImageIndex) scrollToIndex(target, imagesRef);
|
|
43
|
+
};
|
|
35
44
|
useScrollToInitialImage(initialImageIndex, imagesRef);
|
|
36
45
|
return /*#__PURE__*/_jsxs("div", {
|
|
37
46
|
className: getClassName('bpk-carousel'),
|
|
@@ -49,10 +58,12 @@ const BpkCarousel = ({
|
|
|
49
58
|
children: /*#__PURE__*/_jsx(BpkPageIndicator, {
|
|
50
59
|
currentIndex: shownImageIndex,
|
|
51
60
|
totalIndicators: images.length,
|
|
52
|
-
variant: VARIANT.
|
|
53
|
-
indicatorLabel: "Go to slide",
|
|
54
|
-
prevNavLabel: "Previous slide",
|
|
55
|
-
nextNavLabel: "Next slide"
|
|
61
|
+
variant: VARIANT.overImageSpaced,
|
|
62
|
+
indicatorLabel: accessibilityLabels.indicatorLabel ?? "Go to slide",
|
|
63
|
+
prevNavLabel: accessibilityLabels.prevNavLabel ?? "Previous slide",
|
|
64
|
+
nextNavLabel: accessibilityLabels.nextNavLabel ?? "Next slide",
|
|
65
|
+
showNav: isDesktop,
|
|
66
|
+
onClick: isDesktop ? handleIndicatorClick : () => {}
|
|
56
67
|
})
|
|
57
68
|
})]
|
|
58
69
|
});
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
|
-
.bpk-carousel{position:relative}.bpk-carousel__page-indicator-over-image{position:absolute;right:0;bottom:.25rem;left:0;display:flex;margin:auto 1rem;justify-content:center;overflow:clip}
|
|
18
|
+
.bpk-carousel{position:relative;width:100%;height:100%}.bpk-carousel__page-indicator-over-image{position:absolute;right:0;bottom:.25rem;left:0;display:flex;margin:auto 1rem;justify-content:center;overflow:clip}
|
|
@@ -35,16 +35,25 @@ const BpkScrollContainer = /*#__PURE__*/memo(({
|
|
|
35
35
|
threshold: 0.5
|
|
36
36
|
}, onImageChanged);
|
|
37
37
|
const observeCycleScroll = useIntersectionObserver(index => {
|
|
38
|
+
const container = root;
|
|
38
39
|
const imageElement = imagesRef.current && imagesRef.current[index];
|
|
39
|
-
if (imageElement) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
if (container && imageElement) {
|
|
41
|
+
// Some browsers and test environments don't support smooth scrolling,
|
|
42
|
+
// so we must fall back to simply setting scrollLeft
|
|
43
|
+
if (container.scroll && typeof container.scroll === 'function') {
|
|
44
|
+
container.scroll({
|
|
45
|
+
left: imageElement.offsetLeft,
|
|
46
|
+
behavior: 'auto'
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
container.scrollLeft = imageElement.offsetLeft;
|
|
50
|
+
}
|
|
44
51
|
}
|
|
45
|
-
},
|
|
52
|
+
},
|
|
53
|
+
// when threshold was 1, the loop behaviour (mobile) was not working as expected. This seems to fix it.
|
|
54
|
+
{
|
|
46
55
|
root,
|
|
47
|
-
threshold:
|
|
56
|
+
threshold: 0.98
|
|
48
57
|
});
|
|
49
58
|
if (images.length === 1) {
|
|
50
59
|
return /*#__PURE__*/_jsx("div", {
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
|
-
.bpk-carousel-container{display:grid;width:100%;height:100%;grid:1fr/auto-flow 100%;overflow-x:auto;overflow-y:hidden;backface-visibility:hidden;overscroll-behavior:
|
|
18
|
+
.bpk-carousel-container{display:grid;width:100%;height:100%;grid:1fr/auto-flow 100%;overflow-x:auto;overflow-y:hidden;backface-visibility:hidden;overscroll-behavior:auto;scroll-snap-type:x mandatory;scrollbar-width:none;user-select:none}.bpk-carousel-container::-webkit-scrollbar{display:none}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
export type OnImageChangedHandler = ((shownImageIndex: number) => void) | null | undefined;
|
|
3
|
+
export type AccessibilityLabels = {
|
|
4
|
+
indicatorLabel?: string;
|
|
5
|
+
prevNavLabel?: string;
|
|
6
|
+
nextNavLabel?: string;
|
|
7
|
+
};
|
|
3
8
|
export type Props = {
|
|
4
9
|
images: ReactNode[];
|
|
5
10
|
initialImageIndex?: number;
|
|
@@ -8,4 +13,5 @@ export type Props = {
|
|
|
8
13
|
* This prop is used to let the consumer adjust the spacing between the page indicator and the bottom of the image when variant is VARIANT.overImage
|
|
9
14
|
*/
|
|
10
15
|
bottom?: number;
|
|
16
|
+
accessibilityLabels?: AccessibilityLabels;
|
|
11
17
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MutableRefObject } from 'react';
|
|
2
2
|
import type { OnImageChangedHandler } from './types';
|
|
3
3
|
export declare function useScrollToInitialImage(initialImageIndex: number, imagesRef: MutableRefObject<Array<HTMLElement | null>>): void;
|
|
4
|
+
export declare function scrollToIndex(index: number, imagesRef?: MutableRefObject<Array<HTMLElement | null>>): void;
|
|
4
5
|
export declare function useIntersectionObserver(onIntersecting: (index: number) => void, { root, threshold }: IntersectionObserverInit, onImageChanged?: OnImageChangedHandler): (element: HTMLElement | null) => void;
|
|
@@ -17,17 +17,52 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { useEffect, useMemo, useRef } from 'react';
|
|
20
|
+
// Scrolls to a specific image in the carousel by index.
|
|
21
|
+
//
|
|
22
|
+
// This function provides programmatic scrolling control for the carousel component.
|
|
23
|
+
// It locates the target image element and scrolls its parent container to bring
|
|
24
|
+
// the image into view.
|
|
25
|
+
//
|
|
26
|
+
// The scrolling behavior uses the native browser scroll API with the 'left' position
|
|
27
|
+
// set to the target element's offsetLeft, ensuring the image aligns properly within
|
|
28
|
+
// the visible viewport of the carousel container.
|
|
29
|
+
function scrollImageToView(index, imagesRef, behavior = 'smooth') {
|
|
30
|
+
const element = imagesRef.current[index];
|
|
31
|
+
if (!element) return;
|
|
32
|
+
const parent = element.parentElement;
|
|
33
|
+
if (!parent) return;
|
|
34
|
+
|
|
35
|
+
// Some browsers and test environments don't support smooth scrolling,
|
|
36
|
+
// so we must fall back to simply setting scrollLeft
|
|
37
|
+
if (parent.scroll && typeof parent.scroll === 'function') {
|
|
38
|
+
parent.scroll({
|
|
39
|
+
left: element.offsetLeft,
|
|
40
|
+
behavior
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
parent.scrollLeft = element.offsetLeft;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Hook that scrolls to the initial image when the carousel mounts.
|
|
48
|
+
//
|
|
49
|
+
// This ensures that when a carousel is rendered with a non-zero initialImageIndex,
|
|
50
|
+
// it immediately displays that image without animation. Uses 'auto' behavior to
|
|
51
|
+
// prevent unwanted animation on initial render.
|
|
20
52
|
export function useScrollToInitialImage(initialImageIndex, imagesRef) {
|
|
21
53
|
useEffect(() => {
|
|
22
|
-
|
|
23
|
-
if (element) {
|
|
24
|
-
element.scrollIntoView({
|
|
25
|
-
block: 'nearest',
|
|
26
|
-
inline: 'start'
|
|
27
|
-
});
|
|
28
|
-
}
|
|
54
|
+
scrollImageToView(initialImageIndex, imagesRef, 'auto');
|
|
29
55
|
}, [initialImageIndex, imagesRef]);
|
|
30
56
|
}
|
|
57
|
+
|
|
58
|
+
// Scrolls to a specific image in the carousel with smooth animation.
|
|
59
|
+
//
|
|
60
|
+
// This function is typically used for user-initiated navigation (clicking indicators,
|
|
61
|
+
// navigation buttons, etc.) where a smooth animated scroll provides better UX.
|
|
62
|
+
export function scrollToIndex(index, imagesRef) {
|
|
63
|
+
if (!imagesRef) return;
|
|
64
|
+
scrollImageToView(index, imagesRef, 'smooth');
|
|
65
|
+
}
|
|
31
66
|
export function useIntersectionObserver(onIntersecting, {
|
|
32
67
|
root,
|
|
33
68
|
threshold
|