Skip to content

Commit

Permalink
First time instructions (#130)
Browse files Browse the repository at this point in the history
* fix text color of share button

* add responsive design breakpoints to main header

* add text-black class to minimap and layout control center buttons

* minor styling adjustments for accessibility

* add show guide button header, guide.html, and show the guide on the visitors first use

* remove animated pulse for current decision

* mock local storage to indicate whether it is a user's first time
  • Loading branch information
dpgraham4401 authored May 15, 2024
1 parent 42ea6e4 commit 4e9d349
Show file tree
Hide file tree
Showing 19 changed files with 167 additions and 40 deletions.
42 changes: 42 additions & 0 deletions public/help/guide.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<h2 class="text-2xl font-semibold text-black">Getting Started</h2>
<div class="p-2 flex flex-col space-y-4">
<p class="text-left text-gray-900">Welcome to <b class="font-bold">The
Manifest Game</b>, an interactive decision tree to help you
use the U.S. EPA's hazardous waste tracking system known as
<a
class="text-blue-700 underline hover:text-blue-900"
href="https://epa.gov/e-manifest">e-Manifest</a>.
</p>
<p class="text-gray-900">
Start by answering the "Yes" or "No" questions in the boxes. As you answer
questions, new questions will appear based on your previous answers.
</p>
<p class="text-gray-900">
If you are unsure about a question, click the question mark in the question
box to learn more.
</p>
<h3 class="text-xl font-bold text-black">Navigate</h3>
<p class="text-gray-900">
You can move around the page by clicking and dragging anywhere on the page.
You can also zoom in and out by using the scroll wheel on your mouse or
pinching on a touch screen.
</p>
<h4 class="text-lg font-bold text-black">Mini Map</h4>
<p class="text-gray-900">
The mini map in the bottom right corner shows you where you are on the
decision tree. You can click the mini map to quickly navigate to
different parts of the decision tree, and scroll to zoom in and out.
</p>
<h4 class="text-lg font-bold text-black">Change the Layout</h4>
<p class="text-gray-900">
You can change the layout of the decision tree from horizontal to vertical
by clicking the "Layout" button to help you visualize the decision tree in
different ways.
</p>
<h3 class="text-xl font-bold text-black">Share Your Decisions</h3>
<p class="text-gray-900">
Once you have completed the decision tree, you can share your decisions with
others by clicking the "Share" button. This will generate a unique URL
you can share with others.
</p>
</div>
3 changes: 2 additions & 1 deletion src/App.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { setupServer } from 'msw/node';
import React from 'react';
import { ReactFlowProvider } from 'reactflow';
import useTreeStore from 'store';
import { renderWithProviders } from 'test-utils';
import { notFirstTimeMock, renderWithProviders } from 'test-utils';
import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest';

const TestComponent = () => {
Expand Down Expand Up @@ -48,6 +48,7 @@ afterAll(() => {
});

describe('App', () => {
notFirstTimeMock();
test('shows a spinner while waiting for config', () => {
server.use(
http.get('/default.json', async () => {
Expand Down
4 changes: 1 addition & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ export default function App() {
) : configError ? (
<ErrorMsg message={'Error parsing the Decision Tree'} />
) : (
<>
<Tree nodes={nodes} edges={edges} />
</>
<Tree nodes={nodes} edges={edges} />
)}
<OffCanvas isOpen={helpIsOpen} onClose={hideHelp} />
</>
Expand Down
20 changes: 18 additions & 2 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { HelpIcon } from 'components/Help/HelpIcon/HelpIcon';
import { useHelp } from 'hooks';
import React, { MouseEventHandler } from 'react';
import { Panel } from 'reactflow';

interface HeaderProps {
Expand All @@ -6,12 +9,25 @@ interface HeaderProps {

export const Header = ({ treeTitle }: HeaderProps) => {
const issueUrl = import.meta.env.VITE_ISSUE_URL;
const { showHelp } = useHelp();

const showInstructions: MouseEventHandler = (event) => {
try {
showHelp('guide.html');
event.stopPropagation();
} catch (error) {
console.error(error);
}
};

return (
<Panel position="top-left" className="w-3/12">
<Panel position="top-left" className=" sm:w-5/12 md:w-4/12 lg:w-3/12">
<div className="box-border w-full rounded-xl bg-gradient-to-b from-sky-700 to-sky-900 p-2 align-middle">
<div>
<div className="flex min-w-60 justify-between">
<h1 className="text-xl font-semibold text-white">{treeTitle}</h1>
<div className="absolute right-3 top-3">
<HelpIcon onClick={showInstructions} size={20} />
</div>
</div>
{issueUrl && (
<div>
Expand Down
2 changes: 2 additions & 0 deletions src/components/Help/Help.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Help } from 'components/Help/Help';
import { delay, http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import useTreeStore from 'store';
import { notFirstTimeMock } from 'test-utils';
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';

const handlers = [
Expand Down Expand Up @@ -37,6 +38,7 @@ beforeAll(() => server.listen());
afterAll(() => server.close());

describe('Help', () => {
notFirstTimeMock();
test('renders error message when help content ID is undefined', () => {
render(<Help />);
expect(screen.getByText(/problem/i)).toBeInTheDocument();
Expand Down
1 change: 0 additions & 1 deletion src/components/Help/Help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const Help = () => {

return (
<>
<h2 className="text-xl font-semibold text-black">More Information</h2>
{help?.type === 'text' && <TextualHelp content={help.content} />}
{help?.type === 'html' && <HtmlHelp html={help.content} />}
</>
Expand Down
5 changes: 3 additions & 2 deletions src/components/Help/HelpIcon/HelpIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { FaQuestionCircle } from 'react-icons/fa';

interface HelpIconProps {
onClick?: React.MouseEventHandler;
size?: number;
}

/**
* icon to help users make decisions or direct them to more information
* @constructor
*/
export const HelpIcon = ({ onClick }: HelpIconProps) => {
export const HelpIcon = ({ onClick, size = 30 }: HelpIconProps) => {
return (
<div>
<button
Expand All @@ -18,7 +19,7 @@ export const HelpIcon = ({ onClick }: HelpIconProps) => {
className="rounded-full border-2 border-transparent bg-transparent focus:outline-none focus:ring focus:ring-white"
>
<FaQuestionCircle
size={30}
size={size}
className="rounded-full text-slate-50 transition-all duration-200 ease-in-out hover:text-slate-400"
/>
</button>
Expand Down
14 changes: 11 additions & 3 deletions src/components/OffCanvas/OffCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Help } from 'components/Help/Help';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { FaX } from 'react-icons/fa6';

interface OffCanvasProps {
Expand All @@ -13,6 +13,8 @@ interface OffCanvasProps {
*/
export const OffCanvas = ({ isOpen, onClose }: OffCanvasProps) => {
/** handle when user clicks outside the off canvas component*/
const contentRef = useRef<HTMLDivElement | null>(null);

const onClickOutside = useCallback(() => {
if (isOpen) {
if (onClose) onClose();
Expand Down Expand Up @@ -41,6 +43,12 @@ export const OffCanvas = ({ isOpen, onClose }: OffCanvasProps) => {
return () => document.removeEventListener('click', onClickOutside);
}, [onClickOutside]);

useEffect(() => {
if (isOpen) {
contentRef.current?.focus();
}
}, [contentRef, isOpen]);

return (
<>
<div
Expand Down Expand Up @@ -70,13 +78,13 @@ export const OffCanvas = ({ isOpen, onClose }: OffCanvasProps) => {
<FaX size={20} />
</button>
</div>
<div className="offcanvas-scrollbar max-h-full overflow-x-hidden px-6 hover:overflow-y-scroll">
<div className="max-h-full overflow-x-hidden overflow-y-scroll px-6" ref={contentRef}>
<Help />
</div>
</div>
{/* backdrop while open*/}
<div
className={`fixed bottom-0 left-0 right-0 top-0 bg-black transition-opacity duration-200 ease-in-out ${isOpen ? 'visible opacity-60' : 'invisible opacity-0'}`}
className={`fixed bottom-0 left-0 right-0 top-0 bg-black transition-opacity duration-200 ease-in-out ${isOpen ? 'visible opacity-60' : 'invisible opacity-0'}`}
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const LayoutBtn = ({ isHorizontal, toggleDirection }: LayoutBtnProps) =>
<ControlButton
aria-label={`switch to ${isHorizontal ? 'vertical' : 'horizontal'} layout`}
onClick={toggleDirection}
className="text-black"
>
{isHorizontal ? <LuMoveVertical color={'000'} /> : <LuMoveHorizontal color={'000'} />}
</ControlButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ interface MiniMapBtnProps {
/** LayoutBtn toggles the layout direction of the tree.*/
export const MiniMapBtn = ({ visible, onClick }: MiniMapBtnProps) => {
return (
<ControlButton aria-label={`${visible ? 'hide minimap' : 'show minimap'}`} onClick={onClick}>
<ControlButton
aria-label={`${visible ? 'hide minimap' : 'show minimap'}`}
onClick={onClick}
className="text-black"
>
{visible ? <LuMapPinOff color={'000'} /> : <LuMapPin color={'000'} />}
</ControlButton>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export const ShareBtn = () => {
const { copyTreeUrlToClipboard } = useUrl();

return (
<ControlButton aria-label="share diagram" onClick={copyTreeUrlToClipboard}>
<ControlButton
aria-label="share diagram"
className="text-black"
onClick={copyTreeUrlToClipboard}
>
<LuShare className="font-bold" />
</ControlButton>
);
Expand Down
5 changes: 2 additions & 3 deletions src/components/Tree/Nodes/BoolNode/BoolNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const BoolNode = ({
}: NodeProps<BoolNodeData>) => {
const { showHelp } = useHelp();
const { retractDecision, makeDecision } = useDecisionTree();
const { decisionIsInPath, decision, isCurrentDecision } = useDecisions(id);
const { decisionIsInPath, decision } = useDecisions(id);

const handleHelpClick: MouseEventHandler = (event) => {
try {
Expand All @@ -44,8 +44,7 @@ export const BoolNode = ({
data-testid={`bool-node-${id}-content`}
className={`flex min-w-80 flex-col items-center justify-center rounded-xl
p-6 text-xl text-white
${decisionIsInPath(id) ? 'bg-gradient-to-b from-teal-700 to-teal-800' : 'bg-gradient-to-b from-sky-700 to-sky-900'}
${isCurrentDecision ? 'animate-pulse' : ''}`}
${decisionIsInPath(id) ? 'bg-gradient-to-b from-teal-700 to-teal-800' : 'bg-gradient-to-b from-sky-700 to-sky-900'}`}
>
{help && (
<div className="absolute right-3 top-3">
Expand Down
4 changes: 1 addition & 3 deletions src/components/Tree/Nodes/DefaultNode/DefaultNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ export const DefaultNode = ({ data, ...props }: NodeProps<VertexData>) => {
? 'bg-teal-700'
: 'bg-gradient-to-b from-sky-700 to-sky-900';

const nodeFocusedClasses = isCurrentDecision ? 'animate-pulse' : '';

return (
<BaseNode {...props}>
<div
data-testid={`default-node-${props.id}-content`}
className={`flex min-w-full justify-center rounded-xl p-6 text-xl text-white ${nodeBackgroundColor} ${nodeFocusedClasses}`}
className={`flex min-w-full justify-center rounded-xl p-6 text-xl text-white ${nodeBackgroundColor}`}
>
{data.help && (
<div className="absolute right-3 top-3">
Expand Down
7 changes: 4 additions & 3 deletions src/components/Tree/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,18 @@ export const Tree = ({ nodes, edges }: TreeProps) => {
onNodesChange={onNodesChange}
fitView
edgesFocusable={false}
fitViewOptions={{ padding: 5, minZoom: 0, maxZoom: 5 }}
fitViewOptions={{ padding: 5, minZoom: 0, maxZoom: 2 }}
proOptions={{ hideAttribution: true }}
>
{mapVisible && (
<MiniMap
nodeStrokeWidth={3}
ariaLabel="Mini Map"
offsetScale={50}
data-testid="tree-mini-map"
nodeColor="#3E6D9BAA"
zoomable={true}
onClick={(_event: React.MouseEvent, position: XYPosition) =>
setCenter(position.x, position.y, { zoom: zoom, duration: 1.5 })
setCenter(position.x, position.y, { zoom: zoom })
}
/>
)}
Expand Down
18 changes: 16 additions & 2 deletions src/hooks/useHelp/useHelp.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import '@testing-library/jest-dom';
import { renderHook } from '@testing-library/react';
import { useHelp } from 'hooks/useHelp/useHelp';
import { describe, expect, test } from 'vitest';
import { afterEach, describe, expect, test, vi } from 'vitest';

describe('useHelp hook', () => {
const getItemSpy = vi.spyOn(Storage.prototype, 'getItem');
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');

afterEach(() => {
localStorage.clear();
getItemSpy.mockClear();
setItemSpy.mockClear();
});

test('helpIsOpen is true on a user first visit', () => {
const { result } = renderHook(() => useHelp());
expect(result.current.helpIsOpen).toBeTruthy();
});
test('helpIsOpen is initially false', () => {
localStorage.setItem('tmg-first-time', 'true');
const { result } = renderHook(() => useHelp());
expect(result.current.helpIsOpen).toBe(false);
expect(result.current.helpIsOpen).toBeFalsy();
});
test('showHelp returns if arg is undefined', () => {
const { result } = renderHook(() => useHelp());
Expand Down
27 changes: 23 additions & 4 deletions src/hooks/useHelp/useHelp.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useCallback, useEffect, useState } from 'react';
import useTreeStore from 'store';

export interface UseHelpReturn {
helpIsOpen: boolean;
contentFilename: string | undefined;
showHelp: (contentId: string | undefined) => void;
hideHelp: () => void;
showInstructions: () => void;
}

/**
Expand All @@ -17,16 +19,33 @@ export const useHelp = () => {
hideHelp,
showHelp: storeShowHelp,
} = useTreeStore((state) => state);
const [firstTime, setFirstTime] = useState(window.localStorage.getItem('tmg-first-time'));

const showHelp = (contentId: string | undefined) => {
if (!contentId) throw new Error('contentId is required');
storeShowHelp(contentId);
};
const showHelp = useCallback(
(contentId: string | undefined) => {
if (!contentId) throw new Error('contentId is required');
storeShowHelp(contentId);
},
[storeShowHelp]
);

const showInstructions = useCallback(() => {
showHelp('guide.html');
}, [showHelp]);

useEffect(() => {
if (!firstTime) {
showInstructions();
}
setFirstTime('false');
window.localStorage.setItem('tmg-first-time', 'false');
}, [firstTime, showInstructions]);

return {
helpIsOpen: helpIsOpen,
contentFilename,
showHelp,
hideHelp,
showInstructions,
} as UseHelpReturn;
};
11 changes: 3 additions & 8 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,20 @@ body,
.react-flow__controls button {
border-radius: 10px;
border: 2px solid #fff;
margin-top: 5px;
margin-top: 2px;
background: #fefefe !important;
}

.react-flow__node {
border-radius: 15px;
border: 2px solid #333333;
border: 3px solid #333333;
}

.react-flow__node:focus {
border: white 2px solid;
border: white 3px solid;
border-radius: 15px;
}

.offcanvas-scrollbar {
scrollbar-color: #333333 transparent;
}


@layer components {
.z-top {
z-index: 1050;
Expand Down
Loading

0 comments on commit 4e9d349

Please sign in to comment.