Skip to content

Adding Flex utils, extended tokens, useFuiProviderNode #20

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 22 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: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"eamodio.gitlens",
"wayou.vscode-todo-highlight",
"PKief.material-icon-theme",
"yoavbls.pretty-ts-errors"
"yoavbls.pretty-ts-errors",
"unifiedjs.vscode-mdx"
]
}
},
Expand Down
2 changes: 0 additions & 2 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ extends:
- "plugin:jest/style"
# -- STORYBOOK --
# NOTE: Quote from docs: "This plugin will only be applied to files following the *.stories.* (we recommend this) or *.story.* pattern."
# Its recommended to lint also the config files in .storybook, which in our current file context, is painfull to achieve, so this is emmited.
# It would not have made any major difference anyhow, can only help with mistyped addon names, we will survive without it...
- "plugin:storybook/recommended"

# --------------------------- Formatting -------------------------
Expand Down
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"trailingComma": "all"
}
2 changes: 1 addition & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StorybookConfig } from "@storybook/react-vite";

const config: StorybookConfig = {
stories: ["../**/stories.@(ts|tsx)"],
stories: ["../**/stories.@(ts|tsx)", "../**/readme.mdx"],
addons: ["@storybook/addon-essentials"],
framework: "@storybook/react-vite",
};
Expand Down
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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fluentui-helpers",
"version": "0.0.3",
"version": "0.1.0",
"description": "Helper library for microsofts fluentui react library",
"main": "src/index.ts",
"author": "bubulus",
Expand Down
30 changes: 28 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
TEST FOR PUBLISHING; UNSTABLE DO NOT USE
**Disclaimer:** _I am not affiliated in any way with Microsoft or Fluent UI. This is **NOT** an official repository from the Fluent UI ecosystem_.

FURTHER REL FOR PIPELINE TEST 0.0.2
### Who is this for?

This library is aimed at folks who work with Microsoft's Fluent UI library but think it lacks some features here and there.

### Is this stable?

Not quite yet, from `1.*` onward it will be, as long as its in `0.*` consider it unstable.

### What does it contain?

It contains helpful hooks, components, and theme utilities that I was maintaining across different projects. I decided to create this central repository to share these utilities.

These utilities can be common layout utilities (like `Flex`, `Grid`, etc.), generic and library-specific hooks, extended theme tokens, common animations, and also full-fledged components that are not yet available in Fluent UI itself (like `Pagination`, for instance).

### Where can I find the docs?

The library documentation resides at a dedicated [Storybook](https://bubulux.github.io/fluentui-helpers). You will also find planned updates and preview components there.

### How long will I be committed to this?

I am maintaining two larger projects with Fluent UI, one private and one for my company, so there are long-lasting factors that will bind me to this for a while.

### How to contribute or report bugs?

If there are any bugs, please open an issue on GitHub.

If you want to contribute (other than a bug fix) or suggest a change, open a discussion thread :)
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);
}
Loading
Loading