Skip to content

4 add consistent flexbox component #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"trailingComma": "all"
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 121 additions & 0 deletions src/components/layout/Flex/component.tsx
Original file line number Diff line number Diff line change
@@ -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)
*
* <Flex padding={["S"]} /> // like saying padding: tokens.spacing*S;
* <Flex padding={["S", "M"]} /> // like saying padding: `${tokens.spacing*S} ${tokens.spacing*M}`;
* <Flex padding={["S", "M", "L"]} /> // like saying padding: `${tokens.spacing*S} ${tokens.spacing*M} ${tokens.spacing*L}`;
* <Flex padding={["S", "M", "L", "XL"]} /> // 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 (
<div
// spreading of implicit aria props is okay here
// eslint-disable-next-line react/jsx-props-no-spreading
{...ariaProps}
data-testid={testId}
className={mergeClasses(
positionClass,
flexBoxClass,
gapClass,
marginClass,
paddingClass,
dimensionClass,
className,
)}
>
{children}
</div>
);
}
3 changes: 0 additions & 3 deletions src/components/layout/Flex/func.tsx

This file was deleted.

17 changes: 17 additions & 0 deletions src/components/layout/Flex/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
13 changes: 13 additions & 0 deletions src/components/layout/Flex/hooks/useAriaProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type TAriaAttributes = Record<string, string | boolean | undefined>;

export default function useAriaAprops(
props: Record<string, unknown>,
): 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;
}, {});
}
36 changes: 36 additions & 0 deletions src/components/layout/Flex/hooks/useFlexBox.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
9 changes: 9 additions & 0 deletions src/components/layout/Flex/hooks/useGap.ts
Original file line number Diff line number Diff line change
@@ -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;
39 changes: 39 additions & 0 deletions src/components/layout/Flex/hooks/useMargin.ts
Original file line number Diff line number Diff line change
@@ -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;
39 changes: 39 additions & 0 deletions src/components/layout/Flex/hooks/usePadding.ts
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions src/components/layout/Flex/hooks/usePosition.ts
Original file line number Diff line number Diff line change
@@ -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];
}
17 changes: 17 additions & 0 deletions src/components/layout/Flex/hooks/useShorthandDimension.ts
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions src/components/layout/Flex/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Flex from "@components/layout/Flex/component";

export default Flex;
15 changes: 0 additions & 15 deletions src/components/layout/Flex/stories.ts

This file was deleted.

Loading
Loading