@festo-ui/react 8.2.0-dev.627 → 8.2.0-dev.629
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/lib/components/modals/ModalBase.js +49 -1
- package/lib/components/modals/Prompt.js +2 -1
- package/lib/forms/text-input/TextInput.d.ts +1 -0
- package/lib/forms/text-input/TextInput.js +16 -1
- package/node/lib/components/modals/ModalBase.js +48 -0
- package/node/lib/components/modals/Prompt.js +2 -1
- package/node/lib/forms/text-input/TextInput.js +15 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
|
3
3
|
|
|
4
4
|
import classNames from "classnames";
|
|
5
|
-
import { forwardRef, useEffect, useRef } from "react";
|
|
5
|
+
import { forwardRef, useEffect, useRef, useCallback } from "react";
|
|
6
6
|
import ReactDOM from "react-dom";
|
|
7
7
|
import { CSSTransition } from "react-transition-group";
|
|
8
8
|
import { RemoveScroll } from "react-remove-scroll";
|
|
@@ -24,6 +24,8 @@ const ModalBase = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
24
24
|
const mouseDownOnBackdropActive = useRef(false);
|
|
25
25
|
const allRefs = useForkRef(ref, modalRef);
|
|
26
26
|
const container = modalRef?.current?.ownerDocument || document;
|
|
27
|
+
|
|
28
|
+
// Handle Escape key to close modal
|
|
27
29
|
useEffect(() => {
|
|
28
30
|
const handleKeyDown = event => {
|
|
29
31
|
if (event.key === "Escape" && isOpen) {
|
|
@@ -33,6 +35,52 @@ const ModalBase = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
33
35
|
container.addEventListener("keydown", handleKeyDown);
|
|
34
36
|
return () => container.removeEventListener("keydown", handleKeyDown);
|
|
35
37
|
}, [onClose, isOpen, container]);
|
|
38
|
+
|
|
39
|
+
// Focus trap implementation
|
|
40
|
+
const handleTabKey = useCallback(e => {
|
|
41
|
+
if (e.key === "Tab" && modalRef.current && isOpen) {
|
|
42
|
+
// Query for all focusable elements
|
|
43
|
+
const focusableElements = modalRef.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
44
|
+
const firstElement = focusableElements[0];
|
|
45
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
46
|
+
|
|
47
|
+
// If shift+tab and we're on the first element, move to the last
|
|
48
|
+
if (e.shiftKey && document.activeElement === firstElement) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
lastElement?.focus();
|
|
51
|
+
}
|
|
52
|
+
// If tab and we're on the last element, move to the first
|
|
53
|
+
else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
firstElement?.focus();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, [isOpen]);
|
|
59
|
+
|
|
60
|
+
// Set up the focus trap listener
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (isOpen) {
|
|
63
|
+
container.addEventListener("keydown", handleTabKey);
|
|
64
|
+
|
|
65
|
+
// Auto-focus first focusable element when modal opens
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
if (modalRef.current) {
|
|
68
|
+
const focusableElements = modalRef.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
69
|
+
|
|
70
|
+
// Focus the first input element, or the first focusable element if no input exists
|
|
71
|
+
const firstInput = Array.from(focusableElements).find(el => el.tagName.toLowerCase() === "input");
|
|
72
|
+
if (firstInput) {
|
|
73
|
+
firstInput.focus();
|
|
74
|
+
} else if (focusableElements.length > 0) {
|
|
75
|
+
focusableElements[0].focus();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}, 100);
|
|
79
|
+
}
|
|
80
|
+
return () => {
|
|
81
|
+
container.removeEventListener("keydown", handleTabKey);
|
|
82
|
+
};
|
|
83
|
+
}, [isOpen, handleTabKey, container]);
|
|
36
84
|
return /*#__PURE__*/_jsx(_Fragment, {
|
|
37
85
|
children: /*#__PURE__*/ReactDOM.createPortal(/*#__PURE__*/_jsxs(_Fragment, {
|
|
38
86
|
children: [/*#__PURE__*/_jsx(CSSTransition, {
|
|
@@ -17,6 +17,7 @@ interface TextInputProps {
|
|
|
17
17
|
error?: string;
|
|
18
18
|
labelClassName?: string;
|
|
19
19
|
icon?: React.ReactNode;
|
|
20
|
+
autoFocus?: boolean;
|
|
20
21
|
}
|
|
21
22
|
declare const TextInput: (props: TextInputProps & React.HTMLProps<HTMLInputElement> & React.RefAttributes<HTMLLabelElement>) => React.ReactElement<any, string | React.JSXElementConstructor<any>> | null;
|
|
22
23
|
export default TextInput;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, useEffect, useState } from "react";
|
|
1
|
+
import React, { forwardRef, useEffect, useState, useRef } from "react";
|
|
2
2
|
import classNames from "classnames";
|
|
3
3
|
import useId from "../../helper/useId.js";
|
|
4
4
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -22,13 +22,27 @@ const TextInput = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
22
22
|
labelClassName,
|
|
23
23
|
icon,
|
|
24
24
|
id: idProps,
|
|
25
|
+
autoFocus,
|
|
25
26
|
...props
|
|
26
27
|
} = _ref;
|
|
27
28
|
const id = useId(idProps);
|
|
28
29
|
const [innerValue, setInnerValue] = useState(value);
|
|
30
|
+
const inputRef = useRef(null);
|
|
29
31
|
useEffect(() => {
|
|
30
32
|
setInnerValue(value);
|
|
31
33
|
}, [value]);
|
|
34
|
+
|
|
35
|
+
// Programmatically focus the input instead of using autoFocus attribute
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (autoFocus && inputRef.current) {
|
|
38
|
+
// Short delay to ensure component is fully rendered
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
inputRef.current?.focus();
|
|
41
|
+
}, 0);
|
|
42
|
+
return () => clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}, [autoFocus]);
|
|
32
46
|
const supported = ["text", "number", "password", "datetime-local"];
|
|
33
47
|
const innerType = type && supported.indexOf(type) !== -1 ? type : "text";
|
|
34
48
|
const labelClasses = classNames("fwe-input-text", {
|
|
@@ -61,6 +75,7 @@ const TextInput = /*#__PURE__*/forwardRef((_ref, ref) => {
|
|
|
61
75
|
type: innerType,
|
|
62
76
|
value: innerValue,
|
|
63
77
|
id: id,
|
|
78
|
+
ref: inputRef,
|
|
64
79
|
...props
|
|
65
80
|
}), icon, /*#__PURE__*/_jsx("span", {
|
|
66
81
|
className: "fwe-input-text-label",
|
|
@@ -31,6 +31,8 @@ const ModalBase = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
|
|
|
31
31
|
const mouseDownOnBackdropActive = (0, _react.useRef)(false);
|
|
32
32
|
const allRefs = (0, _useForkRef.default)(ref, modalRef);
|
|
33
33
|
const container = modalRef?.current?.ownerDocument || document;
|
|
34
|
+
|
|
35
|
+
// Handle Escape key to close modal
|
|
34
36
|
(0, _react.useEffect)(() => {
|
|
35
37
|
const handleKeyDown = event => {
|
|
36
38
|
if (event.key === "Escape" && isOpen) {
|
|
@@ -40,6 +42,52 @@ const ModalBase = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
|
|
|
40
42
|
container.addEventListener("keydown", handleKeyDown);
|
|
41
43
|
return () => container.removeEventListener("keydown", handleKeyDown);
|
|
42
44
|
}, [onClose, isOpen, container]);
|
|
45
|
+
|
|
46
|
+
// Focus trap implementation
|
|
47
|
+
const handleTabKey = (0, _react.useCallback)(e => {
|
|
48
|
+
if (e.key === "Tab" && modalRef.current && isOpen) {
|
|
49
|
+
// Query for all focusable elements
|
|
50
|
+
const focusableElements = modalRef.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
51
|
+
const firstElement = focusableElements[0];
|
|
52
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
53
|
+
|
|
54
|
+
// If shift+tab and we're on the first element, move to the last
|
|
55
|
+
if (e.shiftKey && document.activeElement === firstElement) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
lastElement?.focus();
|
|
58
|
+
}
|
|
59
|
+
// If tab and we're on the last element, move to the first
|
|
60
|
+
else if (!e.shiftKey && document.activeElement === lastElement) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
firstElement?.focus();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [isOpen]);
|
|
66
|
+
|
|
67
|
+
// Set up the focus trap listener
|
|
68
|
+
(0, _react.useEffect)(() => {
|
|
69
|
+
if (isOpen) {
|
|
70
|
+
container.addEventListener("keydown", handleTabKey);
|
|
71
|
+
|
|
72
|
+
// Auto-focus first focusable element when modal opens
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
if (modalRef.current) {
|
|
75
|
+
const focusableElements = modalRef.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
76
|
+
|
|
77
|
+
// Focus the first input element, or the first focusable element if no input exists
|
|
78
|
+
const firstInput = Array.from(focusableElements).find(el => el.tagName.toLowerCase() === "input");
|
|
79
|
+
if (firstInput) {
|
|
80
|
+
firstInput.focus();
|
|
81
|
+
} else if (focusableElements.length > 0) {
|
|
82
|
+
focusableElements[0].focus();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}, 100);
|
|
86
|
+
}
|
|
87
|
+
return () => {
|
|
88
|
+
container.removeEventListener("keydown", handleTabKey);
|
|
89
|
+
};
|
|
90
|
+
}, [isOpen, handleTabKey, container]);
|
|
43
91
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {
|
|
44
92
|
children: /*#__PURE__*/_reactDom.default.createPortal(/*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
45
93
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactTransitionGroup.CSSTransition, {
|
|
@@ -31,13 +31,27 @@ const TextInput = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
|
|
|
31
31
|
labelClassName,
|
|
32
32
|
icon,
|
|
33
33
|
id: idProps,
|
|
34
|
+
autoFocus,
|
|
34
35
|
...props
|
|
35
36
|
} = _ref;
|
|
36
37
|
const id = (0, _useId.default)(idProps);
|
|
37
38
|
const [innerValue, setInnerValue] = (0, _react.useState)(value);
|
|
39
|
+
const inputRef = (0, _react.useRef)(null);
|
|
38
40
|
(0, _react.useEffect)(() => {
|
|
39
41
|
setInnerValue(value);
|
|
40
42
|
}, [value]);
|
|
43
|
+
|
|
44
|
+
// Programmatically focus the input instead of using autoFocus attribute
|
|
45
|
+
(0, _react.useEffect)(() => {
|
|
46
|
+
if (autoFocus && inputRef.current) {
|
|
47
|
+
// Short delay to ensure component is fully rendered
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
inputRef.current?.focus();
|
|
50
|
+
}, 0);
|
|
51
|
+
return () => clearTimeout(timer);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}, [autoFocus]);
|
|
41
55
|
const supported = ["text", "number", "password", "datetime-local"];
|
|
42
56
|
const innerType = type && supported.indexOf(type) !== -1 ? type : "text";
|
|
43
57
|
const labelClasses = (0, _classnames.default)("fwe-input-text", {
|
|
@@ -70,6 +84,7 @@ const TextInput = /*#__PURE__*/(0, _react.forwardRef)((_ref, ref) => {
|
|
|
70
84
|
type: innerType,
|
|
71
85
|
value: innerValue,
|
|
72
86
|
id: id,
|
|
87
|
+
ref: inputRef,
|
|
73
88
|
...props
|
|
74
89
|
}), icon, /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
|
|
75
90
|
className: "fwe-input-text-label",
|
package/package.json
CHANGED