Skip to content

Commit

Permalink
Card: Adding first prototype for Basic Card component (#7954)
Browse files Browse the repository at this point in the history
* Adding first implementation of Card component.

* Removing unnecessary template code and updating image with placeholder.

* Removing children property as it is inherited.

* Adding change file.

* Removing defaultProps from CardStatics and updating CardItem.tsx with the changes made in StackItem.tsx.

* Removing Stack that was wrapping basic example given that there is only one.

* Renaming preventPadding to disableChildPadding and removing as property from both Card and CardItem.

* Adding defaultPadding const so that padding and disableChildPadding is linked across both Card and CardItem.

* Replacing const that linked padding in Card and margin in CardItem with unit test that leverages styling functions of the components.

* Renaming preventShrink to disableShrink in use of Stack slot.

* Updating disableChildPadding prop margin test.
  • Loading branch information
khmakoto authored and msft-github-bot committed Feb 14, 2019
1 parent 3551e64 commit 42df10c
Show file tree
Hide file tree
Showing 20 changed files with 567 additions and 0 deletions.
11 changes: 11 additions & 0 deletions common/changes/@uifabric/experiments/card_2019-02-11-20-41.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@uifabric/experiments",
"comment": "Card: Adding first prototype for Basic Card component.",
"type": "minor"
}
],
"packageName": "@uifabric/experiments",
"email": "[email protected]"
}
1 change: 1 addition & 0 deletions packages/experiments/src/Card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './components/Card/index';
51 changes: 51 additions & 0 deletions packages/experiments/src/components/Card/Card.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ICardComponent, ICardStylesReturnType, ICardTokenReturnType } from './Card.types';
import { getGlobalClassNames } from '../../Styling';

const GlobalClassNames = {
root: 'ms-Card',
stack: 'ms-Card-stack'
};

export const CardTokens: ICardComponent['tokens'] = (props, theme): ICardTokenReturnType => [];

export const CardStyles: ICardComponent['styles'] = (props, theme, tokens): ICardStylesReturnType => {
const { compact } = props;

const classNames = getGlobalClassNames(GlobalClassNames, theme);

return {
root: [
classNames.root,
{
borderColor: 'lightgray',
borderWidth: '1px',
borderStyle: 'solid',
boxShadow: '1px 2px lightgray',
padding: 12,
height: '350px',
width: '250px'
}
],

stack: [
classNames.stack,
{
selectors: {
'> *': {
height: 'auto',
textOverflow: 'ellipsis'
},

'> *:not(:first-child)': [
compact && {
marginLeft: '12px'
},
!compact && {
marginTop: '12px'
}
]
}
}
]
};
};
22 changes: 22 additions & 0 deletions packages/experiments/src/components/Card/Card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CardView as view } from './Card.view';
import { CardStyles as styles, CardTokens as tokens } from './Card.styles';
import { ICardProps } from './Card.types';
import { CardItem } from './CardItem/CardItem';
import { ICardItemProps } from './CardItem/CardItem.types';
import { createComponent } from '../../Foundation';

const CardStatics = {
Item: CardItem
};

export const Card: React.StatelessComponent<ICardProps> & {
Item: React.StatelessComponent<ICardItemProps>;
} = createComponent({
displayName: 'Card',
view,
styles,
tokens,
statics: CardStatics
});

export default Card;
39 changes: 39 additions & 0 deletions packages/experiments/src/components/Card/Card.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IComponent, IComponentStyles, IHTMLSlot, IStyleableComponentProps } from '../../Foundation';
import { IStackSlot } from '../../Stack';
import { IBaseProps } from '../../Utilities';

export type ICardComponent = IComponent<ICardProps, ICardTokens, ICardStyles>;

// These types are redundant with ICardComponent but are needed until TS function return widening issue is resolved:
// https://github.com/Microsoft/TypeScript/issues/241
// For now, these helper types can be used to provide return type safety for tokens and styles functions.
export type ICardTokenReturnType = ReturnType<Extract<ICardComponent['tokens'], Function>>;
export type ICardStylesReturnType = ReturnType<Extract<ICardComponent['styles'], Function>>;

export interface ICard {}

export interface ICardSlots {
/**
* Defines root slot of the component.
*/
root?: IHTMLSlot;

/**
* Defines a stack slot for managing the layout of the Card.
*/
stack?: IStackSlot;
}

// Extending IStyleableComponentProps will automatically add stylable props for you, such as styles and theme.
// If you don't want these props to be included in your component, just remove this extension.
export interface ICardProps extends ICardSlots, IStyleableComponentProps<ICardProps, ICardTokens, ICardStyles>, IBaseProps<ICard> {
/**
* Defines whether to render a regular or a compact Card.
* @defaultvalue false
*/
compact?: boolean;
}

export interface ICardTokens {}

export type ICardStyles = IComponentStyles<ICardSlots>;
13 changes: 13 additions & 0 deletions packages/experiments/src/components/Card/Card.view.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';
import * as renderer from 'react-test-renderer';

import { CardView } from './Card.view';

// Views are just pure functions with no statefulness, which means they can get full code coverage
// with snapshot tests exercising permutations of the props.
describe('CardView', () => {
it('renders correctly', () => {
const tree = renderer.create(<CardView />).toJSON();
expect(tree).toMatchSnapshot();
});
});
23 changes: 23 additions & 0 deletions packages/experiments/src/components/Card/Card.view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** @jsx withSlots */
import { withSlots, getSlots } from '../../Foundation';
import { Stack } from '../../Stack';
import { getNativeProps, htmlElementProperties } from '../../Utilities';

import { ICardComponent, ICardProps, ICardSlots } from './Card.types';

export const CardView: ICardComponent['view'] = props => {
const Slots = getSlots<ICardProps, ICardSlots>(props, {
root: 'div',
stack: Stack
});

const nativeProps = getNativeProps(props, htmlElementProperties);

return (
<Slots.root {...nativeProps}>
<Slots.stack disableShrink verticalFill verticalAlign="space-between">
{props.children}
</Slots.stack>
</Slots.root>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getGlobalClassNames } from '../../../Styling';
import { ICardItemComponent, ICardItemStylesReturnType } from './CardItem.types';

const GlobalClassNames = {
root: 'ms-CardItem'
};

export const CardItemStyles: ICardItemComponent['styles'] = (props, theme): ICardItemStylesReturnType => {
const { disableChildPadding } = props;

const classNames = getGlobalClassNames(GlobalClassNames, theme);

return {
root: [
theme.fonts.medium,
classNames.root,
{
width: 'auto',
height: 'auto'
},
disableChildPadding && {
marginLeft: -12,
marginRight: -13
}
]
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as React from 'react';
import { mount } from 'enzyme';
import { Card } from '../Card';
import * as renderer from 'react-test-renderer';

import { CardStyles } from '../Card.styles';
import { ICardProps, ICardTokens, ICardStyles } from '../Card.types';
import { CardItemStyles } from './CardItem.styles';
import { ICardItemProps, ICardItemTokens, ICardItemStyles } from './CardItem.types';

import { IStylesFunction } from '@uifabric/foundation';
import { createTheme } from 'office-ui-fabric-react';

const testTheme = createTheme({});

describe('Card Item', () => {
it('can handle not having a class', () => {
const wrapper = mount(
<Card>
<Card.Item>
<div />
</Card.Item>
</Card>
);

expect(wrapper.find('.test').length).toBe(0);
});

it('renders correctly', () => {
const tree = renderer
.create(
<Card>
<Card.Item>
<div />
</Card.Item>
</Card>
)
.toJSON();

expect(tree).toMatchSnapshot();
});

it('renders correctly when having the disableChildPadding prop set to true', () => {
const tree = renderer
.create(
<Card>
<Card.Item disableChildPadding>
<div />
</Card.Item>
</Card>
)
.toJSON();

expect(tree).toMatchSnapshot();
});

it('has the correct margin values when the disableChildPadding prop is set to true', () => {
const cardStylesFunc = CardStyles as IStylesFunction<ICardProps, ICardTokens, ICardStyles>;
const cardItemStylesFunc = CardItemStyles as IStylesFunction<ICardItemProps, ICardItemTokens, ICardItemStyles>;

const cardStyles = cardStylesFunc({}, testTheme, {}).root;
const cardItemStylesArray = cardItemStylesFunc({ disableChildPadding: true }, testTheme, {}).root;

expect(cardStyles).not.toBeNull();
expect(cardItemStylesArray).not.toBeNull();

expect(cardStyles).toBeInstanceOf(Array);
expect(cardItemStylesArray).toBeInstanceOf(Array);

const cardPadding = (cardStyles as Array<any>).find(style => style.padding).padding;

const cardItemStyles = (cardItemStylesArray as Array<any>).find(style => style.marginLeft || style.marginRight);
const cardItemMarginLeft = cardItemStyles.marginLeft;
const cardItemMarginRight = cardItemStyles.marginRight;

expect(cardItemMarginLeft).toBe(-cardPadding);
expect(cardItemMarginRight).toBe(-cardPadding - 1);
});
});
26 changes: 26 additions & 0 deletions packages/experiments/src/components/Card/CardItem/CardItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @jsx withSlots */
import * as React from 'react';
import { withSlots, createComponent, getSlots } from '../../../Foundation';
import { ICardItemComponent, ICardItemProps, ICardItemSlots } from './CardItem.types';
import { CardItemStyles as styles } from './CardItem.styles';

const view: ICardItemComponent['view'] = props => {
const { children } = props;
if (React.Children.count(children) < 1) {
return null;
}

const Slots = getSlots<ICardItemProps, ICardItemSlots>(props, {
root: 'div'
});

return <Slots.root>{children}</Slots.root>;
};

export const CardItem: React.StatelessComponent<ICardItemProps> = createComponent({
displayName: 'CardItem',
styles,
view
});

export default CardItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IComponentStyles, IHTMLSlot, IComponent, IStyleableComponentProps } from '../../../Foundation';

export type ICardItemComponent = IComponent<ICardItemProps, ICardItemTokens, ICardItemStyles>;

export interface ICardItemSlots {
root?: IHTMLSlot;
}

// These types are redundant with ICardItemComponent but are needed until TS function return widening issue is resolved:
// https://github.com/Microsoft/TypeScript/issues/241
// For now, these helper types can be used to provide return type safety when specifying tokens and styles functions.
export type ICardItemTokenReturnType = ReturnType<Extract<ICardItemComponent['tokens'], Function>>;
export type ICardItemStylesReturnType = ReturnType<Extract<ICardItemComponent['styles'], Function>>;

export interface ICardItemProps extends ICardItemSlots, IStyleableComponentProps<ICardItemProps, ICardItemTokens, ICardItemStyles> {
/**
* Defines if the default horizontal padding of the Card applies to this CardItem child or not.
* @defaultvalue false
*/
disableChildPadding?: boolean;
}

export interface ICardItemTokens {}

export type ICardItemStyles = IComponentStyles<ICardItemSlots>;
Loading

0 comments on commit 42df10c

Please sign in to comment.