-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into B-18411-UI-shipment-address-update-review-sc…
…reen-updates-v2
- Loading branch information
Showing
24 changed files
with
2,169 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
68
src/components/ButtonDropdownMenu/ButtonDropdownMenu.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
21
src/components/ButtonDropdownMenu/ButtonDropdownMenu.stories.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
29
src/components/ButtonDropdownMenu/ButtonDropdownMenu.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.