diff --git a/docs/app/Component Guidelines.md b/docs/app/Component Guidelines.md index f0dc424b62..c2b4c520e9 100644 --- a/docs/app/Component Guidelines.md +++ b/docs/app/Component Guidelines.md @@ -1,12 +1,21 @@ # Stardust Component Guidelines -Every component in Stardust must conform to these guidelines. They ensure consistency and optimal development experience for Stardust users. +Every component in Stardust must conform to these guidelines. +They ensure consistency and optimal development experience for Stardust users. -This document will be minimally maintained. See [`\test`](https://github.com/TechnologyAdvice/stardust/tree/master/test) for the current conformance tests. +1. [Classes](#Classes) + - [Extend React.Component](#Extend React.Component) + - [Identical class and component names](#Identical class and component names) + - [No wrapping elements](#No wrapping elements) +1. [Events](#Events) +1. [Props](#Props) + - [className](#className) -## Extends React.Component +## Classes -**Valid** +### Extend React.Component + +**Always** ```jsx import React, {Component} from 'react'; @@ -14,7 +23,7 @@ import React, {Component} from 'react'; export default class MyComponent extends Component {...} ``` -**Invalid** +**Never** ```jsx import React, {Component} from 'react'; @@ -23,17 +32,17 @@ export default class MyComponent {...} ``` >This is a new class, does not extend React.Component. -## Constructor name matches component name +### Identical class and component names -Give `MyComponent.js` is a component attached to `stardust.MyComponent`: +Given `MyComponent.js` is a component attached to `stardust.MyComponent`: -**Valid** +**Always** ```jsx export default class MyComponent extends Component {...} ``` -**Invalid** +**Never** ```jsx export default class extends Component {...} @@ -43,30 +52,32 @@ export default class extends Component {...} ```jsx export default class YourComponent extends Component {...} ``` ->This class has the wrong name, it should be . +>This class has the wrong name, it should be `MyComponent`. + +### No wrapping elements -## Has the `sd-` element as its first child (no wrapper elements) +The element with the `sd-*` className is the first child (no wrapper elements). -**Valid** +**Always** ```jsx export default class Input extends Component { render() { return ( - + ); } } ``` -**Invalid** +**Never** ```jsx export default class Input extends Component { render() { return ( + ); } @@ -74,16 +85,68 @@ export default class Input extends Component { ``` >Never wrap the component with other components, whether DOM or Composite. -## Has props.className after `sd-` +## Events + +Stardust manages Semantic UI's jQuery via React events and lifecycle methods. + +Example, the Message component can be dismissed on click of the close icon. Per +the Semantic UI docs, this is done by calling Semantic UI's `transition()` +jQuery plugin on the message via a jQuery click event handler. Instead, +Stardust uses React's `onClick` listener to trigger the `transition()`. + +## Props + +### Spread props + +Stardust components [spread](https://facebook.github.io/react/docs/jsx-spread.html) +props onto the internal Semantic UI element of concern. This allows access to the +underlying element. + +**Always** -**Valid** +```jsx + +// => +``` + +```jsx + +// => +``` + +**Never** + +```jsx + +// => +``` +>`onFocus` prop was lost. + +```jsx + +// => +``` +>`data-my-plugin` prop was lost. + +### className + +Stardust components construct the `className` prop in this order. + +1. `sd-` +1. `ui` (Semantic UI class, if required) +1. `this.props.className` +1. `` (Semantic UI class) + +#### Inherits `props.className` after `sd-` + +**Always** ```jsx // =>
@@ -103,16 +166,16 @@ export default class Input extends Component { ``` >className was not inherited before sd-field -## Has `sd-` as the first class +#### Has `sd-` as the first class -**Valid** +**Always** ```jsx // =>
``` -**Invalid** +**Never** ```jsx @@ -126,17 +189,18 @@ export default class Input extends Component { ``` >`sd-divider` className does not come first. -## Has `ui` class immediately after `sd-` +#### Has `ui` immediately after `sd-` + Only for components that utilize the `ui` class (i.e `ui grid` but not `column`). -**Valid** +**Always** ```jsx // =>
``` -**Invalid** +**Never** ```jsx @@ -144,18 +208,18 @@ Only for components that utilize the `ui` class (i.e `ui grid` but not `column`) ``` >`grid` is immediately after `sd-grid`. -## Has props.className immediately after `ui` +#### Has `props.className` immediately after `ui` Only for components that utilize the `ui` class (i.e `ui form` but not `field`). -**Valid** +**Always** ```jsx
// =>
``` -**Invalid** +**Never** ```jsx @@ -168,32 +232,3 @@ Only for components that utilize the `ui` class (i.e `ui form` but not `field`). // =>
``` >Inherited `loading` className comes after `form`. - -## Spreads props -Allows access to the underlying element of concern. - -**Valid** - -```jsx - -// => -``` - -```jsx - -// => -``` - -**Invalid** - -```jsx - -// => -``` ->`onFocus` prop was lost. - -```jsx - -// => -``` ->`data-my-plugin` prop was lost. diff --git a/docs/app/Components/ComponentDoc/ComponentExample.js b/docs/app/Components/ComponentDoc/ComponentExample.js index f45e2791fa..57fca32c78 100644 --- a/docs/app/Components/ComponentDoc/ComponentExample.js +++ b/docs/app/Components/ComponentDoc/ComponentExample.js @@ -32,7 +32,7 @@ export default class ComponentExample extends Component { ); return ( - + diff --git a/docs/app/Examples/collections/Message/MessageExamples.js b/docs/app/Examples/collections/Message/MessageExamples.js new file mode 100644 index 0000000000..5515630af0 --- /dev/null +++ b/docs/app/Examples/collections/Message/MessageExamples.js @@ -0,0 +1,16 @@ +import React, {Component} from 'react'; +import Variations from './Variations/Variations'; +import States from './States/States'; +import Types from './Types/Types'; + +export default class MessageExamples extends Component { + render() { + return ( +
+ + + +
+ ); + } +} diff --git a/docs/app/Examples/collections/Message/States/States.js b/docs/app/Examples/collections/Message/States/States.js new file mode 100644 index 0000000000..250ef27647 --- /dev/null +++ b/docs/app/Examples/collections/Message/States/States.js @@ -0,0 +1,17 @@ +import React, {Component} from 'react'; +import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'; +import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'; + +export default class MessageStatesExamples extends Component { + render() { + return ( + + + + ); + } +} diff --git a/docs/app/Examples/collections/Message/States/Visible.js b/docs/app/Examples/collections/Message/States/Visible.js new file mode 100644 index 0000000000..a3e7ce524e --- /dev/null +++ b/docs/app/Examples/collections/Message/States/Visible.js @@ -0,0 +1,12 @@ +import React, {Component} from 'react'; +import {Message} from 'stardust'; + +export default class MessageVisibleExample extends Component { + render() { + return ( + + You can always see me + + ); + } +} diff --git a/docs/app/Examples/collections/Message/Types/DismissableBlock.js b/docs/app/Examples/collections/Message/Types/DismissableBlock.js new file mode 100644 index 0000000000..a0a857c840 --- /dev/null +++ b/docs/app/Examples/collections/Message/Types/DismissableBlock.js @@ -0,0 +1,12 @@ +import React, {Component} from 'react'; +import {Message} from 'stardust'; + +export default class MessageDismissableBlockExample extends Component { + render() { + return ( + + This is a special notification which you can dismiss. + + ); + } +} diff --git a/docs/app/Examples/collections/Message/Types/Icon.js b/docs/app/Examples/collections/Message/Types/Icon.js new file mode 100644 index 0000000000..c6c8aa3d13 --- /dev/null +++ b/docs/app/Examples/collections/Message/Types/Icon.js @@ -0,0 +1,18 @@ +import React, {Component} from 'react'; +import {Message} from 'stardust'; + +export default class MessageIconExample extends Component { + render() { + return ( +
+ + Get the best news in your e-mail every day. + + + + We're fetching that content for you. + +
+ ); + } +} diff --git a/docs/app/Examples/collections/Message/Types/Types.js b/docs/app/Examples/collections/Message/Types/Types.js new file mode 100644 index 0000000000..41e5120445 --- /dev/null +++ b/docs/app/Examples/collections/Message/Types/Types.js @@ -0,0 +1,22 @@ +import React, {Component} from 'react'; +import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'; +import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'; + +export default class MessageTypesExamples extends Component { + render() { + return ( + + + + + ); + } +} diff --git a/docs/app/Examples/collections/Message/Variations/Info.js b/docs/app/Examples/collections/Message/Variations/Info.js new file mode 100644 index 0000000000..1cbdf039e8 --- /dev/null +++ b/docs/app/Examples/collections/Message/Variations/Info.js @@ -0,0 +1,12 @@ +import React, {Component} from 'react'; +import {Message} from 'stardust'; + +export default class MessageInfoExample extends Component { + render() { + return ( + + Did you know it's been a while? + + ); + } +} diff --git a/docs/app/Examples/collections/Message/Variations/Variations.js b/docs/app/Examples/collections/Message/Variations/Variations.js new file mode 100644 index 0000000000..6d624a26d0 --- /dev/null +++ b/docs/app/Examples/collections/Message/Variations/Variations.js @@ -0,0 +1,22 @@ +import React, {Component} from 'react'; +import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'; +import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'; + +export default class MessageVariationsExamples extends Component { + render() { + return ( + + + + + ); + } +} diff --git a/docs/app/Examples/collections/Message/Variations/Warning.js b/docs/app/Examples/collections/Message/Variations/Warning.js new file mode 100644 index 0000000000..d7fbd1a7e6 --- /dev/null +++ b/docs/app/Examples/collections/Message/Variations/Warning.js @@ -0,0 +1,12 @@ +import React, {Component} from 'react'; +import {Message} from 'stardust'; + +export default class MessageWarningExample extends Component { + render() { + return ( + + Visit our registration page, then try again. + + ); + } +} diff --git a/gulp/plugins/gulp-react-docgen.js b/gulp/plugins/gulp-react-docgen.js index 2341fbf721..6110c45797 100644 --- a/gulp/plugins/gulp-react-docgen.js +++ b/gulp/plugins/gulp-react-docgen.js @@ -29,22 +29,29 @@ module.exports = function(filename) { return; } - var relativePath = file.path.replace(process.cwd() + '/', ''); - var parsed = docgen.parse(file.contents); + try { + var relativePath = file.path.replace(process.cwd() + '/', ''); + var parsed = docgen.parse(file.contents); - // replace the component`description` string with a parsed doc block object - parsed.docBlock = parseDocBlock(parsed.description); - delete parsed.description; + // replace the component`description` string with a parsed doc block object + parsed.docBlock = parseDocBlock(parsed.description); + delete parsed.description; - // replace prop `description` strings with a parsed doc block object - _.each(parsed.props, function(propDef, propName) { - parsed.props[propName].docBlock = parseDocBlock(propDef.description); - delete parsed.props[propName].description; - }); + // replace prop `description` strings with a parsed doc block object + _.each(parsed.props, function(propDef, propName) { + parsed.props[propName].docBlock = parseDocBlock(propDef.description); + delete parsed.props[propName].description; + }); - result[relativePath] = parsed; + result[relativePath] = parsed; - cb(); + cb(); + } catch (err) { + this.emit( + 'error', + new gutil.PluginError(pluginName, err) + ); + } } function endStream(cb) { diff --git a/gulp/tasks/build.js b/gulp/tasks/build.js index 312d681349..0def408feb 100644 --- a/gulp/tasks/build.js +++ b/gulp/tasks/build.js @@ -31,7 +31,10 @@ gulp.task('generate-doc-json', function(cb) { paths.srcViews + '/**/*.js', '!' + paths.src + '/**/Style.js' ]) - .pipe(g.plumber()) + .pipe(g.plumber(function(err) { + g.util.log(err); + this.emit('end'); + })) .pipe(gulpReactDocgen()) .pipe(gulp.dest(paths.docsApp)); }); diff --git a/index.js b/index.js index 1879567805..e16841c97c 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ import Grid from 'src/collections/Grid/Grid'; import Row from 'src/collections/Grid/Row'; import Menu from 'src/collections/Menu/Menu'; import MenuItem from 'src/collections/Menu/MenuItem'; +import Message from 'src/collections/Message/Message'; import Table from 'src/collections/Table/Table'; import TableColumn from 'src/collections/Table/TableColumn'; import TableCell from 'src/collections/Table/TableCell'; @@ -43,6 +44,7 @@ export default { Row, Menu, MenuItem, + Message, Table, TableColumn, TableCell, diff --git a/src/collections/Message/Message.js b/src/collections/Message/Message.js new file mode 100644 index 0000000000..4d55b1942a --- /dev/null +++ b/src/collections/Message/Message.js @@ -0,0 +1,66 @@ +import React, {Component, findDOMNode, PropTypes} from 'react'; +import classNames from 'classnames'; +import $ from 'jquery'; + +export default class Message extends Component { + static propTypes = { + children: PropTypes.node, + className: PropTypes.string, + dismissable: PropTypes.bool, + header: PropTypes.string, + icon: PropTypes.string, + }; + + componentDidMount() { + this.messageElm = $(findDOMNode(this.refs.message)); + } + + handleDismiss = (e) => { + this.messageElm.transition('fade'); + }; + + render() { + let classes = classNames( + 'sd-message', + 'ui', + this.props.className, + {icon: this.props.icon}, + 'message', + ); + + let iconClasses = classNames( + 'sd-message-icon', + this.props.icon, + 'icon' + ); + + let closeIcon = ; + let header =
{this.props.header}
; + let icon = ; + + // wrap children in

if there is a header + let children = this.props.header ?

{this.props.children}

: this.props.children; + + // wrap header and children in content if there is an icon + let content = ( +
+ {this.props.header && header} + {children} +
+ ); + + // prevent spreading icon classes as props on message element + let messageProps = _.clone(this.props); + delete messageProps.icon; + + return ( +
+ {this.props.dismissable && closeIcon} + {this.props.icon && icon} + {this.props.icon && content} + {!this.props.icon && this.props.header && header} + {!this.props.icon && this.props.children && children} +
+ ); + } +} diff --git a/test/mocks/SemanticjQuery-mock.js b/test/mocks/SemanticjQuery-mock.js index cfb4c24d62..5e208cdbfa 100644 --- a/test/mocks/SemanticjQuery-mock.js +++ b/test/mocks/SemanticjQuery-mock.js @@ -4,7 +4,9 @@ import sandbox from 'test/utils/Sandbox-util'; // // jQuery Mock // -let jQueryObject = {}; +let jQueryObject = { + on: sandbox.stub().returnsThis(), +}; function jQuery() { return jQueryObject; @@ -19,6 +21,7 @@ let jQueryPlugins = { popup: sandbox.stub().returnsThis(), checkbox: sandbox.stub().returnsThis(), modal: sandbox.stub().returnsThis(), + transition: sandbox.stub().returnsThis(), }; // Extend jQuery with plugins diff --git a/test/specs/collections/Message/Message-test.js b/test/specs/collections/Message/Message-test.js new file mode 100644 index 0000000000..89236b7450 --- /dev/null +++ b/test/specs/collections/Message/Message-test.js @@ -0,0 +1,61 @@ +import faker from 'faker'; +import React from 'react'; +const Simulate = React.addons.TestUtils.Simulate; +import {Message} from 'stardust'; + +describe('Message', () => { + describe('with header', () => { + it('has a header', () => { + let header = faker.hacker.phrase(); + let message = render(); + + message.findClass('sd-message-header'); + message.findText(header); + }); + }); + describe('without header', () => { + it('has no header', () => { + render().scryClass('sd-message-header') + .should.have.a.lengthOf(0); + }); + }); + describe('with icon', () => { + it('has an icon', () => { + render().findClass('sd-message-icon'); + }); + it('has a "content" wrapper', () => { + render().findClass('sd-message-content'); + }); + }); + describe('without icon', () => { + it('has no icon', () => { + render().scryClass('sd-message-icon') + .should.have.a.lengthOf(0); + }); + it('has no "content" wrapper', () => { + render().scryClass('sd-message-content') + .should.have.a.lengthOf(0); + }); + }); + describe('dismissable', () => { + it('adds a close icon', () => { + render().findClass('sd-message-close-icon'); + }); + + it('calls transition "fade" when dismissed', () => { + let tree = render(); + let message = tree.first(); + let closeIcon = tree.findClass('sd-message-close-icon'); + + message.messageElm.transition.called.should.equal(false); + Simulate.click(closeIcon); + message.messageElm.transition.calledWith('fade').should.equal(true); + }); + }); + describe('not dismissable', () => { + it('has no close icon', () => { + render().scryClass('sd-message-close-icon') + .should.have.a.lengthOf(0); + }); + }); +});