mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 15:23:33 +00:00
[PM-2276] Upgrade Storybook to v7 (#5258)
This commit is contained in:
149
libs/components/src/table/table.mdx
Normal file
149
libs/components/src/table/table.mdx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Meta, Story, Source, Primary, Controls } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./table.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
# Table
|
||||
|
||||
The table component provides a comprehensive way to display, sort and filter data. It consists of
|
||||
two portions, a UI component called `bit-table` and the underlying data source `TableDataSource`.
|
||||
This documentation will initially focus on the UI portion before covering the data source.
|
||||
|
||||
<Primary />
|
||||
|
||||
<Controls />
|
||||
|
||||
## UI Component
|
||||
|
||||
The UI component consists of a couple of elements.
|
||||
|
||||
- `bit-table`: The main component that creates a native table element and applies the table styling.
|
||||
- `header`: A container for the table header.
|
||||
- `body`: A container for the table body.
|
||||
- `bitCell`: A cell within the table. Used for both headers and content.
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Always include a row or column header with your table; this allows screen readers to better
|
||||
contextualize the data
|
||||
- Avoid spanning data across cells.
|
||||
- Be sure to make repeating actions unique by associating them with the object they relate to.
|
||||
Example: if there are multiple “Edit” buttons on a table, a screen reader should read “Edit,
|
||||
Netflix” for an edit option for a Netflix item.
|
||||
- Use [Virtual Scrolling](#virtual-scrolling) for large data sets.
|
||||
|
||||
### Usage
|
||||
|
||||
The below code is the minimum required to create a table. However we strongly advise you to use the
|
||||
`dataSource` input to provide a data source for your table. This allows you to easily sort data.
|
||||
|
||||
```html
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>Header 1</th>
|
||||
<th bitCell>Header 2</th>
|
||||
<th bitCell>Header 3</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow>
|
||||
<td bitCell>Cell 1</td>
|
||||
<td bitCell>Cell 2</td>
|
||||
<td bitCell>Cell 3</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
```
|
||||
|
||||
## Data Source
|
||||
|
||||
Bitwarden provides a data source for tables that can be used in place of a traditional data array.
|
||||
The `TableDataSource` implements sorting and filtering capabilities. This allows the `bitTable`
|
||||
component to focus on rendering while offloading the data management to the data source.
|
||||
|
||||
```ts
|
||||
// External data source
|
||||
const data: T[];
|
||||
|
||||
const dataSource = new TableDataSource<T>();
|
||||
dataSource.data = data;
|
||||
```
|
||||
|
||||
We use the `dataSource` as an input to the `bit-table` component, and access the rows to render
|
||||
within the `ng-template`which provides access to the rows using `let-rows$`.
|
||||
|
||||
```html
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="id" default>Id</th>
|
||||
<th bitCell bitSortable="name">Name</th>
|
||||
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
```
|
||||
|
||||
### Sorting
|
||||
|
||||
We provide a simple component for displaying sortable column headers. The `bitSortable` component
|
||||
wires up to the `TableDataSource` and will automatically sort the data when clicked and display an
|
||||
indicator for which column is currently sorted. The dafault sorting can be specified by setting the
|
||||
`default`.
|
||||
|
||||
```html
|
||||
<th bitCell bitSortable="id" default>Id</th>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
```
|
||||
|
||||
It's also possible to define a custom sorting function by setting the `fn` input.
|
||||
|
||||
```ts
|
||||
const sortFn = (a: T, b: T) => (a.id > b.id ? 1 : -1);
|
||||
```
|
||||
|
||||
### Filtering
|
||||
|
||||
The `TableDataSource` supports a rudimentary filtering capability most commonly used to implement a
|
||||
search function. It works by converting each entry into a string of it's properties. The string is
|
||||
then compared against the filter value using a simple `indexOf`check.
|
||||
|
||||
```ts
|
||||
dataSource.filter = "search value";
|
||||
```
|
||||
|
||||
### Virtual Scrolling
|
||||
|
||||
It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount
|
||||
of data. This is easily done by wrapping the table in the `cdk-virtual-scroll-viewport` component,
|
||||
specify a `itemSize`, set `scrollWindow` to `true` and replace `*ngFor` with `*cdkVirtualFor`.
|
||||
|
||||
```html
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="id" default>Id</th>
|
||||
<th bitCell bitSortable="name">Name</th>
|
||||
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { countries } from "../form/countries";
|
||||
|
||||
@@ -27,41 +27,43 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>Header 1</th>
|
||||
<th bitCell>Header 2</th>
|
||||
<th bitCell>Header 3</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 1</td>
|
||||
<td bitCell>Cell 2 <br> Multiline Cell</td>
|
||||
<td bitCell>Cell 3</td>
|
||||
</tr>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 4</td>
|
||||
<td bitCell>Cell 5</td>
|
||||
<td bitCell>Cell 6</td>
|
||||
</tr>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 7 <br> Multiline Cell</td>
|
||||
<td bitCell>Cell 8</td>
|
||||
<td bitCell>Cell 9</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
alignRowContent: "baseline",
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>Header 1</th>
|
||||
<th bitCell>Header 2</th>
|
||||
<th bitCell>Header 3</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 1</td>
|
||||
<td bitCell>Cell 2 <br> Multiline Cell</td>
|
||||
<td bitCell>Cell 3</td>
|
||||
</tr>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 4</td>
|
||||
<td bitCell>Cell 5</td>
|
||||
<td bitCell>Cell 6</td>
|
||||
</tr>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 7 <br> Multiline Cell</td>
|
||||
<td bitCell>Cell 8</td>
|
||||
<td bitCell>Cell 9</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
alignRowContent: "baseline",
|
||||
},
|
||||
};
|
||||
|
||||
const data = new TableDataSource<{ id: number; name: string; other: string }>();
|
||||
@@ -72,48 +74,13 @@ data.data = [...Array(5).keys()].map((i) => ({
|
||||
other: `other-${i}`,
|
||||
}));
|
||||
|
||||
const DataSourceTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="id" default>Id</th>
|
||||
<th bitCell bitSortable="name">Name</th>
|
||||
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
});
|
||||
|
||||
export const DataSource = DataSourceTemplate.bind({});
|
||||
|
||||
const data2 = new TableDataSource<{ id: number; name: string; other: string }>();
|
||||
|
||||
data2.data = [...Array(100).keys()].map((i) => ({
|
||||
id: i,
|
||||
name: `name-${i}`,
|
||||
other: `other-${i}`,
|
||||
}));
|
||||
|
||||
const ScrollableTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data2,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
export const DataSource: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
dataSource: data,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
@@ -123,51 +90,86 @@ const ScrollableTemplate: Story = (args) => ({
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
`,
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
export const Scrollable = ScrollableTemplate.bind({});
|
||||
const data2 = new TableDataSource<{ id: number; name: string; other: string }>();
|
||||
|
||||
data2.data = [...Array(100).keys()].map((i) => ({
|
||||
id: i,
|
||||
name: `name-${i}`,
|
||||
other: `other-${i}`,
|
||||
}));
|
||||
|
||||
export const Scrollable: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
dataSource: data2,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="id" default>Id</th>
|
||||
<th bitCell bitSortable="name">Name</th>
|
||||
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
const data3 = new TableDataSource<{ value: string; name: string }>();
|
||||
|
||||
// Chromatic has a max page size, lowering the number of entries to ensure we don't hit it
|
||||
data3.data = countries.slice(0, 100);
|
||||
|
||||
const FilterableTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data3,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
<th bitCell bitSortable="value" width="120px">Value</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.value }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
export const Filterable: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
dataSource: data3,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
<th bitCell bitSortable="value" width="120px">Value</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.value }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Filterable = FilterableTemplate.bind({});
|
||||
}),
|
||||
};
|
||||
|
||||
const data4 = new TableDataSource<{ name: string }>();
|
||||
|
||||
@@ -175,24 +177,24 @@ data4.data = [...Array(5).keys()].map((i) => ({
|
||||
name: i % 2 == 0 ? `name-${i}`.toUpperCase() : `name-${i}`.toLowerCase(),
|
||||
}));
|
||||
|
||||
const VariableCaseTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data4,
|
||||
},
|
||||
template: `
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
export const VariableCase: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
dataSource: data4,
|
||||
},
|
||||
template: `
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
});
|
||||
|
||||
export const VariableCase = VariableCaseTemplate.bind({});
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user