Skip to content

Commit 3fc0999

Browse files
authored
48 add entirecataloguepartial (#57)
- revamped OptionLayoutTemplate to be more reusable - updated loading / disable logic on OptionConceptsPartial in Storybook and Tests - added EntireCataloguePartial with Storybook and Tests
1 parent a776840 commit 3fc0999

File tree

11 files changed

+303
-83
lines changed

11 files changed

+303
-83
lines changed

apps/frontend/ui/src/navigation/partials/OptionConcepts/func.tsx

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { JSX } from "react";
1+
import type { JSX, Dispatch, SetStateAction } from "react";
22

33
import { MenuList } from "@lib-components";
44

@@ -9,23 +9,34 @@ import { useSelectionState } from "@app-ui/navigation/partials/OptionConcepts/ho
99

1010
type TProps = {
1111
concepts: string[];
12-
onSearch: (value: string) => void;
12+
/** must be initialized with { concept: [] }, only then inner functions apply state correctly */
13+
checkedValuesState: Record<string, string[]>;
14+
setCheckedValuesState: Dispatch<SetStateAction<Record<string, string[]>>>;
15+
onSearch: () => void;
16+
isReqestingConcepts: boolean;
17+
disableButton: boolean;
1318
};
1419

1520
export default function OptionConcepts({
1621
concepts,
1722
onSearch,
23+
isReqestingConcepts,
24+
checkedValuesState,
25+
setCheckedValuesState,
26+
disableButton,
1827
}: TProps): JSX.Element {
1928
const classes = useOptionConceptsClasses();
20-
const { checkedValues, onChange } = useSelectionState();
29+
const { checkedValues, onChange } = useSelectionState(
30+
checkedValuesState,
31+
setCheckedValuesState,
32+
);
2133
return (
2234
<OptionLayoutTemplate
2335
header="Search through recongizable concepts"
24-
subtitle="Choose from the given list below"
25-
onSearch={() => {
26-
onSearch(checkedValues.concept[0]);
27-
}}
28-
disabledSearch={checkedValues.concept.length === 0}
36+
subtitle="Choose one from the given list below"
37+
onClick={onSearch}
38+
disableClick={disableButton}
39+
isLoading={isReqestingConcepts}
2940
>
3041
<MenuList
3142
className={classes.list}

apps/frontend/ui/src/navigation/partials/OptionConcepts/hooks.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { useState } from "react";
1+
import type { SetStateAction, Dispatch } from "react";
22
import type { MenuProps as TMenuProps } from "@fluentui/react-components";
33

4-
function useSelectionState() {
5-
const [checkedValues, setCheckedValues] = useState<Record<string, string[]>>({
6-
concept: [],
7-
});
4+
function useSelectionState(
5+
checkedValues: Record<string, string[]>,
6+
setCheckedValues: Dispatch<SetStateAction<Record<string, string[]>>>,
7+
) {
88
const onChange: TMenuProps["onCheckedValueChange"] = (
99
_,
1010
{ name, checkedItems },

apps/frontend/ui/src/navigation/partials/OptionConcepts/stories.ts

-34
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useState } from "react";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
4+
import OptionConcepts from "@app-ui/navigation/partials/OptionConcepts";
5+
6+
const meta: Meta = {
7+
title: "App/UI/Navigation/Partials/OptionConcepts",
8+
component: OptionConcepts,
9+
};
10+
11+
export default meta;
12+
13+
type Story = StoryObj<typeof OptionConcepts>;
14+
15+
export const Index: Story = {
16+
argTypes: {
17+
concepts: { control: false },
18+
onSearch: { control: false },
19+
checkedValuesState: { control: false },
20+
setCheckedValuesState: { control: false },
21+
},
22+
render: (props) => {
23+
const [checkedValues, setCheckedValues] = useState<
24+
Record<string, string[]>
25+
>({
26+
concept: [],
27+
});
28+
29+
return (
30+
<OptionConcepts
31+
concepts={[
32+
"Partiality",
33+
"Signaling",
34+
"Connectivity",
35+
"Transformativity",
36+
]}
37+
onSearch={() => {}}
38+
isReqestingConcepts={props.isReqestingConcepts}
39+
disableButton={props.disableButton}
40+
checkedValuesState={checkedValues}
41+
setCheckedValuesState={setCheckedValues}
42+
/>
43+
);
44+
},
45+
};
46+
47+
export const FlowWithoutOverflow: Story = {
48+
render: () => {
49+
const [checkedValues, setCheckedValues] = useState<
50+
Record<string, string[]>
51+
>({
52+
concept: [],
53+
});
54+
const [isFetching, setIsFetching] = useState(false);
55+
56+
return (
57+
<OptionConcepts
58+
concepts={[
59+
"Partiality",
60+
"Signaling",
61+
"Connectivity",
62+
"Transformativity",
63+
]}
64+
onSearch={() => {
65+
setIsFetching(true);
66+
setTimeout(() => {
67+
setIsFetching(false);
68+
}, 2000);
69+
}}
70+
isReqestingConcepts={isFetching}
71+
disableButton={checkedValues.concept.length === 0 || isFetching}
72+
checkedValuesState={checkedValues}
73+
setCheckedValuesState={setCheckedValues}
74+
/>
75+
);
76+
},
77+
};
78+
79+
export const FlowWithOverflow: Story = {
80+
render: () => {
81+
const [checkedValues, setCheckedValues] = useState<
82+
Record<string, string[]>
83+
>({
84+
concept: [],
85+
});
86+
const [isFetching, setIsFetching] = useState(false);
87+
88+
return (
89+
<OptionConcepts
90+
concepts={[
91+
"Partiality",
92+
"Signaling",
93+
"Connectivity",
94+
"Transformativity",
95+
"Adaptability",
96+
"Inclusivity",
97+
"Interactivity",
98+
"Reactivity",
99+
"Sustainability",
100+
]}
101+
onSearch={() => {
102+
setIsFetching(true);
103+
setTimeout(() => {
104+
setIsFetching(false);
105+
}, 2000);
106+
}}
107+
isReqestingConcepts={isFetching}
108+
disableButton={checkedValues.concept.length === 0 || isFetching}
109+
checkedValuesState={checkedValues}
110+
setCheckedValuesState={setCheckedValues}
111+
/>
112+
);
113+
},
114+
};
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
1+
import { useState } from "react";
2+
13
import { render, screen, fireEvent } from "@tests-unit-browser";
24
import "@testing-library/jest-dom";
35

46
import OptionConcepts from "@app-ui/navigation/partials/OptionConcepts";
57

8+
function Wrapper() {
9+
const [checkedValues, setCheckedValues] = useState<Record<string, string[]>>({
10+
concept: [],
11+
});
12+
const [isFetching, setIsFetching] = useState(false);
13+
14+
return (
15+
<OptionConcepts
16+
concepts={["concept1", "concept2", "concept3"]}
17+
onSearch={() => {
18+
setIsFetching(true);
19+
setTimeout(() => {
20+
setIsFetching(false);
21+
}, 3000);
22+
}}
23+
isReqestingConcepts={isFetching}
24+
disableButton={checkedValues.concept.length === 0 || isFetching}
25+
checkedValuesState={checkedValues}
26+
setCheckedValuesState={setCheckedValues}
27+
/>
28+
);
29+
}
30+
631
describe("OptionConcepts", () => {
732
it("should render with given concepts", () => {
8-
const onSearch = jest.fn((value: string) => value);
9-
10-
render(
11-
<OptionConcepts
12-
concepts={["concept1", "concept2", "concept3"]}
13-
onSearch={onSearch}
14-
/>,
15-
);
33+
render(<Wrapper />);
1634

1735
const menuList = screen.getByRole("menu");
1836
expect(menuList).toBeInTheDocument();
@@ -26,40 +44,45 @@ describe("OptionConcepts", () => {
2644
});
2745

2846
it("should be able to select different concepts", () => {
29-
const onSearch = jest.fn((value: string) => value);
30-
31-
render(
32-
<OptionConcepts
33-
concepts={["concept1", "concept2", "concept3"]}
34-
onSearch={onSearch}
35-
/>,
36-
);
47+
render(<Wrapper />);
3748

3849
const concept1 = screen.getByText("concept1");
39-
expect(concept1).toBeInTheDocument();
4050
const concept2 = screen.getByText("concept2");
41-
expect(concept2).toBeInTheDocument();
4251
const concept3 = screen.getByText("concept3");
43-
expect(concept3).toBeInTheDocument();
4452

4553
const searchButton = screen.getByRole("button", { name: "Search" });
4654
expect(searchButton).toBeInTheDocument();
4755
expect(searchButton).toBeDisabled();
4856

4957
fireEvent.click(concept1);
58+
// has aria-checked attribute, when checked
59+
expect(concept1.parentElement).toHaveAttribute("aria-checked", "true");
5060
expect(searchButton).toBeEnabled();
5161

52-
fireEvent.click(searchButton);
53-
expect(onSearch).toHaveBeenCalledWith("concept1");
54-
5562
fireEvent.click(concept2);
56-
fireEvent.click(searchButton);
57-
expect(onSearch).toHaveBeenCalledWith("concept2");
63+
expect(concept2.parentElement).toHaveAttribute("aria-checked", "true");
64+
// concept1 should be unchecked -> exclusitivity
65+
expect(concept1.parentElement).toHaveAttribute("aria-checked", "false");
5866

5967
fireEvent.click(concept3);
68+
expect(concept3.parentElement).toHaveAttribute("aria-checked", "true");
69+
expect(concept2.parentElement).toHaveAttribute("aria-checked", "false");
70+
});
71+
72+
it("should show loading state and disable search button", () => {
73+
render(<Wrapper />);
74+
75+
const concept1 = screen.getByText("concept1");
76+
const searchButton = screen.getByRole("button", { name: "Search" });
77+
78+
fireEvent.click(concept1);
79+
expect(searchButton).toBeEnabled();
80+
6081
fireEvent.click(searchButton);
61-
expect(onSearch).toHaveBeenCalledWith("concept3");
82+
expect(searchButton).toBeDisabled();
6283

63-
expect(onSearch).toHaveBeenCalledTimes(3);
84+
// role progressbar is used for loading state
85+
const loading = screen.getByRole("progressbar");
86+
expect(loading).toBeInTheDocument();
6487
});
6588
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { JSX } from "react";
2+
3+
import { OptionLayoutTemplate } from "@app-ui/navigation/templates";
4+
5+
type TProps = {
6+
onRequestCatalogue: () => void;
7+
isRequestingCatalogue: boolean;
8+
disableButton: boolean;
9+
};
10+
11+
export default function OptionEntireCatalogue({
12+
onRequestCatalogue,
13+
isRequestingCatalogue,
14+
disableButton,
15+
}: TProps): JSX.Element {
16+
return (
17+
<OptionLayoutTemplate
18+
header="Fetch the entire catalogue"
19+
onClick={onRequestCatalogue}
20+
disableClick={disableButton}
21+
isLoading={isRequestingCatalogue}
22+
/>
23+
);
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import OptionEntireCatalogue from "./func";
2+
3+
export default OptionEntireCatalogue;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useState } from "react";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
4+
import OptionEntireCatalogue from "./func";
5+
6+
const meta: Meta = {
7+
title: "app/ui/navigation/partials/OptionEntireCatalogue",
8+
component: OptionEntireCatalogue,
9+
args: {},
10+
};
11+
12+
export default meta;
13+
14+
type Story = StoryObj<typeof OptionEntireCatalogue>;
15+
16+
export const Index: Story = {
17+
args: {
18+
isRequestingCatalogue: false,
19+
disableButton: false,
20+
},
21+
};
22+
23+
export const Flow: Story = {
24+
render: () => {
25+
const [isRequestingCatalogue, setIsRequestingCatalogue] = useState(false);
26+
27+
return (
28+
<OptionEntireCatalogue
29+
onRequestCatalogue={() => {
30+
setIsRequestingCatalogue(true);
31+
setTimeout(() => {
32+
setIsRequestingCatalogue(false);
33+
}, 3000);
34+
}}
35+
isRequestingCatalogue={isRequestingCatalogue}
36+
disableButton={isRequestingCatalogue}
37+
/>
38+
);
39+
},
40+
};

0 commit comments

Comments
 (0)