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.
@@ -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
- export { defaultFocusableSelectors, getNextElement, setElementFocus };
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
- export { defaultFocusableSelectors, getNextElement, setElementFocus };
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?: CustomRefObject<HTMLElement>;
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, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
2
2
  import PropTypes from "prop-types";
3
- import { defaultFocusableSelectors, getNextElement, setElementFocus } from "./focus-trap-utils";
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"; // TODO investigate why React.RefObject<T> produces a failed prop type when current = null
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 hasNewInputs = useCallback(candidate => {
27
- if (!focusableElements || candidate.length !== focusableElements.length) {
28
- return true;
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
- return Array.from(candidate).some((el, i) => el !== focusableElements[i]);
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 allRefs = trapWrappers.map(ref => ref === null || ref === void 0 ? void 0 : ref.current);
35
- const updateFocusableElements = useCallback(() => {
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
- const observer = new MutationObserver(updateFocusableElements);
51
- trapWrappers.forEach(wrapper => {
52
- if (wrapper !== null && wrapper !== void 0 && wrapper.current) {
53
- observer.observe(wrapper === null || wrapper === void 0 ? void 0 : wrapper.current, {
54
- subtree: true,
55
- childList: true,
56
- attributes: true,
57
- characterData: true
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 () => observer.disconnect();
62
- }, [updateFocusableElements, trapWrappers]);
63
- useLayoutEffect(() => {
64
- updateFocusableElements();
65
- }, [children, updateFocusableElements]);
66
- const shouldSetFocus = autoFocus && isOpen && isAnimationComplete && (focusFirstElement || (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current));
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
- setElementFocus(candidateFirstElement || (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current));
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 (bespokeTrap) {
77
- bespokeTrap(ev, firstElement, lastElement);
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 activeElement = document.activeElement;
90
- const isWrapperFocused = activeElement === (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current);
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
- }, [firstElement, lastElement, focusableElements, bespokeTrap, wrapperRef]);
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
- }, [focusableElements]);
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 (wrapperRef !== null && wrapperRef !== void 0 && (_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.hasAttribute("tabindex")) {
133
+ } else if ((_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.hasAttribute("tabindex")) {
120
134
  setElementFocus(wrapperRef.current);
121
- } else if (firstElement) {
122
- setElementFocus(firstElement);
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, firstElement, wrapperRef]);
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": "tab-guard-top" // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
181
+ "data-element": TAB_GUARD_TOP // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
163
182
  ,
164
183
  tabIndex: 0,
165
- onFocus: () => setElementFocus(lastElement)
184
+ onFocus: onTabGuardTopFocus(wrapperRef)
166
185
  }), clonedChildren, /*#__PURE__*/React.createElement("div", {
167
- "data-element": "tab-guard-bottom" // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
186
+ "data-element": TAB_GUARD_BOTTOM // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
168
187
  ,
169
188
  tabIndex: 0,
170
- onFocus: () => setElementFocus(firstElement)
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
- export { defaultFocusableSelectors, getNextElement, setElementFocus };
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?: CustomRefObject<HTMLElement>;
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 hasNewInputs = (0, _react.useCallback)(candidate => {
44
- if (!focusableElements || candidate.length !== focusableElements.length) {
45
- return true;
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
- return Array.from(candidate).some((el, i) => el !== focusableElements[i]);
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 allRefs = trapWrappers.map(ref => ref === null || ref === void 0 ? void 0 : ref.current);
52
- const updateFocusableElements = (0, _react.useCallback)(() => {
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
- const observer = new MutationObserver(updateFocusableElements);
68
- trapWrappers.forEach(wrapper => {
69
- if (wrapper !== null && wrapper !== void 0 && wrapper.current) {
70
- observer.observe(wrapper === null || wrapper === void 0 ? void 0 : wrapper.current, {
71
- subtree: true,
72
- childList: true,
73
- attributes: true,
74
- characterData: true
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 () => observer.disconnect();
79
- }, [updateFocusableElements, trapWrappers]);
80
- (0, _react.useLayoutEffect)(() => {
81
- updateFocusableElements();
82
- }, [children, updateFocusableElements]);
83
- const shouldSetFocus = autoFocus && isOpen && isAnimationComplete && (focusFirstElement || (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current));
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
- (0, _focusTrapUtils.setElementFocus)(candidateFirstElement || (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current));
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 (bespokeTrap) {
94
- bespokeTrap(ev, firstElement, lastElement);
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 activeElement = document.activeElement;
107
- const isWrapperFocused = activeElement === (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current);
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
- }, [firstElement, lastElement, focusableElements, bespokeTrap, wrapperRef]);
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
- }, [focusableElements]);
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 (wrapperRef !== null && wrapperRef !== void 0 && (_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.hasAttribute("tabindex")) {
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 if (firstElement) {
139
- (0, _focusTrapUtils.setElementFocus)(firstElement);
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, firstElement, wrapperRef]);
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": "tab-guard-top" // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
201
+ "data-element": TAB_GUARD_TOP // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
181
202
  ,
182
203
  tabIndex: 0,
183
- onFocus: () => (0, _focusTrapUtils.setElementFocus)(lastElement)
204
+ onFocus: onTabGuardTopFocus(wrapperRef)
184
205
  }), clonedChildren, /*#__PURE__*/_react.default.createElement("div", {
185
- "data-element": "tab-guard-bottom" // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
206
+ "data-element": TAB_GUARD_BOTTOM // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
186
207
  ,
187
208
  tabIndex: 0,
188
- onFocus: () => (0, _focusTrapUtils.setElementFocus)(firstElement)
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carbon-react",
3
- "version": "116.1.0",
3
+ "version": "116.1.1",
4
4
  "description": "A library of reusable React components for easily building user interfaces.",
5
5
  "files": [
6
6
  "lib",