UNPKG

3.05 kBJavaScriptView Raw
1import React, {Component} from 'react';
2import PropTypes from 'prop-types';
3
4import styles from './tab-trap.css';
5
6export const FOCUSABLE_ELEMENTS = 'input, button:not([data-trap-button]), select, textarea, a[href], *[tabindex]';
7
8/**
9 * @name TabTrap
10 */
11
12export default class TabTrap extends Component {
13 static propTypes = {
14 children: PropTypes.node.isRequired,
15 trapDisabled: PropTypes.bool,
16 autoFocusFirst: PropTypes.bool,
17 focusBackOnClose: PropTypes.bool
18 };
19
20 static defaultProps = {
21 trapDisabled: false,
22 autoFocusFirst: true,
23 focusBackOnClose: true
24 };
25
26 componentDidMount() {
27 this.previousFocusedNode = document.activeElement;
28
29 if (this.props.autoFocusFirst) {
30 this.focusFirst();
31 } else {
32 this.trapWithoutFocus = true;
33 this.trapButtonNode.focus();
34 }
35 }
36
37 componentWillUnmount() {
38 this.restoreFocus();
39 }
40
41 restoreFocus() {
42 if (!this.props.focusBackOnClose) {
43 return;
44 }
45 const {previousFocusedNode} = this;
46 if (previousFocusedNode && previousFocusedNode.focus) {
47 previousFocusedNode.focus();
48 }
49 }
50
51 containerRef = node => {
52 if (!node) {
53 return;
54 }
55 this.node = node;
56 };
57
58 focusElement = (first = true) => {
59 const {node} = this;
60 if (!node) {
61 return;
62 }
63
64 const tabables = [...node.querySelectorAll(FOCUSABLE_ELEMENTS)].
65 filter(item => item.tabIndex >= 0);
66
67 const toBeFocused = first ? tabables[0] : tabables[tabables.length - 1];
68
69 if (toBeFocused) {
70 toBeFocused.focus();
71 }
72 };
73
74 focusFirst = () => this.focusElement(true);
75
76 focusLast = () => this.focusElement(false);
77
78 focusLastIfEnabled = () => {
79 if (this.trapWithoutFocus) {
80 return;
81 }
82 this.focusLast();
83 };
84
85 handleBlurIfWithoutFocus = event => {
86 if (!this.trapWithoutFocus) {
87 return;
88 }
89 this.trapWithoutFocus = false;
90
91 const newFocused = event.nativeEvent.relatedTarget;
92 if (!newFocused) {
93 return;
94 }
95
96 if (this.node.contains(newFocused)) {
97 return;
98 }
99
100 this.focusLast();
101 };
102
103 trapButtonRef = node => {
104 if (!node) {
105 return;
106 }
107
108 this.trapButtonNode = node;
109 };
110
111 render() {
112 // eslint-disable-next-line no-unused-vars
113 const {children, trapDisabled, autoFocusFirst, focusBackOnClose, ...restProps} = this.props;
114
115 if (trapDisabled) {
116 return (
117 <div
118 ref={this.containerRef}
119 {...restProps}
120 >
121 {children}
122 </div>
123 );
124 }
125
126 return (
127 <div
128 ref={this.containerRef}
129 {...restProps}
130 >
131 <button
132 type="button"
133 ref={this.trapButtonRef}
134 className={styles.trapButton}
135 onFocus={this.focusLastIfEnabled}
136 onBlur={this.handleBlurIfWithoutFocus}
137 data-trap-button
138 />
139 {children}
140 <button
141 type="button"
142 className={styles.trapButton}
143 onFocus={this.focusFirst}
144 data-trap-button
145 />
146 </div>
147 );
148 }
149}