Level: organism.
Purpose and use
Compose a desktop record table with filters, typed cells, selection, actions and pagination. Supply complete fixture/data inputs; Table does not call backend APIs.
Anatomy, behavior, and accessibility
Optional Filter Row and bulk-action bar surround a horizontally scrollable table with Header/Cell components; optional Pagination follows it. Column IDs address each row's cells; row IDs must be stable and unique.
Table forwards Filter Row's controlled
filterOpenRequestand semanticfilterActionwithout changing their payloads. The enclosing screen owns filter values, overlay state, and local or remote data filtering.With
containedScroll=true, Table fills its available height and makes only the scroll frame vertically and horizontally scrollable. The Filter Row stays outside that viewport and the column header remains sticky at its top edge. The viewport exposesrole="region",tabindex="0", andtableAriaLabel.Sort requests emit column/direction and a local flag. Rows are sorted locally when pagination is hidden or totalItems does not exceed the supplied rows; otherwise the parent provides ordered pages. Pagination emits changes without slicing rows. Replacing the columns array resets internal sort override.
Enabled checkbox cells update internal selection and aggregate header state. Replacing the rows array resets internal selection overrides. Bulk buttons emit selectedRows then clear selection; bulkActions=false removes checkbox columns.
Fixed edge columns receive a
fixedOffsetcalculated from the widths of preceding fixed columns and are anchored natively throughinset-inline-start/inset-inline-end. No scroll-driven transform or signal updates their position. Table passes the boundary side through Header'sstickyinput; Header owns its bottom and boundary separators. Loading marks busy, overlays a progress indicator and disables selection/action paths. filteredEmpty with zero rows shows filter-result copy only when loading is false.fixedColumn: trueon the first data column and on consecutive following columns creates one leading fixed group. The group ends at the first column without explicittrue; Table derives cumulative left offsets and applies the boundary separator to its final column.WblTableColumn.fixedColumn: falseexplicitly removes that column from automatic edge fixation. A first data column or actions column with this value receives no fixed side, inset offset, fixed divider, or Table Cell fixed boundary. OmittingfixedColumnpreserves the current automatic first-data/actions behavior for existing consumers.When Table is projected into
wbl-main-container, its Filter Row, any rendered Action Bar, and scroll frame/grid remain inside the same page-layout-selected rail. Main Container never creates a Table breakout; the viewport, including any native scrollbar gutter, uses exactly the rail's inline edges.containedScrolland pagination never widen it to reserve that gutter. The grid keeps ownership of horizontal scrolling and fixed edges. This is a Main Container composition rule, not a Table input, variant, or public API.Table, rows, headers and cells expose structural roles and accessible names; interactive descendants provide native buttons/links/checkboxes. Use column, row-selection and action labels meaningful without surrounding visual context.
With
adaptive=true, Table measures its own inline size (not the window). Below 1024px it replaces the grid with a list of cards, one per row; at 1024px and wider it stays a table.adaptivedefaults tofalse, so existing consumers are unchanged. Each card starts with a header row: the selection checkbox (when bulk actions are on) at the start and a «more» (three dots) menu at the end. Below it come «label — value» pairs for the firstcardPrimaryColumnsdata columns (default 5); labels use--color-text-icon-primary-gray. The menu holds the row actions; selecting one emits the usualactionSelect. When there are more data columns, a «Показать ещё» button at the bottom of the card reveals them inside the same card and becomes «Свернуть». The block expands and collapses smoothly (height and opacity); withprefers-reduced-motion: reduceit switches instantly. Collapsed fields stay in the DOM but areinert. In card mode the action bar stays on one line with 8px block padding: «Выбрано» at the start, bulk buttons at the end, rendered as primary flat buttons. Once rows are selected, «Выбрать все N строк» takes a second line under «Выбрано» so it never overlaps the buttons. Cards stack in one full-width column with no border or radius and 12px inline padding, matching the action bar; a divider separates neighbouring cards. Number cells are left-aligned in cards; fixed columns and horizontal scroll do not apply. Cards keeprole="table"/row, and each label is arowheader.
Open Questions
Adaptive cards (A-011, prototype under review): there is no column header, so
sorting and the header «select all» checkbox are unavailable in card mode
(selecting all remains possible from the action bar after the first selection).
Chevron columns are omitted from cards. The 1024px threshold is a component
constant, not a token: a shared breakpoint scale (390 / 780 / 1024 / 1280 /
1440 / 1920) is proposed but not yet recorded in specs/ux/foundations or
tokens.scss. Filter Row and Pagination have no adaptive behaviour yet. Row
action variants are not distinguished inside the card menu. No Figma evidence exists
for the card layout.
There is no virtual scrolling, backend sorting/paging or full grid keyboard navigation. A chevron event does not itself render nested rows. Empty rows without filteredEmpty have no dedicated empty illustration; screen-level state selection and data-error messaging remain caller responsibilities.
Local component contract
This is the active owned contract for component.table. The API below
is verified against the current local implementation, focused test and story.
The Source commit 0112cba0779a2d2a4b6c7a17243201694344d70a remains
migration provenance, not the runtime owner.
The Main Container composition rule above does not add a Table input, output, model, selector, type, or standalone variant.
The fixedColumn: false behavior described above is the current M-002 semantic
extension of the existing WblTableColumn.fixedColumn field. Focused tests
verify the opt-out while omitted values preserve automatic edge fixation.
Classes and selectors
WblTableComponent—wbl-table(src/app/design-system/organisms/table/table.component.ts).
Inputs
WblTableComponent.columns = input<WblTableColumn[]>(DEFAULT_COLUMNS)WblTableComponent.rows = input<WblTableRow[]>(DEFAULT_ROWS)WblTableComponent.ariaLabel = input('Таблица')WblTableComponent.tableAriaLabel = input('Данные таблицы')WblTableComponent.bulkActions = input(true)WblTableComponent.loading = input(false)WblTableComponent.containedScroll = input(false)WblTableComponent.adaptive = input(false)WblTableComponent.cardPrimaryColumns = input(5)WblTableComponent.cardExpandLabel = input('Показать ещё')WblTableComponent.cardCollapseLabel = input('Свернуть')WblTableComponent.cardMenuLabel = input('Действия')WblTableComponent.filteredEmpty = input(false)WblTableComponent.filteredEmptyEntityName = input('Записи')WblTableComponent.actionBarButton2 = input(true)WblTableComponent.actionBarButton3 = input(true)WblTableComponent.actionBarButton1Label = input('Отменить')WblTableComponent.actionBarButton2Label = input('Скачать')WblTableComponent.actionBarButton3Label = input('Подтвердить')WblTableComponent.showFilterRow = input(true)WblTableComponent.filters = input<WblFilterRowFilter[]>([])WblTableComponent.addFilterItems = input<WblDropdownItem[]>([])WblTableComponent.showSearch = input(true)WblTableComponent.searchLabel = input('Поиск')WblTableComponent.searchPlaceholder = input('Поиск')WblTableComponent.searchValue = input('')WblTableComponent.showAddFilter = input(true)WblTableComponent.addFilterLabel = input('Фильтр')WblTableComponent.showReset = input(true)WblTableComponent.resetDisabled = input<boolean | null>(null)WblTableComponent.showSwitchColumn = input(true)WblTableComponent.showSecondSwitch = input(false)WblTableComponent.switches = input<WblFilterRowSwitch[]>([])WblTableComponent.showColumns = input(true)WblTableComponent.columnsLabel = input('Настроить колонки')WblTableComponent.showActions = input(true)WblTableComponent.showMoreAction = input(true)WblTableComponent.downloadLabel = input('Скачать')WblTableComponent.moreLabel = input('Ещё')WblTableComponent.moreActionItems = input<WblDropdownItem[]>([])WblTableComponent.downloadDisabled = input(false)WblTableComponent.moreDisabled = input(false)WblTableComponent.filterAriaLabel = input('Фильтры таблицы')WblTableComponent.showPagination = input(true)WblTableComponent.page = input(1)WblTableComponent.pageSize = input(25)WblTableComponent.totalItems = input(0)WblTableComponent.pageSizeOptions = input<number[]>([25, 50, 100])WblTableComponent.rowsPerPageLabel = input('Строк на странице')WblTableComponent.paginationAriaLabel = input('Пагинация таблицы')WblTableComponent.paginationDisabled = input(false)WblTableComponent.filteredEmptyTitleText = input<string | null>(null)WblTableComponent.filteredEmptyCaptionText = input<string | null>(null)
Each nullable input overrides only the matching filtered-empty line. null
preserves the current copy derived from filteredEmptyEntityName and
showSearch. Transport Requests passes a tab-specific title and the shared
caption «Попробуйте изменить фильтры». Focused tests and the FilteredEmpty
Storybook state are current evidence.
Public types and allowed values
type WblTableColumn = {
id: string;
label: string;
width?: string;
alignment?: WblTableHeaderAlignment;
variant?: WblTableHeaderVariant;
fixedColumn?: boolean;
infoIcon?: boolean;
sortable?: boolean;
sortDirection?: WblTableHeaderSortDirection;
checked?: boolean;
intermediate?: boolean;
ariaLabel?: string;
checkboxLabel?: string;
};
type WblTableRow = {
id: string;
cells: Record<string, WblTableCell>;
ariaLabel?: string;
};
type WblFilterRowFilter = {
id: string;
label: string;
value?: string;
filled?: boolean;
multiple?: boolean;
count?: string | number;
lifecycle: WblFilterLifecycle;
opened: boolean;
invalid?: boolean;
errorText?: string;
disabled?: boolean;
};
type WblFilterLifecycle = 'required' | 'pinned' | 'optional';
type WblFilterAction = 'clear' | 'reset' | 'remove';
type WblFilterRowOpenRequest = {
filter: WblFilterRowFilter;
opened: boolean;
};
type WblFilterRowFilterAction = {
filter: WblFilterRowFilter;
action: WblFilterAction;
};
type WblDropdownItem = {
id: string;
label: string;
iconName?: WblIconName;
imageSrc?: string;
imageAlt?: string;
badge?: string;
rightText?: string;
selected?: boolean;
disabled?: boolean;
separatorBefore?: boolean;
};
type WblFilterRowSwitch = {
id: string;
label: string;
selected?: boolean;
disabled?: boolean;
};
type WblTableSortChange = {
column: WblTableColumn;
direction: WblTableHeaderSortDirection;
local: boolean;
};
type WblTableHeaderCheckedChange = {
column: WblTableColumn;
checked: boolean;
};
type WblTableCellCheckedChange = {
row: WblTableRow;
column: WblTableColumn;
checked: boolean;
};
type WblTableRowOpenedChange = {
row: WblTableRow;
column: WblTableColumn;
opened: boolean;
};
type WblTableActionSelect = {
row: WblTableRow;
column: WblTableColumn;
action: WblTableCellAction;
};
type WblTableActionBarButtonClick = {
button: WblTableActionBarButton;
selectedRows: WblTableRow[];
};
type WblFilterRowSwitchChange = {
switchItem: WblFilterRowSwitch;
selected: boolean;
};
interface WblPaginationChange {
page: number;
pageSize: number;
}
type WblTableHeaderAlignment = 'left' | 'right';
type WblTableHeaderVariant = 'text' | 'checkbox';
type WblTableHeaderSortDirection = 'none' | 'ascending' | 'descending';
type WblTableCell = {
type?: WblTableCellType;
fixed?: WblTableCellFixed;
link?: WblTableCellLink;
alignment?: WblTableCellAlignment;
href?: string;
leftIcon?: boolean;
leftIconName?: WblIconName;
text?: string;
description?: boolean;
descriptionText?: string;
rightIcon?: boolean;
rightIconName?: WblIconName;
rightIconPlacement?: WblTableCellRightIconPlacement;
hint?: boolean;
hintLabel?: string;
badgeText?: string;
badgeAppearance?: WblBadgeAppearance;
checked?: boolean;
intermediate?: boolean;
disable?: boolean;
checkboxLabel?: string;
opened?: boolean;
expandLabel?: string;
collapseLabel?: string;
actions?: WblTableCellAction[];
moreActionLabel?: string;
};
type WblTableCellAction = {
id: string;
label: string;
variant?: WblTableCellActionButtonVariant;
iconName?: WblIconName;
showLabel?: boolean;
tooltipText?: string;
disabled?: boolean;
overflow?: boolean;
separatorBefore?: boolean;
};
type WblTableActionBarButton = 'button1' | 'button2' | 'button3';
type WblTableCellType =
| 'none'
| 'text'
| 'number'
| 'link'
| 'badge'
| 'checkbox'
| 'chevron'
| 'slot'
| 'actions';
type WblTableCellFixed = 'none' | 'left' | 'right';
type WblTableCellLink = 'accent' | 'secondary';
type WblTableCellAlignment = 'left' | 'right';
type WblBadgeAppearance =
| 'default'
| 'red'
| 'orange'
| 'green'
| 'success'
| 'warning'
| 'error'
| 'clear'
| 'disabled'
| 'info';
type WblTableCellActionButtonVariant = WblButtonVariant | `flat-${WblButtonFlatVariant}`;
type WblButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverted';
type WblButtonFlatVariant = 'primary' | 'secondary' | 'warning' | 'success' | 'danger';
Icon-name inputs use WblIconName from the shared icon contract;
choose a key present in the owned icon pack, not an arbitrary external icon name.
Contained rows viewport
Transport Requests passes [containedScroll]="true" to constrain the rows
region to the remaining vertical space. Filter Row stays outside that vertical
scrollport, the column header remains sticky at its top edge, and the screen's
infinite-scroll listener uses this viewport. Existing Table consumers keep the
unbounded vertical behavior through the false default. The current
implementation, focused tests, and ContainedScroll Storybook story verify the
current contract.
Models
- None.
Outputs
WblTableComponent.sortChange = output<WblTableSortChange>()WblTableComponent.headerCheckedChange = output<WblTableHeaderCheckedChange>()WblTableComponent.cellCheckedChange = output<WblTableCellCheckedChange>()WblTableComponent.rowOpenedChange = output<WblTableRowOpenedChange>()WblTableComponent.actionSelect = output<WblTableActionSelect>()WblTableComponent.actionBarButtonClick = output<WblTableActionBarButtonClick>()WblTableComponent.searchValueChange = output<string>()WblTableComponent.filterOpenRequest = output<WblFilterRowOpenRequest>()WblTableComponent.filterAction = output<WblFilterRowFilterAction>()WblTableComponent.addFilterSelect = output<WblDropdownItem>()WblTableComponent.resetClick = output<void>()WblTableComponent.switchChange = output<WblFilterRowSwitchChange>()WblTableComponent.columnsClick = output<void>()WblTableComponent.downloadClick = output<void>()WblTableComponent.moreActionSelect = output<WblDropdownItem>()WblTableComponent.pageChange = output<WblPaginationChange>()WblTableComponent.pageSizeChange = output<number>()
Slots and projection markers
WblTableComponent: <ng-content select="[wblFilterRowControl]">
Local evidence
- Code:
src/app/design-system/organisms/table/table.component.ts. - Focused test:
src/app/design-system/organisms/table/table.component.spec.ts. - Public export:
export * from './organisms/table/table.component';insrc/app/design-system/index.ts. - Storybook:
storybook/stories/table.stories.ts(Design System/Organisms/wbl-table, includingContainedScroll). - Adaptive story:
storybook/stories/adaptive-table.stories.ts(Адаптивность/Таблица), a container-width switcher for 390–1920px. 390, 780 and 1024 render inside a device mock-up taken from UI Kit node12101:104965: status bar, logo with menu icon, and a 60px system area at the bottom. Desktop widths show a 48px menu placeholder. Every width shows a adaptive Header Widget and a non-interactive «Фильтры» button above the Table.
Figma status
This runtime contract does not infer a Figma owner. Assembly evidence, when verified, is maintained separately through the inventory-linked sidecar.
Provenance
The archived source-document copy is historical evidence only. The active resolver is local code/test/export and the exact Storybook story above.
M-002 scrollbar refinement
The Table scroll frame uses the owned custom scrollbar treatment evidenced
by UI Kit node
9036:58781
for both contained vertical overflow and horizontal overflow. The native
scrollbar is 4px, uses tertiary #c4c4d4 for the pill-radius thumb, keeps the
track transparent, and replaces the former decorative rail. Styling does not
change the viewport's accessible region, focusability, sticky header, column
widths, or scroll ownership. The public API is unchanged.
M-002 contained scrollbar within the rail
In contained-scroll mode, the scroll frame is a flex container with
overflow: visible and no inline-end padding. The viewport uses
inline-size: 100%, overflow: auto, and native scrollbar-gutter: stable.
Any browser-reserved gutter stays inside that rail-bounded viewport; it never
widens the Table or creates outside layout area. The existing 4px thumb
treatment, native scroll ownership, public API, focused tests, and
ContainedScroll story remain current evidence.
M-002 per-tab fixed mapping and content transition
Transport Requests passes a separate column definition for each tab. The
«Предварительные» columns opt out of fixed edges. The other four tabs mark their
first two columns (ID / статус and Тип заявки) fixedColumn: true, producing
a consecutive leading fixed group, and mark their final actions column
fixedColumn: true for the right edge. Table derives the sides, cumulative
offsets and boundary separators from that configuration and does not contain
screen-specific column IDs or defaults.
The screen-owned wrapper, rather than Table, animates content after a selected
tab changes: opacity: 0.9 transitions to the normal state over
--duration-fast with --ease-standard. It does not animate Filter Row or the
sticky column header, change Table geometry, or block input; the screen resets
horizontal scroll to the start on each tab change. With
prefers-reduced-motion: reduce, the animation is disabled and content is
replaced immediately. The Table public API is unchanged.
M-002 sorting and contained scrollbar
Transport Requests marks ID, creation date and vehicle-submission date sortable
in its four non-preliminary working/archive tabs. ID keeps the existing
localeCompare path with numeric: true. Table recognizes
DD.MM.YYYY HH:mm cell values and compares their timestamps chronologically;
other values retain the existing numeric/string fallbacks. Creation date starts
descending. This behavior adds no public input.
In contained-scroll and paginated layouts, .wbl-table__scroll-frame uses flex,
overflow: visible, and no inline-end padding. The viewport keeps
inline-size: 100%, overflow: auto, and scrollbar-gutter: stable. The
native vertical scrollbar is reserved inside the rail-bounded viewport, so its
outer inline edges still match the Table, Filter Row, and any Action Bar.
Vertical and horizontal scrolling remain on the same accessible viewport. The
public API is unchanged.
M-002 fixed columns and stable state geometry
Configured leading and trailing columns remain visually stationary while the
middle data region scrolls horizontally. Table calculates each fixedOffset
from the widths of preceding fixed columns and anchors the group with native
inline-start/inline-end insets. It does not subscribe to scrollLeft or drive
transforms/signals. Fixed surfaces retain opaque backgrounds and one boundary
separator without covering scrolling cells. Browser verification at
scrollLeft 0 and 1200 keeps the left columns at x=92–228 and x=228–452 and the
right column at x=1177–1249. This behavior uses the existing fixedColumn
contract and adds no public API.
.wbl-table__inner, the header grid and row grids retain their actual
max-content width. A feature-level minimum-width override is not required;
Transport Requests removed its former 1316px override. Its screen-owned
Preliminary class sets Table inner, header and rows to
inline-size/min-inline-size: 100%; with a minmax(376px, 1fr) route and 68px
action column, the table fills the 1156px rail without an empty area or
unnecessary horizontal overflow. Extended tabs retain their real max-content
width.
Changing tabs, roles, screen state or row-action availability must preserve the
Table viewport size, fixed-column positions, column widths and action-column
footprint. In filtered-empty, the viewport uses hidden overflow, renders no
scrollbar or divider, and keeps its title/caption. Per-tab loading, empty, error
and filtered-empty selection remains a screen responsibility; Table does not
infer authorization or role state.