-
Notifications
You must be signed in to change notification settings - Fork 14
Nessie Dev Guide
This guide to creating Nessie components is work in progress, feel free to contribute.
Nessie components are React components. They are:
- ✅ Purely presentational
- ✅ (Almost) always stateless
- ✅ They use modular/namespaced CSS
Nessie components by themselves are not interactive
nessie-ui
is a library of components, while loch-ness
is a webapp for viewing those components.
-
yarn start
: Run LochNess in dev mode. -
yarn test
: Run tests. -
yarn tdd
: Initialize test driven development. -
yarn build
: Generate up-to-date dist components and CSS for use.
Post install, yarn test:v:build
should be run to generate the test site.
You can then yarn test:v
to run visual tests or yarn test:v -- -f <component>
to test a particular component.
yarn test:ref
generates new reference screenshots.
Chrome Canary is required to run visual tests.
On macOS, sometimes visual tests start failing to due Canary instances hanging around. Check for any running instances in the macOS Activity Monitor and close them, then everything should go fine.
Each component lives inside a folder with that component’s name in PascalCase inside /src
. Each component should have the following files:
- ✅
ComponentName/index.jsx
which renders the full component (noteComponentName
initial capital) - ✅
ComponentName/componentName.css
which contains the CSS for the component (notecomponentName
initial lowercase) - ✅
ComponentName/README.md
with component description in Markdown format - ✅
ComponentName/tests.jsx
with component tests (we use Enzyme; note.jsx
file extension) - ✅
ComponentName/driver.js
with component test drivers (note.js
extension)
Be sure to add your component to src/index.js
when it’s ready to be exported in the Nessie dist bundle.
Every component should:
- ✅ have a
propTypes
object with all props defined (including our “standard” props:children
,className
,cssMap
, etc.) - ✅ have a
defaultProps
object with default values defined for all props*. - ✅ use the
buildClassName
helper function to set theclassName
of the outermost<div>
(or other element) of the component - ✅ have
defaultProps.cssMap
defined by importing./componentName.css
as an object
* Example props should not be defined in defaultProps
. Props to be used as example data can be added to src/defaults.json
. These will be used by LochNess for demo purposes.
import React from 'react';
import PropTypes from 'prop-types';
import { buildClassName } from '../utils';
import styles from './componentName.css';
const ComponentName = ( {
className,
cssMap,
prop1,
prop2,
...
} ) =>
{
...
return (
<div
className = { buildClassName( className, cssMap, {
class1 : prop1,
class2 : prop2,
...
} ) }>
... component markup here ...
</div>
);
};
ComponentName.propTypes =
{
/**
* CSS class name
*/
className : PropTypes.string,
/**
* CSS class map
*/
cssMap : PropTypes.objectOf( PropTypes.string ),
/**
* Prop 1 description...
*/
prop1 : PropTypes.prop1Type,
/**
* Prop 2 description...
*/
prop2 : PropTypes.prop2Type,
...
};
ComponentName.defaultProps =
{
className : undefined,
cssMap : styles,
prop1 : ...,
prop2 : ...,
};
export default ComponentName;
Please keep the propTypes
and defaultProps
definitions in alphabetical order.
(Almost) all Nessie components use the Css wrapper component to inject the className
prop into their root element.
The Css component is sort of like a higher-order component but not quite (more like a higher-order element actually). We call this type of component an injector component.
It will always look for a .default
class defined in './componentName.css'
and apply this as the base CSS string.
The className
of the outermost <div>
(or other element) of the component must be set as the className
prop of the component:
<Css cssMap = { cssMap }>
<div className = { className }>
... component markup here ...
</div>
</Css>
The buildClassName
helper function should be used to set the className
of the outer div (or other element) of the component.
<div className = { buildClassName( className, cssMap, cssProps ) } ...>
...
</div>
Additional CSS classes for the component can be toggled using third argument of buildClassName
: the cssProps
object. The object that can contain entries of the format className: (boolean)
or className: (string)
.
-
If no
cssProps
entries are defined, the outermost<div>
of the component will getclass="default"
. -
Adding
disabled: isDisabled
to theCssProps
object, the outermost<div>
getsclass="default disabled"
wheneverisDisabled
istrue
. -
Adding
align: textAlign
to theCssProps
object, the outermost<div>
getsclass="default align__left"
wheretextAlign
is'left'
. -
If we set the
className
argument then that value gets added to the end of the CSS string. For example, given the above examples and aclassName
prop of'myExtraClassName'
we get:class="default disabled align__left myExtraClassName"
.
To apply a class .myClassName
from './componentName.css'
to an element inside your component (i.e. not the root element) use className = { cssMap.myClassName }
:
<div className = { buildClassName( className, cssMap ) }>
<div className = { cssMap.myClassName }>...</div>
<div className = { cssMap.anotherClass }>...</div>
</div>
Where possible all styling should be achieved via CSS (eg: :hover
styles), rather than JS.
The CSS file should (almost) always:
- ✅ import
proto/base.css
to set our common CSS properties - ✅ contain a
.default
class
It will often:
- import
proto/definitions/_colors.css
orproto/definitions/_fonts.css
, where necessary - contain the classes
.disabled
,.error
,.fakeHovered
@import "../proto/base.css";
...
.default
{
... default CSS rules for root element ...
.myClassName
{
... default CSS rules for a child element ...
}
.anotherClass
{
... default CSS rules for another child element ...
}
...
}
.disabled
{
... rules for root element when component isDisabled ...
.myClassName
{
... rules for a child element when component isDisabled ...
}
...
}
.error
{
... rules for root element when component hasError ...
.anotherClass
{
... rules for a child element when component hasError ...
}
...
}
... more CSS blocks corresponding to cssProps classes ...
CSS variables should be used instead of magic numbers. To make the most of CSS variables they can be combined with CSS calc
, e.g:
border-radius: calc( var( --switchHeight ) / 2 );
The CSS variable naming conventions are as follows:
-
--camelCase
for regular variables (no underscore or hyphen) -
--UPPER_CASE
for global constants (always underscore, never hyphen)
Each component should implement unit tests in src/ComponentName/tests.jsx
.
We use Enzyme to implement our component tests.
- ✅ use
describe()
blocks to group tests - ✅ write
it()
blocks that read as real English sentences that start withit...
- ✅ use Enzyme ShallowWrapper (
shallow()
) - ✅ wherever possible
find()
nodes by matching components (find( Component )
orfind( { prop: 'value' } )
) - ✅ if it’s necessary to find a node by CSS class, use the class returned by cssMap:
find( `.${cssMap.whatever}` )
- ❌ use Enzyme ReactWrapper (
mount()
) - ❌ use
dive()
- ❌ find nodes using hard-coded CSS class strings
This is a good thing. Any refs required for testing purposes should be mocked:
const wrapper = shallow( <TextInputWithIcon /> );
const instance = wrapper.instance();
instance.refs.input = <input id="mockedInput"/>;
...
We can also test that refs are correctly set even though they are not available on the ShallowWrapper:
Let’s say we have in our component’s render method:
<input ref = { ref => this.input = ref } />
we can test that the ref gets stored under the correct key (here, this.input
) by manually calling the ref callback function:
const instance = wrapper.instance;
const inputEl = wrapper.find( input ).node;
inputEl.ref( inputEl );
expect( instance.input ).to.equal( inputEl );
If you need to find()
a thing contained inside a prop that is a JSX node, just call shallow()
on the prop:
const wrapper = shallow(
<Module customHeader={ <Row><Column><Button>Click me</Button></Column></Row> } />
);
const instance = wrapper.instance();
const headerWrapper = shallow( instance.props.customHeader );
headerWrapper.find( Button ).simulate( 'click' );
...
If you want your component to show up in LochNess, all you need to do is make sure it’s listed in src/index.js
(please keep the file in alphabetical order):
...
export ComponentName from './ComponentName';
...
If you want to add some example/demo props, you can put them in src/defaults.json
. These will be used by LochNess for demo purposes.
...
"ComponentName" :
{
"prop1" : "Example data!",
"prop2" : ...,
...
},
...
(Please keep this file in alphabetical order too!)
LochNess-specific flags are added for a given component using a lochness
object prop:
...
"ComponentName" :
{
...,
"lochness" : { ...LochNess flags... }
},
...
To be flag a given component as beta:
...
"ComponentName" :
{
...,
"lochness" : { "isBeta" : true }
},
...
To disable the HTML code export for a given component:
...
"ComponentName" :
{
...,
"lochness" : { "disableCode" : true }
},
...
This is useful for components that are wrappers for external libraries (e.g. Flounder, CodeMirror, ...)