Skip to content

Commit eeeb941

Browse files
authored
Merge pull request #11 from chcederquist/chcederquist/feat/step-indicator
feat(StepIndicator): added step indicator and simple variant
2 parents b7606c6 + 9cb6fd8 commit eeeb941

File tree

7 files changed

+326
-6
lines changed

7 files changed

+326
-6
lines changed

src/components/Modal/Modal.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ function setInert(inert: boolean, exceptId?: string) {
3030
export type ModalProps = {
3131
id: string;
3232
header: HeadingProps;
33+
modalType?: "default" | "step-indicator";
3334
children: ReactNode;
3435
show?: boolean;
3536
onClose?: () => void;
3637
footer?: ReactNode;
3738
hasCloseButton?: boolean;
3839
};
39-
// TODO: Close all other modals, create backdrop
4040

4141
/**
4242
* Renders a modal dialog component with optional header and footer sections.
@@ -60,6 +60,7 @@ export function Modal({
6060
show,
6161
onClose,
6262
hasCloseButton = true,
63+
modalType = "default",
6364
}: Readonly<ModalProps>) {
6465
// Local state to control visibility if 'show' is not provided
6566
const [visible, setVisible] = useState(show ?? false);
@@ -99,16 +100,25 @@ export function Modal({
99100

100101
return createPortal(
101102
<>
102-
{visible && <ModalBackdrop></ModalBackdrop>}
103+
{<ModalBackdrop type={modalType} visible={visible}></ModalBackdrop>}
103104
<div
104-
className="fds-modal"
105+
className={mergeStrings(
106+
"fds-modal",
107+
modalType === "step-indicator" ? "modal-step-indicator" : null,
108+
)}
105109
id={id}
106110
aria-hidden={visible ? "false" : "true"}
107111
role="dialog"
108112
aria-modal="true"
109113
aria-labelledby={`${id}-header`}
110114
>
111-
<div className="modal-content">
115+
<div
116+
className={mergeStrings(
117+
"modal-content",
118+
modalType === "step-indicator" && "has-transition-effect",
119+
show && modalType === "step-indicator" && "show-modal-content",
120+
)}
121+
>
112122
{header && (
113123
<div className="modal-header">
114124
<Heading
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1-
export function ModalBackdrop() {
2-
return <div className="modal-backdrop show" id="modal-backdrop"></div>;
1+
import { mergeStrings } from "../../util/merge-classnames";
2+
3+
export function ModalBackdrop({
4+
type,
5+
visible,
6+
}: {
7+
type?: "default" | "step-indicator";
8+
visible?: boolean;
9+
}) {
10+
return (
11+
<div
12+
className={mergeStrings(
13+
"modal-backdrop",
14+
visible && `show`,
15+
type === "step-indicator" && `step-indicator`,
16+
)}
17+
id="modal-backdrop"
18+
></div>
19+
);
320
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useT } from "../../hooks/useT";
2+
3+
export function SimpleStepIndicator({
4+
stepCount,
5+
currentStep,
6+
}: {
7+
stepCount: number;
8+
currentStep: number;
9+
}) {
10+
const t = useT();
11+
return (
12+
<p className="step-subheading">
13+
{t("simple_step_indicator_current_step_sr_label", {
14+
stepNumber: currentStep,
15+
totalSteps: stepCount,
16+
}) ?? `Trin ${currentStep} af ${stepCount}`}
17+
</p>
18+
);
19+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { useState } from "react";
2+
import { useT } from "../../hooks/useT";
3+
import { mergeStrings } from "../../util/merge-classnames";
4+
import { Modal } from "../Modal/Modal";
5+
import { Icon } from "../Shared/Icon";
6+
7+
export function StepIndicator({
8+
steps,
9+
stepIndicatorId,
10+
onStepClick,
11+
}: {
12+
onStepClick?: (stepIndex: number, disabled?: boolean) => void;
13+
stepIndicatorId: string;
14+
steps: {
15+
title: string;
16+
status: "completed" | "error" | "default";
17+
stepInformation?: string;
18+
disabled?: boolean;
19+
current?: boolean;
20+
}[];
21+
}) {
22+
const t = useT();
23+
const [showMobileDialog, setShowMobileDialog] = useState(false);
24+
const stepList = (
25+
<ol className="step-indicator">
26+
{steps.map((step, index) => {
27+
const StepElement = step.disabled ? "div" : "a";
28+
return (
29+
<li
30+
key={index}
31+
className={mergeStrings(
32+
step.disabled && "disabled",
33+
step.current && "current",
34+
step.status === "error" && "error",
35+
)}
36+
>
37+
<StepElement
38+
className="step"
39+
aria-current={step.current ? "step" : undefined}
40+
href={StepElement === "a" ? "#" : undefined}
41+
onClick={(e) => {
42+
e.preventDefault();
43+
e.stopPropagation();
44+
onStepClick?.(index, step.disabled);
45+
}}
46+
>
47+
{(step.status === "completed" || step.status === "error") && (
48+
<span className="step-icon">
49+
{/* Step completed */}
50+
{step.status === "completed" && (
51+
<Icon
52+
icon="check"
53+
svgProps={{
54+
"aria-label": t(
55+
"step_indicator_step_completed_sr_label",
56+
{
57+
stepNumber: index + 1,
58+
},
59+
),
60+
}}
61+
/>
62+
)}
63+
{/* Step has error */}
64+
{step.status === "error" && (
65+
<Icon
66+
icon="error"
67+
svgProps={{
68+
"aria-label": t("step_indicator_error_icon_sr_label", {
69+
stepNumber: index + 1,
70+
}),
71+
}}
72+
/>
73+
)}
74+
</span>
75+
)}
76+
{step.status === "default" && (
77+
<span className="step-number">
78+
<span>{index + 1}</span>
79+
</span>
80+
)}
81+
<div>
82+
<span className="step-title">{step.title}</span>
83+
{step.stepInformation && (
84+
<span className="step-information">
85+
{step.stepInformation}
86+
</span>
87+
)}
88+
</div>
89+
</StepElement>
90+
</li>
91+
);
92+
})}
93+
</ol>
94+
);
95+
const currentStep = steps.findIndex((step) => step.current === true) + 1 || 1;
96+
return (
97+
<>
98+
<nav aria-label="Trinindikator" className="d-none d-md-block">
99+
{stepList}
100+
</nav>
101+
<div>
102+
<button
103+
className="step-indicator-button d-md-none"
104+
aria-haspopup="dialog"
105+
onClick={() => setShowMobileDialog(true)}
106+
type="button"
107+
>
108+
<span>
109+
{t("step_indicator_mobile_indicator_button", {
110+
stepNumber: currentStep,
111+
totalSteps: steps.length,
112+
}) ?? (
113+
<>
114+
Trin <strong>{currentStep}</strong> af {steps.length}
115+
</>
116+
)}
117+
</span>
118+
</button>
119+
</div>
120+
<Modal
121+
modalType="step-indicator"
122+
header={{
123+
children:
124+
t("step_indicator_mobile_modal_heading", {
125+
stepNumber: currentStep,
126+
totalSteps: steps.length,
127+
}) ?? `Trin ${currentStep} af ${steps.length}`,
128+
level: "h2",
129+
}}
130+
id={stepIndicatorId}
131+
onClose={() => setShowMobileDialog(false)}
132+
show={showMobileDialog}
133+
>
134+
<nav aria-label="Trinindikator" className="d-md-none">
135+
{stepList}
136+
</nav>
137+
</Modal>
138+
</>
139+
);
140+
}

src/contexts/translation-context.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,24 @@ export type TranslationConfig = {
4444
table_pagination_ellipsis_sr_label: void;
4545
table_pagination_page_selection_next: void;
4646
table_pagination_page_selection_last_sr_label: void;
47+
step_indicator_step_completed_sr_label: {
48+
stepNumber: number;
49+
};
50+
step_indicator_error_icon_sr_label: {
51+
stepNumber: number;
52+
};
53+
simple_step_indicator_current_step_sr_label: {
54+
stepNumber: number;
55+
totalSteps: number;
56+
};
57+
step_indicator_mobile_indicator_button: {
58+
stepNumber: number;
59+
totalSteps: number;
60+
};
61+
step_indicator_mobile_modal_heading: {
62+
stepNumber: number;
63+
totalSteps: number;
64+
};
4765
};
4866

4967
export type TranslateFn = <K extends keyof TranslationConfig>(
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { StepIndicator } from "../../components/StepIndicator/StepIndicator";
3+
import { SimpleStepIndicator } from "../../components/StepIndicator/SimpleStepIndicator";
4+
5+
const meta = {
6+
title: "DKFDS/StepIndicator",
7+
component: StepIndicator,
8+
} satisfies Meta<typeof StepIndicator>;
9+
10+
export default meta;
11+
type Story = StoryObj<typeof meta>;
12+
13+
export const MainStepIndicator: Story = {
14+
args: {
15+
steps: [
16+
{
17+
title: "Trin 1",
18+
status: "completed",
19+
},
20+
{
21+
title: "Trin 2",
22+
status: "default",
23+
current: true,
24+
},
25+
{
26+
title: "Trin 3",
27+
status: "default",
28+
disabled: true,
29+
},
30+
],
31+
stepIndicatorId: "main-step-indicator",
32+
onStepClick: (stepIndex, disabled) => {
33+
if (disabled) {
34+
alert(`Trin ${stepIndex + 1} er deaktiveret`);
35+
} else {
36+
alert(`Klikket på trin ${stepIndex + 1}`);
37+
}
38+
},
39+
},
40+
};
41+
42+
export const StepIndicatorWithExtraInfo: Story = {
43+
args: {
44+
steps: [
45+
{
46+
title: "Trin 1",
47+
status: "completed",
48+
stepInformation: "Informationstekst for trin 1",
49+
},
50+
{
51+
title: "Trin 2",
52+
status: "default",
53+
current: true,
54+
stepInformation: "Informationstekst for trin 2",
55+
},
56+
{
57+
title: "Trin 3",
58+
status: "default",
59+
disabled: true,
60+
stepInformation: "Informationstekst for trin 3",
61+
},
62+
],
63+
stepIndicatorId: "step-indicator-with-extra-info",
64+
onStepClick: (stepIndex, disabled) => {
65+
if (disabled) {
66+
alert(`Trin ${stepIndex + 1} er deaktiveret`);
67+
} else {
68+
alert(`Klikket på trin ${stepIndex + 1}`);
69+
}
70+
},
71+
},
72+
};
73+
74+
export const StepIndicatorWithError: Story = {
75+
args: {
76+
steps: [
77+
{
78+
title: "Trin 1",
79+
status: "error",
80+
},
81+
{
82+
title: "Trin 2",
83+
status: "default",
84+
current: true,
85+
},
86+
{
87+
title: "Trin 3",
88+
status: "default",
89+
disabled: true,
90+
},
91+
],
92+
stepIndicatorId: "main-step-indicator",
93+
onStepClick: (stepIndex, disabled) => {
94+
if (disabled) {
95+
alert(`Trin ${stepIndex + 1} er deaktiveret`);
96+
} else {
97+
alert(`Klikket på trin ${stepIndex + 1}`);
98+
}
99+
},
100+
},
101+
};
102+
103+
export const SimpleStepIndicatorExample: Story = {
104+
args: {
105+
steps: [],
106+
stepIndicatorId: "none",
107+
},
108+
render: () => (
109+
<SimpleStepIndicator currentStep={2} stepCount={3}></SimpleStepIndicator>
110+
),
111+
};

src/stories/translations/translations.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ export const defaultTranslationMap: Partial<TranslationMap> = {
3131
table_pagination_ellipsis_sr_label: undefined,
3232
table_pagination_page_selection_next: undefined,
3333
table_pagination_page_selection_last_sr_label: undefined,
34+
simple_step_indicator_current_step_sr_label: undefined,
35+
step_indicator_error_icon_sr_label: undefined,
36+
step_indicator_step_completed_sr_label: undefined,
37+
step_indicator_mobile_indicator_button: undefined,
38+
step_indicator_mobile_modal_heading: undefined,
3439
};
3540

3641
const STORAGE_KEY = "storybook-translations";

0 commit comments

Comments
 (0)