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

feat(@clayui/core): adds the implementation of the new data-oriented composition for Table #5679

Merged
merged 1 commit into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/clay-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@clayui/loading-indicator": "^3.60.0",
"@clayui/modal": "^3.104.0",
"@clayui/provider": "^3.93.0",
"@clayui/table": "^3.56.0",
"@clayui/shared": "^3.104.0",
"@tanstack/react-virtual": "3.0.0-beta.54",
"aria-hidden": "^1.2.2",
Expand Down
47 changes: 47 additions & 0 deletions packages/clay-core/src/table/Body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

import React from 'react';

import {ChildrenFunction, Collection} from '../collection';
import {Scope, ScopeContext} from './ScopeContext';

type Props<T> = {
/**
* Children content to render a dynamic or static content.
*/
children: React.ReactNode | ChildrenFunction<T, unknown>;

/**
* Property to render content with dynamic data.
*/
items?: Array<T>;
} & React.TableHTMLAttributes<HTMLTableSectionElement>;

function BodyInner<T extends Record<string, any>>(
{children, items, ...otherProps}: Props<T>,
ref: React.Ref<HTMLTableSectionElement>
) {
return (
<tbody {...otherProps} ref={ref}>
<ScopeContext.Provider value={Scope.Body}>
<Collection items={items} passthroughKey={false}>
{children}
</Collection>
</ScopeContext.Provider>
</tbody>
);
}

type ForwardRef = {
displayName: string;
<T>(
props: Props<T> & {ref?: React.Ref<HTMLTableSectionElement>}
): JSX.Element;
};

export const Body = React.forwardRef(BodyInner) as ForwardRef;

Body.displayName = 'TableBody';
93 changes: 93 additions & 0 deletions packages/clay-core/src/table/Column.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

import classNames from 'classnames';
import React from 'react';

import {Scope, useScope} from './ScopeContext';

type Props = {
/**
* Aligns the text inside the Cell.
*/
align?: 'center' | 'left' | 'right';

/**
* Children content to render content.
*/
children: React.ReactNode;

/**
* Sometimes we are unable to remove specific table columns from the DOM
* and need to hide it using CSS. This property can be added to the "new"
* first or last cell to maintain table styles on the left and right side.
*/
delimiter?: 'start' | 'end';

/**
* Fills out the remaining space inside a Cell.
*/
expanded?: boolean;

/**
* Aligns horizontally contents inside the Cell.
*/
textAlign?: 'center' | 'end' | 'start';

/**
* Truncates the text inside a Cell.
*/
truncate?: boolean;

/*
* Break the text into lines when necessary.
*/
wrap?: boolean;
} & React.ThHTMLAttributes<HTMLTableCellElement> &
React.TdHTMLAttributes<HTMLTableCellElement>;

export const Column = React.forwardRef<HTMLTableCellElement, Props>(
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason why we use 'Column' instead of 'Cell' as the name?

Copy link
Member Author

Choose a reason for hiding this comment

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

In fact, there isn't much of a reason, just using a slightly more "popular/common" nomenclature.

Copy link
Member

Choose a reason for hiding this comment

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

Hey @matuzalemsteles can you point me towards where you've seen column used? If we were to think about this like google sheets, this component renders a single cell such as A1, right? In the context of google sheets a column would refer to A:A or all the cells in a column.

Checking a few other libraries, MaterialUI uses TableCell, NextUI uses TableColumn only for cells in the header and TableCell for cells in the body.

Additionally, FDS uses TableCell and TableHeadCell. Also, most of the classnames being applied to this component are prefixed table-cell-*.

So what do you think, does Cell make more sense, or do you still see more reasons for Column?

Copy link
Member Author

Choose a reason for hiding this comment

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

@ethib137 These are good points, I hadn't thought about this side of Google Sheets. I really think that the case of Google sheets makes sense mainly because you deal with cells specifically although you call a column like A which groups several cells I would say that this is different for Table where sheets have the concept of cells much stronger because the cell can have a relationship between different cells, not necessarily related only to the specific column but can, for example, be diagonal and column is only used as a reference, which is different in a table where the cell has to be associated with a column to have context, which is not true in Google Sheets since the nature of the cell can have its own context and it is a more flexible term we can say like this.

But yes, calling cells for body elements makes sense, I don't like it particularly because the cell seems to not necessarily be "associated" with a column directly... For example Column is also a common name for components not necessarily related to a table , for example a group of elements to be rendered horizontally or vertically. For example:

  • Braid Design System
  • Base Web They don't necessarily use Column as a composition, there are some components as part of their Table that use the term column but their composition is very different.
  • Adobe Spectrum - They use the same Column pattern for the header and Cell for the body.
  • Polaris - Their composition is a little confusing but I would say that they use the same naming pattern of Column for header and cell for cell, in other table components the cell is not mentioned as part of the APIs, only rows.
  • Evergreen - Here they already use Cell for anywhere but you can understand that the cell is not necessarily associated with a column so it seems that it can be rendered in different ways which makes sense here.
  • Carbon - Composition follows the same pattern of having Column for header but they only call it Header, and Cell just for the body.
  • Radix - Composition is based on HeaderCell and Cell for the body only.

I don't really like the idea of creating a Column component to be used only in the Head and a Cell for only the body, I want to reduce the number of components for the composition and make the component more intelligent, which is also one of the reasons because I went with Column instead of just Cell for naming.

Anyway, we can go with Cell because we are already using this term in several places, so it will be simpler for current developers to associate this.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good. Thanks @matuzalemsteles . Can you create a ticket for making this change?

function ColumnInner(
{
align,
children,
className,
delimiter,
expanded,
textAlign,
truncate,
wrap = true,
...otherProps
},
ref
) {
const scope = useScope();
const As = scope === Scope.Head ? 'th' : 'td';

return (
<As
{...otherProps}
className={classNames(className, {
'table-cell-expand': truncate || expanded,
[`table-cell-${delimiter}`]: delimiter,
[`table-column-text-${textAlign}`]: textAlign,
[`text-${align}`]: align,
'table-cell-ws-nowrap': !wrap,
})}
ref={ref}
>
{truncate ? (
<span className="text-truncate-inline">
<span className="text-truncate">{children}</span>
</span>
) : (
children
)}
</As>
);
}
);

Column.displayName = 'Item';
49 changes: 49 additions & 0 deletions packages/clay-core/src/table/Head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

import React from 'react';

import {ChildrenFunction, Collection} from '../collection';
import {Scope, ScopeContext} from './ScopeContext';

type Props<T> = {
/**
* Children content to render a dynamic or static content.
*/
children: React.ReactNode | ChildrenFunction<T, unknown>;

/**
* Property to render content with dynamic data.
*/
items?: Array<T>;
} & React.TableHTMLAttributes<HTMLTableSectionElement>;

function HeadInner<T extends Record<string, any>>(
{children, items, ...otherProps}: Props<T>,
ref: React.Ref<HTMLTableSectionElement>
) {
return (
<thead {...otherProps} ref={ref}>
<ScopeContext.Provider value={Scope.Head}>
<tr>
<Collection items={items} passthroughKey={false}>
{children}
</Collection>
</tr>
</ScopeContext.Provider>
</thead>
);
}

type ForwardRef = {
displayName: string;
<T>(
props: Props<T> & {ref?: React.Ref<HTMLTableSectionElement>}
): JSX.Element;
};

export const Head = React.forwardRef(HeadInner) as ForwardRef;

Head.displayName = 'TableHead';
61 changes: 61 additions & 0 deletions packages/clay-core/src/table/Row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

import classNames from 'classnames';
import React from 'react';

import {ChildrenFunction, Collection} from '../collection';

type Props<T> = {
/**
* Children content to render a dynamic or static content.
*/
children: React.ReactNode | ChildrenFunction<T, unknown>;

/**
* This property can be added to the "new" first
* or last ClayTable.Row to maintain table styles on the top and bottom sides.
*/
delimiter?: 'start' | 'end';

/**
* Applies a divider style inside the row.
*/
divider?: boolean;

/**
* Property to render content with dynamic data.
*/
items?: Array<T>;
} & React.HTMLAttributes<HTMLTableRowElement>;

function RowInner<T extends Record<string, any>>(
{children, className, delimiter, divider, items, ...otherProps}: Props<T>,
ref: React.Ref<HTMLTableRowElement>
) {
return (
<tr
{...otherProps}
className={classNames(className, {
'table-divider': divider,
[`table-row-${delimiter}`]: delimiter,
})}
ref={ref}
>
<Collection items={items} passthroughKey={false}>
{children}
</Collection>
</tr>
);
}

type ForwardRef = {
displayName: string;
<T>(props: Props<T> & {ref?: React.Ref<HTMLTableRowElement>}): JSX.Element;
};

export const Row = React.forwardRef(RowInner) as ForwardRef;

Row.displayName = 'TableRow';
17 changes: 17 additions & 0 deletions packages/clay-core/src/table/ScopeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

import React, {useContext} from 'react';

export enum Scope {
Head = 'head',
Body = 'body',
}

export const ScopeContext = React.createContext<Scope>(Scope.Head);

export function useScope() {
return useContext(ScopeContext);
}
8 changes: 8 additions & 0 deletions packages/clay-core/src/table/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

import RootTable from '@clayui/table';

export const Table = RootTable;
10 changes: 10 additions & 0 deletions packages/clay-core/src/table/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

export * from './Body';
export * from './Column';
export * from './Head';
export * from './Row';
export * from './Table';
81 changes: 81 additions & 0 deletions packages/clay-core/stories/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* SPDX-FileCopyrightText: © 2023 Liferay, Inc. <https://liferay.com>
* SPDX-License-Identifier: BSD-3-Clause
*/

import React from 'react';

import {Body, Column, Head, Row, Table} from '../src/table';

export default {
title: 'Design System/Components/Table',
};

const columns = [
{
id: '1',
name: 'Name',
},
{
id: '2',
name: 'Type',
},
];

const rows = [
{id: 1, name: 'Games', type: 'File folder'},
{id: 2, name: 'Program Files', type: 'File folder'},
];

export const Dynamic = () => {
return (
<Table>
<Head items={columns}>
{(column) => <Column key={column.id}>{column.name}</Column>}
</Head>

<Body items={rows}>
{(row) => (
<Row>
<Column>{row.name}</Column>
<Column>{row.type}</Column>
</Row>
)}
</Body>
</Table>
);
};

const columns2 = [
{
id: 'name',
name: 'Name',
},
{
id: 'type',
name: 'Type',
},
];

const rows2 = [
{id: 1, name: 'Games', type: 'File folder'},
{id: 2, name: 'Program Files', type: 'File folder'},
];

export const DynamicCells = () => {
return (
<Table>
<Head items={columns2}>
{(column) => <Column key={column.id}>{column.name}</Column>}
</Head>

<Body items={rows2}>
{(row) => (
<Row items={columns2}>
{(column) => <Column>{row[column.id]}</Column>}
</Row>
)}
</Body>
</Table>
);
};
Loading