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

Commit

Permalink
feat(Table): New clickable column headers
Browse files Browse the repository at this point in the history
  • Loading branch information
diondiondion committed Feb 7, 2020
1 parent b6eef4d commit 2628d6b
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 33 deletions.
137 changes: 137 additions & 0 deletions src/Table/ClickableHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled, {css} from 'styled-components';

import {alpha} from '../utils/colors';
import ArrowIcon from '../icons/Arrow';
import ButtonCore from '../ButtonCore';

import VisuallyHidden from '../VisuallyHidden';

const HeaderButton = styled(ButtonCore)`
vertical-align: baseline;
padding-right: ${p => p.theme.globals.spacing.xxs};
font-weight: bold;
`;

const ClickabilityIndicator = styled.span`
position: relative;
margin-left: ${p => p.theme.globals.spacing.xxs};
&::after {
content: '';
position: absolute;
width: 4px;
height: 4px;
top: calc(50% - 2px);
left: calc(50% - 2px);
background-color: currentcolor;
border-radius: 50%;
opacity: ${p => (p.isActive ? 0 : 0.5)};
transition: opacity 0.15s ease-in-out 0.15s;
}
${HeaderButton}:hover &::after,
${HeaderButton}.focus-visible &::after {
opacity: 0;
transition-delay: 0s;
}
`;

const StyledSortArrow = styled(ArrowIcon)`
border-radius: 50%;
transition-property: all;
${p =>
!p.isActive &&
css`
opacity: 0;
`}
${HeaderButton}:hover &,
${HeaderButton}.focus-visible & {
fill: ${p => p.theme.links};
opacity: 1;
transition-delay: 0.15s;
${p =>
p.isActive &&
`
background-color: ${alpha(p.theme.shade, p.theme.shadeStrength)};
box-shadow: 0 0 0 3px
${alpha(p.theme.shade, p.isActive ? p.theme.shadeStrength : 0)};
`}
}
`;

function SortArrow({isActive, orderDir}) {
return (
<ClickabilityIndicator isActive={isActive}>
<StyledSortArrow
vAlign
dimmed
isActive={isActive}
scale={0.8}
rotate={orderDir === 'desc' ? -90 : 90}
/>
</ClickabilityIndicator>
);
}

const reverseOrder = {
asc: 'desc',
desc: 'asc',
};

function ClickableHeader({
isActive,
column,
order,
orderLabels,
onRequestSort,
children,
}) {
if (!column.sortable) {
return children;
}

function getSortHandler({name, defaultOrder = 'asc'}) {
if (typeof onRequestSort !== 'function') return null;

return () => {
// Only flip the order when the column to
// be ordered by changes. Otherwise use the
// column's `defaultOrder` prop
onRequestSort({
property: name,
order: isActive ? reverseOrder[order] : defaultOrder,
});
};
}

return (
<HeaderButton onClick={getSortHandler(column)}>
{children}
{column.sortable && (
<SortArrow
isActive={isActive}
orderDir={isActive ? order : column.defaultOrder}
/>
)}
{isActive && <VisuallyHidden>{orderLabels[order]}</VisuallyHidden>}
</HeaderButton>
);
}

ClickableHeader.propTypes = {
isActive: PropTypes.bool,
children: PropTypes.node,
column: PropTypes.object.isRequired,
order: PropTypes.oneOf(['asc', 'desc']),
orderLabels: PropTypes.shape({
asc: PropTypes.string.isRequired,
desc: PropTypes.string.isRequired,
}),
onClick: PropTypes.func,
};

export default ClickableHeader;
2 changes: 2 additions & 0 deletions src/Table/Column.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Column.propTypes = {
cellRenderer: PropTypes.func,
hideBelowBreakpoint: PropTypes.string,
name: PropTypes.string.isRequired,
sortable: PropTypes.bool,
defaultOrder: PropTypes.oneOf(['asc', 'desc']),
subtitle: PropTypes.string,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
Expand Down
82 changes: 51 additions & 31 deletions src/Table/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import Icon from '../Icon';
import Table from './';
import Column from './Column';
import CenterContent from '../CenterContent';
import dummyData from './dummyData';
import dummyData from './demo/dummyData';
import DemoTableState from './demo/DemoTableState';

# Table

A table component with a sticky header row.
A table component with a sticky header row and clickable headers for sorting.

- By default, all columns have an equal width
- Control the width of columns by specifying a fixed or percentage width
- You can define a breakpoint under which a list-like **mobile view** will be enabled (by default this is "xs")
- 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
Expand Down Expand Up @@ -67,42 +68,61 @@ You can also define your columns as an array of objects, if you prefer:

- Wrapped in a box with rounded corners (not part of component API)
- Use `cellRenderer` to easily customise the way a cell's content is rendered
- Use the `rowHeader` prop to determine which column will be used as the header in the mobile view (only needs to be specified if it's not the first column)
- Use the `rowHeader` prop to specify which column will be used as the header in the mobile view (only needs to be specified if it's not the first column)
- Use fixed or percentage `width` values to customise column sizes
- Use the `shadedHeader` prop for a shaded table header background
- Use the `subtitle` prop to add a subtitle to a column header
- Use the `pl` or `pr` props to specify left and right inner padding on the table, to visually align the first and last columns with the rest of the design without affecting horizontal rules
- Enable sortable columns by adding `<Column sortable />` props and passing `sort` and `onRequestSort` props to `Table`

<Playground>
<div style={{border: '1px solid grey', borderRadius: 10}}>
<Table
shadedHeader
data={dummyData.slice(4, 8)}
itemKey="name"
rowHeader="Name"
pl="xl"
pr={20}
>
<Column
name="Role"
width={32}
cellRenderer={item => (
<Icon
name={item.role === 'manager' ? 'star' : 'user'}
aria-label={
item.role === 'manager' ? 'Manager' : 'User'
}
<DemoTableState data={dummyData.slice(4, 8)}>
{({data, sort: {property, order}, handleSort}) => (
<Table
shadedHeader
data={data}
itemKey="name"
rowHeader="Name"
sort={{
property,
order,
}}
onRequestSort={handleSort}
pl="xl"
pr={20}
>
<Column
name="Role"
width={32}
cellRenderer={item => (
<Icon
name={item.role === 'manager' ? 'star' : 'user'}
aria-label={
item.role === 'manager' ? 'Manager' : 'User'
}
/>
)}
/>
)}
/>
<Column
name="Name"
width="40%"
cellRenderer={item => <strong>{item.name}</strong>}
/>
<Column name="Type" subtitle="Random characters" />
<Column name="Time" />
</Table>
<Column
sortable
name="Name"
width="40%"
cellRenderer={item => <strong>{item.name}</strong>}
/>
<Column
sortable
name="Type"
subtitle="Random characters"
/>
<Column
sortable
name="Time"
defaultOrder="desc"
/>
</Table>
)}
</DemoTableState>
</div>
</Playground>

Expand Down
38 changes: 38 additions & 0 deletions src/Table/demo/DemoTableState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {useState} from 'react';

function sortBy(property, order = 'asc') {
const orderAsNumber = {
asc: 1,
desc: -1,
};

const prop = property.toLowerCase();

return function(a, b) {
const result = a[prop] < b[prop] ? -1 : a[prop] > b[prop] ? 1 : 0;
return result * orderAsNumber[order];
};
}

function DemoTableState({data, children}) {
const [order, setOrder] = useState('asc');
const [orderProp, setOrderProp] = useState('Name');

function handleSort(newOrder) {
setOrder(newOrder.order);
setOrderProp(newOrder.property);
}

const orderedData = data.sort(sortBy(orderProp, order));

return children({
data: orderedData,
sort: {
order,
property: orderProp,
},
handleSort,
});
}

export default DemoTableState;
File renamed without changes.
Loading

0 comments on commit 2628d6b

Please sign in to comment.