Skip to content

Commit 8e0aeab

Browse files
committed
Rewrite links for footer and selected page elements
1 parent 9755c06 commit 8e0aeab

File tree

8 files changed

+90
-38
lines changed

8 files changed

+90
-38
lines changed
Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,64 @@
11
import React from 'react';
2+
import usePortalContext from '~/contexts/portal';
23

34
// Making scripts work, per https://stackoverflow.com/a/47614491/392102
45
function activateScripts(el: HTMLElement) {
5-
const scripts: HTMLScriptElement[] = Array.from(el.querySelectorAll('script'));
6-
const processOne = (() => {
6+
const scripts: HTMLScriptElement[] = Array.from(
7+
el.querySelectorAll('script')
8+
);
9+
const processOne = () => {
710
const s = scripts.shift();
811

912
if (!s) {
1013
return;
1114
}
1215
const newScript = document.createElement('script');
13-
const p = (s.src) ? new Promise((resolve) => {
14-
newScript.onload = resolve;
15-
}) : Promise.resolve();
16+
const p = s.src
17+
? new Promise((resolve) => {
18+
newScript.onload = resolve;
19+
})
20+
: Promise.resolve();
1621

17-
Array.from(s.attributes)
18-
.forEach((a) => newScript.setAttribute(a.name, a.value));
22+
Array.from(s.attributes).forEach((a) =>
23+
newScript.setAttribute(a.name, a.value)
24+
);
1925
if (s.textContent) {
2026
newScript.appendChild(document.createTextNode(s.textContent));
2127
}
2228
newScript.async = false;
2329
s.parentNode?.replaceChild(newScript, s);
2430

2531
p.then(processOne);
26-
});
32+
};
2733

2834
processOne();
2935
}
3036

31-
type RawHTMLArgs = {Tag?: string, html?: TrustedHTML, embed?: boolean} & React.HTMLAttributes<HTMLDivElement>;
37+
type RawHTMLArgs = {
38+
Tag?: string;
39+
html?: TrustedHTML;
40+
embed?: boolean;
41+
} & React.HTMLAttributes<HTMLDivElement>;
3242

33-
export default function RawHTML({Tag='div', html, embed=false, ...otherProps}: RawHTMLArgs) {
43+
export default function RawHTML({
44+
Tag = 'div',
45+
html,
46+
embed = false,
47+
...otherProps
48+
}: RawHTMLArgs) {
3449
const ref = React.useRef<HTMLElement>();
50+
const {rewriteLinks} = usePortalContext();
3551

3652
React.useEffect(() => {
3753
if (embed && ref.current) {
3854
activateScripts(ref.current);
3955
}
4056
});
41-
return (
42-
React.createElement(Tag, {ref, dangerouslySetInnerHTML: {__html: html}, ...otherProps})
43-
);
57+
React.useLayoutEffect(() => rewriteLinks?.(ref.current as HTMLElement), [rewriteLinks]);
58+
59+
return React.createElement(Tag, {
60+
ref,
61+
dangerouslySetInnerHTML: {__html: html},
62+
...otherProps
63+
});
4464
}

src/app/components/shell/portal-router.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ import {useParams} from 'react-router-dom';
55

66
export default function PortalRouter() {
77
// If we want to validate the portal name, that can be done here
8-
const {portal} = useParams();
8+
const {portal, '*': rest} = useParams();
99
const {setPortal} = usePortalContext();
1010

11-
React.useEffect(() => setPortal(portal as string), [portal, setPortal]);
11+
React.useEffect(() => {
12+
// It seems that the path "/press" in particular winds up matching
13+
// the portal-router Route, so we need to ensure it's really a portal
14+
// route by verifying that there's something after the :portal param
15+
if (rest) {
16+
setPortal(portal as string);
17+
}
18+
}, [rest, portal, setPortal]);
1219

1320
return <RoutesAlsoInPortal />;
1421
}

src/app/components/shell/router-helpers/use-link-handler.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {useCallback} from 'react';
22
import {useNavigate, NavigateOptions} from 'react-router-dom';
3-
import usePortalContext from '~/contexts/portal';
43
import linkHelper from '~/helpers/link';
54
import $ from '~/helpers/$';
65
import retry from '~/helpers/retry';
@@ -11,12 +10,9 @@ export type TrackingInfo = {
1110
book_format?: string;
1211
contact_id?: string;
1312
resource_name?: string;
14-
};
13+
}
1514

16-
export type TrackedMouseEvent = React.MouseEvent<
17-
HTMLAnchorElement,
18-
MouseEvent
19-
> & {
15+
export type TrackedMouseEvent = React.MouseEvent<HTMLAnchorElement, MouseEvent> & {
2016
trackingInfo: TrackingInfo;
2117
};
2218

@@ -36,18 +32,12 @@ function handleExternalLink(href: Location['href'], el: HTMLElement) {
3632
type State = NavigateOptions & {x: number; y: number};
3733

3834
export default function useLinkHandler() {
39-
const {portal} = usePortalContext();
4035
const navigate = useNavigate();
4136
const navigateTo = useCallback(
4237
(path: Location['href'], state: State = {x: 0, y: 0}) => {
43-
let adjustedPath = linkHelper.stripOpenStaxDomain(path);
44-
45-
if (portal && !adjustedPath.startsWith(`/${portal}`)) {
46-
adjustedPath = `/${portal}${adjustedPath}`;
47-
}
48-
navigate(adjustedPath, state);
38+
navigate(linkHelper.stripOpenStaxDomain(path), state);
4939
},
50-
[navigate, portal]
40+
[navigate]
5141
);
5242
const linkHandler = useCallback(
5343
// eslint-disable-next-line complexity

src/app/contexts/portal.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1-
import {useState} from 'react';
1+
import React from 'react';
22
import buildContext from '~/components/jsx-helpers/build-context';
33

44
function useContextValue() {
5-
const [portal, setPortal] = useState('');
5+
const [portal, setPortal] = React.useState('');
6+
const portalPrefix = portal ? `/${portal}` : '';
7+
const rewriteLinks = React.useCallback((container: HTMLElement) => {
8+
if (!portalPrefix) {return;}
9+
const linkNodes = container.querySelectorAll('a[href^="/"]');
610

7-
return {portal, setPortal};
11+
for (const node of linkNodes) {
12+
const href = node.getAttribute('href');
13+
14+
node.setAttribute('href', `${portalPrefix}${href}`);
15+
}
16+
},
17+
[portalPrefix]);
18+
19+
React.useEffect(() => console.info('** Portal?', portal), [portal]);
20+
21+
return {portalPrefix, setPortal, rewriteLinks};
822
}
923

1024
const {useContext, ContextProvider} = buildContext({useContextValue});

src/app/layouts/default/footer/footer.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {faXTwitter} from '@fortawesome/free-brands-svg-icons/faXTwitter';
99
import {faLinkedinIn} from '@fortawesome/free-brands-svg-icons/faLinkedinIn';
1010
import {faInstagram} from '@fortawesome/free-brands-svg-icons/faInstagram';
1111
import {faYoutube} from '@fortawesome/free-brands-svg-icons/faYoutube';
12+
import usePortalContext from '~/contexts/portal';
1213
import './footer.scss';
1314

1415
function ListOfLinks({children}) {
@@ -25,6 +26,11 @@ function Footer({
2526
facebookLink, twitterLink, linkedinLink
2627
}
2728
}) {
29+
const {rewriteLinks} = usePortalContext();
30+
31+
React.useLayoutEffect(() => rewriteLinks?.(document.querySelector('.page-footer')),
32+
[rewriteLinks]);
33+
2834
return (
2935
<React.Fragment>
3036
<div className="top">

src/app/layouts/default/header/menus/main-menu/dropdown/dropdown.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ export function MenuItem({label, url, local=undefined}) {
1919
const {innerWidth: _} = useWindowContext();
2020
const urlPath = url.replace('/view-all', '');
2121
const {pathname} = useLocation();
22-
const {portal} = usePortalContext();
22+
const {portalPrefix} = usePortalContext();
2323

2424
return (
2525
<RawHTML
2626
Tag="a"
2727
html={label}
28-
href={portal ? `/${portal}${url}` : url}
28+
href={`${portalPrefix}${url}`}
2929
tabIndex={0}
3030
data-local={local}
3131
{...(urlPath === pathname ? {'aria-current': 'page'} : {})}

src/app/pages/details/common/let-us-know/let-us-know.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import {useIntl} from 'react-intl';
55
import {FontAwesomeIcon, FontAwesomeIconProps} from '@fortawesome/react-fontawesome';
66
import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus';
77
import {faBook} from '@fortawesome/free-solid-svg-icons/faBook';
8+
import usePortalContext from '~/contexts/portal';
89
import cn from 'classnames';
910
import './let-us-know.scss';
1011

1112
function useDataStuffFor(title:string) {
1213
const intl = useIntl();
14+
const {portalPrefix} = usePortalContext();
1315
const [text1, text2] = [
1416
intl.formatMessage({id: 'letusknow.text1'}),
1517
intl.formatMessage({id: 'letusknow.text2'})
@@ -25,8 +27,8 @@ function useDataStuffFor(title:string) {
2527
}
2628

2729
return {
28-
url1: `/interest?${encodeURIComponent(title)}`,
29-
url2: `/adoption?${encodeURIComponent(title)}`,
30+
url1: `${portalPrefix}/interest?${encodeURIComponent(title)}`,
31+
url2: `${portalPrefix}/adoption?${encodeURIComponent(title)}`,
3032
text1, text2
3133
};
3234
}

test/src/layouts/landing/footer.test.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {describe, it} from '@jest/globals';
55
import { MemoryRouter } from 'react-router-dom';
66
import { useContactDialog } from '~/layouts/landing/footer/flex';
77

8+
// @ts-expect-error does not exist on
9+
const {routerFuture} = global;
10+
811
const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime});
912

1013
function ShowContactDialog(props: Parameters<ReturnType<typeof useContactDialog>['ContactDialog']>[0]) {
@@ -23,15 +26,15 @@ describe('flex landing footer', () => {
2326
beforeEach(() => {
2427
jest.useFakeTimers();
2528
});
29+
const getIframe = () => document.querySelector('iframe');
2630

2731
it('opens and closes', async () => {
28-
const getIframe = () => document.querySelector('iframe');
2932
const contactFormParams = [
3033
{ key: 'userId', value: 'test' }
3134
];
3235

3336
render(
34-
<MemoryRouter initialEntries={['']}>
37+
<MemoryRouter initialEntries={['']} future={routerFuture}>
3538
<ShowContactDialog contactFormParams={contactFormParams} />
3639
</MemoryRouter>
3740
);
@@ -48,5 +51,15 @@ describe('flex landing footer', () => {
4851
window.postMessage('CONTACT_FORM_SUBMITTED', '*');
4952
await waitFor(() => expect(getIframe()).toBeNull());
5053
});
54+
55+
it('handles no contactFormParams', async () => {
56+
render(
57+
<MemoryRouter initialEntries={['']} future={routerFuture}>
58+
<ShowContactDialog />
59+
</MemoryRouter>
60+
);
61+
await user.click(await screen.findByText('Contact Us'));
62+
expect(getIframe()?.src.endsWith('/contact')).toBe(true);
63+
});
5164
});
5265
});

0 commit comments

Comments
 (0)