diff --git a/packages/react-core/src/components/Modal/Modal.tsx b/packages/react-core/src/components/Modal/Modal.tsx index 6cc7d78db7e..c3942c5760e 100644 --- a/packages/react-core/src/components/Modal/Modal.tsx +++ b/packages/react-core/src/components/Modal/Modal.tsx @@ -31,6 +31,10 @@ export interface ModalProps extends React.HTMLProps, OUIAProps { 'aria-label'?: string; /** Id to use for Modal Box descriptor */ 'aria-describedby'?: string; + /** Accessible label applied to the modal box body. This should be used to communicate important information about the modal box body div if needed, such as that it is scrollable */ + bodyAriaLabel?: string; + /** Accessible role applied to the modal box body. This will default to region if a body aria label is applied. Set to a more appropriate role as applicable based on the modal content and context */ + bodyAriaRole?: string; /** Flag to show the close button in the header area of the modal */ showClose?: boolean; /** Custom footer */ @@ -206,6 +210,8 @@ export class Modal extends React.Component { 'aria-labelledby': ariaLabelledby, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, + bodyAriaLabel, + bodyAriaRole, title, titleIconVariant, titleLabel, @@ -231,6 +237,8 @@ export class Modal extends React.Component { aria-label={ariaLabel} aria-describedby={ariaDescribedby} aria-labelledby={ariaLabelledby} + bodyAriaLabel={bodyAriaLabel} + bodyAriaRole={bodyAriaRole} ouiaId={ouiaId !== undefined ? ouiaId : this.state.ouiaStateId} ouiaSafe={ouiaSafe} />, diff --git a/packages/react-core/src/components/Modal/ModalContent.tsx b/packages/react-core/src/components/Modal/ModalContent.tsx index 4058c3dcff0..d6e4deec8c5 100644 --- a/packages/react-core/src/components/Modal/ModalContent.tsx +++ b/packages/react-core/src/components/Modal/ModalContent.tsx @@ -47,6 +47,10 @@ export interface ModalContentProps extends OUIAProps { 'aria-label'?: string; /** Id of Modal Box description */ 'aria-describedby'?: string; + /** Accessible label applied to the modal box body. This should be used to communicate important information about the modal box body div if needed, such as that it is scrollable */ + bodyAriaLabel?: string; + /** Accessible role applied to the modal box body. This will default to region if a body aria label is applied. Set to a more appropriate role as applicable based on the modal content and context */ + bodyAriaRole?: string; /** Flag to show the close button in the header area of the modal */ showClose?: boolean; /** Default width of the content. */ @@ -82,6 +86,8 @@ export const ModalContent: React.FunctionComponent = ({ 'aria-label': ariaLabel = '', 'aria-describedby': ariaDescribedby, 'aria-labelledby': ariaLabelledby, + bodyAriaLabel, + bodyAriaRole, showClose = true, footer = null, actions = [], @@ -120,10 +126,17 @@ export const ModalContent: React.FunctionComponent = ({ actions.length > 0 && {actions} ); + const defaultModalBodyAriaRole = bodyAriaLabel ? 'region' : undefined; + const modalBody = hasNoBodyWrapper ? ( children ) : ( - + {children} ); diff --git a/packages/react-core/src/components/Modal/__tests__/Modal.test.tsx b/packages/react-core/src/components/Modal/__tests__/Modal.test.tsx index 094a0abf167..70ceebedb3d 100644 --- a/packages/react-core/src/components/Modal/__tests__/Modal.test.tsx +++ b/packages/react-core/src/components/Modal/__tests__/Modal.test.tsx @@ -86,4 +86,60 @@ describe('Modal', () => { expect(consoleErrorMock).toBeCalled(); }); + + test('The modalBoxBody has no aria-label when bodyAriaLabel is not passed', () => { + const props = { + isOpen: true + }; + + render(This is a ModalBox); + + const modalBoxBody = screen.getByText('This is a ModalBox'); + expect(modalBoxBody).not.toHaveAccessibleName('modal box body aria label'); + }); + + test('The modalBoxBody has the expected aria-label when bodyAriaLabel 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/components/Modal/examples/Modal.md b/packages/react-core/src/components/Modal/examples/Modal.md index b44d6d9e680..e4e167d8fbd 100644 --- a/packages/react-core/src/components/Modal/examples/Modal.md +++ b/packages/react-core/src/components/Modal/examples/Modal.md @@ -869,3 +869,10 @@ class HelpModal extends React.Component { ```ts file="ModalWithForm.tsx" ``` + +### With overflowing content + +If the content that you're passing to the modal is likely to overflow the modal content area, pass tabIndex={0} to the modal to enable keyboard accessible scrolling. + +```ts file="ModalWithOverflowingContent.tsx" +``` diff --git a/packages/react-core/src/components/Modal/examples/ModalWithOverflowingContent.tsx b/packages/react-core/src/components/Modal/examples/ModalWithOverflowingContent.tsx new file mode 100644 index 00000000000..da6a7ea177e --- /dev/null +++ b/packages/react-core/src/components/Modal/examples/ModalWithOverflowingContent.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Modal, ModalVariant, Button } from '@patternfly/react-core'; + +export const ModalWithOverflowingContent: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const handleModalToggle = () => { + setIsModalOpen(prevIsModalOpen => !prevIsModalOpen); + }; + + return ( + + + + Confirm + , + + ]} + > + 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. +
+
+ ); +};