Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/table #2593

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8101e4d
wip: made basic table component
adamhaeger Oct 9, 2024
5b43cbc
wip
adamhaeger Oct 11, 2024
4bc3a0d
Merge branch 'main' into feature/table
adamhaeger Oct 11, 2024
be190fc
wip
adamhaeger Oct 11, 2024
8d32b21
first working example of new table setup
adamhaeger Oct 11, 2024
0b5de14
clean
adamhaeger Oct 11, 2024
a7ed412
basic add to form working
adamhaeger Oct 14, 2024
7258547
rendering table in summary
adamhaeger Oct 15, 2024
60f5207
switched to simplebinding for table
adamhaeger Oct 15, 2024
a773367
add to list working boys
adamhaeger Oct 15, 2024
1916114
delete woring
adamhaeger Oct 15, 2024
b1db8ad
edit and delete working
adamhaeger Oct 16, 2024
5fbb6cb
wip
adamhaeger Oct 16, 2024
49ea97a
support multiple accessors
adamhaeger Oct 17, 2024
9cb7e12
wip
adamhaeger Oct 17, 2024
6fadfc3
wip
adamhaeger Oct 17, 2024
7d09df1
temp looser type checking until we find a better solution
adamhaeger Oct 17, 2024
3236593
ADR update
adamhaeger Oct 17, 2024
384a75b
add mobile table styling
Magnusrm Oct 17, 2024
1f30861
added test, updated ADR
adamhaeger Oct 17, 2024
5b724cd
Merge branch 'feature/table' of https://github.com/Altinn/app-fronten…
adamhaeger Oct 17, 2024
e66ab7a
cleanup
adamhaeger Oct 17, 2024
3bc7897
fix button cell styling
Magnusrm Oct 17, 2024
604f811
fix mobile styling
Magnusrm Oct 17, 2024
37c6900
add accessible header for action buttons
Magnusrm Oct 17, 2024
7ba8796
merge
adamhaeger Oct 17, 2024
401f770
change to list when showing multiple values in the same cell
adamhaeger Oct 17, 2024
36905da
Merge branch 'main' into feature/table
adamhaeger Oct 17, 2024
76d4cef
move caption component and add to Table component
Magnusrm Oct 17, 2024
d71d2d0
fix mobile caption
Magnusrm Oct 17, 2024
2d51bc5
Update adr/001-component-library.md
adamhaeger Oct 17, 2024
75b931f
Added support for nested accessors in the data table
adamhaeger Oct 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions adr/001-component-library.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Introduce component library

- Status: Proposed
- Deciders: Team
- Date: 17.10.2024

## Result

A1: Component library is introduced, firstly as just a folder in our current setup, adding yarn workspaces or similar requires more research and testing

## Problem context

Today our UI components are tightly coupled to the app in which they are rendered.

This leads to several issues:

- Makes it hard to do component testing outside a fully rendered app.
- Makes refactoring the app framework a lot harder.
- Leads to unclear interfaces between UI components and the app framework.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have linked examples of code/components for some of these points, I'm sure you've discussed and looked at tons of examples here to inform this assertion but for myself presently and future readers it would be helpful to know the context here 😄

- Makes developing UI components complex without deep understanding of the application.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Enables sharing of pure components (docs, Studio, ???)

?

## Decision drivers

A list of decision drivers. These are points which can differ in importance. If a point is "nice to have" rather than
"need to have", then prefix the description.

- B1: UI components should only receive data to display, notify the app when data is changed, and notify validity of user input.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- B1: UI components should only receive data to display, notify the app when data is changed, and notify validity of user input.
- B1: UI components should only receive data to display, and notify the app when data is changed.

I suggest that we remove the validation of user input part, as it makes little sense to leave this to UI components in our context

- B2: UI components should live in a separate folder from the app itself, and have no dependencies to the app.
- B3: UI components should live in a lib separately to the src folder to have stricter control of dependencies.

## Alternatives considered

List the alternatives that were considered as a solution to the problem context.

- A1: Simply put a new folder inside the src folder.
- A2: Use yarn workspaces to manage the library separately from the src folder.
- A3: Set up NX.js to manage our app and libraries.

## Pros and cons

List the pros and cons with the alternatives. This should be in regards to the decision drivers.

### A1

- Good because B1 and B2 is covered
- Allows us to really quickly get started with a component library
- Bad, because it does not fulfill B3. If we simply use a new folder, it will be up to developers to enforce the rules of the UI components, like the avoiding dependencies to the app.

### A2

- Good, because this alternative adheres to B1, B2 and B3.
- This way our libs would live separately to the app, and it would be obvious that it is a lib.
- The con is that it takes more setup.

### A2
adamhaeger marked this conversation as resolved.
Show resolved Hide resolved

- Good, because this alternative adheres to B1, B2 and B3.
- This way our libs would live separately to the app, and it would be obvious that it is a lib.
- Also gives us powerful monorepo tooling.
- Bad because it takes a lot more time to set up, and might be overkill before we have decided to integrate frontend and backend into monorepo.
11 changes: 11 additions & 0 deletions src/app-components/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## IMPORTANT

Components in this folder should be 'dumb', meaning the should have no knowledge about the containing app.

They should implement three things:

1. Receive and display data in the form of props.
2. Report data changes in a callback.
3. Report if the input is valid or not.

These components will form the basis of our future component library, and will enable refactoring of the frontend app.
84 changes: 84 additions & 0 deletions src/app-components/table/Table.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.table {
width: 100%;
}

.buttonContainer {
display: flex;
justify-content: end;
gap: var(--fds-spacing-2);
}

.mobileTable {
display: block;
}

.mobileTable thead {
display: none;
}

.mobileTable th {
display: block;
border: none;
}

.mobileTable tbody,
.mobileTable tr {
display: block;
}

.mobileTable td {
display: flex;
flex-direction: column;
border: none;
padding: var(--fds-spacing-2) 0;
}

.mobileTable td .contentWrapper {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}

.mobileTable tbody tr:last-child {
border-bottom: 2px solid var(--fds-semantic-border-neutral-default);
}

.mobileTable tbody tr:first-child {
border-top: 2px solid var(--fds-semantic-border-neutral-default);
}

.mobileTable tr {
border-bottom: 1px solid var(--fds-semantic-border-neutral-default);
padding-top: var(--fds-spacing-3);
padding-bottom: var(--fds-spacing-3);
}

.mobileTable tr:hover td {
background-color: unset;
}

.mobileTable td[data-header-title]:not([data-header-title=''])::before,
.mobileTable th[data-header-title]:not([data-header-title=''])::before {
content: attr(data-header-title);
display: block;
text-align: left;
font-weight: 500;
}

.mobileTable td .buttonContainer {
justify-content: start;
}

.visuallyHidden {
border: none;
padding: 0;
margin: 0;
position: absolute;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px);
clip-path: inset(50%);
white-space: nowrap;
}
191 changes: 191 additions & 0 deletions src/app-components/table/Table.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// AppTable.test.tsx
import React from 'react';

import { fireEvent, render, screen } from '@testing-library/react';

import { AppTable } from 'src/app-components/table/Table';

// Sample data
const data = [
{ id: 1, name: 'Alice', date: '2023-10-05', amount: 100 },
{ id: 2, name: 'Bob', date: '2023-10-06', amount: 200 },
];

// Columns configuration
const columns = [
{ header: 'Name', accessors: ['name'] },
{ header: 'Date', accessors: ['date'] },
{ header: 'Amount', accessors: ['amount'] },
];

// Action buttons configuration
const actionButtons = [
{
onClick: jest.fn(),
buttonText: 'Edit',
icon: null,
},
{
onClick: jest.fn(),
buttonText: 'Delete',
icon: null,
},
];

describe('AppTable Component', () => {
test('renders table with correct headers', () => {
render(
<AppTable
data={data}
columns={columns}
/>,
);
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Date')).toBeInTheDocument();
expect(screen.getByText('Amount')).toBeInTheDocument();
});

test('renders correct number of rows', () => {
render(
<AppTable
data={data}
columns={columns}
/>,
);
const rows = screen.getAllByRole('row');
expect(rows.length).toBe(data.length + 1); // +1 for header row
});

test('renders action buttons when provided', () => {
render(
<AppTable
data={data}
columns={columns}
actionButtons={actionButtons}
/>,
);
expect(screen.getAllByText('Edit').length).toBe(data.length);
expect(screen.getAllByText('Delete').length).toBe(data.length);
});

test('correctly formats dates in cells', () => {
render(
<AppTable
data={data}
columns={columns}
/>,
);
expect(screen.getByText('05.10.2023')).toBeInTheDocument();
expect(screen.getByText('06.10.2023')).toBeInTheDocument();
});

test('uses renderCell function when provided', () => {
const columnsWithRenderCell = [
...columns,
{
header: 'Custom',
accessors: ['name', 'amount'],
renderCell: (values) => `Name: ${values[0]}, Amount: ${values[1]}`,
},
];
render(
<AppTable
data={data}
columns={columnsWithRenderCell}
/>,
);
expect(screen.getByText('Name: Alice, Amount: 100')).toBeInTheDocument();
expect(screen.getByText('Name: Bob, Amount: 200')).toBeInTheDocument();
});

test('calls action button onClick when clicked', () => {
const onClickMock = jest.fn();
const actionButtonsMock = [
{
onClick: onClickMock,
buttonText: 'Edit',
icon: null,
},
];

render(
<AppTable
data={data}
columns={columns}
actionButtons={actionButtonsMock}
/>,
);

const editButtons = screen.getAllByText('Edit');
fireEvent.click(editButtons[0]);
expect(onClickMock).toHaveBeenCalledWith(0, data[0]);

fireEvent.click(editButtons[1]);
expect(onClickMock).toHaveBeenCalledWith(1, data[1]);

expect(onClickMock).toHaveBeenCalledTimes(2);
});

test('renders "-" when cell values are null or undefined', () => {
const dataWithNull = [
{ id: 1, name: 'Alice', date: null, amount: 100 },
{ id: 2, name: 'Bob', date: '2023-10-06', amount: 200 },
];
render(
<AppTable
data={dataWithNull}
columns={columns}
/>,
);
expect(screen.getAllByText('-').length).toBe(1);
});

test('does not render action buttons column when actionButtons is not provided', () => {
render(
<AppTable
data={data}
columns={columns}
/>,
);
const headerCells = screen.getAllByRole('columnheader');
expect(headerCells.length).toBe(columns.length);
});

test('renders extra header cell when actionButtons are provided', () => {
render(
<AppTable
data={data}
columns={columns}
actionButtons={actionButtons}
/>,
);
const headerCells = screen.getAllByRole('columnheader');
expect(headerCells.length).toBe(columns.length + 1);
});

test('non-date values are not changed by formatIfDate', () => {
const dataWithNonDate = [
{ id: 1, name: 'Alice', date: 'Not a date', amount: 100 },
{ id: 2, name: 'Bob', date: 'Also not a date', amount: 200 },
];
render(
<AppTable
data={dataWithNonDate}
columns={columns}
/>,
);
expect(screen.getByText('Not a date')).toBeInTheDocument();
expect(screen.getByText('Also not a date')).toBeInTheDocument();
});

test('non-string date values are converted to string', () => {
const dataWithNumberDate = [{ id: 1, name: 'Alice', date: 1234567890, amount: 100 }];
render(
<AppTable
data={dataWithNumberDate}
columns={columns}
/>,
);
expect(screen.getByText('1234567890')).toBeInTheDocument();
});
});
Loading
Loading