Skip to content

Commit

Permalink
Further improvements for Carousel component. (#19368)
Browse files Browse the repository at this point in the history
  • Loading branch information
linuspahl committed May 16, 2024
1 parent 38445e3 commit d4020fd
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 30 deletions.
22 changes: 16 additions & 6 deletions graylog2-web-interface/src/components/common/Carousel/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import styled from 'styled-components';
import CarouselSlide from './CarouselSlide';
import CarouselContext from './CarouselContext';

export const CAROUSEL_CONTAINER_CLASS_NAME = 'carousel-container';

const useCarouselRef = (carouselId: string) => {
const carouselContext = useContext(CarouselContext);

Expand All @@ -35,20 +37,23 @@ const useCarouselRef = (carouselId: string) => {
};

/*
* Carousel component based on embla carousel. Needs to be wrapped in CarouselProvider.
* The CarouselProvider also allows configuring the carousel.
* Carousel component based on embla carousel. It needs to be wrapped with the CarouselProvider.
* The CarouselProvider allows accessing the carouselApi object in all children, like the carousel navigation components.
* The CarouselProvider also maintains the carousel configuration options.
*/

type Props = {
children: React.ReactNode,
className?: string
containerRef?: React.Ref<HTMLDivElement>
carouselId: string
};

const StyledDiv = styled.div`
&.carousel {
overflow: hidden;
.carousel-container {
.${CAROUSEL_CONTAINER_CLASS_NAME} {
backface-visibility: hidden;
display: flex;
flex-direction: row;
Expand All @@ -57,17 +62,22 @@ const StyledDiv = styled.div`
}
`;

const Carousel = ({ children, carouselId }: Props) => {
const Carousel = ({ children, className, containerRef, carouselId }: Props) => {
const carouselRef = useCarouselRef(carouselId);

return (
<StyledDiv className="carousel" ref={carouselRef}>
<div className="carousel-container">
<StyledDiv className={`carousel ${className}`} ref={carouselRef}>
<div className={CAROUSEL_CONTAINER_CLASS_NAME} ref={containerRef}>
{children}
</div>
</StyledDiv>
);
};

Carousel.defaultProps = {
className: undefined,
containerRef: undefined,
};

Carousel.Slide = CarouselSlide;
export default Carousel;
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ import useEmblaCarousel from 'embla-carousel-react';
import CarouselContext from './CarouselContext';

type Props = React.PropsWithChildren<{
carouselId: string
carouselId: string,
options?: Partial<{ align: 'start' }>
}>

const CarouselProvider = ({ carouselId, children } : Props) => {
const CarouselProvider = ({ carouselId, children, options } : Props) => {
const existingContextValue = useContext(CarouselContext);
const [ref, api] = useEmblaCarousel({ containScroll: 'trimSnaps' });
const [ref, api] = useEmblaCarousel(options);

const value = useMemo(() => ({
...(existingContextValue ?? {}),
Expand All @@ -40,4 +41,8 @@ const CarouselProvider = ({ carouselId, children } : Props) => {
);
};

CarouselProvider.defaultProps = {
options: {},
};

export default CarouselProvider;
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ import styled, { css } from 'styled-components';

export interface CarouselSlideProps extends React.ComponentPropsWithoutRef<'div'> {
children?: React.ReactNode;

size?: string | number;

className?: string,
gap?: number;
size?: string | number;
}

const StyledSlide = styled.div<{ $size?: string | number, $gap?: number }>(({ $size, $gap, theme }: {
Expand All @@ -38,16 +37,17 @@ const StyledSlide = styled.div<{ $size?: string | number, $gap?: number }>(({ $s
position: relative;
`);

const CarouselSlide = ({ children, size, gap }: CarouselSlideProps) => (
<StyledSlide $size={size} $gap={gap}>
const CarouselSlide = ({ children, size, gap, className }: CarouselSlideProps) => (
<StyledSlide $size={size} $gap={gap} className={className}>
{children}
</StyledSlide>
);

CarouselSlide.defaultProps = {
children: undefined,
size: undefined,
className: undefined,
gap: undefined,
size: undefined,
};

export default CarouselSlide;
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,27 @@ import useCarouselApi from './useCarouselApi';

const useCarouselActions = (carouselId: string) => {
const carouselApi = useCarouselApi(carouselId);
const canScrollPrev = useCallback(() => !!carouselApi?.canScrollPrev(), [carouselApi]);
const canScrollNext = useCallback(() => !!carouselApi?.canScrollNext(), [carouselApi]);

const [nextBtnDisabled, setNextBtnDisabled] = useState(false);
const [prevBtnDisabled, setPrevBtnDisabled] = useState(false);
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true);
const [nextBtnDisabled, setNextBtnDisabled] = useState(true);

const onSelect = useCallback(() => {
setPrevBtnDisabled(!canScrollPrev());
setNextBtnDisabled(!canScrollNext());
}, [canScrollNext, canScrollPrev]);
setPrevBtnDisabled(!carouselApi.canScrollPrev());
setNextBtnDisabled(!carouselApi.canScrollNext());
}, [carouselApi]);

useEffect(() => {
if (carouselApi) {
carouselApi.on('reInit', onSelect);
carouselApi.on('select', onSelect);
}
if (!carouselApi) return;

onSelect();
carouselApi.on('reInit', onSelect);
carouselApi.on('select', onSelect);
}, [carouselApi, onSelect]);

return {
prevBtnDisabled,
nextBtnDisabled,
scrollNext: carouselApi?.scrollNext ? carouselApi.scrollNext : () => {},
scrollPrev: carouselApi?.scrollPrev ? carouselApi.scrollPrev : () => {},
nextBtnDisabled,
prevBtnDisabled,
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import { useCallback, useEffect, useState } from 'react';

import useCarouselApi from './useCarouselApi';

// Hook which returns all slide indices which have been in the view at least once.
const useSlidesInView = (carouselId: string) => {
const carouselApi = useCarouselApi(carouselId);
const [slidesInView, setSlidesInView] = useState<Array<number>>([]);

const updateSlidesInView = useCallback(() => {
setSlidesInView((cur) => {
if (cur.length === carouselApi.slideNodes().length) {
carouselApi.off('slidesInView', updateSlidesInView);
}

const inView = carouselApi
.slidesInView()
.filter((index) => !cur.includes(index));

return cur.concat(inView);
});
}, [carouselApi]);

useEffect(() => {
if (!carouselApi) return;

updateSlidesInView();
carouselApi.on('slidesInView', updateSlidesInView);
carouselApi.on('reInit', updateSlidesInView);
}, [carouselApi, updateSlidesInView]);

return slidesInView;
};

export default useSlidesInView;
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/

import Carousel from './Carousel';
import Carousel, { CAROUSEL_CONTAINER_CLASS_NAME } from './Carousel';

export { CAROUSEL_CONTAINER_CLASS_NAME };

export { default as useCarouselApi } from './hooks/useCarouselApi';
export { default as useSlidesInView } from './hooks/useSlidesInView';
export { default as useCarouselActions } from './hooks/useCarouselActions';
export { default as CarouselProvider } from './CarouselProvider';

Expand Down

0 comments on commit d4020fd

Please sign in to comment.