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.
- package/esm/__internal__/field-help/field-help.component.d.ts +10 -0
- package/esm/__internal__/field-help/field-help.component.js +12 -16
- package/esm/__internal__/field-help/field-help.style.d.ts +8 -0
- package/esm/__internal__/field-help/field-help.style.js +2 -10
- package/esm/__internal__/field-help/index.d.ts +2 -1
- package/esm/__internal__/focus-trap/focus-trap-utils.d.ts +1 -2
- package/esm/__internal__/focus-trap/focus-trap-utils.js +57 -8
- package/esm/__internal__/focus-trap/focus-trap.component.js +35 -25
- package/esm/__spec_helper__/index.d.ts +1 -0
- package/esm/__spec_helper__/index.js +4 -10
- package/esm/__spec_helper__/mock-match-media.d.ts +2 -2
- package/esm/__spec_helper__/mock-match-media.js +2 -2
- package/esm/__spec_helper__/mock-resize-observer.d.ts +2 -0
- package/esm/components/alert/alert.component.js +9 -0
- package/esm/components/dialog/dialog.component.js +9 -2
- package/esm/components/dialog/dialog.d.ts +2 -0
- package/esm/components/dialog-full-screen/dialog-full-screen.component.js +9 -2
- package/esm/components/dialog-full-screen/dialog-full-screen.d.ts +2 -0
- package/esm/components/sidebar/sidebar.component.js +9 -2
- package/esm/components/sidebar/sidebar.d.ts +2 -0
- package/esm/components/toast/toast.component.js +35 -9
- package/esm/components/toast/toast.d.ts +5 -1
- package/lib/__internal__/field-help/field-help.component.d.ts +10 -0
- package/lib/__internal__/field-help/field-help.component.js +12 -16
- package/lib/__internal__/field-help/field-help.style.d.ts +8 -0
- package/lib/__internal__/field-help/field-help.style.js +2 -13
- package/lib/__internal__/field-help/index.d.ts +2 -1
- package/lib/__internal__/focus-trap/focus-trap-utils.d.ts +1 -2
- package/lib/__internal__/focus-trap/focus-trap-utils.js +57 -10
- package/lib/__internal__/focus-trap/focus-trap.component.js +34 -24
- package/lib/__spec_helper__/index.d.ts +1 -0
- package/lib/__spec_helper__/index.js +3 -10
- package/lib/__spec_helper__/mock-match-media.d.ts +2 -2
- package/lib/__spec_helper__/mock-match-media.js +4 -4
- package/lib/__spec_helper__/mock-resize-observer.d.ts +2 -0
- package/lib/components/alert/alert.component.js +9 -0
- package/lib/components/dialog/dialog.component.js +9 -2
- package/lib/components/dialog/dialog.d.ts +2 -0
- package/lib/components/dialog-full-screen/dialog-full-screen.component.js +9 -2
- package/lib/components/dialog-full-screen/dialog-full-screen.d.ts +2 -0
- package/lib/components/sidebar/sidebar.component.js +9 -2
- package/lib/components/sidebar/sidebar.d.ts +2 -0
- package/lib/components/toast/toast.component.js +35 -7
- package/lib/components/toast/toast.d.ts +5 -1
- package/package.json +3 -4
- package/esm/__internal__/field-help/field-help.d.ts +0 -14
- 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
|
|
3
|
+
import StyledFieldHelp from "./field-help.style";
|
|
6
4
|
|
|
7
5
|
const FieldHelp = ({
|
|
8
6
|
children,
|
|
9
7
|
labelInline,
|
|
10
|
-
labelWidth,
|
|
11
|
-
|
|
12
|
-
}) => /*#__PURE__*/React.createElement(
|
|
8
|
+
labelWidth = 30,
|
|
9
|
+
id
|
|
10
|
+
}) => /*#__PURE__*/React.createElement(StyledFieldHelp, {
|
|
13
11
|
"data-element": "help",
|
|
14
12
|
labelInline: labelInline,
|
|
15
|
-
labelWidth: labelWidth
|
|
16
|
-
|
|
13
|
+
labelWidth: labelWidth,
|
|
14
|
+
id: id
|
|
15
|
+
}, children);
|
|
17
16
|
|
|
18
17
|
FieldHelp.propTypes = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
99
|
+
return foundElement;
|
|
51
100
|
};
|
|
52
101
|
|
|
53
|
-
export { defaultFocusableSelectors,
|
|
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,
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
ev.
|
|
86
|
+
if (activeElement === wrapperRef.current) {
|
|
87
|
+
elementToFocus = getNextElement(firstElement, focusableElements, ev.shiftKey);
|
|
88
|
+
} else {
|
|
89
|
+
elementToFocus = getNextElement(activeElement, focusableElements, ev.shiftKey);
|
|
92
90
|
}
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
2
|
-
import
|
|
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
|
-
|
|
10
|
-
Enzyme.configure({
|
|
11
|
-
adapter: new Adapter()
|
|
12
|
-
});
|
|
6
|
+
setupMatchMediaMock();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
let mocked = false;
|
|
2
2
|
let _matches = false;
|
|
3
3
|
const removeListener = jest.fn();
|
|
4
|
-
export const
|
|
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
|
|
26
|
+
throw new Error("window.matchMedia has not been mocked. Did you call setupMatchMediaMock()?");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
_matches = matches;
|
|
@@ -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,
|
|
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:
|
|
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
|
|
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
|
-
|
|
23
|
-
}) => /*#__PURE__*/_react.default.createElement(_fieldHelp.default,
|
|
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
|
-
|
|
24
|
+
labelWidth: labelWidth,
|
|
25
|
+
id: id
|
|
26
|
+
}, children);
|
|
28
27
|
|
|
28
|
+
exports.FieldHelp = FieldHelp;
|
|
29
29
|
FieldHelp.propTypes = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
107
|
+
return foundElement;
|
|
61
108
|
};
|
|
62
109
|
|
|
63
|
-
exports.
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
ev.
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
11
|
+
(0, _jestFetchMock.enableFetchMocks)();
|
|
15
12
|
(0, _mockResizeObserver.default)();
|
|
16
|
-
(0, _mockMatchMedia.
|
|
17
|
-
|
|
18
|
-
_enzyme.default.configure({
|
|
19
|
-
adapter: new _enzymeAdapterReact.default()
|
|
20
|
-
});
|
|
13
|
+
(0, _mockMatchMedia.setupMatchMediaMock)();
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.mockMatchMedia = exports.
|
|
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
|
|
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.
|
|
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
|
|
36
|
+
throw new Error("window.matchMedia has not been mocked. Did you call setupMatchMediaMock()?");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
_matches = matches;
|
|
@@ -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,
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
20
|
-
"test-update": "jest --config=./jest.
|
|
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;
|