diff --git a/package.json b/package.json
index 36eda832a..a2427b8d5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@openstax/ui-components",
- "version": "1.14.2",
+ "version": "1.15.0",
"license": "MIT",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
diff --git a/src/components/HelpMenu.spec.tsx b/src/components/HelpMenu.spec.tsx
new file mode 100644
index 000000000..46878cc31
--- /dev/null
+++ b/src/components/HelpMenu.spec.tsx
@@ -0,0 +1,31 @@
+import { render } from '@testing-library/react';
+import { BodyPortalSlotsContext } from './BodyPortalSlotsContext';
+import { useHelpMenu } from './HelpMenu';
+import { NavBar } from './NavBar';
+
+describe('useHelpMenu', () => {
+ let root: HTMLElement;
+
+ beforeEach(() => {
+ root = document.createElement('main');
+ root.id = 'root';
+ document.body.append(root);
+ });
+
+ it('matches snapshot', () => {
+ const NavBarWithHelpMenu = () => {
+ const [HelpMenu, ContactFormIframe] = useHelpMenu(
+ [{ label: 'Test Callback', callback: () => window.alert('Ran HelpMenu callback function') }]
+ );
+
+ return
+
+
+ ;
+ };
+
+ render();
+
+ expect(document.body).toMatchSnapshot();
+ });
+});
diff --git a/src/components/HelpMenu.stories.tsx b/src/components/HelpMenu.stories.tsx
new file mode 100644
index 000000000..a53667832
--- /dev/null
+++ b/src/components/HelpMenu.stories.tsx
@@ -0,0 +1,24 @@
+import { createGlobalStyle } from 'styled-components';
+import { BodyPortalSlotsContext } from './BodyPortalSlotsContext';
+import { useHelpMenu } from './HelpMenu';
+import { NavBar } from './NavBar';
+
+const BodyPortalGlobalStyle = createGlobalStyle`
+ [data-portal-slot="nav"] {
+ position: fixed;
+ top: 0;
+ width: 100%;
+ }
+`;
+
+export const Default = () => {
+ const [HelpMenu, ContactFormIframe] = useHelpMenu(
+ [{ label: 'Test Callback', callback: () => window.alert('Ran HelpMenu callback function') }]
+ );
+
+ return
+
+
+
+ ;
+};
diff --git a/src/components/HelpMenu.tsx b/src/components/HelpMenu.tsx
new file mode 100644
index 000000000..be2382527
--- /dev/null
+++ b/src/components/HelpMenu.tsx
@@ -0,0 +1,175 @@
+import React from 'react';
+import { NavBarMenuButton, NavBarMenuItem } from './NavBarMenuButtons';
+import { colors } from '../theme';
+import styled from 'styled-components';
+
+export const styledMenu = {
+ Button: styled(NavBarMenuButton)`
+ color: ${colors.palette.gray};
+ font-size: 1.4rem;
+ `,
+ Item: styled(NavBarMenuItem)`
+ color: ${colors.palette.neutralDarker};
+ text-decoration: none;
+
+ :focus-visible {
+ outline: 0;
+ background: ${colors.palette.neutralLighter};
+ }
+ :hover {
+ color: ${colors.palette.neutralDarker};
+ text-decoration: none;
+ }
+ `,
+};
+
+const IframeWrapper = styled.div`
+ background-color: ${colors.palette.neutralBright};
+ position: absolute;
+ width: 100%;
+ top: 4rem;
+ left: 0;
+ bottom: 0;
+ z-index: 20;
+`;
+
+const Iframe = styled.iframe`
+ border: 0;
+ width: 100%;
+ height: calc(100% - 5rem);
+`;
+
+function PutAway({onClick, className}: {onClick: () => void; className?: string}) {
+ return (
+
+
+
+ );
+}
+
+const StyledPutAway = styled(PutAway)`
+ border-top: 0.1rem solid ${colors.palette.pale};
+ width: 100%;
+ height: 5.6rem;
+ display: flex;
+ align-items: center;
+ background-color: ${colors.palette.neutralBright};
+ padding-left: 1.5rem;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: 20;
+
+ @media(min-width: 56em) {
+ padding: 0 calc(50vw - 43rem);
+ }
+
+ button {
+ height: 3rem;
+ background-color: ${colors.palette.white};
+ border: 1px solid ${colors.palette.pale};
+ box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
+ width: 9rem;
+ border-radius: 0.5rem;
+ }
+`;
+
+const newTabIcon = ;
+
+export const useHelpMenu = (actions: {label: string; callback: () => void}[] = []) => {
+ const [showIframe, setShowIframe] = React.useState();
+
+ function HelpMenu({contactFormParams}: {contactFormParams: { key: string; value: string }[]}) {
+ const contactFormUrl = React.useMemo(() => {
+ const formUrl = 'https://openstax.org/embedded/contact';
+ const params = contactFormParams
+ .map(({key, value}) => encodeURIComponent(`${key}=${value}`))
+ .map((p) => `body=${p}`)
+ .join('&');
+
+ return `${formUrl}?${params}`;
+ }, [contactFormParams]);
+
+ return (
+ <>
+
+ {actions.map((action, i) =>
+ {action.label}
+
+ )}
+ setShowIframe(contactFormUrl)}
+ >
+ Report an issue
+
+
+ Support center
+ {newTabIcon}
+
+
+ Accessibility statement
+ {newTabIcon}
+
+ {
+ window.parent.postMessage({type: 'TriggerConsentModalMessage'}, '*');
+ }}
+ >
+ Cookie settings
+
+
+ >
+ );
+ }
+
+ function ContactFormIframe() {
+ React.useEffect(
+ () => {
+ const closeIt = ({data}: MessageEvent) => {
+ if (data === 'CONTACT_FORM_SUBMITTED') {
+ setShowIframe(undefined);
+ }
+ };
+
+ window.addEventListener('message', closeIt, false);
+
+ return () => window.removeEventListener('message', closeIt, false);
+ },
+ []
+ );
+
+ if (!showIframe) {
+ return null;
+ }
+
+ return (
+
+
+ setShowIframe(undefined)} />
+
+ );
+ }
+
+ return [HelpMenu, ContactFormIframe] as const;
+};
diff --git a/src/components/SidebarNav.stories.tsx b/src/components/SidebarNav.stories.tsx
index ae2b3a74d..ce4a367b4 100644
--- a/src/components/SidebarNav.stories.tsx
+++ b/src/components/SidebarNav.stories.tsx
@@ -53,8 +53,9 @@ const BodyPortalGlobalStyle = createGlobalStyle`
}
}
- .ladle-background, #ladle-root {
- display: none;
+ #ladle-root {
+ position: absolute;
+ right: 0;
}
`;
diff --git a/src/components/__snapshots__/HelpMenu.spec.tsx.snap b/src/components/__snapshots__/HelpMenu.spec.tsx.snap
new file mode 100644
index 000000000..6ed363de4
--- /dev/null
+++ b/src/components/__snapshots__/HelpMenu.spec.tsx.snap
@@ -0,0 +1,72 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`useHelpMenu matches snapshot 1`] = `
+
+
+
+
+
+`;