carbon-react 116.1.0 → 116.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/__internal__/focus-trap/focus-trap-utils.d.ts +6 -1
- package/esm/__internal__/focus-trap/focus-trap-utils.js +77 -7
- package/esm/__internal__/focus-trap/focus-trap.component.d.ts +1 -1
- package/esm/__internal__/focus-trap/focus-trap.component.js +93 -74
- package/lib/__internal__/focus-trap/focus-trap-utils.d.ts +6 -1
- package/lib/__internal__/focus-trap/focus-trap-utils.js +82 -8
- package/lib/__internal__/focus-trap/focus-trap.component.d.ts +1 -1
- package/lib/__internal__/focus-trap/focus-trap.component.js +92 -71
- package/package.json +1 -1
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
declare type CustomRefObject<T> = {
|
|
2
|
+
current?: T | null;
|
|
3
|
+
};
|
|
1
4
|
declare const defaultFocusableSelectors = "button:not([disabled]), [href], input:not([type=\"hidden\"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]";
|
|
2
5
|
declare const setElementFocus: (element: HTMLElement) => void;
|
|
3
6
|
declare const getNextElement: (element: HTMLElement, focusableElements: HTMLElement[], shiftKey: boolean) => HTMLElement | undefined;
|
|
4
|
-
|
|
7
|
+
declare const onTabGuardFocus: (trapWrappers: CustomRefObject<HTMLElement>[], focusableSelectors: string | undefined, position: "top" | "bottom") => (guardWrapperRef: CustomRefObject<HTMLElement>) => () => void;
|
|
8
|
+
declare const trapFunction: (ev: KeyboardEvent, defaultFocusableElements: HTMLElement[], isWrapperFocused: boolean, focusableSelectors?: string | undefined, bespokeTrap?: ((event: KeyboardEvent, firstElement?: HTMLElement | undefined, lastElement?: HTMLElement | undefined) => void) | undefined) => void;
|
|
9
|
+
export { defaultFocusableSelectors, getNextElement, setElementFocus, onTabGuardFocus, trapFunction, };
|
|
@@ -45,12 +45,6 @@ const getNextElement = (element, focusableElements, shiftKey) => {
|
|
|
45
45
|
if (currentIndex === -1) {
|
|
46
46
|
// we're not currently on a focusable element - most likely because the focusableElements come from a different focus trap!
|
|
47
47
|
// So we need to leave focus where it is.
|
|
48
|
-
// The exception is when the focus is on the document body - perhaps because the previously-focused element was dynamically removed.
|
|
49
|
-
// In that case focus the first element.
|
|
50
|
-
if (element === document.body) {
|
|
51
|
-
return focusableElements[0];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
48
|
return undefined;
|
|
55
49
|
}
|
|
56
50
|
|
|
@@ -110,4 +104,80 @@ const getNextElement = (element, focusableElements, shiftKey) => {
|
|
|
110
104
|
return foundElement;
|
|
111
105
|
};
|
|
112
106
|
|
|
113
|
-
|
|
107
|
+
const onTabGuardFocus = (trapWrappers, focusableSelectors, position) => guardWrapperRef => () => {
|
|
108
|
+
var _allFocusableElements2;
|
|
109
|
+
|
|
110
|
+
const isTop = position === "top";
|
|
111
|
+
const currentIndex = trapWrappers.indexOf(guardWrapperRef);
|
|
112
|
+
let index = currentIndex;
|
|
113
|
+
let nextWrapper, allFocusableElementsInNextWrapper;
|
|
114
|
+
|
|
115
|
+
do {
|
|
116
|
+
var _allFocusableElements, _nextWrapper, _nextWrapper$current;
|
|
117
|
+
|
|
118
|
+
index += isTop ? -1 : 1;
|
|
119
|
+
|
|
120
|
+
if (index < 0) {
|
|
121
|
+
index += trapWrappers.length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (index >= trapWrappers.length) {
|
|
125
|
+
index -= trapWrappers.length;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
nextWrapper = trapWrappers[index];
|
|
129
|
+
allFocusableElementsInNextWrapper = (_nextWrapper = nextWrapper) === null || _nextWrapper === void 0 ? void 0 : (_nextWrapper$current = _nextWrapper.current) === null || _nextWrapper$current === void 0 ? void 0 : _nextWrapper$current.querySelectorAll(focusableSelectors || defaultFocusableSelectors);
|
|
130
|
+
} while (index !== currentIndex && !((_allFocusableElements = allFocusableElementsInNextWrapper) !== null && _allFocusableElements !== void 0 && _allFocusableElements.length));
|
|
131
|
+
|
|
132
|
+
const toFocus = (_allFocusableElements2 = allFocusableElementsInNextWrapper) === null || _allFocusableElements2 === void 0 ? void 0 : _allFocusableElements2[isTop ? allFocusableElementsInNextWrapper.length - 1 : 0];
|
|
133
|
+
|
|
134
|
+
if (isRadio(toFocus)) {
|
|
135
|
+
const radioToFocus = getRadioElementToFocus(toFocus.getAttribute("name"), isTop);
|
|
136
|
+
setElementFocus(radioToFocus);
|
|
137
|
+
} else {
|
|
138
|
+
setElementFocus(toFocus);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const trapFunction = (ev, defaultFocusableElements, isWrapperFocused, focusableSelectors, bespokeTrap) => {
|
|
143
|
+
const customFocusableElements = focusableSelectors ? defaultFocusableElements.filter(element => element.matches(focusableSelectors)) : defaultFocusableElements;
|
|
144
|
+
const firstElement = customFocusableElements[0];
|
|
145
|
+
const lastElement = customFocusableElements[customFocusableElements.length - 1];
|
|
146
|
+
|
|
147
|
+
if (bespokeTrap) {
|
|
148
|
+
bespokeTrap(ev, firstElement, lastElement);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (ev.key !== "Tab") return;
|
|
153
|
+
|
|
154
|
+
if (!(customFocusableElements !== null && customFocusableElements !== void 0 && customFocusableElements.length)) {
|
|
155
|
+
/* Block the trap */
|
|
156
|
+
ev.preventDefault();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const activeElement = document.activeElement; // special case if focus is on document body
|
|
161
|
+
|
|
162
|
+
if (activeElement === document.body) {
|
|
163
|
+
ev.preventDefault();
|
|
164
|
+
setElementFocus(firstElement);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!focusableSelectors) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const elementWhenWrapperFocused = ev.shiftKey ? firstElement : lastElement;
|
|
173
|
+
const elementToFocus = getNextElement(isWrapperFocused ? elementWhenWrapperFocused : activeElement, customFocusableElements, ev.shiftKey);
|
|
174
|
+
const defaultNextElement = getNextElement(isWrapperFocused ? elementWhenWrapperFocused : activeElement, defaultFocusableElements, ev.shiftKey);
|
|
175
|
+
|
|
176
|
+
if (elementToFocus && elementToFocus !== defaultNextElement) {
|
|
177
|
+
// if next element would match the custom selector anyway, then no need to prevent default
|
|
178
|
+
setElementFocus(elementToFocus);
|
|
179
|
+
ev.preventDefault();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export { defaultFocusableSelectors, getNextElement, setElementFocus, onTabGuardFocus, trapFunction };
|
|
@@ -12,7 +12,7 @@ export interface FocusTrapProps {
|
|
|
12
12
|
/** optional selector to identify the focusable elements, if not provided a default selector is used */
|
|
13
13
|
focusableSelectors?: string;
|
|
14
14
|
/** a ref to the container wrapping the focusable elements */
|
|
15
|
-
wrapperRef
|
|
15
|
+
wrapperRef: CustomRefObject<HTMLElement>;
|
|
16
16
|
isOpen?: boolean;
|
|
17
17
|
/** an optional array of refs to containers whose content should also be reachable from the FocusTrap */
|
|
18
18
|
additionalWrapperRefs?: CustomRefObject<HTMLElement>[];
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import React, { useCallback, useContext, useEffect,
|
|
1
|
+
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import PropTypes from "prop-types";
|
|
3
|
-
import { defaultFocusableSelectors,
|
|
3
|
+
import { defaultFocusableSelectors, setElementFocus, onTabGuardFocus, trapFunction } from "./focus-trap-utils";
|
|
4
4
|
import { ModalContext } from "../../components/modal/modal.component";
|
|
5
|
-
import usePrevious from "../../hooks/__internal__/usePrevious";
|
|
5
|
+
import usePrevious from "../../hooks/__internal__/usePrevious";
|
|
6
|
+
import TopModalContext from "../../components/carbon-provider/top-modal-context";
|
|
7
|
+
const TAB_GUARD_TOP = "tab-guard-top";
|
|
8
|
+
const TAB_GUARD_BOTTOM = "tab-guard-bottom"; // TODO investigate why React.RefObject<T> produces a failed prop type when current = null
|
|
6
9
|
|
|
7
10
|
const FocusTrap = ({
|
|
8
11
|
children,
|
|
@@ -15,113 +18,129 @@ const FocusTrap = ({
|
|
|
15
18
|
additionalWrapperRefs
|
|
16
19
|
}) => {
|
|
17
20
|
const trapRef = useRef(null);
|
|
18
|
-
const [focusableElements, setFocusableElements] = useState();
|
|
19
|
-
const [firstElement, setFirstElement] = useState();
|
|
20
|
-
const [lastElement, setLastElement] = useState();
|
|
21
21
|
const [currentFocusedElement, setCurrentFocusedElement] = useState();
|
|
22
22
|
const {
|
|
23
23
|
isAnimationComplete = true,
|
|
24
24
|
triggerRefocusFlag
|
|
25
25
|
} = useContext(ModalContext);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const {
|
|
27
|
+
topModal
|
|
28
|
+
} = useContext(TopModalContext); // we ensure that isTopModal is true if there is no TopModalContext, so that consumers who have not
|
|
29
|
+
// wrapped their app in CarbonProvider do not have all FocusTrap behaviour broken
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
}, [focusableElements]);
|
|
31
|
+
const isTopModal = !topModal || wrapperRef.current && topModal.contains(wrapperRef.current);
|
|
33
32
|
const trapWrappers = useMemo(() => additionalWrapperRefs !== null && additionalWrapperRefs !== void 0 && additionalWrapperRefs.length ? [wrapperRef, ...additionalWrapperRefs] : [wrapperRef], [additionalWrapperRefs, wrapperRef]);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const elements = [];
|
|
37
|
-
allRefs.forEach(ref => {
|
|
38
|
-
if (ref) {
|
|
39
|
-
elements.push(...Array.from(ref.querySelectorAll(focusableSelectors || defaultFocusableSelectors)).filter(el => Number(el.tabIndex) !== -1));
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
if (hasNewInputs(elements)) {
|
|
44
|
-
setFocusableElements(Array.from(elements));
|
|
45
|
-
setFirstElement(elements[0]);
|
|
46
|
-
setLastElement(elements[elements.length - 1]);
|
|
47
|
-
}
|
|
48
|
-
}, [hasNewInputs, allRefs, focusableSelectors]);
|
|
33
|
+
const onTabGuardTopFocus = useMemo(() => onTabGuardFocus(trapWrappers, focusableSelectors, "top"), [focusableSelectors, trapWrappers]);
|
|
34
|
+
const onTabGuardBottomFocus = useMemo(() => onTabGuardFocus(trapWrappers, focusableSelectors, "bottom"), [focusableSelectors, trapWrappers]);
|
|
49
35
|
useEffect(() => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
36
|
+
additionalWrapperRefs === null || additionalWrapperRefs === void 0 ? void 0 : additionalWrapperRefs.forEach(ref => {
|
|
37
|
+
const {
|
|
38
|
+
current: containerElement
|
|
39
|
+
} = ref; // istanbul ignore else
|
|
40
|
+
|
|
41
|
+
if (containerElement) {
|
|
42
|
+
var _containerElement$pre, _containerElement$nex;
|
|
43
|
+
|
|
44
|
+
// istanbul ignore else
|
|
45
|
+
if (!((_containerElement$pre = containerElement.previousElementSibling) !== null && _containerElement$pre !== void 0 && _containerElement$pre.matches(`[data-element=${TAB_GUARD_TOP}]`))) {
|
|
46
|
+
const topTabGuard = document.createElement("div");
|
|
47
|
+
topTabGuard.tabIndex = 0;
|
|
48
|
+
topTabGuard.dataset.element = TAB_GUARD_TOP;
|
|
49
|
+
containerElement.insertAdjacentElement("beforebegin", topTabGuard);
|
|
50
|
+
topTabGuard.addEventListener("focus", onTabGuardTopFocus(ref));
|
|
51
|
+
} // istanbul ignore else
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if (!((_containerElement$nex = containerElement.nextElementSibling) !== null && _containerElement$nex !== void 0 && _containerElement$nex.matches(`[data-element=${TAB_GUARD_BOTTOM}]`))) {
|
|
55
|
+
const bottomTabGuard = document.createElement("div");
|
|
56
|
+
bottomTabGuard.tabIndex = 0;
|
|
57
|
+
bottomTabGuard.dataset.element = TAB_GUARD_BOTTOM;
|
|
58
|
+
containerElement.insertAdjacentElement("afterend", bottomTabGuard);
|
|
59
|
+
bottomTabGuard.addEventListener("focus", onTabGuardBottomFocus(ref));
|
|
60
|
+
}
|
|
59
61
|
}
|
|
60
62
|
});
|
|
61
|
-
return () =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
return () => {
|
|
64
|
+
additionalWrapperRefs === null || additionalWrapperRefs === void 0 ? void 0 : additionalWrapperRefs.forEach(ref => {
|
|
65
|
+
var _ref$current, _ref$current2;
|
|
66
|
+
|
|
67
|
+
const previousElement = (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.previousElementSibling;
|
|
68
|
+
|
|
69
|
+
if (previousElement !== null && previousElement !== void 0 && previousElement.matches(`[data-element=${TAB_GUARD_TOP}]`)) {
|
|
70
|
+
previousElement.remove();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const nextElement = (_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.nextElementSibling;
|
|
74
|
+
|
|
75
|
+
if (nextElement !== null && nextElement !== void 0 && nextElement.matches(`[data-element=${TAB_GUARD_BOTTOM}]`)) {
|
|
76
|
+
nextElement.remove();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
}, [additionalWrapperRefs, onTabGuardTopFocus, onTabGuardBottomFocus]);
|
|
81
|
+
const shouldSetFocus = autoFocus && isOpen && isAnimationComplete;
|
|
67
82
|
const prevShouldSetFocus = usePrevious(shouldSetFocus);
|
|
68
83
|
useEffect(() => {
|
|
69
84
|
if (shouldSetFocus && !prevShouldSetFocus) {
|
|
70
85
|
const candidateFirstElement = focusFirstElement && "current" in focusFirstElement ? focusFirstElement.current : focusFirstElement;
|
|
71
|
-
|
|
86
|
+
const elementToFocus = candidateFirstElement || wrapperRef.current; // istanbul ignore else
|
|
87
|
+
|
|
88
|
+
if (elementToFocus) {
|
|
89
|
+
setElementFocus(elementToFocus);
|
|
90
|
+
}
|
|
72
91
|
}
|
|
73
92
|
}, [shouldSetFocus, prevShouldSetFocus, focusFirstElement, wrapperRef]);
|
|
93
|
+
const getFocusableElements = useCallback(selector => {
|
|
94
|
+
const elements = [];
|
|
95
|
+
trapWrappers.forEach(ref => {
|
|
96
|
+
// istanbul ignore else
|
|
97
|
+
if (ref.current) {
|
|
98
|
+
elements.push(...Array.from(ref.current.querySelectorAll(selector)).filter(el => Number(el.tabIndex) !== -1));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return elements;
|
|
102
|
+
}, [trapWrappers]);
|
|
74
103
|
useEffect(() => {
|
|
75
104
|
const trapFn = ev => {
|
|
76
|
-
if
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (ev.key !== "Tab") return;
|
|
82
|
-
|
|
83
|
-
if (!(focusableElements !== null && focusableElements !== void 0 && focusableElements.length)) {
|
|
84
|
-
/* Block the trap */
|
|
85
|
-
ev.preventDefault();
|
|
105
|
+
// block focus trap from working if it's not the topmost trap
|
|
106
|
+
if (!isTopModal) {
|
|
86
107
|
return;
|
|
87
108
|
}
|
|
88
109
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
const elementWhenWrapperFocused = ev.shiftKey ? firstElement : lastElement;
|
|
92
|
-
const elementToFocus = getNextElement(isWrapperFocused ? elementWhenWrapperFocused : activeElement, focusableElements, ev.shiftKey);
|
|
93
|
-
|
|
94
|
-
if (elementToFocus) {
|
|
95
|
-
setElementFocus(elementToFocus);
|
|
96
|
-
ev.preventDefault();
|
|
97
|
-
}
|
|
110
|
+
const focusableElements = getFocusableElements(defaultFocusableSelectors);
|
|
111
|
+
trapFunction(ev, focusableElements, document.activeElement === wrapperRef.current, focusableSelectors, bespokeTrap);
|
|
98
112
|
};
|
|
99
113
|
|
|
100
114
|
document.addEventListener("keydown", trapFn, true);
|
|
101
115
|
return function cleanup() {
|
|
102
116
|
document.removeEventListener("keydown", trapFn, true);
|
|
103
117
|
};
|
|
104
|
-
}, [
|
|
118
|
+
}, [bespokeTrap, wrapperRef, focusableSelectors, getFocusableElements, isTopModal]);
|
|
105
119
|
const updateCurrentFocusedElement = useCallback(() => {
|
|
120
|
+
const focusableElements = getFocusableElements(focusableSelectors || defaultFocusableSelectors);
|
|
106
121
|
const element = focusableElements === null || focusableElements === void 0 ? void 0 : focusableElements.find(el => el === document.activeElement);
|
|
107
122
|
|
|
108
123
|
if (element) {
|
|
109
124
|
setCurrentFocusedElement(element);
|
|
110
125
|
}
|
|
111
|
-
}, [
|
|
126
|
+
}, [getFocusableElements, focusableSelectors]);
|
|
112
127
|
const refocusTrap = useCallback(() => {
|
|
113
128
|
var _wrapperRef$current;
|
|
114
129
|
|
|
115
|
-
/* istanbul ignore else */
|
|
116
130
|
if (currentFocusedElement && !currentFocusedElement.hasAttribute("disabled")) {
|
|
117
131
|
// the trap breaks if it tries to refocus a disabled element
|
|
118
132
|
setElementFocus(currentFocusedElement);
|
|
119
|
-
} else if (
|
|
133
|
+
} else if ((_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.hasAttribute("tabindex")) {
|
|
120
134
|
setElementFocus(wrapperRef.current);
|
|
121
|
-
} else
|
|
122
|
-
|
|
135
|
+
} else {
|
|
136
|
+
const focusableElements = getFocusableElements(focusableSelectors || defaultFocusableSelectors);
|
|
137
|
+
/* istanbul ignore else */
|
|
138
|
+
|
|
139
|
+
if (focusableElements.length) {
|
|
140
|
+
setElementFocus(focusableElements[0]);
|
|
141
|
+
}
|
|
123
142
|
}
|
|
124
|
-
}, [currentFocusedElement,
|
|
143
|
+
}, [currentFocusedElement, wrapperRef, focusableSelectors, getFocusableElements]);
|
|
125
144
|
useEffect(() => {
|
|
126
145
|
if (triggerRefocusFlag) {
|
|
127
146
|
refocusTrap();
|
|
@@ -159,15 +178,15 @@ const FocusTrap = ({
|
|
|
159
178
|
return /*#__PURE__*/React.createElement("div", {
|
|
160
179
|
ref: trapRef
|
|
161
180
|
}, /*#__PURE__*/React.createElement("div", {
|
|
162
|
-
"data-element":
|
|
181
|
+
"data-element": TAB_GUARD_TOP // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
163
182
|
,
|
|
164
183
|
tabIndex: 0,
|
|
165
|
-
onFocus: ()
|
|
184
|
+
onFocus: onTabGuardTopFocus(wrapperRef)
|
|
166
185
|
}), clonedChildren, /*#__PURE__*/React.createElement("div", {
|
|
167
|
-
"data-element":
|
|
186
|
+
"data-element": TAB_GUARD_BOTTOM // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
168
187
|
,
|
|
169
188
|
tabIndex: 0,
|
|
170
|
-
onFocus: ()
|
|
189
|
+
onFocus: onTabGuardBottomFocus(wrapperRef)
|
|
171
190
|
}));
|
|
172
191
|
};
|
|
173
192
|
|
|
@@ -209,6 +228,6 @@ FocusTrap.propTypes = {
|
|
|
209
228
|
return new Error("Expected prop '" + propName + "' to be of type Element");
|
|
210
229
|
}
|
|
211
230
|
}
|
|
212
|
-
})
|
|
231
|
+
}).isRequired
|
|
213
232
|
};
|
|
214
233
|
export default FocusTrap;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
declare type CustomRefObject<T> = {
|
|
2
|
+
current?: T | null;
|
|
3
|
+
};
|
|
1
4
|
declare const defaultFocusableSelectors = "button:not([disabled]), [href], input:not([type=\"hidden\"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]";
|
|
2
5
|
declare const setElementFocus: (element: HTMLElement) => void;
|
|
3
6
|
declare const getNextElement: (element: HTMLElement, focusableElements: HTMLElement[], shiftKey: boolean) => HTMLElement | undefined;
|
|
4
|
-
|
|
7
|
+
declare const onTabGuardFocus: (trapWrappers: CustomRefObject<HTMLElement>[], focusableSelectors: string | undefined, position: "top" | "bottom") => (guardWrapperRef: CustomRefObject<HTMLElement>) => () => void;
|
|
8
|
+
declare const trapFunction: (ev: KeyboardEvent, defaultFocusableElements: HTMLElement[], isWrapperFocused: boolean, focusableSelectors?: string | undefined, bespokeTrap?: ((event: KeyboardEvent, firstElement?: HTMLElement | undefined, lastElement?: HTMLElement | undefined) => void) | undefined) => void;
|
|
9
|
+
export { defaultFocusableSelectors, getNextElement, setElementFocus, onTabGuardFocus, trapFunction, };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.setElementFocus = exports.getNextElement = exports.defaultFocusableSelectors = void 0;
|
|
6
|
+
exports.trapFunction = exports.onTabGuardFocus = exports.setElementFocus = exports.getNextElement = exports.defaultFocusableSelectors = void 0;
|
|
7
7
|
const defaultFocusableSelectors = 'button:not([disabled]), [href], input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]';
|
|
8
8
|
exports.defaultFocusableSelectors = defaultFocusableSelectors;
|
|
9
9
|
const INTERVAL = 10;
|
|
@@ -54,12 +54,6 @@ const getNextElement = (element, focusableElements, shiftKey) => {
|
|
|
54
54
|
if (currentIndex === -1) {
|
|
55
55
|
// we're not currently on a focusable element - most likely because the focusableElements come from a different focus trap!
|
|
56
56
|
// So we need to leave focus where it is.
|
|
57
|
-
// The exception is when the focus is on the document body - perhaps because the previously-focused element was dynamically removed.
|
|
58
|
-
// In that case focus the first element.
|
|
59
|
-
if (element === document.body) {
|
|
60
|
-
return focusableElements[0];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
57
|
return undefined;
|
|
64
58
|
}
|
|
65
59
|
|
|
@@ -119,4 +113,84 @@ const getNextElement = (element, focusableElements, shiftKey) => {
|
|
|
119
113
|
return foundElement;
|
|
120
114
|
};
|
|
121
115
|
|
|
122
|
-
exports.getNextElement = getNextElement;
|
|
116
|
+
exports.getNextElement = getNextElement;
|
|
117
|
+
|
|
118
|
+
const onTabGuardFocus = (trapWrappers, focusableSelectors, position) => guardWrapperRef => () => {
|
|
119
|
+
var _allFocusableElements2;
|
|
120
|
+
|
|
121
|
+
const isTop = position === "top";
|
|
122
|
+
const currentIndex = trapWrappers.indexOf(guardWrapperRef);
|
|
123
|
+
let index = currentIndex;
|
|
124
|
+
let nextWrapper, allFocusableElementsInNextWrapper;
|
|
125
|
+
|
|
126
|
+
do {
|
|
127
|
+
var _allFocusableElements, _nextWrapper, _nextWrapper$current;
|
|
128
|
+
|
|
129
|
+
index += isTop ? -1 : 1;
|
|
130
|
+
|
|
131
|
+
if (index < 0) {
|
|
132
|
+
index += trapWrappers.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (index >= trapWrappers.length) {
|
|
136
|
+
index -= trapWrappers.length;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
nextWrapper = trapWrappers[index];
|
|
140
|
+
allFocusableElementsInNextWrapper = (_nextWrapper = nextWrapper) === null || _nextWrapper === void 0 ? void 0 : (_nextWrapper$current = _nextWrapper.current) === null || _nextWrapper$current === void 0 ? void 0 : _nextWrapper$current.querySelectorAll(focusableSelectors || defaultFocusableSelectors);
|
|
141
|
+
} while (index !== currentIndex && !((_allFocusableElements = allFocusableElementsInNextWrapper) !== null && _allFocusableElements !== void 0 && _allFocusableElements.length));
|
|
142
|
+
|
|
143
|
+
const toFocus = (_allFocusableElements2 = allFocusableElementsInNextWrapper) === null || _allFocusableElements2 === void 0 ? void 0 : _allFocusableElements2[isTop ? allFocusableElementsInNextWrapper.length - 1 : 0];
|
|
144
|
+
|
|
145
|
+
if (isRadio(toFocus)) {
|
|
146
|
+
const radioToFocus = getRadioElementToFocus(toFocus.getAttribute("name"), isTop);
|
|
147
|
+
setElementFocus(radioToFocus);
|
|
148
|
+
} else {
|
|
149
|
+
setElementFocus(toFocus);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
exports.onTabGuardFocus = onTabGuardFocus;
|
|
154
|
+
|
|
155
|
+
const trapFunction = (ev, defaultFocusableElements, isWrapperFocused, focusableSelectors, bespokeTrap) => {
|
|
156
|
+
const customFocusableElements = focusableSelectors ? defaultFocusableElements.filter(element => element.matches(focusableSelectors)) : defaultFocusableElements;
|
|
157
|
+
const firstElement = customFocusableElements[0];
|
|
158
|
+
const lastElement = customFocusableElements[customFocusableElements.length - 1];
|
|
159
|
+
|
|
160
|
+
if (bespokeTrap) {
|
|
161
|
+
bespokeTrap(ev, firstElement, lastElement);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (ev.key !== "Tab") return;
|
|
166
|
+
|
|
167
|
+
if (!(customFocusableElements !== null && customFocusableElements !== void 0 && customFocusableElements.length)) {
|
|
168
|
+
/* Block the trap */
|
|
169
|
+
ev.preventDefault();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const activeElement = document.activeElement; // special case if focus is on document body
|
|
174
|
+
|
|
175
|
+
if (activeElement === document.body) {
|
|
176
|
+
ev.preventDefault();
|
|
177
|
+
setElementFocus(firstElement);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!focusableSelectors) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const elementWhenWrapperFocused = ev.shiftKey ? firstElement : lastElement;
|
|
186
|
+
const elementToFocus = getNextElement(isWrapperFocused ? elementWhenWrapperFocused : activeElement, customFocusableElements, ev.shiftKey);
|
|
187
|
+
const defaultNextElement = getNextElement(isWrapperFocused ? elementWhenWrapperFocused : activeElement, defaultFocusableElements, ev.shiftKey);
|
|
188
|
+
|
|
189
|
+
if (elementToFocus && elementToFocus !== defaultNextElement) {
|
|
190
|
+
// if next element would match the custom selector anyway, then no need to prevent default
|
|
191
|
+
setElementFocus(elementToFocus);
|
|
192
|
+
ev.preventDefault();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
exports.trapFunction = trapFunction;
|
|
@@ -12,7 +12,7 @@ export interface FocusTrapProps {
|
|
|
12
12
|
/** optional selector to identify the focusable elements, if not provided a default selector is used */
|
|
13
13
|
focusableSelectors?: string;
|
|
14
14
|
/** a ref to the container wrapping the focusable elements */
|
|
15
|
-
wrapperRef
|
|
15
|
+
wrapperRef: CustomRefObject<HTMLElement>;
|
|
16
16
|
isOpen?: boolean;
|
|
17
17
|
/** an optional array of refs to containers whose content should also be reachable from the FocusTrap */
|
|
18
18
|
additionalWrapperRefs?: CustomRefObject<HTMLElement>[];
|
|
@@ -15,12 +15,17 @@ var _modal = require("../../components/modal/modal.component");
|
|
|
15
15
|
|
|
16
16
|
var _usePrevious = _interopRequireDefault(require("../../hooks/__internal__/usePrevious"));
|
|
17
17
|
|
|
18
|
+
var _topModalContext = _interopRequireDefault(require("../../components/carbon-provider/top-modal-context"));
|
|
19
|
+
|
|
18
20
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
19
21
|
|
|
20
22
|
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
|
|
21
23
|
|
|
22
24
|
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
23
25
|
|
|
26
|
+
const TAB_GUARD_TOP = "tab-guard-top";
|
|
27
|
+
const TAB_GUARD_BOTTOM = "tab-guard-bottom"; // TODO investigate why React.RefObject<T> produces a failed prop type when current = null
|
|
28
|
+
|
|
24
29
|
const FocusTrap = ({
|
|
25
30
|
children,
|
|
26
31
|
autoFocus = true,
|
|
@@ -32,113 +37,129 @@ const FocusTrap = ({
|
|
|
32
37
|
additionalWrapperRefs
|
|
33
38
|
}) => {
|
|
34
39
|
const trapRef = (0, _react.useRef)(null);
|
|
35
|
-
const [focusableElements, setFocusableElements] = (0, _react.useState)();
|
|
36
|
-
const [firstElement, setFirstElement] = (0, _react.useState)();
|
|
37
|
-
const [lastElement, setLastElement] = (0, _react.useState)();
|
|
38
40
|
const [currentFocusedElement, setCurrentFocusedElement] = (0, _react.useState)();
|
|
39
41
|
const {
|
|
40
42
|
isAnimationComplete = true,
|
|
41
43
|
triggerRefocusFlag
|
|
42
44
|
} = (0, _react.useContext)(_modal.ModalContext);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const {
|
|
46
|
+
topModal
|
|
47
|
+
} = (0, _react.useContext)(_topModalContext.default); // we ensure that isTopModal is true if there is no TopModalContext, so that consumers who have not
|
|
48
|
+
// wrapped their app in CarbonProvider do not have all FocusTrap behaviour broken
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
}, [focusableElements]);
|
|
50
|
+
const isTopModal = !topModal || wrapperRef.current && topModal.contains(wrapperRef.current);
|
|
50
51
|
const trapWrappers = (0, _react.useMemo)(() => additionalWrapperRefs !== null && additionalWrapperRefs !== void 0 && additionalWrapperRefs.length ? [wrapperRef, ...additionalWrapperRefs] : [wrapperRef], [additionalWrapperRefs, wrapperRef]);
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const elements = [];
|
|
54
|
-
allRefs.forEach(ref => {
|
|
55
|
-
if (ref) {
|
|
56
|
-
elements.push(...Array.from(ref.querySelectorAll(focusableSelectors || _focusTrapUtils.defaultFocusableSelectors)).filter(el => Number(el.tabIndex) !== -1));
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (hasNewInputs(elements)) {
|
|
61
|
-
setFocusableElements(Array.from(elements));
|
|
62
|
-
setFirstElement(elements[0]);
|
|
63
|
-
setLastElement(elements[elements.length - 1]);
|
|
64
|
-
}
|
|
65
|
-
}, [hasNewInputs, allRefs, focusableSelectors]);
|
|
52
|
+
const onTabGuardTopFocus = (0, _react.useMemo)(() => (0, _focusTrapUtils.onTabGuardFocus)(trapWrappers, focusableSelectors, "top"), [focusableSelectors, trapWrappers]);
|
|
53
|
+
const onTabGuardBottomFocus = (0, _react.useMemo)(() => (0, _focusTrapUtils.onTabGuardFocus)(trapWrappers, focusableSelectors, "bottom"), [focusableSelectors, trapWrappers]);
|
|
66
54
|
(0, _react.useEffect)(() => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
55
|
+
additionalWrapperRefs === null || additionalWrapperRefs === void 0 ? void 0 : additionalWrapperRefs.forEach(ref => {
|
|
56
|
+
const {
|
|
57
|
+
current: containerElement
|
|
58
|
+
} = ref; // istanbul ignore else
|
|
59
|
+
|
|
60
|
+
if (containerElement) {
|
|
61
|
+
var _containerElement$pre, _containerElement$nex;
|
|
62
|
+
|
|
63
|
+
// istanbul ignore else
|
|
64
|
+
if (!((_containerElement$pre = containerElement.previousElementSibling) !== null && _containerElement$pre !== void 0 && _containerElement$pre.matches(`[data-element=${TAB_GUARD_TOP}]`))) {
|
|
65
|
+
const topTabGuard = document.createElement("div");
|
|
66
|
+
topTabGuard.tabIndex = 0;
|
|
67
|
+
topTabGuard.dataset.element = TAB_GUARD_TOP;
|
|
68
|
+
containerElement.insertAdjacentElement("beforebegin", topTabGuard);
|
|
69
|
+
topTabGuard.addEventListener("focus", onTabGuardTopFocus(ref));
|
|
70
|
+
} // istanbul ignore else
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if (!((_containerElement$nex = containerElement.nextElementSibling) !== null && _containerElement$nex !== void 0 && _containerElement$nex.matches(`[data-element=${TAB_GUARD_BOTTOM}]`))) {
|
|
74
|
+
const bottomTabGuard = document.createElement("div");
|
|
75
|
+
bottomTabGuard.tabIndex = 0;
|
|
76
|
+
bottomTabGuard.dataset.element = TAB_GUARD_BOTTOM;
|
|
77
|
+
containerElement.insertAdjacentElement("afterend", bottomTabGuard);
|
|
78
|
+
bottomTabGuard.addEventListener("focus", onTabGuardBottomFocus(ref));
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
});
|
|
78
|
-
return () =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
return () => {
|
|
83
|
+
additionalWrapperRefs === null || additionalWrapperRefs === void 0 ? void 0 : additionalWrapperRefs.forEach(ref => {
|
|
84
|
+
var _ref$current, _ref$current2;
|
|
85
|
+
|
|
86
|
+
const previousElement = (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.previousElementSibling;
|
|
87
|
+
|
|
88
|
+
if (previousElement !== null && previousElement !== void 0 && previousElement.matches(`[data-element=${TAB_GUARD_TOP}]`)) {
|
|
89
|
+
previousElement.remove();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nextElement = (_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.nextElementSibling;
|
|
93
|
+
|
|
94
|
+
if (nextElement !== null && nextElement !== void 0 && nextElement.matches(`[data-element=${TAB_GUARD_BOTTOM}]`)) {
|
|
95
|
+
nextElement.remove();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
}, [additionalWrapperRefs, onTabGuardTopFocus, onTabGuardBottomFocus]);
|
|
100
|
+
const shouldSetFocus = autoFocus && isOpen && isAnimationComplete;
|
|
84
101
|
const prevShouldSetFocus = (0, _usePrevious.default)(shouldSetFocus);
|
|
85
102
|
(0, _react.useEffect)(() => {
|
|
86
103
|
if (shouldSetFocus && !prevShouldSetFocus) {
|
|
87
104
|
const candidateFirstElement = focusFirstElement && "current" in focusFirstElement ? focusFirstElement.current : focusFirstElement;
|
|
88
|
-
|
|
105
|
+
const elementToFocus = candidateFirstElement || wrapperRef.current; // istanbul ignore else
|
|
106
|
+
|
|
107
|
+
if (elementToFocus) {
|
|
108
|
+
(0, _focusTrapUtils.setElementFocus)(elementToFocus);
|
|
109
|
+
}
|
|
89
110
|
}
|
|
90
111
|
}, [shouldSetFocus, prevShouldSetFocus, focusFirstElement, wrapperRef]);
|
|
112
|
+
const getFocusableElements = (0, _react.useCallback)(selector => {
|
|
113
|
+
const elements = [];
|
|
114
|
+
trapWrappers.forEach(ref => {
|
|
115
|
+
// istanbul ignore else
|
|
116
|
+
if (ref.current) {
|
|
117
|
+
elements.push(...Array.from(ref.current.querySelectorAll(selector)).filter(el => Number(el.tabIndex) !== -1));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return elements;
|
|
121
|
+
}, [trapWrappers]);
|
|
91
122
|
(0, _react.useEffect)(() => {
|
|
92
123
|
const trapFn = ev => {
|
|
93
|
-
if
|
|
94
|
-
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (ev.key !== "Tab") return;
|
|
99
|
-
|
|
100
|
-
if (!(focusableElements !== null && focusableElements !== void 0 && focusableElements.length)) {
|
|
101
|
-
/* Block the trap */
|
|
102
|
-
ev.preventDefault();
|
|
124
|
+
// block focus trap from working if it's not the topmost trap
|
|
125
|
+
if (!isTopModal) {
|
|
103
126
|
return;
|
|
104
127
|
}
|
|
105
128
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const elementWhenWrapperFocused = ev.shiftKey ? firstElement : lastElement;
|
|
109
|
-
const elementToFocus = (0, _focusTrapUtils.getNextElement)(isWrapperFocused ? elementWhenWrapperFocused : activeElement, focusableElements, ev.shiftKey);
|
|
110
|
-
|
|
111
|
-
if (elementToFocus) {
|
|
112
|
-
(0, _focusTrapUtils.setElementFocus)(elementToFocus);
|
|
113
|
-
ev.preventDefault();
|
|
114
|
-
}
|
|
129
|
+
const focusableElements = getFocusableElements(_focusTrapUtils.defaultFocusableSelectors);
|
|
130
|
+
(0, _focusTrapUtils.trapFunction)(ev, focusableElements, document.activeElement === wrapperRef.current, focusableSelectors, bespokeTrap);
|
|
115
131
|
};
|
|
116
132
|
|
|
117
133
|
document.addEventListener("keydown", trapFn, true);
|
|
118
134
|
return function cleanup() {
|
|
119
135
|
document.removeEventListener("keydown", trapFn, true);
|
|
120
136
|
};
|
|
121
|
-
}, [
|
|
137
|
+
}, [bespokeTrap, wrapperRef, focusableSelectors, getFocusableElements, isTopModal]);
|
|
122
138
|
const updateCurrentFocusedElement = (0, _react.useCallback)(() => {
|
|
139
|
+
const focusableElements = getFocusableElements(focusableSelectors || _focusTrapUtils.defaultFocusableSelectors);
|
|
123
140
|
const element = focusableElements === null || focusableElements === void 0 ? void 0 : focusableElements.find(el => el === document.activeElement);
|
|
124
141
|
|
|
125
142
|
if (element) {
|
|
126
143
|
setCurrentFocusedElement(element);
|
|
127
144
|
}
|
|
128
|
-
}, [
|
|
145
|
+
}, [getFocusableElements, focusableSelectors]);
|
|
129
146
|
const refocusTrap = (0, _react.useCallback)(() => {
|
|
130
147
|
var _wrapperRef$current;
|
|
131
148
|
|
|
132
|
-
/* istanbul ignore else */
|
|
133
149
|
if (currentFocusedElement && !currentFocusedElement.hasAttribute("disabled")) {
|
|
134
150
|
// the trap breaks if it tries to refocus a disabled element
|
|
135
151
|
(0, _focusTrapUtils.setElementFocus)(currentFocusedElement);
|
|
136
|
-
} else if (
|
|
152
|
+
} else if ((_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.hasAttribute("tabindex")) {
|
|
137
153
|
(0, _focusTrapUtils.setElementFocus)(wrapperRef.current);
|
|
138
|
-
} else
|
|
139
|
-
(
|
|
154
|
+
} else {
|
|
155
|
+
const focusableElements = getFocusableElements(focusableSelectors || _focusTrapUtils.defaultFocusableSelectors);
|
|
156
|
+
/* istanbul ignore else */
|
|
157
|
+
|
|
158
|
+
if (focusableElements.length) {
|
|
159
|
+
(0, _focusTrapUtils.setElementFocus)(focusableElements[0]);
|
|
160
|
+
}
|
|
140
161
|
}
|
|
141
|
-
}, [currentFocusedElement,
|
|
162
|
+
}, [currentFocusedElement, wrapperRef, focusableSelectors, getFocusableElements]);
|
|
142
163
|
(0, _react.useEffect)(() => {
|
|
143
164
|
if (triggerRefocusFlag) {
|
|
144
165
|
refocusTrap();
|
|
@@ -177,15 +198,15 @@ const FocusTrap = ({
|
|
|
177
198
|
return /*#__PURE__*/_react.default.createElement("div", {
|
|
178
199
|
ref: trapRef
|
|
179
200
|
}, /*#__PURE__*/_react.default.createElement("div", {
|
|
180
|
-
"data-element":
|
|
201
|
+
"data-element": TAB_GUARD_TOP // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
181
202
|
,
|
|
182
203
|
tabIndex: 0,
|
|
183
|
-
onFocus: ()
|
|
204
|
+
onFocus: onTabGuardTopFocus(wrapperRef)
|
|
184
205
|
}), clonedChildren, /*#__PURE__*/_react.default.createElement("div", {
|
|
185
|
-
"data-element":
|
|
206
|
+
"data-element": TAB_GUARD_BOTTOM // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
186
207
|
,
|
|
187
208
|
tabIndex: 0,
|
|
188
|
-
onFocus: ()
|
|
209
|
+
onFocus: onTabGuardBottomFocus(wrapperRef)
|
|
189
210
|
}));
|
|
190
211
|
};
|
|
191
212
|
|
|
@@ -227,7 +248,7 @@ FocusTrap.propTypes = {
|
|
|
227
248
|
return new Error("Expected prop '" + propName + "' to be of type Element");
|
|
228
249
|
}
|
|
229
250
|
}
|
|
230
|
-
})
|
|
251
|
+
}).isRequired
|
|
231
252
|
};
|
|
232
253
|
var _default = FocusTrap;
|
|
233
254
|
exports.default = _default;
|