From ed0b429f3fbfa61141514619cc259d826145251c Mon Sep 17 00:00:00 2001 From: Titani Date: Mon, 4 Dec 2023 11:06:15 -0500 Subject: [PATCH 1/6] feat(Modal next): Introduce a next composble Modal --- .../src/next/components/Modal/Modal.tsx | 206 +++++++++++++++++ .../src/next/components/Modal/ModalBox.tsx | 63 +++++ .../next/components/Modal/ModalBoxBody.tsx | 45 ++++ .../components/Modal/ModalBoxCloseButton.tsx | 38 +++ .../components/Modal/ModalBoxDescription.tsx | 24 ++ .../next/components/Modal/ModalBoxFooter.tsx | 21 ++ .../next/components/Modal/ModalBoxHeader.tsx | 64 +++++ .../next/components/Modal/ModalBoxTitle.tsx | 73 ++++++ .../next/components/Modal/ModalContent.tsx | 130 +++++++++++ .../components/Modal/__tests__/Modal.test.tsx | 141 +++++++++++ .../Modal/__tests__/ModalBox.test.tsx | 58 +++++ .../Modal/__tests__/ModalBoxBody.test.tsx | 60 +++++ .../__tests__/ModalBoxCloseButton.test.tsx | 18 ++ .../__tests__/ModalBoxDescription.test.tsx | 8 + .../Modal/__tests__/ModalBoxFooter.test.tsx | 10 + .../Modal/__tests__/ModalBoxHeader.test.tsx | 25 ++ .../Modal/__tests__/ModalBoxTitle.test.tsx | 47 ++++ .../Modal/__tests__/ModalContent.test.tsx | 53 +++++ .../__snapshots__/ModalBox.test.tsx.snap | 86 +++++++ .../__snapshots__/ModalBoxBody.test.tsx.snap | 12 + .../ModalBoxDescription.test.tsx.snap | 12 + .../ModalBoxFooter.test.tsx.snap | 11 + .../ModalBoxHeader.test.tsx.snap | 46 ++++ .../__snapshots__/ModalBoxTitle.test.tsx.snap | 218 ++++++++++++++++++ .../__snapshots__/ModalContent.test.tsx.snap | 209 +++++++++++++++++ .../next/components/Modal/examples/Modal.md | 157 +++++++++++++ .../components/Modal/examples/ModalBasic.tsx | 43 ++++ .../Modal/examples/ModalCustomFocus.tsx | 43 ++++ .../examples/ModalCustomHeaderFooter.tsx | 55 +++++ .../Modal/examples/ModalCustomTitleIcon.tsx | 54 +++++ .../Modal/examples/ModalCustomWidth.tsx | 43 ++++ .../components/Modal/examples/ModalLarge.tsx | 43 ++++ .../components/Modal/examples/ModalMedium.tsx | 43 ++++ .../Modal/examples/ModalNoHeaderFooter.tsx | 40 ++++ .../components/Modal/examples/ModalSmall.tsx | 44 ++++ .../Modal/examples/ModalTitleIcon.tsx | 48 ++++ .../Modal/examples/ModalTopAligned.tsx | 43 ++++ .../Modal/examples/ModalWithDescription.tsx | 47 ++++ .../Modal/examples/ModalWithDropdown.tsx | 105 +++++++++ .../Modal/examples/ModalWithForm.tsx | 211 +++++++++++++++++ .../Modal/examples/ModalWithHelp.tsx | 63 +++++ .../examples/ModalWithOverflowingContent.tsx | 75 ++++++ .../Modal/examples/ModalWithWizard.tsx | 61 +++++ .../src/next/components/Modal/index.ts | 4 + .../react-core/src/next/components/index.ts | 2 +- 45 files changed, 2901 insertions(+), 1 deletion(-) create mode 100644 packages/react-core/src/next/components/Modal/Modal.tsx create mode 100644 packages/react-core/src/next/components/Modal/ModalBox.tsx create mode 100644 packages/react-core/src/next/components/Modal/ModalBoxBody.tsx create mode 100644 packages/react-core/src/next/components/Modal/ModalBoxCloseButton.tsx create mode 100644 packages/react-core/src/next/components/Modal/ModalBoxDescription.tsx create mode 100644 packages/react-core/src/next/components/Modal/ModalBoxFooter.tsx create mode 100644 packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx create mode 100644 packages/react-core/src/next/components/Modal/ModalBoxTitle.tsx create mode 100644 packages/react-core/src/next/components/Modal/ModalContent.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/Modal.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalBox.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalBoxCloseButton.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalBoxDescription.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalBoxFooter.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalBoxHeader.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalBoxTitle.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalContent.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBox.test.tsx.snap create mode 100644 packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxBody.test.tsx.snap create mode 100644 packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxDescription.test.tsx.snap create mode 100644 packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxFooter.test.tsx.snap create mode 100644 packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxHeader.test.tsx.snap create mode 100644 packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxTitle.test.tsx.snap create mode 100644 packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap create mode 100644 packages/react-core/src/next/components/Modal/examples/Modal.md create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalBasic.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalCustomFocus.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalCustomHeaderFooter.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalCustomTitleIcon.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalCustomWidth.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalLarge.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalMedium.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalNoHeaderFooter.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalSmall.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalTitleIcon.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalTopAligned.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalWithDescription.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalWithDropdown.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalWithForm.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalWithHelp.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalWithOverflowingContent.tsx create mode 100644 packages/react-core/src/next/components/Modal/examples/ModalWithWizard.tsx create mode 100644 packages/react-core/src/next/components/Modal/index.ts diff --git a/packages/react-core/src/next/components/Modal/Modal.tsx b/packages/react-core/src/next/components/Modal/Modal.tsx new file mode 100644 index 00000000000..e6708dbcb09 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/Modal.tsx @@ -0,0 +1,206 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { canUseDOM, KeyTypes, PickOptional } from '../../../helpers'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/Backdrop/backdrop'; +import { ModalContent } from './ModalContent'; +import { OUIAProps, getDefaultOUIAId } from '../../../helpers'; + +export interface ModalProps extends React.HTMLProps, OUIAProps { + /** The parent container to append the modal to. Defaults to "document.body". */ + appendTo?: HTMLElement | (() => HTMLElement); + /** Id to use for the modal box description. This should match the ModalBoxHeader labelId or descriptorId. */ + 'aria-describedby'?: string; + /** Accessible descriptor of the modal. */ + 'aria-label'?: string; + /** Id to use for the modal box label. This should include the ModalBoxHeader labelId. */ + 'aria-labelledby'?: string; + /** Content rendered inside the modal. */ + children: React.ReactNode; + /** Additional classes added to the modal. */ + className?: string; + /** Flag to disable focus trap. */ + disableFocusTrap?: boolean; + /** The element to focus when the modal opens. By default the first + * focusable element will receive focus. + */ + elementToFocus?: HTMLElement | SVGElement | string; + /** An id to use for the modal box container. */ + id?: string; + /** Flag to show the modal. */ + isOpen?: boolean; + /** A callback for when the close button is clicked. */ + onClose?: (event: KeyboardEvent | React.MouseEvent) => void; + /** Modal handles pressing of the escape key and closes the modal. If you want to handle + * this yourself you can use this callback function. */ + onEscapePress?: (event: KeyboardEvent) => void; + /** Position of the modal. By default a modal will be positioned vertically and horizontally centered. */ + position?: 'default' | 'top'; + /** Offset from alternate position. Can be any valid CSS length/percentage. */ + positionOffset?: string; + /** Flag to show the close button in the header area of the modal. */ + showClose?: boolean; + /** Variant of the modal. */ + variant?: 'small' | 'medium' | 'large' | 'default'; + /** Default width of the modal. */ + width?: number | string; + /** Maximum width of the modal. */ + maxWidth?: number | string; + /** Value to overwrite the randomly generated data-ouia-component-id.*/ + ouiaId?: number | string; + /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ + ouiaSafe?: boolean; +} + +export enum ModalVariant { + small = 'small', + medium = 'medium', + large = 'large', + default = 'default' +} + +interface ModalState { + container: HTMLElement; + ouiaStateId: string; +} + +class Modal extends React.Component { + static displayName = 'Modal'; + static currentId = 0; + boxId = ''; + + static defaultProps: PickOptional = { + isOpen: false, + showClose: true, + onClose: () => undefined as any, + variant: 'default', + appendTo: () => document.body, + ouiaSafe: true, + position: 'default' + }; + + constructor(props: ModalProps) { + super(props); + const boxIdNum = Modal.currentId++; + this.boxId = props.id || `pf-modal-part-${boxIdNum}`; + + this.state = { + container: undefined, + ouiaStateId: getDefaultOUIAId(Modal.displayName, props.variant) + }; + } + + handleEscKeyClick = (event: KeyboardEvent): void => { + const { onEscapePress } = this.props; + if (event.key === KeyTypes.Escape && this.props.isOpen) { + onEscapePress ? onEscapePress(event) : this.props.onClose?.(event); + } + }; + + getElement = (appendTo: HTMLElement | (() => HTMLElement)) => { + if (typeof appendTo === 'function') { + return appendTo(); + } + return appendTo || document.body; + }; + + toggleSiblingsFromScreenReaders = (hide: boolean) => { + const { appendTo } = this.props; + const target: HTMLElement = this.getElement(appendTo); + const bodyChildren = target.children; + for (const child of Array.from(bodyChildren)) { + if (child !== this.state.container) { + hide ? child.setAttribute('aria-hidden', '' + hide) : child.removeAttribute('aria-hidden'); + } + } + }; + + isEmpty = (value: string | null | undefined) => value === null || value === undefined || value === ''; + + componentDidMount() { + const { + appendTo, + 'aria-describedby': ariaDescribedby, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby + } = this.props; + const target: HTMLElement = this.getElement(appendTo); + const container = document.createElement('div'); + this.setState({ container }); + target.appendChild(container); + target.addEventListener('keydown', this.handleEscKeyClick, false); + + if (this.props.isOpen) { + target.classList.add(css(styles.backdropOpen)); + } else { + target.classList.remove(css(styles.backdropOpen)); + } + + if (!ariaDescribedby && !ariaLabel && !ariaLabelledby) { + // eslint-disable-next-line no-console + console.error('Modal: Specify at least one of: aria-describedby, aria-label, aria-labelledby.'); + } + } + + componentDidUpdate() { + const { appendTo } = this.props; + const target: HTMLElement = this.getElement(appendTo); + if (this.props.isOpen) { + target.classList.add(css(styles.backdropOpen)); + this.toggleSiblingsFromScreenReaders(true); + } else { + target.classList.remove(css(styles.backdropOpen)); + this.toggleSiblingsFromScreenReaders(false); + } + } + + componentWillUnmount() { + const { appendTo } = this.props; + const target: HTMLElement = this.getElement(appendTo); + if (this.state.container) { + target.removeChild(this.state.container); + } + target.removeEventListener('keydown', this.handleEscKeyClick, false); + target.classList.remove(css(styles.backdropOpen)); + this.toggleSiblingsFromScreenReaders(false); + } + + render() { + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + appendTo, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onEscapePress, + 'aria-labelledby': ariaLabelledby, + 'aria-label': ariaLabel, + 'aria-describedby': ariaDescribedby, + ouiaId, + ouiaSafe, + position, + elementToFocus, + ...props + } = this.props; + const { container } = this.state; + + if (!canUseDOM || !container) { + return null; + } + + return ReactDOM.createPortal( + , + container + ) as React.ReactElement; + } +} + +export { Modal }; diff --git a/packages/react-core/src/next/components/Modal/ModalBox.tsx b/packages/react-core/src/next/components/Modal/ModalBox.tsx new file mode 100644 index 00000000000..40b9b2fac6c --- /dev/null +++ b/packages/react-core/src/next/components/Modal/ModalBox.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; +import topSpacer from '@patternfly/react-tokens/dist/esm/c_modal_box_m_align_top_spacer'; + +export interface ModalBoxProps extends React.HTMLProps { + /** Id to use for the modal box description. This should match the ModalBoxHeader labelId or descriptorId */ + 'aria-describedby'?: string; + /** Accessible descriptor of the modal. */ + 'aria-label'?: string; + /** Id to use for the modal box label. */ + 'aria-labelledby'?: string; + /** Content rendered inside the modal box. */ + children: React.ReactNode; + /** Additional classes added to the modal box. */ + className?: string; + /** Position of the modal. By default a modal will be positioned vertically and horizontally centered. */ + position?: 'default' | 'top'; + /** Offset from alternate position. Can be any valid CSS length/percentage. */ + positionOffset?: string; + /** Variant of the modal. */ + variant?: 'small' | 'medium' | 'large' | 'default'; +} + +export const ModalBox: React.FunctionComponent = ({ + children, + className, + variant = 'default', + position, + positionOffset, + 'aria-labelledby': ariaLabelledby, + 'aria-label': ariaLabel, + 'aria-describedby': ariaDescribedby, + style, + ...props +}: ModalBoxProps) => { + if (positionOffset) { + style = style || {}; + (style as any)[topSpacer.name] = positionOffset; + } + return ( +
+ {children} +
+ ); +}; +ModalBox.displayName = 'ModalBox'; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx b/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx new file mode 100644 index 00000000000..5c3ee16ba95 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; + +export interface ModalBoxBodyProps extends React.HTMLProps { + /** Content rendered inside the modal box body. */ + children?: React.ReactNode; + /** Additional classes added to the modal box body. */ + className?: string; + /** Accessible label applied to the modal box body. This should be used to communicate + * important information about the modal box body div element if needed, such as that it + * is scrollable. + */ + 'aria-label'?: string; + /** Accessible role applied to the modal box body. This will default to "region" if the + * bodyAriaLabel property is passed in. Set to a more appropriate role as applicable + * based on the modal content and context. + */ + 'aria-role'?: string; + /** Id of the modal box body. This should mathc hte modal box header descriptorId????? */ + id?: string; +} + +export const ModalBoxBody: React.FunctionComponent = ({ + children, + className, + 'aria-label': ariaLabel, + 'aria-role': ariaRole, + id, + ...props +}: ModalBoxBodyProps) => { + const defaultModalBodyAriaRole = ariaLabel ? 'region' : undefined; + return ( +
+ {children} +
+ ); +}; +ModalBoxBody.displayName = 'ModalBoxBody'; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxCloseButton.tsx b/packages/react-core/src/next/components/Modal/ModalBoxCloseButton.tsx new file mode 100644 index 00000000000..a3549530fcf --- /dev/null +++ b/packages/react-core/src/next/components/Modal/ModalBoxCloseButton.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; +import { Button } from '../../../components/Button'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import { OUIAProps } from '../../../helpers'; + +export interface ModalBoxCloseButtonProps extends OUIAProps { + /** Additional classes added to the close button. */ + className?: string; + /** A callback for when the close button is clicked. */ + onClose?: (event: KeyboardEvent | React.MouseEvent) => void; + /** Accessible descriptor of the close button. */ + 'aria-label'?: string; + /** Value to set the data-ouia-component-id.*/ + ouiaId?: number | string; +} + +export const ModalBoxCloseButton: React.FunctionComponent = ({ + className, + onClose = () => undefined as any, + 'aria-label': ariaLabel = 'Close', + ouiaId, + ...props +}: ModalBoxCloseButtonProps) => ( +
+ +
+); +ModalBoxCloseButton.displayName = 'ModalBoxCloseButton'; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxDescription.tsx b/packages/react-core/src/next/components/Modal/ModalBoxDescription.tsx new file mode 100644 index 00000000000..1ce6385e88c --- /dev/null +++ b/packages/react-core/src/next/components/Modal/ModalBoxDescription.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; + +export interface ModalBoxDescriptionProps { + /** Content rendered inside the description. */ + children?: React.ReactNode; + /** Additional classes added to the description. */ + className?: string; + /** Id of the description. */ + id?: string; +} + +export const ModalBoxDescription: React.FunctionComponent = ({ + children = null, + className = '', + id = '', + ...props +}: ModalBoxDescriptionProps) => ( +
+ {children} +
+); +ModalBoxDescription.displayName = 'ModalBoxDescription'; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxFooter.tsx b/packages/react-core/src/next/components/Modal/ModalBoxFooter.tsx new file mode 100644 index 00000000000..5c4432f71d4 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/ModalBoxFooter.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; + +export interface ModalBoxFooterProps { + /** Content rendered inside the modal box footer. */ + children?: React.ReactNode; + /** Additional classes added to the modal box footer. */ + className?: string; +} + +export const ModalBoxFooter: React.FunctionComponent = ({ + children, + className, + ...props +}: ModalBoxFooterProps) => ( +
+ {children} +
+); +ModalBoxFooter.displayName = 'ModalBoxFooter'; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx b/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx new file mode 100644 index 00000000000..a7eaad72fb1 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { css } from '@patternfly/react-styles'; +import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; +import { ModalBoxDescription } from './ModalBoxDescription'; +import { ModalBoxTitle } from './ModalBoxTitle'; + +export interface ModalBoxHeaderProps { + /** Custom content rendered inside the modal box header. If children are supplied then the tile, tileIconVariant and titleLabel props are ignored. */ + children?: React.ReactNode; + /** Additional classes added to the modal box header. */ + className?: string; + /** Description of the modal. */ + description?: React.ReactNode; + /** Id of the modal box description. */ + descriptorId?: string; + /** Optional help section for the modal box header. */ + help?: React.ReactNode; + /** Id of the modal box title. */ + labelId?: string; + /** Content rendered inside the modal box title. */ + title?: React.ReactNode; + /** Optional alert icon (or other) to show before the title. When the predefined alert types + * are used the default styling will be automatically applied. */ + titleIconVariant?: 'success' | 'danger' | 'warning' | 'info' | 'custom' | React.ComponentType; + /** Optional title label text for screen readers. */ + titleLabel?: string; +} + +export const ModalBoxHeader: React.FunctionComponent = ({ + children, + className, + descriptorId, + description, + labelId, + title, + titleIconVariant, + titleLabel, + help, + ...props +}: ModalBoxHeaderProps) => { + const headerContent = children ? ( + children + ) : ( + <> + + {description && {description}} + + ); + + // TODO: apply variant modifier for icon styling. Core fix needed first. similar to this: + // className={css(className, isVariantIcon(titleIconVariant) && modalStyles.modifiers[titleIconVariant])} + return ( +
+ {help && ( + <> +
{headerContent}
+
{help}
+ + )} + {!help && headerContent} +
+ ); +}; +ModalBoxHeader.displayName = 'ModalBoxHeader'; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxTitle.tsx b/packages/react-core/src/next/components/Modal/ModalBoxTitle.tsx new file mode 100644 index 00000000000..f8466b055cb --- /dev/null +++ b/packages/react-core/src/next/components/Modal/ModalBoxTitle.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import modalStyles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; +import { css } from '@patternfly/react-styles'; +import { capitalize } from '../../../helpers'; +import { Tooltip } from '../../../components/Tooltip'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; +import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; +import InfoCircleIcon from '@patternfly/react-icons/dist/esm/icons/info-circle-icon'; +import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; +import { useIsomorphicLayoutEffect } from '../../../helpers'; + +export const isVariantIcon = (icon: any): icon is string => + ['success', 'danger', 'warning', 'info', 'custom'].includes(icon as string); + +export interface ModalBoxTitleProps { + /** Additional classes added to the modal box title. */ + className?: string; + /** Id of the modal box title. */ + id?: string; + /** Content rendered inside the modal box title. */ + title: React.ReactNode; + /** Optional alert icon (or other) to show before the title. When the predefined alert types + * are used the default styling will be automatically applied. */ + titleIconVariant?: 'success' | 'danger' | 'warning' | 'info' | 'custom' | React.ComponentType; + /** Optional title label text for screen readers. */ + titleLabel?: string; +} + +export const ModalBoxTitle: React.FunctionComponent = ({ + className, + id, + title, + titleIconVariant, + titleLabel, + ...props +}: ModalBoxTitleProps) => { + const [hasTooltip, setHasTooltip] = React.useState(false); + const h1 = React.useRef(null); + const label = titleLabel || (isVariantIcon(titleIconVariant) ? `${capitalize(titleIconVariant)} alert:` : titleLabel); + const variantIcons = { + success: , + danger: , + warning: , + info: , + custom: + }; + const CustomIcon = !isVariantIcon(titleIconVariant) && titleIconVariant; + + useIsomorphicLayoutEffect(() => { + setHasTooltip(h1.current && h1.current.offsetWidth < h1.current.scrollWidth); + }, []); + + const content = ( +

+ {titleIconVariant && ( + + {isVariantIcon(titleIconVariant) ? variantIcons[titleIconVariant] : } + + )} + {label && {label}} + {title} +

+ ); + + return hasTooltip ? {content} : content; +}; +ModalBoxTitle.displayName = 'ModalBoxTitle'; diff --git a/packages/react-core/src/next/components/Modal/ModalContent.tsx b/packages/react-core/src/next/components/Modal/ModalContent.tsx new file mode 100644 index 00000000000..2dbd1fdf45a --- /dev/null +++ b/packages/react-core/src/next/components/Modal/ModalContent.tsx @@ -0,0 +1,130 @@ +import * as React from 'react'; +import { FocusTrap } from '../../../helpers'; +import bullsEyeStyles from '@patternfly/react-styles/css/layouts/Bullseye/bullseye'; +import { css } from '@patternfly/react-styles'; +import { getOUIAProps, OUIAProps } from '../../../helpers'; +import { Backdrop } from '../../../components/Backdrop'; +import { ModalBoxCloseButton } from './ModalBoxCloseButton'; +import { ModalBox } from './ModalBox'; + +export interface ModalContentProps extends OUIAProps { + /** Id to use for the modal box description. This should match the ModalBoxHeader labelId or descriptorId. */ + 'aria-describedby'?: string; + /** Accessible descriptor of the modal. */ + 'aria-label'?: string; + /** Id to use for the modal box label. This should include the ModalBoxHeader labelId. */ + 'aria-labelledby'?: string; + /** Id of the modal box container. */ + boxId: string; + /** Content rendered inside the modal. */ + children: React.ReactNode; + /** Additional classes added to the modal box. */ + className?: string; + /** Flag to disable focus trap. */ + disableFocusTrap?: boolean; + /** The element to focus when the modal opens. By default the first + * focusable element will receive focus. + */ + elementToFocus?: HTMLElement | SVGElement | string; + /** Flag to show the modal. */ + isOpen?: boolean; + /** A callback for when the close button is clicked. */ + onClose?: (event: KeyboardEvent | React.MouseEvent) => void; + /** Position of the modal. By default a modal will be positioned vertically and horizontally centered. */ + position?: 'default' | 'top'; + /** Offset from alternate position. Can be any valid CSS length/percentage. */ + positionOffset?: string; + /** Flag to show the close button in the header area of the modal. */ + showClose?: boolean; + /** Variant of the modal. */ + variant?: 'small' | 'medium' | 'large' | 'default'; + /** Default width of the modal. */ + width?: number | string; + /** Maximum width of the modal. */ + maxWidth?: number | string; + /** Value to overwrite the randomly generated data-ouia-component-id.*/ + ouiaId?: number | string; + /** Set the value of data-ouia-safe. Only set to true when the component is in a static state, i.e. no animations are occurring. At all other times, this value must be false. */ + ouiaSafe?: boolean; +} + +export const ModalContent: React.FunctionComponent = ({ + children, + className, + isOpen = false, + 'aria-label': ariaLabel, + 'aria-describedby': ariaDescribedby, + 'aria-labelledby': ariaLabelledby, + showClose = true, + onClose = () => undefined as any, + variant = 'default', + position, + positionOffset, + width, + maxWidth, + boxId, + disableFocusTrap = false, + ouiaId, + ouiaSafe = true, + elementToFocus, + ...props +}: ModalContentProps) => { + if (!isOpen) { + return null; + } + + const ariaLabelledbyFormatted = (): string => { + const idRefList: string[] = []; + if (ariaLabel && boxId) { + idRefList.push(ariaLabel && boxId); + } + if (ariaLabelledby) { + idRefList.push(ariaLabelledby); + } + return idRefList.join(' '); + }; + + const modalBox = ( + + {showClose && onClose(event)} ouiaId={ouiaId} />} + {children} + + ); + return ( + + + {modalBox} + + + ); +}; +ModalContent.displayName = 'ModalContent'; diff --git a/packages/react-core/src/next/components/Modal/__tests__/Modal.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/Modal.test.tsx new file mode 100644 index 00000000000..d3664a5cc58 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/Modal.test.tsx @@ -0,0 +1,141 @@ +import * as React from 'react'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { css } from '../../../../../../react-styles/dist/js'; +import styles from '@patternfly/react-styles/css/components/Backdrop/backdrop'; + +import { Modal } from '../Modal'; +import { KeyTypes } from '../../../../helpers'; + +jest.spyOn(document, 'createElement'); +jest.spyOn(document.body, 'addEventListener'); + +const props = { + onClose: jest.fn(), + isOpen: false, + children: 'modal content' +}; + +const target = document.createElement('div'); + +const ModalWithSiblings = () => { + const [isOpen, setIsOpen] = React.useState(true); + const [isModalMounted, setIsModalMounted] = React.useState(true); + const modalProps = { ...props, isOpen, appendTo: target, onClose: () => setIsOpen(false) }; + + return ( + <> + +
Section sibling
+ {isModalMounted && ( + + + + )} + + ); +}; + +describe('Modal', () => { + test('Modal creates a container element once for div', () => { + render(); + expect(document.createElement).toHaveBeenCalledWith('div'); + }); + + test('modal closes with escape', async () => { + const user = userEvent.setup(); + + render(); + + await user.type(screen.getByLabelText("modal-div"), `{${KeyTypes.Escape}}`); + expect(props.onClose).toHaveBeenCalled(); + }); + + test('modal does not call onClose for esc key if it is not open', () => { + render(); + expect(screen.queryByRole('dialog')).toBeNull(); + expect(props.onClose).not.toHaveBeenCalled(); + }); + + test('modal has body backdropOpen class when open', () => { + render(); + expect(document.body).toHaveClass(css(styles.backdropOpen)); + }); + + test('modal has no body backdropOpen class when not open', () => { + render(); + expect(document.body).not.toHaveClass(css(styles.backdropOpen)); + }); + + test('modal shows the close button when showClose is true (true by default)', () => { + render(); + expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument(); + }); + + test('modal does not show the close button when showClose is false', () => { + render(); + expect(screen.queryByRole('button', { name: 'Close' })).toBeNull(); + }); + + test('modal generates console error when no accessible name is provided', () => { + const props = { + onClose: jest.fn(), + isOpen: true, + children: 'modal content' + }; + const consoleErrorMock = jest.fn(); + global.console = { error: consoleErrorMock } as any; + + render(); + + expect(consoleErrorMock).toHaveBeenCalled(); + }); + + test('modal adds aria-hidden attribute to its siblings when open', () => { + render(, { container: document.body.appendChild(target) }); + + const asideSibling = screen.getByRole('complementary', { hidden: true }); + const articleSibling = screen.getByRole('article', { hidden: true }); + + expect(asideSibling).toHaveAttribute('aria-hidden'); + expect(articleSibling).toHaveAttribute('aria-hidden'); + }); + + test('modal removes the aria-hidden attribute from its siblings when closed', async () => { + const user = userEvent.setup(); + + render(, { container: document.body.appendChild(target) }); + + const asideSibling = screen.getByRole('complementary', { hidden: true }); + const articleSibling = screen.getByRole('article', { hidden: true }); + const closeButton = screen.getByRole('button', { name: 'Close' }); + + expect(articleSibling).toHaveAttribute('aria-hidden'); + expect(asideSibling).toHaveAttribute('aria-hidden'); + + await user.click(closeButton); + + expect(articleSibling).not.toHaveAttribute('aria-hidden'); + expect(asideSibling).not.toHaveAttribute('aria-hidden'); + }); + + test('modal removes the aria-hidden attribute from its siblings when unmounted', async () => { + const user = userEvent.setup(); + + render(, { container: document.body.appendChild(target) }); + + const asideSibling = screen.getByRole('complementary', { hidden: true }); + const articleSibling = screen.getByRole('article', { hidden: true }); + const unmountButton = screen.getByRole('button', { name: 'Unmount Modal' }); + + expect(asideSibling).toHaveAttribute('aria-hidden'); + expect(articleSibling).toHaveAttribute('aria-hidden'); + + await user.click(unmountButton); + + expect(asideSibling).not.toHaveAttribute('aria-hidden'); + expect(articleSibling).not.toHaveAttribute('aria-hidden'); + }); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBox.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBox.test.tsx new file mode 100644 index 00000000000..9a8f8b24cad --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBox.test.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { ModalBox } from '../ModalBox'; + +test('ModalBox Test', () => { + const { asFragment } = render( + + This is a ModalBox + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBox Test large', () => { + const { asFragment } = render( + + This is a ModalBox + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBox Test medium', () => { + const { asFragment } = render( + + This is a ModalBox + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBox Test small', () => { + const { asFragment } = render( + + This is a ModalBox + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBox Test top aligned', () => { + const { asFragment } = render( + + This is a ModalBox + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBox Test top aligned distance', () => { + const { asFragment } = render( + + This is a ModalBox + + ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx new file mode 100644 index 00000000000..a45a5cf480a --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { ModalBoxBody } from '../ModalBoxBody'; + +describe('ModalBoxBody tests', () => { + test('ModalBoxBody renders', () => { + const { asFragment } = render( + + This is a ModalBox body + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('The modalBoxBody has the expected aria-label when it is passed', () => { + const props = { + isOpen: true + }; + + render( + + This is a ModalBox + + ); + + const modalBoxBody = screen.getByText('This is a ModalBox'); + expect(modalBoxBody).toHaveAccessibleName('modal box body aria label'); + }); + + test('The modalBoxBody has the expected aria role when bodyAriaLabel is passed and bodyAriaRole is not', () => { + const props = { + isOpen: true + }; + + render( + + This is a ModalBox + + ); + + const modalBoxBody = screen.getByRole('region', { name: 'modal box body aria label' }); + expect(modalBoxBody).toBeInTheDocument(); + }); + + test('The modalBoxBody has the expected aria role when bodyAriaRole is passed', () => { + const props = { + isOpen: true + }; + + render( + + This is a ModalBox + + ); + + const modalBoxBody = screen.getByRole('article', { name: 'modal box body aria label' }); + expect(modalBoxBody).toBeInTheDocument(); + }); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxCloseButton.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxCloseButton.test.tsx new file mode 100644 index 00000000000..13339dfd28f --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxCloseButton.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { ModalBoxCloseButton } from '../ModalBoxCloseButton'; + +describe('ModalBoxCloseButton', () => { + test('onClose called when clicked', async () => { + const onClose = jest.fn(); + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByRole('button')); + expect(onClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxDescription.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxDescription.test.tsx new file mode 100644 index 00000000000..8aca48ac935 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxDescription.test.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { ModalBoxDescription } from '../ModalBoxDescription'; + +test('ModalBoxDescription Test', () => { + const { asFragment } = render(This is a ModalBox Description); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxFooter.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxFooter.test.tsx new file mode 100644 index 00000000000..bf94f92dbf6 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxFooter.test.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { ModalBoxFooter } from '../ModalBoxFooter'; + +test('ModalBoxFooter Test', () => { + const { asFragment } = render( + This is a ModalBox Footer + ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxHeader.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxHeader.test.tsx new file mode 100644 index 00000000000..34a642718e5 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxHeader.test.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { ModalBoxHeader } from '../ModalBoxHeader'; + +test('ModalBoxHeader Test', () => { + const { asFragment } = render(This is a ModalBox header); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBoxHeader help renders', () => { + const { asFragment } = render(test}>This is a ModalBox header); + expect(asFragment()).toMatchSnapshot(); +}); + +test('Modal Test with custom header', () => { + const header = TEST; + + const { asFragment } = render( + + {header} + + ); + expect(asFragment()).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxTitle.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxTitle.test.tsx new file mode 100644 index 00000000000..fdda1eac2f8 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxTitle.test.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { ModalBoxTitle } from '../ModalBoxTitle'; +import BullhornIcon from '@patternfly/react-icons/dist/esm/icons/bullhorn-icon'; + +test('ModalBoxTitle alert variant', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBoxTitle info variant', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBoxTitle danger variant', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBoxTitle custom variant', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBoxTitle success variant', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('ModalBoxTitle custom icon variant', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalContent.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalContent.test.tsx new file mode 100644 index 00000000000..d9170b36b0e --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalContent.test.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { ModalContent } from '../ModalContent'; + +const modalContentProps = { + boxId: 'boxId', + labelId: 'labelId', + descriptorId: 'descriptorId' +}; +test('Modal Content Test only body', () => { + const { asFragment } = render( + + This is a ModalBox header + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('Modal Content Test isOpen', () => { + const { asFragment } = render( + + This is a ModalBox header + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('Modal Content Test description', () => { + const { asFragment } = render( + + This is a ModalBox header + + ); + expect(asFragment()).toMatchSnapshot(); +}); + +test('Modal Content Test with onclose', () => { + const { asFragment } = render( + undefined} + isOpen + {...modalContentProps} + > + This is a ModalBox header + + ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBox.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBox.test.tsx.snap new file mode 100644 index 00000000000..24d8d7a60f9 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBox.test.tsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalBox Test 1`] = ` + + + +`; + +exports[`ModalBox Test large 1`] = ` + + + +`; + +exports[`ModalBox Test medium 1`] = ` + + + +`; + +exports[`ModalBox Test small 1`] = ` + + + +`; + +exports[`ModalBox Test top aligned 1`] = ` + + + +`; + +exports[`ModalBox Test top aligned distance 1`] = ` + + + +`; diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxBody.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxBody.test.tsx.snap new file mode 100644 index 00000000000..a0820e8a994 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxBody.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalBoxBody tests ModalBoxBody renders 1`] = ` + +
+ This is a ModalBox body +
+
+`; diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxDescription.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxDescription.test.tsx.snap new file mode 100644 index 00000000000..f313d198539 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxDescription.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalBoxDescription Test 1`] = ` + +
+ This is a ModalBox Description +
+
+`; diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxFooter.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxFooter.test.tsx.snap new file mode 100644 index 00000000000..a0e7455ddf9 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxFooter.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalBoxFooter Test 1`] = ` + +
+ This is a ModalBox Footer +
+
+`; diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxHeader.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxHeader.test.tsx.snap new file mode 100644 index 00000000000..85d9c3e9012 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxHeader.test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Modal Test with custom header 1`] = ` + +
+ + TEST + +
+
+`; + +exports[`ModalBoxHeader Test 1`] = ` + +
+ This is a ModalBox header +
+
+`; + +exports[`ModalBoxHeader help renders 1`] = ` + +
+
+ This is a ModalBox header +
+
+
+ test +
+
+
+
+`; diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxTitle.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxTitle.test.tsx.snap new file mode 100644 index 00000000000..112406099e3 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxTitle.test.tsx.snap @@ -0,0 +1,218 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalBoxTitle alert variant 1`] = ` + +

+ + + + + Warning alert: + + + Test Modal Box warning + +

+
+`; + +exports[`ModalBoxTitle custom icon variant 1`] = ` + +

+ + + + + Test Modal Box custom + +

+
+`; + +exports[`ModalBoxTitle custom variant 1`] = ` + +

+ + + + + Custom alert: + + + Test Modal Box warning + +

+
+`; + +exports[`ModalBoxTitle danger variant 1`] = ` + +

+ + + + + Danger alert: + + + Test Modal Box danger + +

+
+`; + +exports[`ModalBoxTitle info variant 1`] = ` + +

+ + + + + Info alert: + + + Test Modal Box info + +

+
+`; + +exports[`ModalBoxTitle success variant 1`] = ` + +

+ + + + + Success alert: + + + Test Modal Box success + +

+
+`; diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap new file mode 100644 index 00000000000..8ddfbefa140 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap @@ -0,0 +1,209 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Modal Content Test description 1`] = ` + +
+
+ +
+
+
+`; + +exports[`Modal Content Test isOpen 1`] = ` + +
+
+ +
+
+
+`; + +exports[`Modal Content Test only body 1`] = ` + +
+
+ +
+
+
+`; + +exports[`Modal Content Test with onclose 1`] = ` + +
+
+ +
+
+
+`; diff --git a/packages/react-core/src/next/components/Modal/examples/Modal.md b/packages/react-core/src/next/components/Modal/examples/Modal.md new file mode 100644 index 00000000000..4e77affd5ea --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/Modal.md @@ -0,0 +1,157 @@ +--- +id: Modal +section: components +cssPrefix: pf-v5-c-modal-box +propComponents: ['Modal', 'ModalBoxBody', 'ModalBoxHeader', 'ModalBoxFooter'] +ouia: true +beta: true +--- + +import WarningTriangleIcon from '@patternfly/react-icons/dist/esm/icons/warning-triangle-icon'; +import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-icon'; +import BullhornIcon from '@patternfly/react-icons/dist/esm/icons/bullhorn-icon'; +import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; +import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; +import formStyles from '@patternfly/react-styles/css/components/Form/form'; + +## Examples + +### Basic modals + +Basic modals give users the option to either confirm or cancel an action. To flag an open modal, use the `isOpen` property. To execute a callback when a modal is closed, use the `onClose` property. + +```ts file="./ModalBasic.tsx" + +``` + +### Scrollable modals + +To enable keyboard-accessible scrolling of a modal’s content, pass `tabIndex={0}` to the ``. + +```ts file="ModalWithOverflowingContent.tsx" + +``` + +### With a static description + +To provide additional information about a modal, use the `description` property. Descriptions are static and do not scroll with other modal content. + +```ts file="./ModalWithDescription.tsx" + +``` + +### Top aligned + +To override a modal's default center alignment, use the `position` property. In this example, `position` is set to "top", which moves the modal to the top of the screen. + +```ts file="./ModalTopAligned.tsx" + +``` + +### Small modal + +To adjust the size of a modal, use the `variant` property. Modal variants include "small", "medium", "large", and "default". + +The following example displays a "small" modal by passing in `variant={ModalVariant.small}`. + +```ts file="./ModalSmall.tsx" + +``` + +### Medium modal + +The following example displays a "medium" modal by passing in `variant={ModalVariant.medium}`. + +```ts file="./ModalMedium.tsx" + +``` + +### Large modal + +The following example displays a "large" modal by passing in `variant={ModalVariant.large}`. + +```ts file="./ModalLarge.tsx" + +``` + +### Custom width + +To choose a specific width for a modal, use the `width` property. The following example has a `width` of "50%". + +```ts file="./ModalCustomWidth.tsx" + +``` + +### Custom header and footer + +To add a custom header and footer to a modal, set the `header` and `footer` properties to a custom implementation. The following example passes title components into both the header and the footer and also passes an icon to the footer. + +```ts file="./ModalCustomHeaderFooter.tsx" + +``` + +### No header or footer + +To exclusively present information in a modal, remove the `header` and/or `footer`. + +```ts file="./ModalNoHeaderFooter.tsx" + +``` + +### Title icon + +To add an icon before a modal’s title, use the `titleIconVariant`, which can be set to one of the predefined variants -- "success", "danger", "warning", "info", and "custom" -- or to an imported custom icon. The following example uses a "warning" variant. + +```ts file="./ModalTitleIcon.tsx" + +``` + +### Custom title icon + +To add a custom icon before a modal’s title, set `titleIconVariant` to an imported custom icon. The following example imports and uses a bullhorn icon. + +```ts file="./ModalCustomTitleIcon.tsx" + +``` + +### With wizard + +To guide users through a series of steps in a modal, you can add a [wizard](/components/wizard) to a modal. To configure the ``, pass an array that contains a “name” and “component” value for each step into the `steps` property. + +```ts file="./ModalWithWizard.tsx" + +``` + +### With dropdown + +To present a menu of actions or links to a user, you can add a [dropdown](/components/dropdown) to a modal. To allow the dropdown to visually break out of the modal container, set the `menuAppendTo` property to “parent”. Handle the modal’s closing behavior by listening to the `onEscapePress` callback on the `` component. This allows the "escape" key to collapse the dropdown without closing the entire modal. + +```ts file="./ModalWithDropdown.tsx" + +``` + +### With help + +To help simplify and explain complex models, add a help [popover](/components/popover). Only place a help icon at the modal level if its information applies to all content in the modal. If the help popover is specific to a particular modal section, place the help icon beside that section instead. + +```ts file="./ModalWithHelp.tsx" + +``` + +### With form + +To collect user input within a modal, you can add a [form](/components/form). + +To submit the form from a button in the modal's footer (outside of the `
`), set the button's `form` property equal to the form's id. + +```ts file="ModalWithForm.tsx" + +``` + +### Custom focus + +Use the `elementToFocus` property to customize which element inside the Modal receives focus when initially opened. + +```ts file="./ModalCustomFocus.tsx" + +``` diff --git a/packages/react-core/src/next/components/Modal/examples/ModalBasic.tsx b/packages/react-core/src/next/components/Modal/examples/ModalBasic.tsx new file mode 100644 index 00000000000..d7b696fc493 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalBasic.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader } from '@patternfly/react-core/next'; + +export const ModalBasic: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalCustomFocus.tsx b/packages/react-core/src/next/components/Modal/examples/ModalCustomFocus.tsx new file mode 100644 index 00000000000..9dad6df2ddc --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalCustomFocus.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader } from '@patternfly/react-core/next'; + +export const ModalCustomFocus: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalCustomHeaderFooter.tsx b/packages/react-core/src/next/components/Modal/examples/ModalCustomHeaderFooter.tsx new file mode 100644 index 00000000000..63f142c408b --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalCustomHeaderFooter.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Button, Title, TitleSizes } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader, ModalVariant } from '@patternfly/react-core/next'; + +import WarningTriangleIcon from '@patternfly/react-icons/dist/esm/icons/warning-triangle-icon'; +import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; + +export const ModalCustomHeaderFooter: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + Custom header/footer modal + +

Allows for custom content in the header and/or footer by passing components.

+
+ + + When static text describing the modal is available outside of the modal header, it can be given an ID that + is then passed in as the modal's aria-describedby value. + +
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +
+ + + <WarningTriangleIcon /> + <span className={spacing.plSm}>Custom modal footer.</span> + + +
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalCustomTitleIcon.tsx b/packages/react-core/src/next/components/Modal/examples/ModalCustomTitleIcon.tsx new file mode 100644 index 00000000000..2eaa45e41be --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalCustomTitleIcon.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader } from '@patternfly/react-core/next'; + +import BullhornIcon from '@patternfly/react-icons/dist/esm/icons/bullhorn-icon'; + +export const ModalCustomTitleIcon: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + + When static text describing the modal is available outside of the modal header, it can be given an ID that + is then passed in as the modal's aria-describedby value. + +
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +
+ + + + +
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalCustomWidth.tsx b/packages/react-core/src/next/components/Modal/examples/ModalCustomWidth.tsx new file mode 100644 index 00000000000..87ef1fbfedb --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalCustomWidth.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader } from '@patternfly/react-core/next'; + +export const ModalCustomWidth: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalLarge.tsx b/packages/react-core/src/next/components/Modal/examples/ModalLarge.tsx new file mode 100644 index 00000000000..043b84fcb9e --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalLarge.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader, ModalVariant } from '@patternfly/react-core/next'; + +export const ModalLarge: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalMedium.tsx b/packages/react-core/src/next/components/Modal/examples/ModalMedium.tsx new file mode 100644 index 00000000000..f696beed61b --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalMedium.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader, ModalVariant } from '@patternfly/react-core/next'; + +export const ModalMedium: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalNoHeaderFooter.tsx b/packages/react-core/src/next/components/Modal/examples/ModalNoHeaderFooter.tsx new file mode 100644 index 00000000000..5e9e0da2d2b --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalNoHeaderFooter.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalVariant } from '@patternfly/react-core/next'; + +export const ModalNoHeaderFooter: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + When static text describing the modal is available outside of the modal header, it can be given an ID that + is then passed in as the modal's aria-describedby value. + +
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +
+
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalSmall.tsx b/packages/react-core/src/next/components/Modal/examples/ModalSmall.tsx new file mode 100644 index 00000000000..e6441758a6d --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalSmall.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader, ModalVariant } from '@patternfly/react-core/next'; + +export const ModalSmall: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalTitleIcon.tsx b/packages/react-core/src/next/components/Modal/examples/ModalTitleIcon.tsx new file mode 100644 index 00000000000..4f19c3ef35f --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalTitleIcon.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader } from '@patternfly/react-core/next'; + +export const ModalTitleIcon: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + + When static text describing the modal is available outside of the modal header, it can be given an ID that + is then passed in as the modal's aria-describedby value. + +
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +
+ + + + +
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalTopAligned.tsx b/packages/react-core/src/next/components/Modal/examples/ModalTopAligned.tsx new file mode 100644 index 00000000000..244b9c08f9c --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalTopAligned.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader } from '@patternfly/react-core/next'; + +export const ModalTopAligned: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalWithDescription.tsx b/packages/react-core/src/next/components/Modal/examples/ModalWithDescription.tsx new file mode 100644 index 00000000000..ef3445e0b11 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalWithDescription.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxHeader, ModalBoxFooter } from '@patternfly/react-core/next'; + +export const ModalWithDescription: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalWithDropdown.tsx b/packages/react-core/src/next/components/Modal/examples/ModalWithDropdown.tsx new file mode 100644 index 00000000000..fc402d629b6 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalWithDropdown.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { Button, Dropdown, DropdownList, DropdownItem, MenuToggle, MenuToggleElement } from '@patternfly/react-core'; + +import { Modal, ModalBoxBody, ModalBoxHeader, ModalBoxFooter, ModalVariant } from '@patternfly/react-core/next'; + +export const ModalWithDropdown: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + const [isDropdownOpen, setIsDropdownOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + setIsDropdownOpen(false); + }; + + const handleDropdownToggle = () => { + setIsDropdownOpen(!isDropdownOpen); + }; + + const onSelect = () => { + setIsDropdownOpen(!isDropdownOpen); + onFocus(); + }; + + const onFocus = () => { + const element = document.getElementById('modal-dropdown-toggle'); + (element as HTMLElement).focus(); + }; + + const onEscapePress = (event: KeyboardEvent) => { + if (isDropdownOpen) { + setIsDropdownOpen(!isDropdownOpen); + onFocus(); + } else { + handleModalToggle(event); + } + }; + + return ( + + + + + +
+ Set the dropdown menuAppendTo prop to parent in order to allow the dropdown menu + break out of the modal container. You'll also want to handle closing of the modal yourself, by listening to + the onEscapePress callback on the Modal component, so you can close the Dropdown first if + it's open without closing the entire modal. +
+
+
+ setIsDropdownOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + + Dropdown + + )} + > + + + Action + + ev.preventDefault()} + > + Link + + + Disabled Action + + + Disabled Link + + + +
+
+ + + + +
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalWithForm.tsx b/packages/react-core/src/next/components/Modal/examples/ModalWithForm.tsx new file mode 100644 index 00000000000..5fb16a1a339 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalWithForm.tsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { Button, Form, FormGroup, Popover, TextInput } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader, ModalVariant } from '@patternfly/react-core/next'; +import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; +import formStyles from '@patternfly/react-styles/css/components/Form/form'; + +export const ModalWithForm: React.FunctionComponent = () => { + const [isModalOpen, setModalOpen] = React.useState(false); + const [nameValue, setNameValue] = React.useState(''); + const [emailValue, setEmailValue] = React.useState(''); + const [addressValue, setAddressValue] = React.useState(''); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setModalOpen(!isModalOpen); + }; + + const handleNameInputChange = (_event, value: string) => { + setNameValue(value); + }; + + const handleEmailInputChange = (_event, value: string) => { + setEmailValue(value); + }; + const handleAddressInputChange = (_event, value: string) => { + setAddressValue(value); + }; + + return ( + + + + + + + + The + + name + + of a + + Person + + + } + bodyContent={ +
+ Often composed of + + givenName + + and + + familyName + + . +
+ } + > + + + } + isRequired + fieldId="modal-with-form-form-name" + > + +
+ + The + + e-mail + + of a + + person + + + } + bodyContent={ +
+ Valid + + e-mail + + address. +
+ } + > + + + } + isRequired + fieldId="modal-with-form-form-email" + > + +
+ + The + + adress + + of a + + person + + + } + bodyContent={ + + } + > + + + } + isRequired + fieldId="modal-with-form-form-address" + > + + + +
+ + + + +
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalWithHelp.tsx b/packages/react-core/src/next/components/Modal/examples/ModalWithHelp.tsx new file mode 100644 index 00000000000..b3118e1a0b3 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalWithHelp.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Button, Popover } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader } from '@patternfly/react-core/next'; +import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; + +export const ModalWithHelp: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen(!isModalOpen); + }; + + return ( + + + + Help Popover} + bodyContent={ +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id feugiat augue, nec fringilla + turpis. +
+ } + footerContent="Popover Footer" + > + + + } + /> + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + +
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalWithOverflowingContent.tsx b/packages/react-core/src/next/components/Modal/examples/ModalWithOverflowingContent.tsx new file mode 100644 index 00000000000..dab58867e42 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalWithOverflowingContent.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import { Modal, ModalBoxBody, ModalBoxHeader, ModalBoxFooter, ModalVariant } from '@patternfly/react-core/next'; + +export const ModalWithOverflowingContent: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }; + + return ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Quis eleifend quam adipiscing vitae proin sagittis nisl rhoncus. Semper auctor neque vitae + tempus. Diam donec adipiscing tristique risus. Augue eget arcu dictum varius duis. Ut enim blandit volutpat + maecenas volutpat blandit aliquam. Sit amet mauris commodo quis imperdiet massa tincidunt. Habitant morbi + tristique senectus et netus. Fames ac turpis egestas sed tempus urna. Neque laoreet suspendisse interdum + consectetur libero id. Volutpat lacus laoreet non curabitur gravida arcu ac tortor. Porta nibh venenatis cras + sed felis eget velit. Nullam non nisi est sit amet facilisis. Nunc mi ipsum faucibus vitae. Lorem sed risus + ultricies tristique nulla aliquet enim tortor at. Egestas sed tempus urna et pharetra pharetra massa massa + ultricies. Lacinia quis vel eros donec ac odio tempor orci. Malesuada fames ac turpis egestas integer eget + aliquet. +
+
+ Neque aliquam vestibulum morbi blandit cursus risus at ultrices. Molestie at elementum eu facilisis sed odio + morbi. Elit pellentesque habitant morbi tristique. Consequat nisl vel pretium lectus quam id leo in vitae. + Quis varius quam quisque id diam vel quam elementum. Viverra nam libero justo laoreet sit amet cursus. + Sollicitudin tempor id eu nisl nunc. Orci nulla pellentesque dignissim enim sit amet venenatis. Dignissim enim + sit amet venenatis urna cursus eget. Iaculis at erat pellentesque adipiscing commodo elit. Faucibus pulvinar + elementum integer enim neque volutpat. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Nunc mattis + enim ut tellus elementum sagittis vitae. Blandit cursus risus at ultrices. Tellus mauris a diam maecenas sed + enim. Non diam phasellus vestibulum lorem sed risus ultricies tristique nulla. +
+
+ Nulla pharetra diam sit amet nisl suscipit adipiscing. Ac tortor vitae purus faucibus ornare suspendisse sed + nisi. Sed felis eget velit aliquet sagittis id consectetur purus. Tincidunt tortor aliquam nulla facilisi cras + fermentum. Volutpat est velit egestas dui id ornare arcu odio. Pharetra magna ac placerat vestibulum. Ultrices + sagittis orci a scelerisque purus semper eget duis at. Nisi est sit amet facilisis magna etiam tempor orci eu. + Convallis tellus id interdum velit. Facilisis sed odio morbi quis commodo odio aenean sed. +
+
+ Eu scelerisque felis imperdiet proin fermentum leo vel orci porta. Facilisi etiam dignissim diam quis enim + lobortis scelerisque fermentum. Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada. Magna + etiam tempor orci eu lobortis elementum. Quis auctor elit sed vulputate mi sit. Eleifend quam adipiscing vitae + proin sagittis nisl rhoncus mattis rhoncus. Erat velit scelerisque in dictum non. Sit amet nulla facilisi + morbi tempus iaculis urna. Enim ut tellus elementum sagittis vitae et leo duis ut. Lectus arcu bibendum at + varius vel pharetra vel turpis. Morbi tristique senectus et netus et. Eget aliquet nibh praesent tristique + magna sit amet purus gravida. Nisl purus in mollis nunc sed id semper risus. Id neque aliquam vestibulum + morbi. Mauris a diam maecenas sed enim ut sem. Egestas tellus rutrum tellus pellentesque. +
+ + + + +
+
+ ); +}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalWithWizard.tsx b/packages/react-core/src/next/components/Modal/examples/ModalWithWizard.tsx new file mode 100644 index 00000000000..93eac234229 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/examples/ModalWithWizard.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Button, Wizard, WizardHeader, WizardStep } from '@patternfly/react-core'; +import { Modal, ModalVariant } from '@patternfly/react-core/next'; + +export const ModalWithWizard: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }; + + const handleWizardToggle = () => { + setIsModalOpen((prevIsModalOpen) => !prevIsModalOpen); + }; + + const numberedSteps = [1, 2, 3, 4].map((stepNumber) => ( + + {`Step ${stepNumber}`} + + )); + + return ( + + + + + } + onClose={handleWizardToggle} + > + {numberedSteps} + + Review step + + + + + ); +}; diff --git a/packages/react-core/src/next/components/Modal/index.ts b/packages/react-core/src/next/components/Modal/index.ts new file mode 100644 index 00000000000..2958403bbb0 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/index.ts @@ -0,0 +1,4 @@ +export * from './Modal'; +export * from './ModalBoxBody'; +export * from './ModalBoxHeader'; +export * from './ModalBoxFooter'; diff --git a/packages/react-core/src/next/components/index.ts b/packages/react-core/src/next/components/index.ts index b13bb4eb87d..cb89ee17889 100644 --- a/packages/react-core/src/next/components/index.ts +++ b/packages/react-core/src/next/components/index.ts @@ -1 +1 @@ -export * from './'; +export * from './Modal'; From dac87ea8cc51ee5c455948442bae32516ad51340 Mon Sep 17 00:00:00 2001 From: Titani Date: Mon, 4 Dec 2023 11:56:44 -0500 Subject: [PATCH 2/6] upodate for failing tests --- packages/react-core/src/next/components/Modal/ModalBox.tsx | 4 ++-- .../react-core/src/next/components/Modal/ModalContent.tsx | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/react-core/src/next/components/Modal/ModalBox.tsx b/packages/react-core/src/next/components/Modal/ModalBox.tsx index 40b9b2fac6c..a4a5bc207a9 100644 --- a/packages/react-core/src/next/components/Modal/ModalBox.tsx +++ b/packages/react-core/src/next/components/Modal/ModalBox.tsx @@ -41,8 +41,8 @@ export const ModalBox: React.FunctionComponent = ({ return (
= ({ if (ariaLabelledby) { idRefList.push(ariaLabelledby); } - return idRefList.join(' '); + if (idRefList.length === 0) { + return undefined; + } else { + return idRefList.join(' '); + } }; const modalBox = ( From b0715377b26b8c04b0ef3a6883f19b3ed56e4143 Mon Sep 17 00:00:00 2001 From: Titani Date: Mon, 4 Dec 2023 16:25:49 -0500 Subject: [PATCH 3/6] add integration test --- .../cypress/integration/modalnext.spec.ts | 106 +++ .../demo-app-ts/src/Demos.ts | 5 + .../demos/ModalNextDemo/ModalNextDemo.tsx | 622 ++++++++++++++++++ .../demo-app-ts/src/components/demos/index.ts | 1 + 4 files changed, 734 insertions(+) create mode 100644 packages/react-integration/cypress/integration/modalnext.spec.ts create mode 100644 packages/react-integration/demo-app-ts/src/components/demos/ModalNextDemo/ModalNextDemo.tsx diff --git a/packages/react-integration/cypress/integration/modalnext.spec.ts b/packages/react-integration/cypress/integration/modalnext.spec.ts new file mode 100644 index 00000000000..55627209328 --- /dev/null +++ b/packages/react-integration/cypress/integration/modalnext.spec.ts @@ -0,0 +1,106 @@ +describe('Modal Test', () => { + it('Navigate to Modal next section', () => { + cy.visit('http://localhost:3000/modal-next-demo-nav-link'); + }); + + it('Verify Half Width Modal', () => { + cy.get('#showHalfWidthModalButton').then((modalButton: JQuery) => { + cy.wrap(modalButton).click(); + + cy.get('.pf-v5-c-page').then((page: JQuery) => { + cy.get('.pf-v5-c-modal-box') + .then(() => { + cy.get('.pf-v5-c-modal-box').should('have.css', 'width', `${page.width() / 2}px`); + cy.get('.pf-v5-c-modal-box .pf-v5-c-button[aria-label="Close"]').then((closeButton) => { + cy.wrap(closeButton).click(); + cy.get('.pf-v5-c-modal-box').should('not.exist'); + }); + }) + .then(() => { + cy.wrap(modalButton).click(); + cy.get('.pf-v5-c-modal-box').should('exist'); + cy.get('body').type('{esc}'); + cy.get('.pf-v5-c-modal-box').should('not.exist'); + }); + }); + }); + }); + + it('Verify Custom Escape Press Modal', () => { + cy.get('#showCustomEscapeModalButton.customEscapePressed').should('not.exist'); + cy.get('#showCustomEscapeModalButton').then((modalButton: JQuery) => { + cy.wrap(modalButton).click(); + cy.get('.pf-v5-c-modal-box').should('exist'); + cy.get('.pf-v5-c-modal-box') + .then(() => { + cy.get('.pf-v5-c-modal-box .pf-v5-c-button[aria-label="Close"]').then((closeButton) => { + cy.wrap(closeButton).click(); + cy.get('.pf-v5-c-modal-box').should('not.exist'); + cy.get('#showCustomEscapeModalButton.customEscapePressed').should('not.exist'); + }); + }) + .then(() => { + cy.wrap(modalButton).click(); + cy.get('.pf-v5-c-modal-box').should('exist'); + cy.get('body').type('{esc}'); + cy.get('.pf-v5-c-modal-box').should('not.exist'); + cy.get('#showCustomEscapeModalButton.customEscapePressed').should('exist'); + }); + }); + }); + + it('Verify focustrap for basic modal', () => { + cy.get('#tabstop-test').focus(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + cy.tab().click(); // click first btn to open first modal + cy.focused().should('have.attr', 'aria-label', 'Close'); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + cy.tab(); + cy.focused().should('have.attr', 'data-id', 'modal-01-confirm-btn'); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + cy.tab(); + cy.focused().should('have.attr', 'data-id', 'modal-01-cancel-btn'); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + cy.tab(); + cy.focused().should('have.attr', 'aria-label', 'Close'); + cy.focused().click(); + }); + + it('Verify escape key closes modal', () => { + cy.get('#tabstop-test').focus(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + cy.tab().tab().click(); // open second modal + + cy.get('.pf-v5-c-modal-box').should('exist'); + // press escape key + cy.get('body').type('{esc}'); + cy.get('.pf-v5-c-modal-box').should('not.exist'); + }); + + it('Verify first focusable element receives focus by default', () => { + cy.get('#showDefaultModalButton').click(); + cy.get('.pf-v5-c-modal-box__close > .pf-v5-c-button.pf-m-plain').should('have.focus'); + cy.get('.pf-v5-c-modal-box__close > .pf-v5-c-button.pf-m-plain').click(); + }); + + it('Verify custom element receives focus', () => { + cy.get('#showCustomFocusModalButton').click(); + cy.get('#modal-custom-focus-confirm-button').should('have.focus'); + cy.get('#modal-custom-focus-cancel-button').click(); + }); + + it("Verify the same id doesn't appear multiple times", () => { + cy.get('#showDescriptionModalButton').click(); + + cy.get('body').find('div#test-modal-id').should('have.length', 1); + + cy.get('.pf-v5-c-modal-box__close > .pf-v5-c-button.pf-m-plain').click(); + }); +}); diff --git a/packages/react-integration/demo-app-ts/src/Demos.ts b/packages/react-integration/demo-app-ts/src/Demos.ts index 638f4410a72..07d7536bd54 100644 --- a/packages/react-integration/demo-app-ts/src/Demos.ts +++ b/packages/react-integration/demo-app-ts/src/Demos.ts @@ -242,6 +242,11 @@ export const Demos: DemoInterface[] = [ name: 'Modal Demo', componentType: Examples.ModalDemo }, + { + id: 'modal-next-demo', + name: 'Modal Next Demo', + componentType: Examples.ModalNextDemo + }, { id: 'nav-demo', name: 'Nav Demo', diff --git a/packages/react-integration/demo-app-ts/src/components/demos/ModalNextDemo/ModalNextDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/ModalNextDemo/ModalNextDemo.tsx new file mode 100644 index 00000000000..f7b382e9781 --- /dev/null +++ b/packages/react-integration/demo-app-ts/src/components/demos/ModalNextDemo/ModalNextDemo.tsx @@ -0,0 +1,622 @@ +import React from 'react'; +import { Button, Title, TitleSizes } from '@patternfly/react-core'; +import { Modal, ModalBoxHeader, ModalBoxBody, ModalBoxFooter, ModalVariant } from '@patternfly/react-core/next'; +import WarningTriangleIcon from '@patternfly/react-icons/dist/esm/icons/warning-triangle-icon'; +import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; + +interface ModalDemoState { + isModalOpen: boolean; + isModalDescriptionOpen: boolean; + isHelpModalOpen: boolean; + isSmallModalOpen: boolean; + isMediumModalOpen: boolean; + isLargeModalOpen: boolean; + isHalfWidthModalOpen: boolean; + isCustomHeaderFooterModalOpen: boolean; + isNoHeaderModalOpen: boolean; + isModalCustomEscapeOpen: boolean; + isModalAlertVariantOpen: boolean; + customEscapePressed: boolean; + isCustomFocusModalOpen: boolean; +} + +// eslint-disable-next-line patternfly-react/no-anonymous-functions +export class ModalNextDemo extends React.Component, ModalDemoState> { + static displayName = 'ModalDemo'; + + state = { + isModalOpen: false, + isModalDescriptionOpen: false, + isHelpModalOpen: false, + isSmallModalOpen: false, + isMediumModalOpen: false, + isLargeModalOpen: false, + isHalfWidthModalOpen: false, + isCustomHeaderFooterModalOpen: false, + isNoHeaderModalOpen: false, + isModalCustomEscapeOpen: false, + isModalAlertVariantOpen: false, + customEscapePressed: false, + isCustomFocusModalOpen: false + }; + + handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { + this.setState(({ isModalOpen }) => ({ + isModalOpen: !isModalOpen + })); + }; + + handleModalDescriptionToggle = () => { + this.setState(({ isModalDescriptionOpen }) => ({ + isModalDescriptionOpen: !isModalDescriptionOpen + })); + }; + + handleSmallModalToggle = () => { + this.setState(({ isSmallModalOpen }) => ({ + isSmallModalOpen: !isSmallModalOpen + })); + }; + + handleHelpModalToggle = () => { + this.setState(({ isHelpModalOpen }) => ({ + isHelpModalOpen: !isHelpModalOpen + })); + }; + + handleMediumModalToggle = () => { + this.setState(({ isMediumModalOpen }) => ({ + isMediumModalOpen: !isMediumModalOpen + })); + }; + + handleLargeModalToggle = () => { + this.setState(({ isLargeModalOpen }) => ({ + isLargeModalOpen: !isLargeModalOpen + })); + }; + + handleHalfWidthModalToggle = () => { + this.setState(({ isHalfWidthModalOpen }) => ({ + isHalfWidthModalOpen: !isHalfWidthModalOpen + })); + }; + + handleCustomHeaderFooterModalToggle = () => { + this.setState(({ isCustomHeaderFooterModalOpen }) => ({ + isCustomHeaderFooterModalOpen: !isCustomHeaderFooterModalOpen + })); + }; + + handleNoHeaderModalToggle = () => { + this.setState(({ isNoHeaderModalOpen }) => ({ + isNoHeaderModalOpen: !isNoHeaderModalOpen + })); + }; + + handleModalCustomEscapeToggle = (event?: any, customEscapePressed?: boolean) => { + this.setState(({ isModalCustomEscapeOpen }) => ({ + isModalCustomEscapeOpen: !isModalCustomEscapeOpen, + customEscapePressed + })); + }; + + handleModalAlertVariantToggle = (event?: any, customEscapePressed?: boolean) => { + this.setState(({ isModalAlertVariantOpen }) => ({ + isModalAlertVariantOpen: !isModalAlertVariantOpen, + customEscapePressed + })); + }; + + handleCustomFocusModalToggle = () => { + this.setState(({ isCustomFocusModalOpen }) => ({ + isCustomFocusModalOpen: !isCustomFocusModalOpen + })); + }; + + componentDidMount() { + window.scrollTo(0, 0); + } + + renderModal() { + const { isModalOpen } = this.state; + + return ( + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderModalWithDescription() { + const { isModalDescriptionOpen } = this.state; + + return ( + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderSmallModal() { + const { isSmallModalOpen } = this.state; + + return ( + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderMediumModal() { + const { isMediumModalOpen } = this.state; + + return ( + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderLargeModal() { + const { isLargeModalOpen } = this.state; + + return ( + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderHalfWidthModal() { + const { isHalfWidthModalOpen } = this.state; + + return ( + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderCustomHeaderFooterModal() { + const { isCustomHeaderFooterModalOpen } = this.state; + + return ( + + + + Custom Modal Header/Footer + +

+ Allows for custom content in the header and/or footer by passing components. +

+
+ + + When static text describing the modal is available, it can be wrapped with an ID referring to the modal's + aria-describedby value. + +
+
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
+ + + <WarningTriangleIcon /> + <span className={spacing.plSm}>Custom modal footer.</span> + + +
+ ); + } + + renderNoHeaderModal() { + const { isNoHeaderModalOpen } = this.state; + + return ( + + + + When static text describing the modal is available, it can be wrapped with an ID referring to the modal's + aria-describedby value. + {' '} + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + + + + ); + } + + renderModalWithCustomEscape() { + const { isModalCustomEscapeOpen } = this.state; + + return ( + this.handleModalCustomEscapeToggle(event, true)} + aria-labelledby="custom-escape-modal-title" + > + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderModalWithAlertVariant() { + const { isModalAlertVariantOpen } = this.state; + + return ( + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderHelpModal() { + const { isHelpModalOpen } = this.state; + + return ( + + Help} /> + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + renderCustomFocusModal() { + const { isCustomFocusModalOpen } = this.state; + + return ( + + Help} + /> + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + ); + } + + render() { + const buttonStyle = { + marginRight: 20, + marginBottom: 20 + }; + + return ( + +
+ + + + + + + + + + + + +
+ {this.renderModal()} + {this.renderSmallModal()} + {this.renderMediumModal()} + {this.renderLargeModal()} + {this.renderHalfWidthModal()} + {this.renderCustomHeaderFooterModal()} + {this.renderNoHeaderModal()} + {this.renderModalWithDescription()} + {this.renderModalWithCustomEscape()} + {this.renderModalWithAlertVariant()} + {this.renderHelpModal()} + {this.renderCustomFocusModal()} +
+ ); + } +} diff --git a/packages/react-integration/demo-app-ts/src/components/demos/index.ts b/packages/react-integration/demo-app-ts/src/components/demos/index.ts index 2eadd9a9609..c879328ac09 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/index.ts +++ b/packages/react-integration/demo-app-ts/src/components/demos/index.ts @@ -44,6 +44,7 @@ export * from './MastheadDemo/MastheadDemo'; export * from './MenuDemo/MenuDemo'; export * from './MenuDemo/MenuDrilldownDemo'; export * from './ModalDemo/ModalDemo'; +export * from './ModalNextDemo/ModalNextDemo'; export * from './NavDemo/NavDemo'; export * from './NotificationBadgeDemo/NotificationBadgeDemo'; export * from './NotificationDrawerDemo/NotificationDrawerBasicDemo'; From aefc469539aca76ce1c59f7fa1330499b20e489e Mon Sep 17 00:00:00 2001 From: Titani Date: Mon, 11 Dec 2023 18:32:30 -0500 Subject: [PATCH 4/6] updates from review --- .../src/next/components/Modal/Modal.tsx | 8 +- .../src/next/components/Modal/ModalBox.tsx | 2 +- .../next/components/Modal/ModalBoxBody.tsx | 17 ++-- .../components/Modal/ModalBoxCloseButton.tsx | 2 +- .../next/components/Modal/ModalBoxHeader.tsx | 13 ++- .../next/components/Modal/ModalBoxTitle.tsx | 8 +- .../next/components/Modal/ModalContent.tsx | 7 +- .../components/Modal/__tests__/Modal.test.tsx | 6 +- .../Modal/__tests__/ModalBoxBody.test.tsx | 4 +- .../Modal/__tests__/ModalContent.test.tsx | 3 +- .../__snapshots__/ModalContent.test.tsx.snap | 83 +------------------ .../next/components/Modal/examples/Modal.md | 30 ++----- ...HeaderFooter.tsx => ModalCustomHeader.tsx} | 4 +- .../components/Modal/examples/ModalLarge.tsx | 43 ---------- .../{ModalMedium.tsx => ModalSize.tsx} | 36 ++++++-- .../components/Modal/examples/ModalSmall.tsx | 44 ---------- .../Modal/examples/ModalWithDescription.tsx | 45 ++++++++-- .../Modal/examples/ModalWithWizard.tsx | 1 - 18 files changed, 111 insertions(+), 245 deletions(-) rename packages/react-core/src/next/components/Modal/examples/{ModalCustomHeaderFooter.tsx => ModalCustomHeader.tsx} (93%) delete mode 100644 packages/react-core/src/next/components/Modal/examples/ModalLarge.tsx rename packages/react-core/src/next/components/Modal/examples/{ModalMedium.tsx => ModalSize.tsx} (54%) delete mode 100644 packages/react-core/src/next/components/Modal/examples/ModalSmall.tsx diff --git a/packages/react-core/src/next/components/Modal/Modal.tsx b/packages/react-core/src/next/components/Modal/Modal.tsx index e6708dbcb09..07eb94a4a62 100644 --- a/packages/react-core/src/next/components/Modal/Modal.tsx +++ b/packages/react-core/src/next/components/Modal/Modal.tsx @@ -11,7 +11,7 @@ export interface ModalProps extends React.HTMLProps, OUIAProps { appendTo?: HTMLElement | (() => HTMLElement); /** Id to use for the modal box description. This should match the ModalBoxHeader labelId or descriptorId. */ 'aria-describedby'?: string; - /** Accessible descriptor of the modal. */ + /** Adds an accessible name to the modal when there is no title in the ModalBoxHeader. */ 'aria-label'?: string; /** Id to use for the modal box label. This should include the ModalBoxHeader labelId. */ 'aria-labelledby'?: string; @@ -29,7 +29,7 @@ export interface ModalProps extends React.HTMLProps, OUIAProps { id?: string; /** Flag to show the modal. */ isOpen?: boolean; - /** A callback for when the close button is clicked. */ + /** Add callback for when the close button is clicked. This prop needs to be passed to render the close button */ onClose?: (event: KeyboardEvent | React.MouseEvent) => void; /** Modal handles pressing of the escape key and closes the modal. If you want to handle * this yourself you can use this callback function. */ @@ -38,8 +38,6 @@ export interface ModalProps extends React.HTMLProps, OUIAProps { position?: 'default' | 'top'; /** Offset from alternate position. Can be any valid CSS length/percentage. */ positionOffset?: string; - /** Flag to show the close button in the header area of the modal. */ - showClose?: boolean; /** Variant of the modal. */ variant?: 'small' | 'medium' | 'large' | 'default'; /** Default width of the modal. */ @@ -71,8 +69,6 @@ class Modal extends React.Component { static defaultProps: PickOptional = { isOpen: false, - showClose: true, - onClose: () => undefined as any, variant: 'default', appendTo: () => document.body, ouiaSafe: true, diff --git a/packages/react-core/src/next/components/Modal/ModalBox.tsx b/packages/react-core/src/next/components/Modal/ModalBox.tsx index a4a5bc207a9..f59fed5e8a0 100644 --- a/packages/react-core/src/next/components/Modal/ModalBox.tsx +++ b/packages/react-core/src/next/components/Modal/ModalBox.tsx @@ -6,7 +6,7 @@ import topSpacer from '@patternfly/react-tokens/dist/esm/c_modal_box_m_align_top export interface ModalBoxProps extends React.HTMLProps { /** Id to use for the modal box description. This should match the ModalBoxHeader labelId or descriptorId */ 'aria-describedby'?: string; - /** Accessible descriptor of the modal. */ + /** Adds an accessible name to the modal when there is no title in the ModalBoxHeader. */ 'aria-label'?: string; /** Id to use for the modal box label. */ 'aria-labelledby'?: string; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx b/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx index 5c3ee16ba95..14ba20800ed 100644 --- a/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx +++ b/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx @@ -8,34 +8,29 @@ export interface ModalBoxBodyProps extends React.HTMLProps { /** Additional classes added to the modal box body. */ className?: string; /** Accessible label applied to the modal box body. This should be used to communicate - * important information about the modal box body div element if needed, such as that it - * is scrollable. + * important information about the modal box body div element if needed, such as when it is scrollable. */ 'aria-label'?: string; /** Accessible role applied to the modal box body. This will default to "region" if the - * bodyAriaLabel property is passed in. Set to a more appropriate role as applicable + * aria-label property is passed in. Set to a more appropriate role as applicable * based on the modal content and context. */ - 'aria-role'?: string; - /** Id of the modal box body. This should mathc hte modal box header descriptorId????? */ - id?: string; + role?: string; } export const ModalBoxBody: React.FunctionComponent = ({ children, className, 'aria-label': ariaLabel, - 'aria-role': ariaRole, - id, + role, ...props }: ModalBoxBodyProps) => { - const defaultModalBodyAriaRole = ariaLabel ? 'region' : undefined; + const defaultModalBodyRole = ariaLabel ? 'region' : undefined; return (
{children} diff --git a/packages/react-core/src/next/components/Modal/ModalBoxCloseButton.tsx b/packages/react-core/src/next/components/Modal/ModalBoxCloseButton.tsx index a3549530fcf..54cd0c04006 100644 --- a/packages/react-core/src/next/components/Modal/ModalBoxCloseButton.tsx +++ b/packages/react-core/src/next/components/Modal/ModalBoxCloseButton.tsx @@ -18,7 +18,7 @@ export interface ModalBoxCloseButtonProps extends OUIAProps { export const ModalBoxCloseButton: React.FunctionComponent = ({ className, - onClose = () => undefined as any, + onClose, 'aria-label': ariaLabel = 'Close', ouiaId, ...props diff --git a/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx b/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx index a7eaad72fb1..e63319b98b7 100644 --- a/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx +++ b/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx @@ -5,7 +5,7 @@ import { ModalBoxDescription } from './ModalBoxDescription'; import { ModalBoxTitle } from './ModalBoxTitle'; export interface ModalBoxHeaderProps { - /** Custom content rendered inside the modal box header. If children are supplied then the tile, tileIconVariant and titleLabel props are ignored. */ + /** Custom content rendered inside the modal box header. If children are supplied then the tile, tileIconVariant and titleScreenReaderText props are ignored. */ children?: React.ReactNode; /** Additional classes added to the modal box header. */ className?: string; @@ -23,7 +23,7 @@ export interface ModalBoxHeaderProps { * are used the default styling will be automatically applied. */ titleIconVariant?: 'success' | 'danger' | 'warning' | 'info' | 'custom' | React.ComponentType; /** Optional title label text for screen readers. */ - titleLabel?: string; + titleScreenReaderText?: string; } export const ModalBoxHeader: React.FunctionComponent = ({ @@ -34,7 +34,7 @@ export const ModalBoxHeader: React.FunctionComponent = ({ labelId, title, titleIconVariant, - titleLabel, + titleScreenReaderText, help, ...props }: ModalBoxHeaderProps) => { @@ -42,7 +42,12 @@ export const ModalBoxHeader: React.FunctionComponent = ({ children ) : ( <> - + {description && {description}} ); diff --git a/packages/react-core/src/next/components/Modal/ModalBoxTitle.tsx b/packages/react-core/src/next/components/Modal/ModalBoxTitle.tsx index f8466b055cb..b7f4fcb97ea 100644 --- a/packages/react-core/src/next/components/Modal/ModalBoxTitle.tsx +++ b/packages/react-core/src/next/components/Modal/ModalBoxTitle.tsx @@ -24,7 +24,7 @@ export interface ModalBoxTitleProps { * are used the default styling will be automatically applied. */ titleIconVariant?: 'success' | 'danger' | 'warning' | 'info' | 'custom' | React.ComponentType; /** Optional title label text for screen readers. */ - titleLabel?: string; + titleScreenReaderText?: string; } export const ModalBoxTitle: React.FunctionComponent = ({ @@ -32,12 +32,14 @@ export const ModalBoxTitle: React.FunctionComponent = ({ id, title, titleIconVariant, - titleLabel, + titleScreenReaderText, ...props }: ModalBoxTitleProps) => { const [hasTooltip, setHasTooltip] = React.useState(false); const h1 = React.useRef(null); - const label = titleLabel || (isVariantIcon(titleIconVariant) ? `${capitalize(titleIconVariant)} alert:` : titleLabel); + const label = + titleScreenReaderText || + (isVariantIcon(titleIconVariant) ? `${capitalize(titleIconVariant)} alert:` : titleScreenReaderText); const variantIcons = { success: , danger: , diff --git a/packages/react-core/src/next/components/Modal/ModalContent.tsx b/packages/react-core/src/next/components/Modal/ModalContent.tsx index fed7411edb4..d7c486952cc 100644 --- a/packages/react-core/src/next/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/next/components/Modal/ModalContent.tsx @@ -34,8 +34,6 @@ export interface ModalContentProps extends OUIAProps { position?: 'default' | 'top'; /** Offset from alternate position. Can be any valid CSS length/percentage. */ positionOffset?: string; - /** Flag to show the close button in the header area of the modal. */ - showClose?: boolean; /** Variant of the modal. */ variant?: 'small' | 'medium' | 'large' | 'default'; /** Default width of the modal. */ @@ -55,8 +53,7 @@ export const ModalContent: React.FunctionComponent = ({ 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-labelledby': ariaLabelledby, - showClose = true, - onClose = () => undefined as any, + onClose, variant = 'default', position, positionOffset, @@ -109,7 +106,7 @@ export const ModalContent: React.FunctionComponent = ({ {...props} id={boxId} > - {showClose && onClose(event)} ouiaId={ouiaId} />} + {onClose && onClose(event)} ouiaId={ouiaId} />} {children} ); diff --git a/packages/react-core/src/next/components/Modal/__tests__/Modal.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/Modal.test.tsx index d3664a5cc58..7f819d0d99b 100644 --- a/packages/react-core/src/next/components/Modal/__tests__/Modal.test.tsx +++ b/packages/react-core/src/next/components/Modal/__tests__/Modal.test.tsx @@ -69,13 +69,13 @@ describe('Modal', () => { expect(document.body).not.toHaveClass(css(styles.backdropOpen)); }); - test('modal shows the close button when showClose is true (true by default)', () => { + test('modal shows the close button when onClose prop is passed (true by default)', () => { render(); expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument(); }); - test('modal does not show the close button when showClose is false', () => { - render(); + test('modal does not show the close button when onClose not passed', () => { + render(No close button ); expect(screen.queryByRole('button', { name: 'Close' })).toBeNull(); }); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx index a45a5cf480a..c00ce6e64fd 100644 --- a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx @@ -28,7 +28,7 @@ describe('ModalBoxBody tests', () => { expect(modalBoxBody).toHaveAccessibleName('modal box body aria label'); }); - test('The modalBoxBody has the expected aria role when bodyAriaLabel is passed and bodyAriaRole is not', () => { + test('The modalBoxBody has the expected aria role when aria-label is passed and role is not', () => { const props = { isOpen: true }; @@ -49,7 +49,7 @@ describe('ModalBoxBody tests', () => { }; render( - + This is a ModalBox ); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalContent.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalContent.test.tsx index d9170b36b0e..ff5fc0aef21 100644 --- a/packages/react-core/src/next/components/Modal/__tests__/ModalContent.test.tsx +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalContent.test.tsx @@ -6,7 +6,8 @@ import { ModalContent } from '../ModalContent'; const modalContentProps = { boxId: 'boxId', labelId: 'labelId', - descriptorId: 'descriptorId' + descriptorId: 'descriptorId', + disableFocusTrap: true }; test('Modal Content Test only body', () => { const { asFragment } = render( diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap index 8ddfbefa140..5456c013268 100644 --- a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalContent.test.tsx.snap @@ -18,33 +18,6 @@ exports[`Modal Content Test description 1`] = ` labelid="labelId" role="dialog" > -
- -
This is a ModalBox header
@@ -70,33 +43,6 @@ exports[`Modal Content Test isOpen 1`] = ` labelid="labelId" role="dialog" > -
- -
This is a ModalBox header @@ -122,33 +68,6 @@ exports[`Modal Content Test only body 1`] = ` labelid="labelId" role="dialog" > -
- -
This is a ModalBox header @@ -181,7 +100,7 @@ exports[`Modal Content Test with onclose 1`] = ` aria-disabled="false" aria-label="Close" class="pf-v5-c-button pf-m-plain" - data-ouia-component-id="OUIA-Generated-Button-plain-4" + data-ouia-component-id="OUIA-Generated-Button-plain-1" data-ouia-component-type="PF5/Button" data-ouia-safe="true" type="button" diff --git a/packages/react-core/src/next/components/Modal/examples/Modal.md b/packages/react-core/src/next/components/Modal/examples/Modal.md index 4e77affd5ea..db84798bd20 100644 --- a/packages/react-core/src/next/components/Modal/examples/Modal.md +++ b/packages/react-core/src/next/components/Modal/examples/Modal.md @@ -48,29 +48,13 @@ To override a modal's default center alignment, use the `position` property. In ``` -### Small modal +### Modal sizes To adjust the size of a modal, use the `variant` property. Modal variants include "small", "medium", "large", and "default". -The following example displays a "small" modal by passing in `variant={ModalVariant.small}`. +The following example displays a saml. medium or large modal based on selected radio button. E.g. if you selcet the "Small variant" option, a small modal will be rendered by passing in `variant={ModalVariant.small}`. -```ts file="./ModalSmall.tsx" - -``` - -### Medium modal - -The following example displays a "medium" modal by passing in `variant={ModalVariant.medium}`. - -```ts file="./ModalMedium.tsx" - -``` - -### Large modal - -The following example displays a "large" modal by passing in `variant={ModalVariant.large}`. - -```ts file="./ModalLarge.tsx" +```ts file="./ModalSize.tsx" ``` @@ -82,17 +66,17 @@ To choose a specific width for a modal, use the `width` property. The following ``` -### Custom header and footer +### Custom header -To add a custom header and footer to a modal, set the `header` and `footer` properties to a custom implementation. The following example passes title components into both the header and the footer and also passes an icon to the footer. +To add a custom header to a modal, do not pass the `title` property to the `ModalBoxHeader`. Custom heade content should be passed as a child of the `ModalBoxHeader` instead. -```ts file="./ModalCustomHeaderFooter.tsx" +```ts file="./ModalCustomHeader.tsx" ``` ### No header or footer -To exclusively present information in a modal, remove the `header` and/or `footer`. +When no header or footer is added to the model, make sure to add the `aria-label` prop since there is no title. ```ts file="./ModalNoHeaderFooter.tsx" diff --git a/packages/react-core/src/next/components/Modal/examples/ModalCustomHeaderFooter.tsx b/packages/react-core/src/next/components/Modal/examples/ModalCustomHeader.tsx similarity index 93% rename from packages/react-core/src/next/components/Modal/examples/ModalCustomHeaderFooter.tsx rename to packages/react-core/src/next/components/Modal/examples/ModalCustomHeader.tsx index 63f142c408b..ac8c3509092 100644 --- a/packages/react-core/src/next/components/Modal/examples/ModalCustomHeaderFooter.tsx +++ b/packages/react-core/src/next/components/Modal/examples/ModalCustomHeader.tsx @@ -28,7 +28,9 @@ export const ModalCustomHeaderFooter: React.FunctionComponent = () => { Custom header/footer modal -

Allows for custom content in the header and/or footer by passing components.

+

+ Add custom content to the header by not passing the titles prop the modal box header component. +

diff --git a/packages/react-core/src/next/components/Modal/examples/ModalLarge.tsx b/packages/react-core/src/next/components/Modal/examples/ModalLarge.tsx deleted file mode 100644 index 043b84fcb9e..00000000000 --- a/packages/react-core/src/next/components/Modal/examples/ModalLarge.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { Button } from '@patternfly/react-core'; -import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader, ModalVariant } from '@patternfly/react-core/next'; - -export const ModalLarge: React.FunctionComponent = () => { - const [isModalOpen, setIsModalOpen] = React.useState(false); - - const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { - setIsModalOpen(!isModalOpen); - }; - - return ( - - - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. - - - - - - - - ); -}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalMedium.tsx b/packages/react-core/src/next/components/Modal/examples/ModalSize.tsx similarity index 54% rename from packages/react-core/src/next/components/Modal/examples/ModalMedium.tsx rename to packages/react-core/src/next/components/Modal/examples/ModalSize.tsx index f696beed61b..eb7d79d870a 100644 --- a/packages/react-core/src/next/components/Modal/examples/ModalMedium.tsx +++ b/packages/react-core/src/next/components/Modal/examples/ModalSize.tsx @@ -1,9 +1,26 @@ import React from 'react'; -import { Button } from '@patternfly/react-core'; +import { Button, Radio } from '@patternfly/react-core'; import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader, ModalVariant } from '@patternfly/react-core/next'; -export const ModalMedium: React.FunctionComponent = () => { +export const ModalSize: React.FunctionComponent = () => { const [isModalOpen, setIsModalOpen] = React.useState(false); + const [selectedVariant, setSelectedVariant] = React.useState(ModalVariant.small); + + const capitalize = (input: string) => input[0].toUpperCase() + input.substring(1); + const formatSizeVariantName = (variant: string) => capitalize(variant); + + const variantOptions = [ModalVariant.small, ModalVariant.medium, ModalVariant.large]; + + const renderSizeOptions = variantOptions.map((variant) => ( + setSelectedVariant(variant)} + key={formatSizeVariantName(variant)} + name="Variant options" + /> + )); const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { setIsModalOpen(!isModalOpen); @@ -11,18 +28,21 @@ export const ModalMedium: React.FunctionComponent = () => { return ( + {renderSizeOptions} +
- - + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla diff --git a/packages/react-core/src/next/components/Modal/examples/ModalSmall.tsx b/packages/react-core/src/next/components/Modal/examples/ModalSmall.tsx deleted file mode 100644 index e6441758a6d..00000000000 --- a/packages/react-core/src/next/components/Modal/examples/ModalSmall.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { Button } from '@patternfly/react-core'; -import { Modal, ModalBoxBody, ModalBoxFooter, ModalBoxHeader, ModalVariant } from '@patternfly/react-core/next'; - -export const ModalSmall: React.FunctionComponent = () => { - const [isModalOpen, setIsModalOpen] = React.useState(false); - - const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { - setIsModalOpen(!isModalOpen); - }; - - return ( - - - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. - - - - - - - - ); -}; diff --git a/packages/react-core/src/next/components/Modal/examples/ModalWithDescription.tsx b/packages/react-core/src/next/components/Modal/examples/ModalWithDescription.tsx index ef3445e0b11..c0e69c5d7e0 100644 --- a/packages/react-core/src/next/components/Modal/examples/ModalWithDescription.tsx +++ b/packages/react-core/src/next/components/Modal/examples/ModalWithDescription.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from '@patternfly/react-core'; -import { Modal, ModalBoxBody, ModalBoxHeader, ModalBoxFooter } from '@patternfly/react-core/next'; +import { Modal, ModalBoxBody, ModalBoxHeader, ModalBoxFooter, ModalVariant } from '@patternfly/react-core/next'; export const ModalWithDescription: React.FunctionComponent = () => { const [isModalOpen, setIsModalOpen] = React.useState(false); @@ -15,6 +15,7 @@ export const ModalWithDescription: React.FunctionComponent = () => { Show modal with description { description="A description is used when you want to provide more info about the modal than the title is able to describe. The content in the description is static and will not scroll with the rest of the modal body." /> - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore - magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla - pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. + magna aliqua. Quis eleifend quam adipiscing vitae proin sagittis nisl rhoncus. Semper auctor neque vitae + tempus. Diam donec adipiscing tristique risus. Augue eget arcu dictum varius duis. Ut enim blandit volutpat + maecenas volutpat blandit aliquam. Sit amet mauris commodo quis imperdiet massa tincidunt. Habitant morbi + tristique senectus et netus. Fames ac turpis egestas sed tempus urna. Neque laoreet suspendisse interdum + consectetur libero id. Volutpat lacus laoreet non curabitur gravida arcu ac tortor. Porta nibh venenatis cras + sed felis eget velit. Nullam non nisi est sit amet facilisis. Nunc mi ipsum faucibus vitae. Lorem sed risus + ultricies tristique nulla aliquet enim tortor at. Egestas sed tempus urna et pharetra pharetra massa massa + ultricies. Lacinia quis vel eros donec ac odio tempor orci. Malesuada fames ac turpis egestas integer eget + aliquet. +
+
+ Neque aliquam vestibulum morbi blandit cursus risus at ultrices. Molestie at elementum eu facilisis sed odio + morbi. Elit pellentesque habitant morbi tristique. Consequat nisl vel pretium lectus quam id leo in vitae. + Quis varius quam quisque id diam vel quam elementum. Viverra nam libero justo laoreet sit amet cursus. + Sollicitudin tempor id eu nisl nunc. Orci nulla pellentesque dignissim enim sit amet venenatis. Dignissim enim + sit amet venenatis urna cursus eget. Iaculis at erat pellentesque adipiscing commodo elit. Faucibus pulvinar + elementum integer enim neque volutpat. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Nunc mattis + enim ut tellus elementum sagittis vitae. Blandit cursus risus at ultrices. Tellus mauris a diam maecenas sed + enim. Non diam phasellus vestibulum lorem sed risus ultricies tristique nulla. +
+
+ Nulla pharetra diam sit amet nisl suscipit adipiscing. Ac tortor vitae purus faucibus ornare suspendisse sed + nisi. Sed felis eget velit aliquet sagittis id consectetur purus. Tincidunt tortor aliquam nulla facilisi cras + fermentum. Volutpat est velit egestas dui id ornare arcu odio. Pharetra magna ac placerat vestibulum. Ultrices + sagittis orci a scelerisque purus semper eget duis at. Nisi est sit amet facilisis magna etiam tempor orci eu. + Convallis tellus id interdum velit. Facilisis sed odio morbi quis commodo odio aenean sed. +
+
+ Eu scelerisque felis imperdiet proin fermentum leo vel orci porta. Facilisi etiam dignissim diam quis enim + lobortis scelerisque fermentum. Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada. Magna + etiam tempor orci eu lobortis elementum. Quis auctor elit sed vulputate mi sit. Eleifend quam adipiscing vitae + proin sagittis nisl rhoncus mattis rhoncus. Erat velit scelerisque in dictum non. Sit amet nulla facilisi + morbi tempus iaculis urna. Enim ut tellus elementum sagittis vitae et leo duis ut. Lectus arcu bibendum at + varius vel pharetra vel turpis. Morbi tristique senectus et netus et. Eget aliquet nibh praesent tristique + magna sit amet purus gravida. Nisl purus in mollis nunc sed id semper risus. Id neque aliquam vestibulum + morbi. Mauris a diam maecenas sed enim ut sem. Egestas tellus rutrum tellus pellentesque.
Date: Tue, 12 Dec 2023 13:41:12 -0500 Subject: [PATCH 5/6] updates from Erin's comments --- .../next/components/Modal/examples/Modal.md | 26 ++++++++++++------- .../Modal/examples/ModalCustomHeader.tsx | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/react-core/src/next/components/Modal/examples/Modal.md b/packages/react-core/src/next/components/Modal/examples/Modal.md index db84798bd20..a4a4007f638 100644 --- a/packages/react-core/src/next/components/Modal/examples/Modal.md +++ b/packages/react-core/src/next/components/Modal/examples/Modal.md @@ -18,7 +18,11 @@ import formStyles from '@patternfly/react-styles/css/components/Form/form'; ### Basic modals -Basic modals give users the option to either confirm or cancel an action. To flag an open modal, use the `isOpen` property. To execute a callback when a modal is closed, use the `onClose` property. +Basic modals give users the option to either confirm or cancel an action. + +To flag an open modal, use the `isOpen` property. To execute a callback when a modal is closed, use the `onClose` property. + +A modal must have a ``, containing the main content of the modal. The `` and `` components are not required, but are typically used to display the modal title and any button actions, respectively. ```ts file="./ModalBasic.tsx" @@ -52,7 +56,7 @@ To override a modal's default center alignment, use the `position` property. In To adjust the size of a modal, use the `variant` property. Modal variants include "small", "medium", "large", and "default". -The following example displays a saml. medium or large modal based on selected radio button. E.g. if you selcet the "Small variant" option, a small modal will be rendered by passing in `variant={ModalVariant.small}`. +In the following example, you can display each modal size option. To launch a modal with a specific size, first select the respective radio button, followed by the "Show modal" button. ```ts file="./ModalSize.tsx" @@ -68,7 +72,7 @@ To choose a specific width for a modal, use the `width` property. The following ### Custom header -To add a custom header to a modal, do not pass the `title` property to the `ModalBoxHeader`. Custom heade content should be passed as a child of the `ModalBoxHeader` instead. +To add a custom header to a modal, your custom content must be passed as a child of the `` component. Do not pass the `title` property to `` when using a custom header. ```ts file="./ModalCustomHeader.tsx" @@ -76,7 +80,9 @@ To add a custom header to a modal, do not pass the `title` property to the `Moda ### No header or footer -When no header or footer is added to the model, make sure to add the `aria-label` prop since there is no title. +To exclusively present information in a modal, remove the header and/or footer. + +When a modal has no header or footer, make sure to add an `aria-label` explicitly stating this, so that those using assistive technologies can understand this context. ```ts file="./ModalNoHeaderFooter.tsx" @@ -84,7 +90,7 @@ When no header or footer is added to the model, make sure to add the `aria-label ### Title icon -To add an icon before a modal’s title, use the `titleIconVariant`, which can be set to one of the predefined variants -- "success", "danger", "warning", "info", and "custom" -- or to an imported custom icon. The following example uses a "warning" variant. +To add an icon before a modal’s title, use the `titleIconVariant` property, which can be set to a "success", "danger", "warning", or "info" variant. The following example uses a "warning" variant. ```ts file="./ModalTitleIcon.tsx" @@ -108,7 +114,9 @@ To guide users through a series of steps in a modal, you can add a [wizard](/com ### With dropdown -To present a menu of actions or links to a user, you can add a [dropdown](/components/dropdown) to a modal. To allow the dropdown to visually break out of the modal container, set the `menuAppendTo` property to “parent”. Handle the modal’s closing behavior by listening to the `onEscapePress` callback on the `` component. This allows the "escape" key to collapse the dropdown without closing the entire modal. +To present a menu of actions or links to a user, you can add a [dropdown](/components/menus/dropdown) to a modal. + +To allow the dropdown to visually break out of the modal container, set the `menuAppendTo` property to “parent”. Handle the modal’s closing behavior by listening to the `onEscapePress` callback on the `` component. This allows the "escape" key to collapse the dropdown without closing the entire modal. ```ts file="./ModalWithDropdown.tsx" @@ -124,9 +132,9 @@ To help simplify and explain complex models, add a help [popover](/components/po ### With form -To collect user input within a modal, you can add a [form](/components/form). +To collect user input within a modal, you can add a [form](/components/forms/form). -To submit the form from a button in the modal's footer (outside of the `
`), set the button's `form` property equal to the form's id. +To enable form submission from a button in the modal's footer (outside of the ``), set the button's `form` property equal to the form's id. ```ts file="ModalWithForm.tsx" @@ -134,7 +142,7 @@ To submit the form from a button in the modal's footer (outside of the ``) ### Custom focus -Use the `elementToFocus` property to customize which element inside the Modal receives focus when initially opened. +To customize which element inside the modal receives focus when initially opened, use the `elementToFocus` property`. ```ts file="./ModalCustomFocus.tsx" diff --git a/packages/react-core/src/next/components/Modal/examples/ModalCustomHeader.tsx b/packages/react-core/src/next/components/Modal/examples/ModalCustomHeader.tsx index ac8c3509092..b69c704ddbe 100644 --- a/packages/react-core/src/next/components/Modal/examples/ModalCustomHeader.tsx +++ b/packages/react-core/src/next/components/Modal/examples/ModalCustomHeader.tsx @@ -26,7 +26,7 @@ export const ModalCustomHeaderFooter: React.FunctionComponent = () => { > - Custom header/footer modal + Custom header modal

Add custom content to the header by not passing the titles prop the modal box header component. From 924534f61a0ce0bc878dc31bbf30e58d0ad3a4f8 Mon Sep 17 00:00:00 2001 From: Titani Date: Thu, 4 Jan 2024 17:19:09 -0500 Subject: [PATCH 6/6] Updates from comments --- .../src/next/components/Modal/Modal.tsx | 6 +- .../Modal/{ModalBoxBody.tsx => ModalBody.tsx} | 20 +-- .../src/next/components/Modal/ModalBox.tsx | 4 +- .../next/components/Modal/ModalContent.tsx | 4 +- .../{ModalBoxFooter.tsx => ModalFooter.tsx} | 14 +- .../{ModalBoxHeader.tsx => ModalHeader.tsx} | 22 ++-- ...dalBoxBody.test.tsx => ModalBody.test.tsx} | 32 ++--- .../Modal/__tests__/ModalBoxFooter.test.tsx | 10 -- .../Modal/__tests__/ModalBoxHeader.test.tsx | 14 +- .../Modal/__tests__/ModalFooter.test.tsx | 10 ++ ....test.tsx.snap => ModalBody.test.tsx.snap} | 4 +- .../ModalBoxHeader.test.tsx.snap | 4 +- ...est.tsx.snap => ModalFooter.test.tsx.snap} | 2 +- .../next/components/Modal/examples/Modal.md | 9 +- .../components/Modal/examples/ModalBasic.tsx | 12 +- .../Modal/examples/ModalCustomFocus.tsx | 12 +- .../Modal/examples/ModalCustomHeader.tsx | 35 ++--- .../Modal/examples/ModalCustomTitleIcon.tsx | 12 +- .../Modal/examples/ModalCustomWidth.tsx | 12 +- .../Modal/examples/ModalNoHeaderFooter.tsx | 6 +- .../components/Modal/examples/ModalSize.tsx | 12 +- .../Modal/examples/ModalTitleIcon.tsx | 12 +- .../Modal/examples/ModalTopAligned.tsx | 12 +- .../Modal/examples/ModalWithDescription.tsx | 12 +- .../Modal/examples/ModalWithDropdown.tsx | 12 +- .../Modal/examples/ModalWithForm.tsx | 12 +- .../Modal/examples/ModalWithHelp.tsx | 12 +- .../examples/ModalWithOverflowingContent.tsx | 12 +- .../Modal/examples/ModalWithWizard.tsx | 1 - .../src/next/components/Modal/index.ts | 6 +- .../demos/ModalNextDemo/ModalNextDemo.tsx | 122 +++++++++--------- 31 files changed, 237 insertions(+), 232 deletions(-) rename packages/react-core/src/next/components/Modal/{ModalBoxBody.tsx => ModalBody.tsx} (54%) rename packages/react-core/src/next/components/Modal/{ModalBoxFooter.tsx => ModalFooter.tsx} (51%) rename packages/react-core/src/next/components/Modal/{ModalBoxHeader.tsx => ModalHeader.tsx} (75%) rename packages/react-core/src/next/components/Modal/__tests__/{ModalBoxBody.test.tsx => ModalBody.test.tsx} (56%) delete mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalBoxFooter.test.tsx create mode 100644 packages/react-core/src/next/components/Modal/__tests__/ModalFooter.test.tsx rename packages/react-core/src/next/components/Modal/__tests__/__snapshots__/{ModalBoxBody.test.tsx.snap => ModalBody.test.tsx.snap} (66%) rename packages/react-core/src/next/components/Modal/__tests__/__snapshots__/{ModalBoxFooter.test.tsx.snap => ModalFooter.test.tsx.snap} (84%) diff --git a/packages/react-core/src/next/components/Modal/Modal.tsx b/packages/react-core/src/next/components/Modal/Modal.tsx index 07eb94a4a62..073ee8805a7 100644 --- a/packages/react-core/src/next/components/Modal/Modal.tsx +++ b/packages/react-core/src/next/components/Modal/Modal.tsx @@ -9,11 +9,11 @@ import { OUIAProps, getDefaultOUIAId } from '../../../helpers'; export interface ModalProps extends React.HTMLProps, OUIAProps { /** The parent container to append the modal to. Defaults to "document.body". */ appendTo?: HTMLElement | (() => HTMLElement); - /** Id to use for the modal box description. This should match the ModalBoxHeader labelId or descriptorId. */ + /** Id to use for the modal box description. This should match the ModalHeader labelId or descriptorId. */ 'aria-describedby'?: string; - /** Adds an accessible name to the modal when there is no title in the ModalBoxHeader. */ + /** Adds an accessible name to the modal when there is no title in the ModalHeader. */ 'aria-label'?: string; - /** Id to use for the modal box label. This should include the ModalBoxHeader labelId. */ + /** Id to use for the modal box label. This should include the ModalHeader labelId. */ 'aria-labelledby'?: string; /** Content rendered inside the modal. */ children: React.ReactNode; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx b/packages/react-core/src/next/components/Modal/ModalBody.tsx similarity index 54% rename from packages/react-core/src/next/components/Modal/ModalBoxBody.tsx rename to packages/react-core/src/next/components/Modal/ModalBody.tsx index 14ba20800ed..aa6689cd69b 100644 --- a/packages/react-core/src/next/components/Modal/ModalBoxBody.tsx +++ b/packages/react-core/src/next/components/Modal/ModalBody.tsx @@ -2,29 +2,31 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; -export interface ModalBoxBodyProps extends React.HTMLProps { - /** Content rendered inside the modal box body. */ +/** Renders content in the body of the modal */ + +export interface ModalBodyProps extends React.HTMLProps { + /** Content rendered inside the modal body. */ children?: React.ReactNode; - /** Additional classes added to the modal box body. */ + /** Additional classes added to the modal body. */ className?: string; - /** Accessible label applied to the modal box body. This should be used to communicate - * important information about the modal box body div element if needed, such as when it is scrollable. + /** Accessible label applied to the modal body. This should be used to communicate + * important information about the modal body div element if needed, such as when it is scrollable. */ 'aria-label'?: string; - /** Accessible role applied to the modal box body. This will default to "region" if the + /** Accessible role applied to the modal body. This will default to "region" if the * aria-label property is passed in. Set to a more appropriate role as applicable * based on the modal content and context. */ role?: string; } -export const ModalBoxBody: React.FunctionComponent = ({ +export const ModalBody: React.FunctionComponent = ({ children, className, 'aria-label': ariaLabel, role, ...props -}: ModalBoxBodyProps) => { +}: ModalBodyProps) => { const defaultModalBodyRole = ariaLabel ? 'region' : undefined; return (

= ({
); }; -ModalBoxBody.displayName = 'ModalBoxBody'; +ModalBody.displayName = 'ModalBody'; diff --git a/packages/react-core/src/next/components/Modal/ModalBox.tsx b/packages/react-core/src/next/components/Modal/ModalBox.tsx index f59fed5e8a0..b52a9660ece 100644 --- a/packages/react-core/src/next/components/Modal/ModalBox.tsx +++ b/packages/react-core/src/next/components/Modal/ModalBox.tsx @@ -4,9 +4,9 @@ import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; import topSpacer from '@patternfly/react-tokens/dist/esm/c_modal_box_m_align_top_spacer'; export interface ModalBoxProps extends React.HTMLProps { - /** Id to use for the modal box description. This should match the ModalBoxHeader labelId or descriptorId */ + /** Id to use for the modal box description. This should match the ModalHeader labelId or descriptorId */ 'aria-describedby'?: string; - /** Adds an accessible name to the modal when there is no title in the ModalBoxHeader. */ + /** Adds an accessible name to the modal when there is no title in the ModalHeader. */ 'aria-label'?: string; /** Id to use for the modal box label. */ 'aria-labelledby'?: string; diff --git a/packages/react-core/src/next/components/Modal/ModalContent.tsx b/packages/react-core/src/next/components/Modal/ModalContent.tsx index d7c486952cc..982356aaa42 100644 --- a/packages/react-core/src/next/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/next/components/Modal/ModalContent.tsx @@ -8,11 +8,11 @@ import { ModalBoxCloseButton } from './ModalBoxCloseButton'; import { ModalBox } from './ModalBox'; export interface ModalContentProps extends OUIAProps { - /** Id to use for the modal box description. This should match the ModalBoxHeader labelId or descriptorId. */ + /** Id to use for the modal box description. This should match the ModalHeader labelId or descriptorId. */ 'aria-describedby'?: string; /** Accessible descriptor of the modal. */ 'aria-label'?: string; - /** Id to use for the modal box label. This should include the ModalBoxHeader labelId. */ + /** Id to use for the modal box label. This should include the ModalHeader labelId. */ 'aria-labelledby'?: string; /** Id of the modal box container. */ boxId: string; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxFooter.tsx b/packages/react-core/src/next/components/Modal/ModalFooter.tsx similarity index 51% rename from packages/react-core/src/next/components/Modal/ModalBoxFooter.tsx rename to packages/react-core/src/next/components/Modal/ModalFooter.tsx index 5c4432f71d4..eee3e83302e 100644 --- a/packages/react-core/src/next/components/Modal/ModalBoxFooter.tsx +++ b/packages/react-core/src/next/components/Modal/ModalFooter.tsx @@ -2,20 +2,22 @@ import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; -export interface ModalBoxFooterProps { - /** Content rendered inside the modal box footer. */ +/** Renders content in the footer of the modal */ + +export interface ModalFooterProps { + /** Content rendered inside the modal footer. */ children?: React.ReactNode; - /** Additional classes added to the modal box footer. */ + /** Additional classes added to the modal footer. */ className?: string; } -export const ModalBoxFooter: React.FunctionComponent = ({ +export const ModalFooter: React.FunctionComponent = ({ children, className, ...props -}: ModalBoxFooterProps) => ( +}: ModalFooterProps) => (
{children}
); -ModalBoxFooter.displayName = 'ModalBoxFooter'; +ModalFooter.displayName = 'ModalFooter'; diff --git a/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx b/packages/react-core/src/next/components/Modal/ModalHeader.tsx similarity index 75% rename from packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx rename to packages/react-core/src/next/components/Modal/ModalHeader.tsx index e63319b98b7..c8973d24d81 100644 --- a/packages/react-core/src/next/components/Modal/ModalBoxHeader.tsx +++ b/packages/react-core/src/next/components/Modal/ModalHeader.tsx @@ -4,20 +4,22 @@ import styles from '@patternfly/react-styles/css/components/ModalBox/modal-box'; import { ModalBoxDescription } from './ModalBoxDescription'; import { ModalBoxTitle } from './ModalBoxTitle'; -export interface ModalBoxHeaderProps { - /** Custom content rendered inside the modal box header. If children are supplied then the tile, tileIconVariant and titleScreenReaderText props are ignored. */ +/** Renders content in the header of the modal */ + +export interface ModalHeaderProps { + /** Custom content rendered inside the modal header. If children are supplied then the tile, tileIconVariant and titleScreenReaderText props are ignored. */ children?: React.ReactNode; - /** Additional classes added to the modal box header. */ + /** Additional classes added to the modal header. */ className?: string; /** Description of the modal. */ description?: React.ReactNode; - /** Id of the modal box description. */ + /** Id of the modal description. */ descriptorId?: string; - /** Optional help section for the modal box header. */ + /** Optional help section for the modal header. */ help?: React.ReactNode; - /** Id of the modal box title. */ + /** Id of the modal title. */ labelId?: string; - /** Content rendered inside the modal box title. */ + /** Content rendered inside the modal title. */ title?: React.ReactNode; /** Optional alert icon (or other) to show before the title. When the predefined alert types * are used the default styling will be automatically applied. */ @@ -26,7 +28,7 @@ export interface ModalBoxHeaderProps { titleScreenReaderText?: string; } -export const ModalBoxHeader: React.FunctionComponent = ({ +export const ModalHeader: React.FunctionComponent = ({ children, className, descriptorId, @@ -37,7 +39,7 @@ export const ModalBoxHeader: React.FunctionComponent = ({ titleScreenReaderText, help, ...props -}: ModalBoxHeaderProps) => { +}: ModalHeaderProps) => { const headerContent = children ? ( children ) : ( @@ -66,4 +68,4 @@ export const ModalBoxHeader: React.FunctionComponent = ({ ); }; -ModalBoxHeader.displayName = 'ModalBoxHeader'; +ModalHeader.displayName = 'ModalHeader'; diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBody.test.tsx similarity index 56% rename from packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx rename to packages/react-core/src/next/components/Modal/__tests__/ModalBody.test.tsx index c00ce6e64fd..c1379a8b745 100644 --- a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxBody.test.tsx +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBody.test.tsx @@ -1,31 +1,31 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; -import { ModalBoxBody } from '../ModalBoxBody'; +import { ModalBody } from '../ModalBody'; -describe('ModalBoxBody tests', () => { - test('ModalBoxBody renders', () => { +describe('ModalBody tests', () => { + test('ModalBody renders', () => { const { asFragment } = render( - - This is a ModalBox body - + + This is a Modal body + ); expect(asFragment()).toMatchSnapshot(); }); - test('The modalBoxBody has the expected aria-label when it is passed', () => { + test('The ModalBody has the expected aria-label when it is passed', () => { const props = { isOpen: true }; render( - + This is a ModalBox - + ); const modalBoxBody = screen.getByText('This is a ModalBox'); - expect(modalBoxBody).toHaveAccessibleName('modal box body aria label'); + expect(modalBoxBody).toHaveAccessibleName('modal body aria label'); }); test('The modalBoxBody has the expected aria role when aria-label is passed and role is not', () => { @@ -34,12 +34,12 @@ describe('ModalBoxBody tests', () => { }; render( - + This is a ModalBox - + ); - const modalBoxBody = screen.getByRole('region', { name: 'modal box body aria label' }); + const modalBoxBody = screen.getByRole('region', { name: 'modal body aria label' }); expect(modalBoxBody).toBeInTheDocument(); }); @@ -49,12 +49,12 @@ describe('ModalBoxBody tests', () => { }; render( - + This is a ModalBox - + ); - const modalBoxBody = screen.getByRole('article', { name: 'modal box body aria label' }); + const modalBoxBody = screen.getByRole('article', { name: 'modal body aria label' }); expect(modalBoxBody).toBeInTheDocument(); }); }); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxFooter.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxFooter.test.tsx deleted file mode 100644 index bf94f92dbf6..00000000000 --- a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxFooter.test.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import { ModalBoxFooter } from '../ModalBoxFooter'; - -test('ModalBoxFooter Test', () => { - const { asFragment } = render( - This is a ModalBox Footer - ); - expect(asFragment()).toMatchSnapshot(); -}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxHeader.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxHeader.test.tsx index 34a642718e5..d3da940a1f7 100644 --- a/packages/react-core/src/next/components/Modal/__tests__/ModalBoxHeader.test.tsx +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalBoxHeader.test.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import { render } from '@testing-library/react'; -import { ModalBoxHeader } from '../ModalBoxHeader'; +import { ModalHeader } from '../ModalHeader'; -test('ModalBoxHeader Test', () => { - const { asFragment } = render(This is a ModalBox header); +test('ModalHeader Test', () => { + const { asFragment } = render(This is a ModalBox header); expect(asFragment()).toMatchSnapshot(); }); -test('ModalBoxHeader help renders', () => { - const { asFragment } = render(test}>This is a ModalBox header); +test('ModalHeader help renders', () => { + const { asFragment } = render(test}>This is a ModalBox header); expect(asFragment()).toMatchSnapshot(); }); @@ -17,9 +17,9 @@ test('Modal Test with custom header', () => { const header = TEST; const { asFragment } = render( - + {header} - + ); expect(asFragment()).toMatchSnapshot(); }); \ No newline at end of file diff --git a/packages/react-core/src/next/components/Modal/__tests__/ModalFooter.test.tsx b/packages/react-core/src/next/components/Modal/__tests__/ModalFooter.test.tsx new file mode 100644 index 00000000000..2e0c256b907 --- /dev/null +++ b/packages/react-core/src/next/components/Modal/__tests__/ModalFooter.test.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { ModalFooter } from '../ModalFooter'; + +test('ModalFooter Test', () => { + const { asFragment } = render( + This is a ModalBox Footer + ); + expect(asFragment()).toMatchSnapshot(); +}); diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxBody.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBody.test.tsx.snap similarity index 66% rename from packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxBody.test.tsx.snap rename to packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBody.test.tsx.snap index a0820e8a994..b977403512b 100644 --- a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxBody.test.tsx.snap +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBody.test.tsx.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ModalBoxBody tests ModalBoxBody renders 1`] = ` +exports[`ModalBody tests ModalBody renders 1`] = `
- This is a ModalBox body + This is a Modal body
`; diff --git a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxHeader.test.tsx.snap b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxHeader.test.tsx.snap index 85d9c3e9012..98edf535b86 100644 --- a/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxHeader.test.tsx.snap +++ b/packages/react-core/src/next/components/Modal/__tests__/__snapshots__/ModalBoxHeader.test.tsx.snap @@ -14,7 +14,7 @@ exports[`Modal Test with custom header 1`] = ` `; -exports[`ModalBoxHeader Test 1`] = ` +exports[`ModalHeader Test 1`] = `
`; -exports[`ModalBoxHeader help renders 1`] = ` +exports[`ModalHeader help renders 1`] = `