diff --git a/changelog-notes.md b/changelog-notes.md
new file mode 100644
index 0000000..de21b54
--- /dev/null
+++ b/changelog-notes.md
@@ -0,0 +1,3 @@
+- added fit-content option to Flex
+- added role to Flex
+- added ButtonGroup
diff --git a/src/components/index.ts b/src/components/index.ts
index 0d16f39..0f05206 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1 +1,2 @@
+export { ButtonGroup } from "@components/molecules";
export { Flex } from "@components/layout";
diff --git a/src/components/layout/Flex/func.tsx b/src/components/layout/Flex/func.tsx
index e68592e..ee8f9ed 100644
--- a/src/components/layout/Flex/func.tsx
+++ b/src/components/layout/Flex/func.tsx
@@ -28,6 +28,7 @@ import type {
type TProps = {
children: ReactNode;
+ role?: string;
position?: TFlexPosition;
direction?: TFlexDirection;
justifyContent?: TFlexOptionContent;
@@ -124,6 +125,7 @@ export default function Flex({
className = undefined,
testId = undefined,
children,
+ role = undefined,
...rest
}: TProps) {
const flexBoxClass = useFlexBox(
@@ -149,6 +151,7 @@ export default function Flex({
// eslint-disable-next-line react/jsx-props-no-spreading
{...ariaProps}
data-testid={testId}
+ role={role}
className={mergeClasses(
positionClass,
flexBoxClass,
diff --git a/src/components/layout/Flex/styles/dimensions.ts b/src/components/layout/Flex/styles/dimensions.ts
index 356a216..a945d56 100644
--- a/src/components/layout/Flex/styles/dimensions.ts
+++ b/src/components/layout/Flex/styles/dimensions.ts
@@ -4,6 +4,9 @@ const useDimensionClasses = makeStyles({
autoWidth: {
width: "auto",
},
+ fitContentWidth: {
+ width: "fit-content",
+ },
"25%Width": {
width: "25%",
},
@@ -17,6 +20,9 @@ const useDimensionClasses = makeStyles({
width: "100%",
},
+ fitContentHeight: {
+ height: "fit-content",
+ },
autoHeight: {
height: "auto",
},
diff --git a/src/components/layout/Flex/tests.tsx b/src/components/layout/Flex/tests.tsx
index 74a288f..2a5d2b6 100644
--- a/src/components/layout/Flex/tests.tsx
+++ b/src/components/layout/Flex/tests.tsx
@@ -359,10 +359,10 @@ describe("Flex", () => {
const FlexElement = screen.getByText("FlexChild");
expect(FlexElement).toHaveStyle("height: 75%");
});
- it("should render with height auto", () => {
- render(FlexChild);
+ it("should render with height fitContent", () => {
+ render(FlexChild);
const FlexElement = screen.getByText("FlexChild");
- expect(FlexElement).toHaveStyle("height: auto");
+ expect(FlexElement).toHaveStyle("height: fit-content");
});
});
describe("for shWidth", () => {
@@ -386,10 +386,10 @@ describe("Flex", () => {
const FlexElement = screen.getByText("FlexChild");
expect(FlexElement).toHaveStyle("width: 75%");
});
- it("should render with width auto", () => {
- render(FlexChild);
+ it("should render with width fitContent", () => {
+ render(FlexChild);
const FlexElement = screen.getByText("FlexChild");
- expect(FlexElement).toHaveStyle("width: auto");
+ expect(FlexElement).toHaveStyle("width: fit-content");
});
});
describe("for className", () => {
diff --git a/src/components/layout/Flex/types.ts b/src/components/layout/Flex/types.ts
index a5843bf..74d0591 100644
--- a/src/components/layout/Flex/types.ts
+++ b/src/components/layout/Flex/types.ts
@@ -31,7 +31,14 @@ export type TFlexBasis =
| "fitContent"
| "content"
| "0";
-export type TFlexShorthandDimensions = "25%" | "50%" | "75%" | "100%" | "auto";
+
+export type TFlexShorthandDimensions =
+ | "25%"
+ | "50%"
+ | "75%"
+ | "100%"
+ | "auto"
+ | "fitContent";
export type TFlexPosition =
| "-moz-initial"
diff --git a/src/components/molecules/ButtonGroup/func.tsx b/src/components/molecules/ButtonGroup/func.tsx
new file mode 100644
index 0000000..95bca00
--- /dev/null
+++ b/src/components/molecules/ButtonGroup/func.tsx
@@ -0,0 +1,52 @@
+import type { JSX } from "react";
+
+import { Flex } from "@components/layout";
+import useButtonGroupClasses from "@components/molecules/ButtonGroup/styles";
+
+type TProps = {
+ children: JSX.Element[];
+ ariaLabel?: string;
+};
+
+/**
+ * @description
+ * - a very simple wrapper to get started with a button group
+ * - will make sure that the first and last childs borderRadius is preserved
+ * - while the ones in between are normalized and the border is removed, to avoid double borders
+ * - all subsequent logic needs to be handled by the consumer, there are examples in the storybook
+ *
+ * @hints
+ * - make sure the buttons are of the same size
+ * - make sure the buttons on the edges have the same shape
+ * - make sure that when disabling a Button, you use the `disableFosusable` prop, in order to ensure consistent tab-index order
+ * - use appearance="primary" for the currently active button and the rest to default "outline"
+ * - probably use it in a XOR fashion, so that only one button is active at a time
+ *
+ * @accessibility
+ * - in general should not be used for 100% accesibility proof interactions
+ * - as its not very clear how to indicate the current state to screen readers, because the buttons dont have an active state
+ *
+ * @props
+ * - `children`: expects the Button component, at least two
+ * - `ariaLabel`: optional, used for accessibility
+ *
+ * @default
+ * arialLabel = "Button group"
+ *
+ */
+export default function ButtonGroup({
+ children,
+ ariaLabel = "Button group",
+}: TProps): JSX.Element {
+ const classes = useButtonGroupClasses();
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/molecules/ButtonGroup/index.ts b/src/components/molecules/ButtonGroup/index.ts
new file mode 100644
index 0000000..10de8d5
--- /dev/null
+++ b/src/components/molecules/ButtonGroup/index.ts
@@ -0,0 +1,3 @@
+import ButtonGroup from "@components/molecules/ButtonGroup/func";
+
+export default ButtonGroup;
diff --git a/src/components/molecules/ButtonGroup/stories.tsx b/src/components/molecules/ButtonGroup/stories.tsx
new file mode 100644
index 0000000..99e8aa9
--- /dev/null
+++ b/src/components/molecules/ButtonGroup/stories.tsx
@@ -0,0 +1,266 @@
+import { useState } from "react";
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { CalendarMonthRegular } from "@fluentui/react-icons";
+import { Button, Tooltip } from "@fluentui/react-components";
+
+import { Flex } from "@components/layout";
+import ButtonGroup from "@components/molecules/ButtonGroup";
+import useFuiProviderNode from "@hooks/useFuiProviderNode";
+
+const meta: Meta = {
+ title: "Components/Molecules/ButtonGroup",
+ component: ButtonGroup,
+ args: {},
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Index: Story = {
+ render: () => {
+ const [activeOption, setActiveOption] = useState(1);
+ const { fuiProviderNode } = useFuiProviderNode();
+
+ return (
+
+
+
+
+
+ }
+ appearance={activeOption === 4 ? "primary" : "secondary"}
+ onClick={() => {
+ setActiveOption(4);
+ }}
+ />
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: `
+import { useState } from "react";
+import { Button, Tooltip } from "@fluentui/react-components";
+import { CalendarMonthRegular } from "@fluentui/react-icons";
+import ButtonGroup from "@components/molecules/ButtonGroup";
+import useFuiProviderNode from "@hooks/useFuiProviderNode";
+
+const Example = () => {
+ const [activeOption, setActiveOption] = useState(1);
+ const { fuiProviderNode } = useFuiProviderNode();
+
+ return (
+
+
+
+
+
+ }
+ appearance={activeOption === 4 ? "primary" : "secondary"}
+ onClick={() => {
+ setActiveOption(4);
+ }}
+ />
+
+
+
+ );
+};
+
+export default Example;
+ `,
+ },
+ },
+ },
+};
+
+export const Sizes: Story = {
+ render: () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: `
+import { Button } from "@fluentui/react-components";
+import ButtonGroup from "@components/molecules/ButtonGroup";
+import { Flex } from "@components/layout";
+
+const Example = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Example;
+ `,
+ },
+ },
+ },
+};
+
+export const Shapes: Story = {
+ render: () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: `
+import { Button } from "@fluentui/react-components";
+import ButtonGroup from "@components/molecules/ButtonGroup";
+import { Flex } from "@components/layout";
+
+const Example = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Example;
+ `,
+ },
+ },
+ },
+};
diff --git a/src/components/molecules/ButtonGroup/styles.ts b/src/components/molecules/ButtonGroup/styles.ts
new file mode 100644
index 0000000..04656ba
--- /dev/null
+++ b/src/components/molecules/ButtonGroup/styles.ts
@@ -0,0 +1,21 @@
+import { makeStyles } from "@fluentui/react-components";
+
+const useButtonGroupClasses = makeStyles({
+ root: {
+ "& > *:first-child": {
+ borderTopRightRadius: 0,
+ borderBottomRightRadius: 0,
+ },
+ "& > *:not(:first-child):not(:last-child)": {
+ borderRadius: 0,
+ borderLeft: "none",
+ },
+ "& > *:last-child": {
+ borderLeft: "none",
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ },
+ },
+});
+
+export default useButtonGroupClasses;
diff --git a/src/components/molecules/ButtonGroup/tests.tsx b/src/components/molecules/ButtonGroup/tests.tsx
new file mode 100644
index 0000000..0208cae
--- /dev/null
+++ b/src/components/molecules/ButtonGroup/tests.tsx
@@ -0,0 +1,55 @@
+import { render, screen } from "@test-utils";
+import "@testing-library/jest-dom";
+
+import { Button } from "@fluentui/react-components";
+import ButtonGroup from "@components/molecules/ButtonGroup";
+
+describe("ButtonGroup", () => {
+ it("should render with defaults", () => {
+ render(
+
+
+
+ ,
+ );
+ const ButtonGroupElement = screen.getByRole("group");
+ expect(ButtonGroupElement).toBeInTheDocument();
+ expect(ButtonGroupElement).toHaveAttribute("aria-label", "Button group");
+ });
+ it("should render with custom aria label", () => {
+ render(
+
+
+
+ ,
+ );
+ const ButtonGroupElement = screen.getByRole("group");
+ expect(ButtonGroupElement).toBeInTheDocument();
+ expect(ButtonGroupElement).toHaveAttribute(
+ "aria-label",
+ "Custom aria label",
+ );
+ });
+ it("should render the given buttons with appropriate styles", () => {
+ render(
+
+
+
+
+ ,
+ );
+ const ButtonOne = screen.getByText("Button 1");
+ expect(ButtonOne).toHaveStyle("border-radius: var(--borderRadiusMedium)");
+ expect(ButtonOne).toHaveStyle("border-top-right-radius: 0");
+ expect(ButtonOne).toHaveStyle("border-bottom-right-radius: 0");
+
+ const ButtonTwo = screen.getByText("Button 2");
+ expect(ButtonTwo).toHaveStyle("border-radius: 0");
+ expect(ButtonTwo).toHaveStyle("border-left: none");
+
+ const ButtonThree = screen.getByText("Button 3");
+ expect(ButtonThree).toHaveStyle("border-radius: var(--borderRadiusMedium)");
+ expect(ButtonThree).toHaveStyle("border-top-left-radius: 0");
+ expect(ButtonThree).toHaveStyle("border-bottom-left-radius: 0");
+ });
+});
diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts
new file mode 100644
index 0000000..ff9a454
--- /dev/null
+++ b/src/components/molecules/index.ts
@@ -0,0 +1 @@
+export { default as ButtonGroup } from "@components/molecules/ButtonGroup";
diff --git a/src/index.ts b/src/index.ts
index 37f88b8..9244973 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,3 @@
-export { EThemeSpacing, EThemeIconSizes, EThemeDimensions } from "@theme/index";
-export { useFuiProviderNode } from "@hooks/index";
-export { Flex } from "@components/index";
+export { EThemeSpacing, EThemeIconSizes, EThemeDimensions } from "@theme";
+export { useFuiProviderNode } from "@hooks";
+export { Flex, ButtonGroup } from "@components";