UNPKG

grommet

Version:

The most advanced UX framework for enterprise applications.

750 lines (642 loc) 26 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _extends2 = require('babel-runtime/helpers/extends'); var _extends3 = _interopRequireDefault(_extends2); var _keys = require('babel-runtime/core-js/object/keys'); var _keys2 = _interopRequireDefault(_keys); var _defineProperty2 = require('babel-runtime/helpers/defineProperty'); var _defineProperty3 = _interopRequireDefault(_defineProperty2); 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 _classnames2 = require('classnames'); var _classnames3 = _interopRequireDefault(_classnames2); var _Box = require('./Box'); var _Box2 = _interopRequireDefault(_Box); var _KeyboardAccelerators = require('../utils/KeyboardAccelerators'); var _KeyboardAccelerators2 = _interopRequireDefault(_KeyboardAccelerators); var _DOM = require('../utils/DOM'); var _Props = require('../utils/Props'); var _Props2 = _interopRequireDefault(_Props); var _Scroll = require('../utils/Scroll'); var _Scroll2 = _interopRequireDefault(_Scroll); var _Responsive = require('../utils/Responsive'); var _Responsive2 = _interopRequireDefault(_Responsive); var _Button = require('./Button'); var _Button2 = _interopRequireDefault(_Button); var _LinkNext = require('./icons/base/LinkNext'); var _LinkNext2 = _interopRequireDefault(_LinkNext); var _LinkPrevious = require('./icons/base/LinkPrevious'); var _LinkPrevious2 = _interopRequireDefault(_LinkPrevious); var _Up = require('./icons/base/Up'); var _Up2 = _interopRequireDefault(_Up); var _Down = require('./icons/base/Down'); var _Down2 = _interopRequireDefault(_Down); var _CSSClassnames = require('../utils/CSSClassnames'); var _CSSClassnames2 = _interopRequireDefault(_CSSClassnames); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var CLASS_ROOT = _CSSClassnames2.default.ARTICLE; // (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP var DEFAULT_PLAY_INTERVAL = 10000; // 10s var Article = function (_Component) { (0, _inherits3.default)(Article, _Component); function Article(props, context) { (0, _classCallCheck3.default)(this, Article); var _this = (0, _possibleConstructorReturn3.default)(this, (Article.__proto__ || (0, _getPrototypeOf2.default)(Article)).call(this, props, context)); _this._onFocusChange = _this._onFocusChange.bind(_this); _this._onScroll = _this._onScroll.bind(_this); _this._onWheel = _this._onWheel.bind(_this); _this._onTouchStart = _this._onTouchStart.bind(_this); _this._onTouchMove = _this._onTouchMove.bind(_this); _this._onResize = _this._onResize.bind(_this); _this._onNext = _this._onNext.bind(_this); _this._onPrevious = _this._onPrevious.bind(_this); _this._onTogglePlay = _this._onTogglePlay.bind(_this); _this._onSelect = _this._onSelect.bind(_this); _this._checkControls = _this._checkControls.bind(_this); _this._checkPreviousNextControls = _this._checkPreviousNextControls.bind(_this); _this._onResponsive = _this._onResponsive.bind(_this); _this._updateHiddenElements = _this._updateHiddenElements.bind(_this); _this._updateProgress = _this._updateProgress.bind(_this); // Necessary to detect for Firefox or Edge to implement accessibility // tabbing var accessibilityTabbingCompatible = typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Firefox') === -1 && navigator.userAgent.indexOf('Edge') === -1; _this.state = { selectedIndex: props.selected || 0, playing: false, showControls: _this.props.controls, accessibilityTabbingCompatible: accessibilityTabbingCompatible }; _this.childRef = {}; return _this; } (0, _createClass3.default)(Article, [{ key: 'componentDidMount', value: function componentDidMount() { if (this.props.scrollStep) { if (this.props.full) { console.warn('Article cannot use `scrollStep` with `full`.'); } this._keys = { up: this._onPrevious, down: this._onNext }; if ('row' === this.props.direction) { this._keys = { left: this._onPrevious, right: this._onNext }; if (this.state.accessibilityTabbingCompatible) { this._updateHiddenElements(); } } //keys.space = this._onTogglePlay; _KeyboardAccelerators2.default.startListeningToKeyboard(this, this._keys); document.addEventListener('wheel', this._onWheel); window.addEventListener('resize', this._onResize); this._scrollParent = (0, _reactDom.findDOMNode)(this.componentRef); this._checkControls(); if ('row' === this.props.direction && this.props.scrollStep) { this._responsive = _Responsive2.default.start(this._onResponsive); } } if (this.props.onProgress) { window.addEventListener('scroll', this._updateProgress); if (this.props.direction === 'row') { this._responsive = _Responsive2.default.start(this._onResponsive); } } this._onSelect(this.state.selectedIndex); } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { // allow updates to selected props to trigger new chapter select if (typeof nextProps.selected !== 'undefined' && nextProps.selected !== null && nextProps.selected !== this.state.selectedIndex) { this._onSelect(nextProps.selected); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this.props.scrollStep) { _KeyboardAccelerators2.default.stopListeningToKeyboard(this, this._keys); document.removeEventListener('wheel', this._onWheel); window.removeEventListener('resize', this._onResize); } if (this._responsive) { this._responsive.stop(); } if (this.props.onProgress) { window.removeEventListener('scroll', this._updateProgress); } } }, { key: '_checkPreviousNextControls', value: function _checkPreviousNextControls(currentScroll, nextProp, prevProp) { if (currentScroll > 0) { var nextStepNode = (0, _reactDom.findDOMNode)(this.childRef[this.state.selectedIndex + 1]); var previousStepNode = (0, _reactDom.findDOMNode)(this.childRef[this.state.selectedIndex - 1]); if (nextStepNode) { var nextStepPosition = nextStepNode.getBoundingClientRect()[nextProp] * (this.state.selectedIndex + 1); if (currentScroll > nextStepPosition) { this.setState({ selectedIndex: this.state.selectedIndex + 1 }); } } if (previousStepNode) { var previousStepPosition = previousStepNode.getBoundingClientRect()[prevProp] * this.state.selectedIndex; if (currentScroll < previousStepPosition) { this.setState({ selectedIndex: this.state.selectedIndex - 1 }); } } } } }, { key: '_checkControls', value: function _checkControls() { if (this.props.direction === 'row') { var currentScroll = this.componentRef.boxContainerRef.scrollLeft; this._checkPreviousNextControls(currentScroll, 'left', 'right'); } else { var _currentScroll = this.componentRef.boxContainerRef.scrollTop; this._checkPreviousNextControls(_currentScroll, 'top', 'bottom'); } } }, { key: '_visibleIndexes', value: function _visibleIndexes() { var _props = this.props, children = _props.children, direction = _props.direction; var result = []; var childCount = _react2.default.Children.count(children); var limit = 'row' === direction ? window.innerWidth : window.innerHeight; for (var index = 0; index < childCount; index += 1) { var childElement = (0, _reactDom.findDOMNode)(this.childRef[index]); var rect = childElement.getBoundingClientRect(); // ignore small drifts of 10 pixels on either end if ('row' === direction) { if (rect.right > 10 && rect.left < limit - 10) { result.push(index); } else if (result.length > 0) { break; } } else { if (rect.bottom > 10 && rect.top < limit - 10) { result.push(index); } else if (result.length > 0) { break; } } } return result; } }, { key: '_shortTimer', value: function _shortTimer(name, duration) { var _this2 = this; if (!this[name]) { this[name] = true; } var timerName = this[name] + 'Timer'; clearTimeout(this[timerName]); this[timerName] = setTimeout(function () { _this2[name] = false; }, duration); } }, { key: '_onWheel', value: function _onWheel(event) { var _this3 = this; if ('row' === this.props.direction) { if (this._scrollingHorizontally) { // no-op } else if (!this._scrollingVertically) { if (Math.abs(event.deltaY * 2) > Math.abs(event.deltaX)) { // user is scrolling vertically this._shortTimer('_scrollingVertically', 1000); } } } else { // Give the user lots of control. var delta = event.deltaY; if (Math.abs(delta) > 100) { // The user is expressing a resolute interest in controlling the // scrolling behavior. Stop doing any of our scroll step aligning // until he stops expressing such interest. clearInterval(this._wheelTimer); clearInterval(this._wheelLongTimer); this._wheelLongTimer = setTimeout(function () { _this3._wheelLongTimer = undefined; }, 2000); } else if (!this._wheelLongTimer) { if (delta > 10) { clearInterval(this._wheelTimer); this._wheelTimer = setTimeout(this._onNext, 200); } else if (delta < -10) { clearInterval(this._wheelTimer); this._wheelTimer = setTimeout(this._onPrevious, 200); } else { clearInterval(this._controlTimer); this._controlTimer = setTimeout(this._checkControls, 200); } } } } }, { key: '_onScroll', value: function _onScroll(event) { var _this4 = this; if ('row' === this.props.direction) { var selectedIndex = this.state.selectedIndex; var childElement = (0, _reactDom.findDOMNode)(this.childRef[selectedIndex]); var rect = childElement.getBoundingClientRect(); if (event.target === this._scrollParent) { // scrolling Article if (this._scrollingVertically) { // prevent Article horizontal scrolling while scrolling vertically this._scrollParent.scrollLeft += rect.left; } else { (function () { var scrollingRight = _this4._priorScrollLeft < _this4._scrollParent.scrollLeft; // once we stop scrolling, align with child boundaries clearTimeout(_this4._scrollTimer); _this4._scrollTimer = setTimeout(function () { if (!_this4._resizing) { var indexes = _this4._visibleIndexes(); if (indexes.length > 1 && scrollingRight) { _this4._onSelect(indexes[1]); } else { _this4._onSelect(indexes[0]); } } }, 100); _this4._priorScrollLeft = _this4._scrollParent.scrollLeft; })(); } } else if (event.target.parentNode === this._scrollParent) { // scrolling child // Has it scrolled near the bottom? if (this.state.accessibilityTabbingCompatible) { // only use lastGrandChild logic if we're not using Firefox or IE. // causes flashing in Firefox, but required for Safari scrolling. var grandchildren = event.target.children; var lastGrandChild = grandchildren[grandchildren.length - 1]; rect = lastGrandChild.getBoundingClientRect(); } if (rect.bottom <= window.innerHeight + 24) { // at the bottom this.setState({ atBottom: true }); } else { // not at the bottom this.setState({ atBottom: false }); } } } } }, { key: '_onTouchStart', value: function _onTouchStart(event) { var touched = event.changedTouches[0]; this._touchStartX = touched.clientX; this._touchStartY = touched.clientY; } }, { key: '_onTouchMove', value: function _onTouchMove(event) { if (!this.state.ignoreScroll) { var touched = event.changedTouches[0]; var deltaX = touched.clientX - this._touchStartX; var deltaY = touched.clientY - this._touchStartY; // Only step if the user isn't scrolling vertically, bias vertically if (Math.abs(deltaY) < Math.abs(deltaX * 2)) { if (deltaX < 0) { this._onNext(); } else { this._onPrevious(); } } } } }, { key: '_onResize', value: function _onResize() { var _this5 = this; clearTimeout(this._resizeTimer); this._resizeTimer = setTimeout(function () { _this5._onSelect(_this5.state.selectedIndex); _this5._shortTimer('_resizing', 1000); }, 50); } }, { key: '_onNext', value: function _onNext(event, wrap) { // only process if the focus is NOT in a form element if (!(0, _DOM.isFormElement)(document.activeElement)) { var children = this.props.children; var selectedIndex = this.state.selectedIndex; var childCount = _react2.default.Children.count(children); if (event) { this._stop(); event.preventDefault(); } var targetIndex = this._visibleIndexes()[0] + 1; if (targetIndex !== selectedIndex) { if (targetIndex < childCount) { this._onSelect(Math.min(childCount - 1, targetIndex)); } else if (wrap) { this._onSelect(1); } } } } }, { key: '_onPrevious', value: function _onPrevious(event) { // only process if the focus is NOT in a form element if (!(0, _DOM.isFormElement)(document.activeElement)) { var selectedIndex = this.state.selectedIndex; if (event) { this._stop(); event.preventDefault(); } var targetIndex = this._visibleIndexes()[0] - 1; if (targetIndex !== selectedIndex) { this._onSelect(Math.max(0, targetIndex)); } } } }, { key: '_start', value: function _start() { var _this6 = this; this._playTimer = setInterval(function () { _this6._onNext(null, true); }, DEFAULT_PLAY_INTERVAL); this.setState({ playing: true }); } }, { key: '_stop', value: function _stop() { clearInterval(this._playTimer); this.setState({ playing: false }); } }, { key: '_onTogglePlay', value: function _onTogglePlay(event) { event.preventDefault(); if (this.state.playing) { this._stop(); } else { this._start(); } } }, { key: '_onSelect', value: function _onSelect(selectedIndex) { var _this7 = this; var childElement = (0, _reactDom.findDOMNode)(this.childRef[selectedIndex]); var windowHeight = window.innerHeight + 24; if (childElement) { var parentElement = childElement.parentNode; var atBottom = Math.round(parentElement.scrollTop) >= parentElement.scrollHeight - parentElement.clientHeight; if (selectedIndex !== this.state.selectedIndex) { // scroll child to top childElement.scrollTop = 0; // ensures controls are displayed when selecting a new index and // scrollbar is at bottom of article this.setState({ selectedIndex: selectedIndex, atBottom: atBottom }, function () { if (_this7.props.onSelect) { _this7.props.onSelect(selectedIndex); } // Necessary to detect for Firefox or Edge to implement accessibility // tabbing if (_this7.props.direction === 'row' && _this7.state.accessibilityTabbingCompatible) { _this7.anchorStepRef.focus(); _this7._updateHiddenElements(); } }); } else if (childElement.scrollHeight <= windowHeight) { // on initial chapter load, ensure arrows are rendered // when there are no scrollbars this.setState({ atBottom: true }); } var rect = childElement.getBoundingClientRect(); if ('row' === this.props.direction) { if (rect.left !== 0) { this._scrollingHorizontally = true; _Scroll2.default.scrollBy(this._scrollParent, 'scrollLeft', rect.left, function () { _this7._scrollingHorizontally = false; }); } } else { if (rect.top !== 0) { this._scrollingVertically = true; _Scroll2.default.scrollBy(this._scrollParent, 'scrollTop', rect.top, function () { _this7._scrollingVertically = false; }); } } } } }, { key: '_onFocusChange', value: function _onFocusChange(e) { var _this8 = this; _react2.default.Children.forEach(this.props.children, function (element, index) { var parent = (0, _reactDom.findDOMNode)(_this8.childRef[index]); if (parent && parent.contains(e.target)) { _this8._onSelect(index); return false; } }); } }, { key: '_onResponsive', value: function _onResponsive(small) { this.setState({ narrow: small }); } }, { key: '_toggleDisableChapter', value: function _toggleDisableChapter(chapter, disabled) { var elements = (0, _DOM.filterByFocusable)(chapter.getElementsByTagName('*')); if (elements) { elements.forEach(function (element) { if (disabled) { element.setAttribute('disabled', 'disabled'); } else { element.removeAttribute('disabled'); } element.setAttribute('tabindex', disabled ? '-1' : '0'); }); } } }, { key: '_updateHiddenElements', value: function _updateHiddenElements() { var component = (0, _reactDom.findDOMNode)(this.componentRef); var children = component.children; for (var i = 0; i < children.length; i++) { var child = children[i]; if (child.getAttribute('aria-hidden')) { this._toggleDisableChapter(child, true); } else { this._toggleDisableChapter(child, false); } } } }, { key: '_updateProgress', value: function _updateProgress(event) { var article = (0, _reactDom.findDOMNode)(this.componentRef); var articleRect = article.getBoundingClientRect(); var offset = this.props.direction === 'column' ? Math.abs(articleRect.top) : Math.abs(articleRect.left); var totalDistance = this.props.direction === 'column' ? window.innerHeight : this._getChildrenWidth(this.componentRef.boxContainerRef.childNodes); var objectDistance = this.props.direction === 'column' ? articleRect.height : articleRect.width; // Covers row responding to column layout. if (this.props.direction === 'row' && this.state.narrow && this.props.responsive !== false) { offset = Math.abs(articleRect.top); totalDistance = window.innerHeight; objectDistance = articleRect.height; } var progress = Math.abs(offset / (objectDistance - totalDistance)); var scrollPercentRounded = Math.round(progress * 100); this.props.onProgress(scrollPercentRounded); } }, { key: '_renderControls', value: function _renderControls() { var CONTROL_CLASS_PREFIX = CLASS_ROOT + '__control ' + CLASS_ROOT + '__control'; var childCount = _react2.default.Children.count(this.props.children); var controls = []; var a11yTitle = this.props.a11yTitle || {}; if ('row' === this.props.direction) { if (!this.state.narrow || this.state.atBottom) { if (this.state.selectedIndex > 0) { controls.push(_react2.default.createElement(_Button2.default, { key: 'previous', plain: true, a11yTitle: a11yTitle.previous, className: CONTROL_CLASS_PREFIX + '-left', onClick: this._onPrevious, icon: _react2.default.createElement(_LinkPrevious2.default, { a11yTitle: 'article-previous-title', size: 'large' }) })); } if (this.state.selectedIndex < childCount - 1) { controls.push(_react2.default.createElement(_Button2.default, { key: 'next', plain: true, a11yTitle: a11yTitle.next, className: CONTROL_CLASS_PREFIX + '-right', onClick: this._onNext, icon: _react2.default.createElement(_LinkNext2.default, { size: 'large', a11yTitle: 'article-next-title' }) })); } } } else { if (this.state.selectedIndex > 0) { controls.push(_react2.default.createElement( _Button2.default, { key: 'previous', plain: true, a11yTitle: a11yTitle.previous, className: CONTROL_CLASS_PREFIX + '-up', onClick: this._onPrevious }, _react2.default.createElement(_Up2.default, null) )); } if (this.state.selectedIndex < childCount - 1) { controls.push(_react2.default.createElement( _Button2.default, { key: 'next', plain: true, a11yTitle: a11yTitle.next, className: CONTROL_CLASS_PREFIX + '-down', onClick: this._onNext }, _react2.default.createElement(_Down2.default, { a11yTitle: 'article-down' }) )); } } return controls; } }, { key: 'render', value: function render() { var _this9 = this; var classes = (0, _classnames3.default)(CLASS_ROOT, (0, _defineProperty3.default)({}, CLASS_ROOT + '--scroll-step', this.props.scrollStep), this.props.className); var boxProps = _Props2.default.pick(this.props, (0, _keys2.default)(_Box2.default.propTypes)); var restProps = _Props2.default.omit(this.props, (0, _keys2.default)(Article.propTypes)); var controls = void 0; if (this.props.controls) { controls = this._renderControls(); } var anchorStepNode = void 0; if (this.state.accessibilityTabbingCompatible) { anchorStepNode = _react2.default.createElement('a', { tabIndex: '-1', 'aria-hidden': 'true', ref: function ref(_ref) { return _this9.anchorStepRef = _ref; } }); } var children = this.props.children; if (this.props.scrollStep || this.props.controls) { children = _react.Children.map(this.props.children, function (element, index) { if (element) { var elementClone = _react2.default.cloneElement(element, { ref: function ref(_ref2) { return _this9.childRef[index] = _ref2; } }); var elementNode = elementClone; var ariaHidden = void 0; if (_this9.state.selectedIndex !== index && _this9.state.accessibilityTabbingCompatible) { ariaHidden = 'true'; } if (_this9.props.controls) { elementNode = _react2.default.createElement( 'div', { 'aria-hidden': ariaHidden }, elementClone ); } return elementNode; } return undefined; }, this); } delete boxProps.a11yTitle; return _react2.default.createElement( _Box2.default, (0, _extends3.default)({}, restProps, boxProps, { ref: function ref(_ref3) { return _this9.componentRef = _ref3; }, tag: 'article', className: classes, primary: this.props.primary, onFocus: this._onFocusChange, onScroll: this._onScroll, onTouchStart: this._onTouchStart, onTouchMove: this._onTouchMove }), anchorStepNode, children, controls ); } }]); return Article; }(_react.Component); Article.displayName = 'Article'; exports.default = Article; Article.propTypes = (0, _extends3.default)({ controls: _react.PropTypes.bool }, _Box2.default.propTypes, { a11yTitle: _react.PropTypes.shape({ next: _react.PropTypes.string, previous: _react.PropTypes.string }), onProgress: _react.PropTypes.func, onSelect: _react.PropTypes.func, scrollStep: _react.PropTypes.bool, selected: _react.PropTypes.number }); Article.defaultProps = { pad: 'none', direction: 'column' }; module.exports = exports['default'];