diff --git a/changelog/unreleased/issue-19337.toml b/changelog/unreleased/issue-19337.toml new file mode 100644 index 000000000000..37ff9cd9daf3 --- /dev/null +++ b/changelog/unreleased/issue-19337.toml @@ -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"] diff --git a/graylog2-web-interface/src/components/common/Carousel/Carousel.md b/graylog2-web-interface/src/components/common/Carousel/Carousel.md new file mode 100644 index 000000000000..ac2c91149bda --- /dev/null +++ b/graylog2-web-interface/src/components/common/Carousel/Carousel.md @@ -0,0 +1,18 @@ +#### Simple Usage + +```tsx +import Carousel from './Carousel'; +import CarouselProvider from './CarouselProvider'; + + + + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + Slide 7 + + +``` diff --git a/graylog2-web-interface/src/components/common/carousel/Carousel.tsx b/graylog2-web-interface/src/components/common/Carousel/Carousel.tsx similarity index 55% rename from graylog2-web-interface/src/components/common/carousel/Carousel.tsx rename to graylog2-web-interface/src/components/common/Carousel/Carousel.tsx index 1aa36a6fd5de..94b690448df5 100644 --- a/graylog2-web-interface/src/components/common/carousel/Carousel.tsx +++ b/graylog2-web-interface/src/components/common/Carousel/Carousel.tsx @@ -14,14 +14,34 @@ * along with this program. If not, see * . */ -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` @@ -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 ( - +
{children}
diff --git a/graylog2-web-interface/src/components/common/Carousel/CarouselContext.tsx b/graylog2-web-interface/src/components/common/Carousel/CarouselContext.tsx new file mode 100644 index 000000000000..d58db30b4272 --- /dev/null +++ b/graylog2-web-interface/src/components/common/Carousel/CarouselContext.tsx @@ -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 + * . + */ + +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, api: EmblaCarouselType | undefined }> | undefined>(undefined); + +export default singleton('contexts.CarouselContext', () => CarouselContext); diff --git a/graylog2-web-interface/src/components/common/Carousel/CarouselProvider.tsx b/graylog2-web-interface/src/components/common/Carousel/CarouselProvider.tsx new file mode 100644 index 000000000000..6c53e9f43513 --- /dev/null +++ b/graylog2-web-interface/src/components/common/Carousel/CarouselProvider.tsx @@ -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 + * . + */ +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 ( + + {children} + + ); +}; + +export default CarouselProvider; diff --git a/graylog2-web-interface/src/components/common/carousel/CarouselSlide.tsx b/graylog2-web-interface/src/components/common/Carousel/CarouselSlide.tsx similarity index 100% rename from graylog2-web-interface/src/components/common/carousel/CarouselSlide.tsx rename to graylog2-web-interface/src/components/common/Carousel/CarouselSlide.tsx diff --git a/graylog2-web-interface/src/components/common/Carousel/hooks/useCarouselActions.ts b/graylog2-web-interface/src/components/common/Carousel/hooks/useCarouselActions.ts new file mode 100644 index 000000000000..1ff159203b8e --- /dev/null +++ b/graylog2-web-interface/src/components/common/Carousel/hooks/useCarouselActions.ts @@ -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 + * . + */ + +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; diff --git a/graylog2-web-interface/src/components/common/Carousel/hooks/useCarouselApi.ts b/graylog2-web-interface/src/components/common/Carousel/hooks/useCarouselApi.ts new file mode 100644 index 000000000000..b307562d6ac0 --- /dev/null +++ b/graylog2-web-interface/src/components/common/Carousel/hooks/useCarouselApi.ts @@ -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 + * . + */ + +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; diff --git a/graylog2-web-interface/src/components/common/Carousel/index.ts b/graylog2-web-interface/src/components/common/Carousel/index.ts new file mode 100644 index 000000000000..a5933e239f43 --- /dev/null +++ b/graylog2-web-interface/src/components/common/Carousel/index.ts @@ -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 + * . + */ + +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; diff --git a/graylog2-web-interface/src/components/common/index.tsx b/graylog2-web-interface/src/components/common/index.tsx index fc87f14a37b4..481dfccf573a 100644 --- a/graylog2-web-interface/src/components/common/index.tsx +++ b/graylog2-web-interface/src/components/common/index.tsx @@ -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'; diff --git a/graylog2-web-interface/src/components/content-stream/ContentStreamNews.test.tsx b/graylog2-web-interface/src/components/content-stream/ContentStreamNews.test.tsx index 13c12ffa3bba..76487afda494 100644 --- a/graylog2-web-interface/src/components/content-stream/ContentStreamNews.test.tsx +++ b/graylog2-web-interface/src/components/content-stream/ContentStreamNews.test.tsx @@ -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 }) =>
{feed.title}
); diff --git a/graylog2-web-interface/src/components/content-stream/ContentStreamNews.tsx b/graylog2-web-interface/src/components/content-stream/ContentStreamNews.tsx index c0ddc362c66f..23b25c50ca48 100644 --- a/graylog2-web-interface/src/components/content-stream/ContentStreamNews.tsx +++ b/graylog2-web-interface/src/components/content-stream/ContentStreamNews.tsx @@ -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(); @@ -46,7 +47,7 @@ const ContentStreamNews = () => { } return ( - + {feedList?.map((feed) => )} ); diff --git a/graylog2-web-interface/src/components/content-stream/ContentStreamSection.tsx b/graylog2-web-interface/src/components/content-stream/ContentStreamSection.tsx index 58741e4f2c5c..f70783623d9f 100644 --- a/graylog2-web-interface/src/components/content-stream/ContentStreamSection.tsx +++ b/graylog2-web-interface/src/components/content-stream/ContentStreamSection.tsx @@ -19,7 +19,7 @@ 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'; @@ -27,6 +27,7 @@ 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; @@ -113,10 +114,10 @@ const ContentStreamSection = () => { isOpen={contentStreamEnabled} /> )}> {contentStreamEnabled && ( - <> + - + )} { - const { scrollPrev, scrollNext } = useCarouselAction('.carousel'); + const { scrollPrev, scrollNext, nextBtnDisabled, prevBtnDisabled } = useCarouselActions(CAROUSEL_ID); const sendTelemetry = useSendTelemetry(); const handlePrev = () => { @@ -46,8 +47,12 @@ const ContentStreamNewsContentActions = () => { return ( <> - - + + ); }; diff --git a/graylog2-web-interface/src/hooks/useCarouselAction.ts b/graylog2-web-interface/src/hooks/useCarouselAction.ts deleted file mode 100644 index 8c841624d8ac..000000000000 --- a/graylog2-web-interface/src/hooks/useCarouselAction.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 - * . - */ -import EmblaCarousel from 'embla-carousel'; -import { useCallback, useMemo, useState, useEffect } from 'react'; - -const useCarouselAction = (carouselElementClass: string) => { - const [carousel, setCarousel] = useState(undefined); - - useEffect(() => { - if (!carousel) { - setInterval(() => { - setCarousel(document.querySelector(carouselElementClass)); - }, 200); - } - }, [carousel, carouselElementClass]); - - const emblaApi = useMemo(() => carousel && EmblaCarousel(carousel, { containScroll: 'trimSnaps' }), [carousel]); - - const scrollPrev = useCallback(() => { - if (emblaApi) emblaApi.scrollPrev(); - }, [emblaApi]); - const scrollNext = useCallback(() => { - if (emblaApi) emblaApi.scrollNext(); - }, [emblaApi]); - - return { - scrollNext, - scrollPrev, - }; -}; - -export default useCarouselAction;