Skip to content

Commit

Permalink
Merge branch 'main' into B-18411-UI-shipment-address-update-review-sc…
Browse files Browse the repository at this point in the history
…reen-updates-v2
  • Loading branch information
danieljordan-caci authored Feb 8, 2024
2 parents c4b175e + 29a1d7b commit 244ebab
Show file tree
Hide file tree
Showing 24 changed files with 2,169 additions and 1 deletion.
80 changes: 80 additions & 0 deletions src/components/ButtonDropdownMenu/ButtonDropdownMenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useRef, useState } from 'react';
import { Button } from '@trussworks/react-uswds';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classnames from 'classnames';

import styles from './ButtonDropdownMenu.module.scss';

function ButtonDropdownMenu({ title, items, multiSelect = false, divClassName, onItemClick, outline }) {
const [open, setOpen] = useState(false);
const [selection, setSelection] = useState([]);
const toggle = () => setOpen(!open);
const dropdownRef = useRef(null);

const handleOutsideClick = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
// Clicked outside the dropdown container, close the dropdown
setOpen(false);
}
};

const handleButtonClick = () => {
toggle(!open);
document.addEventListener('mousedown', handleOutsideClick);
};

function handleOnClick(item) {
if (!selection.some((current) => current.id === item.id)) {
if (!multiSelect) {
setSelection([item]);
} else if (multiSelect) {
setSelection([...selection, item]);
}
} else {
let selectionAfterRemoval = selection;
selectionAfterRemoval = selectionAfterRemoval.filter((current) => current.id !== item.id);
setSelection([...selectionAfterRemoval]);
}

// Call the onItemClick callback with the selected item
if (onItemClick) {
onItemClick(item);
toggle(!open);
}
}

return (
<div className={classnames(styles.dropdownWrapper, divClassName)}>
<div className={styles.dropdownContainer} ref={dropdownRef}>
{outline ? (
<Button className={styles.btn} onClick={() => handleButtonClick()} secondary outline>
<span>{title}</span>
<div>
<FontAwesomeIcon icon="download" />
</div>
</Button>
) : (
<Button className={styles.btn} onClick={() => handleButtonClick()}>
<span>{title}</span>
<div>
<FontAwesomeIcon icon="download" />
</div>
</Button>
)}
{open && (
<ul className={styles.dropdownList}>
{items.map((item) => (
<li key={item.id} data-testid={`${item.id}-dropdown-item`}>
<button type="button" onClick={() => handleOnClick(item)}>
<span>{item.value}</span>
</button>
</li>
))}
</ul>
)}
</div>
</div>
);
}

export default ButtonDropdownMenu;
68 changes: 68 additions & 0 deletions src/components/ButtonDropdownMenu/ButtonDropdownMenu.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@import 'shared/styles/basics';
@import 'shared/styles/mixins';
@import 'shared/styles/colors';
@import 'shared/styles/_variables';

.dropdownWrapper {
display: flex;
min-height: 38px;
flex-wrap: wrap;

.btn {
display: flex;
justify-content: space-between;
align-items: center;
span {
@include u-margin-right(.5em)
}
}

.dropdownContainer {
position: relative;
}

.dropdownList {
position: absolute; /* Set absolute positioning for the dropdown list */
top: 100%; /* Position it below the button */
left: 0;
padding: 0;
margin: 0;
width: 100%;
z-index: 1; /* Ensure it appears above other elements */

li {
list-style-type: none;

&:first-of-type {
> button {
border-top: 1px solid #ccc;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
}

&:last-of-type > button {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}

button {
display: flex;
justify-content: space-between;
background-color: white;
font-size: 16px;
padding: 15px 20px 15px 20px;
border: 0;
border-bottom: 1px solid #ccc;
width: 100%;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;

&:hover, &:focus {
font-weight: bold;
background-color: #ccc;
}
}
}
}
}
21 changes: 21 additions & 0 deletions src/components/ButtonDropdownMenu/ButtonDropdownMenu.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

import ButtonDropdownMenu from './ButtonDropdownMenu';

export default {
title: 'Components/ButtonDropdownMenu',
component: ButtonDropdownMenu,
};

const dropdownMenuItems = [
{
id: 1,
value: 'PCS Orders',
},
{
id: 2,
value: 'PPM Packet',
},
];

export const defaultDropdown = () => <ButtonDropdownMenu title="Download" items={dropdownMenuItems} outline />;
29 changes: 29 additions & 0 deletions src/components/ButtonDropdownMenu/ButtonDropdownMenu.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect'; // For expect assertions

import ButtonDropdownMenu from './ButtonDropdownMenu';

describe('ButtonDropdownMenu', () => {
const items = [
{ id: 1, value: 'Item 1' },
{ id: 2, value: 'Item 2' },
];

it('calls onItemClick callback when an item is clicked', () => {
const onItemClickMock = jest.fn();
const { getByText } = render(<ButtonDropdownMenu title="Test" items={items} onItemClick={onItemClickMock} />);

// Open the dropdown
fireEvent.click(getByText('Test'));

// Click on the first item
fireEvent.click(getByText('Item 1'));

// Ensure that onItemClick is called with the correct item
expect(onItemClickMock).toHaveBeenCalledWith(items[0]);

// Close the dropdown (optional)
fireEvent.click(getByText('Test'));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const ShipmentContainer = ({ id, className, children, shipmentType }) => {
const containerClasses = classNames(
styles.shipmentContainer,
{
'container--accent--default': shipmentType === null,
'container--accent--default':
shipmentType === null ||
shipmentType === SHIPMENT_OPTIONS.BOAT_HAUL_AWAY ||
shipmentType === SHIPMENT_OPTIONS.BOAT_TOW_AWAY ||
shipmentType === SHIPMENT_OPTIONS.MOTOR_HOME ||
!Object.values(SHIPMENT_OPTIONS).includes(shipmentType),
'container--accent--hhg': shipmentType === SHIPMENT_OPTIONS.HHG,
'container--accent--nts': shipmentType === SHIPMENT_OPTIONS.NTS,
'container--accent--ntsr': shipmentType === SHIPMENT_OPTIONS.NTSR,
Expand Down
1 change: 1 addition & 0 deletions src/constants/routes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const generalRoutes = {
HOME_PATH: '/',
SIGN_IN_PATH: '/sign-in',
MULTI_MOVES_LANDING_PAGE: '/moves',
PRIVACY_SECURITY_POLICY_PATH: '/privacy-and-security-policy',
ACCESSIBILITY_PATH: '/accessibility',
};
Expand Down
152 changes: 152 additions & 0 deletions src/pages/MyMove/Multi-Moves/MultiMovesLandingPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React, { useEffect, useState } from 'react';
import { Button } from '@trussworks/react-uswds';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

import styles from './MultiMovesLandingPage.module.scss';
import MultiMovesMoveHeader from './MultiMovesMoveHeader/MultiMovesMoveHeader';
import MultiMovesMoveContainer from './MultiMovesMoveContainer/MultiMovesMoveContainer';
import {
mockMovesPCS,
mockMovesSeparation,
mockMovesRetirement,
mockMovesNoPreviousMoves,
mockMovesNoCurrentMoveWithPreviousMoves,
mockMovesNoCurrentOrPreviousMoves,
} from './MultiMovesTestData';

import { detectFlags } from 'utils/featureFlags';
import { generatePageTitle } from 'hooks/custom';
import { milmoveLogger } from 'utils/milmoveLog';
import retryPageLoading from 'utils/retryPageLoading';
import { loadInternalSchema } from 'shared/Swagger/ducks';
import { loadUser } from 'store/auth/actions';
import { initOnboarding } from 'store/onboarding/actions';
import Helper from 'components/Customer/Home/Helper';

const MultiMovesLandingPage = () => {
const [setErrorState] = useState({ hasError: false, error: undefined, info: undefined });
// ! This is just used for testing and viewing different variations of data that MilMove will use
// user can add params of ?moveData=PCS, etc to view different views
let moves;
const currentUrl = new URL(window.location.href);
const moveDataSource = currentUrl.searchParams.get('moveData');
switch (moveDataSource) {
case 'PCS':
moves = mockMovesPCS;
break;
case 'retirement':
moves = mockMovesRetirement;
break;
case 'separation':
moves = mockMovesSeparation;
break;
case 'noPreviousMoves':
moves = mockMovesNoPreviousMoves;
break;
case 'noCurrentMove':
moves = mockMovesNoCurrentMoveWithPreviousMoves;
break;
case 'noMoves':
moves = mockMovesNoCurrentOrPreviousMoves;
break;
default:
moves = mockMovesPCS;
break;
}
// ! end of test data
useEffect(() => {
const fetchData = async () => {
try {
loadInternalSchema();
loadUser();
initOnboarding();
document.title = generatePageTitle('MilMove');

const script = document.createElement('script');
script.src = '//rum-static.pingdom.net/pa-6567b05deff3250012000426.js';
script.async = true;
document.body.appendChild(script);
} catch (error) {
const { message } = error;
milmoveLogger.error({ message, info: null });
setErrorState({
hasError: true,
error,
info: null,
});
retryPageLoading(error);
}
};

fetchData();
}, [setErrorState]);

const flags = detectFlags(process.env.NODE_ENV, window.location.host, window.location.search);

// ! WILL ONLY SHOW IF MULTIMOVE FLAG IS TRUE
return flags.multiMove ? (
<div>
<div className={styles.homeContainer}>
<header data-testid="customerHeader" className={styles.customerHeader}>
<div className={`usa-prose grid-container ${styles['grid-container']}`}>
<h2>First Last</h2>
</div>
</header>
<div className={`usa-prose grid-container ${styles['grid-container']}`}>
<Helper title="Welcome to MilMove!" className={styles['helper-paragraph-only']}>
<p>
We can put information at the top here - potentially important contact info or basic instructions on how
to start a move?
</p>
</Helper>
<div className={styles.centeredContainer}>
<Button className={styles.createMoveBtn}>
<span>Create a Move</span>
<div>
<FontAwesomeIcon icon="plus" />
</div>
</Button>
</div>
<div className={styles.movesContainer}>
{moves.currentMove.length > 0 ? (
<>
<div data-testid="currentMoveHeader">
<MultiMovesMoveHeader title="Current Move" />
</div>
<div data-testid="currentMoveContainer">
<MultiMovesMoveContainer moves={moves.currentMove} />
</div>
</>
) : (
<>
<div data-testid="currentMoveHeader">
<MultiMovesMoveHeader title="Current Moves" />
</div>
<div>You do not have a current move.</div>
</>
)}
{moves.previousMoves.length > 0 ? (
<>
<div data-testid="prevMovesHeader">
<MultiMovesMoveHeader title="Previous Moves" />
</div>
<div data-testid="prevMovesContainer">
<MultiMovesMoveContainer moves={moves.previousMoves} />
</div>
</>
) : (
<>
<div data-testid="prevMovesHeader">
<MultiMovesMoveHeader title="Previous Moves" />
</div>
<div>You have no previous moves.</div>
</>
)}
</div>
</div>
</div>
</div>
) : null;
};

export default MultiMovesLandingPage;
Loading

0 comments on commit 244ebab

Please sign in to comment.