diff --git a/common/changes/@uifabric/experiments/card_2019-02-11-20-41.json b/common/changes/@uifabric/experiments/card_2019-02-11-20-41.json new file mode 100644 index 0000000000000..004f344e089cd --- /dev/null +++ b/common/changes/@uifabric/experiments/card_2019-02-11-20-41.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/experiments", + "comment": "Card: Adding first prototype for Basic Card component.", + "type": "minor" + } + ], + "packageName": "@uifabric/experiments", + "email": "makotom@microsoft.com" +} \ No newline at end of file diff --git a/packages/experiments/src/Card.ts b/packages/experiments/src/Card.ts new file mode 100644 index 0000000000000..734059d20a2e1 --- /dev/null +++ b/packages/experiments/src/Card.ts @@ -0,0 +1 @@ +export * from './components/Card/index'; diff --git a/packages/experiments/src/components/Card/Card.styles.ts b/packages/experiments/src/components/Card/Card.styles.ts new file mode 100644 index 0000000000000..3c4c3637c6c5b --- /dev/null +++ b/packages/experiments/src/components/Card/Card.styles.ts @@ -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' + } + ] + } + } + ] + }; +}; diff --git a/packages/experiments/src/components/Card/Card.ts b/packages/experiments/src/components/Card/Card.ts new file mode 100644 index 0000000000000..475bce657dd35 --- /dev/null +++ b/packages/experiments/src/components/Card/Card.ts @@ -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 & { + Item: React.StatelessComponent; +} = createComponent({ + displayName: 'Card', + view, + styles, + tokens, + statics: CardStatics +}); + +export default Card; diff --git a/packages/experiments/src/components/Card/Card.types.ts b/packages/experiments/src/components/Card/Card.types.ts new file mode 100644 index 0000000000000..9f1a0054fbfe9 --- /dev/null +++ b/packages/experiments/src/components/Card/Card.types.ts @@ -0,0 +1,39 @@ +import { IComponent, IComponentStyles, IHTMLSlot, IStyleableComponentProps } from '../../Foundation'; +import { IStackSlot } from '../../Stack'; +import { IBaseProps } from '../../Utilities'; + +export type ICardComponent = IComponent; + +// 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>; +export type ICardStylesReturnType = ReturnType>; + +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, IBaseProps { + /** + * Defines whether to render a regular or a compact Card. + * @defaultvalue false + */ + compact?: boolean; +} + +export interface ICardTokens {} + +export type ICardStyles = IComponentStyles; diff --git a/packages/experiments/src/components/Card/Card.view.test.tsx b/packages/experiments/src/components/Card/Card.view.test.tsx new file mode 100644 index 0000000000000..16e1dec47df11 --- /dev/null +++ b/packages/experiments/src/components/Card/Card.view.test.tsx @@ -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().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/experiments/src/components/Card/Card.view.tsx b/packages/experiments/src/components/Card/Card.view.tsx new file mode 100644 index 0000000000000..8aba19357e082 --- /dev/null +++ b/packages/experiments/src/components/Card/Card.view.tsx @@ -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(props, { + root: 'div', + stack: Stack + }); + + const nativeProps = getNativeProps(props, htmlElementProperties); + + return ( + + + {props.children} + + + ); +}; diff --git a/packages/experiments/src/components/Card/CardItem/CardItem.styles.ts b/packages/experiments/src/components/Card/CardItem/CardItem.styles.ts new file mode 100644 index 0000000000000..e394f4bf10ec5 --- /dev/null +++ b/packages/experiments/src/components/Card/CardItem/CardItem.styles.ts @@ -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 + } + ] + }; +}; diff --git a/packages/experiments/src/components/Card/CardItem/CardItem.test.tsx b/packages/experiments/src/components/Card/CardItem/CardItem.test.tsx new file mode 100644 index 0000000000000..b6487d6b5f504 --- /dev/null +++ b/packages/experiments/src/components/Card/CardItem/CardItem.test.tsx @@ -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( + + +
+ + + ); + + expect(wrapper.find('.test').length).toBe(0); + }); + + it('renders correctly', () => { + const tree = renderer + .create( + + +
+ + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly when having the disableChildPadding prop set to true', () => { + const tree = renderer + .create( + + +
+ + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('has the correct margin values when the disableChildPadding prop is set to true', () => { + const cardStylesFunc = CardStyles as IStylesFunction; + const cardItemStylesFunc = CardItemStyles as IStylesFunction; + + 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).find(style => style.padding).padding; + + const cardItemStyles = (cardItemStylesArray as Array).find(style => style.marginLeft || style.marginRight); + const cardItemMarginLeft = cardItemStyles.marginLeft; + const cardItemMarginRight = cardItemStyles.marginRight; + + expect(cardItemMarginLeft).toBe(-cardPadding); + expect(cardItemMarginRight).toBe(-cardPadding - 1); + }); +}); diff --git a/packages/experiments/src/components/Card/CardItem/CardItem.tsx b/packages/experiments/src/components/Card/CardItem/CardItem.tsx new file mode 100644 index 0000000000000..dea71326c9fa3 --- /dev/null +++ b/packages/experiments/src/components/Card/CardItem/CardItem.tsx @@ -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(props, { + root: 'div' + }); + + return {children}; +}; + +export const CardItem: React.StatelessComponent = createComponent({ + displayName: 'CardItem', + styles, + view +}); + +export default CardItem; diff --git a/packages/experiments/src/components/Card/CardItem/CardItem.types.ts b/packages/experiments/src/components/Card/CardItem/CardItem.types.ts new file mode 100644 index 0000000000000..e0f64106818a8 --- /dev/null +++ b/packages/experiments/src/components/Card/CardItem/CardItem.types.ts @@ -0,0 +1,25 @@ +import { IComponentStyles, IHTMLSlot, IComponent, IStyleableComponentProps } from '../../../Foundation'; + +export type ICardItemComponent = IComponent; + +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>; +export type ICardItemStylesReturnType = ReturnType>; + +export interface ICardItemProps extends ICardItemSlots, IStyleableComponentProps { + /** + * 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; diff --git a/packages/experiments/src/components/Card/CardItem/__snapshots__/CardItem.test.tsx.snap b/packages/experiments/src/components/Card/CardItem/__snapshots__/CardItem.test.tsx.snap new file mode 100644 index 0000000000000..e900160c80af1 --- /dev/null +++ b/packages/experiments/src/components/Card/CardItem/__snapshots__/CardItem.test.tsx.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Card Item renders correctly 1`] = ` +
+
* { + height: auto; + text-overflow: ellipsis; + } + & > *:not(:first-child) { + margin-top: 12px; + } + & > *:not(.ms-StackItem) { + flex-shrink: 0; + } + > +
+
+
+
+
+`; + +exports[`Card Item renders correctly when having the disableChildPadding prop set to true 1`] = ` +
+
* { + height: auto; + text-overflow: ellipsis; + } + & > *:not(:first-child) { + margin-top: 12px; + } + & > *:not(.ms-StackItem) { + flex-shrink: 0; + } + > +
+
+
+
+
+`; diff --git a/packages/experiments/src/components/Card/CardPage.tsx b/packages/experiments/src/components/Card/CardPage.tsx new file mode 100644 index 0000000000000..89af791992318 --- /dev/null +++ b/packages/experiments/src/components/Card/CardPage.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { ExampleCard, IComponentDemoPageProps, ComponentPage, PageMarkdown, PropertiesTableSet } from '@uifabric/example-app-base'; + +import { CardBasicExample } from './examples/Card.Basic.Example'; +const CardBasicExampleCode = require('!raw-loader!@uifabric/experiments/src/components/Card/examples/Card.Basic.Example.tsx') as string; + +export class CardPage extends React.Component { + public render(): JSX.Element { + return ( + + + + +
+ } + propertiesTables={ + ('!raw-loader!@uifabric/experiments/src/components/Card/Card.types.ts')]} /> + } + overview={ + {require('!raw-loader!@uifabric/experiments/src/components/Card/docs/CardOverview.md')} + } + bestPractices={
} + dos={{require('!raw-loader!@uifabric/experiments/src/components/Card/docs/CardDos.md')}} + donts={{require('!raw-loader!@uifabric/experiments/src/components/Card/docs/CardDonts.md')}} + isHeaderVisible={this.props.isHeaderVisible} + /> + ); + } +} diff --git a/packages/experiments/src/components/Card/__snapshots__/Card.view.test.tsx.snap b/packages/experiments/src/components/Card/__snapshots__/Card.view.test.tsx.snap new file mode 100644 index 0000000000000..f6c4f5da29fc2 --- /dev/null +++ b/packages/experiments/src/components/Card/__snapshots__/Card.view.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CardView renders correctly 1`] = ` +
+
* { + text-overflow: ellipsis; + } + & > *:not(:first-child) { + margin-top: 0px; + } + & > *:not(.ms-StackItem) { + flex-shrink: 0; + } + /> +
+`; diff --git a/packages/experiments/src/components/Card/docs/CardDonts.md b/packages/experiments/src/components/Card/docs/CardDonts.md new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/experiments/src/components/Card/docs/CardDos.md b/packages/experiments/src/components/Card/docs/CardDos.md new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/experiments/src/components/Card/docs/CardOverview.md b/packages/experiments/src/components/Card/docs/CardOverview.md new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/experiments/src/components/Card/examples/Card.Basic.Example.tsx b/packages/experiments/src/components/Card/examples/Card.Basic.Example.tsx new file mode 100644 index 0000000000000..5ae80563ea52c --- /dev/null +++ b/packages/experiments/src/components/Card/examples/Card.Basic.Example.tsx @@ -0,0 +1,55 @@ +// @codepen +import * as React from 'react'; +import { Card } from '../Card'; +import { Persona } from '../../../Persona'; +import { PersonaTestImages } from '@uifabric/experiments/lib/common/TestImages'; +import { Stack } from '../../../Stack'; +import { Text } from '../../../Text'; +import { Icon, Image } from 'office-ui-fabric-react'; +import { mergeStyleSets } from 'office-ui-fabric-react/lib/Styling'; + +export class CardBasicExample extends React.Component<{}, {}> { + public render(): JSX.Element { + const styles = mergeStyleSets({ + textLarge: { + color: 'teal', + fontWeight: 'bolder' + }, + textBold: { + fontWeight: 'bold' + } + }); + + return ( + + + + Example implementation of the property image fit using the center value on an image larger than the frame. + + + Contoso + + + Contoso Denver expansion design marketing hero guidelines + + Is this recommendation helpful? + + + + + + + + ); + } +} diff --git a/packages/experiments/src/components/Card/index.ts b/packages/experiments/src/components/Card/index.ts new file mode 100644 index 0000000000000..510df690b91d6 --- /dev/null +++ b/packages/experiments/src/components/Card/index.ts @@ -0,0 +1,3 @@ +export * from './CardItem/CardItem'; +export * from './Card'; +export * from './Card.types'; diff --git a/packages/experiments/src/demo/AppDefinition.tsx b/packages/experiments/src/demo/AppDefinition.tsx index 73bcb092f329d..378f6f0b7bfbd 100644 --- a/packages/experiments/src/demo/AppDefinition.tsx +++ b/packages/experiments/src/demo/AppDefinition.tsx @@ -22,6 +22,12 @@ export const AppDefinition: IAppDefinition = { name: 'Button', url: '#/examples/button' }, + { + component: require('../components/Card/CardPage').CardPage, + key: 'Card', + name: 'Card', + url: '#/examples/card' + }, { component: require('../components/CollapsibleSection/CollapsibleSectionPage').CollapsibleSectionPage, key: 'CollapsibleSection',