diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/func.tsx b/apps/frontend/ui/src/navigation/partials/OptionConcepts/func.tsx new file mode 100644 index 0000000..499db14 --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/func.tsx @@ -0,0 +1,41 @@ +import type { JSX } from "react"; + +import { MenuList } from "@lib-components"; + +import { OptionLayoutTemplate } from "@app-ui/navigation/templates"; +import useOptionConceptsClasses from "@app-ui/navigation/partials/OptionConcepts/styles"; +import { MenuItemRadioTemplate } from "@app-ui/navigation/partials/OptionConcepts/template"; +import { useSelectionState } from "@app-ui/navigation/partials/OptionConcepts/hooks"; + +type TProps = { + concepts: string[]; + onSearch: (value: string) => void; +}; + +export default function OptionConcepts({ + concepts, + onSearch, +}: TProps): JSX.Element { + const classes = useOptionConceptsClasses(); + const { checkedValues, onChange } = useSelectionState(); + return ( + { + onSearch(checkedValues.concept[0]); + }} + disabledSearch={checkedValues.concept.length === 0} + > + + {concepts.map((concept) => ( + + ))} + + + ); +} diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/hooks.ts b/apps/frontend/ui/src/navigation/partials/OptionConcepts/hooks.ts new file mode 100644 index 0000000..3d0dd7a --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/hooks.ts @@ -0,0 +1,18 @@ +import { useState } from "react"; +import type { MenuProps as TMenuProps } from "@fluentui/react-components"; + +function useSelectionState() { + const [checkedValues, setCheckedValues] = useState>({ + concept: [], + }); + const onChange: TMenuProps["onCheckedValueChange"] = ( + _, + { name, checkedItems }, + ) => { + setCheckedValues((s) => ({ ...s, [name]: checkedItems })); + }; + + return { checkedValues, onChange }; +} + +export { useSelectionState }; diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/index.ts b/apps/frontend/ui/src/navigation/partials/OptionConcepts/index.ts new file mode 100644 index 0000000..602f93e --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/index.ts @@ -0,0 +1,3 @@ +import OptionConcepts from "@app-ui/navigation/partials/OptionConcepts/func"; + +export default OptionConcepts; diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/stories.ts b/apps/frontend/ui/src/navigation/partials/OptionConcepts/stories.ts new file mode 100644 index 0000000..28cd5bd --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/stories.ts @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import OptionConcepts from "@app-ui/navigation/partials/OptionConcepts"; + +const meta: Meta = { + title: "App/UI/Navigation/Partials/OptionConcepts", + component: OptionConcepts, + args: { + concepts: ["Partiality", "Signaling", "Connectivity", "Transformativity"], + onSearch: () => {}, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Index: Story = {}; + +export const WithOverflow: Story = { + args: { + concepts: [ + "Partiality", + "Signaling", + "Connectivity", + "Transformativity", + "Adaptability", + "Inclusivity", + "Interactivity", + "Reactivity", + "Sustainability", + ], + }, +}; diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/styles.ts b/apps/frontend/ui/src/navigation/partials/OptionConcepts/styles.ts new file mode 100644 index 0000000..bc7f86e --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/styles.ts @@ -0,0 +1,13 @@ +import { makeStyles, EThemeDimensions, tokens } from "@lib-theme"; + +const useOptionConceptsClasses = makeStyles({ + list: { + width: "100%", + height: EThemeDimensions.M4, + overflow: "auto", + backgroundColor: tokens.colorNeutralBackground1, + borderRadius: tokens.borderRadiusMedium, + }, +}); + +export default useOptionConceptsClasses; diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/MenuItemRadio/func.tsx b/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/MenuItemRadio/func.tsx new file mode 100644 index 0000000..be548fc --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/MenuItemRadio/func.tsx @@ -0,0 +1,15 @@ +import type { JSX } from "react"; + +import { MenuItemRadio as MenuItemRadioOrigin } from "@lib-components"; + +type TProps = { + value: string; +}; + +export default function MenuItemRadio({ value }: TProps): JSX.Element { + return ( + + {value} + + ); +} diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/MenuItemRadio/index.ts b/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/MenuItemRadio/index.ts new file mode 100644 index 0000000..40f575b --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/MenuItemRadio/index.ts @@ -0,0 +1,3 @@ +import MenuItemRadio from "@app-ui/navigation/partials/OptionConcepts/template/MenuItemRadio/func"; + +export default MenuItemRadio; diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/index.ts b/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/index.ts new file mode 100644 index 0000000..9e7ebbd --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/template/index.ts @@ -0,0 +1 @@ +export { default as MenuItemRadioTemplate } from "@app-ui/navigation/partials/OptionConcepts/template/MenuItemRadio"; diff --git a/apps/frontend/ui/src/navigation/partials/OptionConcepts/tests.tsx b/apps/frontend/ui/src/navigation/partials/OptionConcepts/tests.tsx new file mode 100644 index 0000000..6257313 --- /dev/null +++ b/apps/frontend/ui/src/navigation/partials/OptionConcepts/tests.tsx @@ -0,0 +1,65 @@ +import { render, screen, fireEvent } from "@tests-unit-browser"; +import "@testing-library/jest-dom"; + +import OptionConcepts from "@app-ui/navigation/partials/OptionConcepts"; + +describe("OptionConcepts", () => { + it("should render with given concepts", () => { + const onSearch = jest.fn((value: string) => value); + + render( + , + ); + + const menuList = screen.getByRole("menu"); + expect(menuList).toBeInTheDocument(); + + const concept1 = screen.getByText("concept1"); + expect(concept1).toBeInTheDocument(); + const concept2 = screen.getByText("concept2"); + expect(concept2).toBeInTheDocument(); + const concept3 = screen.getByText("concept3"); + expect(concept3).toBeInTheDocument(); + }); + + it("should be able to select different concepts", () => { + const onSearch = jest.fn((value: string) => value); + + render( + , + ); + + const concept1 = screen.getByText("concept1"); + expect(concept1).toBeInTheDocument(); + const concept2 = screen.getByText("concept2"); + expect(concept2).toBeInTheDocument(); + const concept3 = screen.getByText("concept3"); + expect(concept3).toBeInTheDocument(); + + const searchButton = screen.getByRole("button", { name: "Search" }); + expect(searchButton).toBeInTheDocument(); + expect(searchButton).toBeDisabled(); + + fireEvent.click(concept1); + expect(searchButton).toBeEnabled(); + + fireEvent.click(searchButton); + expect(onSearch).toHaveBeenCalledWith("concept1"); + + fireEvent.click(concept2); + fireEvent.click(searchButton); + expect(onSearch).toHaveBeenCalledWith("concept2"); + + fireEvent.click(concept3); + fireEvent.click(searchButton); + expect(onSearch).toHaveBeenCalledWith("concept3"); + + expect(onSearch).toHaveBeenCalledTimes(3); + }); +}); diff --git a/apps/frontend/ui/src/navigation/templates/OptionLayout/func.tsx b/apps/frontend/ui/src/navigation/templates/OptionLayout/func.tsx new file mode 100644 index 0000000..2ed218b --- /dev/null +++ b/apps/frontend/ui/src/navigation/templates/OptionLayout/func.tsx @@ -0,0 +1,36 @@ +import type { JSX, ReactNode } from "react"; + +import { Flex, Button } from "@lib-components"; +import { Subtitle2, Caption2 } from "@lib-theme"; + +import useOptionLayoutClasses from "@app-ui/navigation/templates/OptionLayout/styles"; + +type TProps = { + header: string; + subtitle: string; + children?: ReactNode; + onSearch: () => void; + disabledSearch: boolean; +}; + +export default function OptionLayout({ + header, + subtitle, + children = undefined, + onSearch, + disabledSearch, +}: TProps): JSX.Element { + const classes = useOptionLayoutClasses(); + return ( + + + {header} + {subtitle} + + {children} + + + ); +} diff --git a/apps/frontend/ui/src/navigation/templates/OptionLayout/index.ts b/apps/frontend/ui/src/navigation/templates/OptionLayout/index.ts new file mode 100644 index 0000000..22f5e55 --- /dev/null +++ b/apps/frontend/ui/src/navigation/templates/OptionLayout/index.ts @@ -0,0 +1,3 @@ +import OptionLayout from "@app-ui/navigation/templates/OptionLayout/func"; + +export default OptionLayout; diff --git a/apps/frontend/ui/src/navigation/templates/OptionLayout/styles.ts b/apps/frontend/ui/src/navigation/templates/OptionLayout/styles.ts new file mode 100644 index 0000000..dcca2f4 --- /dev/null +++ b/apps/frontend/ui/src/navigation/templates/OptionLayout/styles.ts @@ -0,0 +1,11 @@ +import { makeStyles, tokens, EThemeDimensions } from "@lib-theme"; + +const useOptionLayoutClasses = makeStyles({ + root: { + width: EThemeDimensions.L8, + backgroundColor: tokens.colorNeutralBackground2, + borderRadius: tokens.borderRadiusLarge, + }, +}); + +export default useOptionLayoutClasses; diff --git a/apps/frontend/ui/src/navigation/templates/index.ts b/apps/frontend/ui/src/navigation/templates/index.ts new file mode 100644 index 0000000..0773614 --- /dev/null +++ b/apps/frontend/ui/src/navigation/templates/index.ts @@ -0,0 +1 @@ +export { default as OptionLayoutTemplate } from "@app-ui/navigation/templates/OptionLayout"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 442c269..295896e 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -2,6 +2,8 @@ export { Divider } from "@fluentui/react-components"; export { Tab } from "@fluentui/react-components"; export { TabList } from "@fluentui/react-components"; export { Button } from "@fluentui/react-components"; +export { MenuList } from "@fluentui/react-components"; +export { MenuItemRadio } from "@fluentui/react-components"; export { Collapse } from "@fluentui/react-motion-components-preview"; diff --git a/package-lock.json b/package-lock.json index cb1af74..c84b38e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@fluentui/react-motion-components-preview": "^0.3.1", "@griffel/react": "^1.5.25", "@reduxjs/toolkit": "^2.2.8", - "fluentui-helpers": "0.2.1", + "fluentui-helpers": "0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.1.2" @@ -9951,9 +9951,9 @@ "dev": true }, "node_modules/fluentui-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fluentui-helpers/-/fluentui-helpers-0.2.1.tgz", - "integrity": "sha512-yVTr0yz2ZQLwGX3bgph1pOt4zP7W3b30iNEyNFlWyOPuqUlxMl2JDEGeg/8QOcObsyPPY2D27svFoxz7QT4aag==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/fluentui-helpers/-/fluentui-helpers-0.3.0.tgz", + "integrity": "sha512-syhmFb/HbGzCL9T/dkeHbQ3CTeeBJuJfgitfK2Jxj7T+JseLztIZDgwdLmakJusqkbBLXdEuTO6KHhHyI1ociQ==", "license": "MIT", "peerDependencies": { "@fluentui/react-components": "^9", diff --git a/package.json b/package.json index 84ac324..7deff09 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@fluentui/react-motion-components-preview": "^0.3.1", "@griffel/react": "^1.5.25", "@reduxjs/toolkit": "^2.2.8", - "fluentui-helpers": "0.2.1", + "fluentui-helpers": "0.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-redux": "^9.1.2"