grommet
Version:
The most advanced UX framework for enterprise applications.
697 lines (598 loc) • 24.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _objectWithoutProperties2 = require('babel-runtime/helpers/objectWithoutProperties');
var _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2);
var _defineProperty2 = require('babel-runtime/helpers/defineProperty');
var _defineProperty3 = _interopRequireDefault(_defineProperty2);
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _classnames6 = require('classnames');
var _classnames7 = _interopRequireDefault(_classnames6);
var _KeyboardAccelerators = require('../utils/KeyboardAccelerators');
var _KeyboardAccelerators2 = _interopRequireDefault(_KeyboardAccelerators);
var _Intl = require('../utils/Intl');
var _Intl2 = _interopRequireDefault(_Intl);
var _CSSClassnames = require('../utils/CSSClassnames');
var _CSSClassnames2 = _interopRequireDefault(_CSSClassnames);
var _Announcer = require('../utils/Announcer');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var CLASS_ROOT = _CSSClassnames2.default.DISTRIBUTION; // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
var COLOR_INDEX = _CSSClassnames2.default.COLOR_INDEX;
var BACKGROUND_COLOR_INDEX = _CSSClassnames2.default.BACKGROUND_COLOR_INDEX;
var DEFAULT_WIDTH = 400;
var DEFAULT_HEIGHT = 200;
var SMALL_SIZE = 120;
var THIN_HEIGHT = 72;
var GUTTER_SIZE = 4;
// We pad the labels here instead of CSS to keep the DOM simple for handling
// text overflow.
var LABEL_PAD_VERTICAL = 6;
var LABEL_PAD_HORIZONTAL = 12;
var Distribution = function (_Component) {
(0, _inherits3.default)(Distribution, _Component);
function Distribution(props, context) {
(0, _classCallCheck3.default)(this, Distribution);
var _this = (0, _possibleConstructorReturn3.default)(this, (Distribution.__proto__ || (0, _getPrototypeOf2.default)(Distribution)).call(this, props, context));
_this._onEnter = _this._onEnter.bind(_this);
_this._onPreviousDistribution = _this._onPreviousDistribution.bind(_this);
_this._onNextDistribution = _this._onNextDistribution.bind(_this);
_this._onActivate = _this._onActivate.bind(_this);
_this._onDeactivate = _this._onDeactivate.bind(_this);
_this._onResize = _this._onResize.bind(_this);
_this._layout = _this._layout.bind(_this);
_this._placeItems = _this._placeItems.bind(_this);
_this.state = _this._stateFromProps(props);
_this.state.width = DEFAULT_WIDTH;
_this.state.height = DEFAULT_HEIGHT;
_this.state.activeIndex = 0;
_this.state.mouseActive = false;
return _this;
}
(0, _createClass3.default)(Distribution, [{
key: 'componentDidMount',
value: function componentDidMount() {
this._keyboardHandlers = {
left: this._onPreviousDistribution,
up: this._onPreviousDistribution,
right: this._onNextDistribution,
down: this._onNextDistribution,
enter: this._onEnter,
space: this._onEnter
};
_KeyboardAccelerators2.default.startListeningToKeyboard(this, this._keyboardHandlers);
window.addEventListener('resize', this._onResize);
this._onResize();
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(newProps) {
var state = this._stateFromProps(newProps);
// preserve width and height we calculated already
state.width = this.state.width;
state.height = this.state.height;
state.needLayout = true;
this.setState(state);
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate() {
if (this.state.needLayout) {
this.setState({ needLayout: false, items: undefined }, this._layout);
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
_KeyboardAccelerators2.default.stopListeningToKeyboard(this, this._keyboardHandlers);
clearTimeout(this._resizeTimer);
window.removeEventListener('resize', this._onResize);
}
}, {
key: '_seriesTotal',
value: function _seriesTotal(series) {
var total = 0;
series.some(function (datum) {
total += datum.value;
});
return total;
}
// Generates state based on the provided props.
}, {
key: '_stateFromProps',
value: function _stateFromProps(props) {
var total = void 0;
var allIcons = false;
if (props.series) {
total = this._seriesTotal(props.series);
allIcons = !props.series.some(function (datum) {
return !datum.icon;
});
} else {
total = 100;
}
return {
allIcons: allIcons,
total: total
};
}
}, {
key: '_boxRect',
value: function _boxRect(itemRect, width, height) {
// leave a gutter between items, if we're not at the edge
var boxRect = (0, _extends3.default)({}, itemRect);
if (0 !== boxRect.x && width > boxRect.x + boxRect.width) {
boxRect.x += GUTTER_SIZE / 2;
boxRect.width -= GUTTER_SIZE;
}
if (0 !== boxRect.y && height > boxRect.y + boxRect.height) {
boxRect.y += GUTTER_SIZE / 2;
boxRect.height -= GUTTER_SIZE;
}
boxRect.width -= GUTTER_SIZE;
boxRect.height -= GUTTER_SIZE;
// flush the right edge
if (boxRect.x + boxRect.width > width - 2 * GUTTER_SIZE) {
boxRect.width = width - boxRect.x;
}
// flush the bottom edge
if (boxRect.y + boxRect.height > height - 2 * GUTTER_SIZE) {
boxRect.height = height - boxRect.y;
}
return boxRect;
}
}, {
key: '_labelRect',
value: function _labelRect(boxRect) {
// pad the labels here to keep the DOM simple w.r.t overflow text
var labelRect = (0, _extends3.default)({}, boxRect);
labelRect.x += LABEL_PAD_HORIZONTAL;
labelRect.width -= LABEL_PAD_HORIZONTAL * 2;
labelRect.y += LABEL_PAD_VERTICAL;
labelRect.height -= LABEL_PAD_VERTICAL * 2;
return labelRect;
}
}, {
key: '_placeItems',
value: function _placeItems() {
var _this2 = this;
var width = this.state.width;
var height = this.state.height;
var areaPer = width * height / this.state.total;
var remainingRect = { x: 0, y: 0, width: width, height: height };
var items = [];
var series = this.props.series ? this.props.series.slice(0) : [];
var _loop = function _loop() {
var datum = series.shift();
if (datum.value <= 0) {
return 'continue';
}
// Start a new group.
var groupValue = datum.value;
var targetGroupValue = void 0;
// Make the first item as square as possible.
var itemArea = areaPer * datum.value;
var edgeLength = Math.round(Math.sqrt(itemArea));
var itemHeight = void 0;
var itemWidth = void 0;
// Figure out how much value we can fit inside a rectangle
// that takes the full minor axis length
if (remainingRect.width > remainingRect.height) {
// landscape, lay out left to right
itemHeight = Math.min(remainingRect.height, edgeLength);
itemWidth = Math.round(itemArea / itemHeight);
targetGroupValue = Math.round(itemWidth * remainingRect.height / areaPer);
} else {
// portrait, lay out top to bottom
itemWidth = Math.min(remainingRect.width, edgeLength);
itemHeight = Math.round(itemArea / itemWidth);
targetGroupValue = Math.round(itemHeight * remainingRect.width / areaPer);
}
// Group items until we reach the target group value.
var group = [datum];
while (groupValue < targetGroupValue && series.length > 0) {
var datum2 = series.shift();
groupValue += datum2.value;
group.push(datum2);
}
// Now that we know the actual value of the group, give it a
// rectangle whose area corresponds to the actual group value.
var groupRect = void 0;
if (remainingRect.width > remainingRect.height) {
// landscape, lay out left to right
groupRect = { x: remainingRect.x, y: remainingRect.y,
width: Math.round(areaPer * groupValue / remainingRect.height),
height: remainingRect.height };
remainingRect.x += groupRect.width;
remainingRect.width -= groupRect.width;
} else {
// portrait, lay out top to bottom
groupRect = { x: remainingRect.x, y: remainingRect.y,
width: remainingRect.width,
height: Math.round(areaPer * groupValue / remainingRect.width) };
remainingRect.y += groupRect.height;
remainingRect.height -= groupRect.height;
}
// Place items within the group rectangle.
// We take the full minor axis length and as much major axis length
// as needed to match the item's area.
group.forEach(function (datum) {
var itemRect = void 0;
if (groupRect.width > groupRect.height) {
// landscape, use full height
itemRect = { x: groupRect.x, y: groupRect.y,
width: Math.round(areaPer * datum.value / groupRect.height),
height: groupRect.height };
groupRect.x += itemRect.width;
groupRect.width -= itemRect.width;
} else {
// portrait, use full width
itemRect = { x: groupRect.x, y: groupRect.y,
width: groupRect.width,
height: Math.round(areaPer * datum.value / groupRect.width) };
groupRect.y += itemRect.height;
groupRect.height -= itemRect.height;
}
var boxRect = _this2._boxRect(itemRect, width, height);
var labelRect = _this2._labelRect(boxRect);
// Save this so we can render the item's box and label
// in the correct location.
items.push({ datum: datum, rect: itemRect,
boxRect: boxRect, labelRect: labelRect });
});
};
while (series.length > 0) {
var _ret = _loop();
if (_ret === 'continue') continue;
}
this.setState({ items: items });
}
}, {
key: '_onResize',
value: function _onResize() {
// debounce
clearTimeout(this._resizeTimer);
this._resizeTimer = setTimeout(this._layout, 50);
}
}, {
key: '_layout',
value: function _layout() {
var container = this._containerRef;
var rect = container.getBoundingClientRect();
var width = Math.round(rect.width);
var height = Math.round(rect.height);
if (width !== this.state.width || height !== this.state.height || !this.state.items) {
this.setState({
width: width,
height: height
}, this._placeItems);
}
}
}, {
key: '_itemColorIndex',
value: function _itemColorIndex(item, index) {
return item.colorIndex || 'graph-' + (index + 1);
}
}, {
key: '_onPreviousDistribution',
value: function _onPreviousDistribution(event) {
event.preventDefault();
if (this._distributionRef.contains(document.activeElement)) {
if (this.state.activeIndex - 1 >= 0) {
this._onActivate(this.state.activeIndex - 1);
}
}
//stop event propagation
return true;
}
}, {
key: '_onNextDistribution',
value: function _onNextDistribution(event) {
event.preventDefault();
if (this._distributionRef.contains(document.activeElement)) {
var totalDistributionCount = _reactDom2.default.findDOMNode(this.distributionItemsRef).childNodes.length;
if (this.state.activeIndex + 1 < totalDistributionCount) {
this._onActivate(this.state.activeIndex + 1);
}
}
//stop event propagation
return true;
}
}, {
key: '_onEnter',
value: function _onEnter(event) {
if (this._distributionRef.contains(document.activeElement) && this.activeDistributionRef) {
var index = this.activeDistributionRef.getAttribute('data-index');
var activeDistribution = this.props.series.filter(function (item) {
return item.value > 0;
})[index];
//trigger click on active distribution
if (activeDistribution.onClick) {
activeDistribution.onClick();
}
}
}
}, {
key: '_onActivate',
value: function _onActivate(index) {
var _this3 = this;
var intl = this.context.intl;
this.setState({ activeIndex: index }, function () {
var activeMessage = _this3.activeDistributionRef.getAttribute('aria-label');
var clickable = _this3.state.items[_this3.state.activeIndex].datum.onClick;
var enterSelectMessage = '(' + _Intl2.default.getMessage(intl, 'Enter Select') + ')';
(0, _Announcer.announce)(activeMessage + ' ' + (clickable ? enterSelectMessage : ''));
});
}
}, {
key: '_onDeactivate',
value: function _onDeactivate() {
this.setState({ activeIndex: 0 });
}
}, {
key: '_renderItemLabel',
value: function _renderItemLabel(datum, labelRect, index) {
var _classnames;
var activeIndex = this.state.activeIndex;
var labelClasses = (0, _classnames7.default)(CLASS_ROOT + '__label', (_classnames = {}, (0, _defineProperty3.default)(_classnames, BACKGROUND_COLOR_INDEX + '-' + this._itemColorIndex(datum, index), !datum.icon), (0, _defineProperty3.default)(_classnames, CLASS_ROOT + '__label--icons', datum.icon), (0, _defineProperty3.default)(_classnames, CLASS_ROOT + '__label--small', labelRect.width < SMALL_SIZE || labelRect.height < SMALL_SIZE), (0, _defineProperty3.default)(_classnames, CLASS_ROOT + '__label--thin', labelRect.height < THIN_HEIGHT), (0, _defineProperty3.default)(_classnames, CLASS_ROOT + '__label--active', index === activeIndex), _classnames));
var value = datum.labelValue !== undefined ? datum.labelValue : datum.value;
return _react2.default.createElement(
'div',
{ key: index, className: labelClasses,
'data-box-index': index, role: 'presentation',
style: { top: labelRect.y, left: labelRect.x, maxWidth: labelRect.width,
maxHeight: labelRect.height } },
_react2.default.createElement(
'span',
{ className: CLASS_ROOT + '__label-value' },
value,
_react2.default.createElement(
'span',
{ className: CLASS_ROOT + '__label-units' },
this.props.units
)
),
_react2.default.createElement(
'span',
{ className: CLASS_ROOT + '__label-label' },
datum.label
)
);
}
}, {
key: '_renderItemBox',
value: function _renderItemBox(boxRect, colorIndex) {
var boxClasses = (0, _classnames7.default)(CLASS_ROOT + '__item-box', (0, _defineProperty3.default)({}, COLOR_INDEX + '-' + colorIndex, colorIndex));
return _react2.default.createElement('rect', { className: boxClasses, x: boxRect.x, y: boxRect.y,
width: boxRect.width, height: boxRect.height });
}
}, {
key: '_renderItemIcon',
value: function _renderItemIcon(icon, itemRect, colorIndex) {
var iconClasses = (0, _classnames7.default)(CLASS_ROOT + '__item-icons', COLOR_INDEX + '-' + colorIndex);
var icons = [];
// fill box with icons
var iconX = 0;
var iconY = 0;
var iconIndex = 1;
while (iconY < itemRect.height - icon.height) {
while (iconX < itemRect.width - icon.width) {
var transform = 'translate(' + (itemRect.x + iconX) + ', ' + (itemRect.y + iconY) + ')';
icons.push(_react2.default.createElement(
'g',
{ key: iconIndex, transform: transform },
icon.svgElement
));
iconX += icon.width;
iconIndex += 1;
}
iconY += icon.height;
iconX = 0;
}
return _react2.default.createElement(
'g',
{ className: iconClasses },
icons
);
}
}, {
key: '_renderItem',
value: function _renderItem(datum, rect, index) {
var _this4 = this;
var units = this.props.units;
var itemClasses = (0, _classnames7.default)(CLASS_ROOT + '__item', (0, _defineProperty3.default)({}, CLASS_ROOT + '__item--clickable', datum.onClick));
var activeDistributionRef = void 0;
if (index === this.state.activeIndex) {
activeDistributionRef = function activeDistributionRef(ref) {
return _this4.activeDistributionRef = ref;
};
}
var colorIndex = this._itemColorIndex(datum, index);
var contents = void 0;
if (datum.icon) {
contents = this._renderItemIcon(datum.icon, rect, colorIndex);
} else {
contents = this._renderItemBox(rect, colorIndex);
}
var value = datum.labelValue !== undefined ? datum.labelValue : datum.value;
var labelMessage = value + ' ' + (units || '') + ' ' + datum.label;
return _react2.default.createElement(
'g',
{ key: index, className: itemClasses,
onMouseOver: this._onActivate.bind(this, index),
onMouseLeave: this._onDeactivate, tabIndex: '-1',
role: datum.onClick ? 'button' : 'row',
ref: activeDistributionRef, 'aria-label': labelMessage,
onFocus: function onFocus() {
return _this4.setState({ activeIndex: index });
},
'data-index': index, onClick: datum.onClick },
contents
);
}
}, {
key: '_renderBoxes',
value: function _renderBoxes() {
var _this5 = this;
return this.state.items.map(function (item, index) {
return _this5._renderItem(item.datum, item.boxRect, index);
});
}
}, {
key: '_renderLabels',
value: function _renderLabels() {
var _this6 = this;
return this.state.items.map(function (item, index) {
return _this6._renderItemLabel(item.datum, item.labelRect, index);
});
}
}, {
key: '_renderLoading',
value: function _renderLoading() {
var _state = this.state,
height = _state.height,
width = _state.width;
var loadingClasses = (0, _classnames7.default)(CLASS_ROOT + '__loading-indicator', COLOR_INDEX + '-loading');
var loadingHeight = height / 2;
var loadingWidth = width;
var commands = 'M0,' + loadingHeight + ' L' + loadingWidth + ',' + loadingHeight;
return _react2.default.createElement(
'g',
{ key: 'loading' },
_react2.default.createElement('path', { stroke: 'none', className: loadingClasses, d: commands })
);
}
}, {
key: 'render',
value: function render() {
var _classnames4,
_this7 = this;
var _props = this.props,
a11yTitle = _props.a11yTitle,
className = _props.className,
full = _props.full,
size = _props.size,
vertical = _props.vertical,
props = (0, _objectWithoutProperties3.default)(_props, ['a11yTitle', 'className', 'full', 'size', 'vertical']);
delete props.series;
delete props.units;
var intl = this.context.intl;
var _state2 = this.state,
allIcons = _state2.allIcons,
focus = _state2.focus,
height = _state2.height,
items = _state2.items,
mouseActive = _state2.mouseActive,
width = _state2.width;
var classes = (0, _classnames7.default)(CLASS_ROOT, (_classnames4 = {}, (0, _defineProperty3.default)(_classnames4, CLASS_ROOT + '--full', full), (0, _defineProperty3.default)(_classnames4, CLASS_ROOT + '--icons', allIcons), (0, _defineProperty3.default)(_classnames4, CLASS_ROOT + '--' + size, size), (0, _defineProperty3.default)(_classnames4, CLASS_ROOT + '--vertical', vertical), (0, _defineProperty3.default)(_classnames4, CLASS_ROOT + '--loading', (items || []).length === 0), _classnames4), className);
var background = void 0;
if (!allIcons) {
background = _react2.default.createElement('rect', { className: CLASS_ROOT + '__background', x: 0, y: 0, stroke: 'none',
width: width, height: height });
}
var boxes = [];
var labels = void 0;
if (items) {
boxes = this._renderBoxes();
labels = this._renderLabels();
}
var role = 'group';
var ariaLabel = a11yTitle || _Intl2.default.getMessage(intl, 'Distribution');
var navigationHelpMessage = _Intl2.default.getMessage(intl, 'Navigation Help');
ariaLabel += ' (' + navigationHelpMessage + ')';
if (boxes.length === 0) {
boxes.push(this._renderLoading());
role = 'img';
ariaLabel = _Intl2.default.getMessage(intl, 'Loading');
}
var graphicClasses = (0, _classnames7.default)(CLASS_ROOT + '__graphic', (0, _defineProperty3.default)({}, CLASS_ROOT + '__graphic--focus', focus));
return _react2.default.createElement(
'div',
(0, _extends3.default)({ ref: function ref(_ref3) {
return _this7._containerRef = _ref3;
} }, props, { className: classes }),
_react2.default.createElement(
'svg',
{ ref: function ref(_ref) {
return _this7._distributionRef = _ref;
},
className: graphicClasses,
viewBox: '0 0 ' + this.state.width + ' ' + this.state.height,
preserveAspectRatio: 'none', tabIndex: '0', role: role,
'aria-label': ariaLabel,
onMouseDown: function onMouseDown() {
return _this7.setState({ mouseActive: true });
},
onMouseUp: function onMouseUp() {
return _this7.setState({ mouseActive: false });
},
onFocus: function onFocus() {
if (mouseActive === false) {
_this7.setState({ focus: true });
}
},
onBlur: function onBlur() {
return _this7.setState({
focus: false
});
} },
background,
boxes
),
_react2.default.createElement(
'div',
{ ref: function ref(_ref2) {
return _this7.distributionItemsRef = _ref2;
},
className: CLASS_ROOT + '__labels', role: 'presentation',
'aria-hidden': true },
labels
)
);
}
}]);
return Distribution;
}(_react.Component);
Distribution.displayName = 'Distribution';
exports.default = Distribution;
Distribution.contextTypes = {
intl: _react.PropTypes.object
};
Distribution.propTypes = {
a11yTitle: _react.PropTypes.string,
full: _react.PropTypes.bool, // deprecated, use size='full'
series: _react.PropTypes.arrayOf(_react.PropTypes.shape({
label: _react.PropTypes.node,
value: _react.PropTypes.number.isRequired,
colorIndex: _react.PropTypes.string,
important: _react.PropTypes.bool,
onClick: _react.PropTypes.func,
icon: _react.PropTypes.shape({
width: _react.PropTypes.number,
height: _react.PropTypes.number,
svgElement: _react.PropTypes.node
})
})),
size: _react.PropTypes.oneOf(['small', 'medium', 'large', 'full']),
units: _react.PropTypes.string,
vertical: _react.PropTypes.bool
};
Distribution.defaultProps = {
size: 'medium'
};
module.exports = exports['default'];