diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..bf357fb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "all" +} diff --git a/package-lock.json b/package-lock.json index e9c8abf..9d3d030 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fluentui-helpers", - "version": "0.0.1", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fluentui-helpers", - "version": "0.0.1", + "version": "0.0.3", "license": "MIT", "devDependencies": { "@babel/preset-env": "^7.25.9", diff --git a/src/components/layout/Flex/component.tsx b/src/components/layout/Flex/component.tsx new file mode 100644 index 0000000..dac19ca --- /dev/null +++ b/src/components/layout/Flex/component.tsx @@ -0,0 +1,121 @@ +import React from "react"; +import type { ReactNode } from "react"; + +import { mergeClasses } from "@fluentui/react-components"; +import type { TThemeSpacing, TThemeShorthandSpacing } from "@theme"; + +import { + useGap, + useFlexBox, + useMargin, + usePadding, + useShorthandDimension, + useAriaProps, + usePosition, +} from "@components/layout/Flex/hooks"; + +import type { + TFlexDirection, + TFlexOption, + TFlexShorthandDimensions, + TFlexPosition, +} from "@components/layout/Flex/types"; + +type TProps = { + children: ReactNode; + position?: TFlexPosition; + direction?: TFlexDirection; + justifyContent?: TFlexOption; + alignItems?: TFlexOption; + wrap?: boolean; + className?: string; + gap?: TThemeSpacing; + margin?: TThemeShorthandSpacing; + padding?: TThemeShorthandSpacing; + shWidth?: TFlexShorthandDimensions; + shHeight?: TFlexShorthandDimensions; + testId?: string; +}; + +/** + * @description + * - fluent does not provide a `Flex` component for consistent layout (it was removed in the latest version) + * - having this allows to use fewer makeStyles call and repeting flex configurations in the code + * - its especially usefull when certain layout styles have to be applied conditionally + * - for this the entire conditional logic is abstracted inside this component, providing very much styled-component like ergonomics + * - supports direct data-testid prop as well as all aria props + * + * @props + * - `direction`: flex-direction property + * - `justifyContent`: justify-content property + * - `alignItems`: align-items property + * - `wrap`: flex-wrap property + * - `gap`: gap between children, with fixed predefined values from the design system, not discriminating between horizontal and vertical gap (because there are literally the same values) + * - `margin`: margin property, using the same values like gap, expects the shorthand notation + * - `padding`: same like margin, but for padding, concrete example below + * + * ```jsx + * // the shorthand is not a simple string, but rather defined as an array that can be of size 1 up to 4 + * // each element provides additional restraint from the design system tokens + * // following examples will only use padding, but the same applies to margin + * // the * between tokens.spacing and the further specifier can be interpreted as either horizontal or vertical (both the same values, reasoning is explained in gap comment above) + * + * // like saying padding: tokens.spacing*S; + * // like saying padding: `${tokens.spacing*S} ${tokens.spacing*M}`; + * // like saying padding: `${tokens.spacing*S} ${tokens.spacing*M} ${tokens.spacing*L}`; + * // like saying padding: `${tokens.spacing*S} ${tokens.spacing*M} ${tokens.spacing*L} ${tokens.spacing*XL}`; + * + * ``` + * - `shWidth`: shorthand for width property + * - `shHeight`: shorthand for height property + * - `className`: to add additional classes to the component, will override all specified styles from props + * - `aria-*`: all aria props are supported, they will be spread on the root div + * - `testId`: passed down the data-testid attribute + * + * + * @default + * direction = "row", justifyContent = "start", alignItems = "start", wrap = false, gap = "None", margin = ["None"], padding = ["None"], shHeight = "auto", shWidth = "auto" + */ +export default function Flex({ + direction = "row", + position = "static", + justifyContent = "start", + alignItems = "start", + wrap = false, + gap = "None", + margin = ["None"], + padding = ["None"], + shHeight = "auto", + shWidth = "auto", + className = undefined, + testId = undefined, + children, + ...rest +}: TProps) { + const flexBoxClass = useFlexBox(justifyContent, alignItems, direction, wrap); + const gapClass = useGap(gap); + const marginClass = useMargin(margin); + const paddingClass = usePadding(padding); + const dimensionClass = useShorthandDimension(shWidth, shHeight); + const positionClass = usePosition(position); + const ariaProps = useAriaProps(rest); + return ( +
+ {children} +
+ ); +} diff --git a/src/components/layout/Flex/func.tsx b/src/components/layout/Flex/func.tsx deleted file mode 100644 index c96bcd7..0000000 --- a/src/components/layout/Flex/func.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Flex() { - return
Flex
; -} diff --git a/src/components/layout/Flex/hooks/index.ts b/src/components/layout/Flex/hooks/index.ts new file mode 100644 index 0000000..47af25e --- /dev/null +++ b/src/components/layout/Flex/hooks/index.ts @@ -0,0 +1,17 @@ +import useGap from "@components/layout/Flex/hooks/useGap"; +import useMargin from "@components/layout/Flex/hooks/useMargin"; +import usePadding from "@components/layout/Flex/hooks/usePadding"; +import useFlexBox from "@components/layout/Flex/hooks/useFlexBox"; +import useShorthandDimension from "@components/layout/Flex/hooks/useShorthandDimension"; +import useAriaProps from "@components/layout/Flex/hooks/useAriaProps"; +import usePosition from "@components/layout/Flex/hooks/usePosition"; + +export { + useGap, + useMargin, + usePadding, + useFlexBox, + useShorthandDimension, + useAriaProps, + usePosition, +}; diff --git a/src/components/layout/Flex/hooks/useAriaProps.ts b/src/components/layout/Flex/hooks/useAriaProps.ts new file mode 100644 index 0000000..7e187c8 --- /dev/null +++ b/src/components/layout/Flex/hooks/useAriaProps.ts @@ -0,0 +1,13 @@ +type TAriaAttributes = Record; + +export default function useAriaAprops( + props: Record, +): TAriaAttributes { + return Object.keys(props).reduce((acc, key) => { + if (key.startsWith("aria-")) { + // @ts-expect-error - we know that key is a string, its just difficult to prove to TS by doing key.startsWith + acc[key] = props[key]; + } + return acc; + }, {}); +} diff --git a/src/components/layout/Flex/hooks/useFlexBox.ts b/src/components/layout/Flex/hooks/useFlexBox.ts new file mode 100644 index 0000000..8cebc82 --- /dev/null +++ b/src/components/layout/Flex/hooks/useFlexBox.ts @@ -0,0 +1,36 @@ +import { mergeClasses } from "@fluentui/react-components"; + +import { useFlexBoxClasses } from "@components/layout/Flex/styles"; + +import type { + TFlexDirection, + TFlexOption, +} from "@components/layout/Flex/types"; + +export default function useFlexBox( + justifyContent?: TFlexOption, + alignItems?: TFlexOption, + direction?: TFlexDirection, + wrap?: boolean, +) { + const classes = useFlexBoxClasses(); + const directionClass = direction + ? classes[`${direction}Direction`] + : undefined; + const justifyContentClass = justifyContent + ? classes[`${justifyContent}Content`] + : undefined; + const alignItemsClass = alignItems + ? classes[`${alignItems}Items`] + : undefined; + + const wrapClass = wrap ? classes.wrap : classes.nowrap; + + return mergeClasses( + classes.base, + directionClass, + justifyContentClass, + alignItemsClass, + wrapClass, + ); +} diff --git a/src/components/layout/Flex/hooks/useGap.ts b/src/components/layout/Flex/hooks/useGap.ts new file mode 100644 index 0000000..8809c84 --- /dev/null +++ b/src/components/layout/Flex/hooks/useGap.ts @@ -0,0 +1,9 @@ +import { useGapClasses } from "@components/layout/Flex/styles"; +import type { TThemeSpacing } from "@theme"; + +function useGap(gap?: TThemeSpacing) { + const classes = useGapClasses(); + return gap ? classes[`gap${gap}`] : undefined; +} + +export default useGap; diff --git a/src/components/layout/Flex/hooks/useMargin.ts b/src/components/layout/Flex/hooks/useMargin.ts new file mode 100644 index 0000000..799f764 --- /dev/null +++ b/src/components/layout/Flex/hooks/useMargin.ts @@ -0,0 +1,39 @@ +import { useMarginClasses } from "@components/layout/Flex/styles"; +import { mergeClasses } from "@fluentui/react-components"; + +import type { TThemeShorthandSpacing } from "@theme"; + +function useMargin(margin?: TThemeShorthandSpacing) { + const classes = useMarginClasses(); + + if (margin === undefined) { + return "noMarginValue"; + } + if (margin.length === 1) { + return classes[`margin${margin[0]}`]; + } + if (margin.length === 2) { + return mergeClasses( + classes[`marginTop${margin[0]}`], + classes[`marginRight${margin[1]}`], + classes[`marginBottom${margin[0]}`], + classes[`marginLeft${margin[1]}`], + ); + } + if (margin.length === 3) { + return mergeClasses( + classes[`marginTop${margin[0]}`], + classes[`marginRight${margin[1]}`], + classes[`marginBottom${margin[2]}`], + classes[`marginLeft${margin[1]}`], + ); + } + return mergeClasses( + classes[`marginTop${margin[0]}`], + classes[`marginRight${margin[1]}`], + classes[`marginBottom${margin[2]}`], + classes[`marginLeft${margin[3]}`], + ); +} + +export default useMargin; diff --git a/src/components/layout/Flex/hooks/usePadding.ts b/src/components/layout/Flex/hooks/usePadding.ts new file mode 100644 index 0000000..bbaa4d6 --- /dev/null +++ b/src/components/layout/Flex/hooks/usePadding.ts @@ -0,0 +1,39 @@ +import { usePaddingClasses } from "@components/layout/Flex/styles"; +import { mergeClasses } from "@fluentui/react-components"; + +import type { TThemeShorthandSpacing } from "@theme"; + +function usePadding(padding?: TThemeShorthandSpacing) { + const classes = usePaddingClasses(); + + if (padding === undefined) { + return "noPaddingValue"; + } + if (padding.length === 1) { + return classes[`padding${padding[0]}`]; + } + if (padding.length === 2) { + return mergeClasses( + classes[`paddingTop${padding[0]}`], + classes[`paddingRight${padding[1]}`], + classes[`paddingBottom${padding[0]}`], + classes[`paddingLeft${padding[1]}`], + ); + } + if (padding.length === 3) { + return mergeClasses( + classes[`paddingTop${padding[0]}`], + classes[`paddingRight${padding[1]}`], + classes[`paddingBottom${padding[2]}`], + classes[`paddingLeft${padding[1]}`], + ); + } + return mergeClasses( + classes[`paddingTop${padding[0]}`], + classes[`paddingRight${padding[1]}`], + classes[`paddingBottom${padding[2]}`], + classes[`paddingLeft${padding[3]}`], + ); +} + +export default usePadding; diff --git a/src/components/layout/Flex/hooks/usePosition.ts b/src/components/layout/Flex/hooks/usePosition.ts new file mode 100644 index 0000000..de36b5e --- /dev/null +++ b/src/components/layout/Flex/hooks/usePosition.ts @@ -0,0 +1,8 @@ +import type { TFlexPosition } from "@components/layout/Flex/types"; + +import { usePositionClasses } from "@components/layout/Flex/styles"; + +export default function usePosition(position: TFlexPosition) { + const classes = usePositionClasses(); + return classes[position]; +} diff --git a/src/components/layout/Flex/hooks/useShorthandDimension.ts b/src/components/layout/Flex/hooks/useShorthandDimension.ts new file mode 100644 index 0000000..1ffb935 --- /dev/null +++ b/src/components/layout/Flex/hooks/useShorthandDimension.ts @@ -0,0 +1,17 @@ +import { mergeClasses } from "@fluentui/react-components"; + +import { useDimensionClasses } from "@components/layout/Flex/styles"; + +import type { TFlexShorthandDimensions } from "@components/layout/Flex/types"; + +export default function useShorthandDimension( + shorthandWidth: TFlexShorthandDimensions, + shorthandHeight: TFlexShorthandDimensions, +) { + const classes = useDimensionClasses(); + + const widthClass = classes[`${shorthandWidth}Width`]; + const heightClass = classes[`${shorthandHeight}Height`]; + + return mergeClasses(widthClass, heightClass); +} diff --git a/src/components/layout/Flex/index.ts b/src/components/layout/Flex/index.ts new file mode 100644 index 0000000..e26aa32 --- /dev/null +++ b/src/components/layout/Flex/index.ts @@ -0,0 +1,3 @@ +import Flex from "@components/layout/Flex/component"; + +export default Flex; diff --git a/src/components/layout/Flex/stories.ts b/src/components/layout/Flex/stories.ts deleted file mode 100644 index b31361b..0000000 --- a/src/components/layout/Flex/stories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; - -import Flex from "@components/layout/Flex/func"; - -const meta: Meta = { - title: "Components/Layout/Flex", - component: Flex, - args: {}, -}; - -export default meta; - -type Story = StoryObj; - -export const Index: Story = {}; diff --git a/src/components/layout/Flex/stories.tsx b/src/components/layout/Flex/stories.tsx new file mode 100644 index 0000000..5ec2523 --- /dev/null +++ b/src/components/layout/Flex/stories.tsx @@ -0,0 +1,79 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Flex } from "@components"; +import { makeStyles } from "@fluentui/react-components"; + +import type { TThemeSpacing } from "@theme"; + +const meta: Meta = { + component: Flex, + title: "Components/Layout/Flex", + args: { + children: ( + <> +
+ Child-1 +
+
+ Child-2 +
+ + ), + }, + argTypes: { + className: { control: false }, + children: { control: false }, + gap: { + control: "select", + options: [ + "XXS", + "XS", + "SNudge", + "S", + "MNudge", + "M", + "L", + "XL", + "XXL", + "XXXL", + ] as TThemeSpacing[], + }, + }, +}; + +export default meta; + +type TStory = StoryObj; + +const useClasses = makeStyles({ + root: { + border: "4px solid black", + height: "100px", + }, +}); + +export const Index: TStory = { + args: {}, + render: (args) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const classes = useClasses(); + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {args.children} + + ); + }, +}; diff --git a/src/components/layout/Flex/styles/dimensions.ts b/src/components/layout/Flex/styles/dimensions.ts new file mode 100644 index 0000000..356a216 --- /dev/null +++ b/src/components/layout/Flex/styles/dimensions.ts @@ -0,0 +1,37 @@ +import { makeStyles } from "@fluentui/react-components"; + +const useDimensionClasses = makeStyles({ + autoWidth: { + width: "auto", + }, + "25%Width": { + width: "25%", + }, + "50%Width": { + width: "50%", + }, + "75%Width": { + width: "75%", + }, + "100%Width": { + width: "100%", + }, + + autoHeight: { + height: "auto", + }, + "25%Height": { + height: "25%", + }, + "50%Height": { + height: "50%", + }, + "75%Height": { + height: "75%", + }, + "100%Height": { + height: "100%", + }, +}); + +export default useDimensionClasses; diff --git a/src/components/layout/Flex/styles/flexBox.ts b/src/components/layout/Flex/styles/flexBox.ts new file mode 100644 index 0000000..272f5a3 --- /dev/null +++ b/src/components/layout/Flex/styles/flexBox.ts @@ -0,0 +1,67 @@ +import { makeStyles } from "@fluentui/react-components"; + +const useFlexBoxClasses = makeStyles({ + base: { + display: "flex", + }, + rowDirection: { + flexDirection: "row", + }, + columnDirection: { + flexDirection: "column", + }, + // justify-content + centerContent: { + justifyContent: "center", + }, + startContent: { + justifyContent: "flex-start", + }, + endContent: { + justifyContent: "flex-end", + }, + spaceBetweenContent: { + justifyContent: "space-between", + }, + spaceAroundContent: { + justifyContent: "space-around", + }, + spaceEvenlyContent: { + justifyContent: "space-evenly", + }, + stretchContent: { + justifyContent: "stretch", + }, + // align-items + centerItems: { + alignItems: "center", + }, + startItems: { + alignItems: "flex-start", + }, + endItems: { + alignItems: "flex-end", + }, + stretchItems: { + alignItems: "stretch", + }, + spaceBetweenItems: { + alignItems: "space-between", + }, + spaceAroundItems: { + alignItems: "space-around", + }, + spaceEvenlyItems: { + alignItems: "space-evenly", + }, + + // wrap + wrap: { + flexWrap: "wrap", + }, + nowrap: { + flexWrap: "nowrap", + }, +}); + +export default useFlexBoxClasses; diff --git a/src/components/layout/Flex/styles/gap.ts b/src/components/layout/Flex/styles/gap.ts new file mode 100644 index 0000000..5ddf608 --- /dev/null +++ b/src/components/layout/Flex/styles/gap.ts @@ -0,0 +1,40 @@ +import { makeStyles } from "@fluentui/react-components"; +import { EThemeSpacing } from "@theme"; + +const useGapClasses = makeStyles({ + gapNone: { + gap: EThemeSpacing.None, + }, + gapXXS: { + gap: EThemeSpacing.XXS, + }, + gapXS: { + gap: EThemeSpacing.XS, + }, + gapSNudge: { + gap: EThemeSpacing.SNudge, + }, + gapS: { + gap: EThemeSpacing.S, + }, + gapMNudge: { + gap: EThemeSpacing.MNudge, + }, + gapM: { + gap: EThemeSpacing.M, + }, + gapL: { + gap: EThemeSpacing.L, + }, + gapXL: { + gap: EThemeSpacing.XL, + }, + gapXXL: { + gap: EThemeSpacing.XXL, + }, + gapXXXL: { + gap: EThemeSpacing.XXXL, + }, +}); + +export default useGapClasses; diff --git a/src/components/layout/Flex/styles/index.ts b/src/components/layout/Flex/styles/index.ts new file mode 100644 index 0000000..71d4db6 --- /dev/null +++ b/src/components/layout/Flex/styles/index.ts @@ -0,0 +1,15 @@ +import useFlexBoxClasses from "@components/layout/Flex/styles/flexBox"; +import useMarginClasses from "@components/layout/Flex/styles/margin"; +import usePaddingClasses from "@components/layout/Flex/styles/padding"; +import useGapClasses from "@components/layout/Flex/styles/gap"; +import useDimensionClasses from "@components/layout/Flex/styles/dimensions"; +import usePositionClasses from "@components/layout/Flex/styles/position"; + +export { + useDimensionClasses, + useFlexBoxClasses, + useMarginClasses, + usePaddingClasses, + useGapClasses, + usePositionClasses, +}; diff --git a/src/components/layout/Flex/styles/margin.ts b/src/components/layout/Flex/styles/margin.ts new file mode 100644 index 0000000..98a8943 --- /dev/null +++ b/src/components/layout/Flex/styles/margin.ts @@ -0,0 +1,172 @@ +import { makeStyles } from "@fluentui/react-components"; +import { EThemeSpacing } from "@theme"; + +const useMarginClasses = makeStyles({ + marginNone: { + margin: EThemeSpacing.None, + }, + marginXXS: { + margin: EThemeSpacing.XXS, + }, + marginXS: { + margin: EThemeSpacing.XS, + }, + marginSNudge: { + margin: EThemeSpacing.SNudge, + }, + marginS: { + margin: EThemeSpacing.S, + }, + marginMNudge: { + margin: EThemeSpacing.MNudge, + }, + marginM: { + margin: EThemeSpacing.M, + }, + marginL: { + margin: EThemeSpacing.L, + }, + marginXL: { + margin: EThemeSpacing.XL, + }, + marginXXL: { + margin: EThemeSpacing.XXL, + }, + marginXXXL: { + margin: EThemeSpacing.XXXL, + }, + marginLeftNone: { + marginLeft: EThemeSpacing.None, + }, + marginLeftXXS: { + marginLeft: EThemeSpacing.XXS, + }, + marginLeftXS: { + marginLeft: EThemeSpacing.XS, + }, + marginLeftSNudge: { + marginLeft: EThemeSpacing.SNudge, + }, + marginLeftS: { + marginLeft: EThemeSpacing.S, + }, + marginLeftMNudge: { + marginLeft: EThemeSpacing.MNudge, + }, + marginLeftM: { + marginLeft: EThemeSpacing.M, + }, + marginLeftL: { + marginLeft: EThemeSpacing.L, + }, + marginLeftXL: { + marginLeft: EThemeSpacing.XL, + }, + marginLeftXXL: { + marginLeft: EThemeSpacing.XXL, + }, + marginLeftXXXL: { + marginLeft: EThemeSpacing.XXXL, + }, + marginRightNone: { + marginRight: EThemeSpacing.None, + }, + marginRightXXS: { + marginRight: EThemeSpacing.XXS, + }, + marginRightXS: { + marginRight: EThemeSpacing.XS, + }, + marginRightSNudge: { + marginRight: EThemeSpacing.SNudge, + }, + marginRightS: { + marginRight: EThemeSpacing.S, + }, + marginRightMNudge: { + marginRight: EThemeSpacing.MNudge, + }, + marginRightM: { + marginRight: EThemeSpacing.M, + }, + marginRightL: { + marginRight: EThemeSpacing.L, + }, + marginRightXL: { + marginRight: EThemeSpacing.XL, + }, + marginRightXXL: { + marginRight: EThemeSpacing.XXL, + }, + marginRightXXXL: { + marginRight: EThemeSpacing.XXXL, + }, + marginTopNone: { + marginTop: EThemeSpacing.None, + }, + marginTopXXS: { + marginTop: EThemeSpacing.XXS, + }, + marginTopXS: { + marginTop: EThemeSpacing.XS, + }, + marginTopSNudge: { + marginTop: EThemeSpacing.SNudge, + }, + marginTopS: { + marginTop: EThemeSpacing.S, + }, + marginTopMNudge: { + marginTop: EThemeSpacing.MNudge, + }, + marginTopM: { + marginTop: EThemeSpacing.M, + }, + marginTopL: { + marginTop: EThemeSpacing.L, + }, + marginTopXL: { + marginTop: EThemeSpacing.XL, + }, + marginTopXXL: { + marginTop: EThemeSpacing.XXL, + }, + marginTopXXXL: { + marginTop: EThemeSpacing.XXXL, + }, + marginBottomNone: { + marginBottom: EThemeSpacing.None, + }, + marginBottomXXS: { + marginBottom: EThemeSpacing.XXS, + }, + marginBottomXS: { + marginBottom: EThemeSpacing.XS, + }, + marginBottomSNudge: { + marginBottom: EThemeSpacing.SNudge, + }, + marginBottomS: { + marginBottom: EThemeSpacing.S, + }, + marginBottomMNudge: { + marginBottom: EThemeSpacing.MNudge, + }, + marginBottomM: { + marginBottom: EThemeSpacing.M, + }, + marginBottomL: { + marginBottom: EThemeSpacing.L, + }, + marginBottomXL: { + marginBottom: EThemeSpacing.XL, + }, + marginBottomXXL: { + marginBottom: EThemeSpacing.XXL, + }, + marginBottomXXXL: { + marginBottom: EThemeSpacing.XXXL, + }, +}); + +export default useMarginClasses; diff --git a/src/components/layout/Flex/styles/padding.ts b/src/components/layout/Flex/styles/padding.ts new file mode 100644 index 0000000..a7f073e --- /dev/null +++ b/src/components/layout/Flex/styles/padding.ts @@ -0,0 +1,172 @@ +import { makeStyles } from "@fluentui/react-components"; +import { EThemeSpacing } from "@theme"; + +const usePaddingClasses = makeStyles({ + paddingNone: { + padding: EThemeSpacing.None, + }, + paddingXXS: { + padding: EThemeSpacing.XXS, + }, + paddingXS: { + padding: EThemeSpacing.XS, + }, + paddingSNudge: { + padding: EThemeSpacing.SNudge, + }, + paddingS: { + padding: EThemeSpacing.S, + }, + paddingMNudge: { + padding: EThemeSpacing.MNudge, + }, + paddingM: { + padding: EThemeSpacing.M, + }, + paddingL: { + padding: EThemeSpacing.L, + }, + paddingXL: { + padding: EThemeSpacing.XL, + }, + paddingXXL: { + padding: EThemeSpacing.XXL, + }, + paddingXXXL: { + padding: EThemeSpacing.XXXL, + }, + paddingLeftNone: { + paddingLeft: EThemeSpacing.None, + }, + paddingLeftXXS: { + paddingLeft: EThemeSpacing.XXS, + }, + paddingLeftXS: { + paddingLeft: EThemeSpacing.XS, + }, + paddingLeftSNudge: { + paddingLeft: EThemeSpacing.SNudge, + }, + paddingLeftS: { + paddingLeft: EThemeSpacing.S, + }, + paddingLeftMNudge: { + paddingLeft: EThemeSpacing.MNudge, + }, + paddingLeftM: { + paddingLeft: EThemeSpacing.M, + }, + paddingLeftL: { + paddingLeft: EThemeSpacing.L, + }, + paddingLeftXL: { + paddingLeft: EThemeSpacing.XL, + }, + paddingLeftXXL: { + paddingLeft: EThemeSpacing.XXL, + }, + paddingLeftXXXL: { + paddingLeft: EThemeSpacing.XXXL, + }, + paddingRightNone: { + paddingRight: EThemeSpacing.None, + }, + paddingRightXXS: { + paddingRight: EThemeSpacing.XXS, + }, + paddingRightXS: { + paddingRight: EThemeSpacing.XS, + }, + paddingRightSNudge: { + paddingRight: EThemeSpacing.SNudge, + }, + paddingRightS: { + paddingRight: EThemeSpacing.S, + }, + paddingRightMNudge: { + paddingRight: EThemeSpacing.MNudge, + }, + paddingRightM: { + paddingRight: EThemeSpacing.M, + }, + paddingRightL: { + paddingRight: EThemeSpacing.L, + }, + paddingRightXL: { + paddingRight: EThemeSpacing.XL, + }, + paddingRightXXL: { + paddingRight: EThemeSpacing.XXL, + }, + paddingRightXXXL: { + paddingRight: EThemeSpacing.XXXL, + }, + paddingTopNone: { + paddingTop: EThemeSpacing.None, + }, + paddingTopXXS: { + paddingTop: EThemeSpacing.XXS, + }, + paddingTopXS: { + paddingTop: EThemeSpacing.XS, + }, + paddingTopSNudge: { + paddingTop: EThemeSpacing.SNudge, + }, + paddingTopS: { + paddingTop: EThemeSpacing.S, + }, + paddingTopMNudge: { + paddingTop: EThemeSpacing.MNudge, + }, + paddingTopM: { + paddingTop: EThemeSpacing.M, + }, + paddingTopL: { + paddingTop: EThemeSpacing.L, + }, + paddingTopXL: { + paddingTop: EThemeSpacing.XL, + }, + paddingTopXXL: { + paddingTop: EThemeSpacing.XXL, + }, + paddingTopXXXL: { + paddingTop: EThemeSpacing.XXXL, + }, + paddingBottomNone: { + paddingBottom: EThemeSpacing.None, + }, + paddingBottomXXS: { + paddingBottom: EThemeSpacing.XXS, + }, + paddingBottomXS: { + paddingBottom: EThemeSpacing.XS, + }, + paddingBottomSNudge: { + paddingBottom: EThemeSpacing.SNudge, + }, + paddingBottomS: { + paddingBottom: EThemeSpacing.S, + }, + paddingBottomMNudge: { + paddingBottom: EThemeSpacing.MNudge, + }, + paddingBottomM: { + paddingBottom: EThemeSpacing.M, + }, + paddingBottomL: { + paddingBottom: EThemeSpacing.L, + }, + paddingBottomXL: { + paddingBottom: EThemeSpacing.XL, + }, + paddingBottomXXL: { + paddingBottom: EThemeSpacing.XXL, + }, + paddingBottomXXXL: { + paddingBottom: EThemeSpacing.XXXL, + }, +}); + +export default usePaddingClasses; diff --git a/src/components/layout/Flex/styles/position.ts b/src/components/layout/Flex/styles/position.ts new file mode 100644 index 0000000..9bc1e7e --- /dev/null +++ b/src/components/layout/Flex/styles/position.ts @@ -0,0 +1,42 @@ +import { makeStyles } from "@fluentui/react-components"; + +const usePositionClasses = makeStyles({ + "-moz-initial": { + position: "-moz-initial", + }, + "-webkit-sticky": { + position: "-webkit-sticky", + }, + absolute: { + position: "absolute", + }, + fixed: { + position: "fixed", + }, + inherit: { + position: "inherit", + }, + initial: { + position: "initial", + }, + relative: { + position: "relative", + }, + revert: { + position: "revert", + }, + "revert-layer": { + position: "revert-layer", + }, + static: { + position: "static", + }, + sticky: { + position: "sticky", + }, + unset: { + position: "unset", + }, +}); + +export default usePositionClasses; diff --git a/src/components/layout/Flex/tests.tsx b/src/components/layout/Flex/tests.tsx index 56ae881..0be08c1 100644 --- a/src/components/layout/Flex/tests.tsx +++ b/src/components/layout/Flex/tests.tsx @@ -1,8 +1,388 @@ -// import { render, screen } from '@test-utils'; +import { render, screen } from "@test-utils"; import "@testing-library/jest-dom"; +import { makeStyles } from "@fluentui/react-components"; +import { Flex } from "@components"; + describe("Flex", () => { - it("should render", () => { - expect(true).toBe(true); + it("should render without required props default config", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + + expect(FlexElement).toBeInTheDocument(); + expect(FlexElement).toHaveTextContent("FlexChild"); + expect(FlexElement).toHaveStyle("display: flex"); + expect(FlexElement).toHaveStyle("flex-direction: row"); + expect(FlexElement).toHaveStyle("justify-content: flex-start"); + expect(FlexElement).toHaveStyle("align-items: flex-start"); + expect(FlexElement).toHaveStyle("flex-wrap: nowrap"); + expect(FlexElement).toHaveStyle("gap: 0rem"); + expect(FlexElement).toHaveStyle("margin: 0rem"); + expect(FlexElement).toHaveStyle("padding: 0rem"); + expect(FlexElement).toHaveStyle("height: auto"); + expect(FlexElement).toHaveStyle("width: auto"); + expect(FlexElement).toHaveStyle("position: static"); + }); + describe("when props are passed", () => { + describe("for direction", () => { + it("should render with direction column", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("flex-direction: column"); + }); + it("should render with direction row", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("flex-direction: row"); + }); + }); + describe("for justifyContent", () => { + it("should render with justifyContent center", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: center"); + }); + it("should render with justifyContent end", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: flex-end"); + }); + it("should render with justifyContent spaceBetween", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: space-between"); + }); + it("should render with justifyContent spaceAround", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: space-around"); + }); + it("should render with justifyContent spaceEvenly", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: space-evenly"); + }); + it("should render with justifyContent stretch", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("justify-content: stretch"); + }); + }); + describe("for alignItems", () => { + it("should render with alignItems center", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: center"); + }); + it("should render with alignItems end", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: flex-end"); + }); + it("should render with alignItems spaceBetween", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: space-between"); + }); + it("should render with alignItems spaceAround", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: space-around"); + }); + it("should render with alignItems spaceEvenly", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: space-evenly"); + }); + it("should render with alignItems stretch", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("align-items: stretch"); + }); + }); + describe("for wrap", () => { + it("should render with wrap wrap", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("flex-wrap: wrap"); + }); + it("should render with wrap nowrap", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("flex-wrap: nowrap"); + }); + }); + describe("for gap", () => { + it("should render with gap None", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0rem"); + }); + it("should render with gap XXS", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.125rem"); + }); + it("should render with gap XS", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.25rem"); + }); + it("should render with gap SNudge", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.375rem"); + }); + it("should render with gap S", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.5rem"); + }); + it("should render with gap MNudge", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.625rem"); + }); + it("should render with gap M", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 0.75rem"); + }); + it("should render with gap L", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 1rem"); + }); + it("should render with gap XL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 1.25rem"); + }); + it("should render with gap XXL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 1.5rem"); + }); + it("should render with gap XXXL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("gap: 2rem"); + }); + }); + describe("for margin", () => { + it("should render with margin None", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("margin: 0rem"); + }); + it("should render with margin XS, L", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("margin: 0.25rem 1rem 0.25rem 1rem"); + }); + it("should render with margin XS, L, XL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("margin: 0.25rem 1rem 1.25rem 1rem"); + }); + it("should render with margin XS, L, XL, XXL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("margin: 0.25rem 1rem 1.25rem 1.5rem"); + }); + }); + describe("for padding", () => { + it("should render with padding None", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("padding: 0rem"); + }); + it("should render with padding XS, L", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("padding: 0.25rem 1rem 0.25rem 1rem"); + }); + it("should render with padding XS, L, XL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("padding: 0.25rem 1rem 1.25rem 1rem"); + }); + it("should render with padding XS, L, XL, XXL", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("padding: 0.25rem 1rem 1.25rem 1.5rem"); + }); + }); + describe("for shHeight", () => { + it("should render with height 100%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 100%"); + }); + it("should render with height 25%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 25%"); + }); + it("should render with height 50%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 50%"); + }); + it("should render with height 75%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 75%"); + }); + it("should render with height auto", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: auto"); + }); + }); + describe("for shWidth", () => { + it("should render with width 100%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: 100%"); + }); + it("should render with width 25%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: 25%"); + }); + it("should render with width 50%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: 50%"); + }); + it("should render with width 75%", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: 75%"); + }); + it("should render with width auto", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("width: auto"); + }); + }); + describe("for className", () => { + it("should render with the properties applied from the class and override any other props styles", () => { + const useClasses = makeStyles({ + testClass: { + height: "100px", + border: "1px solid black", + }, + }); + + function Wrapper() { + return FlexChild; + } + + render(); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("height: 100px"); + expect(FlexElement).toHaveStyle("border: 1px solid black"); + }); + }); + describe("for testId", () => { + it("should render with data-testid", () => { + render(FlexChild); + const FlexElement = screen.getByTestId("testId"); + expect(FlexElement).toBeInTheDocument(); + }); + }); + describe("for aria-* attributes (only checks some, cannot be exhaustive)", () => { + // not exhaustive, but at least one example of each type + const ariaAttributes = [ + { name: "aria-label", value: "test-label" }, + { name: "aria-labelledby", value: "test-labelledby" }, + { name: "aria-describedby", value: "test-describedby" }, + { name: "aria-hidden", value: true }, + { name: "aria-placeholder", value: "test-placeholder" }, + { name: "aria-expanded", value: "true" }, + { name: "aria-controls", value: "test-controls" }, + { name: "aria-pressed", value: "true" }, + { name: "aria-current", value: "page" }, + { name: "aria-invalid", value: "true" }, + { name: "aria-busy", value: "true" }, + { name: "aria-readonly", value: "true" }, + { name: "aria-required", value: "true" }, + { name: "aria-modal", value: "true" }, + { name: "aria-orientation", value: "horizontal" }, + { name: "aria-valuemin", value: "0" }, + { name: "aria-valuemax", value: "100" }, + { name: "aria-valuenow", value: "50" }, + { name: "aria-valuetext", value: "50%" }, + ]; + + ariaAttributes.forEach(({ name, value }) => { + it(`should render with ${name}`, () => { + const props = { [name]: value }; + // eslint-disable-next-line react/jsx-props-no-spreading + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + if (value === true) { + // For boolean attributes like aria-hidden + expect(FlexElement).toHaveAttribute(name); + } else { + expect(FlexElement).toHaveAttribute(name, value.toString()); + } + }); + }); + }); + describe("for position", () => { + it("should render with position -moz-initial", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: -moz-initial"); + }); + it("should render with position -webkit-sticky", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: -webkit-sticky"); + }); + it("should render with position absolute", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: absolute"); + }); + it("should render with position fixed", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: fixed"); + }); + it("should render with position inherit", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: inherit"); + }); + it("should render with position initial", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: initial"); + }); + it("should render with position relative", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: relative"); + }); + it("should render with position revert", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: revert"); + }); + it("should render with position revert-layer", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: revert-layer"); + }); + it("should render with position static", () => { + render(FlexChild); + const FlexElement = screen.getByText("FlexChild"); + expect(FlexElement).toHaveStyle("position: static"); + }); + }); }); }); diff --git a/src/components/layout/Flex/types.ts b/src/components/layout/Flex/types.ts new file mode 100644 index 0000000..14334d5 --- /dev/null +++ b/src/components/layout/Flex/types.ts @@ -0,0 +1,28 @@ +export type TFlexDirection = "row" | "column"; + +export type TFlexOption = + | "start" + | "center" + | "end" + | "spaceBetween" + | "spaceAround" + | "spaceEvenly" + | "stretch"; + +export type TFlexWrap = "wrap" | "nowrap"; + +export type TFlexShorthandDimensions = "25%" | "50%" | "75%" | "100%" | "auto"; + +export type TFlexPosition = + | "-moz-initial" + | "-webkit-sticky" + | "absolute" + | "fixed" + | "inherit" + | "initial" + | "relative" + | "revert" + | "revert-layer" + | "static" + | "sticky" + | "unset"; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index 15ebd2e..35f4da9 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -1 +1 @@ -export { default as Flex } from "@components/layout/Flex/func"; +export { default as Flex } from "@components/layout/Flex"; diff --git a/src/theme/index.ts b/src/theme/index.ts index bb3a547..5a826f8 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -5,3 +5,5 @@ export { EThemeIconSizes, EThemeDimensions, } from "@theme/tokens"; + +export type { TThemeSpacing, TThemeShorthandSpacing } from "@theme/types"; diff --git a/src/theme/types.ts b/src/theme/types.ts new file mode 100644 index 0000000..095897c --- /dev/null +++ b/src/theme/types.ts @@ -0,0 +1,7 @@ +import type { EThemeSpacing } from "@theme/tokens"; + +export type TThemeSpacing = keyof typeof EThemeSpacing; + +type TThemeShorthand = [T] | [T, T] | [T, T, T] | [T, T, T, T]; + +export type TThemeShorthandSpacing = TThemeShorthand; diff --git a/tsconfig.json b/tsconfig.json index b0bbe77..18e8b91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,10 +24,12 @@ // Aliases "baseUrl": ".", "paths": { - "@animations/*": ["src/animations/*"], "@components/*": ["src/components/*"], + "@components": ["src/components/index.ts"], "@hooks/*": ["src/hooks/*"], + "@hooks": ["src/hooks/index.ts"], "@theme/*": ["src/theme/*"], + "@theme": ["src/theme/index.ts"], "@test-utils": ["tests/react-testing-library.tsx"] } },