@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.
Files changed (81) hide show
  1. package/dist/cjs/Button/Button.module.cjs.js +1 -1
  2. package/dist/cjs/Button/IconButton.module.cjs.js +1 -1
  3. package/dist/cjs/DataGrid/DataGridActions/DataGridColumnsToggle.cjs.js +1 -1
  4. package/dist/cjs/DataGrid/DataGridActions/DataGridColumnsToggle.cjs.js.map +1 -1
  5. package/dist/cjs/Form/Radio/Radio.cjs.js.map +1 -1
  6. package/dist/cjs/Form/Select/Option.cjs.js +1 -1
  7. package/dist/cjs/Form/Select/Option.cjs.js.map +1 -1
  8. package/dist/cjs/Link/Link.module.cjs.js +1 -1
  9. package/dist/cjs/Notifications/BaseModal/BaseModal.cjs.js +1 -1
  10. package/dist/cjs/Notifications/BaseModal/BaseModal.cjs.js.map +1 -1
  11. package/dist/cjs/Notifications/BaseModal/useRepeatFocus.cjs.js +2 -0
  12. package/dist/cjs/Notifications/BaseModal/useRepeatFocus.cjs.js.map +1 -0
  13. package/dist/cjs/Stepper/Step.cjs.js +1 -1
  14. package/dist/cjs/Stepper/Step.cjs.js.map +1 -1
  15. package/dist/cjs/Stepper/Step.module.cjs.js +1 -1
  16. package/dist/cjs/Stepper/Stepper.cjs.js +1 -1
  17. package/dist/cjs/Stepper/Stepper.cjs.js.map +1 -1
  18. package/dist/cjs/Stepper/Stepper.module.cjs.js +1 -1
  19. package/dist/cjs/Tooltip/Tooltip.cjs.js +1 -1
  20. package/dist/cjs/Tooltip/Tooltip.cjs.js.map +1 -1
  21. package/dist/cjs/Wizard/Wizard.cjs.js.map +1 -1
  22. package/dist/cjs/Wizard/WizardActions/WizardActions.cjs.js.map +1 -1
  23. package/dist/cjs/Wizard/WizardStateProvider.cjs.js.map +1 -1
  24. package/dist/cjs/Wizard/WizardSteps/WizardSteps.cjs.js.map +1 -1
  25. package/dist/cjs/src/components/Notifications/BaseModal/useRepeatFocus.d.ts +7 -0
  26. package/dist/cjs/src/components/Stepper/Step.d.ts +5 -3
  27. package/dist/cjs/src/components/Stepper/Stepper.d.ts +3 -1
  28. package/dist/cjs/src/components/Wizard/WizardActions/WizardActions.d.ts +3 -0
  29. package/dist/cjs/src/components/Wizard/WizardStateProvider.d.ts +3 -0
  30. package/dist/cjs/src/components/Wizard/WizardSteps/WizardSteps.d.ts +3 -0
  31. package/dist/cjs/src/index.d.ts +1 -0
  32. package/dist/esm/Button/Button.module.esm.js +1 -1
  33. package/dist/esm/Button/IconButton.module.esm.js +1 -1
  34. package/dist/esm/DataGrid/DataGridActions/DataGridColumnsToggle.esm.js +1 -1
  35. package/dist/esm/DataGrid/DataGridActions/DataGridColumnsToggle.esm.js.map +1 -1
  36. package/dist/esm/Form/Radio/Radio.esm.js.map +1 -1
  37. package/dist/esm/Form/Select/Option.esm.js +1 -1
  38. package/dist/esm/Form/Select/Option.esm.js.map +1 -1
  39. package/dist/esm/Link/Link.module.esm.js +1 -1
  40. package/dist/esm/Notifications/BaseModal/BaseModal.esm.js +1 -1
  41. package/dist/esm/Notifications/BaseModal/BaseModal.esm.js.map +1 -1
  42. package/dist/esm/Notifications/BaseModal/useRepeatFocus.esm.js +2 -0
  43. package/dist/esm/Notifications/BaseModal/useRepeatFocus.esm.js.map +1 -0
  44. package/dist/esm/Stepper/Step.esm.js +1 -1
  45. package/dist/esm/Stepper/Step.esm.js.map +1 -1
  46. package/dist/esm/Stepper/Step.module.esm.js +1 -1
  47. package/dist/esm/Stepper/Stepper.esm.js +1 -1
  48. package/dist/esm/Stepper/Stepper.esm.js.map +1 -1
  49. package/dist/esm/Stepper/Stepper.module.esm.js +1 -1
  50. package/dist/esm/Tooltip/Tooltip.esm.js +1 -1
  51. package/dist/esm/Tooltip/Tooltip.esm.js.map +1 -1
  52. package/dist/esm/Wizard/Wizard.esm.js.map +1 -1
  53. package/dist/esm/Wizard/WizardActions/WizardActions.esm.js.map +1 -1
  54. package/dist/esm/Wizard/WizardStateProvider.esm.js.map +1 -1
  55. package/dist/esm/Wizard/WizardSteps/WizardSteps.esm.js.map +1 -1
  56. package/dist/esm/src/components/Notifications/BaseModal/useRepeatFocus.d.ts +7 -0
  57. package/dist/esm/src/components/Stepper/Step.d.ts +5 -3
  58. package/dist/esm/src/components/Stepper/Stepper.d.ts +3 -1
  59. package/dist/esm/src/components/Wizard/WizardActions/WizardActions.d.ts +3 -0
  60. package/dist/esm/src/components/Wizard/WizardStateProvider.d.ts +3 -0
  61. package/dist/esm/src/components/Wizard/WizardSteps/WizardSteps.d.ts +3 -0
  62. package/dist/esm/src/index.d.ts +1 -0
  63. package/package.json +1 -1
  64. package/src/components/DataGrid/DataGridActions/DataGridColumnsToggle.tsx +5 -1
  65. package/src/components/Form/Radio/Radio.tsx +3 -1
  66. package/src/components/Form/Select/Option.tsx +1 -1
  67. package/src/components/Notifications/BaseModal/BaseModal.test.tsx +36 -1
  68. package/src/components/Notifications/BaseModal/BaseModal.tsx +10 -3
  69. package/src/components/Notifications/BaseModal/useRepeatFocus.tsx +73 -0
  70. package/src/components/Stepper/Step.module.scss +129 -59
  71. package/src/components/Stepper/Step.tsx +57 -54
  72. package/src/components/Stepper/Stepper.module.scss +12 -8
  73. package/src/components/Stepper/Stepper.test.tsx +3 -3
  74. package/src/components/Stepper/Stepper.tsx +17 -7
  75. package/src/components/Tooltip/Tooltip.tsx +2 -2
  76. package/src/components/Wizard/Wizard.tsx +3 -0
  77. package/src/components/Wizard/WizardActions/WizardActions.tsx +3 -0
  78. package/src/components/Wizard/WizardStateProvider.tsx +3 -0
  79. package/src/components/Wizard/WizardSteps/WizardSteps.tsx +3 -0
  80. package/src/index.ts +1 -0
  81. 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(<BaseModal {...parameters} data-testid="BaseModal" />);
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
- wrappingDivRef.current?.focus();
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={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-content .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
- &:not(:last-child)::after {
38
- @if $status == "current" or $status == "error" {
39
- border-color: var(--stepper-line-color);
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-content .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-content .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
- &:disabled {
79
+ &.disabled {
82
80
  cursor: not-allowed;
83
81
 
84
- & .step-content .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
- & .step-content .label {
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
- &:not(:last-child)::after {
112
- border-color: if(
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
- display: flex;
123
- flex: 1;
124
- flex-direction: row;
125
- justify-content: center;
126
- align-items: center;
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
- @include mixins.focusVisibleOutline($outlineOffset: 0px);
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
- .step-content {
136
- display: flex;
137
- justify-content: center;
138
- align-items: center;
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
- &:last-child {
142
- flex-grow: 0;
143
- flex-basis: fit-content;
144
- }
148
+ .label {
149
+ margin-top: 0.25rem;
150
+ }
145
151
 
146
- &:not(:last-child)::after {
147
- content: "";
148
- margin: 0 0.5rem;
149
- flex-grow: 1;
150
- min-width: 0.5rem;
151
- border-bottom: 2px solid var(--stepper-line-color);
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
- &.vertical {
155
- display: flex;
156
- flex-direction: column;
157
- flex-grow: 1;
186
+ .label {
187
+ width: auto;
188
+ }
189
+ }
190
+ }
191
+
192
+ &.last-step {
193
+ display: flex;
194
+ width: max-content;
158
195
 
159
- &:last-child {
160
- flex-grow: 0;
161
- flex-basis: fit-content;
196
+ .label {
197
+ width: max-content;
198
+ }
162
199
  }
163
200
 
164
- &:not(:last-child)::after {
165
- content: "";
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
- & .step-content .label {
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
- & .step-content .label .caption {
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
- flex-shrink: 0;
220
- position: relative;
221
- margin-left: 0.5rem;
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
- position: absolute;
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<"button"> {
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
- export const StepComponent: ForwardRefRenderFunction<HTMLButtonElement, Props> = (
53
- { label, caption, status, index, direction, disabled, lastStep, ...rest }: Props,
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 [stepContentHeight, setStepContentHeight] = useState<number>(28);
59
- const [stepContentWidth, setStepContentWidth] = useState<number>(28);
60
- direction === "vertical" && additionalClasses.push(classes["vertical"]);
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
- useLayoutEffect(() => {
67
- if (captionRef.current && stepContentRef.current && labelRef.current && lastStep) {
68
- if (direction == "vertical") {
69
- const capionHeight = captionRef.current.getBoundingClientRect().height;
70
- const stepContentHeight = stepContentRef.current.getBoundingClientRect().height;
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
- if (direction == "horizontal") {
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
- <button
84
- {...rest}
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
- style={{
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
- </button>
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
- justify-content: center;
5
- align-items: center;
3
+ gap: 0.5rem;
4
+ justify-content: flex-start;
5
+ width: auto;
6
+ flex-direction: column;
6
7
 
7
- &.vertical {
8
- flex-direction: column;
9
- justify-content: flex-start;
10
- height: 100%;
11
- width: auto;
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, getAllByText } = createStepper(params => ({
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.contains("horizontal")).toBe(true);
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.contains("vertical")).toBe(true);
81
+ expect(StepperComponent.classList).not.toContain("horizontal");
82
82
  });
83
83
  });