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 ( + + + + + + + + ); + }, + 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 ( + + + + + + + + ); +}; + +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";