Skip to content

Commit

Permalink
feat: Create basic input table component (#13719)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasEng authored Oct 8, 2024
1 parent c76efd9 commit 99d9790
Show file tree
Hide file tree
Showing 23 changed files with 497 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ForwardedRef, ForwardRefRenderFunction, PropsWithoutRef } from 'react';
import { forwardRef } from 'react';
import type { InputCellComponent } from '../types/InputCellComponent';
import type { HTMLCellInputElement } from '../types/HTMLCellInputElement';

export abstract class BaseInputCell<Element extends HTMLCellInputElement, Props extends {}> {
displayName: string;

constructor(displayName: string) {
this.displayName = displayName;
}

component(): InputCellComponent<Props, Element> {
const component = forwardRef<Element, Props>(this.render);
component.displayName = this.displayName;
return component;
}

protected abstract render(
props: PropsWithoutRef<Props>,
ref: ForwardedRef<Element>,
): ReturnType<ForwardRefRenderFunction<Element, Props>>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.textfieldCell,
.textareaCell {
padding: var(--fds-spacing-1) 0;
font-size: var(--studio-input-table-font-size);
}

.buttonCell {
text-align: center;
}

.buttonCell button {
display: inline-flex;
}

.textfieldCell:not(:hover) input:not(:hover):not(:active):not(:focus),
.textareaCell:not(:hover) textarea:not(:hover):not(:active):not(:focus) {
background-color: transparent;
border-color: transparent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { StudioTable } from '../../StudioTable';

export const Cell = StudioTable.Cell;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { StudioTable } from '../../StudioTable';
import type { ForwardedRef, ReactElement } from 'react';
import React from 'react';
import classes from './Cell.module.css';
import type { StudioButtonProps } from '../../StudioButton';
import { StudioButton } from '../../StudioButton';
import { BaseInputCell } from './BaseInputCell';
import cn from 'classnames';

export type CellButtonProps = StudioButtonProps;

export class CellButton extends BaseInputCell<HTMLButtonElement, CellButtonProps> {
render(
{ className: givenClass, ...rest }: StudioButtonProps,
ref: ForwardedRef<HTMLButtonElement>,
): ReactElement {
const className = cn(classes.buttonCell, givenClass);
return (
<StudioTable.Cell className={className}>
<StudioButton ref={ref} variant='secondary' {...rest} />
</StudioTable.Cell>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { StudioTable } from '../../StudioTable';
import type { ForwardedRef, ReactElement } from 'react';
import React from 'react';
import type { StudioCheckboxProps } from '../../StudioCheckbox';
import { StudioCheckbox } from '../../StudioCheckbox';
import { BaseInputCell } from './BaseInputCell';

export type CellCheckboxProps = StudioCheckboxProps;

export class CellCheckbox extends BaseInputCell<HTMLInputElement, CellCheckboxProps> {
render(
{ className, ...rest }: CellCheckboxProps,
ref: ForwardedRef<HTMLInputElement>,
): ReactElement {
return (
<StudioTable.Cell className={className}>
<StudioCheckbox ref={ref} {...rest} />
</StudioTable.Cell>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { StudioTable } from '../../StudioTable';
import type { ForwardedRef, ReactElement } from 'react';
import React from 'react';
import classes from './Cell.module.css';
import type { StudioTextareaProps } from '../../StudioTextarea';
import { StudioTextarea } from '../../StudioTextarea';
import { BaseInputCell } from './BaseInputCell';
import cn from 'classnames';

export type CellTextareaProps = StudioTextareaProps;

export class CellTextarea extends BaseInputCell<HTMLTextAreaElement, CellTextareaProps> {
render(
{ className: givenClass, ...rest }: CellTextareaProps,
ref: ForwardedRef<HTMLTextAreaElement>,
): ReactElement {
const className = cn(classes.textareaCell, givenClass);
return (
<StudioTable.Cell className={className}>
<StudioTextarea hideLabel ref={ref} size='small' {...rest} />
</StudioTable.Cell>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { StudioTable } from '../../StudioTable';
import type { ForwardedRef, ReactElement } from 'react';
import React from 'react';
import type { StudioTextfieldProps } from '../../StudioTextfield';
import { StudioTextfield } from '../../StudioTextfield';
import classes from './Cell.module.css';
import { BaseInputCell } from './BaseInputCell';
import cn from 'classnames';

export type CellTextfieldProps = StudioTextfieldProps;

export class CellTextfield extends BaseInputCell<HTMLInputElement, CellTextfieldProps> {
render(
{ className: givenClass, ...rest }: CellTextfieldProps,
ref: ForwardedRef<HTMLInputElement>,
): ReactElement {
const className = cn(classes.textfieldCell, givenClass);
return (
<StudioTable.Cell className={className}>
<StudioTextfield hideLabel ref={ref} size='small' {...rest} />
</StudioTable.Cell>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { CellTextfieldProps } from './CellTextfield';
import { CellTextfield } from './CellTextfield';
import type { CellTextareaProps } from './CellTextarea';
import { CellTextarea } from './CellTextarea';
import type { CellButtonProps } from './CellButton';
import { CellButton } from './CellButton';
import { Cell } from './Cell';
import type { CellCheckboxProps } from './CellCheckbox';
import { CellCheckbox } from './CellCheckbox';
import type { InputCellComponent } from '../types/InputCellComponent';

type CellComponent = typeof Cell & {
Textfield: InputCellComponent<CellTextfieldProps, HTMLInputElement>;
Textarea: InputCellComponent<CellTextareaProps, HTMLTextAreaElement>;
Button: InputCellComponent<CellButtonProps, HTMLButtonElement>;
Checkbox: InputCellComponent<CellCheckboxProps, HTMLInputElement>;
};

export const StudioInputTableCell = Cell as CellComponent;

StudioInputTableCell.Textfield = new CellTextfield('StudioInputTable.Cell.Textfield').component();
StudioInputTableCell.Textarea = new CellTextarea('StudioInputTable.Cell.Textarea').component();
StudioInputTableCell.Button = new CellButton('StudioInputTable.Cell.Button').component();
StudioInputTableCell.Checkbox = new CellCheckbox('StudioInputTable.Cell.Checkbox').component();

StudioInputTableCell.displayName = 'StudioInputTable.Cell';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { StudioTable } from '../../StudioTable';

export const HeaderCell = StudioTable.HeaderCell;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { StudioTable } from '../../StudioTable';
import React, { forwardRef, useId } from 'react';
import type { StudioCheckboxProps } from '../../StudioCheckbox';
import { StudioCheckbox } from '../../StudioCheckbox';

export type HeaderCellCheckboxProps = Omit<StudioCheckboxProps, 'value'> & { value?: string };

export const HeaderCellCheckbox = forwardRef<HTMLInputElement, HeaderCellCheckboxProps>(
({ value: givenValue, className, ...rest }, ref) => {
const defaultValue = useId();
const value = givenValue ?? defaultValue;
return (
<StudioTable.HeaderCell className={className}>
<StudioCheckbox ref={ref} value={value} {...rest} />
</StudioTable.HeaderCell>
);
},
);

HeaderCellCheckbox.displayName = 'HeaderCell.Checkbox';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { HeaderCell } from './HeaderCell';
import { HeaderCellCheckbox } from './HeaderCellCheckbox';

type HeaderCellComponent = typeof HeaderCell & {
Checkbox: typeof HeaderCellCheckbox;
};

export const StudioInputTableHeaderCell = HeaderCell as HeaderCellComponent;

StudioInputTableHeaderCell.Checkbox = HeaderCellCheckbox;

StudioInputTableHeaderCell.displayName = 'StudioInputTableHeaderCell.HeaderCell';
StudioInputTableHeaderCell.Checkbox.displayName = 'StudioInputTableHeaderCell.Checkbox';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ComponentProps } from 'react';
import React, { forwardRef } from 'react';
import { StudioTable } from '../../StudioTable';

type RowProps = ComponentProps<typeof StudioTable.Row>;

export const Row = forwardRef<HTMLTableRowElement, RowProps>(({ children, ...rest }, ref) => (
<StudioTable.Row ref={ref} {...rest}>
{children}
</StudioTable.Row>
));

Row.displayName = 'StudioInputTable.Row';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Row } from './Row';

export const StudioInputTableRow = Row;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.inputTable {
--studio-input-table-font-size: --fds-font-size-f0;
font-size: var(--studio-input-table-font-size);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import { TestTable } from './test-data/TestTable';
import type { StudioInputTableProps } from './StudioInputTable';

type Story = StoryFn<typeof TestTable>;

export function render(props: StudioInputTableProps): ReactElement {
return <TestTable {...props} />;
}

const meta: Meta<Story> = {
title: 'Components/StudioInputTable',
component: TestTable,
render,
};
export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { ForwardedRef, ReactNode } from 'react';
import React from 'react';
import { StudioInputTable } from './';
import type { RenderResult } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { TestTable } from './test-data/TestTable';
import type { StudioInputTableProps } from './StudioInputTable';
import { expect } from '@storybook/test';
import { testRefForwarding } from '../../test-utils/testRefForwarding';
import { testRootClassNameAppending } from '../../test-utils/testRootClassNameAppending';
import { testCustomAttributes } from '../../test-utils/testCustomAttributes';

describe('StudioInputTable', () => {
it('Renders a table', () => {
renderStudioInputTable();
expect(getTable()).toBeInTheDocument();
});

it('Forwards the ref if provided', () => {
const renderTable = (ref: ForwardedRef<HTMLTableElement>) =>
render(
<StudioInputTable ref={ref}>
<StudioInputTable.Body>
<StudioInputTable.Row>
<StudioInputTable.Cell />
</StudioInputTable.Row>
</StudioInputTable.Body>
</StudioInputTable>,
);
testRefForwarding<HTMLTableElement>(renderTable, getTable);
});

it('Appends the given class to the table', () => {
testRootClassNameAppending((className) => renderStudioInputTable({ className }));
});

it('Applies the given props to the table', () => {
testCustomAttributes<HTMLTableElement>(renderStudioInputTable, getTable);
});

it('Renders all headers', () => {
render(<TestTable />);
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(expectedNumberOfColumns);
});

it('Renders all rows', () => {
render(<TestTable />);
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(expectedNumberOfRows);
});

describe('Forwards the refs to the input elements', () => {
type TestCase<Element extends HTMLElement = HTMLElement> = {
render: (ref: ForwardedRef<Element>) => RenderResult;
getElement: () => Element;
};
const testLabel = 'test';
const testCases: {
checkbox: TestCase<HTMLInputElement>;
textfield: TestCase<HTMLInputElement>;
textarea: TestCase<HTMLTextAreaElement>;
button: TestCase<HTMLButtonElement>;
} = {
checkbox: {
render: (ref) =>
render(
<SingleRow>
<StudioInputTable.Cell.Checkbox value='test' aria-label={testLabel} ref={ref} />
</SingleRow>,
),
getElement: () => getCheckbox(testLabel),
},
textfield: {
render: (ref) =>
render(
<SingleRow>
<StudioInputTable.Cell.Textfield label={testLabel} ref={ref} />
</SingleRow>,
),
getElement: () => getTextbox(testLabel) as HTMLInputElement,
},
textarea: {
render: (ref) =>
render(
<SingleRow>
<StudioInputTable.Cell.Textarea label={testLabel} ref={ref} />
</SingleRow>,
),
getElement: () => getTextbox(testLabel) as HTMLTextAreaElement,
},
button: {
render: (ref) =>
render(
<SingleRow>
<StudioInputTable.Cell.Button ref={ref}>{testLabel}</StudioInputTable.Cell.Button>
</SingleRow>,
),
getElement: () => getButton(testLabel),
},
};

it.each(Object.keys(testCases))('%s', (key) => {
const { render: renderComponent, getElement } = testCases[key];
testRefForwarding(renderComponent, getElement);
});
});
});

const renderStudioInputTable = (props: StudioInputTableProps = {}) =>
render(<TestTable {...props} />);

const getTable = (): HTMLTableElement => screen.getByRole('table');
const getCheckbox = (name: string): HTMLInputElement =>
screen.getByRole('checkbox', { name }) as HTMLInputElement;
const getTextbox = (name: string) => screen.getByRole('textbox', { name });
const getButton = (name: string): HTMLButtonElement =>
screen.getByRole('button', { name }) as HTMLButtonElement;

const expectedNumberOfColumns = 5;
const expectedNumberOfHeaderRows = 1;
const expectedNumberOfBodyRows = 3;
const expectedNumberOfRows = expectedNumberOfBodyRows + expectedNumberOfHeaderRows;

function SingleRow({ children }: { children: ReactNode }) {
return (
<StudioInputTable>
<StudioInputTable.Body>
<StudioInputTable.Row>{children}</StudioInputTable.Row>
</StudioInputTable.Body>
</StudioInputTable>
);
}
Loading

0 comments on commit 99d9790

Please sign in to comment.