Skip to content

Commit

Permalink
Add VerticalNavigation, Masthead, Item, and related components
Browse files Browse the repository at this point in the history
  • Loading branch information
mturley committed Feb 2, 2018
1 parent 1ce73ad commit 279a64b
Show file tree
Hide file tree
Showing 29 changed files with 3,572 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ node_modules
npm-debug.log
dist
.out
.DS_Store

# IDEs and editors
/.idea
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"homepage": "https://github.com/patternfly/patternfly-react#readme",
"dependencies": {
"breakjs": "^1.0.0",
"classnames": "^2.2.5",
"patternfly": "^3.37.6",
"react-bootstrap": "^0.31.5",
Expand Down
13 changes: 11 additions & 2 deletions src/common/Timer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@ class Timer {
this.execute = func;
}

startTimer() {
// startTimer optionally takes a new func and delay so the timer instance can be reused.
startTimer(func, delay) {
this.clearTimer();

if (func) this.execute = func;
if (delay) this.delay = delay;
this.timer = setTimeout(this.execute, this.delay);
}
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
skipTimer() {
if (this.timer) {
this.execute(true); // execute can take an optional `skipped` argument
this.clearTimer();
}
}
}
Expand Down
122 changes: 122 additions & 0 deletions src/common/controlled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import PropTypes from 'prop-types';
import { nullValues, bindMethods, selectKeys, filterKeys } from './helpers';

/*
controlled(stateTypes, defaults)(WrappedComponent)
This Higher Order Component provides the controlled component pattern on a prop-by-prop basis.
It's a nice way for components to implement internal state so they "just work" out of the box,
but also give users the option of lifting some or all of that state up into their application.
controlled() takes arguments as an object with three options:
* types - an object of PropTypes for the state that will be contained here
* defaults - an optional object with default values for stateTypes
* persist - an optional array of names from stateTypes which will be persisted to sessionStorage
The WrappedComponent will be rendered with special props:
* setControlledState - a reference to this state wrapper's this.setState.
* Props for all the stateTypes, from this.props if present or from this.state otherwise.
* All other props passed to the controlled component HoC.
The idea is that the values in stateTypes could be stored in state, or passed in via props.
The WrappedComponent doesn't have to care which is being used, and can manage the state
contained here. When present, props are used instead. If you provide these special props,
be sure to also provide corresponding callbacks/handlers to keep them updated.
If you are using the persist option, you can optionally pass a sessionKey prop to the component
to ensure multiple instances of the component store their data separately. If you don't pass
a sessionKey, a stringified list of the persisted keys will be used (not unique to the instance).
*/
const controlled = ({ types, defaults = {}, persist }) => WrappedComponent => {
class ControlledComponent extends React.Component {
constructor() {
super();
this.state = { ...nullValues(types), ...defaults };
bindMethods(this, [
'sessionKey',
'savePersistent',
'loadPersistent',
'setControlledState'
]);
}

componentDidMount() {
this.loadPersistent();
window &&
window.addEventListener &&
window.addEventListener('beforeunload', this.savePersistent);
}

componentWillUnmount() {
this.savePersistent();
window &&
window.removeEventListener &&
window.removeEventListener('beforeunload', this.savePersistent);
}

sessionKey() {
return this.props.sessionKey || JSON.stringify(persist);
}

savePersistent() {
if (persist && persist.length > 0) {
const toPersist = selectKeys(this.state, persist);
window &&
window.sessionStorage &&
window.sessionStorage.setItem(
this.sessionKey(),
JSON.stringify(toPersist)
);
}
}

loadPersistent() {
if (persist && persist.length > 0) {
const fromPersisted =
window &&
window.sessionStorage &&
window.sessionStorage.getItem(this.sessionKey());
fromPersisted && this.setState(JSON.parse(fromPersisted));
}
}

setControlledState(updater) {
this.setState(updater);
}

render() {
const controlledStateProps = filterKeys(
this.props,
key => types.hasOwnProperty(key) && this.props[key] !== null
);
const otherProps = filterKeys(
this.props,
key => !types.hasOwnProperty(key)
);
return (
<WrappedComponent
setControlledState={this.setControlledState}
{...this.state}
{...controlledStateProps}
{...otherProps}
/>
);
}
}

ControlledComponent.displayName = WrappedComponent.displayName;
ControlledComponent.propTypes = {
...WrappedComponent.propTypes,
...types,
sessionKey: PropTypes.string
};

ControlledComponent.defaultProps = {
...WrappedComponent.defaultProps
};

return ControlledComponent; // TODO use recompose withState or withStateHandlers here instead of component state above
};

export default controlled;
31 changes: 31 additions & 0 deletions src/common/helpers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
import React from 'react';

export const bindMethods = (context, methods) => {
methods.forEach(method => {
context[method] = context[method].bind(context);
});
};

// Returns a subset of the given object including only the given keys, with values optionally replaced by a fn.
export const selectKeys = (obj, keys, fn = val => val) =>
keys.reduce((values, key) => ({ ...values, [key]: fn(obj[key]) }), {});

// Returns a subset of the given object with a validator function applied to its keys.
export const filterKeys = (obj, validator) =>
selectKeys(obj, Object.keys(obj).filter(validator));

export const childrenToArray = children =>
children &&
React.Children.count(children) > 0 &&
React.Children.toArray(children);

export const filterChildren = (children, validator) => {
const array = childrenToArray(children);
return array && array.filter(validator);
};

export const findChild = (children, validator) => {
const array = childrenToArray(children);
return array && array.find(validator);
};

export const propsChanged = (propNames, oldProps, newProps) =>
propNames.some(propName => oldProps[propName] !== newProps[propName]);

// Returns an object with the same keys as the given one, but all null values.
export const nullValues = obj => selectKeys(obj, Object.keys(obj), () => null);

export const noop = Function.prototype;
14 changes: 13 additions & 1 deletion src/common/patternfly.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import 'patternfly/dist/js/patternfly-settings';
import 'patternfly/dist/js/patternfly-settings-charts';
import Break from 'breakjs';

const patternfly = window.patternfly;
const c3ChartDefaults = patternfly.c3ChartDefaults();

export { patternfly, c3ChartDefaults };
const mockLayout = {
is: layout => layout === 'desktop',
addChangeListener: () => {},
removeChangeListener: () => {}
};

const layout =
process.env.NODE_ENV === 'test'
? mockLayout
: Break({ mobile: 0, ...patternfly.pfBreakpoints });

export { patternfly, c3ChartDefaults, layout };
1 change: 1 addition & 0 deletions src/components/Navbar/Navbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Navbar as default } from 'react-bootstrap';
1 change: 1 addition & 0 deletions src/components/Navbar/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Navbar } from './Navbar';
Loading

0 comments on commit 279a64b

Please sign in to comment.