Skip to content

Commit

Permalink
feat: Modal component (#1622)
Browse files Browse the repository at this point in the history
* Modal base component, minimum Storybook examples identified, plus test suite

* Add example content to use in Storybook examples

* Add ModalDescription folder and files, passing one test

* Update ModalDeescription to include USWDS class, update tests

* Add folder and files for ModalHeading

* Skeleton for ModalHeading, passing one test

* Flesh out default Modal Storybook example

* Export ModalFooter, ModalDescription from index.ts

* Add ModalFooter component

* Remove ModalCloseButton from Storybook example with a forced action

* Add data-close-modal attribute to close button, comment for unique ID in parent Modal component

* Add skeleton for ModalWrapper component, passing one test

* Rendering placeholder Storybook example for ModalWrapper

* Add isVisible prop to ModalWrapper

* Modal storybook update

* 2.11 update

* Update thumbs down icon

* Simplify modal components

* Update tests, remove unused files

* Move Modal to ModalWindow component

* Setting up modal functionality

* Modal tests

* Working on modal effects and tests

* Hide other elements test, add modalRoot prop

* Style body padding when modal opens

* Add complete Modal stories

* Hook up click on overlay

* Adding focus, event handlers, tests

* Revert button forward ref change

* Fix toggle event propagation, cleanup

* Change Modal API to use forwardRef

* Replace closeModal, openModal with toggleModal

* Update modal exports

* Add ModalToggleButton, ModalOpenLink components

* Update ModalOpenLink test

* Fix custom link implementation

* Add open modal stories

* Fix Storybook action lag by replacing close handler with noop

Co-authored-by: Brandon Lenz <brandonalenz@gmail.com>

Co-authored-by: Arianna Kellogg <ari@truss.works>
Co-authored-by: Brandon Lenz <brandonalenz@gmail.com>
  • Loading branch information
3 people authored Oct 7, 2021
1 parent 6f87276 commit 2b76a7a
Show file tree
Hide file tree
Showing 25 changed files with 2,098 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-react-hooks": "^4.0.0",
"eslint-plugin-security": "^1.4.0",
"focus-trap-react": "^8.8.1",
"happo-plugin-storybook": "^2.7.0",
"happo.io": "^6.0.0",
"husky": "^4.3.8",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import classnames from 'classnames'
import { deprecationWarning } from '../../deprecation'

interface ButtonProps {
export interface ButtonProps {
type: 'button' | 'submit' | 'reset'
children: React.ReactNode
secondary?: boolean
Expand Down
2 changes: 1 addition & 1 deletion src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function Link<FCProps = DefaultLinkProps>(
// 3. Therefore we know that removing those props leaves us
// with FCProps
//
const linkProps: FCProps = (remainingProps as unknown) as FCProps
const linkProps: FCProps = remainingProps as unknown as FCProps
const classes = linkClasses(variant, className)
return React.createElement(
asCustom,
Expand Down
213 changes: 213 additions & 0 deletions src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import React, { useRef } from 'react'

import { Modal, ModalRef } from './Modal'
import { ModalHeading } from './ModalHeading/ModalHeading'
import { ModalFooter } from './ModalFooter/ModalFooter'
import { ModalToggleButton } from './ModalToggleButton'

import { ButtonGroup } from '../ButtonGroup/ButtonGroup'

export default {
title: 'Components/Modal',
component: Modal,
parameters: {
docs: {
description: {
component: `
### USWDS 2.0 Modal component
Source: http://designsystem.digital.gov/components/modal
To use this component, you will need to create a ref and pass it into the rendered Modal component. This ref will expose several properties (also described by the exported ModalRef type):
- modalId: string
- the value of the id attribute given to the modal
- modalIsOpen: boolean
- true if the modal is currently open, otherwise false
- toggleModal: (e?: React.MouseEvent, open?: boolean) => boolean
- use this function to open or close the modal.
- if attached to an event handler, pass the event in
- if the second argument is provided, it will explicitly open the modal if true, or explicitly close it if false
- returns true if the toggle operation was successful, false if the event was prevented.
Follow the [USWDS](https://designsystem.digital.gov/components/modal/) guidance for using modals:
- Pass a unique ID into each modal component.
- Any component that opens the modal should be a button or anchor element, have the [data-open-modal] attribute, and [aria-controls] attribute with the value of the modal ID.
- Any component that closes the modal should be a button element, and have the [data-close-modal] attribute, and [aria-controls] attribute with the value of the modal ID.
- Use the forceAction prop on the modal component if the user should be forced to take an action before closing the modal.
You can also use the provided ModalToggleButton and/or ModalOpenLink components, which will adhere to the above guidelines for convenience.
`,
},
},
},
}

export const defaultModal = (): React.ReactElement => {
const modalRef = useRef<ModalRef>()

return (
<>
<ModalToggleButton modalRef={modalRef} opener>
Open default modal
</ModalToggleButton>
<Modal
ref={modalRef}
id="example-modal-1"
aria-labelledby="modal-1-heading"
aria-describedby="modal-1-description">
<ModalHeading id="modal-1-heading">
Are you sure you want to continue?
</ModalHeading>
<div className="usa-prose">
<p id="modal-1-description">
You have unsaved changes that will be lost.
</p>
</div>
<ModalFooter>
<ButtonGroup>
<ModalToggleButton modalRef={modalRef} closer>
Continue without saving
</ModalToggleButton>
<ModalToggleButton
modalRef={modalRef}
closer
unstyled
className="padding-105 text-center">
Go back
</ModalToggleButton>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
)
}

export const largeModal = (): React.ReactElement => {
const modalRef = useRef<ModalRef>()

return (
<>
<ModalToggleButton modalRef={modalRef} opener>
Open large modal
</ModalToggleButton>
<Modal
ref={modalRef}
isLarge
aria-labelledby="modal-2-heading"
aria-describedby="modal-2-description"
id="example-modal-2">
<ModalHeading id="modal-2-heading">
Are you sure you want to continue?
</ModalHeading>
<div className="usa-prose">
<p id="modal-2-description">
You have unsaved changes that will be lost.
</p>
</div>
<ModalFooter>
<ButtonGroup>
<ModalToggleButton modalRef={modalRef} closer>
Continue without saving
</ModalToggleButton>
<ModalToggleButton
modalRef={modalRef}
closer
unstyled
className="padding-105 text-center">
Go back
</ModalToggleButton>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
)
}

export const forceActionModal = (): React.ReactElement => {
const modalRef = useRef<ModalRef>()

return (
<>
<ModalToggleButton modalRef={modalRef} opener>
Open modal with forced action
</ModalToggleButton>
<Modal
ref={modalRef}
forceAction
aria-labelledby="modal-3-heading"
aria-describedby="modal-3-description"
id="example-modal-3">
<ModalHeading id="modal-3-heading">
Your session will end soon.
</ModalHeading>
<div className="usa-prose">
<p id="modal-3-description">
You’ve been inactive for too long. Please choose to stay signed in
or sign out. Otherwise, you’ll be signed out automatically in 5
minutes.
</p>
</div>
<ModalFooter>
<ButtonGroup>
<ModalToggleButton modalRef={modalRef} closer>
Yes, stay signed in
</ModalToggleButton>
<ModalToggleButton
modalRef={modalRef}
closer
unstyled
className="padding-105 text-center">
Sign out
</ModalToggleButton>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
)
}

export const customFocusElementModal = (): React.ReactElement => {
const modalRef = useRef<ModalRef>()

return (
<>
<ModalToggleButton modalRef={modalRef} opener>
Open modal with custom initial focus element
</ModalToggleButton>
<Modal
ref={modalRef}
id="example-modal-1"
aria-labelledby="modal-1-heading"
aria-describedby="modal-1-description">
<ModalHeading id="modal-1-heading">
Are you sure you want to continue?
</ModalHeading>
<div className="usa-prose">
<p id="modal-1-description">
You have unsaved changes that will be lost.
</p>
<button type="button">Decoy button</button>
<button type="button" data-focus="true">
Focus me first
</button>
</div>
<ModalFooter>
<ButtonGroup>
<ModalToggleButton modalRef={modalRef} closer>
Continue without saving
</ModalToggleButton>
<ModalToggleButton
modalRef={modalRef}
closer
unstyled
className="padding-105 text-center">
Go back
</ModalToggleButton>
</ButtonGroup>
</ModalFooter>
</Modal>
</>
)
}
Loading

0 comments on commit 2b76a7a

Please sign in to comment.