@onewelcome/react-lib-components 5.1.0 → 5.2.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/dist/cjs/Button/Button.module.cjs.js +1 -1
- package/dist/cjs/Button/IconButton.module.cjs.js +1 -1
- package/dist/cjs/DataGrid/DataGridActions/DataGridColumnsToggle.cjs.js +1 -1
- package/dist/cjs/DataGrid/DataGridActions/DataGridColumnsToggle.cjs.js.map +1 -1
- package/dist/cjs/Form/Radio/Radio.cjs.js.map +1 -1
- package/dist/cjs/Form/Select/Option.cjs.js +1 -1
- package/dist/cjs/Form/Select/Option.cjs.js.map +1 -1
- package/dist/cjs/Link/Link.module.cjs.js +1 -1
- package/dist/cjs/Notifications/BaseModal/BaseModal.cjs.js +1 -1
- package/dist/cjs/Notifications/BaseModal/BaseModal.cjs.js.map +1 -1
- package/dist/cjs/Notifications/BaseModal/useRepeatFocus.cjs.js +2 -0
- package/dist/cjs/Notifications/BaseModal/useRepeatFocus.cjs.js.map +1 -0
- package/dist/cjs/Stepper/Step.cjs.js +1 -1
- package/dist/cjs/Stepper/Step.cjs.js.map +1 -1
- package/dist/cjs/Stepper/Step.module.cjs.js +1 -1
- package/dist/cjs/Stepper/Stepper.cjs.js +1 -1
- package/dist/cjs/Stepper/Stepper.cjs.js.map +1 -1
- package/dist/cjs/Stepper/Stepper.module.cjs.js +1 -1
- package/dist/cjs/Tooltip/Tooltip.cjs.js +1 -1
- package/dist/cjs/Tooltip/Tooltip.cjs.js.map +1 -1
- package/dist/cjs/Wizard/Wizard.cjs.js.map +1 -1
- package/dist/cjs/Wizard/WizardActions/WizardActions.cjs.js.map +1 -1
- package/dist/cjs/Wizard/WizardStateProvider.cjs.js.map +1 -1
- package/dist/cjs/Wizard/WizardSteps/WizardSteps.cjs.js.map +1 -1
- package/dist/cjs/src/components/Notifications/BaseModal/useRepeatFocus.d.ts +7 -0
- package/dist/cjs/src/components/Stepper/Step.d.ts +5 -3
- package/dist/cjs/src/components/Stepper/Stepper.d.ts +3 -1
- package/dist/cjs/src/components/Wizard/WizardActions/WizardActions.d.ts +3 -0
- package/dist/cjs/src/components/Wizard/WizardStateProvider.d.ts +3 -0
- package/dist/cjs/src/components/Wizard/WizardSteps/WizardSteps.d.ts +3 -0
- package/dist/cjs/src/index.d.ts +1 -0
- package/dist/esm/Button/Button.module.esm.js +1 -1
- package/dist/esm/Button/IconButton.module.esm.js +1 -1
- package/dist/esm/DataGrid/DataGridActions/DataGridColumnsToggle.esm.js +1 -1
- package/dist/esm/DataGrid/DataGridActions/DataGridColumnsToggle.esm.js.map +1 -1
- package/dist/esm/Form/Radio/Radio.esm.js.map +1 -1
- package/dist/esm/Form/Select/Option.esm.js +1 -1
- package/dist/esm/Form/Select/Option.esm.js.map +1 -1
- package/dist/esm/Link/Link.module.esm.js +1 -1
- package/dist/esm/Notifications/BaseModal/BaseModal.esm.js +1 -1
- package/dist/esm/Notifications/BaseModal/BaseModal.esm.js.map +1 -1
- package/dist/esm/Notifications/BaseModal/useRepeatFocus.esm.js +2 -0
- package/dist/esm/Notifications/BaseModal/useRepeatFocus.esm.js.map +1 -0
- package/dist/esm/Stepper/Step.esm.js +1 -1
- package/dist/esm/Stepper/Step.esm.js.map +1 -1
- package/dist/esm/Stepper/Step.module.esm.js +1 -1
- package/dist/esm/Stepper/Stepper.esm.js +1 -1
- package/dist/esm/Stepper/Stepper.esm.js.map +1 -1
- package/dist/esm/Stepper/Stepper.module.esm.js +1 -1
- package/dist/esm/Tooltip/Tooltip.esm.js +1 -1
- package/dist/esm/Tooltip/Tooltip.esm.js.map +1 -1
- package/dist/esm/Wizard/Wizard.esm.js.map +1 -1
- package/dist/esm/Wizard/WizardActions/WizardActions.esm.js.map +1 -1
- package/dist/esm/Wizard/WizardStateProvider.esm.js.map +1 -1
- package/dist/esm/Wizard/WizardSteps/WizardSteps.esm.js.map +1 -1
- package/dist/esm/src/components/Notifications/BaseModal/useRepeatFocus.d.ts +7 -0
- package/dist/esm/src/components/Stepper/Step.d.ts +5 -3
- package/dist/esm/src/components/Stepper/Stepper.d.ts +3 -1
- package/dist/esm/src/components/Wizard/WizardActions/WizardActions.d.ts +3 -0
- package/dist/esm/src/components/Wizard/WizardStateProvider.d.ts +3 -0
- package/dist/esm/src/components/Wizard/WizardSteps/WizardSteps.d.ts +3 -0
- package/dist/esm/src/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/DataGrid/DataGridActions/DataGridColumnsToggle.tsx +5 -1
- package/src/components/Form/Radio/Radio.tsx +3 -1
- package/src/components/Form/Select/Option.tsx +1 -1
- package/src/components/Notifications/BaseModal/BaseModal.test.tsx +36 -1
- package/src/components/Notifications/BaseModal/BaseModal.tsx +10 -3
- package/src/components/Notifications/BaseModal/useRepeatFocus.tsx +73 -0
- package/src/components/Stepper/Step.module.scss +129 -59
- package/src/components/Stepper/Step.tsx +57 -54
- package/src/components/Stepper/Stepper.module.scss +12 -8
- package/src/components/Stepper/Stepper.test.tsx +3 -3
- package/src/components/Stepper/Stepper.tsx +17 -7
- package/src/components/Tooltip/Tooltip.tsx +2 -2
- package/src/components/Wizard/Wizard.tsx +3 -0
- package/src/components/Wizard/WizardActions/WizardActions.tsx +3 -0
- package/src/components/Wizard/WizardStateProvider.tsx +3 -0
- package/src/components/Wizard/WizardSteps/WizardSteps.tsx +3 -0
- package/src/index.ts +1 -0
- package/src/mixins.module.scss +1 -0
|
@@ -18,6 +18,7 @@ import React, { useEffect, useRef } from "react";
|
|
|
18
18
|
import { BaseModal, Props } from "./BaseModal";
|
|
19
19
|
import { render, getByText, queryByText, fireEvent } from "@testing-library/react";
|
|
20
20
|
import userEvent from "@testing-library/user-event";
|
|
21
|
+
import { act } from "react-dom/test-utils";
|
|
21
22
|
|
|
22
23
|
const classNames = ["class11", "class12"];
|
|
23
24
|
const containerClassNames = ["class21", "class22"];
|
|
@@ -36,7 +37,17 @@ const createBaseModal = (params?: (defaultParams: Props) => Props) => {
|
|
|
36
37
|
if (params) {
|
|
37
38
|
parameters = params(defaultParams);
|
|
38
39
|
}
|
|
39
|
-
const queries = render(
|
|
40
|
+
const queries = render(
|
|
41
|
+
<BaseModal {...parameters} data-testid="BaseModal">
|
|
42
|
+
<button>Button 1</button>
|
|
43
|
+
<button>Button 2</button>
|
|
44
|
+
<button>Button 3</button>
|
|
45
|
+
<span>{defaultParams.children}</span>
|
|
46
|
+
<button>Button 4</button>
|
|
47
|
+
<button>Button 5</button>
|
|
48
|
+
<button>Button 6</button>
|
|
49
|
+
</BaseModal>
|
|
50
|
+
);
|
|
40
51
|
const slideInModal = queries.getByTestId("BaseModal");
|
|
41
52
|
|
|
42
53
|
return {
|
|
@@ -130,6 +141,30 @@ describe("BaseModal", () => {
|
|
|
130
141
|
fireEvent.keyDown(modal, { key: "Escape" });
|
|
131
142
|
expect(defaultParams.onClose).toHaveBeenCalledTimes(2);
|
|
132
143
|
});
|
|
144
|
+
|
|
145
|
+
it("should repeat focus back to the first button when tabbing through the modal", async () => {
|
|
146
|
+
const { getByText } = createBaseModal();
|
|
147
|
+
|
|
148
|
+
const firstButton = getByText("Button 1");
|
|
149
|
+
const lastButton = getByText("Button 6");
|
|
150
|
+
|
|
151
|
+
await act(() => {
|
|
152
|
+
firstButton.focus();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await userEvent.tab();
|
|
156
|
+
await userEvent.tab();
|
|
157
|
+
await userEvent.tab();
|
|
158
|
+
await userEvent.tab();
|
|
159
|
+
await userEvent.tab();
|
|
160
|
+
await userEvent.tab();
|
|
161
|
+
|
|
162
|
+
expect(firstButton).toHaveFocus();
|
|
163
|
+
|
|
164
|
+
await userEvent.tab({ shift: true });
|
|
165
|
+
|
|
166
|
+
expect(lastButton).toHaveFocus();
|
|
167
|
+
});
|
|
133
168
|
});
|
|
134
169
|
|
|
135
170
|
describe("ref should work", () => {
|
|
@@ -19,12 +19,15 @@ import React, {
|
|
|
19
19
|
ComponentPropsWithRef,
|
|
20
20
|
useEffect,
|
|
21
21
|
useRef,
|
|
22
|
-
ReactElement
|
|
22
|
+
ReactElement,
|
|
23
|
+
RefObject,
|
|
24
|
+
createRef
|
|
23
25
|
} from "react";
|
|
24
26
|
import { createPortal } from "react-dom";
|
|
25
27
|
import { useGetDomRoot } from "../../../hooks/useGetDomRoot";
|
|
26
28
|
import classes from "./BaseModal.module.scss";
|
|
27
29
|
import { labelId, descriptionId } from "./BaseModalContext";
|
|
30
|
+
import { useRepeatFocus } from "./useRepeatFocus";
|
|
28
31
|
|
|
29
32
|
const SCROLL_PROPERTY_NAME = "overflow";
|
|
30
33
|
const SCROLL_PROPERTY_VALUE = "hidden";
|
|
@@ -90,6 +93,7 @@ const BaseModalComponent: ForwardRefRenderFunction<HTMLDivElement, Props> = (
|
|
|
90
93
|
) => {
|
|
91
94
|
useSetBodyScroll(open);
|
|
92
95
|
const wrappingDivRef = useRef<HTMLDivElement>(null);
|
|
96
|
+
const modalRef = (ref as RefObject<HTMLDivElement>) || createRef<HTMLDivElement>();
|
|
93
97
|
const { root } = useGetDomRoot(domRoot, wrappingDivRef);
|
|
94
98
|
|
|
95
99
|
const handleEscKeyPress = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
@@ -99,9 +103,11 @@ const BaseModalComponent: ForwardRefRenderFunction<HTMLDivElement, Props> = (
|
|
|
99
103
|
}
|
|
100
104
|
};
|
|
101
105
|
|
|
106
|
+
useRepeatFocus(modalRef);
|
|
107
|
+
|
|
102
108
|
useEffect(() => {
|
|
103
109
|
if (open) {
|
|
104
|
-
|
|
110
|
+
modalRef.current?.focus();
|
|
105
111
|
}
|
|
106
112
|
}, [open]);
|
|
107
113
|
|
|
@@ -122,7 +128,7 @@ const BaseModalComponent: ForwardRefRenderFunction<HTMLDivElement, Props> = (
|
|
|
122
128
|
{createPortal(
|
|
123
129
|
<div
|
|
124
130
|
{...rest}
|
|
125
|
-
ref={
|
|
131
|
+
ref={modalRef}
|
|
126
132
|
id={id}
|
|
127
133
|
className={`${classes["modal"]} ${open ? classes["visible"] : ""} ${className}`}
|
|
128
134
|
role="dialog"
|
|
@@ -137,6 +143,7 @@ const BaseModalComponent: ForwardRefRenderFunction<HTMLDivElement, Props> = (
|
|
|
137
143
|
>
|
|
138
144
|
<div
|
|
139
145
|
{...backdropProps}
|
|
146
|
+
aria-hidden={true}
|
|
140
147
|
className={`${classes["backdrop"]} ${backdropProps?.className ?? ""}`}
|
|
141
148
|
onClick={handleBackdropClick}
|
|
142
149
|
></div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2022 OneWelcome B.V.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { RefObject, useEffect } from "react";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @description This is a hook that will make sure that when a modal is open and the user tabs through the it,
|
|
21
|
+
* the focus will be repeated and the user will not lose their entire focusable element to an element in the background
|
|
22
|
+
* that is being blocked by the modal.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const useRepeatFocus = (ref: RefObject<HTMLDivElement>) => {
|
|
26
|
+
const getFocusableElement = (
|
|
27
|
+
element: HTMLElement,
|
|
28
|
+
position: "first" | "last"
|
|
29
|
+
): HTMLElement | null => {
|
|
30
|
+
const focusableSelectors =
|
|
31
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
32
|
+
const focusableElements = element.querySelectorAll<HTMLElement>(focusableSelectors);
|
|
33
|
+
|
|
34
|
+
if (position === "first") {
|
|
35
|
+
return focusableElements[0] || null;
|
|
36
|
+
} else if (position === "last") {
|
|
37
|
+
return focusableElements[focusableElements.length - 1] || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!ref.current || !open) return;
|
|
45
|
+
|
|
46
|
+
const lastFocusableElement = getFocusableElement(ref.current, "last");
|
|
47
|
+
const firstFocusableElement = getFocusableElement(ref.current, "first");
|
|
48
|
+
|
|
49
|
+
if (!lastFocusableElement || !firstFocusableElement) return;
|
|
50
|
+
|
|
51
|
+
const handleTabKeyPress = (event: KeyboardEvent) => {
|
|
52
|
+
if (event.key !== "Tab") return;
|
|
53
|
+
|
|
54
|
+
if (event.shiftKey) {
|
|
55
|
+
if (document.activeElement === firstFocusableElement) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
lastFocusableElement?.focus();
|
|
58
|
+
}
|
|
59
|
+
} else if (document.activeElement === lastFocusableElement) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
firstFocusableElement?.focus();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
lastFocusableElement.addEventListener("keydown", handleTabKeyPress);
|
|
66
|
+
firstFocusableElement.addEventListener("keydown", handleTabKeyPress);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
lastFocusableElement.removeEventListener("keydown", handleTabKeyPress);
|
|
70
|
+
firstFocusableElement.removeEventListener("keydown", handleTabKeyPress);
|
|
71
|
+
};
|
|
72
|
+
}, [ref, open]);
|
|
73
|
+
};
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
@use "../../mixins.module.scss";
|
|
18
18
|
|
|
19
19
|
@mixin stepState($status: "waiting") {
|
|
20
|
-
& .step
|
|
20
|
+
& .step {
|
|
21
21
|
color: var(--stepper-default-text-color);
|
|
22
22
|
|
|
23
23
|
@if $status == "current" {
|
|
@@ -34,16 +34,14 @@
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
@if $status == "
|
|
39
|
-
|
|
40
|
-
} @else if $status == "done" {
|
|
41
|
-
border-color: var(--stepper-line-bold-color);
|
|
37
|
+
& .stepper-line {
|
|
38
|
+
@if $status == "done" {
|
|
39
|
+
background-color: var(--stepper-line-bold-color);
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
&:hover {
|
|
46
|
-
& .step
|
|
44
|
+
& .step {
|
|
47
45
|
@if $status == "current" {
|
|
48
46
|
border-color: var(--stepper-current-hover-color);
|
|
49
47
|
background-color: var(--stepper-current-hover-color);
|
|
@@ -61,7 +59,7 @@
|
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
&:active {
|
|
64
|
-
& .step
|
|
62
|
+
& .step {
|
|
65
63
|
@if $status == "current" {
|
|
66
64
|
border-color: var(--stepper-current-active-color);
|
|
67
65
|
background-color: var(--stepper-current-active-color);
|
|
@@ -78,10 +76,11 @@
|
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
&.disabled {
|
|
82
80
|
cursor: not-allowed;
|
|
83
81
|
|
|
84
|
-
& .step
|
|
82
|
+
& .step {
|
|
83
|
+
cursor: not-allowed;
|
|
85
84
|
@if $status == "current" {
|
|
86
85
|
border-color: var(--stepper-current-disabled-color);
|
|
87
86
|
background-color: var(--stepper-current-disabled-color);
|
|
@@ -97,7 +96,8 @@
|
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
& .
|
|
99
|
+
& .label {
|
|
100
|
+
cursor: not-allowed;
|
|
101
101
|
color: var(--stepper-label-disabled-color);
|
|
102
102
|
|
|
103
103
|
& .caption {
|
|
@@ -108,8 +108,8 @@
|
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
& .stepper-line {
|
|
112
|
+
background-color: if(
|
|
113
113
|
$status == "done",
|
|
114
114
|
var(--stepper-line-bold-disabled-color),
|
|
115
115
|
var(--stepper-line-disabled-color)
|
|
@@ -119,57 +119,110 @@
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
.step-wrapper {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
pointer-events: none; //turn off pointer-events beside .label and .step where we turn it on
|
|
123
|
+
display: grid;
|
|
124
|
+
gap: 0.5rem;
|
|
125
|
+
grid-template-columns: 1.875rem 1fr;
|
|
126
|
+
grid-template-rows: auto 1fr;
|
|
127
127
|
background-color: transparent;
|
|
128
128
|
border: 0;
|
|
129
129
|
padding: 0;
|
|
130
130
|
margin: 0;
|
|
131
|
-
cursor: pointer;
|
|
132
131
|
|
|
133
|
-
|
|
132
|
+
&.horizontal {
|
|
133
|
+
grid-template-columns: 1.875rem auto 1fr;
|
|
134
|
+
grid-template-rows: auto 1fr;
|
|
135
|
+
align-items: start;
|
|
136
|
+
width: 100%;
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
.stepper-line {
|
|
139
|
+
background-color: var(--stepper-line-color);
|
|
140
|
+
min-height: auto;
|
|
141
|
+
height: 2px;
|
|
142
|
+
width: 100%;
|
|
143
|
+
min-width: 1rem;
|
|
144
|
+
border-radius: 2px;
|
|
145
|
+
margin-top: 0.8125rem;
|
|
146
|
+
}
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
148
|
+
.label {
|
|
149
|
+
margin-top: 0.25rem;
|
|
150
|
+
}
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
&.text-bottom {
|
|
153
|
+
grid-template-columns: 5rem 1fr;
|
|
154
|
+
gap: 0;
|
|
155
|
+
|
|
156
|
+
.step {
|
|
157
|
+
justify-self: center;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.label {
|
|
161
|
+
justify-self: center;
|
|
162
|
+
text-align: center;
|
|
163
|
+
grid-column-start: 1;
|
|
164
|
+
grid-column-end: 1;
|
|
165
|
+
grid-row-start: 2;
|
|
166
|
+
grid-row-end: 2;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.stepper-line {
|
|
170
|
+
margin-right: -1.125rem;
|
|
171
|
+
margin-left: -1.125rem;
|
|
172
|
+
width: calc(100% + 2.25rem);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.label-inner-wrapper {
|
|
176
|
+
display: inline;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&.last-step {
|
|
180
|
+
justify-content: center;
|
|
181
|
+
grid-template-columns: 5rem;
|
|
182
|
+
grid-template-rows: auto 1fr;
|
|
183
|
+
flex: 1 0 5rem;
|
|
184
|
+
display: grid;
|
|
153
185
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
186
|
+
.label {
|
|
187
|
+
width: auto;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
&.last-step {
|
|
193
|
+
display: flex;
|
|
194
|
+
width: max-content;
|
|
158
195
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
196
|
+
.label {
|
|
197
|
+
width: max-content;
|
|
198
|
+
}
|
|
162
199
|
}
|
|
163
200
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
border-bottom: none;
|
|
167
|
-
margin: 0.5rem 0 0.5rem 1.6875rem;
|
|
168
|
-
border-left: 2px solid var(--stepper-line-color);
|
|
169
|
-
min-height: 0.5rem;
|
|
170
|
-
height: 100%;
|
|
171
|
-
width: 100%;
|
|
201
|
+
.label-inner-wrapper {
|
|
202
|
+
display: flex;
|
|
172
203
|
}
|
|
204
|
+
|
|
205
|
+
.stepper-line-extender {
|
|
206
|
+
display: block;
|
|
207
|
+
flex: 1;
|
|
208
|
+
margin-left: 0.4375rem;
|
|
209
|
+
margin-right: -1rem;
|
|
210
|
+
margin-top: 0.5625rem;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.stepper-line {
|
|
215
|
+
pointer-events: none;
|
|
216
|
+
justify-self: center;
|
|
217
|
+
background-color: var(--stepper-line-color);
|
|
218
|
+
min-height: 0.375rem; //on design pixes used in rounding does not count, so that's why it's not 4px but 6px
|
|
219
|
+
height: 100%;
|
|
220
|
+
width: 2px;
|
|
221
|
+
border-radius: 2px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
&.vertical.has-caption .stepper-line {
|
|
225
|
+
min-height: 1.375rem; //on design pixes used in rounding does not count, so that's why it's not 20px but 22px
|
|
173
226
|
}
|
|
174
227
|
|
|
175
228
|
&.waiting {
|
|
@@ -179,7 +232,7 @@
|
|
|
179
232
|
&.current {
|
|
180
233
|
@include stepState($status: "current");
|
|
181
234
|
|
|
182
|
-
& .
|
|
235
|
+
& .label {
|
|
183
236
|
font-weight: 700;
|
|
184
237
|
& .caption {
|
|
185
238
|
font-weight: 400;
|
|
@@ -194,13 +247,15 @@
|
|
|
194
247
|
&.error {
|
|
195
248
|
@include stepState($status: "error");
|
|
196
249
|
|
|
197
|
-
& .
|
|
250
|
+
& .label .caption {
|
|
198
251
|
color: var(--stepper-caption-error-color);
|
|
199
252
|
}
|
|
200
253
|
}
|
|
201
254
|
}
|
|
202
255
|
|
|
203
256
|
.step {
|
|
257
|
+
pointer-events: visible;
|
|
258
|
+
cursor: pointer;
|
|
204
259
|
flex-shrink: 0;
|
|
205
260
|
box-sizing: border-box;
|
|
206
261
|
display: flex;
|
|
@@ -216,16 +271,31 @@
|
|
|
216
271
|
}
|
|
217
272
|
|
|
218
273
|
.label {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
274
|
+
pointer-events: visible;
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
grid-column-start: 2;
|
|
277
|
+
grid-column-end: 2;
|
|
278
|
+
grid-row-start: 1;
|
|
279
|
+
grid-row-end: span 2;
|
|
280
|
+
align-self: start;
|
|
281
|
+
|
|
282
|
+
margin: 0.25rem 0 0;
|
|
283
|
+
padding: 0;
|
|
284
|
+
background-color: initial;
|
|
285
|
+
border: none;
|
|
286
|
+
text-align: left;
|
|
222
287
|
color: var(--stepper-label-color);
|
|
288
|
+
font-family: var(--font-family);
|
|
289
|
+
font-size: var(--font-size-form-label);
|
|
290
|
+
line-height: 1.25rem;
|
|
291
|
+
|
|
292
|
+
@include mixins.focusVisibleOutline($outlineOffset: "1px");
|
|
223
293
|
}
|
|
224
294
|
|
|
225
295
|
.caption {
|
|
226
|
-
|
|
227
|
-
top: 1.25rem;
|
|
228
|
-
left: 0;
|
|
296
|
+
display: block;
|
|
229
297
|
color: var(--stepper-caption-color);
|
|
298
|
+
font-family: var(--font-family);
|
|
230
299
|
font-size: 0.75rem;
|
|
300
|
+
line-height: 1rem;
|
|
231
301
|
}
|
|
@@ -14,19 +14,14 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, {
|
|
18
|
-
ComponentPropsWithRef,
|
|
19
|
-
ForwardRefRenderFunction,
|
|
20
|
-
useLayoutEffect,
|
|
21
|
-
useRef,
|
|
22
|
-
useState
|
|
23
|
-
} from "react";
|
|
17
|
+
import React, { CSSProperties, ComponentPropsWithRef, ForwardRefRenderFunction } from "react";
|
|
24
18
|
import { Icon, Icons } from "../Icon/Icon";
|
|
25
19
|
import classes from "./Step.module.scss";
|
|
20
|
+
import { gapBetweenStepsInRem } from "./Stepper";
|
|
26
21
|
|
|
27
22
|
export type StepStatus = "waiting" | "current" | "done" | "error";
|
|
28
23
|
|
|
29
|
-
export interface Props extends ComponentPropsWithRef<"
|
|
24
|
+
export interface Props extends Omit<ComponentPropsWithRef<"div">, "onClick"> {
|
|
30
25
|
status: StepStatus;
|
|
31
26
|
label: string;
|
|
32
27
|
caption?: string;
|
|
@@ -34,6 +29,8 @@ export interface Props extends ComponentPropsWithRef<"button"> {
|
|
|
34
29
|
lastStep?: boolean;
|
|
35
30
|
disabled?: boolean;
|
|
36
31
|
direction?: "horizontal" | "vertical";
|
|
32
|
+
textPosition?: "bottom" | "right";
|
|
33
|
+
onClick?: (event: React.MouseEvent<HTMLButtonElement | HTMLDivElement>) => void;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
const getStepContent = (index: number, status: StepStatus) => {
|
|
@@ -49,63 +46,69 @@ const getStepContent = (index: number, status: StepStatus) => {
|
|
|
49
46
|
}
|
|
50
47
|
};
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const getStepMaxWidth = (isHorizontal: boolean, lastStep: boolean, index: number) => {
|
|
50
|
+
if (isHorizontal && lastStep) {
|
|
51
|
+
const percentage = 100 / (index + 1);
|
|
52
|
+
const gapSize = index * gapBetweenStepsInRem;
|
|
53
|
+
return `calc(${percentage}% - ${gapSize}rem)`;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const StepComponent: ForwardRefRenderFunction<HTMLDivElement, Props> = (
|
|
58
|
+
{
|
|
59
|
+
label,
|
|
60
|
+
caption,
|
|
61
|
+
status,
|
|
62
|
+
index,
|
|
63
|
+
direction,
|
|
64
|
+
disabled,
|
|
65
|
+
lastStep,
|
|
66
|
+
onClick,
|
|
67
|
+
textPosition,
|
|
68
|
+
...rest
|
|
69
|
+
}: Props,
|
|
54
70
|
ref
|
|
55
71
|
) => {
|
|
56
72
|
const stepIndex = index ?? 0;
|
|
57
73
|
const additionalClasses = [classes[status]];
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const captionRef = useRef<HTMLSpanElement>(null);
|
|
63
|
-
const labelRef = useRef<HTMLSpanElement>(null);
|
|
64
|
-
const stepContentRef = useRef<HTMLDivElement>(null);
|
|
74
|
+
const additionalStyles: CSSProperties = {};
|
|
75
|
+
const isHorizontal = direction === "horizontal";
|
|
76
|
+
const isTextBottom = textPosition === "bottom";
|
|
77
|
+
const hasCaption = !!caption;
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
setStepContentHeight(capionHeight + stepContentHeight);
|
|
72
|
-
}
|
|
79
|
+
additionalClasses.push(isHorizontal ? classes["horizontal"] : classes["vertical"]);
|
|
80
|
+
disabled && additionalClasses.push(classes["disabled"]);
|
|
81
|
+
isTextBottom && additionalClasses.push(classes["text-bottom"]);
|
|
82
|
+
lastStep && additionalClasses.push(classes["last-step"]);
|
|
83
|
+
hasCaption && additionalClasses.push(classes["has-caption"]);
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
const captionWidth = captionRef.current.getBoundingClientRect().width;
|
|
76
|
-
const labelWidth = labelRef.current.getBoundingClientRect().width;
|
|
77
|
-
setStepContentWidth(captionWidth > labelWidth ? captionWidth + 36 : labelWidth + 36);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}, []);
|
|
85
|
+
additionalStyles["maxWidth"] = getStepMaxWidth(isHorizontal, !!lastStep, index!);
|
|
81
86
|
|
|
82
87
|
return (
|
|
83
|
-
<
|
|
84
|
-
{
|
|
85
|
-
ref={ref}
|
|
86
|
-
disabled={disabled}
|
|
87
|
-
aria-current={status === "current" ? "step" : undefined}
|
|
88
|
+
<div
|
|
89
|
+
style={additionalStyles}
|
|
88
90
|
className={`${classes["step-wrapper"]} ${additionalClasses.join(" ")}`}
|
|
91
|
+
{...rest}
|
|
89
92
|
>
|
|
90
|
-
<div
|
|
91
|
-
|
|
92
|
-
height: lastStep && direction == "vertical" ? `${stepContentHeight}px` : "auto",
|
|
93
|
-
width: lastStep && direction == "horizontal" ? `${stepContentWidth}px` : "auto"
|
|
94
|
-
}}
|
|
95
|
-
>
|
|
96
|
-
<div ref={stepContentRef} className={classes["step-content"]}>
|
|
97
|
-
<div aria-hidden className={classes["step"]}>
|
|
98
|
-
{getStepContent(stepIndex, status)}
|
|
99
|
-
</div>
|
|
100
|
-
<span ref={labelRef} className={classes["label"]}>
|
|
101
|
-
{label}
|
|
102
|
-
<span ref={captionRef} className={classes["caption"]}>
|
|
103
|
-
{caption}
|
|
104
|
-
</span>
|
|
105
|
-
</span>
|
|
106
|
-
</div>
|
|
93
|
+
<div aria-hidden className={classes["step"]} onClick={onClick}>
|
|
94
|
+
{getStepContent(stepIndex, status)}
|
|
107
95
|
</div>
|
|
108
|
-
|
|
96
|
+
<button
|
|
97
|
+
aria-current={status === "current" ? "step" : undefined}
|
|
98
|
+
className={classes["label"]}
|
|
99
|
+
disabled={disabled}
|
|
100
|
+
onClick={onClick}
|
|
101
|
+
>
|
|
102
|
+
<span className={classes["label-inner-wrapper"]}>
|
|
103
|
+
{label}{" "}
|
|
104
|
+
{!lastStep && isHorizontal && !isTextBottom && (
|
|
105
|
+
<div className={`${classes["stepper-line"]} ${classes["stepper-line-extender"]}`}></div>
|
|
106
|
+
)}
|
|
107
|
+
</span>
|
|
108
|
+
<span className={classes["caption"]}>{caption}</span>
|
|
109
|
+
</button>
|
|
110
|
+
{!lastStep && <div className={classes["stepper-line"]}></div>}
|
|
111
|
+
</div>
|
|
109
112
|
);
|
|
110
113
|
};
|
|
111
114
|
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
.stepper {
|
|
2
|
-
width: 100%;
|
|
3
2
|
display: flex;
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
gap: 0.5rem;
|
|
4
|
+
justify-content: flex-start;
|
|
5
|
+
width: auto;
|
|
6
|
+
flex-direction: column;
|
|
6
7
|
|
|
7
|
-
&.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
&.horizontal {
|
|
9
|
+
justify-content: center;
|
|
10
|
+
flex-direction: row;
|
|
11
|
+
width: 100%;
|
|
12
|
+
|
|
13
|
+
&.text-bottom {
|
|
14
|
+
gap: 0;
|
|
15
|
+
}
|
|
12
16
|
}
|
|
13
17
|
}
|
|
@@ -62,13 +62,13 @@ describe("<Stepper/> should render", () => {
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
it("should render the horizontal stepper", () => {
|
|
65
|
-
const { StepperComponent
|
|
65
|
+
const { StepperComponent } = createStepper(params => ({
|
|
66
66
|
...params,
|
|
67
67
|
direction: "horizontal"
|
|
68
68
|
}));
|
|
69
69
|
|
|
70
70
|
expect(StepperComponent).toBeDefined();
|
|
71
|
-
expect(StepperComponent.classList.
|
|
71
|
+
expect(StepperComponent.classList).toContain("horizontal");
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
it("should render the vertical stepper", () => {
|
|
@@ -78,6 +78,6 @@ describe("<Stepper/> should render", () => {
|
|
|
78
78
|
}));
|
|
79
79
|
|
|
80
80
|
expect(StepperComponent).toBeDefined();
|
|
81
|
-
expect(StepperComponent.classList.
|
|
81
|
+
expect(StepperComponent.classList).not.toContain("horizontal");
|
|
82
82
|
});
|
|
83
83
|
});
|