carbon-react 106.6.10 → 106.7.0

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.
Files changed (23) hide show
  1. package/esm/__internal__/focus-trap/focus-trap-utils.js +25 -1
  2. package/esm/__internal__/focus-trap/focus-trap.component.d.ts +3 -1
  3. package/esm/__internal__/focus-trap/focus-trap.component.js +44 -12
  4. package/esm/components/advanced-color-picker/advanced-color-picker.component.js +5 -5
  5. package/esm/components/dialog/dialog.component.js +4 -3
  6. package/esm/components/dialog-full-screen/dialog-full-screen.component.js +4 -3
  7. package/esm/components/menu/menu-full-screen/menu-full-screen.component.js +4 -18
  8. package/esm/components/menu/menu-full-screen/menu-full-screen.style.js +1 -0
  9. package/esm/components/sidebar/__internal__/sidebar-header/sidebar-header.component.d.ts +3 -1
  10. package/esm/components/sidebar/__internal__/sidebar-header/sidebar-header.component.js +7 -2
  11. package/esm/components/sidebar/sidebar.component.js +10 -3
  12. package/lib/__internal__/focus-trap/focus-trap-utils.js +25 -1
  13. package/lib/__internal__/focus-trap/focus-trap.component.d.ts +3 -1
  14. package/lib/__internal__/focus-trap/focus-trap.component.js +46 -12
  15. package/lib/components/advanced-color-picker/advanced-color-picker.component.js +5 -5
  16. package/lib/components/dialog/dialog.component.js +4 -3
  17. package/lib/components/dialog-full-screen/dialog-full-screen.component.js +4 -3
  18. package/lib/components/menu/menu-full-screen/menu-full-screen.component.js +3 -17
  19. package/lib/components/menu/menu-full-screen/menu-full-screen.style.js +1 -0
  20. package/lib/components/sidebar/__internal__/sidebar-header/sidebar-header.component.d.ts +3 -1
  21. package/lib/components/sidebar/__internal__/sidebar-header/sidebar-header.component.js +7 -2
  22. package/lib/components/sidebar/sidebar.component.js +11 -3
  23. package/package.json +1 -1
@@ -1,11 +1,35 @@
1
1
  const defaultFocusableSelectors = 'button:not([disabled]), [href], input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]';
2
2
 
3
+ const waitForVisibleAndFocus = element => {
4
+ const INTERVAL = 10;
5
+ const MAX_TIME = 100;
6
+ let timeSoFar = 0;
7
+
8
+ const stylesMatch = () => {
9
+ const actualStyles = window.getComputedStyle(element);
10
+ return actualStyles.visibility === "visible";
11
+ };
12
+
13
+ const check = () => {
14
+ /* istanbul ignore else */
15
+ if (stylesMatch()) {
16
+ element.focus();
17
+ } else if (timeSoFar < MAX_TIME) {
18
+ setTimeout(check, INTERVAL);
19
+ timeSoFar += INTERVAL;
20
+ } // just "fail" silently if maxTime exceeded - callback will never be called
21
+
22
+ };
23
+
24
+ check();
25
+ };
26
+
3
27
  function setElementFocus(element) {
4
28
  if (typeof element === "function") {
5
29
  element();
6
30
  } else {
7
31
  const el = element.current || element;
8
- el.focus();
32
+ waitForVisibleAndFocus(el);
9
33
  }
10
34
  }
11
35
 
@@ -1,10 +1,11 @@
1
1
  export default FocusTrap;
2
- declare function FocusTrap({ children, autoFocus, focusFirstElement, bespokeTrap, wrapperRef, }: {
2
+ declare function FocusTrap({ children, autoFocus, focusFirstElement, bespokeTrap, wrapperRef, isOpen, }: {
3
3
  children: any;
4
4
  autoFocus?: boolean | undefined;
5
5
  focusFirstElement: any;
6
6
  bespokeTrap: any;
7
7
  wrapperRef: any;
8
+ isOpen: any;
8
9
  }): JSX.Element;
9
10
  declare namespace FocusTrap {
10
11
  namespace propTypes {
@@ -17,6 +18,7 @@ declare namespace FocusTrap {
17
18
  const wrapperRef: PropTypes.Requireable<PropTypes.InferProps<{
18
19
  current: PropTypes.Requireable<any>;
19
20
  }>>;
21
+ const isOpen: PropTypes.Requireable<boolean>;
20
22
  }
21
23
  }
22
24
  import PropTypes from "prop-types";
@@ -2,22 +2,23 @@ import React, { useCallback, useContext, useEffect, useLayoutEffect, useRef, use
2
2
  import PropTypes from "prop-types";
3
3
  import { defaultFocusableSelectors, nextNonRadioElementIndex, isRadio, setElementFocus } from "./focus-trap-utils";
4
4
  import { ModalContext } from "../../components/modal/modal.component";
5
+ import usePrevious from "../../hooks/__internal__/usePrevious";
5
6
 
6
7
  const FocusTrap = ({
7
8
  children,
8
9
  autoFocus = true,
9
10
  focusFirstElement,
10
11
  bespokeTrap,
11
- wrapperRef
12
+ wrapperRef,
13
+ isOpen
12
14
  }) => {
13
15
  const trapRef = useRef(null);
14
- const firstOpen = useRef(true);
15
16
  const [focusableElements, setFocusableElements] = useState();
16
17
  const [firstElement, setFirstElement] = useState();
17
18
  const [lastElement, setLastElement] = useState();
18
19
  const [currentFocusedElement, setCurrentFocusedElement] = useState();
19
20
  const {
20
- isAnimationComplete,
21
+ isAnimationComplete = true,
21
22
  triggerRefocusFlag
22
23
  } = useContext(ModalContext);
23
24
  const hasNewInputs = useCallback(candidate => {
@@ -53,12 +54,13 @@ const FocusTrap = ({
53
54
  useLayoutEffect(() => {
54
55
  updateFocusableElements();
55
56
  }, [children, updateFocusableElements]);
57
+ const shouldSetFocus = autoFocus && isOpen && isAnimationComplete && (focusFirstElement || (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current));
58
+ const prevShouldSetFocus = usePrevious(shouldSetFocus);
56
59
  useEffect(() => {
57
- if (autoFocus && firstOpen.current && isAnimationComplete && (focusFirstElement || firstElement)) {
58
- setElementFocus(focusFirstElement || firstElement);
59
- firstOpen.current = false;
60
+ if (shouldSetFocus && !prevShouldSetFocus) {
61
+ setElementFocus(focusFirstElement || (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current));
60
62
  }
61
- }, [autoFocus, firstElement, focusFirstElement, isAnimationComplete]);
63
+ }, [shouldSetFocus, prevShouldSetFocus, focusFirstElement, wrapperRef]);
62
64
  useEffect(() => {
63
65
  const trapFn = ev => {
64
66
  if (bespokeTrap) {
@@ -76,7 +78,7 @@ const FocusTrap = ({
76
78
  ev.preventDefault();
77
79
  } else if (ev.shiftKey) {
78
80
  /* shift + tab */
79
- if (activeElement === firstElement) {
81
+ if (activeElement === firstElement || activeElement === wrapperRef.current) {
80
82
  lastElement.focus();
81
83
  ev.preventDefault();
82
84
  } // If current element is radio button -
@@ -99,7 +101,7 @@ const FocusTrap = ({
99
101
  return function cleanup() {
100
102
  document.removeEventListener("keydown", trapFn);
101
103
  };
102
- }, [firstElement, lastElement, focusableElements, bespokeTrap]);
104
+ }, [firstElement, lastElement, focusableElements, bespokeTrap, wrapperRef]);
103
105
  const updateCurrentFocusedElement = useCallback(() => {
104
106
  const element = focusableElements === null || focusableElements === void 0 ? void 0 : focusableElements.find(el => el === document.activeElement);
105
107
 
@@ -114,22 +116,49 @@ const FocusTrap = ({
114
116
  };
115
117
  }, [updateCurrentFocusedElement]);
116
118
  const refocusTrap = useCallback(() => {
119
+ var _wrapperRef$current;
120
+
117
121
  /* istanbul ignore else */
118
122
  if (currentFocusedElement && !currentFocusedElement.hasAttribute("disabled")) {
119
123
  // the trap breaks if it tries to refocus a disabled element
120
124
  setElementFocus(currentFocusedElement);
125
+ } else if (wrapperRef !== null && wrapperRef !== void 0 && (_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.hasAttribute("tabindex")) {
126
+ setElementFocus(wrapperRef.current);
121
127
  } else if (firstElement) {
122
128
  setElementFocus(firstElement);
123
129
  }
124
- }, [currentFocusedElement, firstElement]);
130
+ }, [currentFocusedElement, firstElement, wrapperRef]);
125
131
  useEffect(() => {
126
132
  if (triggerRefocusFlag) {
127
133
  refocusTrap();
128
134
  }
129
135
  }, [triggerRefocusFlag, refocusTrap]);
136
+ const [tabIndex, setTabIndex] = useState(0);
137
+ useEffect(() => {
138
+ // issue in cypress prevents setting tabIndex to -1, instead tabIndex is set to 0 and removed on blur.
139
+ if (!isOpen) {
140
+ setTabIndex(0);
141
+ }
142
+ }, [isOpen]);
143
+
144
+ const onBlur = () => {
145
+ /* istanbul ignore else */
146
+ if (isOpen) {
147
+ setTabIndex(undefined);
148
+ }
149
+ };
150
+
151
+ const focusProps = {
152
+ tabIndex,
153
+ onBlur
154
+ }; // passes focusProps if no tabindex has been explicitly set on the wrapper
155
+
156
+ const clonedChildren = React.Children.map(children, child => {
157
+ return child.props.tabIndex === undefined ? /*#__PURE__*/React.cloneElement(child, focusProps) : child;
158
+ });
130
159
  return /*#__PURE__*/React.createElement("div", {
131
160
  ref: trapRef
132
- }, children);
161
+ }, clonedChildren);
133
162
  };
134
163
 
135
164
  FocusTrap.propTypes = {
@@ -149,6 +178,9 @@ FocusTrap.propTypes = {
149
178
  /** a ref to the container wrapping the focusable elements */
150
179
  wrapperRef: PropTypes.shape({
151
180
  current: PropTypes.any
152
- })
181
+ }),
182
+
183
+ /* whether the modal (etc.) component that the focus trap is inside is open or not */
184
+ isOpen: PropTypes.bool
153
185
  };
154
186
  export default FocusTrap;
@@ -48,17 +48,17 @@ const AdvancedColorPicker = ({
48
48
  setSelectedColorRef(selected.ref.current);
49
49
  }
50
50
  }, [colors, currentColor, dialogOpen, isOpen]);
51
- const handleFocus = useCallback((e, firstFocusableElement, lastFocusableElement) => {
51
+ const handleFocus = useCallback((e, firstFocusableElement) => {
52
52
  if (e.key === "Tab") {
53
53
  /* istanbul ignore else */
54
54
  if (e.shiftKey) {
55
55
  /* istanbul ignore else */
56
- if (document.activeElement === selectedColorRef) {
57
- lastFocusableElement.focus();
56
+ if (document.activeElement === firstFocusableElement) {
57
+ selectedColorRef.focus();
58
58
  e.preventDefault();
59
59
  }
60
- } else if (document.activeElement === lastFocusableElement) {
61
- selectedColorRef.focus();
60
+ } else if (document.activeElement === selectedColorRef) {
61
+ firstFocusableElement.focus();
62
62
  e.preventDefault();
63
63
  }
64
64
  }
@@ -155,7 +155,8 @@ const Dialog = ({
155
155
  autoFocus: !disableAutoFocus,
156
156
  focusFirstElement: focusFirstElement,
157
157
  bespokeTrap: bespokeFocusTrap,
158
- wrapperRef: dialogRef
158
+ wrapperRef: dialogRef,
159
+ isOpen: open
159
160
  }, /*#__PURE__*/React.createElement(DialogStyle, _extends({
160
161
  "aria-modal": true,
161
162
  ref: dialogRef,
@@ -165,9 +166,9 @@ const Dialog = ({
165
166
  "data-element": "dialog",
166
167
  "data-role": rest["data-role"],
167
168
  role: role
168
- }, contentPadding), dialogTitle(), /*#__PURE__*/React.createElement(DialogContentStyle, contentPadding, /*#__PURE__*/React.createElement(DialogInnerContentStyle, _extends({
169
+ }, contentPadding), dialogTitle(), closeIcon(), /*#__PURE__*/React.createElement(DialogContentStyle, contentPadding, /*#__PURE__*/React.createElement(DialogInnerContentStyle, _extends({
169
170
  ref: innerContentRef
170
- }, contentPadding), children)), closeIcon())));
171
+ }, contentPadding), children)))));
171
172
  };
172
173
 
173
174
  Dialog.propTypes = {
@@ -85,7 +85,8 @@ const DialogFullScreen = ({
85
85
  }, componentTags), /*#__PURE__*/React.createElement(FocusTrap, {
86
86
  autoFocus: !disableAutoFocus,
87
87
  focusFirstElement: focusFirstElement,
88
- wrapperRef: dialogRef
88
+ wrapperRef: dialogRef,
89
+ isOpen: open
89
90
  }, /*#__PURE__*/React.createElement(StyledDialogFullScreen, _extends({
90
91
  "aria-modal": role === "dialog" ? true : undefined
91
92
  }, ariaProps, {
@@ -93,12 +94,12 @@ const DialogFullScreen = ({
93
94
  "data-element": "dialog-full-screen",
94
95
  pagesStyling: pagesStyling,
95
96
  role: role
96
- }), dialogTitle(), /*#__PURE__*/React.createElement(StyledContent, {
97
+ }), dialogTitle(), closeIcon(), /*#__PURE__*/React.createElement(StyledContent, {
97
98
  hasHeader: title !== undefined,
98
99
  "data-element": "content",
99
100
  ref: contentRef,
100
101
  disableContentPadding: disableContentPadding
101
- }, children), closeIcon())));
102
+ }, children))));
102
103
  };
103
104
 
104
105
  DialogFullScreen.defaultProps = {
@@ -1,6 +1,6 @@
1
1
  function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
2
2
 
3
- import React, { useContext, useLayoutEffect, useRef } from "react";
3
+ import React, { useContext, useRef } from "react";
4
4
  import PropTypes from "prop-types";
5
5
  import { StyledMenuFullscreen, StyledMenuFullscreenHeader } from "./menu-full-screen.style";
6
6
  import { StyledMenuWrapper } from "../menu.style";
@@ -33,19 +33,6 @@ const MenuFullscreen = ({
33
33
  }
34
34
  };
35
35
 
36
- useLayoutEffect(() => {
37
- const checkTransitionEnd = () => {
38
- menuContentRef.current.focus();
39
- };
40
-
41
- const wrapperRef = menuWrapperRef.current;
42
-
43
- if (isOpen) {
44
- wrapperRef.addEventListener("transitionend", checkTransitionEnd);
45
- } else {
46
- wrapperRef.removeEventListener("transitionend", checkTransitionEnd);
47
- }
48
- }, [isOpen]);
49
36
  const scrollVariants = {
50
37
  light: "light",
51
38
  dark: "dark",
@@ -61,8 +48,8 @@ const MenuFullscreen = ({
61
48
  return /*#__PURE__*/React.createElement("li", {
62
49
  "aria-label": "menu-fullscreen"
63
50
  }, /*#__PURE__*/React.createElement(Portal, null, /*#__PURE__*/React.createElement(FocusTrap, {
64
- autoFocus: false,
65
- wrapperRef: menuWrapperRef
51
+ wrapperRef: menuWrapperRef,
52
+ isOpen: isOpen
66
53
  }, /*#__PURE__*/React.createElement(StyledMenuFullscreen, _extends({
67
54
  "data-component": "menu-fullscreen",
68
55
  ref: menuWrapperRef,
@@ -93,8 +80,7 @@ const MenuFullscreen = ({
93
80
  display: "flex",
94
81
  flexDirection: "column",
95
82
  role: "list",
96
- inFullscreenView: true,
97
- tabIndex: -1
83
+ inFullscreenView: true
98
84
  }, React.Children.map(children, (child, index) => /*#__PURE__*/React.createElement(MenuContext.Provider, {
99
85
  value: {
100
86
  inFullscreenView: true,
@@ -12,6 +12,7 @@ const StyledMenuFullscreen = styled.div`
12
12
  bottom: 0;
13
13
  height: 100vh;
14
14
  width: 100%;
15
+ outline: none;
15
16
 
16
17
  a,
17
18
  button,
@@ -1,13 +1,15 @@
1
1
  export default SidebarHeader;
2
- declare function SidebarHeader({ className, children, ...props }: {
2
+ declare function SidebarHeader({ className, children, id, ...props }: {
3
3
  [x: string]: any;
4
4
  className: any;
5
5
  children: any;
6
+ id: any;
6
7
  }): JSX.Element;
7
8
  declare namespace SidebarHeader {
8
9
  namespace propTypes {
9
10
  const children: PropTypes.Requireable<PropTypes.ReactNodeLike>;
10
11
  const className: PropTypes.Requireable<string>;
12
+ const id: PropTypes.Requireable<string>;
11
13
  }
12
14
  }
13
15
  import PropTypes from "prop-types";
@@ -8,9 +8,11 @@ import SidebarHeaderStyle from "./sidebar-header.style";
8
8
  const SidebarHeader = ({
9
9
  className,
10
10
  children,
11
+ id,
11
12
  ...props
12
13
  }) => /*#__PURE__*/React.createElement(SidebarHeaderStyle, _extends({
13
- className: className
14
+ className: className,
15
+ id: id
14
16
  }, tagComponent("sidebar-header", props)), children);
15
17
 
16
18
  SidebarHeader.propTypes = {
@@ -18,6 +20,9 @@ SidebarHeader.propTypes = {
18
20
  children: PropTypes.node,
19
21
 
20
22
  /** A custom class name. */
21
- className: PropTypes.string
23
+ className: PropTypes.string,
24
+
25
+ /** A custom id. */
26
+ id: PropTypes.string
22
27
  };
23
28
  export default SidebarHeader;
@@ -10,6 +10,7 @@ import FocusTrap from "../../__internal__/focus-trap";
10
10
  import SidebarHeader from "./__internal__/sidebar-header";
11
11
  import Box from "../box";
12
12
  import { SIDEBAR_SIZES, SIDEBAR_ALIGNMENTS } from "./sidebar.config";
13
+ import createGuid from "../../__internal__/utils/helpers/guid";
13
14
  import useLocale from "../../hooks/__internal__/useLocale";
14
15
  export const SidebarContext = /*#__PURE__*/React.createContext({});
15
16
  const Sidebar = /*#__PURE__*/React.forwardRef(({
@@ -28,6 +29,9 @@ const Sidebar = /*#__PURE__*/React.forwardRef(({
28
29
  ...rest
29
30
  }, ref) => {
30
31
  const locale = useLocale();
32
+ const {
33
+ current: titleId
34
+ } = useRef(createGuid());
31
35
  let sidebarRef = useRef();
32
36
  if (ref) sidebarRef = ref;
33
37
 
@@ -51,14 +55,16 @@ const Sidebar = /*#__PURE__*/React.forwardRef(({
51
55
  "aria-modal": !enableBackgroundUI,
52
56
  "aria-describedby": ariaDescribedBy,
53
57
  "aria-label": ariaLabel,
54
- "aria-labelledby": ariaLabelledBy,
58
+ "aria-labelledby": !ariaLabelledBy && !ariaLabel ? titleId : ariaLabelledBy,
55
59
  ref: sidebarRef,
56
60
  position: position,
57
61
  size: size,
58
62
  "data-element": "sidebar",
59
63
  onCancel: onCancel,
60
64
  role: role
61
- }, closeIcon(), header && /*#__PURE__*/React.createElement(SidebarHeader, null, header), /*#__PURE__*/React.createElement(Box, {
65
+ }, header && /*#__PURE__*/React.createElement(SidebarHeader, {
66
+ id: titleId
67
+ }, header), closeIcon(), /*#__PURE__*/React.createElement(Box, {
62
68
  "data-element": "sidebar-content",
63
69
  p: 4,
64
70
  pt: "27px",
@@ -77,7 +83,8 @@ const Sidebar = /*#__PURE__*/React.forwardRef(({
77
83
  enableBackgroundUI: enableBackgroundUI,
78
84
  className: "carbon-sidebar"
79
85
  }, componentTags), enableBackgroundUI ? sidebar : /*#__PURE__*/React.createElement(FocusTrap, {
80
- wrapperRef: sidebarRef
86
+ wrapperRef: sidebarRef,
87
+ isOpen: open
81
88
  }, sidebar));
82
89
  });
83
90
  Sidebar.propTypes = {
@@ -8,12 +8,36 @@ exports.isRadio = exports.nextNonRadioElementIndex = exports.defaultFocusableSel
8
8
  const defaultFocusableSelectors = 'button:not([disabled]), [href], input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]';
9
9
  exports.defaultFocusableSelectors = defaultFocusableSelectors;
10
10
 
11
+ const waitForVisibleAndFocus = element => {
12
+ const INTERVAL = 10;
13
+ const MAX_TIME = 100;
14
+ let timeSoFar = 0;
15
+
16
+ const stylesMatch = () => {
17
+ const actualStyles = window.getComputedStyle(element);
18
+ return actualStyles.visibility === "visible";
19
+ };
20
+
21
+ const check = () => {
22
+ /* istanbul ignore else */
23
+ if (stylesMatch()) {
24
+ element.focus();
25
+ } else if (timeSoFar < MAX_TIME) {
26
+ setTimeout(check, INTERVAL);
27
+ timeSoFar += INTERVAL;
28
+ } // just "fail" silently if maxTime exceeded - callback will never be called
29
+
30
+ };
31
+
32
+ check();
33
+ };
34
+
11
35
  function setElementFocus(element) {
12
36
  if (typeof element === "function") {
13
37
  element();
14
38
  } else {
15
39
  const el = element.current || element;
16
- el.focus();
40
+ waitForVisibleAndFocus(el);
17
41
  }
18
42
  }
19
43
 
@@ -1,10 +1,11 @@
1
1
  export default FocusTrap;
2
- declare function FocusTrap({ children, autoFocus, focusFirstElement, bespokeTrap, wrapperRef, }: {
2
+ declare function FocusTrap({ children, autoFocus, focusFirstElement, bespokeTrap, wrapperRef, isOpen, }: {
3
3
  children: any;
4
4
  autoFocus?: boolean | undefined;
5
5
  focusFirstElement: any;
6
6
  bespokeTrap: any;
7
7
  wrapperRef: any;
8
+ isOpen: any;
8
9
  }): JSX.Element;
9
10
  declare namespace FocusTrap {
10
11
  namespace propTypes {
@@ -17,6 +18,7 @@ declare namespace FocusTrap {
17
18
  const wrapperRef: PropTypes.Requireable<PropTypes.InferProps<{
18
19
  current: PropTypes.Requireable<any>;
19
20
  }>>;
21
+ const isOpen: PropTypes.Requireable<boolean>;
20
22
  }
21
23
  }
22
24
  import PropTypes from "prop-types";
@@ -13,6 +13,8 @@ var _focusTrapUtils = require("./focus-trap-utils");
13
13
 
14
14
  var _modal = require("../../components/modal/modal.component");
15
15
 
16
+ var _usePrevious = _interopRequireDefault(require("../../hooks/__internal__/usePrevious"));
17
+
16
18
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
19
 
18
20
  function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
@@ -24,16 +26,16 @@ const FocusTrap = ({
24
26
  autoFocus = true,
25
27
  focusFirstElement,
26
28
  bespokeTrap,
27
- wrapperRef
29
+ wrapperRef,
30
+ isOpen
28
31
  }) => {
29
32
  const trapRef = (0, _react.useRef)(null);
30
- const firstOpen = (0, _react.useRef)(true);
31
33
  const [focusableElements, setFocusableElements] = (0, _react.useState)();
32
34
  const [firstElement, setFirstElement] = (0, _react.useState)();
33
35
  const [lastElement, setLastElement] = (0, _react.useState)();
34
36
  const [currentFocusedElement, setCurrentFocusedElement] = (0, _react.useState)();
35
37
  const {
36
- isAnimationComplete,
38
+ isAnimationComplete = true,
37
39
  triggerRefocusFlag
38
40
  } = (0, _react.useContext)(_modal.ModalContext);
39
41
  const hasNewInputs = (0, _react.useCallback)(candidate => {
@@ -69,12 +71,13 @@ const FocusTrap = ({
69
71
  (0, _react.useLayoutEffect)(() => {
70
72
  updateFocusableElements();
71
73
  }, [children, updateFocusableElements]);
74
+ const shouldSetFocus = autoFocus && isOpen && isAnimationComplete && (focusFirstElement || (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current));
75
+ const prevShouldSetFocus = (0, _usePrevious.default)(shouldSetFocus);
72
76
  (0, _react.useEffect)(() => {
73
- if (autoFocus && firstOpen.current && isAnimationComplete && (focusFirstElement || firstElement)) {
74
- (0, _focusTrapUtils.setElementFocus)(focusFirstElement || firstElement);
75
- firstOpen.current = false;
77
+ if (shouldSetFocus && !prevShouldSetFocus) {
78
+ (0, _focusTrapUtils.setElementFocus)(focusFirstElement || (wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current));
76
79
  }
77
- }, [autoFocus, firstElement, focusFirstElement, isAnimationComplete]);
80
+ }, [shouldSetFocus, prevShouldSetFocus, focusFirstElement, wrapperRef]);
78
81
  (0, _react.useEffect)(() => {
79
82
  const trapFn = ev => {
80
83
  if (bespokeTrap) {
@@ -92,7 +95,7 @@ const FocusTrap = ({
92
95
  ev.preventDefault();
93
96
  } else if (ev.shiftKey) {
94
97
  /* shift + tab */
95
- if (activeElement === firstElement) {
98
+ if (activeElement === firstElement || activeElement === wrapperRef.current) {
96
99
  lastElement.focus();
97
100
  ev.preventDefault();
98
101
  } // If current element is radio button -
@@ -115,7 +118,7 @@ const FocusTrap = ({
115
118
  return function cleanup() {
116
119
  document.removeEventListener("keydown", trapFn);
117
120
  };
118
- }, [firstElement, lastElement, focusableElements, bespokeTrap]);
121
+ }, [firstElement, lastElement, focusableElements, bespokeTrap, wrapperRef]);
119
122
  const updateCurrentFocusedElement = (0, _react.useCallback)(() => {
120
123
  const element = focusableElements === null || focusableElements === void 0 ? void 0 : focusableElements.find(el => el === document.activeElement);
121
124
 
@@ -130,22 +133,50 @@ const FocusTrap = ({
130
133
  };
131
134
  }, [updateCurrentFocusedElement]);
132
135
  const refocusTrap = (0, _react.useCallback)(() => {
136
+ var _wrapperRef$current;
137
+
133
138
  /* istanbul ignore else */
134
139
  if (currentFocusedElement && !currentFocusedElement.hasAttribute("disabled")) {
135
140
  // the trap breaks if it tries to refocus a disabled element
136
141
  (0, _focusTrapUtils.setElementFocus)(currentFocusedElement);
142
+ } else if (wrapperRef !== null && wrapperRef !== void 0 && (_wrapperRef$current = wrapperRef.current) !== null && _wrapperRef$current !== void 0 && _wrapperRef$current.hasAttribute("tabindex")) {
143
+ (0, _focusTrapUtils.setElementFocus)(wrapperRef.current);
137
144
  } else if (firstElement) {
138
145
  (0, _focusTrapUtils.setElementFocus)(firstElement);
139
146
  }
140
- }, [currentFocusedElement, firstElement]);
147
+ }, [currentFocusedElement, firstElement, wrapperRef]);
141
148
  (0, _react.useEffect)(() => {
142
149
  if (triggerRefocusFlag) {
143
150
  refocusTrap();
144
151
  }
145
152
  }, [triggerRefocusFlag, refocusTrap]);
153
+ const [tabIndex, setTabIndex] = (0, _react.useState)(0);
154
+ (0, _react.useEffect)(() => {
155
+ // issue in cypress prevents setting tabIndex to -1, instead tabIndex is set to 0 and removed on blur.
156
+ if (!isOpen) {
157
+ setTabIndex(0);
158
+ }
159
+ }, [isOpen]);
160
+
161
+ const onBlur = () => {
162
+ /* istanbul ignore else */
163
+ if (isOpen) {
164
+ setTabIndex(undefined);
165
+ }
166
+ };
167
+
168
+ const focusProps = {
169
+ tabIndex,
170
+ onBlur
171
+ }; // passes focusProps if no tabindex has been explicitly set on the wrapper
172
+
173
+ const clonedChildren = _react.default.Children.map(children, child => {
174
+ return child.props.tabIndex === undefined ? /*#__PURE__*/_react.default.cloneElement(child, focusProps) : child;
175
+ });
176
+
146
177
  return /*#__PURE__*/_react.default.createElement("div", {
147
178
  ref: trapRef
148
- }, children);
179
+ }, clonedChildren);
149
180
  };
150
181
 
151
182
  FocusTrap.propTypes = {
@@ -165,7 +196,10 @@ FocusTrap.propTypes = {
165
196
  /** a ref to the container wrapping the focusable elements */
166
197
  wrapperRef: _propTypes.default.shape({
167
198
  current: _propTypes.default.any
168
- })
199
+ }),
200
+
201
+ /* whether the modal (etc.) component that the focus trap is inside is open or not */
202
+ isOpen: _propTypes.default.bool
169
203
  };
170
204
  var _default = FocusTrap;
171
205
  exports.default = _default;
@@ -68,17 +68,17 @@ const AdvancedColorPicker = ({
68
68
  setSelectedColorRef(selected.ref.current);
69
69
  }
70
70
  }, [colors, currentColor, dialogOpen, isOpen]);
71
- const handleFocus = (0, _react.useCallback)((e, firstFocusableElement, lastFocusableElement) => {
71
+ const handleFocus = (0, _react.useCallback)((e, firstFocusableElement) => {
72
72
  if (e.key === "Tab") {
73
73
  /* istanbul ignore else */
74
74
  if (e.shiftKey) {
75
75
  /* istanbul ignore else */
76
- if (document.activeElement === selectedColorRef) {
77
- lastFocusableElement.focus();
76
+ if (document.activeElement === firstFocusableElement) {
77
+ selectedColorRef.focus();
78
78
  e.preventDefault();
79
79
  }
80
- } else if (document.activeElement === lastFocusableElement) {
81
- selectedColorRef.focus();
80
+ } else if (document.activeElement === selectedColorRef) {
81
+ firstFocusableElement.focus();
82
82
  e.preventDefault();
83
83
  }
84
84
  }
@@ -179,7 +179,8 @@ const Dialog = ({
179
179
  autoFocus: !disableAutoFocus,
180
180
  focusFirstElement: focusFirstElement,
181
181
  bespokeTrap: bespokeFocusTrap,
182
- wrapperRef: dialogRef
182
+ wrapperRef: dialogRef,
183
+ isOpen: open
183
184
  }, /*#__PURE__*/_react.default.createElement(_dialog.DialogStyle, _extends({
184
185
  "aria-modal": true,
185
186
  ref: dialogRef,
@@ -189,9 +190,9 @@ const Dialog = ({
189
190
  "data-element": "dialog",
190
191
  "data-role": rest["data-role"],
191
192
  role: role
192
- }, contentPadding), dialogTitle(), /*#__PURE__*/_react.default.createElement(_dialog.DialogContentStyle, contentPadding, /*#__PURE__*/_react.default.createElement(_dialog.DialogInnerContentStyle, _extends({
193
+ }, contentPadding), dialogTitle(), closeIcon(), /*#__PURE__*/_react.default.createElement(_dialog.DialogContentStyle, contentPadding, /*#__PURE__*/_react.default.createElement(_dialog.DialogInnerContentStyle, _extends({
193
194
  ref: innerContentRef
194
- }, contentPadding), children)), closeIcon())));
195
+ }, contentPadding), children)))));
195
196
  };
196
197
 
197
198
  Dialog.propTypes = {
@@ -109,7 +109,8 @@ const DialogFullScreen = ({
109
109
  }, componentTags), /*#__PURE__*/_react.default.createElement(_focusTrap.default, {
110
110
  autoFocus: !disableAutoFocus,
111
111
  focusFirstElement: focusFirstElement,
112
- wrapperRef: dialogRef
112
+ wrapperRef: dialogRef,
113
+ isOpen: open
113
114
  }, /*#__PURE__*/_react.default.createElement(_dialogFullScreen.default, _extends({
114
115
  "aria-modal": role === "dialog" ? true : undefined
115
116
  }, ariaProps, {
@@ -117,12 +118,12 @@ const DialogFullScreen = ({
117
118
  "data-element": "dialog-full-screen",
118
119
  pagesStyling: pagesStyling,
119
120
  role: role
120
- }), dialogTitle(), /*#__PURE__*/_react.default.createElement(_content.default, {
121
+ }), dialogTitle(), closeIcon(), /*#__PURE__*/_react.default.createElement(_content.default, {
121
122
  hasHeader: title !== undefined,
122
123
  "data-element": "content",
123
124
  ref: contentRef,
124
125
  disableContentPadding: disableContentPadding
125
- }, children), closeIcon())));
126
+ }, children))));
126
127
  };
127
128
 
128
129
  DialogFullScreen.defaultProps = {
@@ -57,19 +57,6 @@ const MenuFullscreen = ({
57
57
  }
58
58
  };
59
59
 
60
- (0, _react.useLayoutEffect)(() => {
61
- const checkTransitionEnd = () => {
62
- menuContentRef.current.focus();
63
- };
64
-
65
- const wrapperRef = menuWrapperRef.current;
66
-
67
- if (isOpen) {
68
- wrapperRef.addEventListener("transitionend", checkTransitionEnd);
69
- } else {
70
- wrapperRef.removeEventListener("transitionend", checkTransitionEnd);
71
- }
72
- }, [isOpen]);
73
60
  const scrollVariants = {
74
61
  light: "light",
75
62
  dark: "dark",
@@ -85,8 +72,8 @@ const MenuFullscreen = ({
85
72
  return /*#__PURE__*/_react.default.createElement("li", {
86
73
  "aria-label": "menu-fullscreen"
87
74
  }, /*#__PURE__*/_react.default.createElement(_portal.default, null, /*#__PURE__*/_react.default.createElement(_focusTrap.default, {
88
- autoFocus: false,
89
- wrapperRef: menuWrapperRef
75
+ wrapperRef: menuWrapperRef,
76
+ isOpen: isOpen
90
77
  }, /*#__PURE__*/_react.default.createElement(_menuFullScreen.StyledMenuFullscreen, _extends({
91
78
  "data-component": "menu-fullscreen",
92
79
  ref: menuWrapperRef,
@@ -117,8 +104,7 @@ const MenuFullscreen = ({
117
104
  display: "flex",
118
105
  flexDirection: "column",
119
106
  role: "list",
120
- inFullscreenView: true,
121
- tabIndex: -1
107
+ inFullscreenView: true
122
108
  }, _react.default.Children.map(children, (child, index) => /*#__PURE__*/_react.default.createElement(_menu2.default.Provider, {
123
109
  value: {
124
110
  inFullscreenView: true,
@@ -33,6 +33,7 @@ const StyledMenuFullscreen = _styledComponents.default.div`
33
33
  bottom: 0;
34
34
  height: 100vh;
35
35
  width: 100%;
36
+ outline: none;
36
37
 
37
38
  a,
38
39
  button,
@@ -1,13 +1,15 @@
1
1
  export default SidebarHeader;
2
- declare function SidebarHeader({ className, children, ...props }: {
2
+ declare function SidebarHeader({ className, children, id, ...props }: {
3
3
  [x: string]: any;
4
4
  className: any;
5
5
  children: any;
6
+ id: any;
6
7
  }): JSX.Element;
7
8
  declare namespace SidebarHeader {
8
9
  namespace propTypes {
9
10
  const children: PropTypes.Requireable<PropTypes.ReactNodeLike>;
10
11
  const className: PropTypes.Requireable<string>;
12
+ const id: PropTypes.Requireable<string>;
11
13
  }
12
14
  }
13
15
  import PropTypes from "prop-types";
@@ -20,9 +20,11 @@ function _extends() { _extends = Object.assign || function (target) { for (var i
20
20
  const SidebarHeader = ({
21
21
  className,
22
22
  children,
23
+ id,
23
24
  ...props
24
25
  }) => /*#__PURE__*/_react.default.createElement(_sidebarHeader.default, _extends({
25
- className: className
26
+ className: className,
27
+ id: id
26
28
  }, (0, _tags.default)("sidebar-header", props)), children);
27
29
 
28
30
  SidebarHeader.propTypes = {
@@ -30,7 +32,10 @@ SidebarHeader.propTypes = {
30
32
  children: _propTypes.default.node,
31
33
 
32
34
  /** A custom class name. */
33
- className: _propTypes.default.string
35
+ className: _propTypes.default.string,
36
+
37
+ /** A custom id. */
38
+ id: _propTypes.default.string
34
39
  };
35
40
  var _default = SidebarHeader;
36
41
  exports.default = _default;
@@ -25,6 +25,8 @@ var _box = _interopRequireDefault(require("../box"));
25
25
 
26
26
  var _sidebar2 = require("./sidebar.config");
27
27
 
28
+ var _guid = _interopRequireDefault(require("../../__internal__/utils/helpers/guid"));
29
+
28
30
  var _useLocale = _interopRequireDefault(require("../../hooks/__internal__/useLocale"));
29
31
 
30
32
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -55,6 +57,9 @@ const Sidebar = /*#__PURE__*/_react.default.forwardRef(({
55
57
  ...rest
56
58
  }, ref) => {
57
59
  const locale = (0, _useLocale.default)();
60
+ const {
61
+ current: titleId
62
+ } = (0, _react.useRef)((0, _guid.default)());
58
63
  let sidebarRef = (0, _react.useRef)();
59
64
  if (ref) sidebarRef = ref;
60
65
 
@@ -79,14 +84,16 @@ const Sidebar = /*#__PURE__*/_react.default.forwardRef(({
79
84
  "aria-modal": !enableBackgroundUI,
80
85
  "aria-describedby": ariaDescribedBy,
81
86
  "aria-label": ariaLabel,
82
- "aria-labelledby": ariaLabelledBy,
87
+ "aria-labelledby": !ariaLabelledBy && !ariaLabel ? titleId : ariaLabelledBy,
83
88
  ref: sidebarRef,
84
89
  position: position,
85
90
  size: size,
86
91
  "data-element": "sidebar",
87
92
  onCancel: onCancel,
88
93
  role: role
89
- }, closeIcon(), header && /*#__PURE__*/_react.default.createElement(_sidebarHeader.default, null, header), /*#__PURE__*/_react.default.createElement(_box.default, {
94
+ }, header && /*#__PURE__*/_react.default.createElement(_sidebarHeader.default, {
95
+ id: titleId
96
+ }, header), closeIcon(), /*#__PURE__*/_react.default.createElement(_box.default, {
90
97
  "data-element": "sidebar-content",
91
98
  p: 4,
92
99
  pt: "27px",
@@ -106,7 +113,8 @@ const Sidebar = /*#__PURE__*/_react.default.forwardRef(({
106
113
  enableBackgroundUI: enableBackgroundUI,
107
114
  className: "carbon-sidebar"
108
115
  }, componentTags), enableBackgroundUI ? sidebar : /*#__PURE__*/_react.default.createElement(_focusTrap.default, {
109
- wrapperRef: sidebarRef
116
+ wrapperRef: sidebarRef,
117
+ isOpen: open
110
118
  }, sidebar));
111
119
  });
112
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carbon-react",
3
- "version": "106.6.10",
3
+ "version": "106.7.0",
4
4
  "description": "A library of reusable React components for easily building user interfaces.",
5
5
  "engineStrict": true,
6
6
  "engines": {