From 45726f7e8513bd86f474a445691d684c5e2d98cc Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 5 May 2025 00:18:37 -0700 Subject: [PATCH 01/38] Create media modal component --- .../content/components/Page/MediaModal.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/app/content/components/Page/MediaModal.tsx diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx new file mode 100644 index 0000000000..5c5b7bb3c2 --- /dev/null +++ b/src/app/content/components/Page/MediaModal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import styled from 'styled-components/macro'; + +const Overlay = styled.div` + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +`; + +const ModalContainer = styled.div` + background-color: white; + padding: 1rem; + margin: 2rem; + border-radius: 1rem; + max-width: 90%; + max-height: 90%; + overflow: auto; +`; + + +interface MediaModalProps { + isOpen: boolean; + onClose: () => void; + children: React.ReactNode; +} + +const MediaModal: React.FC = ({ isOpen, onClose, children }) => { + if (!isOpen) return null; + + return ( + + +
{children}
+
+
+ ); +}; + +export default MediaModal; From f5b7536e1660c647a5558cd40b734e757a9ba9a7 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 5 May 2025 00:19:17 -0700 Subject: [PATCH 02/38] Add expand image message on mobile breakpoint --- src/app/content/components/Page/PageContent.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/app/content/components/Page/PageContent.tsx b/src/app/content/components/Page/PageContent.tsx index 30052fb670..3ecc2a279c 100644 --- a/src/app/content/components/Page/PageContent.tsx +++ b/src/app/content/components/Page/PageContent.tsx @@ -166,6 +166,23 @@ export default styled(MainContent)` margin-bottom: 5px; /* fix double scrollbar bug */ } +@media screen { +[data-type="media"] { + flex-direction: column; + ${theme.breakpoints.mobile(css` + &::after { + content: 'Tap to expand image'; + display: block; + background-color: #F1F1F1; + border: 1px solid #D5D5D5; + text-align: center; + font-size: 1rem; + color: #424242; + } + `)} + } +} + #${MAIN_CONTENT_ID} * { overflow: initial; /* rex styles default to overflow hidden, breaks content */ } From f788ab50e04510682f0a7c07e22f1eade22c8ead Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 5 May 2025 00:32:55 -0700 Subject: [PATCH 03/38] Add onMediaClick event and modal integration --- .../content/components/Page/PageComponent.tsx | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 7b9d694815..9bd711071f 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -1,5 +1,6 @@ import { HTMLAnchorElement, HTMLDivElement, HTMLElement, MouseEvent } from '@openstax/types/lib.dom'; import React, { Component } from 'react'; +import type { ReactNode } from 'react'; import WeakMap from 'weak-map'; import { APP_ENV } from '../../../../config'; import { typesetMath } from '../../../../helpers/mathjax'; @@ -22,14 +23,19 @@ import scrollToTopOrHashManager, { stubScrollToTopOrHashManager } from './scroll import searchHighlightManager, { stubManager, UpdateOptions as SearchUpdateOptions } from './searchHighlightManager'; import { validateDOMContent } from './validateDOMContent'; import isEqual from 'lodash/fp/isEqual'; +import MediaModal from './MediaModal'; if (typeof(document) !== 'undefined') { import(/* webpackChunkName: "NodeList.forEach" */ 'mdn-polyfills/NodeList.prototype.forEach'); } +interface PageComponentState { + isModalOpen: boolean; + modalContent: ReactNode; +} const parser = new DOMParser(); -export default class PageComponent extends Component { +export default class PageComponent extends Component { public container = React.createRef(); private clickListeners = new WeakMap void>(); private searchHighlightManager = stubManager; @@ -37,6 +43,18 @@ export default class PageComponent extends Component { private scrollToTopOrHashManager = stubScrollToTopOrHashManager; private processing: Array> = []; private componentDidUpdateCounter = 0; + state: PageComponentState = { + isModalOpen: false, + modalContent: null + }; + + + private closeMediaModal = () => { + this.setState({ + isModalOpen: false, + modalContent: null + }); + }; public getTransformedContent = () => { const {book, page, services} = this.props; @@ -166,6 +184,12 @@ export default class PageComponent extends Component { const html = this.getTransformedContent() || this.getPrerenderedContent(); return + + {this.state.modalContent} + { private listenersOn() { this.listenersOff(); + if (this.container.current) { + const media = this.container.current.querySelectorAll('[data-type="media"]'); + media.forEach((media) => { + const onMediaClick = () => { + if (typeof window !== 'undefined' && window.matchMedia(`(max-width: 1200px)`).matches) { + this.setState({ + isModalOpen: true, + modalContent:
, + }); + } + }; + media.addEventListener('click', onMediaClick); + this.clickListeners.set(media as HTMLElement, onMediaClick); + }); + } lazyResources.addScrollHandler(); @@ -232,6 +271,11 @@ export default class PageComponent extends Component { }; this.mapLinks(removeIfExists); + if (this.container.current) { + const media = this.container.current.querySelectorAll('[data-type="media"]'); + media.forEach((el) => removeIfExists(el as HTMLElement)); + } + } private postProcess() { From 1909c27bea5b944da48e38221920405d8d1d33e5 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Sun, 11 May 2025 18:39:51 -0700 Subject: [PATCH 04/38] Fix linter issues --- .../content/components/Page/MediaModal.tsx | 15 +++++++--- .../content/components/Page/PageContent.tsx | 28 +++++++++---------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index 5c5b7bb3c2..b190607911 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -3,8 +3,11 @@ import styled from 'styled-components/macro'; const Overlay = styled.div` position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.7); + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.9); display: flex; justify-content: center; align-items: center; @@ -12,12 +15,16 @@ const Overlay = styled.div` `; const ModalContainer = styled.div` + display: flex; + flex-direction: row; + vertical-align: middle; + align-items: center; background-color: white; padding: 1rem; margin: 2rem; border-radius: 1rem; - max-width: 90%; - max-height: 90%; + min-width: 100%; + min-height: 90%; overflow: auto; `; diff --git a/src/app/content/components/Page/PageContent.tsx b/src/app/content/components/Page/PageContent.tsx index 3ecc2a279c..7b5e3531ff 100644 --- a/src/app/content/components/Page/PageContent.tsx +++ b/src/app/content/components/Page/PageContent.tsx @@ -166,22 +166,22 @@ export default styled(MainContent)` margin-bottom: 5px; /* fix double scrollbar bug */ } -@media screen { -[data-type="media"] { - flex-direction: column; - ${theme.breakpoints.mobile(css` - &::after { - content: 'Tap to expand image'; - display: block; - background-color: #F1F1F1; - border: 1px solid #D5D5D5; - text-align: center; - font-size: 1rem; - color: #424242; - } + @media screen { + [data-type="media"] { + flex-direction: column; + ${theme.breakpoints.mobile(css` + &::after { + content: 'Tap to expand image'; + display: block; + background-color: #F1F1F1; + border: 1px solid #D5D5D5; + text-align: center; + font-size: 14px; + color: #424242; + } `)} + } } -} #${MAIN_CONTENT_ID} * { overflow: initial; /* rex styles default to overflow hidden, breaks content */ From 73538615c4b05029aa8dd2dfd1614fee9d8e9382 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Thu, 22 May 2025 13:16:00 -0700 Subject: [PATCH 05/38] Finish MediaModal style --- .../content/components/Page/MediaModal.tsx | 83 ++++++++++++++----- .../components/Page/MediaModalManager.tsx | 50 +++++++++++ .../content/components/Page/PageComponent.tsx | 62 +++++++------- .../content/components/Page/PageContent.tsx | 16 ---- 4 files changed, 141 insertions(+), 70 deletions(-) create mode 100644 src/app/content/components/Page/MediaModalManager.tsx diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index b190607911..0bbef95f78 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -3,31 +3,65 @@ import styled from 'styled-components/macro'; const Overlay = styled.div` position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.9); + inset: 0; + background-color: rgba(0, 0, 0, 0.85); + z-index: 1000; display: flex; justify-content: center; align-items: center; - z-index: 9999; `; -const ModalContainer = styled.div` + +const Modal = styled.div` + background: white; + border-radius: 8px; + width: 100%; + min-width: 100vw; + max-height: 90vh; + box-sizing: border-box; display: flex; - flex-direction: row; - vertical-align: middle; - align-items: center; - background-color: white; + flex-direction: column; + position: relative; + overflow: hidden; +`; + +const ScrollableContent = styled.div` + overflow-y: auto; + flex: 1; padding: 1rem; - margin: 2rem; - border-radius: 1rem; - min-width: 100%; - min-height: 90%; - overflow: auto; + display: block; + width: 100%; + box-sizing: border-box; +`; + +const FloatingCloseButton = styled.button` + position: absolute; + top: -3rem; + right: 2.5rem; + z-index: 10; + background: none; + border: none; + padding: 0; + cursor: pointer; + width: 2.5rem; + height: 2.5rem; + transform: translateY(-3rem); +`; + +const ModalWrapper = styled.div` + position: relative; + max-width: 100vw; + width: 100%; + box-sizing: border-box; `; +const CloseIcon = () => ( + + + + + +); interface MediaModalProps { isOpen: boolean; @@ -39,11 +73,18 @@ const MediaModal: React.FC = ({ isOpen, onClose, children }) => if (!isOpen) return null; return ( - - -
{children}
-
-
+ <> + + + + + + + {children} + + + + ); }; diff --git a/src/app/content/components/Page/MediaModalManager.tsx b/src/app/content/components/Page/MediaModalManager.tsx new file mode 100644 index 0000000000..f2af7b652b --- /dev/null +++ b/src/app/content/components/Page/MediaModalManager.tsx @@ -0,0 +1,50 @@ +import React, { ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import MediaModal from './MediaModal'; + +type ShowModal = (content: ReactNode) => void; + +class ModalManager { + private container: HTMLElement; + private showModal: ShowModal | null = null; + + constructor() { + this.container = document.createElement('div'); + this.container.id = 'media-modal-root'; + document.body.appendChild(this.container); + } + + mount(setModalContent: ShowModal) { + this.showModal = setModalContent; + } + + open(content: ReactNode) { + this.showModal?.(content); + } + + unmount() { + this.showModal = null; + } +} + +export const mediaModalManager = new ModalManager(); + +export function MediaModalPortal() { + const [isOpen, setIsOpen] = React.useState(false); + const [modalContent, setModalContent] = React.useState(null); + + React.useEffect(() => { + mediaModalManager.mount((content) => { + setModalContent(content); + setIsOpen(true); + }); + return () => mediaModalManager.unmount(); + }, []); + + return createPortal( + setIsOpen(false)}> + {modalContent} + , + document.body + ); +} diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 9bd711071f..deb4a6d39c 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -24,7 +24,7 @@ import searchHighlightManager, { stubManager, UpdateOptions as SearchUpdateOptio import { validateDOMContent } from './validateDOMContent'; import isEqual from 'lodash/fp/isEqual'; import MediaModal from './MediaModal'; - +import { mediaModalManager, MediaModalPortal } from './MediaModalManager'; if (typeof(document) !== 'undefined') { import(/* webpackChunkName: "NodeList.forEach" */ 'mdn-polyfills/NodeList.prototype.forEach'); } @@ -90,8 +90,10 @@ export default class PageComponent extends Component this.props.highlights, this.props.services, this.props.intl); this.scrollToTopOrHashManager = scrollToTopOrHashManager(this.container.current); - + // use manager pattern for image preview. Use react portal to move modal into page body. + // accept only media image because media has videos and animations. // Sometimes data is already populated on mount, eg when navigating to a new tab + // tab index and add role for button for accessability if (this.props.searchHighlights.selectedResult) { this.searchHighlightManager.update(null, this.props.searchHighlights, { forceRedraw: true, @@ -166,10 +168,11 @@ export default class PageComponent extends Component + {this.props.pageNotFound ? this.renderPageNotFound() @@ -235,49 +238,42 @@ export default class PageComponent extends Component { - const onMediaClick = () => { - if (typeof window !== 'undefined' && window.matchMedia(`(max-width: 1200px)`).matches) { - this.setState({ - isModalOpen: true, - modalContent:
, - }); - } - }; - media.addEventListener('click', onMediaClick); - this.clickListeners.set(media as HTMLElement, onMediaClick); - }); - } + const container = this.container.current; + if (!container) return; - lazyResources.addScrollHandler(); + const handleClick = (e: MouseEvent) => { + if (e.target instanceof HTMLImageElement) { + mediaModalManager.open( + {e.target.alt + ); + } + }; + + container.addEventListener('click', handleClick); + this.clickListeners.set(container, handleClick); + lazyResources.addScrollHandler(); this.mapLinks((a) => { const handler = contentLinks.contentLinkHandler(a, () => this.props.contentLinks, this.props.services); this.clickListeners.set(a, handler); a.addEventListener('click', handler); }); } - + private listenersOff() { lazyResources.removeScrollHandler(); - - const removeIfExists = (el: HTMLElement) => { + this.mapLinks((el) => { const handler = this.clickListeners.get(el); - if (handler) { - el.removeEventListener('click', handler); - } - }; - - this.mapLinks(removeIfExists); - if (this.container.current) { - const media = this.container.current.querySelectorAll('[data-type="media"]'); - media.forEach((el) => removeIfExists(el as HTMLElement)); + if (handler) el.removeEventListener('click', handler); + }); + + const container = this.container.current; + if (container) { + const handler = this.clickListeners.get(container); + if (handler) container.removeEventListener('click', handler); } - } - + private postProcess() { const container = this.container.current; diff --git a/src/app/content/components/Page/PageContent.tsx b/src/app/content/components/Page/PageContent.tsx index 7b5e3531ff..95afbdc7f3 100644 --- a/src/app/content/components/Page/PageContent.tsx +++ b/src/app/content/components/Page/PageContent.tsx @@ -166,22 +166,6 @@ export default styled(MainContent)` margin-bottom: 5px; /* fix double scrollbar bug */ } - @media screen { - [data-type="media"] { - flex-direction: column; - ${theme.breakpoints.mobile(css` - &::after { - content: 'Tap to expand image'; - display: block; - background-color: #F1F1F1; - border: 1px solid #D5D5D5; - text-align: center; - font-size: 14px; - color: #424242; - } - `)} - } - } #${MAIN_CONTENT_ID} * { overflow: initial; /* rex styles default to overflow hidden, breaks content */ From 97e29f41cdc698d77c1c4d0b5a9044cc8be5ee85 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Sun, 8 Jun 2025 22:20:27 -0700 Subject: [PATCH 06/38] Use modal manager and add accessibility --- .../content/components/Page/MediaModal.tsx | 4 +- .../components/Page/MediaModalManager.tsx | 30 ++++++------- .../content/components/Page/PageComponent.tsx | 45 ++++++++++--------- .../content/components/Page/PageContent.tsx | 1 - .../Page/contentDOMTransformations.ts | 13 ++++++ 5 files changed, 55 insertions(+), 38 deletions(-) diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index 0bbef95f78..48dd9eca8a 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -1,5 +1,6 @@ import React from 'react'; import styled from 'styled-components/macro'; +import ScrollLock from '../../../components/ScrollLock'; const Overlay = styled.div` position: fixed; @@ -74,9 +75,10 @@ const MediaModal: React.FC = ({ isOpen, onClose, children }) => return ( <> + - + diff --git a/src/app/content/components/Page/MediaModalManager.tsx b/src/app/content/components/Page/MediaModalManager.tsx index f2af7b652b..aee2647ca1 100644 --- a/src/app/content/components/Page/MediaModalManager.tsx +++ b/src/app/content/components/Page/MediaModalManager.tsx @@ -1,30 +1,27 @@ import React, { ReactNode } from 'react'; import { createPortal } from 'react-dom'; import MediaModal from './MediaModal'; +import {HTMLElement, HTMLDivElement } from '@openstax/types/lib.dom'; type ShowModal = (content: ReactNode) => void; class ModalManager { - private container: HTMLElement; + private container: HTMLElement | null = null; private showModal: ShowModal | null = null; - constructor() { - this.container = document.createElement('div'); - this.container.id = 'media-modal-root'; - document.body.appendChild(this.container); - } - mount(setModalContent: ShowModal) { this.showModal = setModalContent; + + if (typeof document !== 'undefined' && !this.container) { + this.container = document.createElement('div') as HTMLDivElement; + this.container.id = 'media-modal-root'; + document.body.appendChild(this.container); + } } open(content: ReactNode) { this.showModal?.(content); } - - unmount() { - this.showModal = null; - } } export const mediaModalManager = new ModalManager(); @@ -38,13 +35,16 @@ export function MediaModalPortal() { setModalContent(content); setIsOpen(true); }); - return () => mediaModalManager.unmount(); }, []); + if (typeof document === 'undefined') return null; + return createPortal( - setIsOpen(false)}> - {modalContent} - , + <> + setIsOpen(false)}> + {modalContent} + + , document.body ); } diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index deb4a6d39c..1f9c72fc59 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -1,4 +1,4 @@ -import { HTMLAnchorElement, HTMLDivElement, HTMLElement, MouseEvent } from '@openstax/types/lib.dom'; +import { HTMLAnchorElement, HTMLDivElement, HTMLElement, MouseEvent, KeyboardEvent } from '@openstax/types/lib.dom'; import React, { Component } from 'react'; import type { ReactNode } from 'react'; import WeakMap from 'weak-map'; @@ -23,7 +23,6 @@ import scrollToTopOrHashManager, { stubScrollToTopOrHashManager } from './scroll import searchHighlightManager, { stubManager, UpdateOptions as SearchUpdateOptions } from './searchHighlightManager'; import { validateDOMContent } from './validateDOMContent'; import isEqual from 'lodash/fp/isEqual'; -import MediaModal from './MediaModal'; import { mediaModalManager, MediaModalPortal } from './MediaModalManager'; if (typeof(document) !== 'undefined') { import(/* webpackChunkName: "NodeList.forEach" */ 'mdn-polyfills/NodeList.prototype.forEach'); @@ -48,14 +47,6 @@ export default class PageComponent extends Component { - this.setState({ - isModalOpen: false, - modalContent: null - }); - }; - public getTransformedContent = () => { const {book, page, services} = this.props; @@ -187,12 +178,6 @@ export default class PageComponent extends Component - - {this.state.modalContent} - { - if (e.target instanceof HTMLImageElement) { + const triggerMediaModal = (target: HTMLElement) => { + if (typeof window !== 'undefined' && window.matchMedia(`(max-width: 1200px)`).matches) { + const outerHTML = target.outerHTML; mediaModalManager.open( - {e.target.alt +
); } }; - container.addEventListener('click', handleClick); - this.clickListeners.set(container, handleClick); + const handleInteraction = (e: MouseEvent | KeyboardEvent) => { + const target = e.target as HTMLElement; + + if (target.tagName !== 'IMG' || !target.hasAttribute('tabindex')) return; + + if (e.type === 'keydown') { + const keyEvent = e as KeyboardEvent; + + if (keyEvent.key !== 'Enter' && keyEvent.key !== ' ') return; + + keyEvent.preventDefault(); + } + + triggerMediaModal(target); + }; + + container.addEventListener('click', handleInteraction); + container.addEventListener('keydown', handleInteraction); + this.clickListeners.set(container, handleInteraction); lazyResources.addScrollHandler(); this.mapLinks((a) => { diff --git a/src/app/content/components/Page/PageContent.tsx b/src/app/content/components/Page/PageContent.tsx index 95afbdc7f3..30052fb670 100644 --- a/src/app/content/components/Page/PageContent.tsx +++ b/src/app/content/components/Page/PageContent.tsx @@ -166,7 +166,6 @@ export default styled(MainContent)` margin-bottom: 5px; /* fix double scrollbar bug */ } - #${MAIN_CONTENT_ID} * { overflow: initial; /* rex styles default to overflow hidden, breaks content */ } diff --git a/src/app/content/components/Page/contentDOMTransformations.ts b/src/app/content/components/Page/contentDOMTransformations.ts index 7848f5e002..236db17f81 100644 --- a/src/app/content/components/Page/contentDOMTransformations.ts +++ b/src/app/content/components/Page/contentDOMTransformations.ts @@ -35,6 +35,7 @@ export const transformContent = ( expandSolutionForFragment(document); moveFootnotes(document, rootEl, props.intl); optimizeImages(rootEl, services); + enhanceImagesForAccessibility(rootEl); }; function removeDocumentTitle(rootEl: HTMLElement) { @@ -243,3 +244,15 @@ function moveFootnotes(document: Document, rootEl: HTMLElement, intl: IntlShape) sup.appendChild(link); } } + +function enhanceImagesForAccessibility(rootEl: HTMLElement) { + const isMobile = typeof window !== 'undefined' && window.innerWidth <= 1200; + + if (!isMobile) return; + + rootEl.querySelectorAll('img').forEach((img) => { + img.setAttribute('tabindex', '0'); + img.setAttribute('role', 'button'); + img.setAttribute('aria-label', 'Open media preview'); + }); +} From a9e23de74b6876176e569e98a4603b02148495b2 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Sun, 8 Jun 2025 22:46:25 -0700 Subject: [PATCH 07/38] Fix conflict bug --- src/app/content/components/Page/PageComponent.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 61734bb152..3ca0bdfdae 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -252,26 +252,26 @@ export default class PageComponent extends Component { const handler = contentLinks.contentLinkHandler(a, () => this.props.contentLinks, this.props.services); this.clickListeners.set(a, handler); a.addEventListener('click', handler); }); } - + private listenersOff() { - lazyResources.removeScrollHandler(); - this.mapLinks((el) => { + const removeIfExists = (el: HTMLElement) => { const handler = this.clickListeners.get(el); - if (handler) el.removeEventListener('click', handler); - }); - + if (handler) { + el.removeEventListener('click', handler); + } + }; const container = this.container.current; if (container) { const handler = this.clickListeners.get(container); if (handler) container.removeEventListener('click', handler); } + this.mapLinks(removeIfExists); } private postProcess() { From 3a088ca73fa510c37817e64f2cbdcf4a5e6d85fd Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 9 Jun 2025 07:49:42 -0700 Subject: [PATCH 08/38] Remove old state logic --- src/app/content/components/Page/PageComponent.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 3ca0bdfdae..2347cd9ccf 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -1,6 +1,5 @@ import { HTMLAnchorElement, HTMLDivElement, HTMLElement, MouseEvent, KeyboardEvent } from '@openstax/types/lib.dom'; import React, { Component } from 'react'; -import type { ReactNode } from 'react'; import WeakMap from 'weak-map'; import { APP_ENV } from '../../../../config'; import { typesetMath } from '../../../../helpers/mathjax'; @@ -27,14 +26,10 @@ import { mediaModalManager, MediaModalPortal } from './MediaModalManager'; if (typeof(document) !== 'undefined') { import(/* webpackChunkName: "NodeList.forEach" */ 'mdn-polyfills/NodeList.prototype.forEach'); } -interface PageComponentState { - isModalOpen: boolean; - modalContent: ReactNode; -} const parser = new DOMParser(); -export default class PageComponent extends Component { +export default class PageComponent extends Component { public container = React.createRef(); private clickListeners = new WeakMap void>(); private searchHighlightManager = stubManager; @@ -42,10 +37,6 @@ export default class PageComponent extends Component> = []; private componentDidUpdateCounter = 0; - state: PageComponentState = { - isModalOpen: false, - modalContent: null - }; public getTransformedContent = () => { const {book, page, services} = this.props; From 89c6ded30c13c787700fef0da5eebaa0d26f5700 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 9 Jun 2025 08:56:17 -0700 Subject: [PATCH 09/38] Fix linter errors --- .../content/components/Page/MediaModal.tsx | 19 ++++++++++++------- .../components/Page/MediaModalManager.tsx | 2 +- .../content/components/Page/PageComponent.tsx | 16 ++++++---------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index 48dd9eca8a..db2158c2ae 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -2,6 +2,7 @@ import React from 'react'; import styled from 'styled-components/macro'; import ScrollLock from '../../../components/ScrollLock'; +// tslint:disable-next-line:variable-name const Overlay = styled.div` position: fixed; inset: 0; @@ -12,7 +13,7 @@ const Overlay = styled.div` align-items: center; `; - +// tslint:disable-next-line:variable-name const Modal = styled.div` background: white; border-radius: 8px; @@ -26,6 +27,7 @@ const Modal = styled.div` overflow: hidden; `; +// tslint:disable-next-line:variable-name const ScrollableContent = styled.div` overflow-y: auto; flex: 1; @@ -35,6 +37,7 @@ const ScrollableContent = styled.div` box-sizing: border-box; `; +// tslint:disable-next-line:variable-name const FloatingCloseButton = styled.button` position: absolute; top: -3rem; @@ -49,6 +52,7 @@ const FloatingCloseButton = styled.button` transform: translateY(-3rem); `; +// tslint:disable-next-line:variable-name const ModalWrapper = styled.div` position: relative; max-width: 100vw; @@ -56,11 +60,12 @@ const ModalWrapper = styled.div` box-sizing: border-box; `; +// tslint:disable-next-line:variable-name const CloseIcon = () => ( - - - - + + + + ); @@ -69,7 +74,7 @@ interface MediaModalProps { onClose: () => void; children: React.ReactNode; } - +// tslint:disable-next-line:variable-name const MediaModal: React.FC = ({ isOpen, onClose, children }) => { if (!isOpen) return null; @@ -78,7 +83,7 @@ const MediaModal: React.FC = ({ isOpen, onClose, children }) => - + diff --git a/src/app/content/components/Page/MediaModalManager.tsx b/src/app/content/components/Page/MediaModalManager.tsx index aee2647ca1..e6bdb3cd00 100644 --- a/src/app/content/components/Page/MediaModalManager.tsx +++ b/src/app/content/components/Page/MediaModalManager.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from 'react'; import { createPortal } from 'react-dom'; import MediaModal from './MediaModal'; -import {HTMLElement, HTMLDivElement } from '@openstax/types/lib.dom'; +import { HTMLElement, HTMLDivElement } from '@openstax/types/lib.dom'; type ShowModal = (content: ReactNode) => void; diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 2347cd9ccf..c76a02780c 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -72,10 +72,6 @@ export default class PageComponent extends Component { // tslint:disable-next-line: max-line-length this.highlightManager = highlightManager(this.container.current, () => this.props.highlights, this.props.services, this.props.intl); this.scrollToTopOrHashManager = scrollToTopOrHashManager(this.container.current); - // use manager pattern for image preview. Use react portal to move modal into page body. - // accept only media image because media has videos and animations. - // Sometimes data is already populated on mount, eg when navigating to a new tab - // tab index and add role for button for accessability if (this.props.searchHighlights.selectedResult) { this.searchHighlightManager.update(null, this.props.searchHighlights, { forceRedraw: true, @@ -147,7 +143,7 @@ export default class PageComponent extends Component { public render() { const pageIsReady = this.props.page && this.props.textSize !== null; const PT = this.props.ToastOverride ? this.props.ToastOverride : PageToasts; - + return @@ -225,14 +221,14 @@ export default class PageComponent extends Component { const handleInteraction = (e: MouseEvent | KeyboardEvent) => { const target = e.target as HTMLElement; - + if (target.tagName !== 'IMG' || !target.hasAttribute('tabindex')) return; - + if (e.type === 'keydown') { const keyEvent = e as KeyboardEvent; - + if (keyEvent.key !== 'Enter' && keyEvent.key !== ' ') return; - + keyEvent.preventDefault(); } @@ -264,7 +260,7 @@ export default class PageComponent extends Component { } this.mapLinks(removeIfExists); } - + private postProcess() { const container = this.container.current; From e1720dcb1eacfc3b450996881590c2f5c83264d0 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Wed, 11 Jun 2025 09:09:17 -0700 Subject: [PATCH 10/38] Update snapshots and styles --- src/app/content/__snapshots__/routes.spec.tsx.snap | 2 +- src/app/content/components/Page.spec.tsx | 2 +- src/app/content/components/Page/MediaModal.tsx | 4 +--- .../content/components/__snapshots__/Content.spec.tsx.snap | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/content/__snapshots__/routes.spec.tsx.snap b/src/app/content/__snapshots__/routes.spec.tsx.snap index 41059786c8..7a43204c36 100644 --- a/src/app/content/__snapshots__/routes.spec.tsx.snap +++ b/src/app/content/__snapshots__/routes.spec.tsx.snap @@ -783,7 +783,7 @@ Array [ className="c5" dangerouslySetInnerHTML={ Object { - "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", + "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", } } data-dynamic-style={false} diff --git a/src/app/content/components/Page.spec.tsx b/src/app/content/components/Page.spec.tsx index c3b8fc7023..27ecf42682 100644 --- a/src/app/content/components/Page.spec.tsx +++ b/src/app/content/components/Page.spec.tsx @@ -310,7 +310,7 @@ describe('Page', () => { `)).toEqual(`
- Something happens. + Something happens.
diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index db2158c2ae..f930f06bea 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -6,7 +6,7 @@ import ScrollLock from '../../../components/ScrollLock'; const Overlay = styled.div` position: fixed; inset: 0; - background-color: rgba(0, 0, 0, 0.85); + background-color: rgba(0, 0, 0, 0.90); z-index: 1000; display: flex; justify-content: center; @@ -16,7 +16,6 @@ const Overlay = styled.div` // tslint:disable-next-line:variable-name const Modal = styled.div` background: white; - border-radius: 8px; width: 100%; min-width: 100vw; max-height: 90vh; @@ -31,7 +30,6 @@ const Modal = styled.div` const ScrollableContent = styled.div` overflow-y: auto; flex: 1; - padding: 1rem; display: block; width: 100%; box-sizing: border-box; diff --git a/src/app/content/components/__snapshots__/Content.spec.tsx.snap b/src/app/content/components/__snapshots__/Content.spec.tsx.snap index 58199b4b5d..309ef46cbb 100644 --- a/src/app/content/components/__snapshots__/Content.spec.tsx.snap +++ b/src/app/content/components/__snapshots__/Content.spec.tsx.snap @@ -6826,7 +6826,7 @@ Array [ className="c75" dangerouslySetInnerHTML={ Object { - "__html": "
this is a test page
+ "__html": "
this is a test page
", } } From adc02078571d89dadde77f7847765a9162ea2988 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Wed, 11 Jun 2025 11:03:15 -0700 Subject: [PATCH 11/38] Fix opacity for background color --- src/app/content/components/Page/MediaModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index f930f06bea..a233f03289 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -6,7 +6,7 @@ import ScrollLock from '../../../components/ScrollLock'; const Overlay = styled.div` position: fixed; inset: 0; - background-color: rgba(0, 0, 0, 0.90); + background-color: rgba(0, 0, 0, 0.9); z-index: 1000; display: flex; justify-content: center; From 48a5d0ca494bc2e630caac005644f710767ff55a Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Thu, 12 Jun 2025 15:30:33 -0700 Subject: [PATCH 12/38] Add tests and update components --- .../components/Page/MediaModal.spec.tsx | 67 ++++++++ .../content/components/Page/MediaModal.tsx | 4 +- .../Page/MediaModalManager.spec.tsx | 41 +++++ .../components/Page/MediaModalManager.tsx | 2 +- .../__snapshots__/MediaModal.spec.tsx.snap | 160 ++++++++++++++++++ .../MediaModalManager.spec.tsx.snap | 3 + 6 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 src/app/content/components/Page/MediaModal.spec.tsx create mode 100644 src/app/content/components/Page/MediaModalManager.spec.tsx create mode 100644 src/app/content/components/Page/__snapshots__/MediaModal.spec.tsx.snap create mode 100644 src/app/content/components/Page/__snapshots__/MediaModalManager.spec.tsx.snap diff --git a/src/app/content/components/Page/MediaModal.spec.tsx b/src/app/content/components/Page/MediaModal.spec.tsx new file mode 100644 index 0000000000..d9388dea8d --- /dev/null +++ b/src/app/content/components/Page/MediaModal.spec.tsx @@ -0,0 +1,67 @@ +import renderer, { act } from 'react-test-renderer'; +import MediaModal from './MediaModal'; +import React from 'react'; + +describe('MediaModal', () => { + const childContent =
Test Content
; + const mockClose = jest.fn(); + + beforeEach(() => { + mockClose.mockReset(); + }); + + it('does not render when isOpen is false', () => { + const tree = renderer.create( + + {childContent} + + ).toJSON(); + expect(tree).toBeNull(); + }); + + it('renders correctly when isOpen is true', () => { + const tree = renderer.create( + + {childContent} + + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('calls onClose when overlay is clicked', () => { + const component = renderer.create( + + {childContent} + + ); + + const overlay = component.root + .findAllByType('div') + .find(el => el.props.onClick === mockClose); + if (!overlay) { + throw new Error('Overlay div with onClick handler not found'); + } + + act(() => { + overlay.props.onClick(); + }); + + expect(mockClose).toHaveBeenCalled(); + }); + + it('calls onClose when close button is clicked', () => { + const component = renderer.create( + + {childContent} + + ); + + const closeButton = component.root.findAllByType('button')[0]; + + act(() => { + closeButton.props.onClick(); + }); + + expect(mockClose).toHaveBeenCalled(); + }); +}); diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index a233f03289..22068304b2 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -29,7 +29,6 @@ const Modal = styled.div` // tslint:disable-next-line:variable-name const ScrollableContent = styled.div` overflow-y: auto; - flex: 1; display: block; width: 100%; box-sizing: border-box; @@ -58,6 +57,7 @@ const ModalWrapper = styled.div` box-sizing: border-box; `; + // tslint:disable-next-line:variable-name const CloseIcon = () => ( @@ -80,7 +80,7 @@ const MediaModal: React.FC = ({ isOpen, onClose, children }) => <> - + diff --git a/src/app/content/components/Page/MediaModalManager.spec.tsx b/src/app/content/components/Page/MediaModalManager.spec.tsx new file mode 100644 index 0000000000..adfa64d4d2 --- /dev/null +++ b/src/app/content/components/Page/MediaModalManager.spec.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import renderer, { act } from 'react-test-renderer'; +import ReactDOM from 'react-dom'; +import { mediaModalManager, MediaModalPortal } from './MediaModalManager'; + +describe('MediaModalPortal', () => { + beforeAll(() => { + ReactDOM.createPortal = jest.fn((element) => element) as any; + }); + + it('does not render initially', () => { + const tree = renderer.create().toJSON(); + expect(tree).toBeNull(); + }); + + it('renders content when opened via modal manager', () => { + const wrapper = renderer.create(); + + act(() => { + mediaModalManager.open(
Test modal content
); + }); + + const tree = wrapper.toJSON(); + expect(tree).toMatchSnapshot(); + }); + +// it('closes modal on close button click', () => { +// const wrapper = renderer.create(); + +// act(() => { +// mediaModalManager.open(
Close me
); +// }); + +// const button = wrapper.root.findAllByType('button')[0]; +// act(() => { +// button.props.onClick(); +// }); + +// expect(wrapper.toJSON()).toBeNull(); +// }); +}); diff --git a/src/app/content/components/Page/MediaModalManager.tsx b/src/app/content/components/Page/MediaModalManager.tsx index e6bdb3cd00..feadcc6eb1 100644 --- a/src/app/content/components/Page/MediaModalManager.tsx +++ b/src/app/content/components/Page/MediaModalManager.tsx @@ -26,7 +26,7 @@ class ModalManager { export const mediaModalManager = new ModalManager(); -export function MediaModalPortal() { +export function MediaModalPortal(): React.ReactPortal | null { const [isOpen, setIsOpen] = React.useState(false); const [modalContent, setModalContent] = React.useState(null); diff --git a/src/app/content/components/Page/__snapshots__/MediaModal.spec.tsx.snap b/src/app/content/components/Page/__snapshots__/MediaModal.spec.tsx.snap new file mode 100644 index 0000000000..68140e1f49 --- /dev/null +++ b/src/app/content/components/Page/__snapshots__/MediaModal.spec.tsx.snap @@ -0,0 +1,160 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MediaModal renders correctly when isOpen is true 1`] = ` +Array [ + .c0 { + -webkit-animation: 300ms bcCCNc ease-out; + animation: 300ms bcCCNc ease-out; + background-color: rgba(0,0,0,0.8); + position: absolute; + content: ""; + top: -5rem; + bottom: 0; + left: 0; + right: 0; +} + +@media print { + .c0 { + display: none; + } +} + +
, + .c0 { + position: fixed; + inset: 0; + background-color: rgba(0,0,0,0.9); + z-index: 1000; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c3 { + background: white; + width: 100%; + min-width: 100vw; + max-height: 90vh; + box-sizing: border-box; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + position: relative; + overflow: hidden; +} + +.c4 { + overflow-y: auto; + display: block; + width: 100%; + box-sizing: border-box; +} + +.c2 { + position: absolute; + top: -3rem; + right: 2.5rem; + z-index: 10; + background: none; + border: none; + padding: 0; + cursor: pointer; + width: 2.5rem; + height: 2.5rem; + -webkit-transform: translateY(-3rem); + -ms-transform: translateY(-3rem); + transform: translateY(-3rem); +} + +.c1 { + position: relative; + max-width: 100vw; + width: 100%; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + box-sizing: border-box; +} + +
+
+ +
+
+
+ Test Content +
+
+
+
+
, +] +`; diff --git a/src/app/content/components/Page/__snapshots__/MediaModalManager.spec.tsx.snap b/src/app/content/components/Page/__snapshots__/MediaModalManager.spec.tsx.snap new file mode 100644 index 0000000000..60e0afebfb --- /dev/null +++ b/src/app/content/components/Page/__snapshots__/MediaModalManager.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MediaModalPortal renders content when opened via modal manager 1`] = `null`; From 178e7bcdb6df839dc41e23e2db27787e1139f0a3 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 13 Jun 2025 11:08:02 -0400 Subject: [PATCH 13/38] update media modal style --- src/app/components/ScrollLock.tsx | 2 +- .../content/components/Page/MediaModal.tsx | 76 ++++++++----------- .../content/components/Page/PageComponent.tsx | 15 ++-- .../Page/contentDOMTransformations.ts | 4 - 4 files changed, 40 insertions(+), 57 deletions(-) diff --git a/src/app/components/ScrollLock.tsx b/src/app/components/ScrollLock.tsx index dd8c452d92..1d5dc7a2bb 100644 --- a/src/app/components/ScrollLock.tsx +++ b/src/app/components/ScrollLock.tsx @@ -21,7 +21,7 @@ const ScrollLockBodyClass = createGlobalStyle` `)} `} - ${(props: {mediumScreensOnly?: boolean}) => props.mediumScreensOnly === false && css` + ${(props: {mediumScreensOnly?: boolean}) => !props.mediumScreensOnly && css` @media print { #root { display: none; diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index 22068304b2..eb8cf7fde2 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -1,60 +1,48 @@ import React from 'react'; import styled from 'styled-components/macro'; import ScrollLock from '../../../components/ScrollLock'; +import theme from '../../../theme'; -// tslint:disable-next-line:variable-name -const Overlay = styled.div` - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.9); - z-index: 1000; - display: flex; - justify-content: center; - align-items: center; -`; +const buttonHeight = 4.2; // rem +const buttonMargin = 0.5; // rem // tslint:disable-next-line:variable-name -const Modal = styled.div` +const ScrollableContent = styled.div` background: white; - width: 100%; - min-width: 100vw; - max-height: 90vh; - box-sizing: border-box; - display: flex; - flex-direction: column; - position: relative; - overflow: hidden; + max-width: 100vw; + max-height: calc(100vh - ${(buttonHeight + buttonMargin * 2) * 2}rem); + overflow: auto; `; -// tslint:disable-next-line:variable-name -const ScrollableContent = styled.div` - overflow-y: auto; - display: block; - width: 100%; - box-sizing: border-box; -`; // tslint:disable-next-line:variable-name const FloatingCloseButton = styled.button` position: absolute; - top: -3rem; - right: 2.5rem; + top: -${buttonHeight + buttonMargin}rem; + right: ${buttonMargin}rem; z-index: 10; background: none; border: none; padding: 0; cursor: pointer; - width: 2.5rem; - height: 2.5rem; - transform: translateY(-3rem); + width: ${buttonHeight}rem; + height: ${buttonHeight}rem; `; // tslint:disable-next-line:variable-name -const ModalWrapper = styled.div` +const ContentContainer = styled.div` position: relative; - max-width: 100vw; - width: 100%; - box-sizing: border-box; +`; + +// tslint:disable-next-line:variable-name +const ModalWrapper = styled.div` + position: fixed; + inset: 0; + overflow: hidden; + z-index: ${theme.zIndex.highlightSummaryPopup + 1}; + display: flex; + justify-content: center; + align-items: center; `; @@ -78,17 +66,19 @@ const MediaModal: React.FC = ({ isOpen, onClose, children }) => return ( <> - - - + + + - - {children} - - - + {children} + + ); }; diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index c76a02780c..804f4c7eeb 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -1,4 +1,4 @@ -import { HTMLAnchorElement, HTMLDivElement, HTMLElement, MouseEvent, KeyboardEvent } from '@openstax/types/lib.dom'; +import { HTMLAnchorElement, HTMLImageElement, HTMLDivElement, HTMLElement, MouseEvent, KeyboardEvent } from '@openstax/types/lib.dom'; import React, { Component } from 'react'; import WeakMap from 'weak-map'; import { APP_ENV } from '../../../../config'; @@ -210,13 +210,10 @@ export default class PageComponent extends Component { const container = this.container.current; if (!container) return; - const triggerMediaModal = (target: HTMLElement) => { - if (typeof window !== 'undefined' && window.matchMedia(`(max-width: 1200px)`).matches) { - const outerHTML = target.outerHTML; - mediaModalManager.open( -
- ); - } + const triggerMediaModal = (target: HTMLImageElement) => { + mediaModalManager.open( + {target.alt + ); }; const handleInteraction = (e: MouseEvent | KeyboardEvent) => { @@ -232,7 +229,7 @@ export default class PageComponent extends Component { keyEvent.preventDefault(); } - triggerMediaModal(target); + triggerMediaModal(target as HTMLImageElement); }; container.addEventListener('click', handleInteraction); diff --git a/src/app/content/components/Page/contentDOMTransformations.ts b/src/app/content/components/Page/contentDOMTransformations.ts index 7c979ad0e8..465a17601d 100644 --- a/src/app/content/components/Page/contentDOMTransformations.ts +++ b/src/app/content/components/Page/contentDOMTransformations.ts @@ -252,10 +252,6 @@ function moveFootnotes(document: Document, rootEl: HTMLElement, intl: IntlShape) } function enhanceImagesForAccessibility(rootEl: HTMLElement) { - const isMobile = typeof window !== 'undefined' && window.innerWidth <= 1200; - - if (!isMobile) return; - rootEl.querySelectorAll('img').forEach((img) => { img.setAttribute('tabindex', '0'); img.setAttribute('role', 'button'); From 1b691f4b22276f268573fff217d8728e2483e6f7 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 13 Jun 2025 11:43:55 -0400 Subject: [PATCH 14/38] use full size image in media dialog --- src/app/content/components/Page/PageComponent.tsx | 2 +- src/app/content/components/Page/contentDOMTransformations.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 804f4c7eeb..0701e91c0f 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -212,7 +212,7 @@ export default class PageComponent extends Component { const triggerMediaModal = (target: HTMLImageElement) => { mediaModalManager.open( - {target.alt + {target.alt ); }; diff --git a/src/app/content/components/Page/contentDOMTransformations.ts b/src/app/content/components/Page/contentDOMTransformations.ts index 465a17601d..3411bae86e 100644 --- a/src/app/content/components/Page/contentDOMTransformations.ts +++ b/src/app/content/components/Page/contentDOMTransformations.ts @@ -129,6 +129,7 @@ function optimizeImages(rootEl: HTMLElement, services: AppServices & MiddlewareA const src = assertNotNull(i.getAttribute('src'), 'Somehow got a null src attribute'); i.setAttribute('src', services.imageCDNUtils.getOptimizedImageUrl(src)); + i.setAttribute('data-original-src', src); } } From 532aec95729df56e9b962c7c353c0c644f51ee94 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 13 Jun 2025 11:44:19 -0400 Subject: [PATCH 15/38] fix image height thing --- src/app/content/components/Page/MediaModal.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index eb8cf7fde2..906d7bdcfd 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -12,6 +12,13 @@ const ScrollableContent = styled.div` max-width: 100vw; max-height: calc(100vh - ${(buttonHeight + buttonMargin * 2) * 2}rem); overflow: auto; + + > img { + ${/* + fix ScrollableContent height issue where it is slightly larger than + the image and leaves a gap at the bottom */ ''} + display: block; + } `; From a3273ac736654884c60050ab61ec817f47f6b239 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 13 Jun 2025 12:22:23 -0400 Subject: [PATCH 16/38] use original image width --- src/app/content/components/Page/PageComponent.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 0701e91c0f..6ef26f8455 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -212,7 +212,10 @@ export default class PageComponent extends Component { const triggerMediaModal = (target: HTMLImageElement) => { mediaModalManager.open( - {target.alt + {target.alt ); }; From 9c92f685712cd608ce42ebdd4ebf70d3c81b859b Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 16 Jun 2025 17:58:29 -0700 Subject: [PATCH 17/38] Update media modal manager implementation --- .../Page/MediaModalManager.spec.tsx | 41 -------- .../components/Page/MediaModalManager.tsx | 50 ---------- .../components/Page/mediaModalManager.tsx | 94 +++++++++++++++++++ 3 files changed, 94 insertions(+), 91 deletions(-) delete mode 100644 src/app/content/components/Page/MediaModalManager.spec.tsx delete mode 100644 src/app/content/components/Page/MediaModalManager.tsx create mode 100644 src/app/content/components/Page/mediaModalManager.tsx diff --git a/src/app/content/components/Page/MediaModalManager.spec.tsx b/src/app/content/components/Page/MediaModalManager.spec.tsx deleted file mode 100644 index adfa64d4d2..0000000000 --- a/src/app/content/components/Page/MediaModalManager.spec.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import renderer, { act } from 'react-test-renderer'; -import ReactDOM from 'react-dom'; -import { mediaModalManager, MediaModalPortal } from './MediaModalManager'; - -describe('MediaModalPortal', () => { - beforeAll(() => { - ReactDOM.createPortal = jest.fn((element) => element) as any; - }); - - it('does not render initially', () => { - const tree = renderer.create().toJSON(); - expect(tree).toBeNull(); - }); - - it('renders content when opened via modal manager', () => { - const wrapper = renderer.create(); - - act(() => { - mediaModalManager.open(
Test modal content
); - }); - - const tree = wrapper.toJSON(); - expect(tree).toMatchSnapshot(); - }); - -// it('closes modal on close button click', () => { -// const wrapper = renderer.create(); - -// act(() => { -// mediaModalManager.open(
Close me
); -// }); - -// const button = wrapper.root.findAllByType('button')[0]; -// act(() => { -// button.props.onClick(); -// }); - -// expect(wrapper.toJSON()).toBeNull(); -// }); -}); diff --git a/src/app/content/components/Page/MediaModalManager.tsx b/src/app/content/components/Page/MediaModalManager.tsx deleted file mode 100644 index feadcc6eb1..0000000000 --- a/src/app/content/components/Page/MediaModalManager.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { ReactNode } from 'react'; -import { createPortal } from 'react-dom'; -import MediaModal from './MediaModal'; -import { HTMLElement, HTMLDivElement } from '@openstax/types/lib.dom'; - -type ShowModal = (content: ReactNode) => void; - -class ModalManager { - private container: HTMLElement | null = null; - private showModal: ShowModal | null = null; - - mount(setModalContent: ShowModal) { - this.showModal = setModalContent; - - if (typeof document !== 'undefined' && !this.container) { - this.container = document.createElement('div') as HTMLDivElement; - this.container.id = 'media-modal-root'; - document.body.appendChild(this.container); - } - } - - open(content: ReactNode) { - this.showModal?.(content); - } -} - -export const mediaModalManager = new ModalManager(); - -export function MediaModalPortal(): React.ReactPortal | null { - const [isOpen, setIsOpen] = React.useState(false); - const [modalContent, setModalContent] = React.useState(null); - - React.useEffect(() => { - mediaModalManager.mount((content) => { - setModalContent(content); - setIsOpen(true); - }); - }, []); - - if (typeof document === 'undefined') return null; - - return createPortal( - <> - setIsOpen(false)}> - {modalContent} - - , - document.body - ); -} diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx new file mode 100644 index 0000000000..161eae8ac1 --- /dev/null +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -0,0 +1,94 @@ +import React, { ReactNode, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import MediaModal from './MediaModal'; +import { HTMLElement, MouseEvent, KeyboardEvent } from '@openstax/types/lib.dom'; + +export type MediaModalManager = ReturnType; + +export function createMediaModalManager(container: HTMLElement | null) { + let setModalContent: ((content: ReactNode) => void) | null = null; + + const mount = (setContentHandler: (content: ReactNode) => void) => { + setModalContent = setContentHandler; + }; + + const open = (content: ReactNode) => { + setModalContent?.(content); + }; + + // tslint:disable-next-line:variable-name + const MediaModalPortal = () => { + const [isOpen, setIsOpen] = React.useState(false); + const [modalContent, setContent] = React.useState(null); + + useEffect(() => { + if (!isOpen || typeof document === 'undefined') return; + console.log('Escape key listener added'); + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setIsOpen(false); + } + }; + if (typeof document === 'undefined') return; + + const doc = document; + + doc.addEventListener('keydown', onKeyDown); + return () => { + doc.removeEventListener('keydown', onKeyDown); + }; + }, [isOpen]); + + useEffect(() => { + mount((content) => { + setContent(content); + setIsOpen(true); + }); + }, []); + + if (typeof document === 'undefined') return null; + + return createPortal( + setIsOpen(false)}> + {modalContent} + , + document.body + ); + }; + + const handleMediaInteraction = (e: MouseEvent | KeyboardEvent) => { + const target = e.target as HTMLElement; + if (target.tagName !== 'IMG') return; + + if (e.type === 'keydown') { + const key = (e as KeyboardEvent).key; + if (key !== 'Enter' && key !== ' ') return; + e.preventDefault(); + } + + const outerHTML = target.outerHTML; + if (typeof window !== 'undefined') { + open(
); + } + }; + + const attachListeners = () => { + if (!container) return; + container.addEventListener('click', handleMediaInteraction); + container.addEventListener('keydown', handleMediaInteraction); + }; + + const detachListeners = () => { + if (!container) return; + container.removeEventListener('click', handleMediaInteraction); + container.removeEventListener('keydown', handleMediaInteraction); + }; + + return { + mount, + open, + MediaModalPortal, + attachListeners, + detachListeners, + }; +} From edb751b7d952c12241d7d077cdcabd2957650fe6 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 16 Jun 2025 17:59:19 -0700 Subject: [PATCH 18/38] Fix pointer event issue and add role --- src/app/content/components/Page/MediaModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/content/components/Page/MediaModal.tsx b/src/app/content/components/Page/MediaModal.tsx index 906d7bdcfd..f137917392 100644 --- a/src/app/content/components/Page/MediaModal.tsx +++ b/src/app/content/components/Page/MediaModal.tsx @@ -39,6 +39,7 @@ const FloatingCloseButton = styled.button` // tslint:disable-next-line:variable-name const ContentContainer = styled.div` position: relative; + pointer-events: auto; `; // tslint:disable-next-line:variable-name @@ -50,6 +51,7 @@ const ModalWrapper = styled.div` display: flex; justify-content: center; align-items: center; + pointer-events: none; `; @@ -78,8 +80,8 @@ const MediaModal: React.FC = ({ isOpen, onClose, children }) => overlay={true} zIndex={theme.zIndex.highlightSummaryPopup} /> - - + + From 3a7390551e3442de17e027f95db476c09d632fca Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 16 Jun 2025 18:10:54 -0700 Subject: [PATCH 19/38] Update page component to use new modalManager --- .../content/components/Page/PageComponent.tsx | 47 +++---------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 6ef26f8455..5e4ef68ec1 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -1,4 +1,4 @@ -import { HTMLAnchorElement, HTMLImageElement, HTMLDivElement, HTMLElement, MouseEvent, KeyboardEvent } from '@openstax/types/lib.dom'; +import { HTMLAnchorElement, HTMLDivElement, HTMLElement, MouseEvent } from '@openstax/types/lib.dom'; import React, { Component } from 'react'; import WeakMap from 'weak-map'; import { APP_ENV } from '../../../../config'; @@ -22,7 +22,8 @@ import scrollToTopOrHashManager, { stubScrollToTopOrHashManager } from './scroll import searchHighlightManager, { stubManager, UpdateOptions as SearchUpdateOptions } from './searchHighlightManager'; import { validateDOMContent } from './validateDOMContent'; import isEqual from 'lodash/fp/isEqual'; -import { mediaModalManager, MediaModalPortal } from './MediaModalManager'; +import { createMediaModalManager } from './mediaModalManager'; + if (typeof(document) !== 'undefined') { import(/* webpackChunkName: "NodeList.forEach" */ 'mdn-polyfills/NodeList.prototype.forEach'); } @@ -37,6 +38,7 @@ export default class PageComponent extends Component { private scrollToTopOrHashManager = stubScrollToTopOrHashManager; private processing: Array> = []; private componentDidUpdateCounter = 0; + private mediaModalManager = createMediaModalManager(this.container.current); public getTransformedContent = () => { const {book, page, services} = this.props; @@ -79,6 +81,7 @@ export default class PageComponent extends Component { }); } this.scrollToTopOrHashManager(null, this.props.scrollToTopOrHash); + this.mediaModalManager = createMediaModalManager(this.container.current); } public async componentDidUpdate(prevProps: PagePropTypes) { @@ -138,6 +141,7 @@ export default class PageComponent extends Component { this.listenersOff(); this.searchHighlightManager.unmount(); this.highlightManager.unmount(); + this.mediaModalManager.detachListeners(); } public render() { @@ -147,7 +151,7 @@ export default class PageComponent extends Component { return - + {this.props.pageNotFound ? this.renderPageNotFound() @@ -207,38 +211,6 @@ export default class PageComponent extends Component { private listenersOn() { this.listenersOff(); - const container = this.container.current; - if (!container) return; - - const triggerMediaModal = (target: HTMLImageElement) => { - mediaModalManager.open( - {target.alt - ); - }; - - const handleInteraction = (e: MouseEvent | KeyboardEvent) => { - const target = e.target as HTMLElement; - - if (target.tagName !== 'IMG' || !target.hasAttribute('tabindex')) return; - - if (e.type === 'keydown') { - const keyEvent = e as KeyboardEvent; - - if (keyEvent.key !== 'Enter' && keyEvent.key !== ' ') return; - - keyEvent.preventDefault(); - } - - triggerMediaModal(target as HTMLImageElement); - }; - - container.addEventListener('click', handleInteraction); - container.addEventListener('keydown', handleInteraction); - this.clickListeners.set(container, handleInteraction); - this.mapLinks((a) => { const handler = contentLinks.contentLinkHandler(a, () => this.props.contentLinks, this.props.services); this.clickListeners.set(a, handler); @@ -253,11 +225,6 @@ export default class PageComponent extends Component { el.removeEventListener('click', handler); } }; - const container = this.container.current; - if (container) { - const handler = this.clickListeners.get(container); - if (handler) container.removeEventListener('click', handler); - } this.mapLinks(removeIfExists); } From f6c028afdee3a8970f07b77606f744082751b6f0 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 16 Jun 2025 18:16:59 -0700 Subject: [PATCH 20/38] Update specs and manager --- .../__snapshots__/routes.spec.tsx.snap | 2 +- .../__snapshots__/MediaModal.spec.tsx.snap | 86 +++++++------------ .../MediaModalManager.spec.tsx.snap | 3 - .../components/Page/mediaModalManager.tsx | 5 +- .../__snapshots__/Content.spec.tsx.snap | 2 +- 5 files changed, 35 insertions(+), 63 deletions(-) delete mode 100644 src/app/content/components/Page/__snapshots__/MediaModalManager.spec.tsx.snap diff --git a/src/app/content/__snapshots__/routes.spec.tsx.snap b/src/app/content/__snapshots__/routes.spec.tsx.snap index 7a43204c36..f260da2079 100644 --- a/src/app/content/__snapshots__/routes.spec.tsx.snap +++ b/src/app/content/__snapshots__/routes.spec.tsx.snap @@ -783,7 +783,7 @@ Array [ className="c5" dangerouslySetInnerHTML={ Object { - "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", + "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", } } data-dynamic-style={false} diff --git a/src/app/content/components/Page/__snapshots__/MediaModal.spec.tsx.snap b/src/app/content/components/Page/__snapshots__/MediaModal.spec.tsx.snap index 68140e1f49..7015dacaa5 100644 --- a/src/app/content/components/Page/__snapshots__/MediaModal.spec.tsx.snap +++ b/src/app/content/components/Page/__snapshots__/MediaModal.spec.tsx.snap @@ -6,6 +6,7 @@ Array [ -webkit-animation: 300ms bcCCNc ease-out; animation: 300ms bcCCNc ease-out; background-color: rgba(0,0,0,0.8); + z-index: 110; position: absolute; content: ""; top: -5rem; @@ -25,82 +26,61 @@ Array [ data-testid="scroll-lock-overlay" onClick={[MockFunction]} />, - .c0 { - position: fixed; - inset: 0; - background-color: rgba(0,0,0,0.9); - z-index: 1000; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; -} - -.c3 { + .c3 { background: white; - width: 100%; - min-width: 100vw; - max-height: 90vh; - box-sizing: border-box; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - position: relative; - overflow: hidden; + max-width: 100vw; + max-height: calc(100vh - 10.4rem); + overflow: auto; } -.c4 { - overflow-y: auto; +.c3 > img { display: block; - width: 100%; - box-sizing: border-box; } .c2 { position: absolute; - top: -3rem; - right: 2.5rem; + top: -4.7rem; + right: 0.5rem; z-index: 10; background: none; border: none; padding: 0; cursor: pointer; - width: 2.5rem; - height: 2.5rem; - -webkit-transform: translateY(-3rem); - -ms-transform: translateY(-3rem); - transform: translateY(-3rem); + width: 4.2rem; + height: 4.2rem; } .c1 { position: relative; - max-width: 100vw; - width: 100%; + pointer-events: auto; +} + +.c0 { + position: fixed; + inset: 0; + overflow: hidden; + z-index: 111; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; - box-sizing: border-box; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + pointer-events: none; }
diff --git a/src/app/content/components/Page/__snapshots__/MediaModalManager.spec.tsx.snap b/src/app/content/components/Page/__snapshots__/MediaModalManager.spec.tsx.snap deleted file mode 100644 index 60e0afebfb..0000000000 --- a/src/app/content/components/Page/__snapshots__/MediaModalManager.spec.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MediaModalPortal renders content when opened via modal manager 1`] = `null`; diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx index 161eae8ac1..a778ef5365 100644 --- a/src/app/content/components/Page/mediaModalManager.tsx +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -3,13 +3,14 @@ import { createPortal } from 'react-dom'; import MediaModal from './MediaModal'; import { HTMLElement, MouseEvent, KeyboardEvent } from '@openstax/types/lib.dom'; -export type MediaModalManager = ReturnType; export function createMediaModalManager(container: HTMLElement | null) { let setModalContent: ((content: ReactNode) => void) | null = null; const mount = (setContentHandler: (content: ReactNode) => void) => { setModalContent = setContentHandler; + attachListeners(); + }; const open = (content: ReactNode) => { @@ -23,7 +24,6 @@ export function createMediaModalManager(container: HTMLElement | null) { useEffect(() => { if (!isOpen || typeof document === 'undefined') return; - console.log('Escape key listener added'); const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { setIsOpen(false); @@ -88,7 +88,6 @@ export function createMediaModalManager(container: HTMLElement | null) { mount, open, MediaModalPortal, - attachListeners, detachListeners, }; } diff --git a/src/app/content/components/__snapshots__/Content.spec.tsx.snap b/src/app/content/components/__snapshots__/Content.spec.tsx.snap index 309ef46cbb..b342f4535f 100644 --- a/src/app/content/components/__snapshots__/Content.spec.tsx.snap +++ b/src/app/content/components/__snapshots__/Content.spec.tsx.snap @@ -6826,7 +6826,7 @@ Array [ className="c75" dangerouslySetInnerHTML={ Object { - "__html": "
this is a test page
+ "__html": "
this is a test page
", } } From c3e07d9e4cda2740c347c6a08328c63f53663888 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 16 Jun 2025 18:32:52 -0700 Subject: [PATCH 21/38] Add removed comment --- src/app/content/components/Page/PageComponent.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 5e4ef68ec1..7b40c29158 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -74,6 +74,8 @@ export default class PageComponent extends Component { // tslint:disable-next-line: max-line-length this.highlightManager = highlightManager(this.container.current, () => this.props.highlights, this.props.services, this.props.intl); this.scrollToTopOrHashManager = scrollToTopOrHashManager(this.container.current); + + // Sometimes data is already populated on mount, eg when navigating to a new tab if (this.props.searchHighlights.selectedResult) { this.searchHighlightManager.update(null, this.props.searchHighlights, { forceRedraw: true, From 9eeaf121e6500c8640d5c59eaa60c21d19e6b4c6 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Tue, 24 Jun 2025 04:15:55 -0700 Subject: [PATCH 22/38] Update label with alt text --- src/app/content/__snapshots__/routes.spec.tsx.snap | 2 +- src/app/content/components/Page.spec.tsx | 2 +- .../content/components/Page/contentDOMTransformations.ts | 4 +++- src/app/content/components/Page/mediaModalManager.tsx | 7 +++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/app/content/__snapshots__/routes.spec.tsx.snap b/src/app/content/__snapshots__/routes.spec.tsx.snap index f260da2079..1cad460123 100644 --- a/src/app/content/__snapshots__/routes.spec.tsx.snap +++ b/src/app/content/__snapshots__/routes.spec.tsx.snap @@ -783,7 +783,7 @@ Array [ className="c5" dangerouslySetInnerHTML={ Object { - "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", + "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", } } data-dynamic-style={false} diff --git a/src/app/content/components/Page.spec.tsx b/src/app/content/components/Page.spec.tsx index 27ecf42682..737a7b7cfd 100644 --- a/src/app/content/components/Page.spec.tsx +++ b/src/app/content/components/Page.spec.tsx @@ -310,7 +310,7 @@ describe('Page', () => { `)).toEqual(`
- Something happens. + Something happens.
diff --git a/src/app/content/components/Page/contentDOMTransformations.ts b/src/app/content/components/Page/contentDOMTransformations.ts index 3411bae86e..75dabd493e 100644 --- a/src/app/content/components/Page/contentDOMTransformations.ts +++ b/src/app/content/components/Page/contentDOMTransformations.ts @@ -256,6 +256,8 @@ function enhanceImagesForAccessibility(rootEl: HTMLElement) { rootEl.querySelectorAll('img').forEach((img) => { img.setAttribute('tabindex', '0'); img.setAttribute('role', 'button'); - img.setAttribute('aria-label', 'Open media preview'); + const alt = img.getAttribute('alt'); + const label = alt ? `Open preview of ${alt}` : 'Open media preview'; + img.setAttribute('aria-label', label); }); } diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx index a778ef5365..8d600879b4 100644 --- a/src/app/content/components/Page/mediaModalManager.tsx +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -1,7 +1,7 @@ import React, { ReactNode, useEffect } from 'react'; import { createPortal } from 'react-dom'; import MediaModal from './MediaModal'; -import { HTMLElement, MouseEvent, KeyboardEvent } from '@openstax/types/lib.dom'; +import { HTMLElement, MouseEvent, KeyboardEvent, HTMLImageElement } from '@openstax/types/lib.dom'; export function createMediaModalManager(container: HTMLElement | null) { @@ -57,7 +57,7 @@ export function createMediaModalManager(container: HTMLElement | null) { }; const handleMediaInteraction = (e: MouseEvent | KeyboardEvent) => { - const target = e.target as HTMLElement; + const target = e.target as HTMLImageElement; if (target.tagName !== 'IMG') return; if (e.type === 'keydown') { @@ -66,9 +66,8 @@ export function createMediaModalManager(container: HTMLElement | null) { e.preventDefault(); } - const outerHTML = target.outerHTML; if (typeof window !== 'undefined') { - open(
); + open({target.alt); } }; From 7150543810c87296c381bc35f2d830c321552bad Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Tue, 24 Jun 2025 10:49:28 -0700 Subject: [PATCH 23/38] Refactor test --- .../components/Page/MediaModal.spec.tsx | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/app/content/components/Page/MediaModal.spec.tsx b/src/app/content/components/Page/MediaModal.spec.tsx index d9388dea8d..b230e822bc 100644 --- a/src/app/content/components/Page/MediaModal.spec.tsx +++ b/src/app/content/components/Page/MediaModal.spec.tsx @@ -3,37 +3,31 @@ import MediaModal from './MediaModal'; import React from 'react'; describe('MediaModal', () => { - const childContent =
Test Content
; const mockClose = jest.fn(); + const renderMediaModal = (isOpen: boolean) => + renderer.create( + +
Test Content
+
+ ); + beforeEach(() => { mockClose.mockReset(); }); it('does not render when isOpen is false', () => { - const tree = renderer.create( - - {childContent} - - ).toJSON(); + const tree = renderMediaModal(false).toJSON(); expect(tree).toBeNull(); }); it('renders correctly when isOpen is true', () => { - const tree = renderer.create( - - {childContent} - - ).toJSON(); + const tree = renderMediaModal(true).toJSON(); expect(tree).toMatchSnapshot(); }); it('calls onClose when overlay is clicked', () => { - const component = renderer.create( - - {childContent} - - ); + const component = renderMediaModal(true); const overlay = component.root .findAllByType('div') @@ -50,11 +44,7 @@ describe('MediaModal', () => { }); it('calls onClose when close button is clicked', () => { - const component = renderer.create( - - {childContent} - - ); + const component = renderMediaModal(true); const closeButton = component.root.findAllByType('button')[0]; From 05c0a0126429495c54e74d60f77037508b702a64 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Wed, 25 Jun 2025 14:45:48 -0700 Subject: [PATCH 24/38] Remove redundant window check --- src/app/content/components/Page/mediaModalManager.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx index 8d600879b4..0c865b72b7 100644 --- a/src/app/content/components/Page/mediaModalManager.tsx +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -66,9 +66,9 @@ export function createMediaModalManager(container: HTMLElement | null) { e.preventDefault(); } - if (typeof window !== 'undefined') { - open({target.alt); - } + open({target.alt); + }; const attachListeners = () => { From 1ffde53d46a5fffeb909f9f315bcf5099b224759 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 14 Jul 2025 08:02:00 -0700 Subject: [PATCH 25/38] Update dom to use button for media interactive content --- .../__snapshots__/routes.spec.tsx.snap | 23 ++++- src/app/content/components/Page.spec.tsx | 2 +- .../content/components/Page/PageComponent.tsx | 20 +++- .../content/components/Page/PageContent.tsx | 22 +++++ .../Page/contentDOMTransformations.ts | 22 +++-- .../components/Page/mediaModalManager.tsx | 93 ++++++++++--------- .../__snapshots__/Content.spec.tsx.snap | 44 ++++++++- 7 files changed, 166 insertions(+), 60 deletions(-) diff --git a/src/app/content/__snapshots__/routes.spec.tsx.snap b/src/app/content/__snapshots__/routes.spec.tsx.snap index 1cad460123..aa5e18a881 100644 --- a/src/app/content/__snapshots__/routes.spec.tsx.snap +++ b/src/app/content/__snapshots__/routes.spec.tsx.snap @@ -533,6 +533,27 @@ Array [ margin-bottom: 5px; } +.c4 .image-button-wrapper { + border: none; + padding: 0; + margin: 0; + background: none; + display: inline-block; + cursor: pointer; +} + +.c4 .image-button-wrapper:focus { + outline: 2px solid Highlight; + outline: 2px solid -webkit-focus-ring-color; + outline-offset: 2px; +} + +.c4 .image-button-wrapper img { + display: block; + max-width: 100%; + height: auto; +} + .c4 #main-content * { overflow: initial; } @@ -783,7 +804,7 @@ Array [ className="c5" dangerouslySetInnerHTML={ Object { - "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", + "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", } } data-dynamic-style={false} diff --git a/src/app/content/components/Page.spec.tsx b/src/app/content/components/Page.spec.tsx index 737a7b7cfd..b08af95492 100644 --- a/src/app/content/components/Page.spec.tsx +++ b/src/app/content/components/Page.spec.tsx @@ -310,7 +310,7 @@ describe('Page', () => { `)).toEqual(`
- Something happens. +
diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 7b40c29158..9263f12b43 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -10,7 +10,7 @@ import { preloadedPageIdIs } from '../../utils'; import getCleanContent from '../../utils/getCleanContent'; import PageToasts from '../Page/PageToasts'; import { PagePropTypes } from './connector'; -import { transformContent, linksToOtherPagesOpenInNewTab } from './contentDOMTransformations'; +import { transformContent, linksToOtherPagesOpenInNewTab, enhanceImagesForAccessibility } from './contentDOMTransformations'; import * as contentLinks from './contentLinkHandler'; import highlightManager, { stubHighlightManager, UpdateOptions as HighlightUpdateOptions } from './highlightManager'; import * as lazyResources from './lazyResourceManager'; @@ -38,7 +38,7 @@ export default class PageComponent extends Component { private scrollToTopOrHashManager = stubScrollToTopOrHashManager; private processing: Array> = []; private componentDidUpdateCounter = 0; - private mediaModalManager = createMediaModalManager(this.container.current); + private mediaModalManager = createMediaModalManager(); public getTransformedContent = () => { const {book, page, services} = this.props; @@ -83,14 +83,21 @@ export default class PageComponent extends Component { }); } this.scrollToTopOrHashManager(null, this.props.scrollToTopOrHash); - this.mediaModalManager = createMediaModalManager(this.container.current); - } + enhanceImagesForAccessibility(this.container.current); + this.mediaModalManager.mount(this.container.current); +} public async componentDidUpdate(prevProps: PagePropTypes) { // Store the id of this update. We need it because we want to update highlight managers only once // per rerender. componentDidUpdate is called multiple times when user navigates quickly. const runId = this.getRunId(); + // When the page changes we want to mount the media modal manager + if (this.container.current){ + enhanceImagesForAccessibility(this.container.current); + this.mediaModalManager.mount(this.container.current); + } + // If page has changed, call postProcess that will remove old and attach new listeners // and start mathjax typesetting. if (prevProps.page !== this.props.page) { @@ -143,7 +150,7 @@ export default class PageComponent extends Component { this.listenersOff(); this.searchHighlightManager.unmount(); this.highlightManager.unmount(); - this.mediaModalManager.detachListeners(); + this.mediaModalManager.unmount(); } public render() { @@ -245,6 +252,9 @@ export default class PageComponent extends Component { return promise.then(() => { this.processing = this.processing.filter((p) => p !== promise); + // enhanceImagesForAccessibility(container); + this.mediaModalManager.mount(container); + }); } diff --git a/src/app/content/components/Page/PageContent.tsx b/src/app/content/components/Page/PageContent.tsx index 30052fb670..fed97e4747 100644 --- a/src/app/content/components/Page/PageContent.tsx +++ b/src/app/content/components/Page/PageContent.tsx @@ -166,6 +166,28 @@ export default styled(MainContent)` margin-bottom: 5px; /* fix double scrollbar bug */ } + .image-button-wrapper { + /* Remove default button styles for media modal img wrapper */ + border: none; + padding: 0; + margin: 0; + background: none; + display: inline-block; + cursor: pointer; + } + + .image-button-wrapper:focus { + outline: 1px solid Highlight; + outline: 1px solid -webkit-focus-ring-color; + outline-offset: 2px; + } + + .image-button-wrapper img { + display: block; + max-width: 100%; + height: auto; + } + #${MAIN_CONTENT_ID} * { overflow: initial; /* rex styles default to overflow hidden, breaks content */ } diff --git a/src/app/content/components/Page/contentDOMTransformations.ts b/src/app/content/components/Page/contentDOMTransformations.ts index 75dabd493e..40991aad60 100644 --- a/src/app/content/components/Page/contentDOMTransformations.ts +++ b/src/app/content/components/Page/contentDOMTransformations.ts @@ -35,7 +35,6 @@ export const transformContent = ( expandSolutionForFragment(document); moveFootnotes(document, rootEl, props.intl); optimizeImages(rootEl, services); - enhanceImagesForAccessibility(rootEl); }; function removeDocumentTitle(rootEl: HTMLElement) { @@ -252,12 +251,23 @@ function moveFootnotes(document: Document, rootEl: HTMLElement, intl: IntlShape) } } -function enhanceImagesForAccessibility(rootEl: HTMLElement) { +export function enhanceImagesForAccessibility(rootEl: HTMLElement) { rootEl.querySelectorAll('img').forEach((img) => { - img.setAttribute('tabindex', '0'); - img.setAttribute('role', 'button'); + if (img.parentElement?.tagName.toLowerCase() === 'button') { + return; + } + if (document === undefined) { + return + } + const button = document.createElement('button'); + button.type = 'button'; const alt = img.getAttribute('alt'); - const label = alt ? `Open preview of ${alt}` : 'Open media preview'; - img.setAttribute('aria-label', label); + const label = alt ? `Click to enlarge image of ${alt}` : 'Open media preview'; + button.setAttribute('aria-label', label); + + button.classList.add('image-button-wrapper'); + + img.parentElement?.insertBefore(button, img); + button.appendChild(img); }); } diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx index 0c865b72b7..f6b0d208ba 100644 --- a/src/app/content/components/Page/mediaModalManager.tsx +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -1,64 +1,41 @@ import React, { ReactNode, useEffect } from 'react'; import { createPortal } from 'react-dom'; import MediaModal from './MediaModal'; -import { HTMLElement, MouseEvent, KeyboardEvent, HTMLImageElement } from '@openstax/types/lib.dom'; +import { HTMLElement, MouseEvent, KeyboardEvent, HTMLButtonElement, HTMLImageElement } from '@openstax/types/lib.dom'; - -export function createMediaModalManager(container: HTMLElement | null) { +export function createMediaModalManager() { + let container: HTMLElement | null = null; let setModalContent: ((content: ReactNode) => void) | null = null; - const mount = (setContentHandler: (content: ReactNode) => void) => { - setModalContent = setContentHandler; - attachListeners(); - - }; - const open = (content: ReactNode) => { setModalContent?.(content); }; - // tslint:disable-next-line:variable-name const MediaModalPortal = () => { const [isOpen, setIsOpen] = React.useState(false); const [modalContent, setContent] = React.useState(null); useEffect(() => { - if (!isOpen || typeof document === 'undefined') return; - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - setIsOpen(false); - } - }; - if (typeof document === 'undefined') return; - - const doc = document; - - doc.addEventListener('keydown', onKeyDown); - return () => { - doc.removeEventListener('keydown', onKeyDown); + setModalContent = (content) => { + setContent(content); + setIsOpen(true); }; - }, [isOpen]); - - useEffect(() => { - mount((content) => { - setContent(content); - setIsOpen(true); - }); - }, []); - + return () => { setModalContent = null; }; + }, []); if (typeof document === 'undefined') return null; - return createPortal( + return isOpen ? createPortal( setIsOpen(false)}> {modalContent} , document.body - ); + ) : null; }; - const handleMediaInteraction = (e: MouseEvent | KeyboardEvent) => { - const target = e.target as HTMLImageElement; - if (target.tagName !== 'IMG') return; + const handleInteraction = (e: MouseEvent | KeyboardEvent) => { + const target = e.target as HTMLElement; + const button = target.closest('button.image-button-wrapper') as HTMLButtonElement | null; + if (!button) return; if (e.type === 'keydown') { const key = (e as KeyboardEvent).key; @@ -66,27 +43,51 @@ export function createMediaModalManager(container: HTMLElement | null) { e.preventDefault(); } - open({target.alt); - + const img = button.querySelector('img') as HTMLImageElement | null; + if (!img) return; + + open( + {img.alt + ); }; const attachListeners = () => { if (!container) return; - container.addEventListener('click', handleMediaInteraction); - container.addEventListener('keydown', handleMediaInteraction); + container.addEventListener('click', handleInteraction); + container.addEventListener('keydown', handleInteraction); }; const detachListeners = () => { if (!container) return; - container.removeEventListener('click', handleMediaInteraction); - container.removeEventListener('keydown', handleMediaInteraction); + container.removeEventListener('click', handleInteraction); + container.removeEventListener('keydown', handleInteraction); + }; + + const mount = (newContainer: HTMLElement) => { + // detach from previous if different container + if (container && container !== newContainer) { + detachListeners(); + } + + container = newContainer; + attachListeners(); + }; + + const unmount = () => { + detachListeners(); + container = null; }; return { - mount, open, MediaModalPortal, - detachListeners, + mount, + unmount, }; } diff --git a/src/app/content/components/__snapshots__/Content.spec.tsx.snap b/src/app/content/components/__snapshots__/Content.spec.tsx.snap index b342f4535f..c609784988 100644 --- a/src/app/content/components/__snapshots__/Content.spec.tsx.snap +++ b/src/app/content/components/__snapshots__/Content.spec.tsx.snap @@ -957,6 +957,27 @@ Array [ margin-bottom: 5px; } +.c74 .image-button-wrapper { + border: none; + padding: 0; + margin: 0; + background: none; + display: inline-block; + cursor: pointer; +} + +.c74 .image-button-wrapper:focus { + outline: 2px solid Highlight; + outline: 2px solid -webkit-focus-ring-color; + outline-offset: 2px; +} + +.c74 .image-button-wrapper img { + display: block; + max-width: 100%; + height: auto; +} + .c74 #main-content * { overflow: initial; } @@ -6826,7 +6847,7 @@ Array [ className="c75" dangerouslySetInnerHTML={ Object { - "__html": "
this is a test page
+ "__html": "
this is a test page
", } } @@ -8330,6 +8351,27 @@ Array [ margin-bottom: 5px; } +.c74 .image-button-wrapper { + border: none; + padding: 0; + margin: 0; + background: none; + display: inline-block; + cursor: pointer; +} + +.c74 .image-button-wrapper:focus { + outline: 2px solid Highlight; + outline: 2px solid -webkit-focus-ring-color; + outline-offset: 2px; +} + +.c74 .image-button-wrapper img { + display: block; + max-width: 100%; + height: auto; +} + .c74 #main-content * { overflow: initial; } From bb7b2c0a6456afe0fafeb4d0cf9415b5c6a878e9 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 14 Jul 2025 09:12:53 -0700 Subject: [PATCH 26/38] Update snapshot --- src/app/content/__snapshots__/routes.spec.tsx.snap | 4 ++-- .../components/__snapshots__/Content.spec.tsx.snap | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/content/__snapshots__/routes.spec.tsx.snap b/src/app/content/__snapshots__/routes.spec.tsx.snap index 5c4e60d07c..8885864093 100644 --- a/src/app/content/__snapshots__/routes.spec.tsx.snap +++ b/src/app/content/__snapshots__/routes.spec.tsx.snap @@ -543,8 +543,8 @@ Array [ } .c4 .image-button-wrapper:focus { - outline: 2px solid Highlight; - outline: 2px solid -webkit-focus-ring-color; + outline: 1px solid Highlight; + outline: 1px solid -webkit-focus-ring-color; outline-offset: 2px; } diff --git a/src/app/content/components/__snapshots__/Content.spec.tsx.snap b/src/app/content/components/__snapshots__/Content.spec.tsx.snap index c7872c481e..1a25bbe506 100644 --- a/src/app/content/components/__snapshots__/Content.spec.tsx.snap +++ b/src/app/content/components/__snapshots__/Content.spec.tsx.snap @@ -968,8 +968,8 @@ Array [ } .c74 .image-button-wrapper:focus { - outline: 2px solid Highlight; - outline: 2px solid -webkit-focus-ring-color; + outline: 1px solid Highlight; + outline: 1px solid -webkit-focus-ring-color; outline-offset: 2px; } @@ -8349,8 +8349,8 @@ Array [ } .c74 .image-button-wrapper:focus { - outline: 2px solid Highlight; - outline: 2px solid -webkit-focus-ring-color; + outline: 1px solid Highlight; + outline: 1px solid -webkit-focus-ring-color; outline-offset: 2px; } From 013d467d0c00155006396072f04355e9de9f61f6 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 14 Jul 2025 14:11:03 -0700 Subject: [PATCH 27/38] Update page component and mediaModalManager --- src/app/content/__snapshots__/routes.spec.tsx.snap | 2 +- src/app/content/components/Page/PageComponent.tsx | 11 +++-------- .../components/Page/contentDOMTransformations.ts | 7 ++++--- src/app/content/components/Page/mediaModalManager.tsx | 1 + .../components/__snapshots__/Content.spec.tsx.snap | 2 +- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/app/content/__snapshots__/routes.spec.tsx.snap b/src/app/content/__snapshots__/routes.spec.tsx.snap index 8885864093..da63c0b4ea 100644 --- a/src/app/content/__snapshots__/routes.spec.tsx.snap +++ b/src/app/content/__snapshots__/routes.spec.tsx.snap @@ -804,7 +804,7 @@ Array [ className="c5" dangerouslySetInnerHTML={ Object { - "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
\\"AhashlinkCurrent link", + "__html": "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ex erat, lacinia vitae mattis id, commodo vitae est. Nam quis arcu quis enim congue aliquet. Vivamus dictum rhoncus tortor, malesuada placerat tortor imperdiet ac. Fusce ac viverra nisi. Aliquam ut tempor diam. Cras rhoncus sodales massa vitae porttitor. Integer pharetra lorem a ante sollicitudin condimentum. Aliquam imperdiet erat et tellus mattis finibus. Sed nisl lectus, ultricies et ante ut, porttitor varius sem. Interdum et malesuada fames ac ante ipsum primis in faucibus. Fusce tempus interdum ante, nec placerat felis facilisis quis. Nunc nec malesuada nisi. Cras eu iaculis felis. Sed id maximus enim, sit amet sodales sapien. Proin pulvinar sem risus, nec ultricies odio viverra a. Aliquam feugiat euismod dapibus.

1.1 A Section, Probably

Morbi lobortis mattis velit, W=KEf+PEg=12mvf2+mghW=KEf+PEg=12mvf2+mgh size 12{W=\\"KE\\" rSub { size 8{f} } +\\"PE\\" rSub { size 8{g} } = { { size 8{1} } over { size 8{2} } } ital \\"mv\\" rSub { size 8{f} rSup { size 8{2} } } + ital \\"mgh\\"} {} dapibus convallis est mollis sed. Sed ullamcorper est tortor, at ultricies felis tincidunt ut. Mauris interdum nunc et convallis pharetra. Nulla ac erat nulla. Vivamus a ornare leo. Quisque luctus dui eget auctor tristique. Sed vel est eget lorem volutpat mattis non et nisi. Aliquam ultricies ultrices velit, id fringilla ex suscipit in. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat blandit felis, non finibus neque dignissim sit amet. Integer laoreet dapibus quam eget pulvinar. Aenean lobortis, arcu dignissim euismod ornare, eros est tincidunt urna, nec auctor odio quam eget tortor. Praesent sit amet metus a metus varius tincidunt. Pellentesque convallis sit amet erat eget malesuada. Proin pretium est euismod risus facilisis dignissim. Interdum et malesuada fames ac ante ipsum primis in faucibus.

Integer maximus augue vitae orci vehicula tincidunt. Integer auctor ante odio, nec porta nisi pellentesque id. Donec porttitor velit vel turpis sagittis molestie. Sed eu rhoncus orci. Donec feugiat tristique posuere. Fusce consequat, nisi vitae porttitor ornare, augue velit sodales augue, ac facilisis turpis orci vitae lectus. Mauris mauris urna, dictum a dui et, imperdiet commodo justo. Pellentesque scelerisque dui sed libero porta, sit amet rhoncus ex dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus.

P=WtP=Wt size 12{P= { {W} over {t} } } {}
1.01

Donec congue tortor sed magna ullamcorper mollis. Suspendisse eu tincidunt augue, et pulvinar purus. Fusce gravida quam lorem, sed pharetra libero bibendum et. Etiam facilisis ex vel nisi egestas, non consequat mi malesuada. Donec scelerisque magna sem, ut ornare massa rhoncus sed. In ultricies tellus porttitor enim elementum, ut mollis nisl euismod. Pellentesque eu lobortis neque. Phasellus eget luctus velit, in eleifend lectus. Pellentesque leo odio, cursus eget feugiat eu, finibus in nibh. In nec urna justo. Donec ut nisi eleifend, auctor lacus quis, hendrerit sem. Pellentesque sagittis nunc a molestie finibus. Donec pulvinar nibh elit, nec ornare nibh egestas quis. Curabitur luctus euismod lobortis.

Figure 1.0

Maecenas vehicula nec quam vitae sodales. Vestibulum bibendum accumsan mauris. Nunc rhoncus libero odio, in ullamcorper massa ornare convallis. Nullam metus tortor, aliquam a ultrices et, rhoncus in risus. Nunc vel turpis erat. Cras semper sagittis odio, eu mattis nulla faucibus quis. Aliquam egestas nunc elit, eget condimentum lectus congue et. Curabitur non gravida lorem. Integer mi nunc, commodo vitae augue vel, bibendum pretium magna. Sed ut nibh venenatis ipsum tempus mollis pretium eget est. Aliquam commodo quam ipsum, at fermentum enim tristique eget. Mauris imperdiet dapibus maximus. Donec tristique varius lacinia. Pellentesque pharetra nunc eu nisi ullamcorper placerat. Suspendisse potenti.

Integer fringilla blandit dolor ut mattis. Nam commodo ex at blandit sodales. Nullam suscipit, nunc eu mollis sagittis, purus arcu tincidunt ligula, vitae pharetra metus tortor id ex. Donec malesuada aliquet ligula id mollis. Maecenas scelerisque maximus sollicitudin. Quisque sagittis dolor at ligula congue, quis rhoncus mi porttitor. Quisque lobortis ut ante sed convallis. Integer luctus cursus molestie.

Morbi a purus a elit dapibus fermentum. Sed varius ex quis tortor tincidunt posuere. Mauris luctus risus id aliquet mollis. Phasellus ornare leo a arcu imperdiet dictum. Praesent accumsan venenatis urna, sed egestas sem pharetra a. Sed vulputate sit amet nisl a aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Mauris facilisis augue vitae est maximus pharetra in ac elit. Pellentesque viverra purus non ligula tempor convallis.

Pellentesque lectus ante, malesuada ut ornare vitae, euismod non urna. Morbi varius eros eget felis accumsan vehicula. In diam ex, finibus rhoncus dapibus non, lacinia in lacus. Nullam pretium mollis laoreet. Ut interdum dui sit amet elementum dignissim. Etiam in nibh vitae ligula fringilla aliquet quis vel ex. Nullam quis elit libero. In aliquet laoreet arcu at posuere. Duis consectetur et dolor non accumsan. Fusce enim est, dignissim sed mauris at, ornare cursus ligula. Nullam tincidunt eu purus vel lacinia. Praesent varius, arcu sed facilisis elementum, orci metus vestibulum nisl, sed sodales urna eros ullamcorper turpis. Donec mollis ac massa ac elementum. Nulla maximus purus sed orci efficitur, ut porta mauris elementum.

Vestibulum blandit enim quis sapien ultrices pretium. Cras dapibus ornare fringilla. Duis ut laoreet dolor. Proin ullamcorper molestie dui eu vestibulum. Nullam convallis mollis mauris nec accumsan. Proin egestas enim non nisl suscipit, ut feugiat felis pharetra. In faucibus fringilla ipsum vel sollicitudin. Maecenas blandit augue odio, ac mattis justo feugiat sit amet. Etiam nec molestie eros. Sed gravida velit libero, at pretium diam venenatis sit amet. Donec tellus leo, faucibus eu sapien nec, mollis efficitur lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

test term
test definition with math element
hashlinkCurrent link", } } data-dynamic-style={false} diff --git a/src/app/content/components/Page/PageComponent.tsx b/src/app/content/components/Page/PageComponent.tsx index 9263f12b43..564bee63bf 100644 --- a/src/app/content/components/Page/PageComponent.tsx +++ b/src/app/content/components/Page/PageComponent.tsx @@ -10,7 +10,7 @@ import { preloadedPageIdIs } from '../../utils'; import getCleanContent from '../../utils/getCleanContent'; import PageToasts from '../Page/PageToasts'; import { PagePropTypes } from './connector'; -import { transformContent, linksToOtherPagesOpenInNewTab, enhanceImagesForAccessibility } from './contentDOMTransformations'; +import { transformContent, linksToOtherPagesOpenInNewTab } from './contentDOMTransformations'; import * as contentLinks from './contentLinkHandler'; import highlightManager, { stubHighlightManager, UpdateOptions as HighlightUpdateOptions } from './highlightManager'; import * as lazyResources from './lazyResourceManager'; @@ -83,7 +83,6 @@ export default class PageComponent extends Component { }); } this.scrollToTopOrHashManager(null, this.props.scrollToTopOrHash); - enhanceImagesForAccessibility(this.container.current); this.mediaModalManager.mount(this.container.current); } @@ -92,9 +91,8 @@ export default class PageComponent extends Component { // per rerender. componentDidUpdate is called multiple times when user navigates quickly. const runId = this.getRunId(); - // When the page changes we want to mount the media modal manager - if (this.container.current){ - enhanceImagesForAccessibility(this.container.current); + // When the page changes we want to mount it to the media modal manager + if (this.container.current) { this.mediaModalManager.mount(this.container.current); } @@ -252,9 +250,6 @@ export default class PageComponent extends Component { return promise.then(() => { this.processing = this.processing.filter((p) => p !== promise); - // enhanceImagesForAccessibility(container); - this.mediaModalManager.mount(container); - }); } diff --git a/src/app/content/components/Page/contentDOMTransformations.ts b/src/app/content/components/Page/contentDOMTransformations.ts index 40991aad60..5a1f89f87b 100644 --- a/src/app/content/components/Page/contentDOMTransformations.ts +++ b/src/app/content/components/Page/contentDOMTransformations.ts @@ -35,6 +35,7 @@ export const transformContent = ( expandSolutionForFragment(document); moveFootnotes(document, rootEl, props.intl); optimizeImages(rootEl, services); + enhanceImagesForAccessibility(rootEl); }; function removeDocumentTitle(rootEl: HTMLElement) { @@ -251,18 +252,18 @@ function moveFootnotes(document: Document, rootEl: HTMLElement, intl: IntlShape) } } -export function enhanceImagesForAccessibility(rootEl: HTMLElement) { +function enhanceImagesForAccessibility(rootEl: HTMLElement) { rootEl.querySelectorAll('img').forEach((img) => { if (img.parentElement?.tagName.toLowerCase() === 'button') { return; } if (document === undefined) { - return + return; } const button = document.createElement('button'); button.type = 'button'; const alt = img.getAttribute('alt'); - const label = alt ? `Click to enlarge image of ${alt}` : 'Open media preview'; + const label = alt ? `Click to enlarge image of ${alt}` : 'Click to enlarge this image'; button.setAttribute('aria-label', label); button.classList.add('image-button-wrapper'); diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx index f6b0d208ba..5d4fd0d181 100644 --- a/src/app/content/components/Page/mediaModalManager.tsx +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -11,6 +11,7 @@ export function createMediaModalManager() { setModalContent?.(content); }; +// tslint:disable-next-line:variable-name const MediaModalPortal = () => { const [isOpen, setIsOpen] = React.useState(false); const [modalContent, setContent] = React.useState(null); diff --git a/src/app/content/components/__snapshots__/Content.spec.tsx.snap b/src/app/content/components/__snapshots__/Content.spec.tsx.snap index 1a25bbe506..05b178737e 100644 --- a/src/app/content/components/__snapshots__/Content.spec.tsx.snap +++ b/src/app/content/components/__snapshots__/Content.spec.tsx.snap @@ -6834,7 +6834,7 @@ Array [ className="c75" dangerouslySetInnerHTML={ Object { - "__html": "
this is a test page
+ "__html": "
this is a test page
", } } From 0b4599a59310d84464f8484b88ce1789c73bd453 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 14 Jul 2025 14:44:02 -0700 Subject: [PATCH 28/38] Change enhanceImagesForAccessibility function signature --- .../content/components/Page/contentDOMTransformations.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/content/components/Page/contentDOMTransformations.ts b/src/app/content/components/Page/contentDOMTransformations.ts index 5a1f89f87b..fdcab8b2c3 100644 --- a/src/app/content/components/Page/contentDOMTransformations.ts +++ b/src/app/content/components/Page/contentDOMTransformations.ts @@ -35,7 +35,7 @@ export const transformContent = ( expandSolutionForFragment(document); moveFootnotes(document, rootEl, props.intl); optimizeImages(rootEl, services); - enhanceImagesForAccessibility(rootEl); + enhanceImagesForAccessibility(document, rootEl); }; function removeDocumentTitle(rootEl: HTMLElement) { @@ -252,14 +252,12 @@ function moveFootnotes(document: Document, rootEl: HTMLElement, intl: IntlShape) } } -function enhanceImagesForAccessibility(rootEl: HTMLElement) { +function enhanceImagesForAccessibility(document: Document, rootEl: HTMLElement) { rootEl.querySelectorAll('img').forEach((img) => { if (img.parentElement?.tagName.toLowerCase() === 'button') { return; } - if (document === undefined) { - return; - } + const button = document.createElement('button'); button.type = 'button'; const alt = img.getAttribute('alt'); From 5f36c5614c2e7eeaa29a3e8176bcdcd79a547994 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Tue, 15 Jul 2025 12:31:48 -0700 Subject: [PATCH 29/38] Update EXPECTED_SCROLL_TOPS values --- src/app/content/components/Content.browserspec.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/content/components/Content.browserspec.tsx b/src/app/content/components/Content.browserspec.tsx index b64e08ffdf..a1d105d3dd 100644 --- a/src/app/content/components/Content.browserspec.tsx +++ b/src/app/content/components/Content.browserspec.tsx @@ -10,8 +10,8 @@ const TEST_CASES: { [testCase: string]: (target: Page) => Promise } = { Desktop: setDesktopViewport, Mobile: setMobileViewport, }; const EXPECTED_SCROLL_TOPS: { [testCase: string]: number[] } = { - Desktop: [242, 90, 122, 242, 365, 668, 761, 1263, 1607], - Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1751, 2118], + Desktop: [242, 90, 122, 242, 365, 668, 761, 1270, 1614], + Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1758, 2125], }; beforeAll(async() => { @@ -55,7 +55,6 @@ describe('Content', () => { await navigate(page, TEST_PAGE_URL); await finishRender(page); - // scrolling on initial load doesn't work on the dev build if (process.env.SERVER_MODE === 'built') { // Loading page with anchor From 9ba613ef70c9971fc9e1e95b7d188f170ef25a6d Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Wed, 16 Jul 2025 08:50:40 -0700 Subject: [PATCH 30/38] Update EXPECTED_SCROLL_TOPS to match CI --- src/app/content/components/Content.browserspec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/content/components/Content.browserspec.tsx b/src/app/content/components/Content.browserspec.tsx index a1d105d3dd..310f32f108 100644 --- a/src/app/content/components/Content.browserspec.tsx +++ b/src/app/content/components/Content.browserspec.tsx @@ -10,8 +10,8 @@ const TEST_CASES: { [testCase: string]: (target: Page) => Promise } = { Desktop: setDesktopViewport, Mobile: setMobileViewport, }; const EXPECTED_SCROLL_TOPS: { [testCase: string]: number[] } = { - Desktop: [242, 90, 122, 242, 365, 668, 761, 1270, 1614], - Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1758, 2125], + Desktop: [242, 90, 122, 242, 365, 668, 761, 1268, 1614], + Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1756, 2125], }; beforeAll(async() => { From aa37c3ca2fcee44e5cfe5bc76ba59d9674ff66c0 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Wed, 16 Jul 2025 09:38:51 -0700 Subject: [PATCH 31/38] Match CI test output --- src/app/content/components/Content.browserspec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/content/components/Content.browserspec.tsx b/src/app/content/components/Content.browserspec.tsx index 310f32f108..1e01f9c2b5 100644 --- a/src/app/content/components/Content.browserspec.tsx +++ b/src/app/content/components/Content.browserspec.tsx @@ -10,8 +10,8 @@ const TEST_CASES: { [testCase: string]: (target: Page) => Promise } = { Desktop: setDesktopViewport, Mobile: setMobileViewport, }; const EXPECTED_SCROLL_TOPS: { [testCase: string]: number[] } = { - Desktop: [242, 90, 122, 242, 365, 668, 761, 1268, 1614], - Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1756, 2125], + Desktop: [242, 90, 122, 242, 365, 668, 761, 1268, 1612], + Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1756, 2123], }; beforeAll(async() => { From c303af96dddceab29955fd9148ed1f7d912c5980 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 15 Sep 2025 02:42:14 -0500 Subject: [PATCH 32/38] Update tests --- src/app/content/components/Page.spec.tsx | 205 ++++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/src/app/content/components/Page.spec.tsx b/src/app/content/components/Page.spec.tsx index ba4b39f762..24e0ef7828 100644 --- a/src/app/content/components/Page.spec.tsx +++ b/src/app/content/components/Page.spec.tsx @@ -1,6 +1,6 @@ import { Highlight } from '@openstax/highlighter'; import { SearchResult } from '@openstax/open-search-client'; -import { Document, HTMLDetailsElement, HTMLElement, HTMLAnchorElement } from '@openstax/types/lib.dom'; +import { Document, HTMLDetailsElement, HTMLElement, HTMLAnchorElement, HTMLImageElement, HTMLButtonElement } from '@openstax/types/lib.dom'; import defer from 'lodash/fp/defer'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -37,6 +37,7 @@ import { formatBookData } from '../utils'; import ConnectedPage, { PageComponent } from './Page'; import PageNotFound from './Page/PageNotFound'; import allImagesLoaded from './utils/allImagesLoaded'; +import { createMediaModalManager } from '../components/Page/mediaModalManager'; // fix path jest.mock('./utils/allImagesLoaded', () => jest.fn()); jest.mock('../highlights/components/utils/showConfirmation', () => () => new Promise((resolve) => resolve(false))); @@ -1485,4 +1486,206 @@ describe('Page', () => { expect(target.innerHTML).toEqual(''); }); }); + describe('media modal interactions', () => { + const figureHtml = ` +
+
+ + Something happens. + + +
+
+ Figure + 1.1 + + Some explanation. +
+
+ `; + + it('opens the media modal when clicking the image button', async () => { + const { root } = renderDomWithReferences({ html: figureHtml }); + + const img = root.querySelector('.image-button-wrapper img'); + if (!img) return expect(img).toBeTruthy(); + + // use the same click helper as other tests + const evt = makeClickEvent(); + img.dispatchEvent(evt); + + // the modal portal renders into document.body + const opened = assertDocument().body.querySelector('img[tabindex="0"]'); + expect(opened).toBeTruthy(); + if (!opened) return; + + expect(opened.getAttribute('src')).toBe('http://localhost/resources/hash'); + expect(opened.getAttribute('alt')).toBe('Something happens.'); + }); + + it('closes the media modal on Escape', async () => { + const { root } = renderDomWithReferences({ html: figureHtml }); + await Promise.resolve(); + + const img = root.querySelector('.image-button-wrapper img'); + if (!img) return expect(img).toBeTruthy(); + + // open first + img.dispatchEvent(makeClickEvent()); + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeTruthy(); + + // send escape + assertDocument().dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); + + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeFalsy(); + + img.dispatchEvent(makeClickEvent()); + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeTruthy(); + + // send Esc event + assertDocument().dispatchEvent(new KeyboardEvent('keydown', { key: 'Esc', bubbles: true })); + + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeFalsy(); + + }); + + it('mount does nothing when container is missing', () => { + const { mount, MediaModalPortal } = createMediaModalManager(); + if (!document) return + // Render portal + const host = document.createElement('div'); + document.body.appendChild(host); + ReactDOM.render(, host); + + // Intentionally pass an invalid container to hit if (!container) return; + expect(() => mount(undefined!)).not.toThrow(); + + // Sanity: nothing opened (no listeners were attached) + document.body.dispatchEvent(makeClickEvent()); + expect(document.body.querySelector('img[tabindex="0"]')).toBeFalsy(); + }); + + it('does not open after unmount', async () => { + const { root } = renderDomWithReferences({ html: figureHtml }); + await Promise.resolve(); + + const img = root.querySelector('.image-button-wrapper img'); + if (!img) return expect(img).toBeTruthy(); + + // unmount page + ReactDOM.unmountComponentAtNode(root); + + // try clicking again + img.dispatchEvent(makeClickEvent()); + + // still query document.body + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeFalsy(); + }); + + it('opens via Enter/Space keydown and ignores other keys', async () => { + const { root } = renderDomWithReferences({ html: figureHtml }); + await Promise.resolve(); + + const button = root.querySelector('.image-button-wrapper'); + if (!button) return expect(button).toBeTruthy(); + + // Enter + const enterEvt = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }); + Object.defineProperty(enterEvt, 'preventDefault', { value: jest.fn() }); + button.dispatchEvent(enterEvt); + + let opened = assertDocument().body.querySelector('img[tabindex="0"]'); + expect(opened).toBeTruthy(); + expect((enterEvt.preventDefault as jest.Mock)).toHaveBeenCalled(); + + // Close again + assertDocument().dispatchEvent(new KeyboardEvent('keydown', { key: 'Esc', bubbles: true })); + + // Space + const spaceEvt = new KeyboardEvent('keydown', { key: ' ', bubbles: true }); + Object.defineProperty(spaceEvt, 'preventDefault', { value: jest.fn() }); + button.dispatchEvent(spaceEvt); + + opened = assertDocument().body.querySelector('img[tabindex="0"]'); + expect(opened).toBeTruthy(); + expect((spaceEvt.preventDefault as jest.Mock)).toHaveBeenCalled(); + + // Close again + assertDocument().dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); + + // Irrelevant key + const otherEvt = new KeyboardEvent('keydown', { key: 'a', bubbles: true }); + Object.defineProperty(otherEvt, 'preventDefault', { value: jest.fn() }); + button.dispatchEvent(otherEvt); + + // should not open or call preventDefault + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeFalsy(); + expect((otherEvt.preventDefault as jest.Mock)).not.toHaveBeenCalled(); + }); + }); + + describe('media modal guard: no inside wrapper', () => { + const htmlNoImg = ` +
+
+ + + +
+
+ `; + + it('returns early when wrapper has no img', async () => { + const { root } = renderDomWithReferences({ html: htmlNoImg }); + await Promise.resolve(); + + const button = root.querySelector('.image-button-wrapper'); + if (!button) return expect(button).toBeTruthy(); + + const evt = makeClickEvent(); + button.dispatchEvent(evt); + + // assert nothing opened + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeFalsy(); + }); + }); + + describe('media modal onClose', () => { + const figureHtml = ` +
+
+ + + +
+
+ `; + + it('calls onClose and closes the modal', async () => { + const { root } = renderDomWithReferences({ html: figureHtml }); + await Promise.resolve(); + + const img = root.querySelector('.image-button-wrapper img'); + if (!img) return expect(img).toBeTruthy(); + + // Open via click + img.dispatchEvent(makeClickEvent()); + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeTruthy(); + expect(img.getAttribute('alt')).toBe(null); + + // Click the close button + const closeBtn = assertDocument().body.querySelector('[aria-label="Close media preview"]') as HTMLButtonElement | null; + expect(closeBtn).toBeTruthy(); + + if (closeBtn) { + closeBtn.dispatchEvent(makeClickEvent()); + } + + // Closed + expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeFalsy(); + expect(assertDocument().body.querySelector('[aria-label="Close media preview"]')).toBeFalsy(); + }); + + }); + }); From f00e14646158e42bf00a3da410f08b72ae7925c7 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 15 Sep 2025 02:42:42 -0500 Subject: [PATCH 33/38] Refactor modal manager --- .../Page/contentDOMTransformations.ts | 2 +- .../components/Page/mediaModalManager.tsx | 121 ++++++++++++------ 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/src/app/content/components/Page/contentDOMTransformations.ts b/src/app/content/components/Page/contentDOMTransformations.ts index fdcab8b2c3..0fe1e58dc1 100644 --- a/src/app/content/components/Page/contentDOMTransformations.ts +++ b/src/app/content/components/Page/contentDOMTransformations.ts @@ -254,7 +254,7 @@ function moveFootnotes(document: Document, rootEl: HTMLElement, intl: IntlShape) function enhanceImagesForAccessibility(document: Document, rootEl: HTMLElement) { rootEl.querySelectorAll('img').forEach((img) => { - if (img.parentElement?.tagName.toLowerCase() === 'button') { + if (img.closest('button')) { return; } diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx index 5d4fd0d181..de72243aa6 100644 --- a/src/app/content/components/Page/mediaModalManager.tsx +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -1,90 +1,127 @@ import React, { ReactNode, useEffect } from 'react'; import { createPortal } from 'react-dom'; import MediaModal from './MediaModal'; -import { HTMLElement, MouseEvent, KeyboardEvent, HTMLButtonElement, HTMLImageElement } from '@openstax/types/lib.dom'; +import { + HTMLElement, + MouseEvent, + KeyboardEvent, + HTMLButtonElement, + HTMLImageElement, +} from '@openstax/types/lib.dom'; +import { assertDocument } from '../../../utils'; -export function createMediaModalManager() { - let container: HTMLElement | null = null; +function createInteractionHandler(open: (content: ReactNode) => void) { + return (e: MouseEvent | KeyboardEvent) => { + const target = e.target as HTMLElement; + + const button = target.closest('button.image-button-wrapper') as HTMLButtonElement; + if (!button) return; + + if (e.type === 'keydown') { + const key = (e as KeyboardEvent).key; + if (key !== 'Enter' && key !== ' ') return; + e.preventDefault(); + } + + const img = button.querySelector('img') as HTMLImageElement | null; + if (!img) return; + + open( + {img.alt + ); + }; +} + +function createMediaModalPortal() { let setModalContent: ((content: ReactNode) => void) | null = null; const open = (content: ReactNode) => { setModalContent?.(content); }; -// tslint:disable-next-line:variable-name - const MediaModalPortal = () => { + const MediaModalPortal: React.FC = () => { const [isOpen, setIsOpen] = React.useState(false); const [modalContent, setContent] = React.useState(null); + const document = assertDocument(); useEffect(() => { setModalContent = (content) => { setContent(content); setIsOpen(true); }; - return () => { setModalContent = null; }; + return () => { + setModalContent = null; + }; }, []); - if (typeof document === 'undefined') return null; - return isOpen ? createPortal( + useEffect(() => { + if (!isOpen || typeof document === 'undefined') return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' || e.key === 'Esc') { + setIsOpen(false); + } + }; + + + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('keydown', onKeyDown); + }; + }, [document, isOpen]); + return createPortal( setIsOpen(false)}> {modalContent} , document.body - ) : null; + ); }; - const handleInteraction = (e: MouseEvent | KeyboardEvent) => { - const target = e.target as HTMLElement; - const button = target.closest('button.image-button-wrapper') as HTMLButtonElement | null; - if (!button) return; - - if (e.type === 'keydown') { - const key = (e as KeyboardEvent).key; - if (key !== 'Enter' && key !== ' ') return; - e.preventDefault(); - } - - const img = button.querySelector('img') as HTMLImageElement | null; - if (!img) return; + return { open, MediaModalPortal }; +} - open( - {img.alt - ); - }; +function createListeners(open: (content: ReactNode) => void) { + let container: HTMLElement | null = null; + const handleInteraction = createInteractionHandler(open); - const attachListeners = () => { + const attach = () => { if (!container) return; container.addEventListener('click', handleInteraction); container.addEventListener('keydown', handleInteraction); }; - const detachListeners = () => { - if (!container) return; + const detach = () => { + if (!container ) return; container.removeEventListener('click', handleInteraction); container.removeEventListener('keydown', handleInteraction); }; const mount = (newContainer: HTMLElement) => { - // detach from previous if different container - if (container && container !== newContainer) { - detachListeners(); + if (container !== newContainer) { + detach(); + container = newContainer; } - - container = newContainer; - attachListeners(); + attach(); }; + const unmount = () => { - detachListeners(); + detach(); container = null; }; + return { mount, unmount }; +} + +export function createMediaModalManager() { + const { open, MediaModalPortal } = createMediaModalPortal(); + const { mount, unmount } = createListeners(open); + return { open, MediaModalPortal, From 1e66835a160b4696aafcda0e6126ab0269fc12fc Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 15 Sep 2025 02:56:41 -0500 Subject: [PATCH 34/38] Fix linter errors --- src/app/content/components/Page.spec.tsx | 16 ++++++++-------- .../components/Page/mediaModalManager.tsx | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/content/components/Page.spec.tsx b/src/app/content/components/Page.spec.tsx index 24e0ef7828..939ca88b8f 100644 --- a/src/app/content/components/Page.spec.tsx +++ b/src/app/content/components/Page.spec.tsx @@ -1504,7 +1504,7 @@ describe('Page', () => {
`; - it('opens the media modal when clicking the image button', async () => { + it('opens the media modal when clicking the image button', async() => { const { root } = renderDomWithReferences({ html: figureHtml }); const img = root.querySelector('.image-button-wrapper img'); @@ -1523,7 +1523,7 @@ describe('Page', () => { expect(opened.getAttribute('alt')).toBe('Something happens.'); }); - it('closes the media modal on Escape', async () => { + it('closes the media modal on Escape', async() => { const { root } = renderDomWithReferences({ html: figureHtml }); await Promise.resolve(); @@ -1551,7 +1551,7 @@ describe('Page', () => { it('mount does nothing when container is missing', () => { const { mount, MediaModalPortal } = createMediaModalManager(); - if (!document) return + const document = assertDocument(); // Render portal const host = document.createElement('div'); document.body.appendChild(host); @@ -1565,7 +1565,7 @@ describe('Page', () => { expect(document.body.querySelector('img[tabindex="0"]')).toBeFalsy(); }); - it('does not open after unmount', async () => { + it('does not open after unmount', async() => { const { root } = renderDomWithReferences({ html: figureHtml }); await Promise.resolve(); @@ -1582,7 +1582,7 @@ describe('Page', () => { expect(assertDocument().body.querySelector('img[tabindex="0"]')).toBeFalsy(); }); - it('opens via Enter/Space keydown and ignores other keys', async () => { + it('opens via Enter/Space keydown and ignores other keys', async() => { const { root } = renderDomWithReferences({ html: figureHtml }); await Promise.resolve(); @@ -1635,7 +1635,7 @@ describe('Page', () => {
`; - it('returns early when wrapper has no img', async () => { + it('returns early when wrapper has no img', async() => { const { root } = renderDomWithReferences({ html: htmlNoImg }); await Promise.resolve(); @@ -1661,7 +1661,7 @@ describe('Page', () => {
`; - it('calls onClose and closes the modal', async () => { + it('calls onClose and closes the modal', async() => { const { root } = renderDomWithReferences({ html: figureHtml }); await Promise.resolve(); @@ -1674,7 +1674,7 @@ describe('Page', () => { expect(img.getAttribute('alt')).toBe(null); // Click the close button - const closeBtn = assertDocument().body.querySelector('[aria-label="Close media preview"]') as HTMLButtonElement | null; + const closeBtn = assertDocument().body.querySelector('[aria-label="Close media preview"]'); expect(closeBtn).toBeTruthy(); if (closeBtn) { diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx index de72243aa6..4b6e67a617 100644 --- a/src/app/content/components/Page/mediaModalManager.tsx +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -45,6 +45,7 @@ function createMediaModalPortal() { setModalContent?.(content); }; +// tslint:disable-next-line:variable-name const MediaModalPortal: React.FC = () => { const [isOpen, setIsOpen] = React.useState(false); const [modalContent, setContent] = React.useState(null); From 57e3245d372bd0c5f07c186a93497a79c19e13c5 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 15 Sep 2025 11:52:30 -0500 Subject: [PATCH 35/38] Add test and change EXPECTED_SCROLL_TOPS --- .../components/Content.browserspec.tsx | 4 +-- src/app/content/components/Page.spec.tsx | 26 +++++++++++++++++++ .../components/Page/mediaModalManager.tsx | 13 +++++----- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/app/content/components/Content.browserspec.tsx b/src/app/content/components/Content.browserspec.tsx index 1e01f9c2b5..a1d105d3dd 100644 --- a/src/app/content/components/Content.browserspec.tsx +++ b/src/app/content/components/Content.browserspec.tsx @@ -10,8 +10,8 @@ const TEST_CASES: { [testCase: string]: (target: Page) => Promise } = { Desktop: setDesktopViewport, Mobile: setMobileViewport, }; const EXPECTED_SCROLL_TOPS: { [testCase: string]: number[] } = { - Desktop: [242, 90, 122, 242, 365, 668, 761, 1268, 1612], - Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1756, 2123], + Desktop: [242, 90, 122, 242, 365, 668, 761, 1270, 1614], + Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1758, 2125], }; beforeAll(async() => { diff --git a/src/app/content/components/Page.spec.tsx b/src/app/content/components/Page.spec.tsx index 939ca88b8f..fe0fab1d72 100644 --- a/src/app/content/components/Page.spec.tsx +++ b/src/app/content/components/Page.spec.tsx @@ -1686,6 +1686,32 @@ describe('Page', () => { expect(assertDocument().body.querySelector('[aria-label="Close media preview"]')).toBeFalsy(); }); + it('returns null when document.body is unavailable', () => { + const { MediaModalPortal } = createMediaModalManager(); + + // Create a host before the mock + const doc = assertDocument(); + const host = doc.createElement('div'); + doc.body.appendChild(host); + + // Make document.body appear unavailable + const getBody = jest.spyOn(doc, 'body', 'get'); + getBody.mockReturnValue(undefined as unknown as any); + + try { + // Should render nothing and not throw + expect(() => { + ReactDOM.render(, host); + }).not.toThrow(); + + expect(host.innerHTML).toBe(''); + } finally { + getBody.mockRestore(); + ReactDOM.unmountComponentAtNode(host); + host.remove(); + } + }); + }); }); diff --git a/src/app/content/components/Page/mediaModalManager.tsx b/src/app/content/components/Page/mediaModalManager.tsx index 4b6e67a617..5aa0a227f4 100644 --- a/src/app/content/components/Page/mediaModalManager.tsx +++ b/src/app/content/components/Page/mediaModalManager.tsx @@ -49,7 +49,6 @@ function createMediaModalPortal() { const MediaModalPortal: React.FC = () => { const [isOpen, setIsOpen] = React.useState(false); const [modalContent, setContent] = React.useState(null); - const document = assertDocument(); useEffect(() => { setModalContent = (content) => { @@ -63,18 +62,20 @@ function createMediaModalPortal() { useEffect(() => { if (!isOpen || typeof document === 'undefined') return; + const doc = assertDocument(); const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' || e.key === 'Esc') { setIsOpen(false); } }; - - - document.addEventListener('keydown', onKeyDown); + doc.addEventListener('keydown', onKeyDown); return () => { - document.removeEventListener('keydown', onKeyDown); + doc.removeEventListener('keydown', onKeyDown); }; - }, [document, isOpen]); + }, [isOpen]); + + if (typeof document === 'undefined' || !document?.body) return null; + return createPortal( setIsOpen(false)}> {modalContent} From 9ea208cac08ef69519a26ba4172b25a4e5336999 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 15 Sep 2025 12:10:30 -0500 Subject: [PATCH 36/38] Update browser spec --- src/app/content/components/Content.browserspec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/content/components/Content.browserspec.tsx b/src/app/content/components/Content.browserspec.tsx index a1d105d3dd..1e01f9c2b5 100644 --- a/src/app/content/components/Content.browserspec.tsx +++ b/src/app/content/components/Content.browserspec.tsx @@ -10,8 +10,8 @@ const TEST_CASES: { [testCase: string]: (target: Page) => Promise } = { Desktop: setDesktopViewport, Mobile: setMobileViewport, }; const EXPECTED_SCROLL_TOPS: { [testCase: string]: number[] } = { - Desktop: [242, 90, 122, 242, 365, 668, 761, 1270, 1614], - Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1758, 2125], + Desktop: [242, 90, 122, 242, 365, 668, 761, 1268, 1612], + Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1756, 2123], }; beforeAll(async() => { From 289040eb55327cbcfc812222aee12ab302804ca3 Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 15 Sep 2025 12:18:50 -0500 Subject: [PATCH 37/38] Match CI EXPECTED_SCROLL_TOPS --- src/app/content/components/Content.browserspec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/content/components/Content.browserspec.tsx b/src/app/content/components/Content.browserspec.tsx index 1e01f9c2b5..4b4277a635 100644 --- a/src/app/content/components/Content.browserspec.tsx +++ b/src/app/content/components/Content.browserspec.tsx @@ -10,8 +10,8 @@ const TEST_CASES: { [testCase: string]: (target: Page) => Promise } = { Desktop: setDesktopViewport, Mobile: setMobileViewport, }; const EXPECTED_SCROLL_TOPS: { [testCase: string]: number[] } = { - Desktop: [242, 90, 122, 242, 365, 668, 761, 1268, 1612], - Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1756, 2123], + Desktop: [242, 90, 122, 242, 365, 668, 761, 1270, 1612], + Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1758, 2123], }; beforeAll(async() => { From d595b3cec6e0ef6a7c70251e48b47b83d07c352d Mon Sep 17 00:00:00 2001 From: Prabhdip Gill Date: Mon, 15 Sep 2025 12:29:20 -0500 Subject: [PATCH 38/38] Update EXPECTED_SCROLL_TOPS again --- src/app/content/components/Content.browserspec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/content/components/Content.browserspec.tsx b/src/app/content/components/Content.browserspec.tsx index 4b4277a635..1e01f9c2b5 100644 --- a/src/app/content/components/Content.browserspec.tsx +++ b/src/app/content/components/Content.browserspec.tsx @@ -10,8 +10,8 @@ const TEST_CASES: { [testCase: string]: (target: Page) => Promise } = { Desktop: setDesktopViewport, Mobile: setMobileViewport, }; const EXPECTED_SCROLL_TOPS: { [testCase: string]: number[] } = { - Desktop: [242, 90, 122, 242, 365, 668, 761, 1270, 1612], - Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1758, 2123], + Desktop: [242, 90, 122, 242, 365, 668, 761, 1268, 1612], + Mobile: [239, 66, 96, 239, 523, 1263, 1402, 1756, 2123], }; beforeAll(async() => {