From 7fee2516912734d08ee402c4ef8365f72bd823f1 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Tue, 16 Jul 2024 01:38:45 -0500 Subject: [PATCH] Content warning (#2270) * ContentWarning * Test * Check cookie for content warning * Cookie test coverage * Fix proxy for OSWebLoader * Update fixture file * Use Cookies package * Update fixture file 2 * Fix cookie key * Fix cookie key; trap tab navigation * Add label to content warning dialog * Revert changes to Modal; add overlay * Fix Trap Tab --------- Co-authored-by: staxly[bot] <35789409+staxly[bot]@users.noreply.github.com> --- src/app/content/components/Content.tsx | 7 +- .../components/ContentWarning.spec.tsx | 48 ++++++++++ src/app/content/components/ContentWarning.tsx | 88 +++++++++++++++++++ src/app/content/hooks/locationChange.spec.ts | 2 + src/app/content/utils/seoUtils.spec.data.ts | 2 + src/app/reactUtils.ts | 7 +- src/gateways/createOSWebLoader.ts | 4 + src/setupProxy.js | 3 +- ...nt_warning_text%2Cid%26slug%3Dbook-slug-1} | 4 +- ...rning_text%2Cid%26cnx_id%3Dtestbook1-uuid} | 0 src/test/mocks/osWebLoader.ts | 2 + 11 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 src/app/content/components/ContentWarning.spec.tsx create mode 100644 src/app/content/components/ContentWarning.tsx rename src/test/fixtures/apps/cms/api/v2/pages/{%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%26slug%3Dbook-slug-1 => %3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%2Ccontent_warning_text%2Cid%26slug%3Dbook-slug-1} (82%) rename src/test/fixtures/apps/cms/api/v2/pages/{%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cpolish_site_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%26cnx_id%3Dtestbook1-uuid => %3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cpolish_site_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%2Ccontent_warning_text%2Cid%26cnx_id%3Dtestbook1-uuid} (100%) diff --git a/src/app/content/components/Content.tsx b/src/app/content/components/Content.tsx index 7df8ed4a22..8de252b7c4 100644 --- a/src/app/content/components/Content.tsx +++ b/src/app/content/components/Content.tsx @@ -7,10 +7,12 @@ import ErrorBoundary from '../../errors/components/ErrorBoundary'; import Notifications from '../../notifications/components/Notifications'; import theme from '../../theme'; import { AppState } from '../../types'; +import { Book } from '../types'; import HighlightsPopUp from '../highlights/components/HighlightsPopUp'; import KeyboardShortcutsPopup from '../keyboardShortcuts/components/KeyboardShortcutsPopup'; import PracticeQuestionsPopup from '../practiceQuestions/components/PracticeQuestionsPopup'; import { mobileToolbarOpen } from '../search/selectors'; +import { book as bookSelector } from '../selectors'; import StudyguidesPopUp from '../studyGuides/components/StudyGuidesPopUp'; import Footer from './../../components/Footer'; import Attribution from './Attribution'; @@ -25,6 +27,7 @@ import { topbarMobileHeight } from './constants'; import ContentPane from './ContentPane'; +import ContentWarning from './ContentWarning'; import LabsCTA from './LabsCall'; import NudgeStudyTools from './NudgeStudyTools'; import Page from './Page'; @@ -69,7 +72,7 @@ const OuterWrapper = styled.div` `; // tslint:disable-next-line:variable-name -const Content = ({mobileExpanded}: {mobileExpanded: boolean}) => +const Content = ({mobileExpanded, book}: {mobileExpanded: boolean; book: Book}) => + @@ -113,5 +117,6 @@ const Content = ({mobileExpanded}: {mobileExpanded: boolean}) => export default connect( (state: AppState) => ({ mobileExpanded: mobileToolbarOpen(state), + book: bookSelector(state), }) )(Content); diff --git a/src/app/content/components/ContentWarning.spec.tsx b/src/app/content/components/ContentWarning.spec.tsx new file mode 100644 index 0000000000..80b59e3a01 --- /dev/null +++ b/src/app/content/components/ContentWarning.spec.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { renderToDom } from '../../../test/reactutils'; +import ContentWarning from './ContentWarning'; +import { useServices } from '../../context/Services'; +import { OSWebBook } from '../../../gateways/createOSWebLoader'; +import { BookWithOSWebData } from '../types'; +import { act } from 'react-dom/test-utils'; +import TestContainer from '../../../test/TestContainer'; +import ReactTestUtils from 'react-dom/test-utils'; + +const dummyBook = ({ + id: 'dummy-id', +} as unknown) as BookWithOSWebData; + +const dummyBookInfo = ({ + content_warning_text: 'some warning text', + id: 72, +} as unknown) as OSWebBook; + +const services = { + osWebLoader: { + getBookFromId: jest.fn(() => Promise.resolve(dummyBookInfo)), + }, +}; + +jest.mock('../../context/Services', () => ({ + ...jest.requireActual('../../context/Services'), + useServices: jest.fn(), +})); + +(useServices as jest.Mock).mockReturnValue(services); + +describe('ContentWarning', () => { + it('renders warning modal', async() => { + renderToDom(); + + expect(services.osWebLoader.getBookFromId).toBeCalledWith(dummyBook.id); + + await act(() => new Promise((resolve) => setTimeout(resolve, 1))); + + const root = document?.body; + const b = root?.querySelector('button'); + + expect(b).toBeTruthy(); + act(() => ReactTestUtils.Simulate.click(b!)); + expect(root?.querySelector('button')).toBeFalsy(); + }); +}); diff --git a/src/app/content/components/ContentWarning.tsx b/src/app/content/components/ContentWarning.tsx new file mode 100644 index 0000000000..282561f3e5 --- /dev/null +++ b/src/app/content/components/ContentWarning.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { useServices } from '../../context/Services'; +import { OSWebBook } from '../../../gateways/createOSWebLoader'; +import { Book } from '../types'; +import { HTMLDivElement } from '@openstax/types/lib.dom'; +import styled from 'styled-components/macro'; +import Button from '../../components/Button'; +import Modal from './Modal'; +import theme from '../../theme'; +import Cookies from 'js-cookie'; +import { useTrapTabNavigation } from '../../reactUtils'; + +// tslint:disable-next-line +const WarningDiv = styled.div` + display: flex; + flex-direction: column; + gap: 4rem; + justify-content: center; + align-items: center; + font-size: 1.8rem; + padding: 2rem; + min-height: 50vh; + top: 25vh; + z-index: 4; + + > div { + max-width: 80rem; + } +`; + +function WarningDivWithTrap({ + text, + dismiss, +}: { + text: string; + dismiss: () => void; +}) { + const ref = React.useRef(null); + + React.useEffect(() => ref.current?.focus(), []); + + useTrapTabNavigation(ref); + + return ( + +
{text}
+ +
+ ); +} + +export default function ContentWarning({ book }: { book: Book }) { + const services = useServices(); + const [bookInfo, setBookInfo] = React.useState(); + const cookieKey = `content-warning-${bookInfo?.id}`; + const dismiss = React.useCallback(() => { + // This is only called when bookInfo is populated + Cookies.set(cookieKey, 'true', { expires: 28 }); + setBookInfo(undefined); + }, [cookieKey]); + + React.useEffect(() => { + services.osWebLoader.getBookFromId(book.id).then(setBookInfo); + }, [book, services]); + + if (!bookInfo?.content_warning_text || Cookies.get(cookieKey)) { + return null; + } + + return ( + + + + ); +} diff --git a/src/app/content/hooks/locationChange.spec.ts b/src/app/content/hooks/locationChange.spec.ts index 77bee5037f..70039f39fc 100644 --- a/src/app/content/hooks/locationChange.spec.ts +++ b/src/app/content/hooks/locationChange.spec.ts @@ -214,6 +214,8 @@ describe('contentRouteHookBody', () => { polish_site_link: '', promote_image: null, publish_date: '2012-06-21', + content_warning_text: '', + id: 72, }; beforeEach(() => { diff --git a/src/app/content/utils/seoUtils.spec.data.ts b/src/app/content/utils/seoUtils.spec.data.ts index 93217a52ae..4654e36ada 100644 --- a/src/app/content/utils/seoUtils.spec.data.ts +++ b/src/app/content/utils/seoUtils.spec.data.ts @@ -75,6 +75,8 @@ export const mockOsWebBook: OSWebBook = { cnx_id: '', amazon_link: '', polish_site_link: '', + content_warning_text: '', + id: 72, }; export const emptyPage = { diff --git a/src/app/reactUtils.ts b/src/app/reactUtils.ts index efd90cc70e..d79ae2af6a 100644 --- a/src/app/reactUtils.ts +++ b/src/app/reactUtils.ts @@ -7,7 +7,7 @@ import theme from './theme'; import { assertDefined, assertDocument, assertWindow } from './utils'; export const useDrawFocus = () => { - const ref = React.useRef(null); + const ref = React.useRef(null); React.useEffect(() => { if (ref && ref.current) { @@ -99,10 +99,11 @@ export function createTrapTab(...elements: HTMLElement[]) { return; } const trapTab = createTrapTab(el); + const document = assertDocument(); - el.addEventListener('keydown', trapTab, true); + document.body.addEventListener('keydown', trapTab, true); - return () => el.removeEventListener('keydown', trapTab, true); + return () => document.body.removeEventListener('keydown', trapTab, true); }, [ref, otherDep] ); diff --git a/src/gateways/createOSWebLoader.ts b/src/gateways/createOSWebLoader.ts index ba37f01118..d975fb8726 100644 --- a/src/gateways/createOSWebLoader.ts +++ b/src/gateways/createOSWebLoader.ts @@ -21,6 +21,8 @@ export interface OSWebBook { cnx_id: string; amazon_link: string; polish_site_link: string; + content_warning_text: string | null; + id: number; } interface OSWebResponse { @@ -41,6 +43,8 @@ export const fields = [ 'promote_image', 'book_subjects', 'book_categories', + 'content_warning_text', + 'id', ].join(','); interface Options { diff --git a/src/setupProxy.js b/src/setupProxy.js index a668ea43e0..2899dbb355 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -2,7 +2,6 @@ * this file is shared between webpack-dev-server and the pre-renderer */ const url = require('url'); -const util = require('util'); const fs = require('fs'); const path = require('path'); const proxy = require('http-proxy-middleware'); @@ -32,7 +31,7 @@ const { default: createOSWebLoader } = require('./gateways/createOSWebLoader'); const archiveLoader = createArchiveLoader({ archivePrefix: ARCHIVE_URL }); -const osWebLoader = createOSWebLoader(`${ARCHIVE_URL}${REACT_APP_OS_WEB_API_URL}`); +const osWebLoader = createOSWebLoader(`${OS_WEB_URL}${REACT_APP_OS_WEB_API_URL}`); const archivePaths = [ '/apps/archive', diff --git a/src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%26slug%3Dbook-slug-1 b/src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%2Ccontent_warning_text%2Cid%26slug%3Dbook-slug-1 similarity index 82% rename from src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%26slug%3Dbook-slug-1 rename to src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%2Ccontent_warning_text%2Cid%26slug%3Dbook-slug-1 index ee98891c16..82474ed541 100644 --- a/src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%26slug%3Dbook-slug-1 +++ b/src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%2Ccontent_warning_text%2Cid%26slug%3Dbook-slug-1 @@ -15,7 +15,9 @@ "cover_color": "blue", "authors": [{"value": {"name": "Bam Bammerson"}}], "publish_date": "2012-06-21", - "book_state": "live" + "book_state": "live", + "content_warning_text": "sensitive material is covered", + "id": 72 } ] } diff --git a/src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cpolish_site_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%26cnx_id%3Dtestbook1-uuid b/src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cpolish_site_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%2Ccontent_warning_text%2Cid%26cnx_id%3Dtestbook1-uuid similarity index 100% rename from src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cpolish_site_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%26cnx_id%3Dtestbook1-uuid rename to src/test/fixtures/apps/cms/api/v2/pages/%3Ftype%3Dbooks.Book%26fields%3Dcnx_id%2Cauthors%2Cpublish_date%2Ccover_color%2Camazon_link%2Cpolish_site_link%2Cbook_state%2Cpromote_image%2Cbook_subjects%2Cbook_categories%2Ccontent_warning_text%2Cid%26cnx_id%3Dtestbook1-uuid diff --git a/src/test/mocks/osWebLoader.ts b/src/test/mocks/osWebLoader.ts index 71aa0d6fc0..6f01bed561 100644 --- a/src/test/mocks/osWebLoader.ts +++ b/src/test/mocks/osWebLoader.ts @@ -14,6 +14,8 @@ export const mockCmsBook: OSWebBook = { }, promote_image: null, publish_date: '2012-06-21', + content_warning_text: '', + id: 72, }; export default () => ({