Skip to content

Commit ec7624d

Browse files
authored
fix(Modal): Various bug fixes V5 (#11168)
* fix(Modal): Various bug fixes * update modal next * update test example * remove test examples
1 parent 9d53914 commit ec7624d

File tree

7 files changed

+112
-46
lines changed

7 files changed

+112
-46
lines changed

eslint.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export default [
1616
'packages/react-core/src/helpers/Popper/thirdparty',
1717
'packages/react-docs/patternfly-docs/generated',
1818
'packages/react-docs/static',
19-
'**/.cache'
19+
'**/.cache',
20+
'.history'
2021
]
2122
},
2223
js.configs.recommended,

packages/react-core/src/components/Modal/Modal.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ export enum ModalVariant {
9292
}
9393

9494
interface ModalState {
95-
container: HTMLElement;
9695
ouiaStateId: string;
9796
}
9897

@@ -102,6 +101,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
102101
boxId = '';
103102
labelId = '';
104103
descriptorId = '';
104+
backdropId = '';
105105

106106
static defaultProps: PickOptional<ModalProps> = {
107107
className: '',
@@ -128,12 +128,13 @@ class Modal extends React.Component<ModalProps, ModalState> {
128128
const boxIdNum = Modal.currentId++;
129129
const labelIdNum = boxIdNum + 1;
130130
const descriptorIdNum = boxIdNum + 2;
131+
const backdropIdNum = boxIdNum + 3;
131132
this.boxId = props.id || `pf-modal-part-${boxIdNum}`;
132133
this.labelId = `pf-modal-part-${labelIdNum}`;
133134
this.descriptorId = `pf-modal-part-${descriptorIdNum}`;
135+
this.backdropId = `pf-modal-part-${backdropIdNum}`;
134136

135137
this.state = {
136-
container: undefined,
137138
ouiaStateId: getDefaultOUIAId(Modal.displayName, props.variant)
138139
};
139140
}
@@ -157,7 +158,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
157158
const target: HTMLElement = this.getElement(appendTo);
158159
const bodyChildren = target.children;
159160
for (const child of Array.from(bodyChildren)) {
160-
if (child !== this.state.container) {
161+
if (child.id !== this.backdropId) {
161162
hide ? child.setAttribute('aria-hidden', '' + hide) : child.removeAttribute('aria-hidden');
162163
}
163164
}
@@ -175,15 +176,11 @@ class Modal extends React.Component<ModalProps, ModalState> {
175176
header
176177
} = this.props;
177178
const target: HTMLElement = this.getElement(appendTo);
178-
const container = document.createElement('div');
179-
this.setState({ container });
180-
target.appendChild(container);
181179
target.addEventListener('keydown', this.handleEscKeyClick, false);
182180

183181
if (this.props.isOpen) {
184182
target.classList.add(css(styles.backdropOpen));
185-
} else {
186-
target.classList.remove(css(styles.backdropOpen));
183+
this.toggleSiblingsFromScreenReaders(true);
187184
}
188185

189186
if (!title && this.isEmpty(ariaLabel) && this.isEmpty(ariaLabelledby)) {
@@ -199,32 +196,31 @@ class Modal extends React.Component<ModalProps, ModalState> {
199196
}
200197
}
201198

202-
componentDidUpdate() {
199+
componentDidUpdate(prevProps: ModalProps) {
203200
const { appendTo } = this.props;
204201
const target: HTMLElement = this.getElement(appendTo);
205202
if (this.props.isOpen) {
206203
target.classList.add(css(styles.backdropOpen));
207204
this.toggleSiblingsFromScreenReaders(true);
208205
} else {
209-
target.classList.remove(css(styles.backdropOpen));
210-
this.toggleSiblingsFromScreenReaders(false);
206+
if (prevProps.isOpen !== this.props.isOpen) {
207+
target.classList.remove(css(styles.backdropOpen));
208+
this.toggleSiblingsFromScreenReaders(false);
209+
}
211210
}
212211
}
213212

214213
componentWillUnmount() {
215214
const { appendTo } = this.props;
216215
const target: HTMLElement = this.getElement(appendTo);
217-
if (this.state.container) {
218-
target.removeChild(this.state.container);
219-
}
216+
220217
target.removeEventListener('keydown', this.handleEscKeyClick, false);
221218
target.classList.remove(css(styles.backdropOpen));
222219
this.toggleSiblingsFromScreenReaders(false);
223220
}
224221

225222
render() {
226223
const {
227-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
228224
appendTo,
229225
// eslint-disable-next-line @typescript-eslint/no-unused-vars
230226
onEscapePress,
@@ -242,9 +238,8 @@ class Modal extends React.Component<ModalProps, ModalState> {
242238
elementToFocus,
243239
...props
244240
} = this.props;
245-
const { container } = this.state;
246241

247-
if (!canUseDOM || !container) {
242+
if (!canUseDOM || !this.getElement(appendTo)) {
248243
return null;
249244
}
250245

@@ -254,6 +249,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
254249
boxId={this.boxId}
255250
labelId={this.labelId}
256251
descriptorId={this.descriptorId}
252+
backdropId={this.backdropId}
257253
title={title}
258254
titleIconVariant={titleIconVariant}
259255
titleLabel={titleLabel}
@@ -267,7 +263,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
267263
position={position}
268264
elementToFocus={elementToFocus}
269265
/>,
270-
container
266+
this.getElement(appendTo)
271267
) as React.ReactElement;
272268
}
273269
}

packages/react-core/src/components/Modal/ModalContent.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface ModalContentProps extends OUIAProps {
2525
'aria-label'?: string;
2626
/** Id to use for the modal box label. */
2727
'aria-labelledby'?: string | null;
28+
/** Id of the backdrop. */
29+
backdropId?: string;
2830
/** Accessible label applied to the modal box body. This should be used to communicate
2931
* important information about the modal box body div element if needed, such as that it
3032
* is scrollable.
@@ -117,6 +119,7 @@ export const ModalContent: React.FunctionComponent<ModalContentProps> = ({
117119
maxWidth,
118120
boxId,
119121
labelId,
122+
backdropId,
120123
descriptorId,
121124
disableFocusTrap = false,
122125
hasNoBodyWrapper = false,
@@ -202,7 +205,7 @@ export const ModalContent: React.FunctionComponent<ModalContentProps> = ({
202205
</ModalBox>
203206
);
204207
return (
205-
<Backdrop>
208+
<Backdrop id={backdropId}>
206209
<FocusTrap
207210
active={!disableFocusTrap}
208211
focusTrapOptions={{

packages/react-core/src/components/Modal/__tests__/Modal.test.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22

33
import { render, screen } from '@testing-library/react';
44
import userEvent from '@testing-library/user-event';
5+
import '@testing-library/jest-dom';
56

67
import { css } from '../../../../../react-styles/dist/js';
78
import styles from '@patternfly/react-styles/css/components/Backdrop/backdrop';
@@ -39,12 +40,33 @@ const ModalWithSiblings = () => {
3940
);
4041
};
4142

42-
describe('Modal', () => {
43-
test('Modal creates a container element once for div', () => {
44-
render(<Modal {...props} />);
45-
expect(document.createElement).toHaveBeenCalledWith('div');
46-
});
43+
const ModalWithAdjacentModal = () => {
44+
const [isOpen, setIsOpen] = React.useState(true);
45+
const [isModalMounted, setIsModalMounted] = React.useState(true);
46+
const modalProps = { ...props, isOpen, appendTo: target, onClose: () => setIsOpen(false) };
4747

48+
return (
49+
<>
50+
<aside>Aside sibling</aside>
51+
<article>Section sibling</article>
52+
{isModalMounted && (
53+
<>
54+
<Modal {...modalProps}>
55+
<button onClick={() => setIsModalMounted(false)}>Unmount Modal</button>
56+
</Modal>
57+
<Modal isOpen={false} onClose={() => {}}>
58+
Modal closed for test
59+
</Modal>
60+
<Modal isOpen={false} onClose={() => {}}>
61+
modal closed for test
62+
</Modal>
63+
</>
64+
)}
65+
</>
66+
);
67+
};
68+
69+
describe('Modal', () => {
4870
test('modal closes with escape', async () => {
4971
const user = userEvent.setup();
5072

@@ -137,10 +159,10 @@ describe('Modal', () => {
137159
expect(asideSibling).not.toHaveAttribute('aria-hidden');
138160
});
139161

140-
test('modal removes the aria-hidden attribute from its siblings when unmounted', async () => {
162+
test('modal siblings have the aria-hidden attribute when it has adjacent modals', async () => {
141163
const user = userEvent.setup();
142164

143-
render(<ModalWithSiblings />, { container: document.body.appendChild(target) });
165+
render(<ModalWithAdjacentModal />, { container: document.body.appendChild(target) });
144166

145167
const asideSibling = screen.getByRole('complementary', { hidden: true });
146168
const articleSibling = screen.getByRole('article', { hidden: true });
@@ -154,6 +176,7 @@ describe('Modal', () => {
154176
expect(asideSibling).not.toHaveAttribute('aria-hidden');
155177
expect(articleSibling).not.toHaveAttribute('aria-hidden');
156178
});
179+
157180
test('The modalBoxBody has no aria-label when bodyAriaLabel is not passed', () => {
158181
const props = {
159182
isOpen: true

packages/react-core/src/next/components/Modal/Modal.tsx

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ export enum ModalVariant {
5858
}
5959

6060
interface ModalState {
61-
container: HTMLElement;
6261
ouiaStateId: string;
6362
}
6463

6564
class Modal extends React.Component<ModalProps, ModalState> {
6665
static displayName = 'Modal';
6766
static currentId = 0;
6867
boxId = '';
68+
backdropId = '';
6969

7070
static defaultProps: PickOptional<ModalProps> = {
7171
isOpen: false,
@@ -78,10 +78,11 @@ class Modal extends React.Component<ModalProps, ModalState> {
7878
constructor(props: ModalProps) {
7979
super(props);
8080
const boxIdNum = Modal.currentId++;
81+
const backdropId = boxIdNum + 1;
8182
this.boxId = props.id || `pf-modal-part-${boxIdNum}`;
83+
this.backdropId = `pf-modal-part-${backdropId}`;
8284

8385
this.state = {
84-
container: undefined,
8586
ouiaStateId: getDefaultOUIAId(Modal.displayName, props.variant)
8687
};
8788
}
@@ -105,7 +106,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
105106
const target: HTMLElement = this.getElement(appendTo);
106107
const bodyChildren = target.children;
107108
for (const child of Array.from(bodyChildren)) {
108-
if (child !== this.state.container) {
109+
if (child.id !== this.backdropId) {
109110
hide ? child.setAttribute('aria-hidden', '' + hide) : child.removeAttribute('aria-hidden');
110111
}
111112
}
@@ -116,44 +117,38 @@ class Modal extends React.Component<ModalProps, ModalState> {
116117
componentDidMount() {
117118
const { appendTo } = this.props;
118119
const target: HTMLElement = this.getElement(appendTo);
119-
const container = document.createElement('div');
120-
this.setState({ container });
121-
target.appendChild(container);
122120
target.addEventListener('keydown', this.handleEscKeyClick, false);
123121

124122
if (this.props.isOpen) {
125123
target.classList.add(css(styles.backdropOpen));
126-
} else {
127-
target.classList.remove(css(styles.backdropOpen));
124+
this.toggleSiblingsFromScreenReaders(true);
128125
}
129126
}
130127

131-
componentDidUpdate() {
128+
componentDidUpdate(prevProps: ModalProps) {
132129
const { appendTo } = this.props;
133130
const target: HTMLElement = this.getElement(appendTo);
134131
if (this.props.isOpen) {
135132
target.classList.add(css(styles.backdropOpen));
136133
this.toggleSiblingsFromScreenReaders(true);
137134
} else {
138-
target.classList.remove(css(styles.backdropOpen));
139-
this.toggleSiblingsFromScreenReaders(false);
135+
if (prevProps.isOpen !== this.props.isOpen) {
136+
target.classList.remove(css(styles.backdropOpen));
137+
this.toggleSiblingsFromScreenReaders(false);
138+
}
140139
}
141140
}
142141

143142
componentWillUnmount() {
144143
const { appendTo } = this.props;
145144
const target: HTMLElement = this.getElement(appendTo);
146-
if (this.state.container) {
147-
target.removeChild(this.state.container);
148-
}
149145
target.removeEventListener('keydown', this.handleEscKeyClick, false);
150146
target.classList.remove(css(styles.backdropOpen));
151147
this.toggleSiblingsFromScreenReaders(false);
152148
}
153149

154150
render() {
155151
const {
156-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
157152
appendTo,
158153
// eslint-disable-next-line @typescript-eslint/no-unused-vars
159154
onEscapePress,
@@ -166,15 +161,15 @@ class Modal extends React.Component<ModalProps, ModalState> {
166161
elementToFocus,
167162
...props
168163
} = this.props;
169-
const { container } = this.state;
170164

171-
if (!canUseDOM || !container) {
165+
if (!canUseDOM || !this.getElement(appendTo)) {
172166
return null;
173167
}
174168

175169
return ReactDOM.createPortal(
176170
<ModalContent
177171
boxId={this.boxId}
172+
backdropId={this.backdropId}
178173
aria-label={ariaLabel}
179174
aria-describedby={ariaDescribedby}
180175
aria-labelledby={ariaLabelledby}
@@ -184,7 +179,7 @@ class Modal extends React.Component<ModalProps, ModalState> {
184179
elementToFocus={elementToFocus}
185180
{...props}
186181
/>,
187-
container
182+
this.getElement(appendTo)
188183
) as React.ReactElement;
189184
}
190185
}

packages/react-core/src/next/components/Modal/ModalContent.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export interface ModalContentProps extends OUIAProps {
1616
'aria-labelledby'?: string;
1717
/** Id of the modal box container. */
1818
boxId: string;
19+
/** Id of the backdrop. */
20+
backdropId?: string;
1921
/** Content rendered inside the modal. */
2022
children: React.ReactNode;
2123
/** Additional classes added to the modal box. */
@@ -60,6 +62,7 @@ export const ModalContent: React.FunctionComponent<ModalContentProps> = ({
6062
width,
6163
maxWidth,
6264
boxId,
65+
backdropId,
6366
disableFocusTrap = false,
6467
ouiaId,
6568
ouiaSafe = true,
@@ -106,7 +109,7 @@ export const ModalContent: React.FunctionComponent<ModalContentProps> = ({
106109
</ModalBox>
107110
);
108111
return (
109-
<Backdrop>
112+
<Backdrop id={backdropId}>
110113
<FocusTrap
111114
active={!disableFocusTrap}
112115
focusTrapOptions={{

0 commit comments

Comments
 (0)