Skip to content

Commit f9bc6a0

Browse files
Hancock, Matthewdiasbruno
Hancock, Matthew
authored andcommitted
[fixed] strict matching for tabbable nodes
1 parent e7c4a63 commit f9bc6a0

File tree

6 files changed

+213
-2
lines changed

6 files changed

+213
-2
lines changed

examples/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ <h2>an accessible React modal dialog component</h2>
1414
<div id="example" class="padbox">
1515
<a class="btn btn-primary" href="/basic/">Basic</a>
1616
<a class="btn btn-primary" href="/bootstrap/">Bootstrap</a>
17+
<a class="btn btn-primary" href="/wc/">Web Component</a>
1718
</div>
1819
<a target="_top" href="https://github.com/reactjs/react-modal"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://github-camo.global.ssl.fastly.net/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"></a>
1920
</body>

examples/wc/app.css

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.ReactModal__Overlay {
2+
-webkit-perspective: 600;
3+
perspective: 600;
4+
opacity: 0;
5+
overflow-x: hidden;
6+
overflow-y: auto;
7+
background-color: rgba(0, 0, 0, 0.5);
8+
}
9+
10+
.ReactModal__Overlay--after-open {
11+
opacity: 1;
12+
transition: opacity 150ms ease-out;
13+
}
14+
15+
.ReactModal__Content {
16+
-webkit-transform: scale(0.5) rotateX(-30deg);
17+
transform: scale(0.5) rotateX(-30deg);
18+
}
19+
20+
.ReactModal__Content--after-open {
21+
-webkit-transform: scale(1) rotateX(0deg);
22+
transform: scale(1) rotateX(0deg);
23+
transition: all 150ms ease-in;
24+
}
25+
26+
.ReactModal__Overlay--before-close {
27+
opacity: 0;
28+
}
29+
30+
.ReactModal__Content--before-close {
31+
-webkit-transform: scale(0.5) rotateX(30deg);
32+
transform: scale(0.5) rotateX(30deg);
33+
transition: all 150ms ease-in;
34+
}
35+
36+
.ReactModal__Content.modal-dialog {
37+
border: none;
38+
background-color: transparent;
39+
}

examples/wc/app.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { Component } from 'react';
2+
import ReactDOM from 'react-dom';
3+
import Modal from 'react-modal';
4+
5+
import '@webcomponents/custom-elements/src/native-shim';
6+
7+
var appElement = document.getElementById('example');
8+
9+
Modal.setAppElement(appElement);
10+
11+
class App extends Component {
12+
constructor(props) {
13+
super(props);
14+
this.state = { modalIsOpen: false };
15+
}
16+
17+
openModal = () => {
18+
this.setState({modalIsOpen: true});
19+
}
20+
21+
closeModal = () => {
22+
this.setState({modalIsOpen: false});
23+
}
24+
25+
handleModalCloseRequest = () => {
26+
// opportunity to validate something and keep the modal open even if it
27+
// requested to be closed
28+
this.setState({modalIsOpen: false});
29+
}
30+
31+
handleSaveClicked = (e) => {
32+
alert('Save button was clicked');
33+
}
34+
35+
render() {
36+
return (
37+
<div>
38+
<button type="button" className="btn btn-primary" onClick={this.openModal}>Open Modal</button>
39+
<Modal
40+
className="Modal__Bootstrap modal-dialog"
41+
closeTimeoutMS={150}
42+
isOpen={this.state.modalIsOpen}
43+
onRequestClose={this.handleModalCloseRequest}
44+
>
45+
<div className="modal-content">
46+
<div className="modal-header">
47+
<h4 className="modal-title">Modal title</h4>
48+
<div className="close">
49+
<awesome-button></awesome-button>
50+
<button type="button" className="close" onClick={this.handleModalCloseRequest}>
51+
<span aria-hidden="true">&times;</span>
52+
<span className="sr-only">Close</span>
53+
</button>
54+
</div>
55+
</div>
56+
<div className="modal-body">
57+
<h4>Really long content...</h4>
58+
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
59+
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
60+
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
61+
</div>
62+
<div className="modal-footer">
63+
<button type="button" className="btn btn-secondary" onClick={this.handleModalCloseRequest}>Close</button>
64+
<button type="button" className="btn btn-primary" onClick={this.handleSaveClicked}>Save changes</button>
65+
</div>
66+
</div>
67+
</Modal>
68+
</div>
69+
);
70+
}
71+
}
72+
73+
ReactDOM.render(<App/>, appElement);
74+
75+
class AwesomeButton extends HTMLElement {
76+
constructor() {
77+
super();
78+
}
79+
80+
// this shows with no shadow root
81+
connectedCallback() {
82+
this.innerHTML = `
83+
<button>I'm Awesome!</button>
84+
`;
85+
}
86+
}
87+
88+
customElements.define("awesome-button", AwesomeButton);

examples/wc/index.html

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!doctype html public "embarassment">
2+
<html>
3+
<head>
4+
<title>Bootstrap-Style Example</title>
5+
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
6+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
7+
<link rel="stylesheet" href="/base.css" type="text/css" />
8+
<link rel="stylesheet" href="app.css"/>
9+
</head>
10+
<body>
11+
<header class="branding padbox">
12+
<h1>react-modal</h1>
13+
<h2>an accessible React modal dialog component</h2>
14+
</header>
15+
<div id="example" class="padbox"></div>
16+
<a target="_top" href="https://github.com/reactjs/react-modal"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://github-camo.global.ssl.fastly.net/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"></a>
17+
<script type="application/javascript" src="/__build__/wc.js"></script>
18+
</body>
19+
</html>

specs/Modal.helpers.spec.js

+64-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,28 @@ export default () => {
116116
tabbable(elem).should.not.containEql(button);
117117
});
118118

119-
describe("inside Web Components", () => {
119+
it("excludes elements that contain reserved node names", () => {
120+
const button = document.createElement("button");
121+
button.innerHTML = "I am a good button";
122+
elem.appendChild(button);
123+
124+
const badButton = document.createElement("bad-button");
125+
badButton.innerHTML = "I am a bad button";
126+
elem.appendChild(badButton);
127+
128+
tabbable(elem).should.deepEqual([button]);
129+
});
130+
131+
it("includes elements that contain reserved node names with tabindex", () => {
132+
const trickButton = document.createElement("trick-button");
133+
trickButton.innerHTML = "I am a good button";
134+
trickButton.tabIndex = '0';
135+
elem.appendChild(trickButton);
136+
137+
tabbable(elem).should.deepEqual([trickButton]);
138+
});
139+
140+
describe("inside Web Components with shadow dom", () => {
120141
let wc;
121142
let input;
122143
class TestWebComponent extends HTMLElement {
@@ -169,6 +190,48 @@ export default () => {
169190
tabbable(elem).should.not.containEql(input);
170191
});
171192
});
193+
194+
describe("inside Web Components with no shadow dom", () => {
195+
let wc;
196+
let button;
197+
class ButtonWebComponent extends HTMLElement {
198+
constructor() {
199+
super();
200+
}
201+
202+
connectedCallback() {
203+
this.innerHTML = '<button>Normal button</button>';
204+
this.style.display = "block";
205+
this.style.width = "100px";
206+
this.style.height = "25px";
207+
}
208+
}
209+
210+
const registerButtonComponent = () => {
211+
if (window.customElements.get("button-web-component")) {
212+
return;
213+
}
214+
window.customElements.define("button-web-component", ButtonWebComponent);
215+
};
216+
217+
beforeEach(() => {
218+
registerButtonComponent();
219+
wc = document.createElement("button-web-component");
220+
221+
elem.appendChild(wc);
222+
});
223+
224+
afterEach(() => {
225+
// remove Web Component
226+
elem.removeChild(wc);
227+
});
228+
229+
it("includes only focusable elements", () => {
230+
button = wc.querySelector('button');
231+
232+
tabbable(elem).should.deepEqual([button]);
233+
});
234+
});
172235
});
173236
});
174237
};

src/helpers/tabbable.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
const DISPLAY_NONE = "none";
1414
const DISPLAY_CONTENTS = "contents";
1515

16-
const tabbableNode = /input|select|textarea|button|object|iframe/;
16+
// match the whole word to prevent fuzzy searching
17+
const tabbableNode = /^(input|select|textarea|button|object|iframe)$/;
1718

1819
function isNotOverflowing(element, style) {
1920
return (

0 commit comments

Comments
 (0)