Skip to content

Commit 3b04a58

Browse files
bsatarnejadmyabc
authored andcommitted
[DREAM-590] Introduce Table and DataTable components
Adds OpenProject table primitives for building Primer-style tables with caption, colgroup, head, body, foot, row, header, and cell slots. The primitive components handle semantic table roles, row headers, alignment, cell padding, column widths, and colgroup rendering. Adds a DataTable wrapper modeled after Primer React's DataTable API. It supports title and subtitle slots, field-backed and custom-rendered columns, row action columns, row header cells, initial sort state, and sortable headers with server-rendered icon state. Implements hybrid DataTable sorting: Ruby orders the initial render, while the Catalyst element re-sorts client-side using raw per-cell sort values. Sortable columns use React-aligned strategies (`basic`, `datetime`, `alphanumeric`), ASC/DESC transitions, stable ordering, and blank values sorted last. Adds Lookbook previews for Table and DataTable, generated static metadata, and component/system coverage for rendering, column widths, sorting, row actions, raw sort metadata, and preview interaction. https://community.openproject.org/wp/DREAM-590
1 parent f53f7e4 commit 3b04a58

60 files changed

Lines changed: 4236 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<%= render(Primer::Box.new(**@container_arguments)) do %>
2+
<%= title %>
3+
<%= subtitle %>
4+
<%= render(Primer::BaseComponent.new(**@wrapper_arguments)) do %>
5+
<data-table>
6+
<%= render(Primer::OpenProject::Table.new(**@system_arguments)) do |table| %>
7+
<% table.with_head(classes: "TableHead") do |thead| %>
8+
<% thead.with_row(classes: "TableRow") do |tr| %>
9+
<% headers.each do |header| %>
10+
<% if header.sortable? %>
11+
<% tr.with_header(
12+
component_klass: Primer::OpenProject::DataTable::SortHeader,
13+
align: header.column.align,
14+
direction: header.sort_direction,
15+
data: { sort_strategy: header.sort_strategy }
16+
) { header.column.header.to_s } %>
17+
<% else %>
18+
<% tr.with_header(classes: "TableHeader", align: header.column.align) { header.column.header.to_s } %>
19+
<% end %>
20+
<% end %>
21+
<% end %>
22+
<% end %>
23+
<% table.with_body(classes: "TableBody") do |tbody| %>
24+
<% rows.each do |row| %>
25+
<% tbody.with_row(classes: "TableRow") do |tr| %>
26+
<% cells_for(row).each do |cell| %>
27+
<% cell_arguments = { classes: "TableCell", align: cell.column.align } %>
28+
<% cell_arguments[:data] = cell.sort_data if cell.sortable? %>
29+
<% if cell.row_header? %>
30+
<% tr.with_row_header(**cell_arguments) { cell.column.render_cell(row).to_s } %>
31+
<% else %>
32+
<% tr.with_cell(**cell_arguments) { cell.column.render_cell(row).to_s } %>
33+
<% end %>
34+
<% end %>
35+
<% end %>
36+
<% end %>
37+
<% end %>
38+
<% end %>
39+
</data-table>
40+
<% end %>
41+
<% end %>
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/* Button reset */
2+
.TableSortButton {
3+
margin: 0;
4+
display: inline-flex;
5+
padding: 0;
6+
border: 0;
7+
appearance: none;
8+
background: none;
9+
cursor: pointer;
10+
text-align: start;
11+
font: inherit;
12+
color: inherit;
13+
align-items: center;
14+
column-gap: var(--base-size-8);
15+
}
16+
17+
.TableSortButton::-moz-focus-inner {
18+
border: 0;
19+
}
20+
21+
/* Container ---------------------------------------------------------------- */
22+
.TableContainer {
23+
display: grid;
24+
grid-template-areas:
25+
'title'
26+
'subtitle'
27+
'table';
28+
}
29+
30+
/* TableTitle */
31+
.TableTitle {
32+
margin: 0;
33+
font-size: var(--text-body-size-medium);
34+
font-weight: var(--base-text-weight-semibold);
35+
line-height: var(--text-body-lineHeight-medium);
36+
color: var(--fgColor-default);
37+
grid-area: title;
38+
align-self: center;
39+
}
40+
41+
/* TableSubtitle */
42+
.TableSubtitle {
43+
margin: 0;
44+
font-size: var(--text-body-size-small);
45+
font-weight: var(--base-text-weight-normal);
46+
line-height: var(--text-title-lineHeight-small);
47+
color: var(--fgColor-default);
48+
grid-area: subtitle;
49+
}
50+
51+
/* Spacing before the table */
52+
.TableTitle + .TableOverflowWrapper,
53+
.TableSubtitle + .TableOverflowWrapper {
54+
margin-block-start: var(--base-size-8);
55+
}
56+
57+
.TableOverflowWrapper {
58+
grid-area: table;
59+
}
60+
61+
/* Table -------------------------------------------------------------------- */
62+
.Table {
63+
/* Default table styles */
64+
--table-border-radius: var(--borderRadius-medium);
65+
--table-cell-padding: var(--cell-padding-block, var(--base-size-8)) var(--cell-padding-inline, var(--base-size-12));
66+
67+
display: grid;
68+
width: 100%;
69+
font-size: var(--text-body-size-small);
70+
line-height: var(--text-body-lineHeight-small);
71+
border-spacing: 0;
72+
/* stylelint-disable-next-line primer/borders */
73+
border-collapse: separate;
74+
background-color: var(--bgColor-default);
75+
grid-area: table;
76+
grid-template-columns: var(--grid-template-columns);
77+
}
78+
79+
/* Density modes: condensed, normal, spacious */
80+
.Table:where([data-cell-padding='condensed']) {
81+
--cell-padding-block: var(--base-size-4);
82+
--cell-padding-inline: var(--base-size-8);
83+
}
84+
85+
.Table:where([data-cell-padding='normal']) {
86+
--cell-padding-block: var(--base-size-8);
87+
--cell-padding-inline: var(--base-size-12);
88+
}
89+
90+
.Table:where([data-cell-padding='spacious']) {
91+
--cell-padding-block: var(--base-size-12);
92+
--cell-padding-inline: var(--base-size-16);
93+
}
94+
95+
/* Borders */
96+
.TableCell:first-child,
97+
.TableHeader:first-child {
98+
border-inline-start: var(--borderWidth-thin) solid var(--borderColor-default);
99+
}
100+
101+
.TableCell:last-child,
102+
.TableHeader:last-child {
103+
border-inline-end: var(--borderWidth-thin) solid var(--borderColor-default);
104+
}
105+
106+
.TableHeader,
107+
.TableCell {
108+
display: flex;
109+
/* stylelint-disable-next-line primer/spacing */
110+
padding: var(--table-cell-padding);
111+
text-align: start;
112+
align-items: center;
113+
border-block-end: var(--borderWidth-thin) solid var(--borderColor-default);
114+
}
115+
116+
.TableHeader:where([data-cell-align='end']),
117+
.TableCell:where([data-cell-align='end']) {
118+
display: flex;
119+
text-align: end;
120+
justify-content: flex-end;
121+
}
122+
123+
.TableHeader[data-cell-align='end'] .TableSortButton {
124+
display: flex;
125+
flex-direction: row-reverse;
126+
}
127+
128+
.TableHead .TableRow:first-of-type .TableHeader {
129+
border-block-start: var(--borderWidth-thin) solid var(--borderColor-default);
130+
}
131+
132+
/* Border radius */
133+
/* stylelint-disable-next-line selector-max-specificity */
134+
.TableHead .TableRow:first-of-type .TableHeader:first-child {
135+
/* stylelint-disable-next-line primer/borders */
136+
border-top-left-radius: var(--table-border-radius);
137+
}
138+
139+
/* stylelint-disable-next-line selector-max-specificity */
140+
.TableHead .TableRow:first-of-type .TableHeader:last-child {
141+
/* stylelint-disable-next-line primer/borders */
142+
border-top-right-radius: var(--table-border-radius);
143+
}
144+
145+
/* stylelint-disable-next-line selector-max-compound-selectors,selector-max-specificity */
146+
.TableOverflowWrapper:last-child .Table .TableBody .TableRow:last-of-type .TableCell:first-child {
147+
/* stylelint-disable-next-line primer/borders */
148+
border-bottom-left-radius: var(--table-border-radius);
149+
}
150+
151+
/* stylelint-disable-next-line selector-max-compound-selectors,selector-max-specificity */
152+
.TableOverflowWrapper:last-child .Table .TableBody .TableRow:last-of-type .TableCell:last-child {
153+
/* stylelint-disable-next-line primer/borders */
154+
border-bottom-right-radius: var(--table-border-radius);
155+
}
156+
157+
/**
158+
* Offset padding to make sure type aligns regardless of cell padding
159+
* selection
160+
*/
161+
.TableRow > *:first-child {
162+
padding-inline-start: var(--base-size-16);
163+
}
164+
165+
.TableRow > *:last-child {
166+
padding-inline-end: var(--base-size-16);
167+
}
168+
169+
/* TableHeader */
170+
.TableHeader {
171+
font-weight: var(--base-text-weight-semibold);
172+
color: var(--fgColor-muted);
173+
background-color: var(--bgColor-muted);
174+
border-block-start: var(--borderWidth-thin) solid var(--borderColor-default);
175+
}
176+
177+
.TableHeader:where([aria-sort='descending']),
178+
.TableHeader:where([aria-sort='ascending']) {
179+
color: var(--fgColor-default);
180+
}
181+
182+
/* Control visibility of sort icons */
183+
.TableSortIcon {
184+
visibility: hidden;
185+
}
186+
187+
/* The ASC icon is visible if the header is sortable and is hovered or focused */
188+
.TableHeader:hover .TableSortIcon--ascending,
189+
.TableHeader .TableSortButton:focus .TableSortIcon--ascending {
190+
visibility: visible;
191+
}
192+
193+
/* Each sort icon is visible if the TableHeader is currently in the corresponding sort state */
194+
.TableHeader[aria-sort='ascending'] .TableSortIcon--ascending,
195+
.TableHeader[aria-sort='descending'] .TableSortIcon--descending {
196+
visibility: visible;
197+
}
198+
199+
/* TableRow */
200+
.TableRow:hover .TableCell {
201+
background-color: var(--control-transparent-bgColor-hover);
202+
}
203+
204+
/* TableCell */
205+
.TableCell:where([scope='row']) {
206+
display: flex;
207+
font-weight: var(--base-text-weight-semibold);
208+
color: var(--fgColor-default);
209+
align-items: center;
210+
}
211+
212+
/* Grid layout */
213+
.TableHead,
214+
.TableBody,
215+
.TableRow {
216+
display: contents;
217+
}
218+
219+
@supports (grid-template-columns: subgrid) {
220+
.TableHead,
221+
.TableBody,
222+
.TableRow {
223+
display: grid;
224+
grid-template-columns: subgrid;
225+
grid-column: -1 / 1;
226+
}
227+
}

0 commit comments

Comments
 (0)