Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend-Jessie #85

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const ruleOverrides = {
'arrow-body-style': 0,
'canonical/destructuring-property-newline': 0,
'canonical/export-specifier-newline': 0,
'canonical/filename-match-exported': 'off',
'canonical/filename-match-regex': 0,
'canonical/import-specifier-newline': 0,
'default-case': 0,
Expand Down
5 changes: 4 additions & 1 deletion frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ const createJestConfig = nextJest({
// Add any custom config to be passed to Jest
const customJestConfig = {
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
setupFilesAfterEnv: ['@testing-library/jest-dom'],
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
moduleDirectories: ['node_modules', '<rootDir>/'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/(.*)': '<rootDir>/src/$1',
},
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
Expand Down
10 changes: 9 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,25 @@
"react-dom": "18.2.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "28.1.3",
"@types/node": "18.0.0",
"@types/react": "18.0.14",
"@types/react-dom": "18.0.5",
"autoprefixer": "^10.4.14",
"eslint": "8.18.0",
"eslint-config-canonical": "35.0.1",
"eslint-config-next": "12.1.6",
"eslint-config-prettier": "8.5.0",
"jest": "28.1.1",
"jest-environment-jsdom": "^29.5.0",
"lint-staged": "13.0.3",
"postcss": "^8.4.21",
"prettier": "2.7.1",
"tailwindcss": "^3.2.7",
"typescript": "4.7.4"
}
},
"packageManager": "yarn@3.5.0"
}
6 changes: 6 additions & 0 deletions frontend/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
9 changes: 9 additions & 0 deletions frontend/src/Utils/common.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const FOCUSABLE_HTML_ELEMENT = [
'a[href]',
'button:not([disabled])',
'input:not([disabled]):not([type="hidden"])',
'textarea:not([disabled])',
'button',
] as const;

export const FOCUSABLE_HTML_ELEMENT_STR = FOCUSABLE_HTML_ELEMENT.join(', ');
70 changes: 70 additions & 0 deletions frontend/src/Utils/hooks.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useEffect, useRef } from 'react';
import { FOCUSABLE_HTML_ELEMENT_STR } from './common.const';

export const useEscapeKey = (handleAction: () => void) => {
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handleAction();
}
};

document.addEventListener('keydown', handleEscape, false);
return () => {
document.removeEventListener('keydown', handleEscape, false);
};
}, [handleAction]);
};

export const useScrollBlock = (shouldBlock: boolean) => {
useEffect(() => {
if (shouldBlock) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'auto';
}

return () => {
document.body.style.overflow = 'auto';
};
}, [shouldBlock]);
};

export const useFocusTrap = () => {
const refOuter = useRef<HTMLDivElement | null>(null);

useEffect(() => {
setTimeout(() => {
const focusableElements = Array.from<HTMLElement>(
refOuter.current?.querySelectorAll(FOCUSABLE_HTML_ELEMENT_STR) ?? []
);

const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

firstElement?.focus();

const handleTabKey = (event: KeyboardEvent): void => {
if (event.key !== 'Tab') {
return;
}

if (document.activeElement === lastElement && !event.shiftKey) {
event.preventDefault();
firstElement?.focus();
}

if (document.activeElement === firstElement && event.shiftKey) {
event.preventDefault();
lastElement?.focus();
}
};

document.addEventListener('keydown', handleTabKey);

return () => document.removeEventListener('keydown', handleTabKey);
}, 100);
}, [refOuter]);

return refOuter;
};
14 changes: 14 additions & 0 deletions frontend/src/components/ConfirmModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { render, screen } from '@testing-library/react';
import ConfirmationModal from './ConfirmModal';

describe('<ConfirmationModal />', () => {
const handleClose = jest.fn();

it('should render when prop isOpen = true', async () => {
expect.hasAssertions();
render(<ConfirmationModal handleClose={handleClose} isOpen />);
const confirmButton = screen.queryByTestId('modal-confirm-button');

expect(confirmButton).toBeInTheDocument();
});
});
71 changes: 71 additions & 0 deletions frontend/src/components/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useState } from 'react';
import LayerModal from './LayerModal';
import Modal from './Modal';

type ConfirmationModalProps = {
handleClose: () => void;
isOpen: boolean;
};

const ConfirmModal = ({ isOpen, handleClose }: ConfirmationModalProps) => {
const [isLayerOpen, setIsLayerOpen] = useState(false);
const [triggerElement, setTriggerElement] = useState<
(EventTarget & HTMLButtonElement) | null
>(null);

const handleConfirm = (event: React.MouseEvent<HTMLButtonElement>) => {
setIsLayerOpen(true);
setTriggerElement(event.currentTarget);
};

const handleLayerClose = () => {
setIsLayerOpen(false);
triggerElement?.focus();
};

return (
<>
{isOpen && (
<Modal handleClose={handleClose} isOpen={isOpen} name="Confirm">
<div className="flex justify-center flex-col items-center">
<div className="pt-1 tb-2">
<h3 className="text-xl font-semibold text-gray-900">
Wait a moment
</h3>
</div>

<p className="text-2lg pt-1 tb-2">
Are you sure you want to open another modal?
</p>

<div className="flex items-center p-6 space-x-2 border-gray-200 rounded-b ">
<button
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-lg px-5 py-2.5 text-center"
data-testid="modal-confirm-button"
onClick={(event) => handleConfirm(event)}
tabIndex={0}
type="button"
>
Confirm
</button>
<button
className="focus:ring-4 focus:outline-none rounded-lg border text-lg font-medium px-5 py-2.5 focus:z-10 bg-gray-700 text-gray-300 border-gray-500 hover:text-white hover:bg-gray-600 focus:ring-gray-600"
onClick={handleClose}
tabIndex={0}
type="button"
>
Cancel
</button>
</div>

{isLayerOpen && (
<LayerModal handleClose={handleLayerClose} isOpen={isLayerOpen} />
)}
</div>
</Modal>
)}
</>
);
};

export default ConfirmModal;
30 changes: 30 additions & 0 deletions frontend/src/components/LayerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Modal from './Modal';

type LayerModalProps = {
handleClose: () => void;
isOpen: boolean;
};

const LayerModal = ({ handleClose, isOpen }: LayerModalProps) => {
return (
<>
{isOpen && (
<Modal handleClose={handleClose} isOpen={isOpen} name="Layer">
<div className="flex justify-center flex-col items-center">
<p className="text-2lg pt-1 tb-2 mb-10">This is a layer modal</p>
<button
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-lg px-5 py-2.5 text-center"
onClick={handleClose}
tabIndex={0}
type="button"
>
close
</button>
</div>
</Modal>
)}
</>
);
};

export default LayerModal;
97 changes: 97 additions & 0 deletions frontend/src/components/Modal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { render, screen, waitFor } from '@testing-library/react';
import user from '@testing-library/user-event';
import Modal from './Modal';

describe('<Modal />', () => {
const contentChild = (
<>
<button data-testid="first-item" tabIndex={0} type="button">
focusable Button
</button>
<button data-testid="second-item" tabIndex={0} type="button">
focusable Button
</button>
<p>This is a test content</p>
</>
);
let handleClose: () => void;

beforeEach(() => {
handleClose = jest.fn();
});

const renderComponent = () =>
render(
<Modal handleClose={handleClose} isOpen name="Confirm">
{contentChild}
</Modal>
);

it('should render when prop isOpen = true', async () => {
expect.hasAssertions();
renderComponent();
const modal = screen.queryByTestId('modal-container');
expect(modal).toBeInTheDocument();
});

it('should call handleClose after click the close button', async () => {
expect.hasAssertions();
renderComponent();

const closeButton = screen.getByTestId('modal-close-button');

await user.click(closeButton);

expect(handleClose).toHaveBeenCalledTimes(1);
});

it('should block the background scrolling', () => {
expect.hasAssertions();

jest.spyOn(window, 'scrollTo').mockImplementation();

const initialScrollPosition = window.pageYOffset;

renderComponent();

window.scrollTo(0, 600);

expect(window.pageYOffset).toBe(initialScrollPosition);
});

it('should focus on first element when render', async () => {
expect.hasAssertions();

renderComponent();

const firstButton = screen.queryByTestId('first-item');

await waitFor(() => expect(document.activeElement).toBe(firstButton));
});

it('should be able to change focus using Tab', async () => {
expect.hasAssertions();

renderComponent();

const firstButton = screen.queryByTestId('first-item');
const secondButton = screen.queryByTestId('second-item');

firstButton?.focus();
await user.tab();

await waitFor(() => {
expect(document.activeElement).toBe(secondButton);
});
});

it('should close the modal when press Esc', async () => {
expect.hasAssertions();

renderComponent();

await user.keyboard('{Escape}');

expect(handleClose).toHaveBeenCalledTimes(1);
});
});