Skip to content

Commit

Permalink
Refactor setup of carousel component (#19341)
Browse files Browse the repository at this point in the history
* Restructure carousel setup.

* Disable navigation buttons in news carousel when there are no more slides.

* Adding changelog.

* Add example.

* Fixing disabled state.
  • Loading branch information
linuspahl committed May 14, 2024
1 parent b569b1a commit 9329e05
Show file tree
Hide file tree
Showing 15 changed files with 249 additions and 64 deletions.
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-19337.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type="f"
message="Fix issue with scroll position which could occur in news section on the startpage when scrolling by dragging cards."

issues=["19337"]
pulls=["19341"]
18 changes: 18 additions & 0 deletions graylog2-web-interface/src/components/common/Carousel/Carousel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#### Simple Usage

```tsx
import Carousel from './Carousel';
import CarouselProvider from './CarouselProvider';

<CarouselProvider>
<Carousel>
<Carousel.Slide>Slide 1</Carousel.Slide>
<Carousel.Slide>Slide 2</Carousel.Slide>
<Carousel.Slide>Slide 3</Carousel.Slide>
<Carousel.Slide>Slide 4</Carousel.Slide>
<Carousel.Slide>Slide 5</Carousel.Slide>
<Carousel.Slide>Slide 6</Carousel.Slide>
<Carousel.Slide>Slide 7</Carousel.Slide>
</Carousel>
</CarouselProvider>
```
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,34 @@
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';
import React, { useContext } from 'react';
import styled from 'styled-components';
import useEmblaCarousel from 'embla-carousel-react';

import CarouselSlide from 'components/common/carousel/CarouselSlide';
import CarouselSlide from './CarouselSlide';
import CarouselContext from './CarouselContext';

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

if (!carouselContext) {
throw new Error('Carousel component needs to be used inside CarouselProvider.');
}

if (!carouselContext[carouselId]) {
throw new Error(`CarouselContext does not contain anything for carousel id ${carouselId}`);
}

return carouselContext[carouselId].ref;
};

/*
* Carousel component based on embla carousel. Needs to be wrapped in CarouselProvider.
* The CarouselProvider also allows configuring the carousel.
*/

type Props = {
children: React.ReactNode,
carouselId: string
};

const StyledDiv = styled.div`
Expand All @@ -37,11 +57,11 @@ const StyledDiv = styled.div`
}
`;

const Carousel = ({ children }: Props) => {
const [emblaRef] = useEmblaCarousel({ containScroll: 'trimSnaps' });
const Carousel = ({ children, carouselId }: Props) => {
const carouselRef = useCarouselRef(carouselId);

return (
<StyledDiv className="carousel" ref={emblaRef}>
<StyledDiv className="carousel" ref={carouselRef}>
<div className="carousel-container">
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 * as React from 'react';
import type { Record } from 'immutable';
import type { EmblaCarouselType } from 'embla-carousel';

import { singleton } from 'logic/singleton';

type CarouselId = string

const CarouselContext = React.createContext<Record<CarouselId, { ref: React.Ref<HTMLDivElement>, api: EmblaCarouselType | undefined }> | undefined>(undefined);

export default singleton('contexts.CarouselContext', () => CarouselContext);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 * as React from 'react';
import { useMemo, useContext } from 'react';
import useEmblaCarousel from 'embla-carousel-react';

import CarouselContext from './CarouselContext';

type Props = React.PropsWithChildren<{
carouselId: string
}>

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

const value = useMemo(() => ({
...(existingContextValue ?? {}),
[carouselId]: { ref, api },
}), [api, carouselId, existingContextValue, ref]);

return (
<CarouselContext.Provider value={value}>
{children}
</CarouselContext.Provider>
);
};

export default CarouselProvider;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 { useState, useEffect, useCallback } from 'react';

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 onSelect = useCallback(() => {
setPrevBtnDisabled(!canScrollPrev());
setNextBtnDisabled(!canScrollNext());
}, [canScrollNext, canScrollPrev]);

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

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

export default useCarouselActions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 { useContext } from 'react';

import CarouselContext from '../CarouselContext';

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

if (!carouselContext) {
throw new Error('useCarouselApi hook needs to be used inside CarouselApiProvider.');
}

if (!carouselContext[carouselId]) {
throw new Error(`CarouselContext does not contain anything for carousel id ${carouselId}`);
}

return carouselContext[carouselId].api;
};

export default useCarouselApi;
24 changes: 24 additions & 0 deletions graylog2-web-interface/src/components/common/Carousel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 Carousel from './Carousel';

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

export default Carousel;
2 changes: 1 addition & 1 deletion graylog2-web-interface/src/components/common/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export { default as BrowserTime } from './BrowserTime';
export { default as BrandIcon } from './BrandIcon';
export { default as Card } from './Card';
export { default as Center } from './Center';
export { default as Carousel } from 'components/common/carousel/Carousel';
export { default as Carousel } from './Carousel';
export { default as ClipboardButton } from './ClipboardButton';
export { default as ColorPicker } from './ColorPicker';
export { default as ColorPickerPopover } from './ColorPickerPopover';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import ContentStreamNews from 'components/content-stream/ContentStreamNews';
import { asMock } from 'helpers/mocking';
import useContentStream from 'components/content-stream/hook/useContentStream';

jest.mock('components/common/carousel/Carousel', () => mockComponent('MockCarousel'));
jest.mock('components/common/Carousel/Carousel', () => mockComponent('MockCarousel'));

jest.mock('components/content-stream/news/ContentStreamNewsItem', () => ({ feed }: { feed: FeedItem }) => <div>{feed.title}</div>);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
import React from 'react';
import isEmpty from 'lodash/isEmpty';

import Carousel from 'components/common/carousel/Carousel';
import { Carousel, Spinner, ExternalLink } from 'components/common';
import useContentStream from 'components/content-stream/hook/useContentStream';
import { Spinner, ExternalLink } from 'components/common';
import ContentStreamNewsItem from 'components/content-stream/news/ContentStreamNewsItem';
import { Alert } from 'components/bootstrap';

export const CAROUSEL_ID = 'content-stream-news';

const ContentStreamNews = () => {
const { feedList, isLoadingFeed, error } = useContentStream();

Expand All @@ -46,7 +47,7 @@ const ContentStreamNews = () => {
}

return (
<Carousel>
<Carousel carouselId="content-stream-news">
{feedList?.map((feed) => <ContentStreamNewsItem key={feed?.guid['#text'] || feed?.title} feed={feed} />)}
</Carousel>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import styled, { css } from 'styled-components';

import SectionGrid from 'components/common/Section/SectionGrid';
import SectionComponent from 'components/common/Section/SectionComponent';
import ContentStreamNews from 'components/content-stream/ContentStreamNews';
import ContentStreamNews, { CAROUSEL_ID } from 'components/content-stream/ContentStreamNews';
import ContentStreamNewsFooter from 'components/content-stream/news/ContentStreamNewsFooter';
import ContentStreamReleasesSection from 'components/content-stream/ContentStreamReleasesSection';
import useContentStreamSettings from 'components/content-stream/hook/useContentStreamSettings';
import useCurrentUser from 'hooks/useCurrentUser';
import ToggleActionButton from 'components/content-stream/ToggleActionButton';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
import { CarouselProvider } from 'components/common/Carousel';

const StyledNewsSectionComponent = styled(SectionComponent)<{ $enabled: boolean }>(({ $enabled, theme }) => css`
overflow: hidden;
Expand Down Expand Up @@ -113,10 +114,10 @@ const ContentStreamSection = () => {
isOpen={contentStreamEnabled} />
)}>
{contentStreamEnabled && (
<>
<CarouselProvider carouselId={CAROUSEL_ID}>
<ContentStreamNews />
<ContentStreamNewsFooter />
</>
</CarouselProvider>
)}
</StyledNewsSectionComponent>
<StyledReleaseSectionComponent title="Releases"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
*/
import React from 'react';

import useCarouselAction from 'hooks/useCarouselAction';
import { Icon } from 'components/common';
import { Button } from 'components/bootstrap';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
import { CAROUSEL_ID } from 'components/content-stream/ContentStreamNews';
import { useCarouselActions } from 'components/common/Carousel';

const ContentStreamNewsContentActions = () => {
const { scrollPrev, scrollNext } = useCarouselAction('.carousel');
const { scrollPrev, scrollNext, nextBtnDisabled, prevBtnDisabled } = useCarouselActions(CAROUSEL_ID);
const sendTelemetry = useSendTelemetry();

const handlePrev = () => {
Expand All @@ -46,8 +47,12 @@ const ContentStreamNewsContentActions = () => {

return (
<>
<Button onClick={() => handlePrev()}><Icon name="chevron_left" /></Button>
<Button onClick={() => handleNext()}><Icon name="chevron_right" /></Button>
<Button onClick={() => handlePrev()} disabled={prevBtnDisabled}>
<Icon name="chevron_left" />
</Button>
<Button onClick={() => handleNext()} disabled={nextBtnDisabled}>
<Icon name="chevron_right" />
</Button>
</>
);
};
Expand Down
Loading

0 comments on commit 9329e05

Please sign in to comment.