diff --git a/src/Table/README.mdx b/src/Table/README.mdx
index f81bb3f5..00bf3f1c 100644
--- a/src/Table/README.mdx
+++ b/src/Table/README.mdx
@@ -14,7 +14,7 @@ A table component with a sticky header row.
- By default, all columns have an equal width
- Control the width of columns by specifying a fixed or percentage width
-- You can define a `minWidth` for the table or individual columns – if they grow smaller than the defined size, a list-like **mobile view** will be enabled
+- You can define a breakpoint under which a list-like **mobile view** will be enabled (by default this is "xs")
- Columns can be defined in JSX using the `Column` component, or using the `columns` prop
## Examples
@@ -93,13 +93,13 @@ You can also define your columns as an array of objects, if you prefer:
### Responsiveness examples
-- **Mobile view:** By default, the table changes to a more list-like view as soon as it has less than 500 pixels of horizontal space available. Use the `
` prop to set a custom pixel threshold under which you want the mobile view to become active. You can also define a `minWidth` for individual columns. As soon as any one column gets smaller than the size specified, the mobile view will kick in, making sure that all data stays readable.
-- **Hiding columns:** If you'd rather hide columns than trigger the mobile view, add the `canHideColumns` prop to ``. In this mode, columns smaller than their specified `minWidth` will be hidden (unless the mobile view is active).
+- **Mobile view:** The table changes to a more list-like view when the screen width gets lower than the breakpoint defined using the `mobileViewBreakpoint` prop.
+- **Hiding columns:** When the mobile view is not active, you can hide columns below a certain breakpoint using the `hideBelowBreakpoint` prop. Columns hidden this way will become visible again in the mobile view.
The table below uses both modes – on medium sized screens, columns that don't fit are hidden, while the mobile view is active on very small screens.
-
+
{item.name}}
/>
-
-
-
+
+
+
diff --git a/src/Table/getColumnWidths.js b/src/Table/getColumnWidths.js
deleted file mode 100644
index e09f2bf3..00000000
--- a/src/Table/getColumnWidths.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import {percentageToFraction} from '../utils/units';
-
-/**
- * @param {string|number|undefined} width - Predefined width
- * of a column, either absolute (number) or a percentage (string)
- * @param {number} availableWidth - Total width of the table
- *
- * @returns {number} Column width in pixels
- */
-
-export function getSingleColumnWidth(width, availableWidth) {
- if (!width) return null;
- if (typeof width === 'string') {
- const fraction = percentageToFraction(width);
- return Math.round(availableWidth * fraction);
- } else return width;
-}
-
-/**
- * The table uses fixed positioning, which means we can calculate
- * the width of each column knowing only the table's configuration
- * and the total width that the table takes up on the page.
- *
- * @param {Array} columns - Table column configuration
- * @param {number} totalWidth - Total table width
- *
- * @returns {Array} column widths, containing one object per column
- * with the fields `name`, `width`, and `shouldHide`
- *
- * `shouldHide` is determined based on the column's `minWidth` prop.
- */
-
-function getColumnWidths(columns, totalWidth) {
- if (columns.length === 0 || !totalWidth || totalWidth < 0) {
- return null;
- }
- // Get the width of any columns that have a predefined width (either % or px)
- const predefinedColumnWidths = columns.map(column =>
- getSingleColumnWidth(column.width, totalWidth)
- );
- // Calculate the space remaining after subtracting the defined widths
- const remainingSpace = predefinedColumnWidths.reduce(
- (remaining, current) => remaining - (current || 0),
- totalWidth
- );
- const remainingColumns = predefinedColumnWidths.filter(width => !width);
- // Calculate the widths of any remaining columns by evenly
- // distributing the remaining space between them
- const columnWidths = predefinedColumnWidths.map(width => {
- if (width) return width;
- return Math.round(remainingSpace / remainingColumns.length);
- });
-
- // Build the output object including the information whether the
- // column should be hidden given its size
- const columnWidthsAndHiddenStatus = columns.map((column, index) => {
- const shouldHide = column.minWidth
- ? columnWidths[index] < column.minWidth
- : false;
-
- return {
- name: column.name,
- width: columnWidths[index],
- shouldHide,
- };
- });
-
- return columnWidthsAndHiddenStatus;
-}
-
-export default getColumnWidths;
diff --git a/src/Table/getColumnsToHide.js b/src/Table/getColumnsToHide.js
deleted file mode 100644
index 99d85150..00000000
--- a/src/Table/getColumnsToHide.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import getColumnWidths from './getColumnWidths';
-
-/**
- * Calculates which columns should be hidden at the width
- * available to the table. It tries to fit as many columns
- * as possible given the constraints defined by the column
- * configuration (width & minWidth)
- *
- * @param {Array} columns - Table column configuration
- * @param {number} totalWidth - Total table width
- *
- * @returns {Array} Names of columns that should be hidden at the given width
- */
-
-function getColumnsToHide(columns, totalWidth) {
- if (!totalWidth) return [];
-
- // Calculate initial column widths if all columns are visible
- const columnWidths = getColumnWidths(columns, totalWidth);
-
- // Get the initial candidates for hiding (i.e. columns whose
- // calculated width is below their defined `minWidth`)
- const hidingCandidates = columnWidths.filter(column => column.shouldHide);
-
- // If there's just one candidate, happy days: return it
- if (hidingCandidates.length === 1) {
- return [hidingCandidates[0].name];
- }
- // If there are more, we must determine which ones _could_ fit
- // if only _some_ of the candidates were removed, in order to
- // fit the maximum number of columns
- else if (hidingCandidates.length > 1) {
- // We'll assign a score to each candidate based on how
- // many of the other candidates could be fit if it was hidden
- const candidateScores = hidingCandidates.map(({name}) => {
- // What are our widths if we hide this candidate?
- const widthsWithoutCandidate = getColumnWidths(
- columns.filter(c => c.name !== name),
- totalWidth
- );
-
- // Get all candidates excluding the current one to
- // check if it could be made visible now
- const otherCandidates = hidingCandidates
- .filter(candidate => candidate.name !== name)
- .map(candidate => candidate.name);
-
- let score = 0;
- otherCandidates.forEach(otherCandidateName => {
- const otherCandidate = widthsWithoutCandidate.find(
- item => item.name === otherCandidateName
- );
- // Increase the score of this 'other candidate' if hiding
- // the 'current candidate' has given it enough space
- // to be made visible again
- if (otherCandidate.shouldHide === false) {
- score += 1;
- }
- });
- return {
- name,
- score,
- };
- });
-
- // Once we know the scores, we can group the candidates into 'final
- // candidates' with a high score which should be hidden immediately
- // (because hiding them creates a lot of room for other candidates)
- // and those where hiding them doesn't change much, in which case
- // we'll give them another pass, this time with the final
- // candidates already out of the picture.
- const finalCandidates = [];
- const remainingCandidates = [];
- // Set the threshold at the lowest score found.
- // Anything higher we consider a final candidate
- const lowestScore = Math.min(
- ...candidateScores.map(item => item.score)
- );
-
- candidateScores.forEach(item => {
- if (item.score > lowestScore) {
- finalCandidates.push(item.name);
- } else {
- remainingCandidates.push(item.name);
- }
- });
-
- // If there are no candidates with a higher score than others,
- // we simply sacrifice and hide the rightmost candidate.
- // Gotta do _something_ to make those columns fit, otherwise we'd
- // enter an infinite loop!
- if (!finalCandidates.length) {
- finalCandidates.push(remainingCandidates.pop());
- }
-
- // As long as there are remaining candidates, we recursively call
- // this function with high-scoring candidates removed, until we can
- // fit the highest number of remaining columns.
- if (remainingCandidates.length) {
- const columnsWithoutFinalCandidates = columns.filter(
- column => !finalCandidates.includes(column.name)
- );
- return [
- ...finalCandidates,
- ...getColumnsToHide(columnsWithoutFinalCandidates, totalWidth),
- ];
- } else return finalCandidates;
- }
-
- // Return an empty array if there's nothing to hide
- return [];
-}
-
-export default getColumnsToHide;
diff --git a/src/Table/index.js b/src/Table/index.js
index e3b0479e..1de341e4 100644
--- a/src/Table/index.js
+++ b/src/Table/index.js
@@ -1,7 +1,6 @@
-import React, {useRef} from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import styled, {css} from 'styled-components';
-import {useSize} from 'react-hook-size';
import {pxToRem} from '../utils/units';
import {alpha, mix} from '../utils/colors';
@@ -11,9 +10,12 @@ import {getSpacing} from '../utils/spacing';
import Text from '../Text';
-import getColumnsToHide from './getColumnsToHide';
import getColumnConfigFromChildren from './getColumnConfigFromChildren';
+function getBreakpoint(key) {
+ return p => p.theme.globals.breakpoints[p[key]];
+}
+
const StyledTable = styled.table`
position: relative;
${positionProps}
@@ -26,20 +28,6 @@ const StyledTable = styled.table`
width: 100%;
- ${p =>
- !p.isMobileView &&
- css`
- /* Highlight table row on hover and focus within */
- tr:hover {
- background-color: ${p =>
- alpha(p.theme.shade, Number(p.theme.shadeStrength) / 2)};
- }
- tr:focus-within {
- background-color: ${p =>
- alpha(p.theme.shade, Number(p.theme.shadeStrength) / 2)};
- }
- `}
-
td,
th {
font-weight: inherit;
@@ -83,94 +71,114 @@ const StyledTable = styled.table`
}
}
- ${p =>
- p.pl &&
- !p.isMobileView &&
- css`
- th:first-child,
- td:first-child {
- padding-left: ${getSpacing(p.pl, p.theme)};
- }
- `}
-
- ${p =>
- p.pr &&
- !p.isMobileView &&
- css`
- th:last-child,
- td:last-child {
- padding-right: ${getSpacing(p.pr, p.theme)};
- }
- `}
-
- ${p =>
- p.isMobileView &&
- css`
- /* Hide the column headers. We'll add them back
- * in inside of each (non-header) cell. */
- thead {
- display: none;
- }
+ /* Non-mobile-view styles only */
+ @media (min-width: ${getBreakpoint('mobileViewBreakpoint')}) {
+ /* Highlight table row on hover and focus within */
+ tr:hover {
+ background-color: ${p =>
+ alpha(p.theme.shade, Number(p.theme.shadeStrength) / 2)};
+ }
+ tr:focus-within {
+ background-color: ${p =>
+ alpha(p.theme.shade, Number(p.theme.shadeStrength) / 2)};
+ }
- /* Remove table layout. */
- table,
- tbody,
- th,
- td {
- display: block;
- }
+ ${p =>
+ p.pl &&
+ css`
+ th:first-child,
+ td:first-child {
+ padding-left: ${getSpacing(p.pl, p.theme)};
+ }
+ `}
+
+ ${p =>
+ p.pr &&
+ css`
+ th:last-child,
+ td:last-child {
+ padding-right: ${getSpacing(p.pr, p.theme)};
+ }
+ `}
+ }
- tr {
- /* Using flex allows us to modify the order of children
- * so we can display the row's header at the top */
- display: flex;
- flex-direction: column;
-
- /* Add some padding for nicer spacing in the content area */
- padding-bottom: ${p => p.theme.globals.spacing.xs};
- background-color: ${p =>
- mix(p.theme.shade)(
- p.theme.background,
- p.theme.shadeStrength
- )};
- border-top: ${p => borderValue(p.theme)};
- }
+ /* Mobile-view styles */
+ @media (max-width: ${getBreakpoint('mobileViewBreakpoint')}) {
+ /* Hide column headers. We'll add them back
+ * inside of each cell (except the row header). */
+ thead {
+ display: none;
+ }
- th {
- display: flex;
- align-items: center;
- font-weight: bold;
- background-color: ${p => p.theme.background};
- /* Make sure to display the header at the top */
- order: -1;
- /* Visually, this is the top spacing of the content area */
- margin-bottom: ${p => p.theme.globals.spacing.xs};
- }
+ /* Remove table layout. */
+ table,
+ tbody,
+ th,
+ td {
+ display: block;
+ }
- td {
- /* Don't use the specified row height in mobile view */
- height: auto;
- display: flex;
- font-weight: bold;
-
- /* Add columns headers as inline labels
- * The parent's display: flex ensures clean
- * content line breaks */
- &::before {
- content: attr(data-columnheader) ': ';
- margin-right: ${p => p.theme.globals.spacing.xs};
- font-weight: normal;
- white-space: nowrap;
- }
+ tr {
+ /* Using flex allows us to modify the order of children
+ * so we can display the row's header at the top */
+ display: flex;
+ flex-direction: column;
+
+ /* Add some padding for nicer spacing in the content area */
+ padding-bottom: ${p => p.theme.globals.spacing.xs};
+ background-color: ${p =>
+ mix(p.theme.shade)(p.theme.background, p.theme.shadeStrength)};
+ border-top: ${p => borderValue(p.theme)};
+ }
+
+ th {
+ /* Make sure to display the header at the top */
+ order: -1;
+
+ /* Flex to vertically align content in cell */
+ display: flex;
+ align-items: center;
+ font-weight: bold;
+ background-color: ${p => p.theme.background};
+ /* Visually, this margin is added to the top padding
+ * of the content area */
+ margin-bottom: ${p => p.theme.globals.spacing.xs};
+ }
+
+ td {
+ /* Override the specified minimum row height */
+ height: auto;
+ display: flex;
+ font-weight: bold;
+
+ /* Add columns headers as inline labels
+ * The parent td's display: flex ensures clean
+ * content line breaks */
+ &::before {
+ content: attr(data-columnheader) ': ';
+ margin-right: ${p => p.theme.globals.spacing.xs};
+ font-weight: normal;
+ white-space: nowrap;
}
- `}
+ }
+ }
`;
-const defaultHeaderRenderer = column => {column.name};
+const Cell = styled.td`
+ @media (max-width: ${getBreakpoint('hideBelowBreakpoint')}) {
+ display: none;
+ }
+`;
+
+/**
+ * Uses either the column's name or its cellRenderer
+ * to access a cell's data. If no cellRenderer is proviced,
+ * the name of the column is used.
+ *
+ * @param {object} item
+ * @param {string|Function} key
+ */
-// Uses either the column's name or its cellRenderer
-// to access a cell's data. If no cellRenderer is proviced,
-// the name of the column is used.
function getCellContent(item, key) {
if (typeof key === 'string') {
return item[key] || item[key.toLowerCase()] || 'No entry found';
@@ -178,109 +186,105 @@ function getCellContent(item, key) {
return key(item);
}
+const defaultHeaderRenderer = column => {column.name};
+
// Note about roles: The table is marked up using roles
// that are seemingly redundant. This is done so that the
// styles of the mobile view don't remove the table's semantics,
// which they'd otherwise do. Explicit roles ensure that
// semantics aren't affected by the styles.
-function Table({
- children,
- columns: columnsProp,
- data,
- headerRenderer,
- stickyHeaderOffset,
- canHideColumns,
- minWidth,
- rowMinHeight,
- shadedHeader,
- ...otherProps
-}) {
- const ref = useRef();
- const {width} = useSize(ref);
+function Table(props) {
+ const {
+ children,
+ columns: columnsProp,
+ data,
+ headerRenderer,
+ stickyHeaderOffset,
+ mobileViewBreakpoint,
+ rowMinHeight,
+ shadedHeader,
+ ...otherProps
+ } = props;
+
const columns = children
? getColumnConfigFromChildren(children)
: columnsProp;
- const hiddenColumns = getColumnsToHide(columns, width);
- const isMobileView =
- width < minWidth || (!canHideColumns && hiddenColumns.length > 0);
-
return (
-
-
- {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
-
-
+
+ {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
+
+
+ {columns.map(column => {
+ const {
+ hideBelowBreakpoint,
+ name,
+ subtitle,
+ width,
+ } = column;
+ return (
+
+ {headerRenderer(column)}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+ |
+ );
+ })}
+
+
+ {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
+
+ {data.map(item => (
+
{columns.map(column => {
- const {name, subtitle, width} = column;
+ const {
+ cellRenderer,
+ isHeading,
+ hideBelowBreakpoint,
+ name,
+ } = column;
+
return (
-
- {headerRenderer(column)}
- {subtitle && (
-
- {subtitle}
-
- )}
- |
+ {getCellContent(item, cellRenderer || name)}
+
);
})}
-
- {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
-
- {data.map(item => (
-
- {columns.map(column => {
- const {cellRenderer, isHeading, name} = column;
-
- const Element = isHeading ? 'th' : 'td';
- const isHidden =
- !isMobileView &&
- hiddenColumns.includes(name);
-
- return (
-
- {getCellContent(
- item,
- cellRenderer || name
- )}
-
- );
- })}
-
- ))}
-
-
-
+ ))}
+
+
);
}
Table.defaultProps = {
- minWidth: 500,
headerRenderer: defaultHeaderRenderer,
+ mobileViewBreakpoint: 'xs',
stickyHeaderOffset: 0,
rowMinHeight: 45,
};
@@ -294,9 +298,10 @@ Table.propTypes = {
*/
stickyHeaderOffset: PropTypes.number,
/**
- * Specify below which width the mobile view should kick in
+ * Specify below which breakpoint (from `theme.globals.breakpoints`)
+ * the mobile view should kick in.
*/
- minWidth: PropTypes.number,
+ mobileViewBreakpoint: PropTypes.number,
pl: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
pr: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
rowMinHeight: PropTypes.number,
@@ -306,7 +311,7 @@ Table.propTypes = {
const columnPropsShape = {
cellRenderer: PropTypes.func,
isHeading: PropTypes.bool,
- minWidth: PropTypes.number,
+ hideBelowBreakpoint: PropTypes.string,
name: PropTypes.string.isRequired,
subtitle: PropTypes.string,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
diff --git a/src/theme/sections.js b/src/theme/sections.js
index 6c54724b..4586b956 100644
--- a/src/theme/sections.js
+++ b/src/theme/sections.js
@@ -8,7 +8,7 @@ const sections = {
shade: colors.black,
highlight: colors.pink,
textDimStrength: 0.75,
- shadeStrength: 0.08,
+ shadeStrength: 0.05,
lineStrength: 0.2,
},
};
diff --git a/src/utils/units.js b/src/utils/units.js
index b89b4419..afc980c8 100644
--- a/src/utils/units.js
+++ b/src/utils/units.js
@@ -13,18 +13,4 @@ const pxToRem = px => pxToRelative(px, rootFontSize, 'rem');
const pxToEm = (px, base = rootFontSize) => pxToRelative(px, base, 'em');
-/**
- * Turns a percentage string, e.g. '57%', into a fraction (0.57)
- *
- * @param {string} percentage - Percentage value ending with '%'
- *
- * @returns {number} Percentage as fraction (0..1)
- */
-
-function percentageToFraction(percentage) {
- if (!percentage || typeof percentage !== 'string') return null;
-
- return Number(percentage.slice(0, -1)) / 100;
-}
-
-export {rootFontSize, remToPx, pxToRem, pxToEm, percentageToFraction};
+export {rootFontSize, remToPx, pxToRem, pxToEm};