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
JavaScript
'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