Skip to content

Commit

Permalink
feat(Table): table actions column added
Browse files Browse the repository at this point in the history
  • Loading branch information
alx-chernigin committed Oct 1, 2024
1 parent 8d506a9 commit a17560d
Show file tree
Hide file tree
Showing 18 changed files with 531 additions and 0 deletions.
15 changes: 15 additions & 0 deletions src/components/TableActions/DefaultRowActions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@use '@gravity-ui/uikit/styles/mixins';
@use '../variables';

$block: '.#{variables.$ns}table';
$popupBlock: '#{$block}-action-popup';

#{$popupBlock} {
&__menu {
@include mixins.max-height(200px);

&-item {
@include mixins.max-text-width(250px);
}
}
}
127 changes: 127 additions & 0 deletions src/components/TableActions/DefaultRowActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React from 'react';

import {Ellipsis} from '@gravity-ui/icons';
import type {PopupPlacement} from '@gravity-ui/uikit';
import {Button, Icon, Menu, Popup, useUniqId} from '@gravity-ui/uikit';

import {block} from '../../utils';

import i18n from './i18n';
import type {TableActionConfig, TableActionGroup, TableActionsSettings} from './types';

import './DefaultRowActions.scss';

type DefaultRowActionsProps<I> = Pick<
TableActionsSettings<I>,
'getRowActions' | 'rowActionsSize'
> & {
item: I;
index: number;
};

const b = block('table');
const actionsCn = b('actions');
const actionsButtonCn = b('actions-button');

const bPopup = block('table-action-popup');
const menuCn = bPopup('menu');
const menuItemCn = bPopup('menu-item');

const DEFAULT_PLACEMENT: PopupPlacement = ['bottom-end', 'top-end', 'auto'];

const isActionGroup = <I extends unknown>(
config: TableActionConfig<I>,
): config is TableActionGroup<I> => {
return Array.isArray((config as TableActionGroup<I>).items);
};

export const DefaultRowActions = <I extends unknown>({
index: rowIndex,
item,
getRowActions,
rowActionsSize,
}: DefaultRowActionsProps<I>) => {
const [isPopupOpen, setIsPopupOpen] = React.useState(false);
const anchorRef = React.useRef<HTMLButtonElement>(null);
const rowId = useUniqId();

if (getRowActions === undefined) {
return null;
}

const renderPopupMenuItem = (action: TableActionConfig<I>, index: number) => {
if (isActionGroup(action)) {
return (
<Menu.Group key={index} label={action.title}>
{action.items.map(renderPopupMenuItem)}
</Menu.Group>
);
}

const {text, icon, handler, href, ...restProps} = action;

return (
<Menu.Item
key={index}
onClick={(event) => {
event.stopPropagation();
handler(item, index, event);

setIsPopupOpen(false);
}}
href={typeof href === 'function' ? href(item, index) : href}
iconStart={icon}
className={menuItemCn}
{...restProps}
>
{text}
</Menu.Item>
);
};

const actions = getRowActions(item, rowIndex);

if (actions.length === 0) {
return null;
}

return (
<div className={actionsCn}>
<Popup
open={isPopupOpen}
anchorRef={anchorRef}
placement={DEFAULT_PLACEMENT}
onOutsideClick={() => setIsPopupOpen(false)}
id={rowId}
>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
onClick={(event) => {
event.stopPropagation();
}}
>
<Menu className={menuCn} size={rowActionsSize}>
{actions.map(renderPopupMenuItem)}
</Menu>
</div>
</Popup>
<Button
view="flat-secondary"
className={actionsButtonCn}
onClick={(event) => {
setIsPopupOpen((value) => !value);
event.stopPropagation();
}}
size={rowActionsSize}
ref={anchorRef}
extraProps={{
'aria-label': i18n('label-actions'),
'aria-expanded': isPopupOpen,
'aria-controls': rowId,
}}
>
<Icon data={Ellipsis} />
</Button>
</div>
);
};
25 changes: 25 additions & 0 deletions src/components/TableActions/__stories__/TableActions.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {Meta, StoryObj} from '@storybook/react';

import {TableActionsColumnStory} from './stories/TableActionsColumnStory';
import {TableActionsColumnWithVirtualizationStory} from './stories/TableActionsColumnWithVirtualizationStory';
import {TableSettingsWithActionsColumnStory} from './stories/TableSettingsWithActionsColumnStory';

const meta: Meta = {
title: 'Table actions',
};

export default meta;

export const ActionsColumn: StoryObj<typeof TableActionsColumnStory> = {
render: TableActionsColumnStory,
};

export const SettingsWithActionsColumn: StoryObj<typeof TableSettingsWithActionsColumnStory> = {
render: TableSettingsWithActionsColumnStory,
};

export const ActionsColumnWithVirtualizationStory: StoryObj<
typeof TableActionsColumnWithVirtualizationStory
> = {
render: TableActionsColumnWithVirtualizationStory,
};
64 changes: 64 additions & 0 deletions src/components/TableActions/__stories__/constants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';

import {Pencil} from '@gravity-ui/icons';
import {Icon} from '@gravity-ui/uikit';
import type {ColumnDef} from '@tanstack/react-table';

import type {TableActionsSettings} from '../types';

import type {Item} from './types';

export const actionsSettings: TableActionsSettings<unknown> = {
getRowActions: (item, index) => [
{
text: 'default',
handler: () => {
alert(JSON.stringify(item));
},
},
{
text: 'with icon',
icon: <Icon data={Pencil} size={14} />,
handler: () => {},
},
{
text: 'disabled',
disabled: true,
handler: () => {},
},
{
text: 'danger theme',
theme: 'danger',
handler: () => {
alert(index);
},
},
{
text: 'with href',
theme: 'normal',
href: 'https://gravity-ui.com',
target: '_blank',
rel: 'noopener noreferrer',
handler: () => {},
},
],
};

export const baseColumns: ColumnDef<Item>[] = [
{accessorKey: 'id', header: 'Index', size: 50},
{accessorKey: 'name', header: 'Name', size: 100},
{accessorKey: 'age', header: 'Age', size: 100},
{
id: 'name-age',
accessorFn: (item) => `${item.name}: ${item.age}`,
header: () => <b>Name: Age</b>,
cell: (info) => <i>{info.getValue<string>()}</i>,
maxSize: 200,
minSize: 100,
},
{
accessorKey: 'status',
header: 'Status',
size: 100,
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';

import {Select} from '@gravity-ui/uikit';
import type {ColumnDef, RowSelectionState} from '@tanstack/react-table';

import {ACTIONS_COLUMN_ID, getActionsColumn, selectionColumn} from '../../../../constants';
import {useTable} from '../../../../hooks';
import {Table} from '../../../Table/Table';
import {actionsSettings, baseColumns} from '../constants';
import type {Item} from '../types';
import {generateData} from '../utils';

const data = generateData(5);

const defaultColumns: ColumnDef<Item>[] = [
selectionColumn as ColumnDef<Item>,
...baseColumns,
getActionsColumn<Item>(ACTIONS_COLUMN_ID, {
...actionsSettings,
}),
];

const columnsWithRenderRowActions: ColumnDef<Item>[] = [
selectionColumn as ColumnDef<Item>,
...baseColumns,
getActionsColumn<Item>(ACTIONS_COLUMN_ID, {
...actionsSettings,
renderRowActions: ({index}) => {
if (index % 2) {
return null;
}

return (
<Select
options={[
{value: '1', text: 'action 1', content: 'action 1'},
{value: '2', text: 'action 2', content: 'action 2'},
{value: '3', text: 'action 3', content: 'action 3'},
]}
size="s"
title="Actions select example"
/>
);
},
}),
];

export const TableActionsColumnStory = () => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const table = useTable({
columns: defaultColumns,
data,
state: {rowSelection},
enableRowSelection: true,
enableMultiRowSelection: true,
onRowSelectionChange: setRowSelection,
});

const tableWithRenderRowActions = useTable({
columns: columnsWithRenderRowActions,
data,
state: {rowSelection},
enableRowSelection: true,
enableMultiRowSelection: true,
onRowSelectionChange: setRowSelection,
});

return (
<React.Fragment>
<h3>{'with getRowActions property'}</h3>
<Table table={table} />
<br />
<h3>{'with renderRowActions property'}</h3>
<Table table={tableWithRenderRowActions} />
</React.Fragment>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

import type {ColumnDef, RowSelectionState} from '@tanstack/react-table';

import {ACTIONS_COLUMN_ID, getActionsColumn, selectionColumn} from '../../../../constants';
import {useTable, useWindowRowVirtualizer} from '../../../../hooks';
import {Table} from '../../../Table/Table';
import {actionsSettings, baseColumns} from '../constants';
import type {Item} from '../types';
import {generateData} from '../utils';

const data = generateData(300);

const columns: ColumnDef<Item>[] = [
selectionColumn as ColumnDef<Item>,
...baseColumns,
getActionsColumn<Item>(ACTIONS_COLUMN_ID, actionsSettings),
];

export const TableActionsColumnWithVirtualizationStory = () => {
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
const table = useTable({
columns,
data,
state: {rowSelection},
enableRowSelection: true,
enableMultiRowSelection: true,
onRowSelectionChange: setRowSelection,
});

const rowVirtualizer = useWindowRowVirtualizer({
count: table.getRowModel().rows.length,
estimateSize: () => 50,
overscan: 5,
});

return <Table table={table} rowVirtualizer={rowVirtualizer} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';

import type {ColumnDef, ColumnPinningState} from '@tanstack/react-table';

import {SETTINGS_WITH_ACTIONS_COLUMN_ID, getSettingsWithActionsColumn} from '../../../../constants';
import {useTable} from '../../../../hooks';
import {Table} from '../../../Table/Table';
import {actionsSettings, baseColumns} from '../constants';
import type {Item} from '../types';
import {generateData} from '../utils';

const data = generateData(5);

const columns: ColumnDef<Item>[] = [
...baseColumns,
getSettingsWithActionsColumn<Item>(SETTINGS_WITH_ACTIONS_COLUMN_ID, {
actions: actionsSettings,
settings: {
sortable: true,
filterable: true,
},
}),
];

export const TableSettingsWithActionsColumnStory = () => {
const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({
left: [],
right: [SETTINGS_WITH_ACTIONS_COLUMN_ID],
});
const table = useTable({
columns,
data,
enableColumnPinning: true,
state: {columnPinning},
onColumnPinningChange: setColumnPinning,
});

return <Table table={table} />;
};
7 changes: 7 additions & 0 deletions src/components/TableActions/__stories__/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Item {
id: string;
parentId?: string | undefined;
name: string;
age: number;
status?: 'free' | 'busy' | 'unknown';
}
Loading

0 comments on commit a17560d

Please sign in to comment.