UNPKG

react-inclusive-sortable-table

Version:

An accessible, sortable table based off of Heydon Pickering's Sortable Table from Inclusive Components.

665 lines (595 loc) 20.3 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var React = require('react'); var React__default = _interopDefault(React); var PropTypes = _interopDefault(require('prop-types')); function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var classnames = createCommonjsModule(function (module) { /*! Copyright (c) 2017 Jed Watson. Licensed under the MIT License (MIT), see http://jedwatson.github.io/classnames */ /* global define */ (function () { var hasOwn = {}.hasOwnProperty; function classNames () { var classes = []; for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (!arg) continue; var argType = typeof arg; if (argType === 'string' || argType === 'number') { classes.push(arg); } else if (Array.isArray(arg) && arg.length) { var inner = classNames.apply(null, arg); if (inner) { classes.push(inner); } } else if (argType === 'object') { for (var key in arg) { if (hasOwn.call(arg, key) && arg[key]) { classes.push(key); } } } } return classes.join(' '); } if (module.exports) { classNames.default = classNames; module.exports = classNames; } else { window.classNames = classNames; } }()); }); /** * Returns a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * N milliseconds. If `immediate` is passed, trigger the function on the * leading edge, instead of the trailing. The function also has a property 'clear' * that is a function which will clear the timer to prevent previously scheduled executions. * * @source underscore.js * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/ * @param {Function} function to wrap * @param {Number} timeout in ms (`100`) * @param {Boolean} whether to execute at the beginning (`false`) * @api public */ function debounce(func, wait, immediate){ var timeout, args, context, timestamp, result; if (null == wait) wait = 100; function later() { var last = Date.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); context = args = null; } } } var debounced = function(){ context = this; args = arguments; timestamp = Date.now(); var callNow = immediate && !timeout; if (!timeout) timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; debounced.clear = function() { if (timeout) { clearTimeout(timeout); timeout = null; } }; debounced.flush = function() { if (timeout) { result = func.apply(context, args); context = args = null; clearTimeout(timeout); timeout = null; } }; return debounced; } // Adds compatibility for ES modules debounce.debounce = debounce; var debounce_1 = debounce; function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css = "div:not([tabindex]):focus {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n\n* {\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n}\n\nbody {\n overflow-x: hidden;\n}\n\n.styles_tableScroll__11GBA {\n overflow-x: auto;\n}\n\n/**\n * Hide the table display on viewports less than 400px wide.\n */\n@media (max-width: 399px) {\n .styles_tableContainer__fjLYp {\n display: none;\n }\n}\n\n/**\n * Display the definition list on viewports less than 400px wide.\n */\n.styles_listsContainer__3xrp- {\n display: block;\n}\n\n@media (min-width: 400px) {\n .styles_listsContainer__3xrp- {\n display: none;\n }\n}\n\ntable {\n border-collapse: collapse;\n}\n\nth,\ntd {\n text-align: left;\n padding: 0.75rem;\n}\n\nth {\n white-space: nowrap;\n}\n\ncaption {\n margin-bottom: 0.5rem;\n text-align: left;\n}\n"; var styles = { "tableScroll": "styles_tableScroll__11GBA", "tableContainer": "styles_tableContainer__fjLYp", "listsContainer": "styles_listsContainer__3xrp-" }; styleInject(css); var css$1 = "dl {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n}\n\ndl > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n margin: 0;\n margin-bottom: 0.5rem;\n}\n\ndt {\n padding-right: 0.5rem;\n}\n\ndt {\n font-weight: bold;\n}\n"; styleInject(css$1); var DefinitionList = function DefinitionList(_ref) { var caption = _ref.caption, className = _ref.className, rows = _ref.rows, headers = _ref.headers; return React__default.createElement( 'div', { className: className }, React__default.createElement( 'h2', null, caption ), rows.map(function (row, i) { return React__default.createElement( 'div', { key: i }, React__default.createElement( 'h3', null, row[0] ), React__default.createElement( 'dl', null, headers.map(function (header, i) { return i > 0 && React__default.createElement( React__default.Fragment, { key: i }, React__default.createElement( 'dt', null, header ), React__default.createElement( 'dd', null, row[i] ) ); }) ) ); }) ); }; DefinitionList.propTypes = { caption: PropTypes.string.isRequired, className: PropTypes.string, rows: PropTypes.array.isRequired, headers: PropTypes.array.isRequired }; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 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 defineProperty = function (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; }; var inherits = function (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; }; var possibleConstructorReturn = function (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; }; var Arrow = function (_Component) { inherits(Arrow, _Component); function Arrow() { classCallCheck(this, Arrow); return possibleConstructorReturn(this, (Arrow.__proto__ || Object.getPrototypeOf(Arrow)).apply(this, arguments)); } createClass(Arrow, [{ key: 'render', value: function render() { var _props = this.props, sortDir = _props.sortDir, isCurrent = _props.isCurrent, customArrow = _props.customArrow; var ascending = sortDir === 'ascending'; if (customArrow && typeof customArrow === 'function') { return customArrow(sortDir, isCurrent); } return React__default.createElement( 'svg', { className: this.props.className, viewBox: '0 0 100 200', width: '100', height: '200' }, !(!ascending && isCurrent) && React__default.createElement('polyline', { points: '20 50, 50 20, 80 50' }), React__default.createElement('line', { x1: '50', y1: '20', x2: '50', y2: '180' }), !(ascending && isCurrent) && React__default.createElement('polyline', { points: '20 150, 50 180, 80 150' }) ); } }]); return Arrow; }(React.Component); Arrow.propTypes = { sortDir: PropTypes.string, isCurrent: PropTypes.bool, customArrow: PropTypes.func, className: PropTypes.string }; var css$2 = ".styles_visuallyHidden__24w3L {\n position: absolute !important;\n clip: rect(1px, 1px, 1px, 1px) !important;\n padding: 0 !important;\n border: 0 !important;\n height: 1px !important;\n width: 1px !important;\n overflow: hidden !important;\n}\n\nth button {\n color: #000;\n border: 0;\n padding: 0.125rem 0.5rem;\n border-radius: 0.25rem;\n font-weight: bold;\n vertical-align: middle;\n margin-left: 0.333rem;\n}\n\n\nth .styles_defaultArrow__22xpJ {\n stroke: currentColor;\n stroke-width: 20;\n stroke-linecap: round;\n stroke-linejoin: round;\n fill: none;\n width: 0.5rem;\n height: 1.5em;\n}\n"; var styles$2 = { "visuallyHidden": "styles_visuallyHidden__24w3L", "defaultArrow": "styles_defaultArrow__22xpJ" }; styleInject(css$2); var th = function th(_ref) { var sortedBy = _ref.sortedBy, i = _ref.i, header = _ref.header, sortBy = _ref.sortBy, sortDir = _ref.sortDir, sortable = _ref.sortable, customArrow = _ref.customArrow; var sortableColumn = Array.isArray(sortable) && sortable.includes(i) || sortable === true; return React__default.createElement( 'th', { role: 'columnheader', scope: 'col', 'aria-sort': sortedBy === i ? sortDir : 'none' }, header, sortableColumn && React__default.createElement( 'button', { onClick: function onClick() { return sortBy(i); } }, React__default.createElement(Arrow, { sortDir: sortDir, isCurrent: sortedBy === i, customArrow: customArrow, className: styles$2.defaultArrow }), React__default.createElement( 'span', { className: styles$2.visuallyHidden }, 'sort by ', header, ' in', sortDir !== 'ascending' ? 'ascending' : 'descending', 'order' ) ) ); }; th.propTypes = { customArrow: PropTypes.func, header: PropTypes.string.isRequired, i: PropTypes.number.isRequired, sortable: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]), sortBy: PropTypes.func.isRequired, sortDir: PropTypes.string, sortedBy: PropTypes.number }; var Table = function Table(_ref) { var caption = _ref.caption, customArrow = _ref.customArrow, className = _ref.className, headers = _ref.headers, rowHeaders = _ref.rowHeaders, rows = _ref.rows, sortable = _ref.sortable, sortBy = _ref.sortBy, sortDir = _ref.sortDir, sortedBy = _ref.sortedBy, tabindex = _ref.tabindex, captionID = _ref.captionID; return React__default.createElement( 'div', { className: className, tabIndex: tabindex, 'aria-labelledby': captionID, role: 'group' }, React__default.createElement( 'table', null, React__default.createElement( 'caption', { id: captionID }, caption, tabindex === '0' && React__default.createElement( 'div', null, React__default.createElement( 'small', null, '(scroll to see more)' ) ) ), React__default.createElement( 'tbody', null, React__default.createElement( 'tr', null, headers.map(function (header, i) { return React__default.createElement(th, { header: header, i: i, key: i, sortable: sortable, sortBy: sortBy, sortDir: sortDir, sortedBy: sortedBy, customArrow: customArrow }); }) ), rows.map(function (row, i) { return React__default.createElement( 'tr', { key: i }, row.map(function (cell, i) { return rowHeaders && i < 1 ? React__default.createElement( 'th', { scope: 'row', key: i }, cell ) : React__default.createElement( 'td', { key: i }, cell ); }) ); }) ) ) ); }; Table.propTypes = { caption: PropTypes.string, className: PropTypes.string, customArrow: PropTypes.func, headers: PropTypes.array.isRequired, rowHeaders: PropTypes.bool, rows: PropTypes.array.isRequired, sortable: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]), sortBy: PropTypes.func.isRequired, sortDir: PropTypes.string.isRequired, sortedBy: PropTypes.number, tabindex: PropTypes.number, captionID: PropTypes.string.isRequired }; var InclusiveTable = function (_Component) { inherits(InclusiveTable, _Component); function InclusiveTable(props) { classCallCheck(this, InclusiveTable); var _this = possibleConstructorReturn(this, (InclusiveTable.__proto__ || Object.getPrototypeOf(InclusiveTable)).call(this, props)); _this.state = { tabindex: null, rows: props.rows, sortedBy: null, sortDir: 'none', windowWidth: 0 }; _this.container = React__default.createRef(); _this.sortBy = _this.sortBy.bind(_this); _this.sort = _this.sort.bind(_this); _this.handleResize = debounce_1(_this.handleResize.bind(_this), 50, false); _this.captionID = 'caption-' + Math.random().toString(36).substr(2, 9); return _this; } createClass(InclusiveTable, [{ key: 'sort', value: function sort(a, b, sortDir, i) { if (sortDir === 'ascending') { return a[i] > b[i] ? 1 : a[i] < b[i] ? -1 : 0; } else { return a[i] < b[i] ? 1 : a[i] > b[i] ? -1 : 0; } } }, { key: 'sortBy', value: function sortBy(i) { var _this2 = this; var customSort = this.props.customSort; var _state = this.state, sortDir = _state.sortDir, sortedBy = _state.sortedBy; var sortDirection = void 0; var ascending = sortDir === 'ascending'; if (i === sortedBy) { sortDirection = !ascending ? 'ascending' : 'descending'; } else { sortDirection = 'ascending'; } this.setState(function (prevState) { return { rows: prevState.rows.slice(0).sort(function (a, b) { return ( // If a custom sort method is provided for this column, use it. customSort && customSort[i] && customSort[i](a, b, sortDirection, i) || // Otherwise use the default sort. _this2.sort(a, b, sortDirection, i) ); }), sortedBy: i, sortDir: sortDirection }; }); } }, { key: 'componentDidMount', value: function componentDidMount() { var _ref = this.container && this.container.current, scrollWidth = _ref.scrollWidth, clientWidth = _ref.clientWidth; var scrollable = scrollWidth > clientWidth; this.setState({ tabindex: scrollable ? '0' : null }); if (this.props.breakpoint) { window.addEventListener('resize', this.handleResize); this.setState({ windowWidth: window.innerWidth }); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this.props.breakpoint) { window.removeEventListener('resize', this.handleResize); } } }, { key: 'handleResize', value: function handleResize() { this.setState({ windowWidth: window.innerWidth }); } }, { key: 'render', value: function render() { var _props = this.props, breakpoint = _props.breakpoint, caption = _props.caption, className = _props.className, customArrow = _props.customArrow, headers = _props.headers, rowHeaders = _props.rowHeaders, rows = _props.rows, sortable = _props.sortable; var _state2 = this.state, tabindex = _state2.tabindex, sortedBy = _state2.sortedBy, sortDir = _state2.sortDir, windowWidth = _state2.windowWidth; return React__default.createElement( 'div', { className: className }, breakpoint && React__default.createElement( 'div', { ref: this.container }, windowWidth >= breakpoint ? React__default.createElement(Table, { caption: caption, captionID: this.captionID, className: classnames(styles.tableScroll, defineProperty({}, styles.tableContainer, !breakpoint)), customArrow: customArrow, headers: headers, rowHeaders: rowHeaders, rows: this.state.rows, sortable: sortable, sortBy: this.sortBy, sortDir: sortDir, sortedBy: sortedBy, tabindex: tabindex }) : React__default.createElement(DefinitionList, { className: classnames(defineProperty({}, styles.listsContainer, !breakpoint)), caption: caption, rows: rows, headers: headers }) ), !breakpoint && React__default.createElement( 'div', { ref: this.container }, React__default.createElement(Table, { caption: caption, captionID: this.captionID, className: classnames(styles.tableScroll, defineProperty({}, styles.tableContainer, !breakpoint)), customArrow: customArrow, headers: headers, rowHeaders: rowHeaders, rows: this.state.rows, sortable: sortable, sortBy: this.sortBy, sortDir: sortDir, sortedBy: sortedBy, tabindex: tabindex }), React__default.createElement(DefinitionList, { className: classnames(defineProperty({}, styles.listsContainer, !breakpoint)), caption: caption, rows: rows, headers: headers }) ) ); } }]); return InclusiveTable; }(React.Component); InclusiveTable.propTypes = { /** An optional breakpoint, determines when the `<dl>` switches to a `<table>` */ breakpoint: PropTypes.number, caption: PropTypes.string, className: PropTypes.string, customArrow: PropTypes.func, customSort: PropTypes.object, headers: PropTypes.array.isRequired, rowHeaders: PropTypes.bool, rows: PropTypes.array.isRequired, sortable: PropTypes.oneOfType([PropTypes.array, PropTypes.bool]) }; module.exports = InclusiveTable; //# sourceMappingURL=index.js.map