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 21, 2024
1 parent f161495 commit 3dee229
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 0 deletions.
20 changes: 20 additions & 0 deletions src/components/RowActions/RowActions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@use '@gravity-ui/uikit/styles/mixins';
@use '../variables';

$block: '.#{variables.$ns}row-actions';

#{$block} {
height: 18px;
display: flex;
align-items: center;

&-popup {
&__menu {
@include mixins.max-height(200px);

&-item {
@include mixins.max-text-width(250px);
}
}
}
}
119 changes: 119 additions & 0 deletions src/components/RowActions/RowActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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 './RowActions.scss';

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

const b = block('row-actions');
const bPopup = block('row-actions-popup');

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 RowActions = <I extends unknown>({
index: rowIndex,
item,
getRowActions,
rowActionsSize,
}: RowActionsProps<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={bPopup('menu-item')}
{...restProps}
>
{text}
</Menu.Item>
);
};

const actions = getRowActions(item, rowIndex);

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

return (
<div className={b()}>
<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={bPopup('menu')} size={rowActionsSize}>
{actions.map(renderPopupMenuItem)}
</Menu>
</div>
</Popup>
<Button
view="flat-secondary"
className={b('actions-button')}
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>
);
};
18 changes: 18 additions & 0 deletions src/components/RowActions/__stories__/RowActions.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type {Meta, StoryObj} from '@storybook/react';

import {RowActionsColumnStory} from './stories/RowActionsColumnStory';
import {RowActionsWithActionsColumnStory} from './stories/RowActionsWithActionsColumnStory';

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

export default meta;

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

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

import {Pencil} from '@gravity-ui/icons';
import {Icon} from '@gravity-ui/uikit';

export {columns as baseColumns} from '../../BaseTable/__stories__/constants/columns';
import type {Item} from '../../BaseTable/__stories__/types';
import type {TableActionsSettings} from '../types';

export const actionsSettings: TableActionsSettings<Item> = {
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: () => {},
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 type {Item} from '../../../BaseTable/__stories__/types';
import {generateData} from '../../../BaseTable/__stories__/utils';
import {Table} from '../../../Table/Table';
import {actionsSettings, baseColumns} from '../constants';

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: ({row}) => {
const {index} = row;
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 RowActionsColumnStory = () => {
const [rowSelectionForTable, setRowSelectionForTable] = React.useState<RowSelectionState>({});
const table = useTable({
columns: defaultColumns,
data,
state: {rowSelection: rowSelectionForTable},
enableRowSelection: true,
enableMultiRowSelection: true,
onRowSelectionChange: setRowSelectionForTable,
});

const [rowSelectionForTableWithRenderRowActions, setRowSelectionForTableWithRenderRowActions] =
React.useState<RowSelectionState>({});
const tableWithRenderRowActions = useTable({
columns: columnsWithRenderRowActions,
data,
state: {rowSelection: rowSelectionForTableWithRenderRowActions},
enableRowSelection: true,
enableMultiRowSelection: true,
onRowSelectionChange: setRowSelectionForTableWithRenderRowActions,
});

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,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 type {Item} from '../../../BaseTable/__stories__/types';
import {generateData} from '../../../BaseTable/__stories__/utils';
import {Table} from '../../../Table/Table';
import {actionsSettings, baseColumns} from '../constants';

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 RowActionsWithActionsColumnStory = () => {
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} />;
};
3 changes: 3 additions & 0 deletions src/components/RowActions/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label-actions": "Actions"
}
10 changes: 10 additions & 0 deletions src/components/RowActions/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {addComponentKeysets} from '@gravity-ui/uikit/i18n';

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

import en from './en.json';
import ru from './ru.json';

const COMPONENT = `${NAMESPACE}table-actions`;

export default addComponentKeysets({en, ru}, COMPONENT);
3 changes: 3 additions & 0 deletions src/components/RowActions/i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label-actions": "Действия"
}
2 changes: 2 additions & 0 deletions src/components/RowActions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type * from './types';
export {RowActions} from './RowActions';
35 changes: 35 additions & 0 deletions src/components/RowActions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type {MenuItemProps} from '@gravity-ui/uikit';
import type {Row} from '@tanstack/react-table';

export interface TableAction<TValue> {
text: string;
handler: (
item: TValue,
index: number,
event: React.MouseEvent<HTMLDivElement | HTMLAnchorElement, MouseEvent>,
) => void;
href?: ((item: TValue, index: number) => string) | string;
target?: string;
rel?: string;
disabled?: boolean;
theme?: MenuItemProps['theme'];
icon?: MenuItemProps['iconStart'];
}
export interface TableActionGroup<I> {
title: string;
items: TableActionConfig<I>[];
}
export type TableActionConfig<TValue> = TableAction<TValue> | TableActionGroup<TValue>;
/**
* common sizes for Menu and Button
*/
export type TableRowActionsSize = 's' | 'm' | 'l' | 'xl';
export type RenderRowActionsProps<TValue> = {
item: TValue;
index: number;
};
export interface TableActionsSettings<TValue> {
getRowActions?: (item: TValue, index: number) => TableActionConfig<TValue>[];
renderRowActions?: (props: {row: Row<TValue>}) => React.ReactNode;
rowActionsSize?: TableRowActionsSize;
}
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './SortIndicator';
export * from './BaseTable';
export * from './Table';
export * from './TableSettings';
export * from './RowActions';
Loading

0 comments on commit 3dee229

Please sign in to comment.