Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

[WIP] feat(create-component): add create component #2200

Closed
wants to merge 14 commits into from
Closed
9 changes: 9 additions & 0 deletions docs/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -393,6 +393,15 @@ class Sidebar extends React.Component<any, any> {
},
public: true,
},
{
key: 'prototype-create-component',
title: {
content: 'Create component',
as: NavLink,
to: 'prototype-create-component',
},
public: false,
},
]

const componentTreeSection = {
122 changes: 122 additions & 0 deletions docs/src/prototypes/createComponent/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as React from 'react'
import { mergeCss } from '@uifabric/merge-styles'
import {
Provider,
FluentTheme,
PlannerFluentTheme,
FluentButton,
FluentMenu,
FluentMenuItem,
} from '@fluentui/react-theming'

const oddRedBorder = mergeCss({ border: '10px solid red' })
const example = mergeCss({ margin: 20 })

const MenuItemText = (props: any) => {
return <span {...props}>{props.children}</span>
}

// This is a bad API... :(
const items = [
{
slots: { text: MenuItemText, menu: FluentMenu },
slotProps: {
text: { id: 'blabla', children: 'Bla' },
menu: {
slotProps: {
items: [
{
slots: { text: MenuItemText },
slotProps: { text: { id: 'blabla', children: 'Boo' } },
},
{
slots: { text: MenuItemText },
slotProps: { text: { id: 'blabla', children: 'Coo' } },
},
],
},
},
},
rounded: true,
},
{ slots: { text: MenuItemText }, slotProps: { text: { id: 'blabla', children: 'Foo' } } },
]

// Much better in my opinion
// const items = [
// { slots: { text: MenuItemText }, text: { id: 'blabla', children: 'Bla' } },
// { slots: { text: MenuItemText }, text: { id: 'blabla', children: 'Foo' } }
// ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, you should update this PR based on your proposal here.


const Icon: React.FunctionComponent<any> = props => <span {...props}>@</span>
const ButtonThemedExample: React.FunctionComponent<{}> = props => {
const onClick = React.useCallback(() => console.log('clicked button'), [])
const variants = () => {
return (
<>
<div className={example}>
<FluentButton tiny>tiny</FluentButton>
</div>
<div className={example}>
<FluentButton large>large</FluentButton>
</div>
<div className={example}>
<FluentButton size="s">small</FluentButton>
<FluentButton size="m">medium</FluentButton>
<FluentButton size="l">large</FluentButton>
</div>

<div className={example}>
<FluentButton shadowed>shadowed</FluentButton>
</div>
<div className={example}>
<FluentButton
bigIcon={true}
slots={{ icon: Icon, primaryText: () => <span>BigIcon</span> }}
/>
</div>
<div className={example}>
<FluentButton id="sdasdas" shadowed tiny>
shadowed & tiny
</FluentButton>
</div>
<div className={example}>
<FluentButton onClick={onClick} shadowed tiny bigIcon>
Shadowed tiny bigIcon
</FluentButton>
</div>
<div className={example}>
<FluentButton onClick={onClick} beautiful>
Beautiful
</FluentButton>
</div>

<div className={example}>
<FluentButton onClick={onClick} className={oddRedBorder}>
Fluent Button with an odd red border
</FluentButton>
</div>
</>
)
}
return (
<div>
<h1>Fluent Theme</h1>
<Provider theme={FluentTheme}>{variants()}</Provider>

<h1>Planner Fluent Theme</h1>
<Provider theme={PlannerFluentTheme}>{variants()}</Provider>

<h1>Menu</h1>
<Provider theme={PlannerFluentTheme}>
<FluentMenu rounded slotProps={{ items }} />
<FluentMenuItem
slots={{ menu: FluentMenu }}
slotProps={{ menu: { slotProps: { items } } }}
/>
</Provider>
</div>
)
}

export default ButtonThemedExample
2 changes: 2 additions & 0 deletions docs/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@ import CustomScrollbarPrototype from './prototypes/customScrollbar'
import EditorToolbarPrototype from './prototypes/EditorToolbar'
import HexagonalAvatarPrototype from './prototypes/hexagonalAvatar'
import TablePrototype from './prototypes/table'
import CreateComponentPrototype from './prototypes/createComponent'

const Routes = () => (
<BrowserRouter basename={__BASENAME__}>
@@ -89,6 +90,7 @@ const Routes = () => (
/>
<Route exact path="/virtualized-tree" component={VirtualizedTreePrototype} />
<Route exact path="/prototype-copy-to-clipboard" component={CopyToClipboardPrototype} />
<Route exact path="/prototype-create-component" component={CreateComponentPrototype} />
<Route exact path="/faq" component={FAQ} />
<Route exact path="/accessibility" component={Accessibility} />
<Route exact path="/accessibility-behaviors" component={AccessibilityBehaviors} />
38 changes: 38 additions & 0 deletions packages/react-theming/src/components/Button/BaseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react';

/**
* TODO:
* 1) do we really need slots prop?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean keeping this in context only opposed to also having a slot?

<Provider theme={{
    components: {
      Button: {
        slots: { icon: MyIcon },
      }
    }
  }}
/>

Ignore the fact that I put this in theme.components. I just mean to show "top-level config via context" vs a prop.

vs

<Button slots={{ icon: MyIcon }} />

This would allow easy inline overrides of the slots, but is it necessary?

*/
interface IBaseButtonProps extends React.AllHTMLAttributes<any> {
slots?: any;
slotProps?: any;
}

export const ButtonText: React.FunctionComponent<any> = props => <span {...props}>my button</span>;

export const BaseButton: React.FunctionComponent<IBaseButtonProps> = props => {
const { slots, children, slotProps, ...rest } = props;
const {
root: Root = 'button',
icon: Icon,
primaryText: PrimaryText,
secondaryText: SecondaryText,
} = slots || {};
const { root = {}, icon = {}, primaryText = {}, secondaryText = {} } = slotProps || {};

const rootClassName = `${root.className || ''}${` ${rest.className}` || ''}`;
const content = children || (
<>
{Icon && <Icon {...icon} />}
{PrimaryText && <PrimaryText {...primaryText} />}
{SecondaryText && <SecondaryText {...secondaryText} />}
</>
);

return (
<Root {...root} {...rest} className={rootClassName}>
{content}
</Root>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { BaseButton } from './BaseButton';
import { createComponent } from '../../create-component/createComponent';

export const FluentButton = createComponent('FluentButton', BaseButton);
22 changes: 22 additions & 0 deletions packages/react-theming/src/components/Menu/BaseMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';
import { BaseMenuItem } from './BaseMenuItem';

interface IMenuProps {
className?: string;
slots?: any;
slotProps?: any;
}

export const BaseMenu: React.FunctionComponent<IMenuProps> = props => {
const { slotProps = {}, slots = {}, ...rest } = props;
const { item: MenuItem = BaseMenuItem, root: Root = 'div' } = slots;
const { root: rootProps = {}, items = [] } = slotProps;
const rootClassName = `${rootProps.className || ''}${` ${rest && rest.className}` || ''}`;
return (
<Root {...rootProps} {...rest} className={rootClassName}>
{items.map((item: any) => (
<MenuItem key={item.id} {...item} />
))}
</Root>
);
};
32 changes: 32 additions & 0 deletions packages/react-theming/src/components/Menu/BaseMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';

interface IMenuItemProps {
className?: string;
slots?: any;
slotProps?: any;
}

export const BaseMenuItem: React.FunctionComponent<IMenuItemProps> = props => {
const { children, slots = {}, slotProps = {}, ...rest } = props;
const { root: Root = 'div', text: Text, icon: Icon, menu: Menu } = slots;
const {
root: rootProps = {},
text: textProps = {},
icon: iconProps = {},
menu: menuProps = {},
} = slotProps;
const rootClassName = `${rootProps.className || ''}${` ${rest && rest.className}` || ''}`;
const content = children || (
<>
{Icon && <Icon {...iconProps} />}
{Text && <Text {...textProps} />}
{Menu && <Menu {...menuProps} />}
</>
);

return (
<Root {...rootProps} {...rest} className={rootClassName}>
{content}
</Root>
);
};
9 changes: 9 additions & 0 deletions packages/react-theming/src/components/Menu/FluentMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BaseMenu } from './BaseMenu';
import { FluentMenuItem } from './FluentMenuItem';
import { createComponent } from '../../create-component/createComponent';

export const FluentMenu = createComponent('FluentMenu', BaseMenu, {
slots: {
item: FluentMenuItem,
},
});
13 changes: 13 additions & 0 deletions packages/react-theming/src/components/Menu/FluentMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BaseMenuItem } from './BaseMenuItem';
import { createComponent } from '../../create-component/createComponent';
// import { FluentMenu } from './'

export const FluentMenuItem = createComponent(
'FluentMenuItem',
BaseMenuItem,
// {
// slots: {
// menu: FluentMenu,
// }
// }
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react';
// import { IBaseThemeShape } from './ThemeShape';

/*
interface IProviderProps<T extends IBaseThemeShape> {
theme: T;
}
*/

export const ProviderContext = React.createContext(null);

export const Provider: React.FunctionComponent<any> = props => {
return <ProviderContext.Provider value={props.theme}>{props.children}</ProviderContext.Provider>;
};
49 changes: 49 additions & 0 deletions packages/react-theming/src/create-component/ClassCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ClassCache, VariantBasedCacheKeyStrategy } from './ClassCache';

describe('ClassCache', () => {
it('allows access via theme and string', () => {
const c = new ClassCache();
const val = {};
const theme = {};
c.set(theme, 'foo-bar-baz', val);
expect(c.get(theme, 'foo-bar-baz')).toBe(val);
});

it('allows access via theme and multiple strings', () => {
const c = new ClassCache();
const val = {};
const theme = {};
c.set(theme, 'foo-bar-baz', val);
c.set(theme, 'foo-bar', {});
expect(c.get(theme, 'foo-bar-baz')).toBe(val);
});

it('returns null if entry not found', () => {
const c = new ClassCache();
expect(c.get({}, '')).toBeNull();
});

describe('getOrSet', () => {
it('allows for passing in of a default value', () => {
const c = new ClassCache();
const cacheEntry = {};
const theme = {};
const key = '';
const fetchedEntry: any = c.getOrSet(theme, key, cacheEntry);
expect(fetchedEntry).toBe(cacheEntry);
expect(c.get(theme, key)).toBe(cacheEntry);
});
});

describe('with automative cache key computation', () => {
it('handles cache key computation', () => {
const c = new ClassCache();
const val = {};
const theme = {};
c.set(theme, new VariantBasedCacheKeyStrategy(['a', 'b', 'c'], {}).toString(), val);
expect(c.get(theme, new VariantBasedCacheKeyStrategy(['a', 'b', 'c'], {}).toString())).toBe(
val,
);
});
});
});
47 changes: 47 additions & 0 deletions packages/react-theming/src/create-component/ClassCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export class ClassCache {
private cache = new WeakMap();

public get(theme: {}, arg1: string): any {
const obj = this.cache.get(theme);
if (!obj) {
return null;
}
return obj[arg1] || null;
}

public set(theme: {}, arg1: string, val: {}) {
let themeEntry;
if (this.cache.get(theme)) {
themeEntry = this.cache.get(theme);
} else {
themeEntry = {};
this.cache.set(theme, themeEntry);
}
themeEntry[arg1] = val;
}

public getOrSet(theme: {}, key: string, cacheEntry: any): any {
const existing = this.get(theme, key);
if (existing !== undefined && existing !== null) {
return existing;
}
this.set(theme, key, cacheEntry);
return cacheEntry;
}
}

export class VariantBasedCacheKeyStrategy {
private computed: string;

constructor(private variants: string[] = [], private props: any = {}) {}

public toString() {
if (this.computed) {
return this.computed;
}
const computedRaw: any = {};
this.variants.slice().forEach(v => (computedRaw[v] = this.props[v]));
this.computed = JSON.stringify(computedRaw);
return this.computed;
}
}
Loading