diff --git a/src/components/Popover/Popover.stories.tsx b/src/components/Popover/Popover.stories.tsx index c97bd3b4..1ded2fe0 100644 --- a/src/components/Popover/Popover.stories.tsx +++ b/src/components/Popover/Popover.stories.tsx @@ -3,21 +3,25 @@ import { Button } from '../Button' import { Icon } from '../Icon' import { placementOptions } from '../utils' import { SbTheme } from '../../../.storybook/preview' +import { styleguide } from 'components/assets/styles' import { useTheme } from 'react-jss' import { Meta, Story } from '@storybook/react/types-6-0' import { Popover, PopoverProps } from './index' import React, { FC } from 'react' +const { spacing } = styleguide + export default { argTypes: { children: { control: { disable: true } }, + classes: { control: { disable: true } }, content: { control: { disable: true }, defaultValue: ( - <> +
View account info
- +
) }, placement: { diff --git a/src/components/Popover/utils.ts b/src/components/Popover/utils.ts index 55956d65..c4ce5653 100644 --- a/src/components/Popover/utils.ts +++ b/src/components/Popover/utils.ts @@ -2,8 +2,11 @@ import { styleguide } from 'components/assets/styles/styleguide' import { ColorManipulationTypes, manipulateColor } from '../utils' import { themedStyles, ThemeType } from 'components/assets/styles/themes' -const { borderRadius, colors } = styleguide -const { blacks, whites } = colors +const { + borderRadius, + colors: { blacks, whites }, + fontWeight +} = styleguide const { dark, light } = ThemeType @@ -40,18 +43,19 @@ export const generatePopoverStyles = (themeType: ThemeType) => { '& > .ant-popover-inner': { '& > .ant-popover-inner-content': { color: base.color, - fontWeight: 300 + fontWeight: fontWeight.light, + padding: 0 }, '& > .ant-popover-title': { borderBottomColor: base.borderColor, color: text.title, - fontWeight: 300 + fontWeight: fontWeight.light }, backgroundColor: background, borderRadius, boxShadow: 'none', color: base.color, - fontWeight: 300 + fontWeight: fontWeight.light } }, filter: `drop-shadow(0px 2px 8px ${accent})` diff --git a/src/components/Skeleton/index.tsx b/src/components/Skeleton/index.tsx index 7b5e1cc3..e0061e0d 100644 --- a/src/components/Skeleton/index.tsx +++ b/src/components/Skeleton/index.tsx @@ -70,7 +70,7 @@ interface DefaultSkeletonProps { /** * Skeleton width. If undefined, skeleton will span the width of parent container. **Note**: width is a required prop for a circle skeleton. */ - width?: number + width?: number | string } interface CircleSkeletonProps diff --git a/src/components/Table/Table.stories.mdx b/src/components/Table/Table.stories.mdx index 34b84062..6a981faa 100644 --- a/src/components/Table/Table.stories.mdx +++ b/src/components/Table/Table.stories.mdx @@ -8,7 +8,7 @@ The `Table` component creates a table from a provided data source. It allows for The following examples start from a basic table and don't show all possible types of data that the table can render. If you'd like to view all possible column types and formats in one place, [click here.](?path=/docs/table--mixed#columntype--all-column-types-and-formats) - + ## Simple Usage diff --git a/src/components/Table/Table.stories.tsx b/src/components/Table/Table.stories.tsx index 384e5844..20df38a4 100644 --- a/src/components/Table/Table.stories.tsx +++ b/src/components/Table/Table.stories.tsx @@ -1,14 +1,20 @@ import { action } from '@storybook/addon-actions' +import { createUseStyles } from 'react-jss' import { Story } from '@storybook/react/types-6-0' import tableData4 from './fixtures/4_sample_data' import { DataId, Table, TableProps } from '.' import React, { Key, useState } from 'react' +import { styleguide, themes, ThemeType } from 'components/assets/styles' import tableData0, { Person } from './fixtures/0_sample_data' import tableData1, { File } from './fixtures/1_sample_data' import tableData2, { Client } from './fixtures/2_sample_data' import tableData3, { Client1 } from './fixtures/3_sample_data' import tableData5, { Dot } from './fixtures/5_sample_data' +const { spacing } = styleguide + +const { dark, light } = ThemeType + const commonArgTypes = { activeRowKey: { control: { disable: true } @@ -47,6 +53,33 @@ const commonArgTypes = { } } +const useStyles = createUseStyles({ + decorator: { + background: themes[light].background.secondary, + height: `calc(100vh - ${spacing.m * 2}px)`, + padding: spacing.l, + width: '100%' + }, + // eslint-disable-next-line sort-keys + '@global': { + [`.${dark}`]: { + '& $decorator': { + background: themes[dark].background.secondary + } + } + } +}) + +export const Decorator = (TableStory: Story) => { + const classes = useStyles() + + return ( +
+ +
+ ) +} + const DecoratedTableStory = (props: TableProps) => { const [activeRowKey, setActiveRowKey] = useState('') @@ -239,3 +272,4 @@ const ColoredDotTemplate: Story> = args => ( ) export const ColoredDot = ColoredDotTemplate.bind({}) ColoredDot.args = tableData5 +ColoredDot.argTypes = commonArgTypes diff --git a/src/components/Table/TableSkeleton.tsx b/src/components/Table/TableSkeleton.tsx new file mode 100644 index 00000000..4ff3b5e6 --- /dev/null +++ b/src/components/Table/TableSkeleton.tsx @@ -0,0 +1,156 @@ +import { createUseStyles } from 'react-jss' +import random from 'lodash/random' +import { Skeleton } from '../Skeleton' +import { tablePalette } from './styles' +import times from 'lodash/times' +import { ColumnFormats, ColumnType, ColumnTypes } from './types' +import React, { FC } from 'react' +import { styleguide, ThemeType } from 'components/assets/styles' + +const { spacing } = styleguide + +const { dark, light } = ThemeType + +const useStyles = createUseStyles({ + skeleton: { + maxWidth: 300 + }, + table: { + borderCollapse: 'separate', + borderSpacing: 0, + tableLayout: 'fixed', + textAlign: 'left', + width: '100%' + }, + td: { + '&:first-of-type': { + paddingLeft: spacing.l + }, + '&:last-of-type': { + paddingRight: spacing.l + }, + background: tablePalette[light].td.base.background, + borderBottom: `1px solid ${tablePalette[light].td.base.border}`, + height: 54, + padding: `0 ${spacing.m}px` + }, + th: { + '&:first-of-type': { + paddingLeft: spacing.l + }, + '&:last-of-type': { + paddingRight: spacing.l + }, + background: tablePalette[light].th.base.background, + height: 55, + padding: `0 ${spacing.m}px` + }, + // eslint-disable-next-line sort-keys + '@global': { + [`.${dark}`]: { + '& $table': {}, + '& $td': { + background: tablePalette[dark].td.base.background, + borderBottom: `1px solid ${tablePalette[dark].td.base.border}` + }, + '& $th': { + background: tablePalette[dark].th.base.background + } + } + } +}) + +// ------------------------------------ + +const THeaderCellSkeleton = () => { + const classes = useStyles() + + return ( + + + + ) +} + +// ------------------------------------ + +const mappedSkeletonProps: Record = { + [ColumnFormats.coloredDot]: { circle: true, width: 15 }, + [ColumnFormats.icon]: { width: 50 }, + [ColumnFormats.toggle]: { width: 50 }, + [ColumnTypes.number]: { width: 100 } +} + +interface TDataCellSkeletonProps extends Pick { + index: number +} + +const TDataCellSkeleton: FC = ({ + columns, + index +}: TDataCellSkeletonProps) => { + const classes = useStyles() + + const format = columns[index].format + const type = columns[index].type + + let props = {} + + if (mappedSkeletonProps[type]) { + props = mappedSkeletonProps[type] + } else { + props = + format && mappedSkeletonProps[format] + ? mappedSkeletonProps[format] + : { width: `${random(25, 100)}%` } + } + + return ( + + + + ) +} + +// ------------------------------------ + +interface TableSkeletonProps { + columns: ColumnType[] + rowCount: number +} + +export const TableSkeleton: FC = ({ + columns, + rowCount +}: TableSkeletonProps) => { + const classes = useStyles() + + return ( + + + + {times(columns.length, (j: number) => ( + + ))} + + + + {times(rowCount, (i: number) => ( + + {times(columns.length, (j: number) => ( + + ))} + + ))} + +
+ ) +} diff --git a/src/components/Table/__tests__/Table.test.tsx b/src/components/Table/__tests__/Table.test.tsx index c4833d94..37b0bc2b 100644 --- a/src/components/Table/__tests__/Table.test.tsx +++ b/src/components/Table/__tests__/Table.test.tsx @@ -1,11 +1,12 @@ import { act } from 'react-dom/test-utils' import moment from 'moment' import React from 'react' +import { TableSkeleton } from '../TableSkeleton' import { Input as AntDInput, Table as AntDTable } from 'antd' import mockData, { Data, dateFormat } from '__mocks__/table_mock_data' import mockData0, { Person } from '../fixtures/0_sample_data' import mockData1, { File } from '../fixtures/4_sample_data' -import { mount, ReactWrapper } from 'enzyme' +import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme' import { Table, TableProps } from '..' /* Helper functions */ @@ -129,6 +130,19 @@ describe('Table props', () => { expect.arrayContaining(mockData0.columns) ) }) + + it('throws an error if passed skeletonRowCount prop is less than 1', () => { + expect(() => + shallow( + + columns={mockData0.columns} + data={[]} + loading + skeletonRowCount={0} + /> + ) + ).toThrow() + }) }) describe('Table search and searchProps', () => { @@ -163,9 +177,9 @@ describe('Table search and searchProps', () => { it('it renders the search bar to the left by default', async () => { const table = wrapper.find(Table) - const searchBar = table.find('input') + const searchBarWrapper = table.find('[className*="searchBarWrapper"]') - const style = window.getComputedStyle(searchBar.getDOMNode()) + const style = window.getComputedStyle(searchBarWrapper.getDOMNode()) expect(style.alignSelf).toBe('flex-start') }) @@ -179,9 +193,9 @@ describe('Table search and searchProps', () => { ) const table = wrapper.find(Table) - const searchBar = table.find('input') + const searchBarWrapper = table.find('[className*="searchBarWrapper"]') - const style = window.getComputedStyle(searchBar.getDOMNode()) + const style = window.getComputedStyle(searchBarWrapper.getDOMNode()) expect(style.alignSelf).toBe('flex-end') }) @@ -233,12 +247,6 @@ describe('Table onRowClick, activeRowKey', () => { describe('Table pagination', () => { it('does not show pagination if there are less than 10 rows', () => { - wrapper = mount( - createTable({ - ...mockData0 - }) - ) - expect(wrapper.find(AntDTable).props().pagination).toBe(false) expect(wrapper.find('.ant-pagination').exists()).toBeFalsy() @@ -258,3 +266,13 @@ describe('Table pagination', () => { expect(wrapper.find('.ant-pagination').exists()).toBeTruthy() }) }) + +describe('Table loading', () => { + it('renders a TableSkeleton if loading prop is passed as true', () => { + const wrapper: ShallowWrapper = shallow( + columns={mockData0.columns} data={[]} loading /> + ) + + expect(wrapper.find(TableSkeleton)).toHaveLength(1) + }) +}) diff --git a/src/components/Table/__tests__/TableSkeleton.test.tsx b/src/components/Table/__tests__/TableSkeleton.test.tsx new file mode 100644 index 00000000..2da08c77 --- /dev/null +++ b/src/components/Table/__tests__/TableSkeleton.test.tsx @@ -0,0 +1,22 @@ +import mockData from '__mocks__/table_mock_data' +import React from 'react' +import { TableSkeleton } from '../TableSkeleton' +import { mount, ReactWrapper } from 'enzyme' + +let wrapper: ReactWrapper + +beforeEach(() => { + wrapper = mount() +}) + +describe('TableSkeleton', () => { + it('renders', () => { + expect(wrapper.find(TableSkeleton)).toHaveLength(1) + }) + + it('renders correct number of skeleton Table rows', () => { + const tBody = wrapper.find(TableSkeleton).find('tbody') + + expect(tBody.find('tr')).toHaveLength(5) + }) +}) diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index eee4fca2..647564f6 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -7,6 +7,7 @@ import debounce from 'lodash/debounce' import Fuse from 'fuse.js' import { getDataTestAttributeProp } from '../utils' import { Input } from '../Input' +import { TableSkeleton } from './TableSkeleton' import { useStyles } from './styles' import { ColumnType, TableData } from './types' import { mapData, mapFilterKeys, processColumns, processData } from './utils' @@ -50,6 +51,10 @@ export interface TableProps extends CommonComponentProps { * Array of data objects */ data: TableData[] + /** + * Whether or not to show skeleton loader + */ + loading?: boolean /** * Optional callback that runs when a table row is clicked */ @@ -58,6 +63,10 @@ export interface TableProps extends CommonComponentProps { * Optional prop to enable/disable table search */ search?: boolean + /** + * Number of skeleton table rows shown if loading is set to true + */ + skeletonRowCount?: number /** * Optional props for search input */ @@ -74,8 +83,10 @@ export const Table = ({ columns, data, dataTag, + loading = false, onRowClick, search = true, + skeletonRowCount = 5, searchProps = {} as SearchProps }: TableProps) => { const [searchTerm, setSearchTerm] = useState('') @@ -156,25 +167,34 @@ export const Table = ({ }) } + if (skeletonRowCount < 1) + throw new Error('skeletonRowCount must be a positive integer') + return (
{search && ( - + +
+ )} + {loading ? ( + + ) : ( + )} - ) } diff --git a/src/components/Table/styles.ts b/src/components/Table/styles.ts index a4c1c401..b38246c5 100644 --- a/src/components/Table/styles.ts +++ b/src/components/Table/styles.ts @@ -60,7 +60,7 @@ export const generatePaginationStyles = (themeType: ThemeType) => { } } -const tablePalette = { +export const tablePalette = { [dark]: { arrow: { active: blacks['lighten-60'], @@ -154,6 +154,18 @@ const generateTableStyles = (themeType: ThemeType) => { }, cursor: 'default' }, + '&.ant-table-empty .ant-table-tbody > tr.ant-table-placeholder': { + '& .ant-empty-image': { + display: 'none' + }, + '& .ant-table-cell': { + borderBottom: 'none' + }, + '& > .ant-table-cell': { + background: td.base.background + }, + color: td.base.background + }, background: td.base.background } } @@ -238,7 +250,7 @@ export const useStyles = createUseStyles({ } } }, - searchBar: { + searchBarWrapper: { alignSelf: props => props.searchProps.placement === 'right' ? 'flex-end' : 'flex-start', marginBottom: spacing.m