carbon-react 109.2.4 → 109.3.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 (47) hide show
  1. package/esm/__internal__/field-help/field-help.component.d.ts +10 -0
  2. package/esm/__internal__/field-help/field-help.component.js +12 -16
  3. package/esm/__internal__/field-help/field-help.style.d.ts +8 -0
  4. package/esm/__internal__/field-help/field-help.style.js +2 -10
  5. package/esm/__internal__/field-help/index.d.ts +2 -1
  6. package/esm/__internal__/focus-trap/focus-trap-utils.d.ts +1 -2
  7. package/esm/__internal__/focus-trap/focus-trap-utils.js +57 -8
  8. package/esm/__internal__/focus-trap/focus-trap.component.js +35 -25
  9. package/esm/__spec_helper__/index.d.ts +1 -0
  10. package/esm/__spec_helper__/index.js +4 -10
  11. package/esm/__spec_helper__/mock-match-media.d.ts +2 -2
  12. package/esm/__spec_helper__/mock-match-media.js +2 -2
  13. package/esm/__spec_helper__/mock-resize-observer.d.ts +2 -0
  14. package/esm/components/alert/alert.component.js +9 -0
  15. package/esm/components/dialog/dialog.component.js +9 -2
  16. package/esm/components/dialog/dialog.d.ts +2 -0
  17. package/esm/components/dialog-full-screen/dialog-full-screen.component.js +9 -2
  18. package/esm/components/dialog-full-screen/dialog-full-screen.d.ts +2 -0
  19. package/esm/components/sidebar/sidebar.component.js +9 -2
  20. package/esm/components/sidebar/sidebar.d.ts +2 -0
  21. package/esm/components/toast/toast.component.js +35 -9
  22. package/esm/components/toast/toast.d.ts +5 -1
  23. package/lib/__internal__/field-help/field-help.component.d.ts +10 -0
  24. package/lib/__internal__/field-help/field-help.component.js +12 -16
  25. package/lib/__internal__/field-help/field-help.style.d.ts +8 -0
  26. package/lib/__internal__/field-help/field-help.style.js +2 -13
  27. package/lib/__internal__/field-help/index.d.ts +2 -1
  28. package/lib/__internal__/focus-trap/focus-trap-utils.d.ts +1 -2
  29. package/lib/__internal__/focus-trap/focus-trap-utils.js +57 -10
  30. package/lib/__internal__/focus-trap/focus-trap.component.js +34 -24
  31. package/lib/__spec_helper__/index.d.ts +1 -0
  32. package/lib/__spec_helper__/index.js +3 -10
  33. package/lib/__spec_helper__/mock-match-media.d.ts +2 -2
  34. package/lib/__spec_helper__/mock-match-media.js +4 -4
  35. package/lib/__spec_helper__/mock-resize-observer.d.ts +2 -0
  36. package/lib/components/alert/alert.component.js +9 -0
  37. package/lib/components/dialog/dialog.component.js +9 -2
  38. package/lib/components/dialog/dialog.d.ts +2 -0
  39. package/lib/components/dialog-full-screen/dialog-full-screen.component.js +9 -2
  40. package/lib/components/dialog-full-screen/dialog-full-screen.d.ts +2 -0
  41. package/lib/components/sidebar/sidebar.component.js +9 -2
  42. package/lib/components/sidebar/sidebar.d.ts +2 -0
  43. package/lib/components/toast/toast.component.js +35 -7
  44. package/lib/components/toast/toast.d.ts +5 -1
  45. package/package.json +3 -4
  46. package/esm/__internal__/field-help/field-help.d.ts +0 -14
  47. package/lib/__internal__/field-help/field-help.d.ts +0 -14
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { StyledFieldHelpProps } from "./field-help.style";
3
+ export interface FieldHelpProps extends StyledFieldHelpProps {
4
+ /** Child elements */
5
+ children?: React.ReactNode;
6
+ /** The unique id of the FieldHelp component */
7
+ id?: string;
8
+ }
9
+ export declare const FieldHelp: ({ children, labelInline, labelWidth, id, }: FieldHelpProps) => JSX.Element;
10
+ export default FieldHelp;
@@ -1,28 +1,24 @@
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
-
3
1
  import React from "react";
4
2
  import PropTypes from "prop-types";
5
- import FieldHelpStyle from "./field-help.style";
3
+ import StyledFieldHelp from "./field-help.style";
6
4
 
7
5
  const FieldHelp = ({
8
6
  children,
9
7
  labelInline,
10
- labelWidth,
11
- ...rest
12
- }) => /*#__PURE__*/React.createElement(FieldHelpStyle, _extends({
8
+ labelWidth = 30,
9
+ id
10
+ }) => /*#__PURE__*/React.createElement(StyledFieldHelp, {
13
11
  "data-element": "help",
14
12
  labelInline: labelInline,
15
- labelWidth: labelWidth
16
- }, rest), children);
13
+ labelWidth: labelWidth,
14
+ id: id
15
+ }, children);
17
16
 
18
17
  FieldHelp.propTypes = {
19
- /** Child elements */
20
- children: PropTypes.node,
21
-
22
- /** When true, label is placed in line an input */
23
- labelInline: PropTypes.bool,
24
-
25
- /** Width of a label in percentage. Works only when labelInline is true */
26
- labelWidth: PropTypes.number
18
+ "children": PropTypes.node,
19
+ "id": PropTypes.string,
20
+ "labelInline": PropTypes.bool,
21
+ "labelWidth": PropTypes.number
27
22
  };
23
+ export { FieldHelp };
28
24
  export default FieldHelp;
@@ -0,0 +1,8 @@
1
+ export interface StyledFieldHelpProps {
2
+ /** When true, label is placed in line an input */
3
+ labelInline?: boolean;
4
+ /** Width of a label in percentage. Works only when labelInline is true */
5
+ labelWidth?: number;
6
+ }
7
+ declare const StyledFieldHelp: import("styled-components").StyledComponent<"span", any, StyledFieldHelpProps, never>;
8
+ export default StyledFieldHelp;
@@ -1,6 +1,5 @@
1
1
  import styled, { css } from "styled-components";
2
- import PropTypes from "prop-types";
3
- const FieldHelpStyle = styled.span`
2
+ const StyledFieldHelp = styled.span`
4
3
  display: block;
5
4
  flex: 1;
6
5
  margin-top: 8px;
@@ -14,11 +13,4 @@ const FieldHelpStyle = styled.span`
14
13
  padding-left: 0;
15
14
  `}
16
15
  `;
17
- FieldHelpStyle.defaultProps = {
18
- labelWidth: 30
19
- };
20
- FieldHelpStyle.propTypes = {
21
- labelWidth: PropTypes.number,
22
- labelInline: PropTypes.bool
23
- };
24
- export default FieldHelpStyle;
16
+ export default StyledFieldHelp;
@@ -1 +1,2 @@
1
- export { default } from "./field-help";
1
+ export { default } from "./field-help.component";
2
+ export type { FieldHelpProps } from "./field-help.component";
@@ -1,4 +1,3 @@
1
1
  export const defaultFocusableSelectors: "button:not([disabled]), [href], input:not([type=\"hidden\"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]";
2
- export function nextNonRadioElementIndex(element: any, focusableElements: any): number;
3
- export function isRadio(element: any): any;
2
+ export function getNextElement(element: any, focusableElements: any, shiftKey: any): any;
4
3
  export function setElementFocus(element: any): void;
@@ -37,17 +37,66 @@ const isRadio = element => {
37
37
  return element.hasAttribute("type") && element.getAttribute("type") === "radio";
38
38
  };
39
39
 
40
- const nextNonRadioElementIndex = (element, focusableElements) => {
40
+ const getRadioElementToFocus = (groupName, shiftKey) => {
41
+ const buttonsInGroup = document.querySelectorAll(`input[type="radio"][name="${groupName}"]`);
42
+ const selectedButton = [...buttonsInGroup].find(button => button.checked);
43
+
44
+ if (selectedButton) {
45
+ return selectedButton;
46
+ }
47
+
48
+ return buttonsInGroup[shiftKey ? buttonsInGroup.length - 1 : 0];
49
+ };
50
+
51
+ const getNextElement = (element, focusableElements, shiftKey) => {
41
52
  const currentIndex = focusableElements.indexOf(element);
42
- let nextIndex = currentIndex - 1;
43
- if (currentIndex === 0) return focusableElements.length - 1;
44
- const isNextRadio = isRadio(focusableElements[nextIndex]);
53
+ const increment = shiftKey ? -1 : 1;
54
+ let nextIndex = currentIndex;
55
+ let foundElement;
56
+
57
+ while (!foundElement) {
58
+ nextIndex += increment;
59
+
60
+ if (nextIndex < 0) {
61
+ nextIndex += focusableElements.length;
62
+ }
63
+
64
+ if (nextIndex >= focusableElements.length) {
65
+ nextIndex -= focusableElements.length;
66
+ }
67
+
68
+ const nextElement = focusableElements[nextIndex];
69
+
70
+ if (nextElement === element) {
71
+ // guard in case there is only one focusable element (or only a single radio group) in the trap.
72
+ // If this happens we don't want to freeze the browser by looping forever, and it's OK to just focus
73
+ // the same element we're already on
74
+ return element;
75
+ }
76
+
77
+ if (isRadio(nextElement)) {
78
+ // if we've reached a radio element we need to ensure we focus the correct button in its group
79
+ const nextElementGroupName = nextElement.getAttribute("name");
80
+
81
+ if (isRadio(element)) {
82
+ const groupName = element.getAttribute("name"); // if the name is different we're in a new group so can focus the appropriate button in it*/
83
+
84
+ if (nextElementGroupName !== groupName) {
85
+ foundElement = getRadioElementToFocus(nextElementGroupName, shiftKey);
86
+ } // otherwise we're still in the same radio group so need to continue the loop
45
87
 
46
- if (isNextRadio) {
47
- nextIndex = nextNonRadioElementIndex(focusableElements[nextIndex], focusableElements);
88
+ } else {
89
+ // if we've moved into a radio group from a non-radio starting point, we still have to ensure we focus
90
+ // the correct button in the group
91
+ foundElement = getRadioElementToFocus(nextElementGroupName, shiftKey);
92
+ }
93
+ } else {
94
+ // if we've reached a non-radio element, we can focus it with no issues
95
+ foundElement = nextElement;
96
+ }
48
97
  }
49
98
 
50
- return nextIndex;
99
+ return foundElement;
51
100
  };
52
101
 
53
- export { defaultFocusableSelectors, nextNonRadioElementIndex, isRadio, setElementFocus };
102
+ export { defaultFocusableSelectors, getNextElement, setElementFocus };
@@ -1,6 +1,6 @@
1
1
  import React, { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import PropTypes from "prop-types";
3
- import { defaultFocusableSelectors, nextNonRadioElementIndex, isRadio, setElementFocus } from "./focus-trap-utils";
3
+ import { defaultFocusableSelectors, getNextElement, setElementFocus } from "./focus-trap-utils";
4
4
  import { ModalContext } from "../../components/modal/modal.component";
5
5
  import usePrevious from "../../hooks/__internal__/usePrevious";
6
6
 
@@ -10,7 +10,8 @@ const FocusTrap = ({
10
10
  focusFirstElement,
11
11
  bespokeTrap,
12
12
  wrapperRef,
13
- isOpen
13
+ isOpen,
14
+ additionalWrapperRefs
14
15
  }) => {
15
16
  const trapRef = useRef(null);
16
17
  const [focusableElements, setFocusableElements] = useState();
@@ -28,19 +29,21 @@ const FocusTrap = ({
28
29
 
29
30
  return Array.from(candidate).some((el, i) => el !== focusableElements[i]);
30
31
  }, [focusableElements]);
32
+ const allRefs = [wrapperRef, ...additionalWrapperRefs].map(ref => ref === null || ref === void 0 ? void 0 : ref.current);
31
33
  const updateFocusableElements = useCallback(() => {
32
- const ref = wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current;
33
-
34
- if (ref) {
35
- const elements = Array.from(ref.querySelectorAll(defaultFocusableSelectors)).filter(el => Number(el.tabIndex) !== -1);
36
-
37
- if (hasNewInputs(elements)) {
38
- setFocusableElements(Array.from(elements));
39
- setFirstElement(elements[0]);
40
- setLastElement(elements[elements.length - 1]);
34
+ const elements = [];
35
+ allRefs.forEach(ref => {
36
+ if (ref) {
37
+ elements.push(...Array.from(ref.querySelectorAll(defaultFocusableSelectors)).filter(el => Number(el.tabIndex) !== -1));
41
38
  }
39
+ });
40
+
41
+ if (hasNewInputs(elements)) {
42
+ setFocusableElements(Array.from(elements));
43
+ setFirstElement(elements[0]);
44
+ setLastElement(elements[elements.length - 1]);
42
45
  }
43
- }, [hasNewInputs, wrapperRef]);
46
+ }, [hasNewInputs, allRefs]);
44
47
  useEffect(() => {
45
48
  const observer = new MutationObserver(updateFocusableElements);
46
49
  observer.observe(trapRef.current, {
@@ -78,20 +81,19 @@ const FocusTrap = ({
78
81
  ev.preventDefault();
79
82
  } else if (ev.shiftKey) {
80
83
  /* shift + tab */
81
- if (activeElement === firstElement || activeElement === wrapperRef.current) {
82
- lastElement.focus();
83
- ev.preventDefault();
84
- } // If current element is radio button -
85
- // find next non radio button element
86
-
84
+ let elementToFocus;
87
85
 
88
- if (isRadio(activeElement)) {
89
- const nextIndex = nextNonRadioElementIndex(activeElement, focusableElements);
90
- setElementFocus(focusableElements[nextIndex]);
91
- ev.preventDefault();
86
+ if (activeElement === wrapperRef.current) {
87
+ elementToFocus = getNextElement(firstElement, focusableElements, ev.shiftKey);
88
+ } else {
89
+ elementToFocus = getNextElement(activeElement, focusableElements, ev.shiftKey);
92
90
  }
93
- } else if (activeElement === lastElement) {
94
- firstElement.focus();
91
+
92
+ setElementFocus(elementToFocus);
93
+ ev.preventDefault();
94
+ } else {
95
+ const elementToFocus = getNextElement(activeElement, focusableElements, ev.shiftKey);
96
+ setElementFocus(elementToFocus);
95
97
  ev.preventDefault();
96
98
  }
97
99
  }
@@ -178,6 +180,14 @@ FocusTrap.propTypes = {
178
180
  }),
179
181
 
180
182
  /* whether the modal (etc.) component that the focus trap is inside is open or not */
181
- isOpen: PropTypes.bool
183
+ isOpen: PropTypes.bool,
184
+
185
+ /** an optional array of refs to containers whose content should also be reachable from the FocusTrap */
186
+ additionalWrapperRefs: PropTypes.arrayOf(PropTypes.shape({
187
+ current: PropTypes.any
188
+ }))
189
+ };
190
+ FocusTrap.defaultProps = {
191
+ additionalWrapperRefs: []
182
192
  };
183
193
  export default FocusTrap;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,12 +1,6 @@
1
- import Enzyme from "enzyme";
2
- import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
3
- import { setup } from "./mock-match-media";
1
+ import { enableFetchMocks } from "jest-fetch-mock";
2
+ import { setupMatchMediaMock } from "./mock-match-media";
4
3
  import setupResizeObserverMock from "./mock-resize-observer";
5
-
6
- require("jest-fetch-mock").enableMocks();
7
-
4
+ enableFetchMocks();
8
5
  setupResizeObserverMock();
9
- setup();
10
- Enzyme.configure({
11
- adapter: new Adapter()
12
- });
6
+ setupMatchMediaMock();
@@ -1,6 +1,6 @@
1
- declare function setup(): void;
1
+ declare function setupMatchMediaMock(): void;
2
2
  declare function mockMatchMedia(
3
3
  matches?: boolean
4
4
  ): { removeListener: jest.Mock };
5
5
 
6
- export { setup, mockMatchMedia };
6
+ export { setupMatchMediaMock, mockMatchMedia };
@@ -1,7 +1,7 @@
1
1
  let mocked = false;
2
2
  let _matches = false;
3
3
  const removeListener = jest.fn();
4
- export const setup = () => {
4
+ export const setupMatchMediaMock = () => {
5
5
  if (!global.window) {
6
6
  return;
7
7
  }
@@ -23,7 +23,7 @@ export const setup = () => {
23
23
  };
24
24
  export const mockMatchMedia = matches => {
25
25
  if (!mocked) {
26
- throw new Error("window.matchMedia has not been mocked. Did you call setup()?");
26
+ throw new Error("window.matchMedia has not been mocked. Did you call setupMatchMediaMock()?");
27
27
  }
28
28
 
29
29
  _matches = matches;
@@ -0,0 +1,2 @@
1
+ export default setupResizeObserverMock;
2
+ declare function setupResizeObserverMock(): void;
@@ -30,6 +30,15 @@ Alert.propTypes = {
30
30
  "disableEscKey": PropTypes.bool,
31
31
  "disableFocusTrap": PropTypes.bool,
32
32
  "enableBackgroundUI": PropTypes.bool,
33
+ "focusableContainers": PropTypes.arrayOf(PropTypes.shape({
34
+ "current": function (props, propName) {
35
+ if (props[propName] == null) {
36
+ return new Error("Prop '" + propName + "' is required but wasn't specified");
37
+ } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {
38
+ return new Error("Expected prop '" + propName + "' to be of type Element");
39
+ }
40
+ }
41
+ })),
33
42
  "focusFirstElement": PropTypes.shape({
34
43
  "current": function (props, propName) {
35
44
  if (props[propName] == null) {
@@ -31,6 +31,7 @@ const Dialog = ({
31
31
  help,
32
32
  role = "dialog",
33
33
  contentPadding = {},
34
+ focusableContainers,
34
35
  ...rest
35
36
  }) => {
36
37
  const locale = useLocale();
@@ -156,7 +157,8 @@ const Dialog = ({
156
157
  focusFirstElement: focusFirstElement,
157
158
  bespokeTrap: bespokeFocusTrap,
158
159
  wrapperRef: dialogRef,
159
- isOpen: open
160
+ isOpen: open,
161
+ additionalWrapperRefs: focusableContainers
160
162
  }, /*#__PURE__*/React.createElement(DialogStyle, _extends({
161
163
  "aria-modal": true,
162
164
  ref: dialogRef,
@@ -247,7 +249,12 @@ Dialog.propTypes = {
247
249
  p: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8]),
248
250
  px: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8]),
249
251
  py: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8])
250
- })
252
+ }),
253
+
254
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the dialog */
255
+ focusableContainers: PropTypes.arrayOf(PropTypes.shape({
256
+ current: PropTypes.any
257
+ }))
251
258
  };
252
259
  Dialog.defaultProps = {
253
260
  size: "medium",
@@ -59,6 +59,8 @@ export interface DialogProps extends ModalProps {
59
59
  role?: string;
60
60
  /** Padding to be set on the Dialog content */
61
61
  contentPadding?: ContentPaddingInterface;
62
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the dialog */
63
+ focusableContainers?: React.MutableRefObject<HTMLElement>[];
62
64
  }
63
65
 
64
66
  declare function Dialog(props: DialogProps): JSX.Element;
@@ -32,6 +32,7 @@ const DialogFullScreen = ({
32
32
  contentRef,
33
33
  help,
34
34
  role = "dialog",
35
+ focusableContainers,
35
36
  ...rest
36
37
  }) => {
37
38
  const locale = useLocale();
@@ -86,7 +87,8 @@ const DialogFullScreen = ({
86
87
  autoFocus: !disableAutoFocus,
87
88
  focusFirstElement: focusFirstElement,
88
89
  wrapperRef: dialogRef,
89
- isOpen: open
90
+ isOpen: open,
91
+ additionalWrapperRefs: focusableContainers
90
92
  }, /*#__PURE__*/React.createElement(StyledDialogFullScreen, _extends({
91
93
  "aria-modal": role === "dialog" ? true : undefined
92
94
  }, ariaProps, {
@@ -170,6 +172,11 @@ DialogFullScreen.propTypes = {
170
172
  })]),
171
173
 
172
174
  /** The ARIA role to be applied to the DialogFullscreen container */
173
- role: PropTypes.string
175
+ role: PropTypes.string,
176
+
177
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the dialog */
178
+ focusableContainers: PropTypes.arrayOf(PropTypes.shape({
179
+ current: PropTypes.any
180
+ }))
174
181
  };
175
182
  export default DialogFullScreen;
@@ -41,6 +41,8 @@ export interface DialogFullScreenProps extends ModalProps {
41
41
  title?: React.ReactNode;
42
42
  /** The ARIA role to be applied to the DialogFullscreen container */
43
43
  role?: string;
44
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the dialog */
45
+ focusableContainers?: React.MutableRefObject<HTMLElement>[];
44
46
  }
45
47
 
46
48
  declare function DialogFullScreen(props: DialogFullScreenProps): JSX.Element;
@@ -26,6 +26,7 @@ const Sidebar = /*#__PURE__*/React.forwardRef(({
26
26
  children,
27
27
  onCancel,
28
28
  role = "dialog",
29
+ focusableContainers,
29
30
  ...rest
30
31
  }, ref) => {
31
32
  const locale = useLocale();
@@ -84,7 +85,8 @@ const Sidebar = /*#__PURE__*/React.forwardRef(({
84
85
  className: "carbon-sidebar"
85
86
  }, componentTags), enableBackgroundUI ? sidebar : /*#__PURE__*/React.createElement(FocusTrap, {
86
87
  wrapperRef: sidebarRef,
87
- isOpen: open
88
+ isOpen: open,
89
+ additionalWrapperRefs: focusableContainers
88
90
  }, sidebar));
89
91
  });
90
92
  Sidebar.propTypes = {
@@ -122,7 +124,12 @@ Sidebar.propTypes = {
122
124
  header: PropTypes.node,
123
125
 
124
126
  /** The ARIA role to be applied to the container */
125
- role: PropTypes.string
127
+ role: PropTypes.string,
128
+
129
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the sidebar */
130
+ focusableContainers: PropTypes.arrayOf(PropTypes.shape({
131
+ current: PropTypes.any
132
+ }))
126
133
  };
127
134
  Sidebar.defaultProps = {
128
135
  position: "right",
@@ -42,6 +42,8 @@ export interface SidebarProps {
42
42
  | "medium-large"
43
43
  | "large"
44
44
  | "extra-large";
45
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the sidebar */
46
+ focusableContainers?: React.MutableRefObject<HTMLElement>[];
45
47
  }
46
48
 
47
49
  declare const SidebarContext: React.Context<SidebarContextProps>;
@@ -11,8 +11,7 @@ import IconButton from "../icon-button";
11
11
  import Events from "../../__internal__/utils/helpers/events";
12
12
  import useLocale from "../../hooks/__internal__/useLocale";
13
13
  import useModalManager from "../../hooks/__internal__/useModalManager";
14
-
15
- const Toast = ({
14
+ const Toast = /*#__PURE__*/React.forwardRef(({
16
15
  children,
17
16
  className,
18
17
  id,
@@ -23,12 +22,16 @@ const Toast = ({
23
22
  targetPortalId,
24
23
  timeout,
25
24
  variant,
25
+ disableAutoFocus,
26
26
  ...restProps
27
- }) => {
27
+ }, ref) => {
28
28
  const locale = useLocale();
29
29
  const toastRef = useRef();
30
30
  const timer = useRef();
31
31
  const toastContentNodeRef = useRef();
32
+ const closeIconRef = useRef();
33
+ const focusedElementBeforeOpening = useRef();
34
+ const refToPass = ref || toastRef;
32
35
  const componentClasses = useMemo(() => {
33
36
  return classNames(className);
34
37
  }, [className]);
@@ -38,7 +41,7 @@ const Toast = ({
38
41
  onDismiss(ev);
39
42
  }
40
43
  }, [onDismiss]);
41
- useModalManager(open, dismissToast, toastRef);
44
+ useModalManager(open, dismissToast, refToPass);
42
45
  useEffect(() => {
43
46
  clearTimeout(timer.current);
44
47
 
@@ -48,13 +51,34 @@ const Toast = ({
48
51
 
49
52
  timer.current = setTimeout(() => onDismiss(), timeout);
50
53
  }, [onDismiss, open, timeout]);
54
+ useEffect(() => {
55
+ if (onDismiss && !disableAutoFocus) {
56
+ if (open) {
57
+ var _closeIconRef$current;
58
+
59
+ focusedElementBeforeOpening.current = document.activeElement;
60
+ (_closeIconRef$current = closeIconRef.current) === null || _closeIconRef$current === void 0 ? void 0 : _closeIconRef$current.focus();
61
+ } else if (focusedElementBeforeOpening.current) {
62
+ focusedElementBeforeOpening.current.focus();
63
+ focusedElementBeforeOpening.current = undefined;
64
+ }
65
+ }
66
+ }, [open, onDismiss, disableAutoFocus]);
67
+ useEffect(() => {
68
+ return () => {
69
+ if (focusedElementBeforeOpening.current) {
70
+ focusedElementBeforeOpening.current.focus();
71
+ }
72
+ };
73
+ }, []);
51
74
 
52
75
  function renderCloseIcon() {
53
76
  if (!onDismiss) return null;
54
77
  return /*#__PURE__*/React.createElement(IconButton, {
55
78
  "aria-label": locale.toast.ariaLabels.close(),
56
79
  "data-element": "close",
57
- onAction: onDismiss
80
+ onAction: onDismiss,
81
+ ref: closeIconRef
58
82
  }, /*#__PURE__*/React.createElement(Icon, {
59
83
  type: "close"
60
84
  }));
@@ -96,10 +120,9 @@ const Toast = ({
96
120
  isCenter: isCenter
97
121
  }, /*#__PURE__*/React.createElement(ToastWrapper, {
98
122
  isCenter: isCenter,
99
- ref: toastRef
123
+ ref: refToPass
100
124
  }, /*#__PURE__*/React.createElement(TransitionGroup, null, renderToastContent())));
101
- };
102
-
125
+ });
103
126
  Toast.propTypes = {
104
127
  /** Customizes the appearance in the DLS theme */
105
128
  variant: PropTypes.oneOf(["error", "info", "success", "warning"]),
@@ -132,6 +155,9 @@ Toast.propTypes = {
132
155
  targetPortalId: PropTypes.string,
133
156
 
134
157
  /** Maximum toast width */
135
- maxWidth: PropTypes.string
158
+ maxWidth: PropTypes.string,
159
+
160
+ /** Disables auto focus functionality when the Toast has a close icon */
161
+ disableAutoFocus: PropTypes.bool
136
162
  };
137
163
  export default Toast;
@@ -25,8 +25,12 @@ export interface ToastPropTypes {
25
25
  targetPortalId?: string;
26
26
  /** Maximum toast width */
27
27
  maxWidth?: string;
28
+ /** Disables auto focus functionality when the Toast has a close icon */
29
+ disableAutoFocus?: boolean;
28
30
  }
29
31
 
30
- declare class Toast extends React.Component<ToastPropTypes> {}
32
+ declare function Toast(
33
+ props: ToastPropTypes & React.RefAttributes<HTMLDivElement>
34
+ ): JSX.Element;
31
35
 
32
36
  export default Toast;
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { StyledFieldHelpProps } from "./field-help.style";
3
+ export interface FieldHelpProps extends StyledFieldHelpProps {
4
+ /** Child elements */
5
+ children?: React.ReactNode;
6
+ /** The unique id of the FieldHelp component */
7
+ id?: string;
8
+ }
9
+ export declare const FieldHelp: ({ children, labelInline, labelWidth, id, }: FieldHelpProps) => JSX.Element;
10
+ export default FieldHelp;
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = void 0;
6
+ exports.default = exports.FieldHelp = void 0;
7
7
 
8
8
  var _react = _interopRequireDefault(require("react"));
9
9
 
@@ -13,28 +13,24 @@ var _fieldHelp = _interopRequireDefault(require("./field-help.style"));
13
13
 
14
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
15
 
16
- 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); }
17
-
18
16
  const FieldHelp = ({
19
17
  children,
20
18
  labelInline,
21
- labelWidth,
22
- ...rest
23
- }) => /*#__PURE__*/_react.default.createElement(_fieldHelp.default, _extends({
19
+ labelWidth = 30,
20
+ id
21
+ }) => /*#__PURE__*/_react.default.createElement(_fieldHelp.default, {
24
22
  "data-element": "help",
25
23
  labelInline: labelInline,
26
- labelWidth: labelWidth
27
- }, rest), children);
24
+ labelWidth: labelWidth,
25
+ id: id
26
+ }, children);
28
27
 
28
+ exports.FieldHelp = FieldHelp;
29
29
  FieldHelp.propTypes = {
30
- /** Child elements */
31
- children: _propTypes.default.node,
32
-
33
- /** When true, label is placed in line an input */
34
- labelInline: _propTypes.default.bool,
35
-
36
- /** Width of a label in percentage. Works only when labelInline is true */
37
- labelWidth: _propTypes.default.number
30
+ "children": _propTypes.default.node,
31
+ "id": _propTypes.default.string,
32
+ "labelInline": _propTypes.default.bool,
33
+ "labelWidth": _propTypes.default.number
38
34
  };
39
35
  var _default = FieldHelp;
40
36
  exports.default = _default;
@@ -0,0 +1,8 @@
1
+ export interface StyledFieldHelpProps {
2
+ /** When true, label is placed in line an input */
3
+ labelInline?: boolean;
4
+ /** Width of a label in percentage. Works only when labelInline is true */
5
+ labelWidth?: number;
6
+ }
7
+ declare const StyledFieldHelp: import("styled-components").StyledComponent<"span", any, StyledFieldHelpProps, never>;
8
+ export default StyledFieldHelp;
@@ -7,15 +7,11 @@ exports.default = void 0;
7
7
 
8
8
  var _styledComponents = _interopRequireWildcard(require("styled-components"));
9
9
 
10
- var _propTypes = _interopRequireDefault(require("prop-types"));
11
-
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
-
14
10
  function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
15
11
 
16
12
  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; }
17
13
 
18
- const FieldHelpStyle = _styledComponents.default.span`
14
+ const StyledFieldHelp = _styledComponents.default.span`
19
15
  display: block;
20
16
  flex: 1;
21
17
  margin-top: 8px;
@@ -29,12 +25,5 @@ const FieldHelpStyle = _styledComponents.default.span`
29
25
  padding-left: 0;
30
26
  `}
31
27
  `;
32
- FieldHelpStyle.defaultProps = {
33
- labelWidth: 30
34
- };
35
- FieldHelpStyle.propTypes = {
36
- labelWidth: _propTypes.default.number,
37
- labelInline: _propTypes.default.bool
38
- };
39
- var _default = FieldHelpStyle;
28
+ var _default = StyledFieldHelp;
40
29
  exports.default = _default;
@@ -1 +1,2 @@
1
- export { default } from "./field-help";
1
+ export { default } from "./field-help.component";
2
+ export type { FieldHelpProps } from "./field-help.component";
@@ -1,4 +1,3 @@
1
1
  export const defaultFocusableSelectors: "button:not([disabled]), [href], input:not([type=\"hidden\"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]";
2
- export function nextNonRadioElementIndex(element: any, focusableElements: any): number;
3
- export function isRadio(element: any): any;
2
+ export function getNextElement(element: any, focusableElements: any, shiftKey: any): any;
4
3
  export function setElementFocus(element: any): void;
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.setElementFocus = setElementFocus;
7
- exports.isRadio = exports.nextNonRadioElementIndex = exports.defaultFocusableSelectors = void 0;
7
+ exports.getNextElement = exports.defaultFocusableSelectors = void 0;
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
 
@@ -45,19 +45,66 @@ const isRadio = element => {
45
45
  return element.hasAttribute("type") && element.getAttribute("type") === "radio";
46
46
  };
47
47
 
48
- exports.isRadio = isRadio;
48
+ const getRadioElementToFocus = (groupName, shiftKey) => {
49
+ const buttonsInGroup = document.querySelectorAll(`input[type="radio"][name="${groupName}"]`);
50
+ const selectedButton = [...buttonsInGroup].find(button => button.checked);
49
51
 
50
- const nextNonRadioElementIndex = (element, focusableElements) => {
52
+ if (selectedButton) {
53
+ return selectedButton;
54
+ }
55
+
56
+ return buttonsInGroup[shiftKey ? buttonsInGroup.length - 1 : 0];
57
+ };
58
+
59
+ const getNextElement = (element, focusableElements, shiftKey) => {
51
60
  const currentIndex = focusableElements.indexOf(element);
52
- let nextIndex = currentIndex - 1;
53
- if (currentIndex === 0) return focusableElements.length - 1;
54
- const isNextRadio = isRadio(focusableElements[nextIndex]);
61
+ const increment = shiftKey ? -1 : 1;
62
+ let nextIndex = currentIndex;
63
+ let foundElement;
64
+
65
+ while (!foundElement) {
66
+ nextIndex += increment;
67
+
68
+ if (nextIndex < 0) {
69
+ nextIndex += focusableElements.length;
70
+ }
71
+
72
+ if (nextIndex >= focusableElements.length) {
73
+ nextIndex -= focusableElements.length;
74
+ }
75
+
76
+ const nextElement = focusableElements[nextIndex];
77
+
78
+ if (nextElement === element) {
79
+ // guard in case there is only one focusable element (or only a single radio group) in the trap.
80
+ // If this happens we don't want to freeze the browser by looping forever, and it's OK to just focus
81
+ // the same element we're already on
82
+ return element;
83
+ }
84
+
85
+ if (isRadio(nextElement)) {
86
+ // if we've reached a radio element we need to ensure we focus the correct button in its group
87
+ const nextElementGroupName = nextElement.getAttribute("name");
88
+
89
+ if (isRadio(element)) {
90
+ const groupName = element.getAttribute("name"); // if the name is different we're in a new group so can focus the appropriate button in it*/
91
+
92
+ if (nextElementGroupName !== groupName) {
93
+ foundElement = getRadioElementToFocus(nextElementGroupName, shiftKey);
94
+ } // otherwise we're still in the same radio group so need to continue the loop
55
95
 
56
- if (isNextRadio) {
57
- nextIndex = nextNonRadioElementIndex(focusableElements[nextIndex], focusableElements);
96
+ } else {
97
+ // if we've moved into a radio group from a non-radio starting point, we still have to ensure we focus
98
+ // the correct button in the group
99
+ foundElement = getRadioElementToFocus(nextElementGroupName, shiftKey);
100
+ }
101
+ } else {
102
+ // if we've reached a non-radio element, we can focus it with no issues
103
+ foundElement = nextElement;
104
+ }
58
105
  }
59
106
 
60
- return nextIndex;
107
+ return foundElement;
61
108
  };
62
109
 
63
- exports.nextNonRadioElementIndex = nextNonRadioElementIndex;
110
+ exports.getNextElement = getNextElement;
@@ -27,7 +27,8 @@ const FocusTrap = ({
27
27
  focusFirstElement,
28
28
  bespokeTrap,
29
29
  wrapperRef,
30
- isOpen
30
+ isOpen,
31
+ additionalWrapperRefs
31
32
  }) => {
32
33
  const trapRef = (0, _react.useRef)(null);
33
34
  const [focusableElements, setFocusableElements] = (0, _react.useState)();
@@ -45,19 +46,21 @@ const FocusTrap = ({
45
46
 
46
47
  return Array.from(candidate).some((el, i) => el !== focusableElements[i]);
47
48
  }, [focusableElements]);
49
+ const allRefs = [wrapperRef, ...additionalWrapperRefs].map(ref => ref === null || ref === void 0 ? void 0 : ref.current);
48
50
  const updateFocusableElements = (0, _react.useCallback)(() => {
49
- const ref = wrapperRef === null || wrapperRef === void 0 ? void 0 : wrapperRef.current;
50
-
51
- if (ref) {
52
- const elements = Array.from(ref.querySelectorAll(_focusTrapUtils.defaultFocusableSelectors)).filter(el => Number(el.tabIndex) !== -1);
53
-
54
- if (hasNewInputs(elements)) {
55
- setFocusableElements(Array.from(elements));
56
- setFirstElement(elements[0]);
57
- setLastElement(elements[elements.length - 1]);
51
+ const elements = [];
52
+ allRefs.forEach(ref => {
53
+ if (ref) {
54
+ elements.push(...Array.from(ref.querySelectorAll(_focusTrapUtils.defaultFocusableSelectors)).filter(el => Number(el.tabIndex) !== -1));
58
55
  }
56
+ });
57
+
58
+ if (hasNewInputs(elements)) {
59
+ setFocusableElements(Array.from(elements));
60
+ setFirstElement(elements[0]);
61
+ setLastElement(elements[elements.length - 1]);
59
62
  }
60
- }, [hasNewInputs, wrapperRef]);
63
+ }, [hasNewInputs, allRefs]);
61
64
  (0, _react.useEffect)(() => {
62
65
  const observer = new MutationObserver(updateFocusableElements);
63
66
  observer.observe(trapRef.current, {
@@ -95,20 +98,19 @@ const FocusTrap = ({
95
98
  ev.preventDefault();
96
99
  } else if (ev.shiftKey) {
97
100
  /* shift + tab */
98
- if (activeElement === firstElement || activeElement === wrapperRef.current) {
99
- lastElement.focus();
100
- ev.preventDefault();
101
- } // If current element is radio button -
102
- // find next non radio button element
103
-
101
+ let elementToFocus;
104
102
 
105
- if ((0, _focusTrapUtils.isRadio)(activeElement)) {
106
- const nextIndex = (0, _focusTrapUtils.nextNonRadioElementIndex)(activeElement, focusableElements);
107
- (0, _focusTrapUtils.setElementFocus)(focusableElements[nextIndex]);
108
- ev.preventDefault();
103
+ if (activeElement === wrapperRef.current) {
104
+ elementToFocus = (0, _focusTrapUtils.getNextElement)(firstElement, focusableElements, ev.shiftKey);
105
+ } else {
106
+ elementToFocus = (0, _focusTrapUtils.getNextElement)(activeElement, focusableElements, ev.shiftKey);
109
107
  }
110
- } else if (activeElement === lastElement) {
111
- firstElement.focus();
108
+
109
+ (0, _focusTrapUtils.setElementFocus)(elementToFocus);
110
+ ev.preventDefault();
111
+ } else {
112
+ const elementToFocus = (0, _focusTrapUtils.getNextElement)(activeElement, focusableElements, ev.shiftKey);
113
+ (0, _focusTrapUtils.setElementFocus)(elementToFocus);
112
114
  ev.preventDefault();
113
115
  }
114
116
  }
@@ -196,7 +198,15 @@ FocusTrap.propTypes = {
196
198
  }),
197
199
 
198
200
  /* whether the modal (etc.) component that the focus trap is inside is open or not */
199
- isOpen: _propTypes.default.bool
201
+ isOpen: _propTypes.default.bool,
202
+
203
+ /** an optional array of refs to containers whose content should also be reachable from the FocusTrap */
204
+ additionalWrapperRefs: _propTypes.default.arrayOf(_propTypes.default.shape({
205
+ current: _propTypes.default.any
206
+ }))
207
+ };
208
+ FocusTrap.defaultProps = {
209
+ additionalWrapperRefs: []
200
210
  };
201
211
  var _default = FocusTrap;
202
212
  exports.default = _default;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
 
3
- var _enzyme = _interopRequireDefault(require("enzyme"));
4
-
5
- var _enzymeAdapterReact = _interopRequireDefault(require("@wojtekmaj/enzyme-adapter-react-17"));
3
+ var _jestFetchMock = require("jest-fetch-mock");
6
4
 
7
5
  var _mockMatchMedia = require("./mock-match-media");
8
6
 
@@ -10,11 +8,6 @@ var _mockResizeObserver = _interopRequireDefault(require("./mock-resize-observer
10
8
 
11
9
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
10
 
13
- require("jest-fetch-mock").enableMocks();
14
-
11
+ (0, _jestFetchMock.enableFetchMocks)();
15
12
  (0, _mockResizeObserver.default)();
16
- (0, _mockMatchMedia.setup)();
17
-
18
- _enzyme.default.configure({
19
- adapter: new _enzymeAdapterReact.default()
20
- });
13
+ (0, _mockMatchMedia.setupMatchMediaMock)();
@@ -1,6 +1,6 @@
1
- declare function setup(): void;
1
+ declare function setupMatchMediaMock(): void;
2
2
  declare function mockMatchMedia(
3
3
  matches?: boolean
4
4
  ): { removeListener: jest.Mock };
5
5
 
6
- export { setup, mockMatchMedia };
6
+ export { setupMatchMediaMock, mockMatchMedia };
@@ -3,12 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.mockMatchMedia = exports.setup = void 0;
6
+ exports.mockMatchMedia = exports.setupMatchMediaMock = void 0;
7
7
  let mocked = false;
8
8
  let _matches = false;
9
9
  const removeListener = jest.fn();
10
10
 
11
- const setup = () => {
11
+ const setupMatchMediaMock = () => {
12
12
  if (!global.window) {
13
13
  return;
14
14
  }
@@ -29,11 +29,11 @@ const setup = () => {
29
29
  mocked = true;
30
30
  };
31
31
 
32
- exports.setup = setup;
32
+ exports.setupMatchMediaMock = setupMatchMediaMock;
33
33
 
34
34
  const mockMatchMedia = matches => {
35
35
  if (!mocked) {
36
- throw new Error("window.matchMedia has not been mocked. Did you call setup()?");
36
+ throw new Error("window.matchMedia has not been mocked. Did you call setupMatchMediaMock()?");
37
37
  }
38
38
 
39
39
  _matches = matches;
@@ -0,0 +1,2 @@
1
+ export default setupResizeObserverMock;
2
+ declare function setupResizeObserverMock(): void;
@@ -42,6 +42,15 @@ Alert.propTypes = {
42
42
  "disableEscKey": _propTypes.default.bool,
43
43
  "disableFocusTrap": _propTypes.default.bool,
44
44
  "enableBackgroundUI": _propTypes.default.bool,
45
+ "focusableContainers": _propTypes.default.arrayOf(_propTypes.default.shape({
46
+ "current": function (props, propName) {
47
+ if (props[propName] == null) {
48
+ return new Error("Prop '" + propName + "' is required but wasn't specified");
49
+ } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {
50
+ return new Error("Expected prop '" + propName + "' to be of type Element");
51
+ }
52
+ }
53
+ })),
45
54
  "focusFirstElement": _propTypes.default.shape({
46
55
  "current": function (props, propName) {
47
56
  if (props[propName] == null) {
@@ -55,6 +55,7 @@ const Dialog = ({
55
55
  help,
56
56
  role = "dialog",
57
57
  contentPadding = {},
58
+ focusableContainers,
58
59
  ...rest
59
60
  }) => {
60
61
  const locale = (0, _useLocale.default)();
@@ -180,7 +181,8 @@ const Dialog = ({
180
181
  focusFirstElement: focusFirstElement,
181
182
  bespokeTrap: bespokeFocusTrap,
182
183
  wrapperRef: dialogRef,
183
- isOpen: open
184
+ isOpen: open,
185
+ additionalWrapperRefs: focusableContainers
184
186
  }, /*#__PURE__*/_react.default.createElement(_dialog.DialogStyle, _extends({
185
187
  "aria-modal": true,
186
188
  ref: dialogRef,
@@ -271,7 +273,12 @@ Dialog.propTypes = {
271
273
  p: _propTypes.default.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8]),
272
274
  px: _propTypes.default.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8]),
273
275
  py: _propTypes.default.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8])
274
- })
276
+ }),
277
+
278
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the dialog */
279
+ focusableContainers: _propTypes.default.arrayOf(_propTypes.default.shape({
280
+ current: _propTypes.default.any
281
+ }))
275
282
  };
276
283
  Dialog.defaultProps = {
277
284
  size: "medium",
@@ -59,6 +59,8 @@ export interface DialogProps extends ModalProps {
59
59
  role?: string;
60
60
  /** Padding to be set on the Dialog content */
61
61
  contentPadding?: ContentPaddingInterface;
62
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the dialog */
63
+ focusableContainers?: React.MutableRefObject<HTMLElement>[];
62
64
  }
63
65
 
64
66
  declare function Dialog(props: DialogProps): JSX.Element;
@@ -56,6 +56,7 @@ const DialogFullScreen = ({
56
56
  contentRef,
57
57
  help,
58
58
  role = "dialog",
59
+ focusableContainers,
59
60
  ...rest
60
61
  }) => {
61
62
  const locale = (0, _useLocale.default)();
@@ -110,7 +111,8 @@ const DialogFullScreen = ({
110
111
  autoFocus: !disableAutoFocus,
111
112
  focusFirstElement: focusFirstElement,
112
113
  wrapperRef: dialogRef,
113
- isOpen: open
114
+ isOpen: open,
115
+ additionalWrapperRefs: focusableContainers
114
116
  }, /*#__PURE__*/_react.default.createElement(_dialogFullScreen.default, _extends({
115
117
  "aria-modal": role === "dialog" ? true : undefined
116
118
  }, ariaProps, {
@@ -194,7 +196,12 @@ DialogFullScreen.propTypes = {
194
196
  })]),
195
197
 
196
198
  /** The ARIA role to be applied to the DialogFullscreen container */
197
- role: _propTypes.default.string
199
+ role: _propTypes.default.string,
200
+
201
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the dialog */
202
+ focusableContainers: _propTypes.default.arrayOf(_propTypes.default.shape({
203
+ current: _propTypes.default.any
204
+ }))
198
205
  };
199
206
  var _default = DialogFullScreen;
200
207
  exports.default = _default;
@@ -41,6 +41,8 @@ export interface DialogFullScreenProps extends ModalProps {
41
41
  title?: React.ReactNode;
42
42
  /** The ARIA role to be applied to the DialogFullscreen container */
43
43
  role?: string;
44
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the dialog */
45
+ focusableContainers?: React.MutableRefObject<HTMLElement>[];
44
46
  }
45
47
 
46
48
  declare function DialogFullScreen(props: DialogFullScreenProps): JSX.Element;
@@ -54,6 +54,7 @@ const Sidebar = /*#__PURE__*/_react.default.forwardRef(({
54
54
  children,
55
55
  onCancel,
56
56
  role = "dialog",
57
+ focusableContainers,
57
58
  ...rest
58
59
  }, ref) => {
59
60
  const locale = (0, _useLocale.default)();
@@ -114,7 +115,8 @@ const Sidebar = /*#__PURE__*/_react.default.forwardRef(({
114
115
  className: "carbon-sidebar"
115
116
  }, componentTags), enableBackgroundUI ? sidebar : /*#__PURE__*/_react.default.createElement(_focusTrap.default, {
116
117
  wrapperRef: sidebarRef,
117
- isOpen: open
118
+ isOpen: open,
119
+ additionalWrapperRefs: focusableContainers
118
120
  }, sidebar));
119
121
  });
120
122
 
@@ -153,7 +155,12 @@ Sidebar.propTypes = {
153
155
  header: _propTypes.default.node,
154
156
 
155
157
  /** The ARIA role to be applied to the container */
156
- role: _propTypes.default.string
158
+ role: _propTypes.default.string,
159
+
160
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the sidebar */
161
+ focusableContainers: _propTypes.default.arrayOf(_propTypes.default.shape({
162
+ current: _propTypes.default.any
163
+ }))
157
164
  };
158
165
  Sidebar.defaultProps = {
159
166
  position: "right",
@@ -42,6 +42,8 @@ export interface SidebarProps {
42
42
  | "medium-large"
43
43
  | "large"
44
44
  | "extra-large";
45
+ /** an optional array of refs to containers whose content should also be reachable by tabbing from the sidebar */
46
+ focusableContainers?: React.MutableRefObject<HTMLElement>[];
45
47
  }
46
48
 
47
49
  declare const SidebarContext: React.Context<SidebarContextProps>;
@@ -35,7 +35,7 @@ function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj;
35
35
 
36
36
  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); }
37
37
 
38
- const Toast = ({
38
+ const Toast = /*#__PURE__*/_react.default.forwardRef(({
39
39
  children,
40
40
  className,
41
41
  id,
@@ -46,12 +46,16 @@ const Toast = ({
46
46
  targetPortalId,
47
47
  timeout,
48
48
  variant,
49
+ disableAutoFocus,
49
50
  ...restProps
50
- }) => {
51
+ }, ref) => {
51
52
  const locale = (0, _useLocale.default)();
52
53
  const toastRef = (0, _react.useRef)();
53
54
  const timer = (0, _react.useRef)();
54
55
  const toastContentNodeRef = (0, _react.useRef)();
56
+ const closeIconRef = (0, _react.useRef)();
57
+ const focusedElementBeforeOpening = (0, _react.useRef)();
58
+ const refToPass = ref || toastRef;
55
59
  const componentClasses = (0, _react.useMemo)(() => {
56
60
  return (0, _classnames.default)(className);
57
61
  }, [className]);
@@ -61,7 +65,7 @@ const Toast = ({
61
65
  onDismiss(ev);
62
66
  }
63
67
  }, [onDismiss]);
64
- (0, _useModalManager.default)(open, dismissToast, toastRef);
68
+ (0, _useModalManager.default)(open, dismissToast, refToPass);
65
69
  (0, _react.useEffect)(() => {
66
70
  clearTimeout(timer.current);
67
71
 
@@ -71,13 +75,34 @@ const Toast = ({
71
75
 
72
76
  timer.current = setTimeout(() => onDismiss(), timeout);
73
77
  }, [onDismiss, open, timeout]);
78
+ (0, _react.useEffect)(() => {
79
+ if (onDismiss && !disableAutoFocus) {
80
+ if (open) {
81
+ var _closeIconRef$current;
82
+
83
+ focusedElementBeforeOpening.current = document.activeElement;
84
+ (_closeIconRef$current = closeIconRef.current) === null || _closeIconRef$current === void 0 ? void 0 : _closeIconRef$current.focus();
85
+ } else if (focusedElementBeforeOpening.current) {
86
+ focusedElementBeforeOpening.current.focus();
87
+ focusedElementBeforeOpening.current = undefined;
88
+ }
89
+ }
90
+ }, [open, onDismiss, disableAutoFocus]);
91
+ (0, _react.useEffect)(() => {
92
+ return () => {
93
+ if (focusedElementBeforeOpening.current) {
94
+ focusedElementBeforeOpening.current.focus();
95
+ }
96
+ };
97
+ }, []);
74
98
 
75
99
  function renderCloseIcon() {
76
100
  if (!onDismiss) return null;
77
101
  return /*#__PURE__*/_react.default.createElement(_iconButton.default, {
78
102
  "aria-label": locale.toast.ariaLabels.close(),
79
103
  "data-element": "close",
80
- onAction: onDismiss
104
+ onAction: onDismiss,
105
+ ref: closeIconRef
81
106
  }, /*#__PURE__*/_react.default.createElement(_icon.default, {
82
107
  type: "close"
83
108
  }));
@@ -119,9 +144,9 @@ const Toast = ({
119
144
  isCenter: isCenter
120
145
  }, /*#__PURE__*/_react.default.createElement(_toast.ToastWrapper, {
121
146
  isCenter: isCenter,
122
- ref: toastRef
147
+ ref: refToPass
123
148
  }, /*#__PURE__*/_react.default.createElement(_reactTransitionGroup.TransitionGroup, null, renderToastContent())));
124
- };
149
+ });
125
150
 
126
151
  Toast.propTypes = {
127
152
  /** Customizes the appearance in the DLS theme */
@@ -155,7 +180,10 @@ Toast.propTypes = {
155
180
  targetPortalId: _propTypes.default.string,
156
181
 
157
182
  /** Maximum toast width */
158
- maxWidth: _propTypes.default.string
183
+ maxWidth: _propTypes.default.string,
184
+
185
+ /** Disables auto focus functionality when the Toast has a close icon */
186
+ disableAutoFocus: _propTypes.default.bool
159
187
  };
160
188
  var _default = Toast;
161
189
  exports.default = _default;
@@ -25,8 +25,12 @@ export interface ToastPropTypes {
25
25
  targetPortalId?: string;
26
26
  /** Maximum toast width */
27
27
  maxWidth?: string;
28
+ /** Disables auto focus functionality when the Toast has a close icon */
29
+ disableAutoFocus?: boolean;
28
30
  }
29
31
 
30
- declare class Toast extends React.Component<ToastPropTypes> {}
32
+ declare function Toast(
33
+ props: ToastPropTypes & React.RefAttributes<HTMLDivElement>
34
+ ): JSX.Element;
31
35
 
32
36
  export default Toast;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carbon-react",
3
- "version": "109.2.4",
3
+ "version": "109.3.0",
4
4
  "description": "A library of reusable React components for easily building user interfaces.",
5
5
  "engineStrict": true,
6
6
  "engines": {
@@ -16,11 +16,10 @@
16
16
  "scripts": {
17
17
  "start": "node ./scripts/check_node_version.js && start-storybook -p 9001 -c .storybook",
18
18
  "start:debug-theme": "cross-env STORYBOOK_DEBUG_THEME=true npm run start",
19
- "test": "jest --config=./jest.conf.json",
20
- "test-update": "jest --config=./jest.conf.json --updateSnapshot",
19
+ "test": "jest --config=./jest.config.json",
20
+ "test-update": "jest --config=./jest.config.json --updateSnapshot",
21
21
  "test:cypress": "npx cypress open --e2e",
22
22
  "cypress:react": "npx cypress open --component",
23
- "debug": "node --inspect ./node_modules/jest-cli/bin/jest --watch --config=./jest.conf.json",
24
23
  "format": "prettier --write './src'",
25
24
  "lint": "eslint ./src",
26
25
  "build": "node ./scripts/build.js",
@@ -1,14 +0,0 @@
1
- import * as React from "react";
2
-
3
- export interface FieldHelpProps {
4
- /** Child elements */
5
- children?: React.ReactNode;
6
- /** When true, label is placed in line an input */
7
- labelInline?: boolean;
8
- /** Width of a label in percentage. Works only when labelInline is true */
9
- labelWidth?: number;
10
- }
11
-
12
- declare function FieldHelp(props: FieldHelpProps): JSX.Element;
13
-
14
- export default FieldHelp;
@@ -1,14 +0,0 @@
1
- import * as React from "react";
2
-
3
- export interface FieldHelpProps {
4
- /** Child elements */
5
- children?: React.ReactNode;
6
- /** When true, label is placed in line an input */
7
- labelInline?: boolean;
8
- /** Width of a label in percentage. Works only when labelInline is true */
9
- labelWidth?: number;
10
- }
11
-
12
- declare function FieldHelp(props: FieldHelpProps): JSX.Element;
13
-
14
- export default FieldHelp;