From 6513705b2391b03a3410e09f4e176b54c8b2b8e8 Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Tue, 29 Dec 2020 12:39:55 +0100 Subject: [PATCH 1/8] Added base modal --- src/components/Modal/Modal.tsx | 121 ++++++++++++++++++++++++++ src/components/Modal/index.tsx | 1 + src/components/Modal/modalManager.ts | 66 ++++++++++++++ src/components/index.ts | 1 + src/components/utils/generateGuuid.ts | 14 +++ src/components/utils/mergeRefs.ts | 26 ++++++ src/style/components/_modal.scss | 32 +++++++ src/style/index.scss | 1 + www/src/index.tsx | 39 ++++++++- 9 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 src/components/Modal/Modal.tsx create mode 100644 src/components/Modal/index.tsx create mode 100644 src/components/Modal/modalManager.ts create mode 100644 src/components/utils/generateGuuid.ts create mode 100644 src/components/utils/mergeRefs.ts create mode 100644 src/style/components/_modal.scss diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx new file mode 100644 index 0000000..056d39d --- /dev/null +++ b/src/components/Modal/Modal.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { createPortal } from 'react-dom'; +import { useEffect, useRef } from 'react'; +import { mergeRefs } from '@/components/utils/mergeRefs'; +import { ModalManager } from '@/components/Modal/modalManager'; + +let modalManager: ModalManager; + +export const getManager = (): ModalManager => { + if (!modalManager) { + modalManager = new ModalManager(); + } + + return modalManager; +} + +export interface ModalProps extends React.HTMLAttributes { + /** + * Indicates whether the modal should be shown + */ + show?: boolean; + /** + * Hook called when modal tries to hide + */ + onHide?: () => void; + /** + * Indicates whether the modal should be horizontally centered. Default: true + */ + horizontallyCentered?: boolean; + /** + * Indicates whether the modal uses a darkened backdrop. Default: true + */ + backdrop?: boolean +} + +/** + * Event fires when a model closes + */ +export const MODAL_CLOSE = 'c:ui:modal.close'; +export const MODAL_OPEN = 'c:ui:modal.open'; + +const Modal = React.forwardRef(( + { + backdrop = true, + children, + horizontallyCentered = true, + onHide, + show, + }, + ref +): React.ReactElement => { + const modalRef = useRef(null); + + const hideModal = (): void => { + getManager().removeModal(modalRef); + if (onHide) { + onHide(); + } + } + + const showModal = (): void => { + getManager().addModal({ + ref: modalRef, + backdrop + }) + } + + useEffect(() => { + if (show) { + showModal(); + } + }, []); + + useEffect(() => { + if (show) { + showModal(); + } else { + hideModal(); + } + }, [show]) + + return createPortal( + show ? ( +
{ + hideModal() + }} + role="dialog" + aria-modal={true} + > +
+ {children} +
+
+ ) : undefined, + document.body + ); +}); + +Modal.displayName = 'Modal'; +Modal.propTypes = { + backdrop: PropTypes.bool, + children: PropTypes.node, + onHide: PropTypes.func, + show: PropTypes.bool, + horizontallyCentered: PropTypes.bool +} + +export default Modal; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx new file mode 100644 index 0000000..c6b3568 --- /dev/null +++ b/src/components/Modal/index.tsx @@ -0,0 +1 @@ +export { default as Modal } from './Modal'; diff --git a/src/components/Modal/modalManager.ts b/src/components/Modal/modalManager.ts new file mode 100644 index 0000000..626c5a0 --- /dev/null +++ b/src/components/Modal/modalManager.ts @@ -0,0 +1,66 @@ +import { RefObject } from 'react'; + +export const MODAL_BACKDROP_CLASS = 'modal-backdrop'; + +export interface StoredModal { + ref: RefObject; + backdrop?: boolean; +} + +export class ModalManager { + modals: StoredModal[] = []; + + public isTop(ref: RefObject): boolean { + return this.modals[this.modals.length - 1].ref === ref; + } + + public addModal(modal: StoredModal): void { + this.modals.push(modal); + + if (modal.backdrop) { + this.placeBackdrop(); + } + + if (!document.body.classList.contains('has-modal')) { + document.body.classList.add('has-modal'); + } + } + + public removeModal(ref: RefObject): void { + const index = this.modals.findIndex( + (modal: StoredModal) => modal.ref === ref + ); + + if (index !== -1) { + this.modals.splice(index, 1); + } + + if (this.modals.length === 0) { + document.body.classList.remove('has-modal'); + this.removeBackdrop(); + } + } + + public placeBackdrop(): void { + const backdrop = document.getElementsByClassName(MODAL_BACKDROP_CLASS).item(0); + const topModalWithBackdrop = this.modals.slice().reverse().find((modal: StoredModal) => modal.backdrop); + + if (topModalWithBackdrop) { + if (backdrop === null) { + const backdropElement = document.createElement('div'); + backdropElement.classList.add(MODAL_BACKDROP_CLASS); + document.body.insertBefore(backdropElement, topModalWithBackdrop.ref.current); + } else { + topModalWithBackdrop.ref.current?.before(backdrop); + } + } + } + + public removeBackdrop(): void { + const backdrop = document.getElementsByClassName(MODAL_BACKDROP_CLASS).item(0); + + if (backdrop) { + document.body.removeChild(backdrop); + } + } +} diff --git a/src/components/index.ts b/src/components/index.ts index 41a7ea9..b8208c2 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,6 +5,7 @@ export * from './Grid'; export * from './Page'; export * from './Panel'; export * from './Icon'; +export * from './Modal'; export * from './utils'; export * from './TextField'; export * from './SelectField'; diff --git a/src/components/utils/generateGuuid.ts b/src/components/utils/generateGuuid.ts new file mode 100644 index 0000000..964a683 --- /dev/null +++ b/src/components/utils/generateGuuid.ts @@ -0,0 +1,14 @@ +const generateGuid = (): string => { + const segment = () => Math.floor(1 + Math.random() * 65536) + .toString(16) + .substring(1); + + return `${segment()}${segment()}-` + + `${segment()}${segment()}-` + + `${segment()}${segment()}-` + + `${segment()}${segment()}`; +}; + +export { + generateGuid +} diff --git a/src/components/utils/mergeRefs.ts b/src/components/utils/mergeRefs.ts new file mode 100644 index 0000000..6d212b2 --- /dev/null +++ b/src/components/utils/mergeRefs.ts @@ -0,0 +1,26 @@ +import { MutableRefObject, RefCallback } from 'react'; + +type Ref = MutableRefObject | RefCallback; + +const mergeRefs = (...refs: (Ref | null)[]): Ref | null => { + // @ts-ignore + const filteredRefs: Exclude | null, null>[] = refs.filter(Boolean); + if (!filteredRefs.length) return null; + if (filteredRefs.length === 0) return filteredRefs[0]; + + return (current: T | null) => { + if (current !== null) { + filteredRefs.forEach((filteredRef: Ref): void => { + if (typeof filteredRef === 'function') { + filteredRef(current); + } else { + filteredRef.current = current; + } + }) + } + } +} + +export { + mergeRefs +} diff --git a/src/style/components/_modal.scss b/src/style/components/_modal.scss new file mode 100644 index 0000000..fdfe593 --- /dev/null +++ b/src/style/components/_modal.scss @@ -0,0 +1,32 @@ +.has-modal { + overflow: hidden; +} +.modal-backdrop { + position: fixed; + height: 100vh; + width: 100vw; + background: rgba(0, 0, 0, .5); + top: 0; + left: 0; +} + +.modal-container { + position: fixed; + height: 100%; + width: 100vw; + top: 0; + left: 0; + display: block; + overflow: auto; + + .modal-content { + max-width: 500px; + margin-top: var(--base-gutter); + margin-bottom: var(--base-gutter); + + &.horizontal-center { + margin-left: auto; + margin-right: auto; + } + } +} diff --git a/src/style/index.scss b/src/style/index.scss index 251cabe..cc5e4be 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -17,6 +17,7 @@ @use "components/tags"; @use "components/overlay"; @use "components/tooltips"; +@use "components/modal"; /** * 3. Layout diff --git a/www/src/index.tsx b/www/src/index.tsx index a276c94..6f26645 100644 --- a/www/src/index.tsx +++ b/www/src/index.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import * as ReactDom from 'react-dom'; import '../../src/style/index.scss'; -import { Button, Card, Col, Grid, Icon, Page, Row, SelectField, Tag, TextField, Variant } from '../../src'; +import { Button, Card, Col, Grid, Icon, Modal, Page, Row, SelectField, Tag, TextField, Variant } from '../../src'; import Container from '../../src/components/Container/Container'; import FormGroup from '../../src/components/Form/Group'; import FormLabel from '../../src/components/Form/Label'; @@ -32,6 +32,35 @@ const TestControllable = () => { ); } +const TestModals = () => { + const [modalOpen, setModalOpen] = useState(false); + + return ( + <> + + { + setModalOpen(false); + }} + > + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! + + + + + ) +}; + ReactDom.render( @@ -302,6 +331,14 @@ ReactDom.render( + + + + + + + + From 8faea2e1352933dc3509814d84767cdffae5f830 Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Tue, 29 Dec 2020 13:37:38 +0100 Subject: [PATCH 2/8] WIP --- src/components/Modal/Modal.tsx | 34 +++++++++++----------------------- www/src/index.tsx | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 056d39d..46ad921 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { createPortal } from 'react-dom'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { mergeRefs } from '@/components/utils/mergeRefs'; import { ModalManager } from '@/components/Modal/modalManager'; @@ -35,12 +35,6 @@ export interface ModalProps extends React.HTMLAttributes { backdrop?: boolean } -/** - * Event fires when a model closes - */ -export const MODAL_CLOSE = 'c:ui:modal.close'; -export const MODAL_OPEN = 'c:ui:modal.open'; - const Modal = React.forwardRef(( { backdrop = true, @@ -52,8 +46,10 @@ const Modal = React.forwardRef(( ref ): React.ReactElement => { const modalRef = useRef(null); + const [shown, setShown] = useState(false); - const hideModal = (): void => { + const hideModal = (_event?: React.MouseEvent): void => { + setShown(false); getManager().removeModal(modalRef); if (onHide) { onHide(); @@ -61,37 +57,29 @@ const Modal = React.forwardRef(( } const showModal = (): void => { + setShown(true); getManager().addModal({ ref: modalRef, backdrop }) } - useEffect(() => { - if (show) { - showModal(); - } - }, []); - - useEffect(() => { - if (show) { + useEffect((): void => { + if (!shown && show) { showModal(); - } else { + } else if (shown && !shown) { hideModal(); } - }, [show]) + }, [show]); return createPortal( show ? (
{ - hideModal() - }} + onClick={hideModal} role="dialog" aria-modal={true} > diff --git a/www/src/index.tsx b/www/src/index.tsx index 6f26645..56de9b2 100644 --- a/www/src/index.tsx +++ b/www/src/index.tsx @@ -34,6 +34,7 @@ const TestControllable = () => { const TestModals = () => { const [modalOpen, setModalOpen] = useState(false); + const [foo, setFoo] = useState(false); return ( <> @@ -53,6 +54,22 @@ const TestModals = () => { > + + { + setFoo(false); + }} + > + Second modal + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! From d3adc67b2c80ef5a8ea7ecf12a9e10baf6626ddd Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Tue, 29 Dec 2020 14:14:33 +0100 Subject: [PATCH 3/8] Fixed closing modal when modal itself clicked --- src/components/Modal/Modal.tsx | 6 +++++- src/components/Modal/modalManager.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 46ad921..030f03a 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -48,7 +48,11 @@ const Modal = React.forwardRef(( const modalRef = useRef(null); const [shown, setShown] = useState(false); - const hideModal = (_event?: React.MouseEvent): void => { + const hideModal = (event?: React.MouseEvent): void => { + if (event && event.target !== modalRef.current) { + return; + } + setShown(false); getManager().removeModal(modalRef); if (onHide) { diff --git a/src/components/Modal/modalManager.ts b/src/components/Modal/modalManager.ts index 626c5a0..5b89bae 100644 --- a/src/components/Modal/modalManager.ts +++ b/src/components/Modal/modalManager.ts @@ -38,6 +38,8 @@ export class ModalManager { if (this.modals.length === 0) { document.body.classList.remove('has-modal'); this.removeBackdrop(); + } else { + this.placeBackdrop(); } } From a4ff52a303b94571496300f40ea168668a95e9be Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Tue, 29 Dec 2020 14:28:13 +0100 Subject: [PATCH 4/8] Added full modals live test --- www/src/index.tsx | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/www/src/index.tsx b/www/src/index.tsx index 56de9b2..a49e321 100644 --- a/www/src/index.tsx +++ b/www/src/index.tsx @@ -34,7 +34,8 @@ const TestControllable = () => { const TestModals = () => { const [modalOpen, setModalOpen] = useState(false); - const [foo, setFoo] = useState(false); + const [secondModalOpen, setSecondModalOpen] = useState(false); + const [thirdModalOpen, setThirdModalOpen] = useState(false); return ( <> @@ -57,18 +58,44 @@ const TestModals = () => { { - setFoo(false); + setSecondModalOpen(false); }} + backdrop={false} > - Second modal + + + + Content + + + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Beatae blanditiis commodi dicta distinctio fugiat labore laboriosam nisi nobis, possimus quasi recusandae repellendus repudiandae tempore. A deleniti omnis perspiciatis quos vero! From 4cfb25526b7d88d0630298ab9482a211a2e41856 Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Tue, 29 Dec 2020 18:55:12 +0100 Subject: [PATCH 5/8] Added modal test --- src/components/Modal/Modal.tsx | 2 +- src/components/Modal/__tests__/Modal.test.tsx | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/components/Modal/__tests__/Modal.test.tsx diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 030f03a..50ba66f 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -71,7 +71,7 @@ const Modal = React.forwardRef(( useEffect((): void => { if (!shown && show) { showModal(); - } else if (shown && !shown) { + } else if (shown && !show) { hideModal(); } }, [show]); diff --git a/src/components/Modal/__tests__/Modal.test.tsx b/src/components/Modal/__tests__/Modal.test.tsx new file mode 100644 index 0000000..6cc97e2 --- /dev/null +++ b/src/components/Modal/__tests__/Modal.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { Modal } from '@/components'; +import { ModalProps } from '@/components/Modal/Modal'; + +describe('Test modal', function (): void { + it('should render modal', (): void => { + const modal = mount( +
+ +
+ ) + + expect(modal.find('.modal-container').length).toBe(1); + }); + + it('should hide modal when show prop changes to false', (): void => { + const modal = mount( + + ); + + modal.setProps({ + show: false + }) + + expect(modal.find('.modal-container').length).toBe(0); + }) + + it('should hide the modal on container click', (): void => { + const mockFn = jest.fn(); + + const modal = mount( + + ); + + modal.find('.modal-container').simulate('click'); + modal.setProps({ + show: false + }); + + expect(modal.find('.modal-container').length).toBe(0); + expect(mockFn).toHaveBeenCalled(); + }) + + it('should not hide modal when clicked on modal', (): void => { + const mockFn = jest.fn(); + + const modal = mount( + + ); + + modal.find('.modal-content').simulate('click'); + + expect(mockFn).not.toHaveBeenCalled(); + }) + + it('should ignore show prop change when shown state is the same', (): void => { + const mockFn = jest.fn(); + + const modal = mount( + + ); + + modal.setProps({ + show: false + }); + + expect(mockFn).not.toHaveBeenCalled(); + }) +}); From 3fa6af8dc5e580ebfa3f91c54bbf18023eaadb74 Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Tue, 29 Dec 2020 19:12:28 +0100 Subject: [PATCH 6/8] Added merge refs test --- src/components/Modal/__tests__/Modal.test.tsx | 2 +- .../utils/__tests__/mergeRefs.test.ts | 45 +++++++++++++++++++ src/components/utils/mergeRefs.ts | 4 +- 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 src/components/utils/__tests__/mergeRefs.test.ts diff --git a/src/components/Modal/__tests__/Modal.test.tsx b/src/components/Modal/__tests__/Modal.test.tsx index 6cc97e2..6f8f84c 100644 --- a/src/components/Modal/__tests__/Modal.test.tsx +++ b/src/components/Modal/__tests__/Modal.test.tsx @@ -75,5 +75,5 @@ describe('Test modal', function (): void { }); expect(mockFn).not.toHaveBeenCalled(); - }) + }); }); diff --git a/src/components/utils/__tests__/mergeRefs.test.ts b/src/components/utils/__tests__/mergeRefs.test.ts new file mode 100644 index 0000000..9884903 --- /dev/null +++ b/src/components/utils/__tests__/mergeRefs.test.ts @@ -0,0 +1,45 @@ +import { mergeRefs } from '@/components/utils/mergeRefs'; + +describe('mergeRefs test', () => { + it('should return null when falsy values applied', () => { + const foo = mergeRefs(null); + + expect(foo).toBeNull(); + }); + + it('should return first if refs length = 1', () => { + const foo = () => ({}); + + const ref = mergeRefs(foo); + + expect(ref).toBe(foo); + }); + + it('should handle object and callback refs', () => { + let callbackRef: any = null; + const objectRef = { current: null }; + + const refs = mergeRefs((node: HTMLDivElement) => { + callbackRef = node; + }, objectRef); + + //@ts-ignore + refs(document.createElement('div')); + + expect(callbackRef).toBeInstanceOf(HTMLDivElement); + expect(objectRef.current).toBeInstanceOf(HTMLDivElement); + }) + + it('should do nothing when node is null', () => { + const mockFn = jest.fn(); + + const refs = mergeRefs(() => { + mockFn(); + }, { current: null }); + + //@ts-ignore + refs(null); + + expect(mockFn).not.toHaveBeenCalled(); + }) +}) diff --git a/src/components/utils/mergeRefs.ts b/src/components/utils/mergeRefs.ts index 6d212b2..05c4b1e 100644 --- a/src/components/utils/mergeRefs.ts +++ b/src/components/utils/mergeRefs.ts @@ -6,9 +6,9 @@ const mergeRefs = (...refs: (Ref | null)[]): Ref | null => { // @ts-ignore const filteredRefs: Exclude | null, null>[] = refs.filter(Boolean); if (!filteredRefs.length) return null; - if (filteredRefs.length === 0) return filteredRefs[0]; + if (filteredRefs.length === 1) return filteredRefs[0]; - return (current: T | null) => { + return (current: T | null): void => { if (current !== null) { filteredRefs.forEach((filteredRef: Ref): void => { if (typeof filteredRef === 'function') { From d50fd3b1b3063f659725ee5989808b77442341dd Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Tue, 29 Dec 2020 19:13:51 +0100 Subject: [PATCH 7/8] Removed unused method --- src/components/Modal/modalManager.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/Modal/modalManager.ts b/src/components/Modal/modalManager.ts index 5b89bae..c6a7e5a 100644 --- a/src/components/Modal/modalManager.ts +++ b/src/components/Modal/modalManager.ts @@ -10,10 +10,6 @@ export interface StoredModal { export class ModalManager { modals: StoredModal[] = []; - public isTop(ref: RefObject): boolean { - return this.modals[this.modals.length - 1].ref === ref; - } - public addModal(modal: StoredModal): void { this.modals.push(modal); From a799c6ea174a0e34494ae798cc1a7f47485cda40 Mon Sep 17 00:00:00 2001 From: Eran Machiels Date: Tue, 29 Dec 2020 21:01:28 +0100 Subject: [PATCH 8/8] Added modal manager --- .../Modal/__tests__/modalManager.test.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/components/Modal/__tests__/modalManager.test.ts diff --git a/src/components/Modal/__tests__/modalManager.test.ts b/src/components/Modal/__tests__/modalManager.test.ts new file mode 100644 index 0000000..f942bce --- /dev/null +++ b/src/components/Modal/__tests__/modalManager.test.ts @@ -0,0 +1,52 @@ +import { ModalManager } from '@/components/Modal/modalManager'; +import { createRef } from 'react'; + +describe('Test modalManager', function () { + it('should add backdrop if modal needs it', () => { + const manager = new ModalManager(); + manager.addModal({ + ref: createRef(), + backdrop: true + }) + + expect(document.body.querySelector('.modal-backdrop')).not.toBeNull(); + }); + + it('should add and remove class from body', () => { + const ref = createRef(); + const manager = new ModalManager(); + manager.addModal({ + ref: ref + }) + + expect(document.body.classList.contains('has-modal')).toBeTruthy(); + + manager.removeModal(ref); + + expect(document.body.classList.contains('has-modal')).toBeFalsy(); + }); + + it('should do nothing when ref not found', () => { + const ref = createRef(); + const otherRef = createRef(); + const manager = new ModalManager(); + manager.addModal({ + ref: ref + }) + + manager.removeModal(otherRef); + + expect(manager.modals.length).toBe(1); + }); + + it('should do nothing when the last modal with no backdrop', () => { + const manager = new ModalManager(); + manager.addModal({ + ref: createRef(), + backdrop: false + }); + manager.removeBackdrop(); + + expect(document.body.querySelector('.modal-backdrop')).toBeNull(); + }) +});