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 }]
|
||||
* @param {boolean} props.selectable - Включить чекбоксы выбора (default: true)
|
||||
* @param {number} props.skeletonRows - Количество строк скелетона (default: 10)
|
||||
* @param {boolean} props.striped - Чередование фона строк (Tabler UX, default: true)
|
||||
* @param {boolean} props.compact - Уменьшенные отступы ячеек (default: false)
|
||||
*/
|
||||
function DataTable({
|
||||
columns = [],
|
||||
@@ -56,11 +58,13 @@ function DataTable({
|
||||
actions = [],
|
||||
selectable = true,
|
||||
skeletonRows = 10,
|
||||
striped = true,
|
||||
compact = false,
|
||||
}) {
|
||||
const hasActions = onEdit || onDelete || onCopy || actions.length > 0;
|
||||
const isEditing = (item) => inlineEdit && item[itemKey] === inlineEdit.editingKey;
|
||||
|
||||
// Рендер заголовка колонки
|
||||
// Рендер заголовка колонки (col.title — подпись, col.tooltip — подсказка для UX)
|
||||
const renderColumnHeader = (col) => {
|
||||
const isSortable = col.sortable !== false && onSort;
|
||||
const isActive = sortField === col.key;
|
||||
@@ -77,22 +81,27 @@ function DataTable({
|
||||
</div>
|
||||
);
|
||||
|
||||
const thProps = {
|
||||
key: col.key,
|
||||
scope: 'col',
|
||||
style: col.width ? { width: col.width } : undefined,
|
||||
};
|
||||
|
||||
if (isSortable) {
|
||||
return (
|
||||
<th
|
||||
key={col.key}
|
||||
<th
|
||||
{...thProps}
|
||||
className="cursor-pointer user-select-none"
|
||||
onClick={() => onSort(col.key)}
|
||||
style={col.width ? { width: col.width } : undefined}
|
||||
>
|
||||
{content}
|
||||
{col.tooltip ? <Tooltip content={col.tooltip} position="top">{content}</Tooltip> : content}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<th key={col.key} style={col.width ? { width: col.width } : undefined}>
|
||||
{content}
|
||||
<th {...thProps}>
|
||||
{col.tooltip ? <Tooltip content={col.tooltip} position="top">{content}</Tooltip> : content}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
@@ -210,15 +219,20 @@ function DataTable({
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="table-responsive">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<table className={tableClass} role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
{selectable && (
|
||||
<th style={{ width: '40px' }}>
|
||||
<th scope="col" style={{ width: '40px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
@@ -229,7 +243,7 @@ function DataTable({
|
||||
</th>
|
||||
)}
|
||||
{columns.map(renderColumnHeader)}
|
||||
{hasActions && <th className="text-end">Действия</th>}
|
||||
{hasActions && <th scope="col" className="text-end td-actions">Действия</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -261,7 +275,7 @@ function DataTable({
|
||||
</td>
|
||||
))}
|
||||
{hasActions && (
|
||||
<td className="text-end">
|
||||
<td className="text-end td-actions">
|
||||
{renderActions(item)}
|
||||
</td>
|
||||
)}
|
||||
|
||||
+15
-1
@@ -296,7 +296,7 @@ button:focus-visible,
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* ===== Table Improvements ===== */
|
||||
/* ===== Table Improvements (Tabler + UX) ===== */
|
||||
.table thead th {
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -318,6 +318,20 @@ button:focus-visible,
|
||||
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;
|
||||
-webkit-user-select: none;
|
||||
|
||||
Reference in New Issue
Block a user