UNPKG

13.9 kBJavaScriptView Raw
1import { __assign, __extends } from "tslib";
2import * as React from 'react';
3import { modalize } from '@uifabric/utilities';
4import { elementContains, getNativeProps, divProperties, getFirstTabbable, getLastTabbable, getNextElement, getDocument, focusAsync, initializeComponentRef, on, } from '../../Utilities';
5var FocusTrapZone = /** @class */ (function (_super) {
6 __extends(FocusTrapZone, _super);
7 function FocusTrapZone(props) {
8 var _this = _super.call(this, props) || this;
9 _this._root = React.createRef();
10 _this._firstBumper = React.createRef();
11 _this._lastBumper = React.createRef();
12 _this._hasFocus = false;
13 _this._onRootFocus = function (ev) {
14 if (_this.props.onFocus) {
15 _this.props.onFocus(ev);
16 }
17 _this._hasFocus = true;
18 };
19 _this._onRootBlur = function (ev) {
20 if (_this.props.onBlur) {
21 _this.props.onBlur(ev);
22 }
23 var relatedTarget = ev.relatedTarget;
24 if (ev.relatedTarget === null) {
25 // In IE11, due to lack of support, event.relatedTarget is always
26 // null making every onBlur call to be "outside" of the ComboBox
27 // even when it's not. Using document.activeElement is another way
28 // for us to be able to get what the relatedTarget without relying
29 // on the event
30 relatedTarget = _this._getDocument().activeElement;
31 }
32 if (!elementContains(_this._root.current, relatedTarget)) {
33 _this._hasFocus = false;
34 }
35 };
36 _this._onFirstBumperFocus = function () {
37 _this._onBumperFocus(true);
38 };
39 _this._onLastBumperFocus = function () {
40 _this._onBumperFocus(false);
41 };
42 _this._onBumperFocus = function (isFirstBumper) {
43 if (_this.props.disabled) {
44 return;
45 }
46 var currentBumper = (isFirstBumper === _this._hasFocus
47 ? _this._lastBumper.current
48 : _this._firstBumper.current);
49 if (_this._root.current) {
50 var nextFocusable = isFirstBumper === _this._hasFocus
51 ? getLastTabbable(_this._root.current, currentBumper, true, false)
52 : getFirstTabbable(_this._root.current, currentBumper, true, false);
53 if (nextFocusable) {
54 if (_this._isBumper(nextFocusable)) {
55 // This can happen when FTZ contains no tabbable elements.
56 // focus will take care of finding a focusable element in FTZ.
57 _this.focus();
58 }
59 else {
60 nextFocusable.focus();
61 }
62 }
63 }
64 };
65 _this._onFocusCapture = function (ev) {
66 if (_this.props.onFocusCapture) {
67 _this.props.onFocusCapture(ev);
68 }
69 if (ev.target !== ev.currentTarget && !_this._isBumper(ev.target)) {
70 // every time focus changes within the trap zone, remember the focused element so that
71 // it can be restored if focus leaves the pane and returns via keystroke (i.e. via a call to this.focus(true))
72 _this._previouslyFocusedElementInTrapZone = ev.target;
73 }
74 };
75 _this._forceFocusInTrap = function (ev) {
76 if (_this.props.disabled) {
77 return;
78 }
79 if (FocusTrapZone._focusStack.length && _this === FocusTrapZone._focusStack[FocusTrapZone._focusStack.length - 1]) {
80 var focusedElement = _this._getDocument().activeElement;
81 if (!elementContains(_this._root.current, focusedElement)) {
82 _this.focus();
83 _this._hasFocus = true; // set focus here since we stop event propagation
84 ev.preventDefault();
85 ev.stopPropagation();
86 }
87 }
88 };
89 _this._forceClickInTrap = function (ev) {
90 if (_this.props.disabled) {
91 return;
92 }
93 if (FocusTrapZone._focusStack.length && _this === FocusTrapZone._focusStack[FocusTrapZone._focusStack.length - 1]) {
94 var clickedElement = ev.target;
95 if (clickedElement && !elementContains(_this._root.current, clickedElement)) {
96 _this.focus();
97 _this._hasFocus = true; // set focus here since we stop event propagation
98 ev.preventDefault();
99 ev.stopPropagation();
100 }
101 }
102 };
103 initializeComponentRef(_this);
104 return _this;
105 }
106 FocusTrapZone.prototype.componentDidMount = function () {
107 this._bringFocusIntoZone();
108 this._updateEventHandlers(this.props);
109 if (!this.props.disabled && this._root.current && this.props.enableAriaHiddenSiblings) {
110 this._unmodalize = modalize(this._root.current);
111 }
112 };
113 FocusTrapZone.prototype.UNSAFE_componentWillReceiveProps = function (nextProps) {
114 var elementToFocusOnDismiss = nextProps.elementToFocusOnDismiss;
115 if (elementToFocusOnDismiss && this._previouslyFocusedElementOutsideTrapZone !== elementToFocusOnDismiss) {
116 this._previouslyFocusedElementOutsideTrapZone = elementToFocusOnDismiss;
117 }
118 this._updateEventHandlers(nextProps);
119 };
120 FocusTrapZone.prototype.componentDidUpdate = function (prevProps) {
121 var prevForceFocusInsideTrap = prevProps.forceFocusInsideTrap !== undefined ? prevProps.forceFocusInsideTrap : true;
122 var newForceFocusInsideTrap = this.props.forceFocusInsideTrap !== undefined ? this.props.forceFocusInsideTrap : true;
123 var prevDisabled = prevProps.disabled !== undefined ? prevProps.disabled : false;
124 var newDisabled = this.props.disabled !== undefined ? this.props.disabled : false;
125 if ((!prevForceFocusInsideTrap && newForceFocusInsideTrap) || (prevDisabled && !newDisabled)) {
126 // Transition from forceFocusInsideTrap / FTZ disabled to enabled.
127 // Emulate what happens when a FocusTrapZone gets mounted.
128 this._bringFocusIntoZone();
129 if (!this._unmodalize && this._root.current && this.props.enableAriaHiddenSiblings) {
130 this._unmodalize = modalize(this._root.current);
131 }
132 }
133 else if ((prevForceFocusInsideTrap && !newForceFocusInsideTrap) || (!prevDisabled && newDisabled)) {
134 // Transition from forceFocusInsideTrap / FTZ enabled to disabled.
135 // Emulate what happens when a FocusTrapZone gets unmounted.
136 this._returnFocusToInitiator();
137 if (this._unmodalize) {
138 this._unmodalize();
139 }
140 }
141 };
142 FocusTrapZone.prototype.componentWillUnmount = function () {
143 // don't handle return focus unless forceFocusInsideTrap is true or focus is still within FocusTrapZone
144 if (!this.props.disabled ||
145 this.props.forceFocusInsideTrap ||
146 !elementContains(this._root.current, this._getDocument().activeElement)) {
147 this._returnFocusToInitiator();
148 }
149 // Dispose of event handlers so their closures can be garbage-collected
150 if (this._disposeClickHandler) {
151 this._disposeClickHandler();
152 this._disposeClickHandler = undefined;
153 }
154 if (this._disposeFocusHandler) {
155 this._disposeFocusHandler();
156 this._disposeFocusHandler = undefined;
157 }
158 if (this._unmodalize) {
159 this._unmodalize();
160 }
161 // Dispose of element references so the DOM Nodes can be garbage-collected
162 delete this._previouslyFocusedElementInTrapZone;
163 delete this._previouslyFocusedElementOutsideTrapZone;
164 };
165 FocusTrapZone.prototype.render = function () {
166 var _a = this.props, className = _a.className, _b = _a.disabled, disabled = _b === void 0 ? false : _b, ariaLabelledBy = _a.ariaLabelledBy;
167 var divProps = getNativeProps(this.props, divProperties);
168 var bumperProps = {
169 'aria-hidden': true,
170 style: {
171 pointerEvents: 'none',
172 position: 'fixed',
173 },
174 tabIndex: disabled ? -1 : 0,
175 'data-is-visible': true,
176 };
177 return (React.createElement("div", __assign({}, divProps, { className: className, ref: this._root, "aria-labelledby": ariaLabelledBy, onFocusCapture: this._onFocusCapture, onFocus: this._onRootFocus, onBlur: this._onRootBlur }),
178 React.createElement("div", __assign({}, bumperProps, { ref: this._firstBumper, onFocus: this._onFirstBumperFocus })),
179 this.props.children,
180 React.createElement("div", __assign({}, bumperProps, { ref: this._lastBumper, onFocus: this._onLastBumperFocus }))));
181 };
182 FocusTrapZone.prototype.focus = function () {
183 // eslint-disable-next-line deprecation/deprecation
184 var _a = this.props, focusPreviouslyFocusedInnerElement = _a.focusPreviouslyFocusedInnerElement, firstFocusableSelector = _a.firstFocusableSelector, firstFocusableTarget = _a.firstFocusableTarget;
185 if (focusPreviouslyFocusedInnerElement &&
186 this._previouslyFocusedElementInTrapZone &&
187 elementContains(this._root.current, this._previouslyFocusedElementInTrapZone)) {
188 // focus on the last item that had focus in the zone before we left the zone
189 this._focusAsync(this._previouslyFocusedElementInTrapZone);
190 return;
191 }
192 var focusSelector = typeof firstFocusableSelector === 'string'
193 ? firstFocusableSelector
194 : firstFocusableSelector && firstFocusableSelector();
195 var _firstFocusableChild = null;
196 if (this._root.current) {
197 if (typeof firstFocusableTarget === 'string') {
198 _firstFocusableChild = this._root.current.querySelector(firstFocusableTarget);
199 }
200 else if (firstFocusableTarget) {
201 _firstFocusableChild = firstFocusableTarget(this._root.current);
202 }
203 else if (focusSelector) {
204 _firstFocusableChild = this._root.current.querySelector('.' + focusSelector);
205 }
206 // Fall back to first element if query selector did not match any elements.
207 if (!_firstFocusableChild) {
208 _firstFocusableChild = getNextElement(this._root.current, this._root.current.firstChild, false, false, false, true);
209 }
210 }
211 if (_firstFocusableChild) {
212 this._focusAsync(_firstFocusableChild);
213 }
214 };
215 FocusTrapZone.prototype._focusAsync = function (element) {
216 if (!this._isBumper(element)) {
217 focusAsync(element);
218 }
219 };
220 FocusTrapZone.prototype._bringFocusIntoZone = function () {
221 var _a = this.props, elementToFocusOnDismiss = _a.elementToFocusOnDismiss, _b = _a.disabled, disabled = _b === void 0 ? false : _b, _c = _a.disableFirstFocus, disableFirstFocus = _c === void 0 ? false : _c;
222 if (disabled) {
223 return;
224 }
225 FocusTrapZone._focusStack.push(this);
226 this._previouslyFocusedElementOutsideTrapZone = elementToFocusOnDismiss
227 ? elementToFocusOnDismiss
228 : this._getDocument().activeElement;
229 if (!disableFirstFocus && !elementContains(this._root.current, this._previouslyFocusedElementOutsideTrapZone)) {
230 this.focus();
231 }
232 };
233 FocusTrapZone.prototype._returnFocusToInitiator = function () {
234 var _this = this;
235 var ignoreExternalFocusing = this.props.ignoreExternalFocusing;
236 FocusTrapZone._focusStack = FocusTrapZone._focusStack.filter(function (value) {
237 return _this !== value;
238 });
239 var doc = this._getDocument();
240 var activeElement = doc.activeElement;
241 if (!ignoreExternalFocusing &&
242 this._previouslyFocusedElementOutsideTrapZone &&
243 typeof this._previouslyFocusedElementOutsideTrapZone.focus === 'function' &&
244 (elementContains(this._root.current, activeElement) || activeElement === doc.body)) {
245 this._focusAsync(this._previouslyFocusedElementOutsideTrapZone);
246 }
247 };
248 FocusTrapZone.prototype._updateEventHandlers = function (newProps) {
249 var _a = newProps.isClickableOutsideFocusTrap, isClickableOutsideFocusTrap = _a === void 0 ? false : _a, _b = newProps.forceFocusInsideTrap, forceFocusInsideTrap = _b === void 0 ? true : _b;
250 if (forceFocusInsideTrap && !this._disposeFocusHandler) {
251 this._disposeFocusHandler = on(window, 'focus', this._forceFocusInTrap, true);
252 }
253 else if (!forceFocusInsideTrap && this._disposeFocusHandler) {
254 this._disposeFocusHandler();
255 this._disposeFocusHandler = undefined;
256 }
257 if (!isClickableOutsideFocusTrap && !this._disposeClickHandler) {
258 this._disposeClickHandler = on(window, 'click', this._forceClickInTrap, true);
259 }
260 else if (isClickableOutsideFocusTrap && this._disposeClickHandler) {
261 this._disposeClickHandler();
262 this._disposeClickHandler = undefined;
263 }
264 };
265 FocusTrapZone.prototype._isBumper = function (element) {
266 return element === this._firstBumper.current || element === this._lastBumper.current;
267 };
268 FocusTrapZone.prototype._getDocument = function () {
269 return getDocument(this._root.current);
270 };
271 FocusTrapZone._focusStack = [];
272 return FocusTrapZone;
273}(React.Component));
274export { FocusTrapZone };
275//# sourceMappingURL=FocusTrapZone.js.map
\No newline at end of file