Skip to content

Commit 2a6e7cb

Browse files
committed
Adds DataTable empty state
Renders a Blankslate-backed empty state instead of the table grid when there are no rows. A default state with an i18n title appears automatically; the empty_state slot customizes title, description, and icon. The API mirrors OpenProject's BorderBoxListComponent empty state, including an interactive option that adds a polite live region for dynamically updated tables. Title, subtitle, and actions remain visible above the empty state, unlike Primer React where consumers replace the whole container.
1 parent a347183 commit 2a6e7cb

15 files changed

Lines changed: 321 additions & 29 deletions

File tree

app/components/primer/open_project/data_table.html.erb

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,48 @@
1111
<div class="TableDivider" role="presentation"></div>
1212
<% end %>
1313
<%= subtitle %>
14-
<%= render(Primer::BaseComponent.new(**@wrapper_arguments)) do %>
15-
<data-table>
16-
<%= render(Primer::OpenProject::Table.new(**@system_arguments)) do |table| %>
17-
<% table.with_head(classes: "TableHead") do |thead| %>
18-
<% thead.with_row(classes: "TableRow") do |tr| %>
19-
<% headers.each do |header| %>
20-
<% if header.sortable? %>
21-
<% tr.with_header(
22-
component_klass: Primer::OpenProject::DataTable::SortHeader,
23-
align: header.column.align,
24-
direction: header.sort_direction,
25-
data: { sort_strategy: header.sort_strategy }
26-
) { header.column.header.to_s } %>
27-
<% else %>
28-
<% tr.with_header(classes: "TableHeader", align: header.column.align) { header.column.header.to_s } %>
14+
<% if rows.empty? %>
15+
<div class="TableEmptyState">
16+
<%= empty_state %>
17+
</div>
18+
<% else %>
19+
<%= render(Primer::BaseComponent.new(**@wrapper_arguments)) do %>
20+
<data-table>
21+
<%= render(Primer::OpenProject::Table.new(**@system_arguments)) do |table| %>
22+
<% table.with_head(classes: "TableHead") do |thead| %>
23+
<% thead.with_row(classes: "TableRow") do |tr| %>
24+
<% headers.each do |header| %>
25+
<% if header.sortable? %>
26+
<% tr.with_header(
27+
component_klass: Primer::OpenProject::DataTable::SortHeader,
28+
align: header.column.align,
29+
direction: header.sort_direction,
30+
data: { sort_strategy: header.sort_strategy }
31+
) { header.column.header.to_s } %>
32+
<% else %>
33+
<% tr.with_header(classes: "TableHeader", align: header.column.align) { header.column.header.to_s } %>
34+
<% end %>
2935
<% end %>
3036
<% end %>
3137
<% end %>
32-
<% end %>
33-
<% table.with_body(classes: "TableBody") do |tbody| %>
34-
<% rows.each do |row| %>
35-
<% tbody.with_row(classes: "TableRow") do |tr| %>
36-
<% cells_for(row).each do |cell| %>
37-
<% cell_arguments = { classes: "TableCell", align: cell.column.align } %>
38-
<% cell_arguments[:data] = cell.sort_data if cell.sortable? %>
39-
<% if cell.row_header? %>
40-
<% tr.with_row_header(**cell_arguments) { cell_content(cell, row) } %>
41-
<% else %>
42-
<% tr.with_cell(**cell_arguments) { cell_content(cell, row) } %>
38+
<% table.with_body(classes: "TableBody") do |tbody| %>
39+
<% rows.each do |row| %>
40+
<% tbody.with_row(classes: "TableRow") do |tr| %>
41+
<% cells_for(row).each do |cell| %>
42+
<% cell_arguments = { classes: "TableCell", align: cell.column.align } %>
43+
<% cell_arguments[:data] = cell.sort_data if cell.sortable? %>
44+
<% if cell.row_header? %>
45+
<% tr.with_row_header(**cell_arguments) { cell_content(cell, row) } %>
46+
<% else %>
47+
<% tr.with_cell(**cell_arguments) { cell_content(cell, row) } %>
48+
<% end %>
4349
<% end %>
4450
<% end %>
4551
<% end %>
4652
<% end %>
4753
<% end %>
48-
<% end %>
49-
</data-table>
54+
</data-table>
55+
<% end %>
5056
<% end %>
5157
<%= pagination %>
5258
<% end %>

app/components/primer/open_project/data_table.pcss

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,14 @@
6868
grid-area: divider;
6969
}
7070

71+
/* TableEmptyState */
72+
.TableEmptyState {
73+
grid-area: table;
74+
}
75+
7176
/* Spacing before the table */
72-
.TableContainer > .TableOverflowWrapper:not(:first-child) {
77+
.TableContainer > .TableOverflowWrapper:not(:first-child),
78+
.TableContainer > .TableEmptyState:not(:first-child) {
7379
margin-block-start: var(--base-size-8);
7480
}
7581

app/components/primer/open_project/data_table.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@ def sort_data
8383
menu: Primer::Alpha::ActionMenu
8484
}
8585

86+
# Empty-state content rendered instead of the table when there are no
87+
# rows. Without this slot, an empty table renders a default
88+
# <%= link_to_component(Primer::OpenProject::DataTable::EmptyState) %>.
89+
# Ignored while rows are present.
90+
renders_one :empty_state, ->(title:, description: nil, icon: nil, interactive: false, **system_arguments) {
91+
EmptyState.new(
92+
title: title,
93+
description: description,
94+
icon: icon,
95+
interactive: interactive,
96+
**system_arguments
97+
)
98+
}
99+
86100
renders_one :pagination, Primer::OpenProject::DataTable::PaginationFooter
87101

88102
# @param data [Array, ActiveRecord::Relation]
@@ -145,6 +159,8 @@ def before_render
145159
@headers = build_headers
146160
@rows = sorted_rows
147161

162+
with_empty_state(title: I18n.t("data_table.empty_state_title")) if @rows.empty? && !empty_state?
163+
148164
@system_arguments[:style] = join_style_arguments(
149165
@system_arguments[:style],
150166
"--grid-template-columns: #{grid_template_from_columns(columns).join(' ')}"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module Primer
4+
module OpenProject
5+
class DataTable
6+
# Empty-state content rendered as a Primer Blankslate when the table
7+
# has no rows. The API mirrors OpenProject's `BorderBoxListComponent`
8+
# empty state so both components share one idiom.
9+
#
10+
# This component is part of `Primer::OpenProject::DataTable` and should
11+
# not be used as a standalone component.
12+
class EmptyState < Primer::Component
13+
status :open_project
14+
15+
# @param title [String] Empty-state heading
16+
# @param description [String, nil] Optional supporting text
17+
# @param icon [Symbol, nil] Optional Primer icon
18+
# @param interactive [Boolean] Whether empty-state updates should be
19+
# announced politely to assistive technology
20+
# @param system_arguments [Hash]
21+
# System arguments passed to the underlying `Primer::Beta::Blankslate`
22+
def initialize(
23+
title:,
24+
description: nil,
25+
icon: nil,
26+
interactive: false,
27+
**system_arguments
28+
)
29+
@title = title
30+
@description = description
31+
@icon = icon
32+
33+
@system_arguments = system_arguments
34+
@system_arguments[:border] = true unless @system_arguments.key?(:border)
35+
36+
return unless interactive
37+
38+
@system_arguments[:role] ||= "status"
39+
@system_arguments[:aria] = merge_aria(
40+
{ aria: { live: "polite" } },
41+
@system_arguments
42+
)
43+
end
44+
45+
def call
46+
render(blankslate)
47+
end
48+
49+
private
50+
51+
def blankslate
52+
blankslate = Primer::Beta::Blankslate.new(**@system_arguments)
53+
blankslate.with_heading(tag: :h4).with_content(@title)
54+
blankslate.with_description_content(@description) if @description
55+
blankslate.with_visual_icon(icon: @icon) if @icon
56+
57+
blankslate
58+
end
59+
end
60+
end
61+
end
62+
end

config/locales/en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ en:
99
button_filter: "Filter"
1010
button_save: "Save"
1111

12+
data_table:
13+
empty_state_title: "No data available"
14+
1215
drag_handle:
1316
button_drag: "Drag to reorder"
1417

previews/primer/open_project/data_table_preview.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ def with_actions
5757
render_with_template(locals: { projects: sample_rows.first(3) })
5858
end
5959

60+
# @label Empty State
61+
# @snapshot
62+
# @param custom toggle
63+
def empty_state(custom: false)
64+
render_with_template(locals: { custom: custom })
65+
end
66+
6067
# @label With Cell Placeholder
6168
# @snapshot
6269
def with_cell_placeholder
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<%=
2+
render(
3+
Primer::OpenProject::DataTable.new([])
4+
) do |data_table|
5+
data_table.with_title(tag: :h2) { "Projects" }
6+
data_table.with_subtitle { "An empty table renders a blankslate instead of the grid." }
7+
8+
if custom
9+
data_table.with_empty_state(
10+
title: "No projects yet",
11+
description: "Create your first project to see it listed here.",
12+
icon: :book
13+
)
14+
end
15+
16+
data_table.with_column(field: :name, header: "Name", row_header: true)
17+
data_table.with_column(field: :status_code, header: "Status")
18+
end
19+
%>

static/arguments.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6200,6 +6200,46 @@
62006200
}
62016201
]
62026202
},
6203+
{
6204+
"component": "OpenProject::DataTable::EmptyState",
6205+
"status": "open_project",
6206+
"a11y_reviewed": false,
6207+
"short_name": "OpenProjectDataTableEmptyState",
6208+
"source": "https://github.com/primer/view_components/tree/main/app/components/primer/open_project/data_table/empty_state.rb",
6209+
"lookbook": "https://primer.style/view-components/lookbook/inspect/primer/open_project/data_table/empty_state/default/",
6210+
"parameters": [
6211+
{
6212+
"name": "title",
6213+
"type": "String",
6214+
"default": "N/A",
6215+
"description": "Empty-state heading"
6216+
},
6217+
{
6218+
"name": "description",
6219+
"type": "String, nil",
6220+
"default": "`nil`",
6221+
"description": "Optional supporting text"
6222+
},
6223+
{
6224+
"name": "icon",
6225+
"type": "Symbol, nil",
6226+
"default": "`nil`",
6227+
"description": "Optional Primer icon"
6228+
},
6229+
{
6230+
"name": "interactive",
6231+
"type": "Boolean",
6232+
"default": "`false`",
6233+
"description": "Whether empty-state updates should be announced politely to assistive technology"
6234+
},
6235+
{
6236+
"name": "system_arguments",
6237+
"type": "Hash",
6238+
"default": "N/A",
6239+
"description": "System arguments passed to the underlying `Primer::Beta::Blankslate`"
6240+
}
6241+
]
6242+
},
62036243
{
62046244
"component": "OpenProject::DataTable::PaginationFooter",
62056245
"status": "open_project",

static/audited_at.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
"Primer::OpenProject::DataTable": "",
153153
"Primer::OpenProject::DataTable::CellPlaceholder": "",
154154
"Primer::OpenProject::DataTable::Column": "",
155+
"Primer::OpenProject::DataTable::EmptyState": "",
155156
"Primer::OpenProject::DataTable::PaginationFooter": "",
156157
"Primer::OpenProject::DataTable::SortHeader": "",
157158
"Primer::OpenProject::DragHandle": "",

static/constants.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1776,6 +1776,7 @@
17761776
"Cell": "Primer::OpenProject::DataTable::Cell",
17771777
"CellPlaceholder": "Primer::OpenProject::DataTable::CellPlaceholder",
17781778
"Column": "Primer::OpenProject::DataTable::Column",
1779+
"EmptyState": "Primer::OpenProject::DataTable::EmptyState",
17791780
"GeneratedSlotMethods": "Primer::OpenProject::DataTable::GeneratedSlotMethods",
17801781
"Header": "Primer::OpenProject::DataTable::Header",
17811782
"PaginationFooter": "Primer::OpenProject::DataTable::PaginationFooter",
@@ -1800,6 +1801,9 @@
18001801
],
18011802
"GeneratedSlotMethods": "Primer::OpenProject::DataTable::Column::GeneratedSlotMethods"
18021803
},
1804+
"Primer::OpenProject::DataTable::EmptyState": {
1805+
"GeneratedSlotMethods": "Primer::OpenProject::DataTable::EmptyState::GeneratedSlotMethods"
1806+
},
18031807
"Primer::OpenProject::DataTable::PaginationFooter": {
18041808
"GeneratedSlotMethods": "Primer::OpenProject::DataTable::PaginationFooter::GeneratedSlotMethods"
18051809
},

0 commit comments

Comments
 (0)