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;