Skip to content
This repository has been archived by the owner on Jun 5, 2023. It is now read-only.

Commit

Permalink
feat: New Table component
Browse files Browse the repository at this point in the history
  • Loading branch information
diondiondion committed Sep 12, 2019
1 parent 666c0ac commit 34d85da
Show file tree
Hide file tree
Showing 7 changed files with 433 additions and 1 deletion.
23 changes: 23 additions & 0 deletions src/Table/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: Table
menu: Components
---

import {Playground, Props} from 'docz';
import Table, {Column} from './';

# Table

More detailed description to follow.

## Examples

<Playground></Playground>

## Table props

<Props of={Table} />

## Column props

<Props of={Column} />
27 changes: 27 additions & 0 deletions src/Table/getColumnConfigFromChildren.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Children} from 'react';

/**
* Get the table's column configuration via JSX from
* its `children` prop, instead of as a JS object via
* the `columns` prop
*
* @param {Array} children - Table column configuration,
* must be made of Column components as exported by the
* Table component
*
* @returns {object} Table config as object
*/

function getColumnConfigFromChildren(children) {
return Children.map(children, child => {
if (child && child.type.displayName === 'Column') {
return child.props;
} else if (child) {
console.warn(
'The Table component only accepts children that are instances of the Column component.'
);
}
}).filter(column => Boolean(column));
}

export default getColumnConfigFromChildren;
71 changes: 71 additions & 0 deletions src/Table/getColumnWidths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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;
114 changes: 114 additions & 0 deletions src/Table/getColumnsToHide.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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;
Loading

0 comments on commit 34d85da

Please sign in to comment.