From aa61f361d8131540781f852804f695e7fff2a782 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Thu, 17 Oct 2024 15:23:31 -0500 Subject: [PATCH 1/6] Do not make the popup taller on large screens --- pages/helpjuice.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pages/helpjuice.css b/pages/helpjuice.css index 1bd471837..b5c93d0ba 100644 --- a/pages/helpjuice.css +++ b/pages/helpjuice.css @@ -4,12 +4,13 @@ } /* - * The default height of #helpjuice-widget-expanded is 620px and #helpjuice-widget-content is 500px. It fits well when - * the screen is 800px. Therefore, the widget content height should be 300px less than the screen height, while keeping - * its height between 300px and 500px. And the widget itself should be 120px taller than the content. + * The default height of #helpjuice-widget-expanded is 620px and #helpjuice-widget-content is 500px. + * It fits well when the screen is 800px. However, on smaller screens, the widget content height + * should be be 300px less than the screen height. And the widget itself should be 120px taller than + * the content. */ #helpjuice-widget #helpjuice-widget-expanded { - --content-height: clamp(100vh - 300px, 300px, 500px); + --content-height: min(100vh - 300px, 500px); height: calc(var(--content-height) + 120px) !important; } From 342a7d8affa6c9c6d40b5070cc18133967943d2a Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Thu, 17 Oct 2024 15:22:25 -0500 Subject: [PATCH 2/6] Make the beacon dismissable --- pages/_app.page.tsx | 6 ++- pages/helpjuice.css | 46 +++++++++++-------- src/components/Helpjuice/Helpjuice.tsx | 62 ++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/pages/_app.page.tsx b/pages/_app.page.tsx index 20d37f3b7..58e433c55 100644 --- a/pages/_app.page.tsx +++ b/pages/_app.page.tsx @@ -191,7 +191,10 @@ const App = ({ pageContent ) : ( - {pageContent} + + {pageContent} + + )} @@ -202,7 +205,6 @@ const App = ({ - {process.env.ALERT_MESSAGE ? ( { @@ -9,6 +10,67 @@ export const Helpjuice: React.FC = () => { const { data: session } = useSession(); const href = useLocation(); + const [dismissed, setDismissed] = useUserPreference({ + key: 'beacon_dismissed', + defaultValue: false, + }); + + // Sync the #helpjuice-widget .visible classname with the dismissed state + useEffect(() => { + const widget = document.getElementById('helpjuice-widget'); + if (dismissed) { + widget?.classList.remove('visible'); + } else { + widget?.classList.add('visible'); + } + }, [dismissed]); + + // Add a Hide Beacon link to the bottom of the popup + useEffect(() => { + const dismissLink = document.createElement('a'); + dismissLink.id = 'dismiss-beacon'; + dismissLink.textContent = 'Hide Beacon'; + dismissLink.tabIndex = 0; + document + .getElementById('helpjuice-widget-contact') + ?.appendChild(dismissLink); + + return () => { + dismissLink?.remove(); + }; + }, []); + + useEffect(() => { + const abortController = new AbortController(); + // Dismiss the beacon when the Hide Beacon link is clicked + document.getElementById('dismiss-beacon')?.addEventListener( + 'click', + () => { + // Hide the popup + document + .getElementById('helpjuice-widget-expanded') + ?.classList.remove('hj-shown'); + + setDismissed(true); + }, + { signal: abortController.signal }, + ); + + // Undismiss the beacon when it is clicked + document.getElementById('helpjuice-widget-trigger')?.addEventListener( + 'click', + () => { + setDismissed(false); + }, + { signal: abortController.signal }, + ); + + // Remove all event listeners on unmount + return () => { + abortController.abort(); + }; + }, [setDismissed]); + useEffect(() => { if (!process.env.HELPJUICE_ORIGIN) { return; From a8bc3f99e18129eb80b360f0f501e4248c7e122f Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 23 Oct 2024 10:20:18 -0500 Subject: [PATCH 3/6] Make the beacon dismissable on pages without an ApolloProvider --- pages/_app.page.tsx | 6 +- .../Helpjuice/DismissableBeacon.tsx | 73 +++++++++++++ src/components/Helpjuice/Helpjuice.tsx | 100 +++++++----------- 3 files changed, 112 insertions(+), 67 deletions(-) create mode 100644 src/components/Helpjuice/DismissableBeacon.tsx diff --git a/pages/_app.page.tsx b/pages/_app.page.tsx index 58e433c55..8f7e51f66 100644 --- a/pages/_app.page.tsx +++ b/pages/_app.page.tsx @@ -110,6 +110,7 @@ const App = ({ + ({ fontFamily: theme.typography.fontFamily, @@ -191,10 +192,7 @@ const App = ({ pageContent ) : ( - - {pageContent} - - + {pageContent} )} diff --git a/src/components/Helpjuice/DismissableBeacon.tsx b/src/components/Helpjuice/DismissableBeacon.tsx new file mode 100644 index 000000000..25703fbda --- /dev/null +++ b/src/components/Helpjuice/DismissableBeacon.tsx @@ -0,0 +1,73 @@ +import { useEffect } from 'react'; + +interface DismissableBeaconProps { + dismissed: boolean; + setDismissed: (dismissed: boolean) => void; +} + +/** + * Like , this component doesn't render anything, but it enhances the existing Helpjuice + * beacon in the DOM by making it dismissable. + */ +export const DismissableBeacon: React.FC = ({ + dismissed, + setDismissed, +}) => { + // Sync the #helpjuice-widget .visible classname with the dismissed state + useEffect(() => { + const widget = document.getElementById('helpjuice-widget'); + if (dismissed) { + widget?.classList.remove('visible'); + } else { + widget?.classList.add('visible'); + } + }, [dismissed]); + + // Add a Hide Beacon link to the bottom of the popup + useEffect(() => { + const dismissLink = document.createElement('a'); + dismissLink.id = 'dismiss-beacon'; + dismissLink.textContent = 'Hide Beacon'; + dismissLink.tabIndex = 0; + document + .getElementById('helpjuice-widget-contact') + ?.appendChild(dismissLink); + + return () => { + dismissLink?.remove(); + }; + }, []); + + useEffect(() => { + const abortController = new AbortController(); + // Dismiss the beacon when the Hide Beacon link is clicked + document.getElementById('dismiss-beacon')?.addEventListener( + 'click', + () => { + // Hide the popup + document + .getElementById('helpjuice-widget-expanded') + ?.classList.remove('hj-shown'); + + setDismissed(true); + }, + { signal: abortController.signal }, + ); + + // Undismiss the beacon when it is clicked + document.getElementById('helpjuice-widget-trigger')?.addEventListener( + 'click', + () => { + setDismissed(false); + }, + { signal: abortController.signal }, + ); + + // Remove all event listeners on unmount + return () => { + abortController.abort(); + }; + }, [setDismissed]); + + return null; +}; diff --git a/src/components/Helpjuice/Helpjuice.tsx b/src/components/Helpjuice/Helpjuice.tsx index 6d28dcf7b..9c90423c7 100644 --- a/src/components/Helpjuice/Helpjuice.tsx +++ b/src/components/Helpjuice/Helpjuice.tsx @@ -1,8 +1,14 @@ -import { useEffect } from 'react'; +import { useContext, useEffect, useState } from 'react'; +import { getApolloContext } from '@apollo/client'; import { useSession } from 'next-auth/react'; import { useUserPreference } from 'src/hooks/useUserPreference'; +import { DismissableBeacon } from './DismissableBeacon'; import { useLocation } from './useLocation'; +/** + * This component doesn't render anything, but it finds the existing Helpjuice component in the DOM + * and tweaks some things about it. + */ export const Helpjuice: React.FC = () => { // Because of the way the Helpjuice script is written, it must be added in _document.page.tsx instead of a component. // It adds content to the DOM in response to the DOMContentLoaded. If we add the Swifty script to this component, the @@ -10,67 +16,6 @@ export const Helpjuice: React.FC = () => { const { data: session } = useSession(); const href = useLocation(); - const [dismissed, setDismissed] = useUserPreference({ - key: 'beacon_dismissed', - defaultValue: false, - }); - - // Sync the #helpjuice-widget .visible classname with the dismissed state - useEffect(() => { - const widget = document.getElementById('helpjuice-widget'); - if (dismissed) { - widget?.classList.remove('visible'); - } else { - widget?.classList.add('visible'); - } - }, [dismissed]); - - // Add a Hide Beacon link to the bottom of the popup - useEffect(() => { - const dismissLink = document.createElement('a'); - dismissLink.id = 'dismiss-beacon'; - dismissLink.textContent = 'Hide Beacon'; - dismissLink.tabIndex = 0; - document - .getElementById('helpjuice-widget-contact') - ?.appendChild(dismissLink); - - return () => { - dismissLink?.remove(); - }; - }, []); - - useEffect(() => { - const abortController = new AbortController(); - // Dismiss the beacon when the Hide Beacon link is clicked - document.getElementById('dismiss-beacon')?.addEventListener( - 'click', - () => { - // Hide the popup - document - .getElementById('helpjuice-widget-expanded') - ?.classList.remove('hj-shown'); - - setDismissed(true); - }, - { signal: abortController.signal }, - ); - - // Undismiss the beacon when it is clicked - document.getElementById('helpjuice-widget-trigger')?.addEventListener( - 'click', - () => { - setDismissed(false); - }, - { signal: abortController.signal }, - ); - - // Remove all event listeners on unmount - return () => { - abortController.abort(); - }; - }, [setDismissed]); - useEffect(() => { if (!process.env.HELPJUICE_ORIGIN) { return; @@ -98,5 +43,34 @@ export const Helpjuice: React.FC = () => { } }, [session, href]); - return null; + // Use NoApolloBeacon on pages without an + const hasApolloClient = Boolean(useContext(getApolloContext()).client); + return hasApolloClient ? : ; +}; + +/** + * This variant of the dismissable beacon saves the dismissed state to a persistent user preference. + * It can only be used on pages with an . + */ +const ApolloBeacon: React.FC = () => { + const [dismissed, setDismissed] = useUserPreference({ + key: 'beacon_dismissed', + defaultValue: false, + }); + + return ( + + ); +}; + +/** + * This variant of the dismissable beacon saves the dismissed state to ephemeral state that will be + * lost when the page is reloaded. It is designed to be used on pages without an . + */ +const NoApolloBeacon: React.FC = () => { + const [dismissed, setDismissed] = useState(false); + + return ( + + ); }; From 0554c503879297a74061b42226b5a5d6cc05b6e3 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 23 Oct 2024 10:42:50 -0500 Subject: [PATCH 4/6] Add tests --- .../Helpjuice/DismissableBeacon.test.tsx | 53 +++++++++++++++++++ src/components/Helpjuice/Helpjuice.test.tsx | 27 +++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/components/Helpjuice/DismissableBeacon.test.tsx diff --git a/src/components/Helpjuice/DismissableBeacon.test.tsx b/src/components/Helpjuice/DismissableBeacon.test.tsx new file mode 100644 index 000000000..5502a2526 --- /dev/null +++ b/src/components/Helpjuice/DismissableBeacon.test.tsx @@ -0,0 +1,53 @@ +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { DismissableBeacon } from './DismissableBeacon'; + +const setDismissed = jest.fn(); + +describe('DismissableBeacon', () => { + beforeEach(() => { + document.body.innerHTML = `
+ +
+
+
+
+
`; + }); + + it('toggles visible class to widget', () => { + const { rerender } = render( + , + ); + expect(document.getElementById('helpjuice-widget')).toHaveClass('visible'); + + rerender( + , + ); + expect(document.getElementById('helpjuice-widget')).not.toHaveClass( + 'visible', + ); + }); + + it('creates Hide Beacon link that hides the popup and removes the visible class from the widget', () => { + render(); + + expect(document.getElementById('helpjuice-widget')).toHaveClass('visible'); + + // Simulating expanding the widget + const expandedWidget = document.getElementById('helpjuice-widget-expanded'); + expandedWidget?.classList.add('hj-shown'); + + userEvent.click(document.getElementById('dismiss-beacon')!); + + expect(setDismissed).toHaveBeenCalledWith(true); + expect(expandedWidget).not.toHaveClass('hj-shown'); + }); + + it('undismisses the beacon when the trigger is clicked', () => { + render(); + + userEvent.click(document.getElementById('helpjuice-widget-trigger')!); + expect(setDismissed).toHaveBeenCalledWith(false); + }); +}); diff --git a/src/components/Helpjuice/Helpjuice.test.tsx b/src/components/Helpjuice/Helpjuice.test.tsx index de879aa1c..b5dca5028 100644 --- a/src/components/Helpjuice/Helpjuice.test.tsx +++ b/src/components/Helpjuice/Helpjuice.test.tsx @@ -1,6 +1,8 @@ -import { act, render } from '@testing-library/react'; +import { act, render, waitFor } from '@testing-library/react'; import { useSession } from 'next-auth/react'; import { session } from '__tests__/fixtures/session'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { UserOptionQuery } from 'src/hooks/UserPreference.generated'; import { Helpjuice } from './Helpjuice'; describe('Helpjuice', () => { @@ -97,4 +99,27 @@ describe('Helpjuice', () => { 'https://domain.helpjuice.com/kb', ); }); + + it('uses the Apollo client when available', async () => { + const mutationSpy = jest.fn(); + render( + + mocks={{ + UserOption: { + userOption: { + key: 'dismissed', + value: 'false', + }, + }, + }} + onCall={mutationSpy} + > + + , + ); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('UserOption'), + ); + }); }); From 61dc508c2d3d82469f1b66a9c7e0752842c22b52 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 23 Oct 2024 14:20:13 -0500 Subject: [PATCH 5/6] Hide beacon while preference is loading --- src/components/Helpjuice/Helpjuice.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/Helpjuice/Helpjuice.tsx b/src/components/Helpjuice/Helpjuice.tsx index 9c90423c7..690fa6ca2 100644 --- a/src/components/Helpjuice/Helpjuice.tsx +++ b/src/components/Helpjuice/Helpjuice.tsx @@ -53,13 +53,16 @@ export const Helpjuice: React.FC = () => { * It can only be used on pages with an . */ const ApolloBeacon: React.FC = () => { - const [dismissed, setDismissed] = useUserPreference({ + const [dismissed, setDismissed, { loading }] = useUserPreference({ key: 'beacon_dismissed', defaultValue: false, }); return ( - + ); }; From ba7f9d90a3402f5bd05b017b77f6299e948a571a Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Wed, 23 Oct 2024 15:51:39 -0500 Subject: [PATCH 6/6] Replace life preserver icon with x when open --- pages/helpjuice.css | 10 ++++++++++ .../Helpjuice/DismissableBeacon.test.tsx | 9 ++------- src/components/Helpjuice/Helpjuice.test.tsx | 10 ++++++++-- src/components/Helpjuice/Helpjuice.tsx | 20 +++++++++++++++++++ src/components/Helpjuice/widget.mock.ts | 13 ++++++++++++ 5 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 src/components/Helpjuice/widget.mock.ts diff --git a/pages/helpjuice.css b/pages/helpjuice.css index 7ce970859..b088178fd 100644 --- a/pages/helpjuice.css +++ b/pages/helpjuice.css @@ -2,6 +2,16 @@ --right-position: 80px; } +/* Hide the life preserver SVG path when the widget is open and show our injected close path */ +#helpjuice-widget:has(#helpjuice-widget-expanded.hj-shown) svg .st0 { + display: none; +} + +/* Hide our injected close path when the widget is closed */ +#helpjuice-widget:has(#helpjuice-widget-expanded:not(.hj-shown)) svg .close { + display: none; +} + #helpjuice-widget .article .footer { /* Hide the estimated reading time and last updated time in search results */ display: none !important; diff --git a/src/components/Helpjuice/DismissableBeacon.test.tsx b/src/components/Helpjuice/DismissableBeacon.test.tsx index 5502a2526..2de97508f 100644 --- a/src/components/Helpjuice/DismissableBeacon.test.tsx +++ b/src/components/Helpjuice/DismissableBeacon.test.tsx @@ -1,18 +1,13 @@ import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { DismissableBeacon } from './DismissableBeacon'; +import { widgetHTML } from './widget.mock'; const setDismissed = jest.fn(); describe('DismissableBeacon', () => { beforeEach(() => { - document.body.innerHTML = `
- -
-
-
-
-
`; + document.body.innerHTML = widgetHTML; }); it('toggles visible class to widget', () => { diff --git a/src/components/Helpjuice/Helpjuice.test.tsx b/src/components/Helpjuice/Helpjuice.test.tsx index b5dca5028..c24a95425 100644 --- a/src/components/Helpjuice/Helpjuice.test.tsx +++ b/src/components/Helpjuice/Helpjuice.test.tsx @@ -4,17 +4,23 @@ import { session } from '__tests__/fixtures/session'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { UserOptionQuery } from 'src/hooks/UserPreference.generated'; import { Helpjuice } from './Helpjuice'; +import { widgetHTML } from './widget.mock'; describe('Helpjuice', () => { beforeEach(() => { process.env.HELPJUICE_ORIGIN = 'https://domain.helpjuice.com'; process.env.HELPJUICE_KNOWLEDGE_BASE_URL = 'https://domain.helpjuice.com/kb'; - document.body.innerHTML = - '
Contact UsVisit Knowledge Base'; + document.body.innerHTML = widgetHTML; location.href = 'https://example.com/'; }); + it('adds close icon svg path', () => { + render(); + + expect(document.querySelector('path.close')).toBeInTheDocument(); + }); + it('does nothing if the element is missing', () => { document.body.innerHTML = ''; diff --git a/src/components/Helpjuice/Helpjuice.tsx b/src/components/Helpjuice/Helpjuice.tsx index 690fa6ca2..e170ff00b 100644 --- a/src/components/Helpjuice/Helpjuice.tsx +++ b/src/components/Helpjuice/Helpjuice.tsx @@ -16,6 +16,26 @@ export const Helpjuice: React.FC = () => { const { data: session } = useSession(); const href = useLocation(); + // Add a white x that is shown using CSS when the panel is open + useEffect(() => { + const closeImage = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path', + ); + closeImage.classList.add('close'); + closeImage.setAttributeNS(null, 'd', 'M20,20L88,88M88,20L20,88'); + closeImage.setAttributeNS(null, 'stroke', 'white'); + closeImage.setAttributeNS(null, 'stroke-linecap', 'round'); + closeImage.setAttributeNS(null, 'stroke-width', '8'); + closeImage.setAttributeNS(null, 'fill', 'none'); + + document + .querySelector('#helpjuice-widget #helpjuice-widget-trigger svg g') + ?.appendChild(closeImage); + + return () => closeImage.remove(); + }); + useEffect(() => { if (!process.env.HELPJUICE_ORIGIN) { return; diff --git a/src/components/Helpjuice/widget.mock.ts b/src/components/Helpjuice/widget.mock.ts new file mode 100644 index 000000000..1802e7f90 --- /dev/null +++ b/src/components/Helpjuice/widget.mock.ts @@ -0,0 +1,13 @@ +export const widgetHTML = ``;