UNPKG

grommet

Version:

The most advanced UX framework for enterprise applications.

630 lines (547 loc) 23.2 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); 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 }; } function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP var CLASS_ROOT = _CSSClassnames2.default.DISTRIBUTION; 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; var Distribution = function (_Component) { _inherits(Distribution, _Component); function Distribution(props, context) { _classCallCheck(this, Distribution); var _this = _possibleConstructorReturn(this, (Distribution.__proto__ || Object.getPrototypeOf(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 = -1; _this.state.mouseActive = false; return _this; } _createClass(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); // delay to allow page layout to settle this._resizeTimer = setTimeout(this._layout, 200); } }, { 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 = _extends({}, 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 - 4 * GUTTER_SIZE) { boxRect.width = width - boxRect.x; } // flush the bottom edge if (boxRect.y + boxRect.height > height - 4 * GUTTER_SIZE) { boxRect.height = height - boxRect.y; } return boxRect; } }, { key: '_labelRect', value: function _labelRect(boxRect) { var labelRect = _extends({}, boxRect); return labelRect; } }, { key: '_placeItems', value: function _placeItems() { 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) : []; while (series.length > 0) { var datum = series.shift(); if (datum.value <= 0) { continue; } // Now that we know the actual value of the group, give it a // rectangle whose area corresponds to the actual group value. var itemRect = void 0; var boxWidth = Math.round(areaPer * datum.value / remainingRect.height); var boxHeight = Math.round(areaPer * datum.value / remainingRect.width); if (remainingRect.width - boxWidth >= SMALL_SIZE && remainingRect.width > remainingRect.height) { // landscape, lay out left to right itemRect = { x: remainingRect.x, y: remainingRect.y, width: boxWidth, height: remainingRect.height }; remainingRect.x += itemRect.width; remainingRect.width -= itemRect.width; } else { // portrait, lay out top to bottom itemRect = { x: remainingRect.x, y: remainingRect.y, width: remainingRect.width, height: boxHeight }; remainingRect.y += itemRect.height; remainingRect.height -= itemRect.height; } var boxRect = this._boxRect(itemRect, width, height); var labelRect = this._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 }); } this.setState({ items: items }); } }, { key: '_onResize', value: function _onResize() { // debounce clearTimeout(this._resizeTimer); this._resizeTimer = setTimeout(this._layout, 0); } }, { 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) { if (this._distributionRef.contains(document.activeElement)) { event.preventDefault(); if (this.state.activeIndex - 1 >= 0) { this._onActivate(this.state.activeIndex - 1); } //stop event propagation return true; } return false; } }, { key: '_onNextDistribution', value: function _onNextDistribution(event) { if (this._distributionRef.contains(document.activeElement)) { event.preventDefault(); 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; } return false; } }, { 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 _this2 = this; var intl = this.context.intl; this.setState({ activeIndex: index }, function () { var activeMessage = _this2.activeDistributionRef.getAttribute('aria-label'); var clickable = _this2.state.items[_this2.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: -1 }); } }, { key: '_renderItemLabel', value: function _renderItemLabel(datum, labelRect, index) { var _classnames; var _state = this.state, activeIndex = _state.activeIndex, width = _state.width; var labelClasses = (0, _classnames7.default)(CLASS_ROOT + '__label', (_classnames = {}, _defineProperty(_classnames, BACKGROUND_COLOR_INDEX + '-' + this._itemColorIndex(datum, index), !datum.icon), _defineProperty(_classnames, CLASS_ROOT + '__label--icons', datum.icon), _defineProperty(_classnames, CLASS_ROOT + '__label--small', labelRect.width < SMALL_SIZE || labelRect.height < SMALL_SIZE), _defineProperty(_classnames, CLASS_ROOT + '__label--thin', labelRect.height < THIN_HEIGHT), _defineProperty(_classnames, CLASS_ROOT + '__label--active', index === activeIndex), _classnames)); var value = datum.labelValue !== undefined ? datum.labelValue : datum.value; var style = { top: labelRect.y }; if (index !== activeIndex) { style.left = labelRect.x; style.maxWidth = labelRect.width; style.maxHeight = labelRect.height; } else { // 4 is to align with styled border width if (labelRect.width < SMALL_SIZE && labelRect.x + labelRect.width >= width) { style.right = width - (labelRect.x + labelRect.width + 4); } else { style.left = labelRect.x - 2; } style.minWidth = labelRect.width + 4; style.minHeight = labelRect.height; } return _react2.default.createElement( 'div', { key: index, className: labelClasses, 'data-box-index': index, role: 'presentation', style: style }, _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', _defineProperty({}, 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 _this3 = this; var units = this.props.units; var itemClasses = (0, _classnames7.default)(CLASS_ROOT + '__item', _defineProperty({}, CLASS_ROOT + '__item--clickable', datum.onClick)); var activeDistributionRef = void 0; if (index === this.state.activeIndex) { activeDistributionRef = function activeDistributionRef(ref) { return _this3.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, role: datum.onClick ? 'button' : 'row', ref: activeDistributionRef, 'aria-label': labelMessage, onFocus: function onFocus() { return _this3.setState({ activeIndex: index }); }, 'data-index': index, onClick: datum.onClick }, contents ); } }, { key: '_renderBoxes', value: function _renderBoxes() { var _this4 = this; return this.state.items.map(function (item, index) { return _this4._renderItem(item.datum, item.boxRect, index); }); } }, { key: '_renderLabels', value: function _renderLabels() { var _this5 = this; return this.state.items.map(function (item, index) { return _this5._renderItemLabel(item.datum, item.labelRect, index); }); } }, { key: '_renderLoading', value: function _renderLoading() { var _state2 = this.state, height = _state2.height, width = _state2.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, _this6 = this; var _props = this.props, a11yTitle = _props.a11yTitle, className = _props.className, full = _props.full, size = _props.size, vertical = _props.vertical, props = _objectWithoutProperties(_props, ['a11yTitle', 'className', 'full', 'size', 'vertical']); delete props.series; delete props.units; var intl = this.context.intl; var _state3 = this.state, allIcons = _state3.allIcons, focus = _state3.focus, height = _state3.height, items = _state3.items, mouseActive = _state3.mouseActive, width = _state3.width; var classes = (0, _classnames7.default)(CLASS_ROOT, (_classnames4 = {}, _defineProperty(_classnames4, CLASS_ROOT + '--full', full), _defineProperty(_classnames4, CLASS_ROOT + '--icons', allIcons), _defineProperty(_classnames4, CLASS_ROOT + '--' + size, size), _defineProperty(_classnames4, CLASS_ROOT + '--vertical', vertical), _defineProperty(_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', _defineProperty({}, CLASS_ROOT + '__graphic--focus', focus)); return _react2.default.createElement( 'div', _extends({ ref: function ref(_ref3) { return _this6._containerRef = _ref3; } }, props, { className: classes }), _react2.default.createElement( 'svg', { ref: function ref(_ref) { return _this6._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 _this6.setState({ mouseActive: true }); }, onMouseUp: function onMouseUp() { return _this6.setState({ mouseActive: false }); }, onFocus: function onFocus() { if (mouseActive === false) { _this6.setState({ focus: true }); } }, onBlur: function onBlur() { return _this6.setState({ focus: false }); } }, background, boxes ), _react2.default.createElement( 'div', { ref: function ref(_ref2) { return _this6.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: _propTypes2.default.object }; Distribution.propTypes = { a11yTitle: _propTypes2.default.string, full: _propTypes2.default.bool, // deprecated, use size='full' series: _propTypes2.default.arrayOf(_propTypes2.default.shape({ label: _propTypes2.default.node, value: _propTypes2.default.number.isRequired, colorIndex: _propTypes2.default.string, important: _propTypes2.default.bool, onClick: _propTypes2.default.func, icon: _propTypes2.default.shape({ width: _propTypes2.default.number, height: _propTypes2.default.number, svgElement: _propTypes2.default.node }) })), size: _propTypes2.default.oneOf(['small', 'medium', 'large', 'full']), units: _propTypes2.default.string, vertical: _propTypes2.default.bool }; Distribution.defaultProps = { size: 'medium' }; module.exports = exports['default'];