@scm-manager/ui-components 2.29.2-20220021-101653 → 2.30.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/package.json +6 -5
- package/src/__snapshots__/storyshots.test.ts.snap +1370 -12
- package/src/buttons/Button.tsx +10 -3
- package/src/forms/PasswordConfirmation.tsx +55 -89
- package/src/modals/ConfirmAlert.stories.tsx +19 -4
- package/src/modals/ConfirmAlert.tsx +13 -3
- package/src/modals/CreateTagModal.tsx +15 -10
- package/src/modals/FullscreenModal.tsx +16 -4
- package/src/modals/Modal.stories.tsx +48 -16
- package/src/modals/Modal.tsx +39 -55
- package/src/useTrapFocus.ts +0 -163
package/src/buttons/Button.tsx
CHANGED
|
@@ -40,6 +40,7 @@ export type ButtonProps = {
|
|
|
40
40
|
reducedMobile?: boolean;
|
|
41
41
|
children?: ReactNode;
|
|
42
42
|
testId?: string;
|
|
43
|
+
ref?: React.ForwardedRef<HTMLButtonElement>;
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
type Props = ButtonProps & {
|
|
@@ -47,7 +48,11 @@ type Props = ButtonProps & {
|
|
|
47
48
|
color?: string;
|
|
48
49
|
};
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
type InnerProps = Props & {
|
|
52
|
+
innerRef: React.Ref<HTMLButtonElement>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const Button: FC<InnerProps> = ({
|
|
51
56
|
link,
|
|
52
57
|
className,
|
|
53
58
|
icon,
|
|
@@ -61,7 +66,8 @@ const Button: FC<Props> = ({
|
|
|
61
66
|
loading,
|
|
62
67
|
disabled,
|
|
63
68
|
action,
|
|
64
|
-
color = "default"
|
|
69
|
+
color = "default",
|
|
70
|
+
innerRef
|
|
65
71
|
}) => {
|
|
66
72
|
const renderIcon = () => {
|
|
67
73
|
return <>{icon ? <Icon name={icon} color="inherit" className="is-medium pr-1" /> : null}</>;
|
|
@@ -109,6 +115,7 @@ const Button: FC<Props> = ({
|
|
|
109
115
|
disabled={disabled}
|
|
110
116
|
onClick={event => action && action(event)}
|
|
111
117
|
className={classes}
|
|
118
|
+
ref={innerRef}
|
|
112
119
|
{...createAttributesForTesting(testId)}
|
|
113
120
|
>
|
|
114
121
|
{content}
|
|
@@ -116,4 +123,4 @@ const Button: FC<Props> = ({
|
|
|
116
123
|
);
|
|
117
124
|
};
|
|
118
125
|
|
|
119
|
-
export default Button;
|
|
126
|
+
export default React.forwardRef<HTMLButtonElement, Props>((props, ref) => <Button {...props} innerRef={ref} />);
|
|
@@ -21,112 +21,78 @@
|
|
|
21
21
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
|
-
import React from "react";
|
|
25
|
-
import {
|
|
24
|
+
import React, { FC, useEffect, useState } from "react";
|
|
25
|
+
import { useTranslation } from "react-i18next";
|
|
26
26
|
import InputField from "./InputField";
|
|
27
27
|
|
|
28
|
-
type
|
|
29
|
-
password: string;
|
|
30
|
-
confirmedPassword: string;
|
|
31
|
-
passwordValid: boolean;
|
|
32
|
-
passwordConfirmationFailed: boolean;
|
|
33
|
-
};
|
|
34
|
-
type Props = WithTranslation & {
|
|
28
|
+
type BaseProps = {
|
|
35
29
|
passwordChanged: (p1: string, p2: boolean) => void;
|
|
36
30
|
passwordValidator?: (p: string) => boolean;
|
|
37
31
|
onReturnPressed?: () => void;
|
|
38
32
|
};
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.state = {
|
|
44
|
-
password: "",
|
|
45
|
-
confirmedPassword: "",
|
|
46
|
-
passwordValid: true,
|
|
47
|
-
passwordConfirmationFailed: false,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
34
|
+
type InnerProps = BaseProps & {
|
|
35
|
+
innerRef: React.Ref<HTMLInputElement>;
|
|
36
|
+
};
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
38
|
+
const PasswordConfirmation: FC<InnerProps> = ({ passwordChanged, passwordValidator, onReturnPressed, innerRef }) => {
|
|
39
|
+
const [t] = useTranslation("commons");
|
|
40
|
+
const [password, setPassword] = useState("");
|
|
41
|
+
const [confirmedPassword, setConfirmedPassword] = useState("");
|
|
42
|
+
const [passwordValid, setPasswordValid] = useState(true);
|
|
43
|
+
const [passwordConfirmationFailed, setPasswordConfirmationFailed] = useState(false);
|
|
44
|
+
const isValid = passwordValid && !passwordConfirmationFailed;
|
|
59
45
|
|
|
60
|
-
|
|
61
|
-
const { t, onReturnPressed } = this.props;
|
|
62
|
-
return (
|
|
63
|
-
<div className="columns is-multiline">
|
|
64
|
-
<div className="column is-half">
|
|
65
|
-
<InputField
|
|
66
|
-
label={t("password.newPassword")}
|
|
67
|
-
type="password"
|
|
68
|
-
onChange={this.handlePasswordChange}
|
|
69
|
-
value={this.state.password ? this.state.password : ""}
|
|
70
|
-
validationError={!this.state.passwordValid}
|
|
71
|
-
errorMessage={t("password.passwordInvalid")}
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
<div className="column is-half">
|
|
75
|
-
<InputField
|
|
76
|
-
label={t("password.confirmPassword")}
|
|
77
|
-
type="password"
|
|
78
|
-
onChange={this.handlePasswordValidationChange}
|
|
79
|
-
value={this.state ? this.state.confirmedPassword : ""}
|
|
80
|
-
validationError={this.state.passwordConfirmationFailed}
|
|
81
|
-
errorMessage={t("password.passwordConfirmFailed")}
|
|
82
|
-
onReturnPressed={onReturnPressed}
|
|
83
|
-
/>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
46
|
+
useEffect(() => passwordChanged(password, isValid), [password, isValid]);
|
|
88
47
|
|
|
89
|
-
validatePassword = (
|
|
90
|
-
const { passwordValidator } = this.props;
|
|
48
|
+
const validatePassword = (newPassword: string) => {
|
|
91
49
|
if (passwordValidator) {
|
|
92
|
-
return passwordValidator(
|
|
50
|
+
return passwordValidator(newPassword);
|
|
93
51
|
}
|
|
94
52
|
|
|
95
|
-
return
|
|
53
|
+
return newPassword.length >= 6 && newPassword.length < 32;
|
|
96
54
|
};
|
|
97
55
|
|
|
98
|
-
handlePasswordValidationChange = (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.setState(
|
|
102
|
-
{
|
|
103
|
-
confirmedPassword,
|
|
104
|
-
passwordConfirmationFailed: !passwordConfirmed,
|
|
105
|
-
},
|
|
106
|
-
this.propagateChange
|
|
107
|
-
);
|
|
56
|
+
const handlePasswordValidationChange = (newConfirmedPassword: string) => {
|
|
57
|
+
setConfirmedPassword(newConfirmedPassword);
|
|
58
|
+
setPasswordConfirmationFailed(password !== newConfirmedPassword);
|
|
108
59
|
};
|
|
109
60
|
|
|
110
|
-
handlePasswordChange = (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
passwordValid: this.validatePassword(password),
|
|
116
|
-
passwordConfirmationFailed,
|
|
117
|
-
password: password,
|
|
118
|
-
},
|
|
119
|
-
this.propagateChange
|
|
120
|
-
);
|
|
61
|
+
const handlePasswordChange = (newPassword: string) => {
|
|
62
|
+
setPasswordConfirmationFailed(newPassword !== confirmedPassword);
|
|
63
|
+
setPassword(newPassword);
|
|
64
|
+
setPasswordValid(validatePassword(newPassword));
|
|
121
65
|
};
|
|
122
66
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
67
|
+
return (
|
|
68
|
+
<div className="columns is-multiline">
|
|
69
|
+
<div className="column is-half">
|
|
70
|
+
<InputField
|
|
71
|
+
label={t("password.newPassword")}
|
|
72
|
+
type="password"
|
|
73
|
+
onChange={event => handlePasswordChange(event.target.value)}
|
|
74
|
+
value={password}
|
|
75
|
+
validationError={!passwordValid}
|
|
76
|
+
errorMessage={t("password.passwordInvalid")}
|
|
77
|
+
ref={innerRef}
|
|
78
|
+
onReturnPressed={onReturnPressed}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="column is-half">
|
|
82
|
+
<InputField
|
|
83
|
+
label={t("password.confirmPassword")}
|
|
84
|
+
type="password"
|
|
85
|
+
onChange={handlePasswordValidationChange}
|
|
86
|
+
value={confirmedPassword}
|
|
87
|
+
validationError={passwordConfirmationFailed}
|
|
88
|
+
errorMessage={t("password.passwordConfirmFailed")}
|
|
89
|
+
onReturnPressed={onReturnPressed}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
131
95
|
|
|
132
|
-
export default
|
|
96
|
+
export default React.forwardRef<HTMLInputElement, BaseProps>((props, ref) => (
|
|
97
|
+
<PasswordConfirmation {...props} innerRef={ref} />
|
|
98
|
+
));
|
|
@@ -37,15 +37,27 @@ const buttons = [
|
|
|
37
37
|
{
|
|
38
38
|
className: "is-outlined",
|
|
39
39
|
label: "Cancel",
|
|
40
|
-
onClick: () => null
|
|
40
|
+
onClick: () => null
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
|
-
label: "Submit"
|
|
43
|
+
label: "Submit"
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const buttonsWithAutofocus = [
|
|
48
|
+
{
|
|
49
|
+
className: "is-outlined",
|
|
50
|
+
label: "Cancel",
|
|
51
|
+
onClick: () => null
|
|
44
52
|
},
|
|
53
|
+
{
|
|
54
|
+
label: "I should be focused",
|
|
55
|
+
autofocus: true
|
|
56
|
+
}
|
|
45
57
|
];
|
|
46
58
|
|
|
47
59
|
storiesOf("Modal/ConfirmAlert", module)
|
|
48
|
-
.addDecorator(
|
|
60
|
+
.addDecorator(story => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
|
|
49
61
|
.add("Default", () => <ConfirmAlert message={body} title={"Are you sure about that?"} buttons={buttons} />)
|
|
50
62
|
.add("WithButton", () => {
|
|
51
63
|
const buttonClick = () => {
|
|
@@ -57,4 +69,7 @@ storiesOf("Modal/ConfirmAlert", module)
|
|
|
57
69
|
<div id="modalRoot" />
|
|
58
70
|
</>
|
|
59
71
|
);
|
|
60
|
-
})
|
|
72
|
+
})
|
|
73
|
+
.add("Autofocus", () => (
|
|
74
|
+
<ConfirmAlert message={body} title={"Are you sure about that?"} buttons={buttonsWithAutofocus} />
|
|
75
|
+
));
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
import * as React from "react";
|
|
25
|
-
import { FC, useState } from "react";
|
|
25
|
+
import { FC, useRef, useState } from "react";
|
|
26
26
|
import ReactDOM from "react-dom";
|
|
27
27
|
import Modal from "./Modal";
|
|
28
28
|
import classNames from "classnames";
|
|
@@ -32,6 +32,7 @@ type Button = {
|
|
|
32
32
|
label: string;
|
|
33
33
|
isLoading?: boolean;
|
|
34
34
|
onClick?: () => void | null;
|
|
35
|
+
autofocus?: boolean;
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
type Props = {
|
|
@@ -43,6 +44,7 @@ type Props = {
|
|
|
43
44
|
|
|
44
45
|
export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
|
|
45
46
|
const [showModal, setShowModal] = useState(true);
|
|
47
|
+
const initialFocusButton = useRef<HTMLButtonElement>(null);
|
|
46
48
|
|
|
47
49
|
const onClose = () => {
|
|
48
50
|
if (typeof close === "function") {
|
|
@@ -69,8 +71,9 @@ export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
|
|
|
69
71
|
className={classNames("button", "is-info", button.className, button.isLoading ? "is-loading" : "")}
|
|
70
72
|
key={index}
|
|
71
73
|
onClick={() => handleClickButton(button)}
|
|
72
|
-
onKeyDown={
|
|
74
|
+
onKeyDown={e => e.key === "Enter" && handleClickButton(button)}
|
|
73
75
|
tabIndex={0}
|
|
76
|
+
ref={button.autofocus ? initialFocusButton : undefined}
|
|
74
77
|
>
|
|
75
78
|
{button.label}
|
|
76
79
|
</button>
|
|
@@ -80,7 +83,14 @@ export const ConfirmAlert: FC<Props> = ({ title, message, buttons, close }) => {
|
|
|
80
83
|
);
|
|
81
84
|
|
|
82
85
|
return (
|
|
83
|
-
|
|
86
|
+
<Modal
|
|
87
|
+
title={title}
|
|
88
|
+
closeFunction={onClose}
|
|
89
|
+
body={body}
|
|
90
|
+
active={showModal}
|
|
91
|
+
footer={footer}
|
|
92
|
+
initialFocusRef={initialFocusButton}
|
|
93
|
+
/>
|
|
84
94
|
);
|
|
85
95
|
};
|
|
86
96
|
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import React, { FC, useEffect, useState } from "react";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import React, { FC, useEffect, useRef, useState } from "react";
|
|
26
|
+
import { apiClient, Button, InputField, Modal } from "@scm-manager/ui-components";
|
|
27
|
+
import { useTranslation } from "react-i18next";
|
|
28
28
|
import { Tag } from "@scm-manager/ui-types";
|
|
29
29
|
import { isBranchValid } from "../validation";
|
|
30
30
|
|
|
31
|
-
type Props =
|
|
31
|
+
type Props = {
|
|
32
32
|
existingTagsLink: string;
|
|
33
33
|
tagCreationLink: string;
|
|
34
34
|
onClose: () => void;
|
|
@@ -40,16 +40,18 @@ type Props = WithTranslation & {
|
|
|
40
40
|
/**
|
|
41
41
|
* @deprecated
|
|
42
42
|
*/
|
|
43
|
-
const CreateTagModal: FC<Props> = ({
|
|
43
|
+
const CreateTagModal: FC<Props> = ({ onClose, tagCreationLink, existingTagsLink, onCreated, onError, revision }) => {
|
|
44
|
+
const [t] = useTranslation("repos");
|
|
44
45
|
const [newTagName, setNewTagName] = useState("");
|
|
45
46
|
const [loading, setLoading] = useState(false);
|
|
46
47
|
const [tagNames, setTagNames] = useState<string[]>([]);
|
|
48
|
+
const initialFocusRef = useRef<HTMLInputElement>(null);
|
|
47
49
|
|
|
48
50
|
useEffect(() => {
|
|
49
51
|
apiClient
|
|
50
52
|
.get(existingTagsLink)
|
|
51
|
-
.then(
|
|
52
|
-
.then(
|
|
53
|
+
.then(response => response.json())
|
|
54
|
+
.then(json => setTagNames(json._embedded.tags.map((tag: Tag) => tag.name)));
|
|
53
55
|
}, [existingTagsLink]);
|
|
54
56
|
|
|
55
57
|
const createTag = () => {
|
|
@@ -57,7 +59,7 @@ const CreateTagModal: FC<Props> = ({ t, onClose, tagCreationLink, existingTagsLi
|
|
|
57
59
|
apiClient
|
|
58
60
|
.post(tagCreationLink, {
|
|
59
61
|
revision,
|
|
60
|
-
name: newTagName
|
|
62
|
+
name: newTagName
|
|
61
63
|
})
|
|
62
64
|
.then(onCreated)
|
|
63
65
|
.catch(onError)
|
|
@@ -83,10 +85,12 @@ const CreateTagModal: FC<Props> = ({ t, onClose, tagCreationLink, existingTagsLi
|
|
|
83
85
|
<InputField
|
|
84
86
|
name="name"
|
|
85
87
|
label={t("tags.create.form.field.name.label")}
|
|
86
|
-
onChange={
|
|
88
|
+
onChange={e => setNewTagName(e.target.value)}
|
|
87
89
|
value={newTagName}
|
|
88
90
|
validationError={!!validationError}
|
|
89
91
|
errorMessage={t(validationError)}
|
|
92
|
+
onReturnPressed={() => !validationError && newTagName.length > 0 && createTag()}
|
|
93
|
+
ref={initialFocusRef}
|
|
90
94
|
/>
|
|
91
95
|
<div className="mt-6">{t("tags.create.hint")}</div>
|
|
92
96
|
</>
|
|
@@ -105,8 +109,9 @@ const CreateTagModal: FC<Props> = ({ t, onClose, tagCreationLink, existingTagsLi
|
|
|
105
109
|
</>
|
|
106
110
|
}
|
|
107
111
|
closeFunction={onClose}
|
|
112
|
+
initialFocusRef={initialFocusRef}
|
|
108
113
|
/>
|
|
109
114
|
);
|
|
110
115
|
};
|
|
111
116
|
|
|
112
|
-
export default
|
|
117
|
+
export default CreateTagModal;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
import * as React from "react";
|
|
25
|
-
import { FC, ReactNode } from "react";
|
|
25
|
+
import { FC, MutableRefObject, ReactNode } from "react";
|
|
26
26
|
import { useTranslation } from "react-i18next";
|
|
27
27
|
import { Modal } from "./Modal";
|
|
28
28
|
import Button from "../buttons/Button";
|
|
@@ -34,6 +34,7 @@ type Props = {
|
|
|
34
34
|
body: ReactNode;
|
|
35
35
|
active: boolean;
|
|
36
36
|
closeButtonLabel?: string;
|
|
37
|
+
initialFocusRef?: MutableRefObject<HTMLElement | null>;
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
const FullSizedModal = styled(Modal)`
|
|
@@ -43,11 +44,22 @@ const FullSizedModal = styled(Modal)`
|
|
|
43
44
|
}
|
|
44
45
|
`;
|
|
45
46
|
|
|
46
|
-
const FullscreenModal: FC<Props> = ({ title, closeFunction, body, active, closeButtonLabel }) => {
|
|
47
|
+
const FullscreenModal: FC<Props> = ({ title, closeFunction, body, active, initialFocusRef, closeButtonLabel }) => {
|
|
47
48
|
const [t] = useTranslation("repos");
|
|
48
|
-
const footer =
|
|
49
|
+
const footer = (
|
|
50
|
+
<Button label={closeButtonLabel || t("diff.fullscreen.close")} action={closeFunction} color="secondary" />
|
|
51
|
+
);
|
|
49
52
|
|
|
50
|
-
return
|
|
53
|
+
return (
|
|
54
|
+
<FullSizedModal
|
|
55
|
+
title={title}
|
|
56
|
+
closeFunction={closeFunction}
|
|
57
|
+
body={body}
|
|
58
|
+
footer={footer}
|
|
59
|
+
active={active}
|
|
60
|
+
initialFocusRef={initialFocusRef}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
51
63
|
};
|
|
52
64
|
|
|
53
65
|
export default FullscreenModal;
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
|
|
25
25
|
import { storiesOf } from "@storybook/react";
|
|
26
26
|
import { MemoryRouter } from "react-router-dom";
|
|
27
|
-
import React, {
|
|
27
|
+
import React, { FC, useRef, useState } from "react";
|
|
28
28
|
import Modal from "./Modal";
|
|
29
29
|
import Checkbox from "../forms/Checkbox";
|
|
30
30
|
import styled from "styled-components";
|
|
31
31
|
import ExternalLink from "../navigation/ExternalLink";
|
|
32
|
-
import { Radio, Textarea
|
|
33
|
-
import {
|
|
32
|
+
import { InputField, Radio, Textarea } from "../forms";
|
|
33
|
+
import { Button, ButtonGroup } from "../buttons";
|
|
34
34
|
import Notification from "../Notification";
|
|
35
35
|
import { Autocomplete } from "../index";
|
|
36
36
|
import { SelectValue } from "@scm-manager/ui-types";
|
|
@@ -52,9 +52,8 @@ const text = `Mind-paralyzing change needed improbability vortex machine sorts s
|
|
|
52
52
|
Kakrafoon humanoid undergarment ship powered by GPP-guided bowl of petunias nothing was frequently away incredibly
|
|
53
53
|
ordinary mob.`;
|
|
54
54
|
|
|
55
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
56
55
|
const doNothing = () => {
|
|
57
|
-
// nothing
|
|
56
|
+
// Do nothing
|
|
58
57
|
};
|
|
59
58
|
const withFormElementsBody = (
|
|
60
59
|
<>
|
|
@@ -114,6 +113,21 @@ storiesOf("Modal/Modal", module)
|
|
|
114
113
|
footer={withFormElementsFooter}
|
|
115
114
|
/>
|
|
116
115
|
))
|
|
116
|
+
.add("With initial input field focus", () => {
|
|
117
|
+
const ref = useRef<HTMLInputElement | null>(null);
|
|
118
|
+
return (
|
|
119
|
+
<Modal
|
|
120
|
+
closeFunction={doNothing}
|
|
121
|
+
active={true}
|
|
122
|
+
title={"Hitchhiker Modal"}
|
|
123
|
+
footer={withFormElementsFooter}
|
|
124
|
+
initialFocusRef={ref}
|
|
125
|
+
>
|
|
126
|
+
<InputField ref={ref} />
|
|
127
|
+
</Modal>
|
|
128
|
+
);
|
|
129
|
+
})
|
|
130
|
+
.add("With initial button focus", () => <RefModal />)
|
|
117
131
|
.add("With long tooltips", () => {
|
|
118
132
|
return (
|
|
119
133
|
<NonCloseableModal>
|
|
@@ -285,9 +299,20 @@ const NestedModal: FC = ({ children }) => {
|
|
|
285
299
|
const [showOuter, setShowOuter] = useState(true);
|
|
286
300
|
const [showInner, setShowInner] = useState(false);
|
|
287
301
|
const outerBody = (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
302
|
+
<>
|
|
303
|
+
{showInner && (
|
|
304
|
+
<Modal
|
|
305
|
+
body={children}
|
|
306
|
+
closeFunction={() => setShowInner(!showInner)}
|
|
307
|
+
active={showInner}
|
|
308
|
+
title="Inner Hitchhiker Modal"
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
<Button title="Open inner modal" className="button" action={() => setShowInner(true)}>
|
|
313
|
+
Open inner modal
|
|
314
|
+
</Button>
|
|
315
|
+
</>
|
|
291
316
|
);
|
|
292
317
|
|
|
293
318
|
return (
|
|
@@ -301,14 +326,21 @@ const NestedModal: FC = ({ children }) => {
|
|
|
301
326
|
size="M"
|
|
302
327
|
/>
|
|
303
328
|
)}
|
|
304
|
-
{showInner && (
|
|
305
|
-
<Modal
|
|
306
|
-
body={children}
|
|
307
|
-
closeFunction={() => setShowInner(!showInner)}
|
|
308
|
-
active={showInner}
|
|
309
|
-
title="Inner Hitchhiker Modal"
|
|
310
|
-
/>
|
|
311
|
-
)}
|
|
312
329
|
</>
|
|
313
330
|
);
|
|
314
331
|
};
|
|
332
|
+
|
|
333
|
+
const RefModal = () => {
|
|
334
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
335
|
+
return (
|
|
336
|
+
<Modal
|
|
337
|
+
closeFunction={doNothing}
|
|
338
|
+
active={true}
|
|
339
|
+
title={"Hitchhiker Modal"}
|
|
340
|
+
footer={withFormElementsFooter}
|
|
341
|
+
initialFocusRef={ref}
|
|
342
|
+
>
|
|
343
|
+
<button ref={ref}>Hello</button>
|
|
344
|
+
</Modal>
|
|
345
|
+
);
|
|
346
|
+
};
|
package/src/modals/Modal.tsx
CHANGED
|
@@ -21,12 +21,10 @@
|
|
|
21
21
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
|
-
import React, { FC,
|
|
24
|
+
import React, { FC, MutableRefObject, useRef } from "react";
|
|
25
25
|
import classNames from "classnames";
|
|
26
|
-
import usePortalRootElement from "../usePortalRootElement";
|
|
27
|
-
import ReactDOM from "react-dom";
|
|
28
26
|
import styled from "styled-components";
|
|
29
|
-
import {
|
|
27
|
+
import { Dialog } from "@headlessui/react";
|
|
30
28
|
|
|
31
29
|
type ModalSize = "S" | "M" | "L";
|
|
32
30
|
|
|
@@ -35,13 +33,14 @@ const modalSizes: { [key in ModalSize]: number } = { S: 33, M: 50, L: 66 };
|
|
|
35
33
|
type Props = {
|
|
36
34
|
title: string;
|
|
37
35
|
closeFunction: () => void;
|
|
38
|
-
body
|
|
36
|
+
body?: any;
|
|
39
37
|
footer?: any;
|
|
40
38
|
active: boolean;
|
|
41
39
|
className?: string;
|
|
42
40
|
headColor?: string;
|
|
43
41
|
headTextColor?: string;
|
|
44
42
|
size?: ModalSize;
|
|
43
|
+
initialFocusRef?: MutableRefObject<HTMLElement | null>;
|
|
45
44
|
overflowVisible?: boolean;
|
|
46
45
|
};
|
|
47
46
|
|
|
@@ -50,82 +49,67 @@ const SizedModal = styled.div<{ size?: ModalSize; overflow: string }>`
|
|
|
50
49
|
overflow: ${props => props.overflow};
|
|
51
50
|
`;
|
|
52
51
|
|
|
53
|
-
const DivWithOptionalOverflow = styled.div<{ overflow: string; borderBottomRadius: string }>`
|
|
54
|
-
overflow: ${props => props.overflow};
|
|
55
|
-
border-bottom-left-radius: ${props => props.borderBottomRadius};
|
|
56
|
-
border-bottom-right-radius: ${props => props.borderBottomRadius};
|
|
57
|
-
`;
|
|
58
|
-
|
|
59
|
-
const SectionWithOptionalOverflow = styled.section<{ overflow: string; borderBottomRadius: string }>`
|
|
60
|
-
overflow: ${props => props.overflow};
|
|
61
|
-
border-bottom-left-radius: ${props => props.borderBottomRadius};
|
|
62
|
-
border-bottom-right-radius: ${props => props.borderBottomRadius};
|
|
63
|
-
`;
|
|
64
|
-
|
|
65
52
|
export const Modal: FC<Props> = ({
|
|
66
53
|
title,
|
|
67
54
|
closeFunction,
|
|
68
55
|
body,
|
|
56
|
+
children,
|
|
69
57
|
footer,
|
|
70
58
|
active,
|
|
71
59
|
className,
|
|
72
60
|
headColor = "secondary-less",
|
|
73
61
|
headTextColor = "secondary-most",
|
|
74
62
|
size,
|
|
63
|
+
initialFocusRef,
|
|
75
64
|
overflowVisible
|
|
76
65
|
}) => {
|
|
77
|
-
const
|
|
78
|
-
const initialFocusRef = useRef(null);
|
|
79
|
-
const trapRef = useTrapFocus({
|
|
80
|
-
includeContainer: true,
|
|
81
|
-
initialFocus: initialFocusRef.current,
|
|
82
|
-
returnFocus: true,
|
|
83
|
-
updateNodes: false
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (!portalRootElement) {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const isActive = active ? "is-active" : null;
|
|
91
|
-
|
|
66
|
+
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
92
67
|
let showFooter = null;
|
|
68
|
+
|
|
93
69
|
if (footer) {
|
|
94
70
|
showFooter = <footer className="modal-card-foot">{footer}</footer>;
|
|
95
71
|
}
|
|
96
72
|
|
|
97
|
-
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
98
|
-
if (closeFunction && "Escape" === event.key) {
|
|
99
|
-
closeFunction();
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
73
|
const overflowAttribute = overflowVisible ? "visible" : "auto";
|
|
104
74
|
const borderBottomRadiusAttribute = overflowVisible && !footer ? "inherit" : "unset";
|
|
105
75
|
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
76
|
+
return (
|
|
77
|
+
<Dialog
|
|
78
|
+
open={active}
|
|
79
|
+
onClose={closeFunction}
|
|
80
|
+
className={classNames(
|
|
81
|
+
"modal",
|
|
82
|
+
{ "is-active": active },
|
|
83
|
+
`is-overflow-${overflowAttribute}`,
|
|
84
|
+
`is-border-bottom-radius-${borderBottomRadiusAttribute}`,
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
initialFocus={initialFocusRef ?? closeButtonRef}
|
|
113
88
|
>
|
|
114
|
-
<
|
|
89
|
+
<Dialog.Overlay className="modal-background" />
|
|
115
90
|
<SizedModal className="modal-card" size={size} overflow={overflowAttribute}>
|
|
116
|
-
<header className={classNames("modal-card-head", `has-background-${headColor}`)}>
|
|
91
|
+
<Dialog.Title as="header" className={classNames("modal-card-head", `has-background-${headColor}`)}>
|
|
117
92
|
<h2 className={`modal-card-title m-0 has-text-${headTextColor}`}>{title}</h2>
|
|
118
|
-
<button
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
93
|
+
<button
|
|
94
|
+
className="delete"
|
|
95
|
+
aria-label="close"
|
|
96
|
+
onClick={closeFunction}
|
|
97
|
+
ref={!initialFocusRef ? closeButtonRef : undefined}
|
|
98
|
+
/>
|
|
99
|
+
</Dialog.Title>
|
|
100
|
+
<section
|
|
101
|
+
className={classNames(
|
|
102
|
+
"modal-card-body",
|
|
103
|
+
`is-overflow-${overflowAttribute}`,
|
|
104
|
+
`is-border-bottom-radius-${borderBottomRadiusAttribute}`
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{body || children}
|
|
108
|
+
</section>
|
|
123
109
|
{showFooter}
|
|
124
110
|
</SizedModal>
|
|
125
|
-
</
|
|
111
|
+
</Dialog>
|
|
126
112
|
);
|
|
127
|
-
|
|
128
|
-
return ReactDOM.createPortal(modalElement, portalRootElement);
|
|
129
113
|
};
|
|
130
114
|
|
|
131
115
|
export default Modal;
|