Skip to content
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

Add ThemelessFluentProvider implementation #149

Merged
merged 12 commits into from
Apr 4, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add ThemelessFluentProvider implementation",
"packageName": "@fluentui-contrib/react-themeless-provider",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "chore: scaffold react-themeless-provider package",
spmonahan marked this conversation as resolved.
Show resolved Hide resolved
"packageName": "fluentui-contrib",
"email": "seanmonahan@microsoft.com",
"dependentChangeType": "none"
}
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fluentui/react-components": "^9.46.3",
"@fluentui/react-shared-contexts": "^9.7.2",
"@griffel/shadow-dom": "~0.2.0",
"@nx/devkit": "17.3.2",
"@nx/eslint-plugin": "17.3.2",
Expand All @@ -44,10 +45,14 @@
"@playwright/experimental-ct-react": "^1.41.2",
"@rnx-kit/eslint-plugin": "0.4.2",
"@storybook/addon-essentials": "7.6.10",
"@storybook/addon-interactions": "^7.5.3",
"@storybook/addon-storysource": "7.6.10",
"@storybook/core-common": "^7.0.9",
"@storybook/core-server": "7.6.10",
"@storybook/jest": "^0.2.3",
"@storybook/react-webpack5": "7.6.10",
"@storybook/test-runner": "^0.13.0",
"@storybook/testing-library": "^0.2.2",
"@swc-node/register": "1.6.8",
"@swc/cli": "~0.1.62",
"@swc/core": "1.3.107",
Expand All @@ -63,6 +68,7 @@
"@typescript-eslint/eslint-plugin": "6.20.0",
"@typescript-eslint/parser": "6.20.0",
"beachball": "^2.33.2",
"core-js": "^3.6.5",
"eslint": "8.48.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.27.5",
Expand Down
Empty file.
3 changes: 2 additions & 1 deletion packages/react-themeless-provider/.swcrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
},
"transform": {
"react": {
"runtime": "classic",
"runtime": "automatic",
"importSource": "@fluentui/react-jsx-runtime",
"useSpread": true
}
},
Expand Down
46 changes: 41 additions & 5 deletions packages/react-themeless-provider/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
# react-themeless-provider

This library was generated with [Nx](https://nx.dev).
This package provides `ThemelessFluentProvider`, a replacement for `FluentProvider` when the provider needs to be rendered inside shadow DOM.

## Building
`ThemelessFluentProvider` does not render the theme contexts or inject theme CSS custom properties into the DOM. When using this provider you are responsible for setting the necessary CSS custom properties in such a way that they will pierce the shadow DOM (e.g., apply them to `:root`).

Run `nx build react-themeless-provider` to build the library.
## Install

## Running unit tests
```sh
yarn add @fluentui-contrib/react-themeless-provider
# or
npm install @fluentui-contrib/react-themeless-provider
```

Run `nx test react-themeless-provider` to execute the unit tests via [Jest](https://jestjs.io).
## Usage

```tsx
import * as React from 'react';
import { root } from '@fluentui-contrib/react-shadow';
import { ThemelessFluentProvider } from '@fluentui-contrib/react-themeless-provider';
import { createCSSRuleFromTheme, webLightTheme, Button } from '@fluentui/react-components';

export function App() {
React.useEffect(() => {
// Example of how to create a style that will piece the shadow DOM.
// This does not need to be created by React and can be created
// by a host Web Component application for example.
const cssRule = createCSSRuleFromTheme(':root', webLightTheme);
const style = document.createElement('style');
document.head.appendChild(style);
style.sheet.insertRule(cssRule);

return () => {
document.head.removeChild(style);
}
}, []);
spmonahan marked this conversation as resolved.
Show resolved Hide resolved

return (
{/* renders a shadow root */}
<root.div>
<ThemelessFluentProvider>
<Button>A themed Fluent React Button in shadow DOM</Button>
</ThemelessFluentProvider>
</root.div>
);
}
```
4 changes: 2 additions & 2 deletions packages/react-themeless-provider/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export default {
displayName: 'react-themeless-provider',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['@swc/jest', swcJestConfig],
'^.+\\.[tj]sx?$': ['@swc/jest', swcJestConfig],
},
moduleFileExtensions: ['ts', 'js', 'html'],
moduleFileExtensions: ['js', 'ts', 'tsx', 'html'],
testEnvironment: 'jsdom',
coverageDirectory: '../../coverage/packages/react-themeless-provider',
};
17 changes: 11 additions & 6 deletions packages/react-themeless-provider/package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
{
"name": "@fluentui-contrib/react-themeless-provider",
"version": "0.0.1",
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"dependencies": {
"@fluentui/react-jsx-runtime": ">=9.0.29 < 10.0.0",
"@griffel/react": "^1.5.14",
"@swc/helpers": "~0.5.2"
},
"main": "./src/index.js",
"typings": "./src/index.d.ts",
"private": true,
"devDependencies": {
"@fluentui/react-jsx-runtime": "^9.0.32",
"@fluentui-contrib/react-shadow": "^0.1.0"
},
"peerDependencies": {
"@fluentui/react-components": ">=9.46.3 <10.0.0",
"@fluentui/react-icons": ">=2.0.204 <3.0.0",
"@fluentui/react-shared-contexts": ">=9.7.2 <10.0.0",
"@types/react": ">=16.8.0 <19.0.0",
"@types/react-dom": ">=16.8.0 <19.0.0",
"react": ">=16.8.0 <19.0.0",
"react-dom": ">=16.8.0 <19.0.0"
"react": ">=16.8.0 <19.0.0"
}
}
6 changes: 6 additions & 0 deletions packages/react-themeless-provider/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@
"quiet": true
}
}
},
"test-storybook": {
"executor": "nx:run-commands",
"options": {
"command": "test-storybook -c packages/react-themeless-provider/.storybook --url=http://localhost:4400"
}
}
},
"tags": []
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ import { render } from '@testing-library/react';
import { ThemelessFluentProvider } from './ThemelessFluentProvider';

describe('ThemelessFluentProvider', () => {
it('should render', () => {
it('should render children', () => {
const { getByText } = render(
<ThemelessFluentProvider>
<span>Hello world!</span>
</ThemelessFluentProvider>
);

expect(() => getByText('Hello world!')).not.toThrow();
});

it('does not render style elements', () => {
render(<ThemelessFluentProvider />);
expect(document.head.querySelector('style')).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,106 @@
import * as React from 'react';
import { mergeClasses } from '@fluentui/react-components';
import { useStyles } from './ThemelessFluentProvider.styles';
import {
getIntrinsicElementProps,
useFluent,
useFluentProviderContextValues_unstable,
useFocusVisible,
useMergedRefs,
slot,
} from '@fluentui/react-components';
import type {
FluentProviderProps,
FluentProviderState,
} from '@fluentui/react-components';
import {
CustomStyleHooksContext_unstable as CustomStyleHooksContext,
useOverrides_unstable as useOverrides,
} from '@fluentui/react-shared-contexts';
import type { CustomStyleHooksContextValue_unstable as CustomStyleHooksContextValue } from '@fluentui/react-shared-contexts';

export const ThemelessFluentProvider: React.FC = () => {
const styles = useStyles();
return <div className={mergeClasses(styles.root)}>Hello World!</div>;
};
import { useThemelessProviderStyles_unstable } from './useThemelessFluentProviderStyles.styles';
import { renderThemelessFluentProvider_unstable } from './renderThemelessFluentProvider';

type ThemelessFluentProviderProps = Omit<
FluentProviderProps,
'applyStylesToPortals' | 'theme'
>;

function shallowMerge<T>(a: T, b: T): T {
// Merge impacts perf: we should like to avoid it if it's possible
if (a && b) {
return { ...a, ...b };
}

if (a) {
return a;
}

return b;
}

function useThemelessFluentProviderState(
props: ThemelessFluentProviderProps,
ref: React.Ref<HTMLDivElement>
): FluentProviderState {
const parentContext = useFluent();
const parentOverrides = useOverrides();
const parentCustomStyleHooks: CustomStyleHooksContextValue =
React.useContext(CustomStyleHooksContext) || {};

const {
customStyleHooks_unstable,
dir = parentContext.dir,
targetDocument = parentContext.targetDocument,
overrides_unstable: overrides = {},
} = props;

const mergedOverrides = shallowMerge(parentOverrides, overrides);
const mergedCustomStyleHooks = shallowMerge(
parentCustomStyleHooks,
customStyleHooks_unstable
) as CustomStyleHooksContextValue;

return {
applyStylesToPortals: false,
customStyleHooks_unstable: mergedCustomStyleHooks,
dir,
targetDocument,
theme: {},
overrides_unstable: mergedOverrides,
themeClassName: '',

components: {
root: 'div',
},
root: slot.always(
getIntrinsicElementProps('div', {
...props,
dir,
ref: useMergedRefs(
ref,
useFocusVisible<HTMLDivElement>({ targetDocument })
),
}),
{ elementType: 'div' }
),

// server-side rendering not supported
serverStyleProps: {
cssRule: '',
attributes: {},
},
};
}

export const ThemelessFluentProvider = React.forwardRef<
HTMLDivElement,
ThemelessFluentProviderProps
>((props, ref) => {
const state = useThemelessFluentProviderState(props, ref);
useThemelessProviderStyles_unstable(state);
const contextValues = useFluentProviderContextValues_unstable(state);

return renderThemelessFluentProvider_unstable(state, contextValues);
});

ThemelessFluentProvider.displayName = 'ThemelessFluentProvider';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/** @jsxRuntime classic */
/** @jsx createElement */

// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { createElement, Fragment } from '@fluentui/react-jsx-runtime';
import { assertSlots } from '@fluentui/react-components';
import { TextDirectionProvider } from '@griffel/react';
import {
OverridesProvider_unstable as OverridesProvider,
Provider_unstable as Provider,
TooltipVisibilityProvider_unstable as TooltipVisibilityProvider,
CustomStyleHooksProvider_unstable as CustomStyleHooksProvider,
CustomStyleHooksContextValue_unstable as CustomStyleHooksContextValue,
} from '@fluentui/react-shared-contexts';
import type {
FluentProviderContextValues,
FluentProviderState,
FluentProviderSlots,
} from '@fluentui/react-components';
import { IconDirectionContextProvider } from '@fluentui/react-icons';

/**
* Render the final JSX of ThemelessFluentProvider
*/
export const renderThemelessFluentProvider_unstable = (
layershifter marked this conversation as resolved.
Show resolved Hide resolved
state: FluentProviderState,
contextValues: FluentProviderContextValues
) => {
assertSlots<FluentProviderSlots>(state);

// Typescript (vscode) incorrectly references the FluentProviderProps.customStyleHooks_unstable
// instead of FluentProviderContextValues.customStyleHooks_unstable and thinks it is
// Partial<CustomStyleHooksContextValue>, so it needs to be cast to Required<CustomStyleHooksContextValue>
return (
<Provider value={contextValues.provider}>
<CustomStyleHooksProvider
value={
contextValues.customStyleHooks_unstable as Required<CustomStyleHooksContextValue>
}
>
<TooltipVisibilityProvider value={contextValues.tooltip}>
<TextDirectionProvider dir={contextValues.textDirection}>
<IconDirectionContextProvider value={contextValues.iconDirection}>
<OverridesProvider value={contextValues.overrides_unstable}>
<state.root>{state.root.children}</state.root>
</OverridesProvider>
</IconDirectionContextProvider>
</TextDirectionProvider>
</TooltipVisibilityProvider>
</CustomStyleHooksProvider>
</Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { mergeClasses } from '@fluentui/react-components';
import type {
FluentProviderSlots,
FluentProviderState,
SlotClassNames,
} from '@fluentui/react-components';

export const themelessFluentProviderClassNames: SlotClassNames<FluentProviderSlots> =
{
root: 'fui-ThemelessFluentProvider',
};

export const useThemelessProviderStyles_unstable = (
state: FluentProviderState
): FluentProviderState => {
state.root.className = mergeClasses(
themelessFluentProviderClassNames.root,
state.root.className
);
return state;
};
1 change: 1 addition & 0 deletions packages/react-themeless-provider/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { ThemelessFluentProvider } from './components/ThemelessFluentProvider';
export { createCSSStyleSheetFromTheme } from './utils/createCSSStyleSheetFromTheme';
Loading
Loading