refactor(DataTable): enhance table styling and functionality with fixed action column, improved striped rows, and new props for UX customization
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
# Таблицы: UX/UI по Tabler и проекту
|
||||||
|
|
||||||
|
Рекомендации по формированию таблиц в приложении — [Tabler Tables](https://docs.tabler.io/ui/components/tables/), [Preview](https://preview.tabler.io/tables.html).
|
||||||
|
|
||||||
|
## Классы (Tabler)
|
||||||
|
|
||||||
|
- **Базовые:** `table card-table table-vcenter table-nowrap mb-0`
|
||||||
|
- **Чередование строк:** `table-striped` — улучшает читаемость
|
||||||
|
- **Компактный режим:** `table-sm` — меньше отступы в ячейках
|
||||||
|
- **Обёртка:** `table-responsive` вокруг `<table>`
|
||||||
|
- **Обрезка длинного текста:** для ячейки использовать `td-truncate` (Tabler) или `text-truncate` + `max-width`
|
||||||
|
|
||||||
|
## Компонент DataTable
|
||||||
|
|
||||||
|
Используйте `DataTable` там, где подходит декларативное описание колонок и общие действия (сортировка, выбор, пагинация):
|
||||||
|
|
||||||
|
- `columns`: `{ key, title, sortable?, icon?, render?, tooltip? }` — для подсказки по заголовку задайте `tooltip`
|
||||||
|
- `striped={true}` по умолчанию
|
||||||
|
- `compact={true}` — компактные ячейки
|
||||||
|
- Колонка «Действия» получает класс `td-actions` (фиксированная минимальная ширина в проекте)
|
||||||
|
|
||||||
|
## Информативность и лаконичность
|
||||||
|
|
||||||
|
1. **Заголовки колонок** — короткие, при необходимости уточнение через `tooltip` (например, значение иконки замка).
|
||||||
|
2. **Действия в строке** — иконки с `Tooltip` и `aria-label`; колонка «Действия» справа, без лишнего текста.
|
||||||
|
3. **Копирование** — кнопка/иконка копирования рядом с ячейкой (IP, домен и т.д.) вместо дублирования в «Действия», если это основной сценарий.
|
||||||
|
4. **Пустое состояние** — через `emptyState` в DataTable или свой блок с призывом к действию (импорт, добавление).
|
||||||
|
|
||||||
|
## Доступность
|
||||||
|
|
||||||
|
- У заголовков: `scope="col"`.
|
||||||
|
- У таблицы: `role="grid"` при семантической сетке данных.
|
||||||
|
- Кнопки действий: `aria-label` и/или Tooltip.
|
||||||
|
|
||||||
|
## Где таблица своя, а не DataTable
|
||||||
|
|
||||||
|
В IPRangesManager, DomainsNewManager, ServerManager и др. таблицы рендерятся вручную. Для единообразия:
|
||||||
|
|
||||||
|
- Те же классы: `table card-table table-vcenter table-nowrap mb-0`, при желании `table-striped`.
|
||||||
|
- Заголовки: `th` с `scope="col"`, короткий текст, при необходимости иконка и тултип.
|
||||||
|
- Колонка действий: класс `td-actions` у `th` и `td`, кнопки с тултипами.
|
||||||
|
|
||||||
|
При рефакторинге страниц можно постепенно переводить такие таблицы на DataTable с описанием `columns` и общих обработчиков.
|
||||||
@@ -34,6 +34,8 @@ import Tooltip from './Tooltip.jsx';
|
|||||||
* - [{ icon: Component, label: string, onClick: (item) => void, variant?: string }]
|
* - [{ icon: Component, label: string, onClick: (item) => void, variant?: string }]
|
||||||
* @param {boolean} props.selectable - Включить чекбоксы выбора (default: true)
|
* @param {boolean} props.selectable - Включить чекбоксы выбора (default: true)
|
||||||
* @param {number} props.skeletonRows - Количество строк скелетона (default: 10)
|
* @param {number} props.skeletonRows - Количество строк скелетона (default: 10)
|
||||||
|
* @param {boolean} props.striped - Чередование фона строк (Tabler UX, default: true)
|
||||||
|
* @param {boolean} props.compact - Уменьшенные отступы ячеек (default: false)
|
||||||
*/
|
*/
|
||||||
function DataTable({
|
function DataTable({
|
||||||
columns = [],
|
columns = [],
|
||||||
@@ -56,11 +58,13 @@ function DataTable({
|
|||||||
actions = [],
|
actions = [],
|
||||||
selectable = true,
|
selectable = true,
|
||||||
skeletonRows = 10,
|
skeletonRows = 10,
|
||||||
|
striped = true,
|
||||||
|
compact = false,
|
||||||
}) {
|
}) {
|
||||||
const hasActions = onEdit || onDelete || onCopy || actions.length > 0;
|
const hasActions = onEdit || onDelete || onCopy || actions.length > 0;
|
||||||
const isEditing = (item) => inlineEdit && item[itemKey] === inlineEdit.editingKey;
|
const isEditing = (item) => inlineEdit && item[itemKey] === inlineEdit.editingKey;
|
||||||
|
|
||||||
// Рендер заголовка колонки
|
// Рендер заголовка колонки (col.title — подпись, col.tooltip — подсказка для UX)
|
||||||
const renderColumnHeader = (col) => {
|
const renderColumnHeader = (col) => {
|
||||||
const isSortable = col.sortable !== false && onSort;
|
const isSortable = col.sortable !== false && onSort;
|
||||||
const isActive = sortField === col.key;
|
const isActive = sortField === col.key;
|
||||||
@@ -77,22 +81,27 @@ function DataTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const thProps = {
|
||||||
|
key: col.key,
|
||||||
|
scope: 'col',
|
||||||
|
style: col.width ? { width: col.width } : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
if (isSortable) {
|
if (isSortable) {
|
||||||
return (
|
return (
|
||||||
<th
|
<th
|
||||||
key={col.key}
|
{...thProps}
|
||||||
className="cursor-pointer user-select-none"
|
className="cursor-pointer user-select-none"
|
||||||
onClick={() => onSort(col.key)}
|
onClick={() => onSort(col.key)}
|
||||||
style={col.width ? { width: col.width } : undefined}
|
|
||||||
>
|
>
|
||||||
{content}
|
{col.tooltip ? <Tooltip content={col.tooltip} position="top">{content}</Tooltip> : content}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th key={col.key} style={col.width ? { width: col.width } : undefined}>
|
<th {...thProps}>
|
||||||
{content}
|
{col.tooltip ? <Tooltip content={col.tooltip} position="top">{content}</Tooltip> : content}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -210,15 +219,20 @@ function DataTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allSelected = items.length > 0 && items.every(i => selectedItems.has(i[itemKey]));
|
const allSelected = items.length > 0 && items.every(i => selectedItems.has(i[itemKey]));
|
||||||
|
const tableClass = [
|
||||||
|
'table card-table table-vcenter table-nowrap mb-0',
|
||||||
|
striped && 'table-striped',
|
||||||
|
compact && 'table-sm',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className={tableClass} role="grid">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{selectable && (
|
{selectable && (
|
||||||
<th style={{ width: '40px' }}>
|
<th scope="col" style={{ width: '40px' }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
@@ -229,7 +243,7 @@ function DataTable({
|
|||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{columns.map(renderColumnHeader)}
|
{columns.map(renderColumnHeader)}
|
||||||
{hasActions && <th className="text-end">Действия</th>}
|
{hasActions && <th scope="col" className="text-end td-actions">Действия</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -261,7 +275,7 @@ function DataTable({
|
|||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{hasActions && (
|
{hasActions && (
|
||||||
<td className="text-end">
|
<td className="text-end td-actions">
|
||||||
{renderActions(item)}
|
{renderActions(item)}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+15
-1
@@ -296,7 +296,7 @@ button:focus-visible,
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Table Improvements ===== */
|
/* ===== Table Improvements (Tabler + UX) ===== */
|
||||||
.table thead th {
|
.table thead th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -318,6 +318,20 @@ button:focus-visible,
|
|||||||
background-color: rgba(32, 107, 196, 0.1);
|
background-color: rgba(32, 107, 196, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Колонка «Действия» — фиксированная ширина, не сжимается (лаконичный UX) */
|
||||||
|
.table .td-actions {
|
||||||
|
min-width: 140px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Выбранная строка поверх zebra (table-striped) */
|
||||||
|
.table-striped tbody tr.table-selected > * {
|
||||||
|
--tblr-table-bg-type: rgba(32, 107, 196, 0.08);
|
||||||
|
}
|
||||||
|
.table-striped tbody tr.table-selected:hover > * {
|
||||||
|
--tblr-table-bg-type: rgba(32, 107, 196, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
.user-select-none {
|
.user-select-none {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user