Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/react-core/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export interface ModalProps extends React.HTMLProps<HTMLDivElement>, OUIAProps {
description?: React.ReactNode;
/** 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;
/** Custom footer. */
footer?: React.ReactNode;
/** Flag indicating if modal content should be placed in a modal box body wrapper. */
Expand Down Expand Up @@ -235,6 +239,7 @@ export class Modal extends React.Component<ModalProps, ModalState> {
ouiaId,
ouiaSafe,
position,
elementToFocus,
...props
} = this.props;
const { container } = this.state;
Expand All @@ -260,6 +265,7 @@ export class Modal extends React.Component<ModalProps, ModalState> {
ouiaId={ouiaId !== undefined ? ouiaId : this.state.ouiaStateId}
ouiaSafe={ouiaSafe}
position={position}
elementToFocus={elementToFocus}
/>,
container
) as React.ReactElement;
Expand Down
13 changes: 12 additions & 1 deletion packages/react-core/src/components/Modal/ModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export interface ModalContentProps extends OUIAProps {
descriptorId: 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;
/** Custom footer. */
footer?: React.ReactNode;
/** Flag indicating if modal content should be placed in a modal box body wrapper. */
Expand Down Expand Up @@ -118,6 +122,7 @@ export const ModalContent: React.FunctionComponent<ModalContentProps> = ({
hasNoBodyWrapper = false,
ouiaId,
ouiaSafe = true,
elementToFocus,
...props
}: ModalContentProps) => {
if (!isOpen) {
Expand Down Expand Up @@ -202,7 +207,13 @@ export const ModalContent: React.FunctionComponent<ModalContentProps> = ({
<Backdrop>
<FocusTrap
active={!disableFocusTrap}
focusTrapOptions={{ clickOutsideDeactivates: true, tabbableOptions: { displayCheck: 'none' } }}
focusTrapOptions={{
clickOutsideDeactivates: true,
tabbableOptions: { displayCheck: 'none' },
// FocusTrap's initialFocus can accept false as a value to prevent initial focus.
// We want to prevent this in case false is ever passed in.
initialFocus: elementToFocus || undefined
}}
className={css(bullsEyeStyles.bullseye)}
>
{modalBox}
Expand Down
8 changes: 8 additions & 0 deletions packages/react-core/src/components/Modal/examples/Modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,11 @@ To submit the form from a button in the modal's footer (outside of the `<Form>`)
```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"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { Modal, Button } from '@patternfly/react-core';

export const ModalCustomFocus: React.FunctionComponent = () => {
const [isModalOpen, setIsModalOpen] = React.useState(false);

const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
setIsModalOpen(!isModalOpen);
};

return (
<React.Fragment>
<Button variant="primary" onClick={handleModalToggle} ouiaId="ShowBasicModal">
Show modal with custom focus
</Button>
<Modal
elementToFocus="#modal-custom-focus-confirm-button"
title="Modal with custom focus"
isOpen={isModalOpen}
onClose={handleModalToggle}
actions={[
<Button id="modal-custom-focus-confirm-button" key="confirm" variant="primary" onClick={handleModalToggle}>
Confirm
</Button>,
<Button key="cancel" variant="link" onClick={handleModalToggle}>
Cancel
</Button>
]}
>
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.
</Modal>
</React.Fragment>
);
};
12 changes: 12 additions & 0 deletions packages/react-integration/cypress/integration/modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,16 @@ describe('Modal Test', () => {
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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ModalDemoState {
isModalCustomEscapeOpen: boolean;
isModalAlertVariantOpen: boolean;
customEscapePressed: boolean;
isCustomFocusModalOpen: boolean;
}

export class ModalDemo extends React.Component<React.HTMLProps<HTMLDivElement>, ModalDemoState> {
Expand All @@ -31,7 +32,8 @@ export class ModalDemo extends React.Component<React.HTMLProps<HTMLDivElement>,
isNoHeaderModalOpen: false,
isModalCustomEscapeOpen: false,
isModalAlertVariantOpen: false,
customEscapePressed: false
customEscapePressed: false,
isCustomFocusModalOpen: false
};

handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => {
Expand Down Expand Up @@ -102,6 +104,12 @@ export class ModalDemo extends React.Component<React.HTMLProps<HTMLDivElement>,
}));
};

handleCustomFocusModalToggle = () => {
this.setState(({ isCustomFocusModalOpen }) => ({
isCustomFocusModalOpen: !isCustomFocusModalOpen
}));
};

componentDidMount() {
window.scrollTo(0, 0);
}
Expand Down Expand Up @@ -426,6 +434,43 @@ export class ModalDemo extends React.Component<React.HTMLProps<HTMLDivElement>,
);
}

renderCustomFocusModal() {
const { isCustomFocusModalOpen } = this.state;

return (
<Modal
elementToFocus="#modal-custom-focus-confirm-button"
title="Modal with custom focus"
isOpen={isCustomFocusModalOpen}
onClose={this.handleCustomFocusModalToggle}
actions={[
<Button
id="modal-custom-focus-confirm-button"
key="confirm"
variant="primary"
onClick={this.handleCustomFocusModalToggle}
>
Confirm
</Button>,
<Button
id="modal-custom-focus-cancel-button"
key="cancel"
variant="link"
onClick={this.handleCustomFocusModalToggle}
>
Cancel
</Button>
]}
>
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.
</Modal>
);
}

render() {
const buttonStyle = {
marginRight: 20,
Expand Down Expand Up @@ -505,6 +550,14 @@ export class ModalDemo extends React.Component<React.HTMLProps<HTMLDivElement>,
<Button style={buttonStyle} variant="primary" onClick={this.handleHelpModalToggle} id="showHelpModalButton">
Show Help Modal
</Button>
<Button
style={buttonStyle}
variant="primary"
onClick={this.handleCustomFocusModalToggle}
id="showCustomFocusModalButton"
>
Show Custom Focus Modal
</Button>
</div>
{this.renderModal()}
{this.renderSmallModal()}
Expand All @@ -517,6 +570,7 @@ export class ModalDemo extends React.Component<React.HTMLProps<HTMLDivElement>,
{this.renderModalWithCustomEscape()}
{this.renderModalWithAlertVariant()}
{this.renderHelpModal()}
{this.renderCustomFocusModal()}
</React.Fragment>
);
}
Expand Down