Skip to content

Commit 61ff716

Browse files
authored
Merge pull request #1430 from ASU/uds-1878
chore(unity-react-core): add Table story
2 parents bb28ac6 + 8d049d5 commit 61ff716

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React, { useEffect } from "react";
2+
import { Table } from "./Tables";
3+
import type { Meta, StoryObj } from '@storybook/react';
4+
5+
const meta: Meta<typeof Table> = {
6+
title: "Components/Table",
7+
component: Table,
8+
decorators: [
9+
Story => (
10+
<div className="container">
11+
<Story />
12+
</div>
13+
),
14+
],
15+
};
16+
17+
export const BasicTable: StoryObj<typeof Table> = {
18+
args: {
19+
columns: 5,
20+
fixed: false
21+
}
22+
};
23+
24+
export const FixedTable: StoryObj<typeof Table> = {
25+
args: {
26+
columns: 12,
27+
fixed: true
28+
}
29+
};
30+
31+
export default meta;
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { render, cleanup, RenderResult } from "@testing-library/react";
2+
import React from "react";
3+
import { expect, describe, it, afterEach, beforeEach } from 'vitest';
4+
import { Table } from "./Tables";
5+
6+
describe("Table Component Tests", () => {
7+
let component: RenderResult;
8+
const defaultProps = {
9+
columns: 5
10+
};
11+
12+
const renderComponent = (props = defaultProps) => {
13+
return render(
14+
<div className="uds-table" tabIndex={0}>
15+
<Table {...props} />
16+
</div>
17+
);
18+
};
19+
20+
beforeEach(() => {
21+
component = renderComponent();
22+
});
23+
24+
afterEach(cleanup);
25+
26+
it("should render the table component", () => {
27+
expect(component).toBeDefined();
28+
});
29+
30+
it("should have correct number of columns", () => {
31+
const headerCells = component.container.querySelectorAll('thead th');
32+
expect(headerCells.length).toBe(defaultProps.columns + 1);
33+
});
34+
35+
it("should display correct year range", () => {
36+
const currentYear = 2024;
37+
const headerCells = component.container.querySelectorAll('thead th');
38+
const years = Array.from(headerCells)
39+
.slice(1)
40+
.map(cell => cell.textContent);
41+
42+
const expectedYears = new Array(defaultProps.columns)
43+
.fill(null)
44+
.map((_, i) => `Fall ${currentYear - (defaultProps.columns - 1) + i}`);
45+
46+
expect(years).toEqual(expectedYears);
47+
});
48+
49+
it("should render all campus rows", () => {
50+
const campuses = [
51+
"Tempe",
52+
"Downtown",
53+
"Polytechnic",
54+
"West",
55+
"Thunderbird",
56+
"Skysong Campus"
57+
];
58+
59+
campuses.forEach(campus => {
60+
expect(component.getByText(campus)).toBeInTheDocument();
61+
});
62+
});
63+
64+
it("should have correct table structure", () => {
65+
expect(component.container.querySelector('table')).toBeInTheDocument();
66+
expect(component.container.querySelector('thead')).toBeInTheDocument();
67+
expect(component.container.querySelector('tbody')).toBeInTheDocument();
68+
});
69+
70+
it("should render example link in first row", () => {
71+
const link = component.container.querySelector('a');
72+
expect(link).toBeInTheDocument();
73+
expect(link?.textContent).toBe('example link');
74+
});
75+
76+
describe("with different column counts", () => {
77+
it("should render with minimum columns", () => {
78+
const minColumns = 4;
79+
const minComponent = renderComponent({ columns: minColumns });
80+
const headerCells = minComponent.container.querySelectorAll('thead th');
81+
expect(headerCells.length).toBe(minColumns + 1);
82+
});
83+
84+
it("should render with maximum columns", () => {
85+
const maxColumns = 14;
86+
const maxComponent = renderComponent({ columns: maxColumns });
87+
const headerCells = maxComponent.container.querySelectorAll('thead th');
88+
expect(headerCells.length).toBe(maxColumns + 1);
89+
});
90+
});
91+
92+
describe("data calculation tests", () => {
93+
it("should generate numbers for each cell", () => {
94+
const firstDataRow = component.container.querySelectorAll('tbody tr')[0];
95+
const dataCells = firstDataRow.querySelectorAll('td');
96+
97+
dataCells.forEach(cell => {
98+
expect(cell.textContent).toMatch(/^\d{1,3}(,\d{3})*$/); // Format like 1,234
99+
});
100+
});
101+
102+
it("should maintain consistent data structure across rows", () => {
103+
const rows = component.container.querySelectorAll('tbody tr');
104+
const expectedCellCount = defaultProps.columns + 1; // columns + header cell
105+
106+
rows.forEach(row => {
107+
const cells = row.querySelectorAll('th, td');
108+
expect(cells.length).toBe(expectedCellCount);
109+
});
110+
});
111+
});
112+
113+
describe("accessibility tests", () => {
114+
it("should have proper scope attributes on headers", () => {
115+
const columnHeaders = component.container.querySelectorAll('thead th');
116+
columnHeaders.forEach(header => {
117+
expect(header).toHaveAttribute('scope', 'col');
118+
});
119+
120+
const rowHeaders = component.container.querySelectorAll('tbody th');
121+
rowHeaders.forEach(header => {
122+
expect(header).toHaveAttribute('scope', 'row');
123+
});
124+
});
125+
126+
it("should have tabIndex on container", () => {
127+
const container = component.container.querySelector('.uds-table');
128+
expect(container).toHaveAttribute('tabIndex', '0');
129+
});
130+
});
131+
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React, { useEffect } from "react";
2+
import { initializeFixedTable } from "./fixedTable";
3+
4+
const makingUpFakeNumbers = (a, b, c) =>
5+
Math.round(a * (b + c)).toLocaleString("en-US");
6+
7+
interface TableProps {
8+
columns: number;
9+
fixed?: boolean;
10+
}
11+
12+
const BaseTable = ({ columns }) => {
13+
let year = 2024;
14+
const arr = new Array(columns)
15+
.fill(null)
16+
.map((v, i) => year - i)
17+
.reverse();
18+
return (
19+
<table>
20+
<thead>
21+
<tr>
22+
<th scope="col">Enrollment</th>
23+
{arr.map((v, i) => (
24+
<th scope="col" key={i}>
25+
Fall {v}
26+
</th>
27+
))}
28+
</tr>
29+
</thead>
30+
<tbody>
31+
<tr>
32+
<th scope="row">
33+
<p>
34+
use of <code>&lt;a&gt;</code> in cells{" "}
35+
<a href="#">example link</a>
36+
</p>
37+
Metropolitan campus population
38+
</th>
39+
{arr.map((v, i) => (
40+
<td key={i}>{makingUpFakeNumbers(v, 35, i)}</td>
41+
))}
42+
</tr>
43+
<tr>
44+
<th scope="row" className="indent">
45+
Tempe
46+
</th>
47+
{arr.map((v, i) => (
48+
<td key={i}>{makingUpFakeNumbers(v, 25, i)}</td>
49+
))}
50+
</tr>
51+
<tr>
52+
<th scope="row" className="indent">
53+
Downtown
54+
</th>
55+
{arr.map((v, i) => (
56+
<td key={i}>{makingUpFakeNumbers(v, 7, i)}</td>
57+
))}
58+
</tr>
59+
<tr>
60+
<th scope="row" className="indent">
61+
Polytechnic
62+
</th>
63+
{arr.map((v, i) => (
64+
<td key={i}>{makingUpFakeNumbers(v, 1.6, i / 2)}</td>
65+
))}
66+
</tr>
67+
<tr>
68+
<th scope="row" className="indent">
69+
West
70+
</th>
71+
{arr.map((v, i) => (
72+
<td key={i}>{makingUpFakeNumbers(v, 0.8, i / 4)}</td>
73+
))}
74+
</tr>
75+
<tr>
76+
<th scope="row" className="indent">
77+
Thunderbird
78+
</th>
79+
{arr.map((v, i) => (
80+
<td key={i}>{makingUpFakeNumbers(v, 0.1, i / 10)}</td>
81+
))}
82+
</tr>
83+
<tr>
84+
<th scope="row" className="normal">
85+
Skysong Campus
86+
</th>
87+
{arr.map((v, i) => (
88+
<td key={i}>{makingUpFakeNumbers(v, 5, i / 5)}</td>
89+
))}
90+
</tr>
91+
<tr>
92+
<th scope="row">Total</th>
93+
{arr.map((v, i) => (
94+
<td key={i}>{makingUpFakeNumbers(v, 50, i)}</td>
95+
))}
96+
</tr>
97+
</tbody>
98+
</table>
99+
);
100+
};
101+
102+
export const Table: React.FC<TableProps> = ({ columns, fixed = false }) => {
103+
useEffect(() => {
104+
if (fixed) {
105+
initializeFixedTable();
106+
}
107+
}, []);
108+
109+
if (!fixed) {
110+
return (
111+
<div className="uds-table" tabIndex={0}>
112+
<BaseTable columns={columns} />
113+
</div>
114+
);
115+
}
116+
return (
117+
<div className="uds-table-fixed-wrapper">
118+
<div className="scroll-control previous">
119+
<button type="button" className="btn btn-circle btn-circle-alt-gray">
120+
<i className="fas fa-chevron-left"></i>
121+
<span className="visually-hidden">Previous</span>
122+
</button>
123+
</div>
124+
125+
<div className="scroll-control next">
126+
<button type="button" className="btn btn-circle btn-circle-alt-gray">
127+
<i className="fas fa-chevron-right"></i>
128+
<span className="visually-hidden">Next</span>
129+
</button>
130+
</div>
131+
132+
<div className="uds-table uds-table-fixed" tabIndex={0}>
133+
<BaseTable columns={columns} />
134+
</div>
135+
</div>
136+
);
137+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
function initializeFixedTable() {
2+
function setPreButtonPosition() {
3+
const wrapperSelector = '.uds-table-fixed-wrapper';
4+
const tableSelector = '.uds-table.uds-table-fixed table';
5+
const prevScrollSelector = '.scroll-control.previous';
6+
7+
const wrappers = document.querySelectorAll(wrapperSelector);
8+
wrappers.forEach((wrapper, index) => {
9+
/** @type {HTMLTableElement} */
10+
const table = wrapper.querySelector(tableSelector);
11+
table.setAttribute('id', 'uds-table-' + index);
12+
/** @type {HTMLTableCellElement} */
13+
const firstCol = table.querySelector('tbody tr > *');
14+
/** @type {HTMLElement} */
15+
const prevButton = wrapper.querySelector(prevScrollSelector);
16+
prevButton.style.left = firstCol.offsetWidth + 'px';
17+
});
18+
}
19+
20+
function setButtonLiListeners() {
21+
const containerSelector = '.uds-table-fixed';
22+
const wrapperSelector = '.uds-table-fixed-wrapper';
23+
const prevScrollSelector = '.scroll-control.previous';
24+
const nextScrollSelector = '.scroll-control.next';
25+
26+
const wrappers = document.querySelectorAll(wrapperSelector);
27+
wrappers.forEach((wrapper, index) => {
28+
const container = wrapper.querySelector(containerSelector);
29+
const prevButton = wrapper.querySelector(prevScrollSelector);
30+
const nextButton = wrapper.querySelector(nextScrollSelector);
31+
32+
['click', 'focus'].forEach((eventName) => {
33+
prevButton.addEventListener(eventName, function () {
34+
/* Scroll can't go beyond it's bounds, it won't go lower than 0 */
35+
container.scrollLeft -= 100;
36+
});
37+
38+
nextButton.addEventListener(eventName, function () {
39+
container.scrollLeft += 100;
40+
});
41+
});
42+
});
43+
}
44+
45+
function debounce(func, timeout) {
46+
let timerId;
47+
return (...args) => {
48+
clearTimeout(timerId);
49+
timerId = setTimeout(() => {
50+
func.apply(this, args);
51+
}, timeout);
52+
};
53+
}
54+
setPreButtonPosition();
55+
setButtonLiListeners();
56+
window.addEventListener('resize', function () {
57+
debounce(setPreButtonPosition, 100)();
58+
});
59+
};
60+
61+
export { initializeFixedTable };

0 commit comments

Comments
 (0)