From 1feaa58bae48916d50973102688f3dd3d0734c5f Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Tue, 28 Jun 2022 16:01:20 -0400 Subject: [PATCH] feat(modal): discovery-149 pf4 modal wrapper * locales, modal default string * modal, consistent pf4 wrapper, allow backdrop disable --- public/locales/en.json | 3 + .../__tests__/__snapshots__/i18n.test.js.snap | 9 + .../__snapshots__/modal.test.js.snap | 395 ++++++++++++++++++ src/components/modal/__tests__/modal.test.js | 73 ++++ src/components/modal/modal.js | 128 ++++++ src/styles/app/_modal.scss | 35 ++ src/styles/index.scss | 1 + 7 files changed, 644 insertions(+) create mode 100644 src/components/modal/__tests__/__snapshots__/modal.test.js.snap create mode 100644 src/components/modal/__tests__/modal.test.js create mode 100644 src/components/modal/modal.js create mode 100644 src/styles/app/_modal.scss diff --git a/public/locales/en.json b/public/locales/en.json index 086abaed..6186fad9 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -8,5 +8,8 @@ "server-version": "Server Version", "ui-version": "UI Version", "username": "Username" + }, + "modal": { + "aria-label-default": "Application modal" } } diff --git a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap index b857cb16..6725253a 100644 --- a/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap +++ b/src/components/i18n/__tests__/__snapshots__/i18n.test.js.snap @@ -48,6 +48,15 @@ Array [ }, ], }, + Object { + "file": "./src/components/modal/modal.js", + "keys": Array [ + Object { + "key": "modal.aria-label-default", + "match": "t('modal.aria-label-default')", + }, + ], + }, ] `; diff --git a/src/components/modal/__tests__/__snapshots__/modal.test.js.snap b/src/components/modal/__tests__/__snapshots__/modal.test.js.snap new file mode 100644 index 00000000..7741463e --- /dev/null +++ b/src/components/modal/__tests__/__snapshots__/modal.test.js.snap @@ -0,0 +1,395 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Modal Component should allow custom headers and footers: element 1`] = ` +
+
+ +
+
+`; + +exports[`Modal Component should allow custom headers and footers: function 1`] = ` +
+
+ +
+
+`; + +exports[`Modal Component should allow custom headers and footers: list 1`] = ` +
+
+ +
+
+`; + +exports[`Modal Component should allow custom headers and footers: string 1`] = ` +
+
+ +
+
+`; + +exports[`Modal Component should allow custom headers and footers: undefined 1`] = ` +
+
+ +
+
+`; + +exports[`Modal Component should allow modifying specific and custom props: aria-label 1`] = ` + +
+
+ } + aria-describedby="" + aria-label="dolor sit" + aria-labelledby="" + aria-live="polite" + className="" + hasNoBodyWrapper={false} + isOpen={false} + onClose={[Function]} + ouiaSafe={true} + position="top" + positionOffset="5%" + showClose={false} + title="" + titleIconVariant={null} + titleLabel="" + variant={null} +> + } + > + + +
+`; + +exports[`Modal Component should allow modifying specific and custom props: backdrop 1`] = ` + +
+
+ } + aria-describedby="" + aria-label="t(modal.aria-label-default)" + aria-labelledby="" + aria-live="polite" + className="" + hasNoBodyWrapper={false} + isOpen={false} + onClose={[Function]} + ouiaSafe={true} + position="top" + positionOffset="5%" + showClose={false} + title="" + titleIconVariant={null} + titleLabel="" + variant={null} +> + } + > + + +
+`; + +exports[`Modal Component should render a basic component: basic 1`] = ` + + +
+
+ } + aria-describedby="" + aria-label="t(modal.aria-label-default)" + aria-labelledby="" + aria-live="polite" + className="" + hasNoBodyWrapper={false} + isOpen={false} + onClose={[Function]} + ouiaSafe={true} + position="top" + positionOffset="5%" + showClose={false} + title="" + titleIconVariant={null} + titleLabel="" + variant={null} + > + } + > + + +
+
+`; diff --git a/src/components/modal/__tests__/modal.test.js b/src/components/modal/__tests__/modal.test.js new file mode 100644 index 00000000..0cb17652 --- /dev/null +++ b/src/components/modal/__tests__/modal.test.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { Modal as PfModal, ModalContent } from '@patternfly/react-core'; +import { Modal } from '../modal'; + +describe('Modal Component', () => { + it('should render a basic component', async () => { + const props = {}; + + const component = await mountHookComponent(lorem ipsum); + expect(component).toMatchSnapshot('basic'); + }); + + it('should allow modifying specific and custom props', async () => { + const props = { + backdrop: false + }; + + const component = await mountHookComponent(lorem ipsum); + expect(component.find(PfModal)).toMatchSnapshot('backdrop'); + + component.setProps({ + backdrop: true, + 'aria-label': 'dolor sit' + }); + + expect(component.find(PfModal)).toMatchSnapshot('aria-label'); + }); + + it('should allow custom headers and footers', async () => { + // disableFocusTrap for testing only + const props = { + isOpen: true, + disableFocusTrap: true + }; + + const component = await mountHookComponent(hello world); + + component.setProps({ + header: undefined, + footer: undefined + }); + + expect(component.find(ModalContent).render()).toMatchSnapshot('undefined'); + + component.setProps({ + header: 'lorem ipsum', + footer: 'dolor sit' + }); + + expect(component.find(ModalContent).render()).toMatchSnapshot('string'); + + component.setProps({ + header: () => 'lorem ipsum', + footer: () => 'dolor sit' + }); + + expect(component.find(ModalContent).render()).toMatchSnapshot('function'); + + component.setProps({ + header: [lorem ipsum], + footer: [dolor sit] + }); + + expect(component.find(ModalContent).render()).toMatchSnapshot('list'); + + component.setProps({ + header: lorem ipsum, + footer: dolor sit + }); + + expect(component.find(ModalContent).render()).toMatchSnapshot('element'); + }); +}); diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js new file mode 100644 index 00000000..a57ed8bd --- /dev/null +++ b/src/components/modal/modal.js @@ -0,0 +1,128 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { useMount, useUnmount } from 'react-use'; +import { Modal as PfModal, ModalProps, ModalVariant } from '@patternfly/react-core'; +import classNames from 'classnames'; +import { translate } from '../i18n/i18n'; + +/** + * Wrapper for adjusting PF Modal styling. + * + * @param {object} props + * @param {string} props.'aria-label' + * @param {boolean} props.backdrop + * @param {React.ReactNode} props.children + * @param {string} props.className + * @param {React.ReactNode|Function} props.header + * @param {React.ReactNode|Function} props.footer + * @param {string} props.position + * @param {string} props.positionOffset + * @param {boolean} props.showClose + * @param {Function} props.t + * @param {string} props.variant + * @param {ModalProps} props.props + * @returns {React.ReactNode} + */ +const Modal = ({ + 'aria-label': ariaLabel, + backdrop, + children, + className, + header, + footer, + position, + positionOffset, + showClose, + t, + variant, + ...props +}) => { + const [element, setElement] = useState(); + const updatedProps = { ...props }; + const cssClassName = classNames( + `quipucords-modal`, + { 'quipucords-modal__hide-backdrop': backdrop === false }, + { 'quipucords-modal__rcue-width': !variant }, + className + ); + + useMount(() => { + const domElement = document.createElement('div'); + document.body.appendChild(domElement); + domElement.className = cssClassName; + setElement(domElement); + }); + + useUnmount(() => { + element?.remove(); + }); + + if (header) { + updatedProps.header = (typeof header === 'function' && header()) || header; + } + + if (footer) { + updatedProps.footer = (typeof footer === 'function' && footer()) || footer; + } + + if (!element) { + return null; + } + + return ( + + {(React.isValidElement(children) && children) ||
{children || ''}
} +
+ ); +}; + +/** + * Prop types + * + * @type {{backdrop: boolean, showClose: boolean, t: Function, children: React.ReactNode, + * footer: React.ReactNode|Function, variant: string, header: React.ReactNode|Function, + * className: string|object, position: string, positionOffset: string, 'aria-label': string}} + */ +Modal.propTypes = { + 'aria-label': PropTypes.string, + backdrop: PropTypes.bool, + children: PropTypes.node.isRequired, + className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + footer: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + header: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + position: PropTypes.oneOf(['top', null]), + positionOffset: PropTypes.string, + showClose: PropTypes.bool, + t: PropTypes.func, + variant: PropTypes.oneOf([...Object.values(ModalVariant)]) +}; + +/** + * Default props. + * + * @type {{backdrop: boolean, showClose: boolean, t: translate, footer: null, variant: null, header: null, + * className: null, position: string, positionOffset: string, 'aria-label': null}} + */ +Modal.defaultProps = { + 'aria-label': null, + backdrop: true, + className: null, + footer: null, + header: null, + position: 'top', + positionOffset: '5%', + showClose: false, + t: translate, + variant: null +}; + +export { Modal as default, Modal, ModalVariant }; diff --git a/src/styles/app/_modal.scss b/src/styles/app/_modal.scss new file mode 100644 index 00000000..47a9c824 --- /dev/null +++ b/src/styles/app/_modal.scss @@ -0,0 +1,35 @@ +.quipucords{ + &-modal { + h4.pf-c-title.pf-m-md { + font-size: 0.9rem; + } + + .pf-c-button { + font-size: inherit; + } + + &__rcue-width { + div.pf-c-modal-box.pf-m-align-top { + @media (min-width: $pf-global--breakpoint--md) { + width: auto; + min-width: 600px; + } + } + } + + &__hide-backdrop { + .pf-c-backdrop { + background-color: transparent; + } + } + + &__confirmation { + .pf-c-backdrop { + z-index: 1100; + } + .pf-c-modal-box__body { + padding-bottom: var(--pf-c-modal-box__body--PaddingTop); + } + } + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss index 4062d36c..7789bdae 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -23,6 +23,7 @@ $icon-font-path: '~patternfly/dist/fonts/'; @import 'app/overrides'; @import 'app/common'; @import 'app/aboutModal'; +@import 'app/modal'; @import 'app/login'; @import 'app/app'; @import 'app/forms';