Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/core/table/__tests__/table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,11 @@ test('exposes Table.HeaderRow', () => {
test('exposes Table.SortButton', () => {
expect(Table.SortButton).toBeDefined()
})

test('exposes Table.Checkbox', () => {
expect(Table.Checkbox).toBeDefined()
})

test('exposes Table.Toolbar', () => {
expect(Table.Toolbar).toBeDefined()
})
1 change: 1 addition & 0 deletions src/core/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './more-actions'
export * from './primary-action'
export * from './primary-data'
export * from './sort-button'
export * from './toolbar'
1 change: 1 addition & 0 deletions src/core/table/table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const Divs: Story = {
function buildTable(as: 'semantic' | 'div') {
return (
<>
<Table.Toolbar leftContent="2 bills" />
<Table.Head as={as === 'semantic' ? 'thead' : 'div'}>
<Table.HeaderRow as={as === 'semantic' ? 'tr' : 'div'}>
<Table.HeaderCell as={as === 'semantic' ? 'th' : 'div'} justifySelf="start">
Expand Down
7 changes: 6 additions & 1 deletion src/core/table/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { elTable } from './styles'
import { TableBody } from './body'
import { TableBodyCell } from './body-cell'
import { TableBodyRow } from './body-row'
import { TableCellCheckbox } from './checkbox'
import { TableCellDoubleLineLayout } from './double-line-layout'
import { TableCellPrimaryData } from './primary-data'
import { TableCellSortButton } from './sort-button'
Expand All @@ -12,9 +13,9 @@ import { TableHeaderRow } from './header-row'
import { TableRowMoreActions } from './more-actions'
import { TableRowPrimaryAction } from './primary-action'
import { TableRowPrimaryActionButton } from './primary-action/primary-action-button'
import { TableToolbar } from './toolbar'

import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'
import { TableCellCheckbox } from './checkbox'

interface CommonTablePros {
/**
Expand Down Expand Up @@ -50,6 +51,8 @@ type TableProps = TableAsTableProps | TableAsDivProps
* A classic table. Renders its children in a `<table>` or `<div>` element based on the specified columns.
*
* Tables are built by composing the following components:
* - **Table toolbar:** [Table.Toolbar](./?path=/docs/core-table-toolbar--docs)
*
* - **Table head:** [Table.Head](./?path=/docs/core-table-head--docs),
* [Table.HeaderRow](./?path=/docs/core-table-headerrow--docs),
* [Table.HeaderCell](./?path=/docs/core-table-headercell--docs),
Expand Down Expand Up @@ -101,3 +104,5 @@ Table.HeaderRow = TableHeaderRow
Table.SortButton = TableCellSortButton

Table.Checkbox = TableCellCheckbox

Table.Toolbar = TableToolbar
23 changes: 23 additions & 0 deletions src/core/table/toolbar/__tests__/toolbar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/react'
import { TableToolbar } from '../toolbar'

test('renders a generic element', () => {
const { container } = render(<TableToolbar />)
expect(container.firstElementChild?.tagName).toBe('DIV')
expect(container.firstElementChild).toBeVisible()
})

test('displays left content when provided', () => {
render(<TableToolbar leftContent="Left content" />)
expect(screen.getByText('Left content')).toBeVisible()
})

test('displays right content when provided', () => {
render(<TableToolbar rightContent="Right content" />)
expect(screen.getByText('Right content')).toBeVisible()
})

test('forwards additional props to the root element', () => {
const { container } = render(<TableToolbar data-testid="test-id" />)
expect(screen.getByTestId('test-id')).toBe(container.firstElementChild)
})
2 changes: 2 additions & 0 deletions src/core/table/toolbar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './styles'
export * from './toolbar'
28 changes: 28 additions & 0 deletions src/core/table/toolbar/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { font } from '#src/core/text'
import { styled } from '@linaria/react'

export const ElTableToolbar = styled.div`
display: grid;
grid-template: 'left-content right-content' 1fr / auto 1fr;
align-items: center;
width: 100%;
height: var(--size-10);

padding: 0;

background: var(--fill-white);
`

export const ElTableToolbarLeftContent = styled.div`
grid-area: left-content;
justify-self: start;
color: var(--colour-text-primary);

${font('sm', 'regular')}
text-align: left;
`

export const ElTableToolbarRightContent = styled.div`
grid-area: right-content;
justify-self: end;
`
142 changes: 142 additions & 0 deletions src/core/table/toolbar/toolbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { ButtonGroup } from '#src/core/button-group'
import { Button } from '#src/core/button'
import { CompactSelectNative } from '#src/core/compact-select-native'
import { Skeleton } from '#src/core/skeleton'
import { TableToolbar } from './toolbar'

import type { Meta, StoryObj } from '@storybook/react-vite'

const meta = {
title: 'Core/Table/Toolbar',
component: TableToolbar,
argTypes: {
leftContent: {
control: 'radio',
options: ['Item count', 'Selection count', 'Skeleton'],
mapping: {
'Item count': '251 properties',
'Selection count': '10 of 251 selected',
Skeleton: <Skeleton width="100px" />,
},
},
rightContent: {
control: 'radio',
options: ['Page size', 'Batch actions'],
mapping: {
'Page size': (
<CompactSelectNative aria-label="Page size" size="small">
<option value="25">Page size: 25</option>
<option value="50">Page size: 50</option>
<option value="100">Page size: 100</option>
</CompactSelectNative>
),
'Batch actions': (
<ButtonGroup>
<Button size="small" variant="secondary">
Action 1
</Button>
<Button size="small" variant="secondary">
Action 2
</Button>
<Button size="small" variant="secondary">
Action 3
</Button>
</ButtonGroup>
),
},
},
},
globals: {
backgrounds: 'light',
},
} satisfies Meta<typeof TableToolbar>

export default meta
type Story = StoryObj<typeof meta>

/**
* By default, the toolbar is used to display the number of items in the result set being displayed by
* the table, as well as a page size control that can be used to change the number of items displayed
* per page.
*
* Importantly, when the page size is changed by the user, their selection should become the default
* page size used by all other tables in the product.
*/
export const Example: Story = {
args: {
leftContent: 'Item count',
rightContent: 'Page size',
},
}

/**
* Correctly formatting the item count information requires some care because languages have different
* patterns for expressing plural numbers (cardinal numbers). For example, English has two forms for
* cardinal numbers: one for the singular "item" (1 dog, 1 fish, 1 property) and the other for zero
* or any other number of "items" (0 contacts, 10 inspections, 100 fish).
*
* Browser's provide the [Intl.PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules)
* API to help with this, especially when needing to support multiple languages/locales. This example shows
* how this API can be used in a simplistic manner (i.e., English-only).
*/
export const PluralNouns: Story = {
name: 'Plural nouns',
parameters: { docs: { source: { type: 'code' } } },
render: () => {
const itemCount = 10
const pluralRules = new Intl.PluralRules('en')
const nounMap = {
one: 'lemming',
other: 'lemmings',
}
const noun = nounMap[pluralRules.select(itemCount)]
return <TableToolbar leftContent={`${itemCount} ${noun}`} />
},
}

/**
* For tables that support bulk actions (actions performed on multiple items), when the user selects one
* or more rows, the toolbar should be used to display the number of selected items as well as all the
* bulk actions available.
*/
export const SelectionCount: Story = {
args: {
...Example.args,
leftContent: 'Selection count',
rightContent: 'Batch actions',
},
}

/**
* In some rare cases, the toolbar may only need to display the item count.
*/
export const LeftContentOnly: Story = {
name: 'Left content only',
args: {
...Example.args,
rightContent: null,
},
}

/**
* In other rare cases, the toolbar may only need to display some controls or actions.
*/
export const RightContentOnly: Story = {
name: 'Right content only',
args: {
...Example.args,
leftContent: null,
},
}

/**
* When the table data is still loading when the toolbar is displayed, a skeleton can be used in place of
* the item count. Generally, the page size control should be available even when data is loading so that
* users can interact with it immediately.
*/
export const Loading: Story = {
args: {
...Example.args,
leftContent: 'Skeleton',
},
}
31 changes: 31 additions & 0 deletions src/core/table/toolbar/toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ElTableToolbar, ElTableToolbarLeftContent, ElTableToolbarRightContent } from './styles'

import type { HTMLAttributes, ReactNode } from 'react'

// NOTE: we omit...
// - children, because we internally control the child content
type AttributesToOmit = 'children'

interface TableToolbarProps extends Omit<HTMLAttributes<HTMLDivElement>, AttributesToOmit> {
/** Typically used to show the total number of items or the number of selected rows. */
leftContent?: ReactNode
/**
* Typically used to display table controls, like page size, or actions available for
* the selected rows.
*/
rightContent?: ReactNode
}

/**
* A simple toolbar for tables that displays information about the number of items present, or selected,
* in the table and/or some controls related to the items. At least one of `leftContent` or `rightContent`
* must be provided.
*/
export function TableToolbar({ leftContent, rightContent, ...rest }: TableToolbarProps) {
return (
<ElTableToolbar {...rest}>
{leftContent && <ElTableToolbarLeftContent>{leftContent}</ElTableToolbarLeftContent>}
{rightContent && <ElTableToolbarRightContent>{rightContent}</ElTableToolbarRightContent>}
</ElTableToolbar>
)
}
1 change: 1 addition & 0 deletions src/storybook/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Beta versions should be relatively stable but subject to occssional breaking cha
- **chore!:** Remove `CheckboxDisabledIcon`.
- **chore!:** Remove `RadioDisabledIcon`.
- **feat:** Added new `Table.Checkbox` component. This forms the first foundational component for enabling row selection and batch actions in tables.
- **feat:** Added new `Table.Toolbar` component.

### **5.0.0-beta.46 - 25/08/25**

Expand Down
Loading