Skip to content

Commit

Permalink
Merge 10c16e2 into b4d85d5
Browse files Browse the repository at this point in the history
  • Loading branch information
NicholasBoll authored Mar 17, 2020
2 parents b4d85d5 + 10c16e2 commit 34dd6bf
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 4 deletions.
49 changes: 48 additions & 1 deletion cypress/integration/Modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ function getModalTargetButton() {

describe('Modal', () => {
before(() => {
cy.viewport(500, 300);
h.stories.visit();
});

Expand Down Expand Up @@ -39,6 +38,30 @@ describe('Modal', () => {
h.modal.get().should('be.visible');
});

it('should place the portal as a child of the body element', () => {
cy.get('body').then($body => {
h.modal
.get()
.parent() // wrapper
.parent()
.should($el => {
expect($el[0]).to.equal($body[0]);
});
});
});

it('should hide non-modal content from assistive technology', () => {
h.modal
.get()
.parent()
.siblings()
.should($siblings => {
$siblings.each((_, $sibling) => {
expect($sibling).to.have.attr('aria-hidden', 'true');
});
});
});

it('should not have any axe errors', () => {
cy.checkA11y();
});
Expand Down Expand Up @@ -161,6 +184,30 @@ describe('Modal', () => {
h.modal.get().should('be.visible');
});

it('should place the portal as a child of the body element', () => {
cy.get('body').then($body => {
h.modal
.get()
.parent() // wrapper
.parent()
.should($el => {
expect($el[0]).to.equal($body[0]);
});
});
});

it('should hide non-modal content from assistive technology', () => {
h.modal
.get()
.parent()
.siblings()
.should($siblings => {
$siblings.each((_, $sibling) => {
expect($sibling).to.have.attr('aria-hidden', 'true');
});
});
});

it('should not have any axe errors', () => {
cy.checkA11y();
});
Expand Down
38 changes: 37 additions & 1 deletion modules/modal/react/lib/ModalContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import styled from '@emotion/styled';
import {keyframes} from '@emotion/core';
import tabTrappingKey from 'focus-trap-js';
Expand Down Expand Up @@ -46,6 +47,17 @@ export interface ModalContentProps extends React.HTMLAttributes<HTMLDivElement>
* it will make the modal heading focusable and focus on that instead.
*/
firstFocusRef?: React.RefObject<HTMLElement>;
/**
* The containing element for the Modal elements. The Modal uses
* {@link https://reactjs.org/docs/portals.html Portals} to place the DOM elements
* of the Modal in a different place in the DOM to prevent issues with overflowed containers.
* When the modal is opened, `aria-hidden` will be added to siblings to hide background
* content from assistive technology like it is visibly hidden from sighted users. This property
* should be set to the element that the application root goes - not containing element of content.
* This should be a sibling or higher than the header and navigation elements of the application.
* @default document.body
*/
container?: HTMLElement;
}

const fadeIn = keyframes`
Expand Down Expand Up @@ -147,6 +159,7 @@ const ModalContent = ({
width,
heading,
padding,
container = document.body,
...elemProps
}: ModalContentProps): JSX.Element => {
const modalRef = React.useRef<HTMLDivElement>(null);
Expand All @@ -172,7 +185,28 @@ const ModalContent = ({
}
};

return (
React.useEffect(() => {
const siblings = [...((container.children as any) as HTMLElement[])].filter(
el => el !== modalRef.current!.parentElement
);
const prevAriaHidden = siblings.map(el => el.getAttribute('aria-hidden'));
siblings.forEach(el => {
el.setAttribute('aria-hidden', 'true');
});

return () => {
siblings.forEach((el, index) => {
const prev = prevAriaHidden[index];
if (prev) {
el.setAttribute('aria-hidden', prev);
} else {
el.removeAttribute('aria-hidden');
}
});
};
}, []);

const content = (
<Container onClick={handleOutsideClick} {...elemProps}>
<Popup
popupRef={modalRef}
Expand All @@ -186,6 +220,8 @@ const ModalContent = ({
</Popup>
</Container>
);

return ReactDOM.createPortal(content, container);
};

export default ModalContent;
4 changes: 2 additions & 2 deletions modules/modal/react/spec/Modal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ describe('Modal', () => {

test('Modal should spread extra props', async () => {
const cb = jest.fn();
const {container} = render(
const {getByRole} = render(
<Modal handleClose={cb} heading="Test" open={true} data-propspread="test" />
);

expect(container.firstChild).toHaveAttribute('data-propspread', 'test');
expect(getByRole('dialog').parentElement).toHaveAttribute('data-propspread', 'test');
});
});
81 changes: 81 additions & 0 deletions modules/modal/react/stories/stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,81 @@ const DefaultModalExample = () => {
<DeleteButton buttonRef={buttonRef} onClick={openModal}>
Delete Item
</DeleteButton>

<Modal data-testid="TestModal" heading="Delete Item" open={open} handleClose={closeModal}>
<p>Are you sure you'd like to delete the item titled 'My Item'?</p>
<DeleteButton style={{marginRight: '16px'}} onClick={closeModal}>
Delete
</DeleteButton>
<Button onClick={closeModal} variant={Button.Variant.Secondary}>
Cancel
</Button>
</Modal>
</>
);
};

const AccessibleModalExample = () => {
const [open, setOpen] = React.useState(false);
const buttonRef = React.useRef<HTMLButtonElement>() as React.RefObject<HTMLButtonElement>; // cast to keep buttonRef happy
const openModal = () => {
setOpen(true);
};
const closeModal = () => {
setOpen(false);
if (buttonRef.current) {
buttonRef.current.focus();
}
};

return (
<>
<DeleteButton buttonRef={buttonRef} onClick={openModal}>
Delete Item
</DeleteButton>
<p>The content below should be hidden from assistive technology while the modal is open.</p>
<p>
<a href="#">Link</a>
</p>

<button type="button">Button</button>
<p tabIndex={0}>Focusable div</p>

<div>
<label htmlFor="text">Text input</label>
<input type="text" id="text" />
</div>

<div>
<label htmlFor="radio">Radio</label> <input type="radio" id="radio" />
</div>

<div>
<label htmlFor="check">Checkbox</label>
<input type="checkbox" />
</div>

<div>
<label htmlFor="textarea">Text area</label>
<textarea id="textarea"></textarea>
</div>

<div>
<label htmlFor="pet-select">Choose a pet:</label>
<select name="pets" id="pet-select">
<option value="">Please choose an option</option>
<option value="dog">Dog</option>
<option value="cat">Cat</option>
<option value="hamster">Hamster</option>
<option value="parrot">Parrot</option>
<option value="spider">Spider</option>
<option value="goldfish">Goldfish</option>
</select>
</div>

<div>
<iframe title="iframe test" src="https://workday.com/" width="300" height="300"></iframe>
</div>
<Modal data-testid="TestModal" heading="Delete Item" open={open} handleClose={closeModal}>
<p>Are you sure you'd like to delete the item titled 'My Item'?</p>
<DeleteButton style={{marginRight: '16px'}} onClick={closeModal}>
Expand Down Expand Up @@ -116,6 +191,12 @@ storiesOf('Components|Popups/Modal/React', module)
<DefaultModalExample />
</div>
))
.add('Accessible', () => (
<div className="story">
<h1 className="section-label">Modal</h1>
<AccessibleModalExample />
</div>
))
.add('With useModal hook', () => (
<div className="story">
<h1 className="section-label">Modal</h1>
Expand Down

0 comments on commit 34dd6bf

Please sign in to comment.