This repository has been archived by the owner on Jun 5, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
666c0ac
commit 34d85da
Showing
7 changed files
with
433 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.