Skip to content

Commit

Permalink
feat: Add Modal component (#309)
Browse files Browse the repository at this point in the history
* feat: Add Modal component

* cleaned up css class names

* add logic to auto focus and focus trapping in the modal

* Add example of Modal usage

* update unit test by adding snapshot to verify the component renders what is expected in the UI

* fix: Modal focus trap works better with keydown listener, add focus trap tests, upgrade testing-library packages

* fix: use userEvent.type to simulate escape key press

* fixed failing test

Co-authored-by: Boima Konuwa <boima.konuwa@cision.com>
Co-authored-by: Chris Garcia <pixelbandito@gmail.com>
  • Loading branch information
3 people authored May 3, 2021
1 parent d6f4538 commit 144c622
Show file tree
Hide file tree
Showing 12 changed files with 812 additions and 262 deletions.
31 changes: 31 additions & 0 deletions example/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Input,
InputTime,
Typography,
Modal,
// IMPORT_INJECTOR
} from '@cision/rover-ui';

Expand All @@ -39,6 +40,7 @@ const App = () => {
const [tooltipOpen, setTooltipOpen] = useState(false);
const [inputValue, setInputValue] = useState('');
const [inputTimeValue, setInputTimeValue] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);

const toggleTooltip = function () {
setTooltipOpen((prev) => !prev);
Expand Down Expand Up @@ -405,6 +407,35 @@ const App = () => {
<Typography />
</Section>

<Section title="Modal">
<div>
<Button modifiers={['primary']} onClick={() => setIsModalOpen(true)}>
Show Modal
</Button>
</div>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
size="md"
>
<Modal.Header>
<h4 className="">Modal Header</h4>
</Modal.Header>
<Modal.Body>
<p>Modal Body</p>
<p>You can put all of your interesting content in the modal body</p>
<p>
Click outside the modal or use the escape key to close the modal
</p>
</Modal.Body>
<Modal.Footer>
<Button onClick={() => setIsModalOpen(false)} className="">
Close
</Button>
</Modal.Footer>
</Modal>
</Section>

{/** USAGE_INJECTOR */}
</div>
);
Expand Down
289 changes: 27 additions & 262 deletions example/yarn.lock

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions src/components/Modal/Modal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
.Modal {
z-index: var(--rvr-zindex-modal-backdrop);
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color:rgba(0, 0, 0, 0.3);
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.3s ease-in-out;
overflow: auto;
pointer-events: none;
}

.enterDone {
opacity: 1;
pointer-events: visible;
}

.exit {
opacity: 0;
}

.content {
display: flex;
flex-flow: column nowrap;
z-index: var(--rvr-zindex-modal);
background-color: var(--rvr-white);
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.5);
border-radius: 4px;
transition: all 0.3s ease-in-out;
overflow: auto;
transform: translateY(-200px);
margin: var(--rvr-space-bordered-lg);
max-height: 100%;
max-width: 100%;
}

.enterDone .content {
transform: translateY(0);
}

.exit .content {
transform: translateY(-200px);
}

.Header {
flex: 0 0 auto;
border-radius: 4px 4px 0px 0px;
}

.Header.level--primary {
background: var(--rvr-gray-10);
border-bottom: 1px solid var(--rvr-gray-20)
}

.Header.level--warning {
background: var(--rvr-yellow-lite-2);
border-bottom: 1px solid var(--rvr-yellow);
}

.Header.level--info {
background: var(--rvr-blue-lite-2);
border-bottom: 1px solid var(--rvr-blue);
}

.Header.level--danger {
background: var(--rvr-red-lite-2);
border-bottom: 1px solid var(--rvr-red);
}


.Footer {
flex: 0 0 auto;
border-top: 1px solid var(--rvr-gray-20);
background: var(--rvr-white);
}

.Header, .Footer {
padding: 12px;
}


.Body {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 10px;
}

.bodyHasOpenModal {
overflow: hidden;
}

.sm {
width: 340px;
}

.md {
width: 578px;
}

.lg {
width: 815px;
}

/*max-height: calc(var(--rvr-baseSize) * 40*/
@media (max-height: 320px) {
.Body {
flex: 0 0 auto;
}

.content {
flex: 0 0 auto;
max-height: none;
}

.Modal {
justify-content: flex-start;
}
}

81 changes: 81 additions & 0 deletions src/components/Modal/Modal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import userEvent from '@testing-library/user-event';
import Modal from './Modal';

const defaultProps = {
isOpen: true,
onClose: jest.fn(),
};

const renderModal = (props = defaultProps) =>
render(
<Modal {...props}>
<Modal.Header>
<p>Modal Header</p>
</Modal.Header>
<Modal.Body>
<p>Modal Body</p>
</Modal.Body>
<Modal.Footer>
<p>Modal Footer</p>
</Modal.Footer>
</Modal>
);
describe('Modal', () => {
it('renders correctly', () => {
const { baseElement } = renderModal();
expect(baseElement).toMatchSnapshot();
});

it('renders', () => {
render(<Modal isOpen data-testid="Modal-Test" />);
expect(screen.getByTestId('Modal-Test')).toBeInTheDocument();
});

it("does not render when 'isOpen' prop is false", () => {
render(<Modal isOpen={false} data-testid="Modal-Test" />);
expect(screen.queryByTestId('Modal-Test')).not.toBeInTheDocument();
});

describe('Modal CSS Classes', () => {
test.each`
size | level | sizeClass | levelClass
${'sm'} | ${'primary'} | ${'sm'} | ${'level--primary'}
${'md'} | ${'warning'} | ${'md'} | ${'level--warning'}
${'lg'} | ${undefined} | ${'lg'} | ${'level--primary'}
${undefined} | ${undefined} | ${'md'} | ${'level--primary'}
${'lg'} | ${'info'} | ${'lg'} | ${'level--info'}
${undefined} | ${'danger'} | ${'md'} | ${'level--danger'}
`(
'when size prop = $size and level prop = $level, the modal should have css classes $sizeClass and $levelClass',
({ size, level, sizeClass, levelClass }) => {
render(
<Modal isOpen size={size}>
<Modal.Header data-testid="Modal-Header" level={level}>
<h4>Test Header</h4>
</Modal.Header>
</Modal>
);
const modalContentDiv = screen.getByRole('dialog');
expect(modalContentDiv).toHaveClass(sizeClass);
expect(screen.getByTestId('Modal-Header')).toHaveClass(levelClass);
}
);
});

describe('onClose callback', () => {
it('calls onClose callback when the escape key is pressed', async () => {
const onClose = jest.fn();
render(<Modal isOpen onClose={onClose} data-testid="Modal-Test" />);
const modal = screen.getByTestId('Modal-Test');
expect(modal).toBeInTheDocument();

userEvent.type(modal, '{esc}');

expect(onClose).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 144c622

Please sign in to comment.