Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aboutmodal,wizard): append component to any element in DOM #3102

Merged
merged 1 commit into from Oct 15, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

feat(aboutmodal,wizard): append component to any element in DOM

Signed-off-by: Boaz Shuster <boaz.shuster.github@gmail.com>
  • Loading branch information
boaz0 committed Oct 8, 2019
commit f6ea277d9d3ea22b13c076a8dff7b42d54e12783
@@ -6,7 +6,7 @@ import { KEY_CODES } from '../../helpers/constants';

const mockListener = jest.spyOn(ReactDOM, 'createPortal');
jest.spyOn(document, 'createElement');
jest.spyOn(document, 'addEventListener');
jest.spyOn(document.body, 'addEventListener');

mockListener.mockImplementation(node => node as React.ReactPortal);

@@ -34,15 +34,15 @@ test('About Modal closes with escape', () => {
Test About Modal
</AboutModal>
);
const [event, handler] = (document.addEventListener as any).mock.calls[0];
const [event, handler] = (document.body.addEventListener as any).mock.calls[0];
expect(event).toBe('keydown');
handler({ keyCode: KEY_CODES.ESCAPE_KEY });
expect(props.onClose).toBeCalled();
});

test('modal does not call onClose for esc key if it is not open', () => {
shallow(<AboutModal {...props} />);
const [event, handler] = (document.addEventListener as any).mock.calls[0];
const [event, handler] = (document.body.addEventListener as any).mock.calls[0];
expect(event).toBe('keydown');
handler({ keyCode: KEY_CODES.ESCAPE_KEY });
expect(props.onClose).not.toBeCalled();
@@ -27,6 +27,8 @@ export interface AboutModalProps {
backgroundImageSrc?: string;
/** Prevents the about modal from rendering content inside a container; allows for more flexible layouts */
noAboutModalBoxContentContainer?: boolean;
/** The parent container to append the modal to. Defaults to document.body */
appendTo?: HTMLElement | (() => HTMLElement);
}

interface ModalState {
@@ -46,7 +48,8 @@ export class AboutModal extends React.Component<AboutModalProps, ModalState> {
productName: '',
trademark: '',
backgroundImageSrc: '',
noAboutModalBoxContentContainer: false
noAboutModalBoxContentContainer: false,
appendTo: null as HTMLElement
};

constructor(props: AboutModalProps) {
@@ -68,42 +71,55 @@ export class AboutModal extends React.Component<AboutModalProps, ModalState> {
};

toggleSiblingsFromScreenReaders = (hide: boolean) => {
const bodyChildren = document.body.children;
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');
}
}
};

getElement = (appendTo: HTMLElement | (() => HTMLElement)) => {
if (typeof appendTo === 'function') {
return appendTo();
}
return appendTo || document.body;
};

componentDidMount() {
const container = document.createElement('div');
const target: HTMLElement = this.getElement(this.props.appendTo);
this.setState({ container });
document.body.appendChild(container);
document.addEventListener('keydown', this.handleEscKeyClick, false);
target.appendChild(container);
target.addEventListener('keydown', this.handleEscKeyClick, false);
This conversation was marked as resolved by boaz0

This comment has been minimized.

Copy link
@dlabrecq

dlabrecq Oct 10, 2019

Member

Considering the default value for appendTo is null, it appears that target.appendChild could generate a null exception.

This comment has been minimized.

Copy link
@boaz0

boaz0 Oct 10, 2019

Author Member

I can add a check before calling these making sure it's not null.
But when target is null it means window.document is not defined which is odd.

This comment has been minimized.

Copy link
@dlabrecq

dlabrecq Oct 10, 2019

Member

I saw the check for document !== 'undefined', so assumed there's a condition where it could be null. Maybe just use appendTo: document.body?


if (this.props.isOpen) {
document.body.classList.add(css(styles.backdropOpen));
target.classList.add(css(styles.backdropOpen));
} else {
document.body.classList.remove(css(styles.backdropOpen));
target.classList.remove(css(styles.backdropOpen));
}
}

componentDidUpdate() {
const target: HTMLElement = this.getElement(this.props.appendTo);
if (this.props.isOpen) {
document.body.classList.add(css(styles.backdropOpen));
target.classList.add(css(styles.backdropOpen));
this.toggleSiblingsFromScreenReaders(true);
} else {
document.body.classList.remove(css(styles.backdropOpen));
target.classList.remove(css(styles.backdropOpen));
this.toggleSiblingsFromScreenReaders(false);
}
}

componentWillUnmount() {
const target: HTMLElement = this.getElement(this.props.appendTo);
if (this.state.container) {
document.body.removeChild(this.state.container);
target.removeChild(this.state.container);
}
document.removeEventListener('keydown', this.handleEscKeyClick, false);
target.removeEventListener('keydown', this.handleEscKeyClick, false);
target.classList.remove(css(styles.backdropOpen));
}

render() {
@@ -94,6 +94,8 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
cancelButtonText?: string;
/** (Unused if footer is controlled) aria-label for the close button */
ariaLabelCloseButton?: string;
/** The parent container to append the modal to. Defaults to document.body */
appendTo?: HTMLElement | (() => HTMLElement);
}

interface WizardState {
@@ -125,7 +127,8 @@ export class Wizard extends React.Component<WizardProps, WizardState> {
width: null as string,
height: null as string,
footer: null as React.ReactNode,
onClose: () => undefined as any
onClose: () => undefined as any,
appendTo: null as HTMLElement
};
private container: HTMLDivElement;
private titleId: string;
@@ -161,7 +164,9 @@ export class Wizard extends React.Component<WizardProps, WizardState> {
};

private toggleSiblingsFromScreenReaders = (hide: boolean): void => {
const bodyChildren = document.body.children;
const { appendTo } = this.props;
const target: HTMLElement = this.getElement(appendTo);
const bodyChildren = target.children;
for (const child of Array.from(bodyChildren)) {
if (child !== this.container) {
hide ? child.setAttribute('aria-hidden', '' + hide) : child.removeAttribute('aria-hidden');
@@ -294,23 +299,34 @@ export class Wizard extends React.Component<WizardProps, WizardState> {
return steps;
};

getElement = (appendTo: HTMLElement | (() => HTMLElement)) => {
if (typeof appendTo === 'function') {
return appendTo();
}
return appendTo || document.body;
};

componentDidMount() {
const { appendTo } = this.props;
const target: HTMLElement = this.getElement(appendTo);
if (this.isModal) {
if (this.container) {
document.body.appendChild(this.container);
target.appendChild(this.container);
}
this.toggleSiblingsFromScreenReaders(true);
document.addEventListener('keydown', this.handleKeyClicks, false);
target.addEventListener('keydown', this.handleKeyClicks, false);
}
}

componentWillUnmount() {
const { appendTo } = this.props;
const target: HTMLElement = this.getElement(appendTo);
if (this.isModal) {
if (this.container) {
document.body.removeChild(this.container);
target.removeChild(this.container);
}
this.toggleSiblingsFromScreenReaders(false);
document.removeEventListener('keydown', this.handleKeyClicks, false);
target.removeEventListener('keydown', this.handleKeyClicks, false);
}
}

@@ -36,6 +36,7 @@ exports[`Wizard should match snapshot 1`] = `
}
>
<div
appendTo={null}
aria-describedby="pf-wizard-description-0"
aria-labelledby="pf-wizard-title-0"
aria-modal="true"
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.