From 9c8f62ae8e6dfcee4ce615f7737cb2ebf6a88d5c Mon Sep 17 00:00:00 2001 From: Tomas Date: Mon, 7 Oct 2024 11:41:52 +0200 Subject: [PATCH 1/3] Create basic input table component --- .../StudioInputTable/Cell/BaseInputCell.ts | 23 +++ .../StudioInputTable/Cell/Cell.module.css | 15 ++ .../components/StudioInputTable/Cell/Cell.tsx | 3 + .../StudioInputTable/Cell/CellButton.tsx | 19 +++ .../StudioInputTable/Cell/CellCheckbox.tsx | 18 +++ .../StudioInputTable/Cell/CellTextarea.tsx | 19 +++ .../StudioInputTable/Cell/CellTextfield.tsx | 19 +++ .../components/StudioInputTable/Cell/index.ts | 26 ++++ .../HeaderCell/HeaderCell.tsx | 3 + .../HeaderCell/HeaderCellCheckbox.tsx | 20 +++ .../StudioInputTable/HeaderCell/index.ts | 13 ++ .../components/StudioInputTable/Row/Row.tsx | 13 ++ .../components/StudioInputTable/Row/index.ts | 3 + .../StudioInputTable.module.css | 4 + .../StudioInputTable.stories.tsx | 18 +++ .../StudioInputTable.test.tsx | 133 ++++++++++++++++++ .../StudioInputTable/StudioInputTable.tsx | 20 +++ .../src/components/StudioInputTable/index.ts | 26 ++++ .../StudioInputTable/test-data/TestTable.tsx | 59 ++++++++ .../test-data/testTableData.ts | 14 ++ .../StudioInputTable/types/CellCoords.ts | 4 + .../types/HTMLCellInputElement.ts | 1 + .../types/InputCellComponent.ts | 5 + .../studio-components/src/components/index.ts | 1 + 24 files changed, 479 insertions(+) create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/BaseInputCell.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/index.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCell.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/index.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Row/Row.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Row/index.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.module.css create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.stories.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.test.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/index.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/test-data/TestTable.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/test-data/testTableData.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/types/HTMLCellInputElement.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/types/InputCellComponent.ts diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/BaseInputCell.ts b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/BaseInputCell.ts new file mode 100644 index 00000000000..84e88030456 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/BaseInputCell.ts @@ -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 { + displayName: string; + + constructor(displayName: string) { + this.displayName = displayName; + } + + component(): InputCellComponent { + const component = forwardRef(this.render); + component.displayName = this.displayName; + return component; + } + + protected abstract render( + props: PropsWithoutRef, + ref: ForwardedRef, + ): ReturnType>; +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css new file mode 100644 index 00000000000..b609bcf72f4 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css @@ -0,0 +1,15 @@ +.textfieldCell, +.textareaCell { + padding: var(--fds-spacing-1) 0; + font-size: var(--studio-input-table-font-size); +} + +.buttonCell button { + display: 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; +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.tsx new file mode 100644 index 00000000000..086831d094f --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.tsx @@ -0,0 +1,3 @@ +import { StudioTable } from '../../StudioTable'; + +export const Cell = StudioTable.Cell; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx new file mode 100644 index 00000000000..612e105dbbb --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx @@ -0,0 +1,19 @@ +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'; + +export type CellButtonProps = StudioButtonProps; + +export class CellButton extends BaseInputCell { + render(props: StudioButtonProps, ref: ForwardedRef): ReactElement { + return ( + + + + ); + } +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx new file mode 100644 index 00000000000..bda837f269a --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx @@ -0,0 +1,18 @@ +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 { + render(props: CellCheckboxProps, ref: ForwardedRef): ReactElement { + return ( + + + + ); + } +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx new file mode 100644 index 00000000000..a43ccc1ac7a --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx @@ -0,0 +1,19 @@ +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'; + +export type CellTextareaProps = StudioTextareaProps; + +export class CellTextarea extends BaseInputCell { + render(props: CellTextareaProps, ref: ForwardedRef): ReactElement { + return ( + + + + ); + } +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx new file mode 100644 index 00000000000..1d77fb00938 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx @@ -0,0 +1,19 @@ +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'; + +export type CellTextfieldProps = StudioTextfieldProps; + +export class CellTextfield extends BaseInputCell { + render(props: CellTextfieldProps, ref: ForwardedRef): ReactElement { + return ( + + + + ); + } +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/index.ts b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/index.ts new file mode 100644 index 00000000000..1af726ee49a --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/index.ts @@ -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; + Textarea: InputCellComponent; + Button: InputCellComponent; + Checkbox: InputCellComponent; +}; + +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'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCell.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCell.tsx new file mode 100644 index 00000000000..13e5a0b4c01 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCell.tsx @@ -0,0 +1,3 @@ +import { StudioTable } from '../../StudioTable'; + +export const HeaderCell = StudioTable.HeaderCell; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx new file mode 100644 index 00000000000..061469beab2 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx @@ -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 & { value?: string }; + +export const HeaderCellCheckbox = forwardRef( + ({ value: givenValue, ...rest }, ref) => { + const defaultValue = useId(); + const value = givenValue ?? defaultValue; + return ( + + + + ); + }, +); + +HeaderCellCheckbox.displayName = 'HeaderCell.Checkbox'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/index.ts b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/index.ts new file mode 100644 index 00000000000..ac99ed99dc9 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/index.ts @@ -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'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Row/Row.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Row/Row.tsx new file mode 100644 index 00000000000..b2ff72491be --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Row/Row.tsx @@ -0,0 +1,13 @@ +import type { ComponentProps } from 'react'; +import React, { forwardRef } from 'react'; +import { StudioTable } from '../../StudioTable'; + +type RowProps = ComponentProps; + +export const Row = forwardRef(({ children, ...rest }, ref) => ( + + {children} + +)); + +Row.displayName = 'StudioInputTable.Row'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Row/index.ts b/frontend/libs/studio-components/src/components/StudioInputTable/Row/index.ts new file mode 100644 index 00000000000..e253f9525bf --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Row/index.ts @@ -0,0 +1,3 @@ +import { Row } from './Row'; + +export const StudioInputTableRow = Row; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.module.css b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.module.css new file mode 100644 index 00000000000..556d0361317 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.module.css @@ -0,0 +1,4 @@ +.inputTable { + --studio-input-table-font-size: --fds-font-size-f0; + font-size: var(--studio-input-table-font-size); +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.stories.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.stories.tsx new file mode 100644 index 00000000000..5aad6ddac61 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.stories.tsx @@ -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; + +export function render(props: StudioInputTableProps): ReactElement { + return ; +} + +const meta: Meta = { + title: 'Components/StudioInputTable', + component: TestTable, + render, +}; +export default meta; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.test.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.test.tsx new file mode 100644 index 00000000000..90adccb1462 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.test.tsx @@ -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) => + render( + + + + + + + , + ); + testRefForwarding(renderTable, getTable); + }); + + it('Appends the given class to the table', () => { + testRootClassNameAppending((className) => renderStudioInputTable({ className })); + }); + + it('Applies the given props to the table', () => { + testCustomAttributes(renderStudioInputTable, getTable); + }); + + it('Renders all headers', () => { + render(); + const headers = screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(expectedNumberOfColumns); + }); + + it('Renders all rows', () => { + render(); + const rows = screen.getAllByRole('row'); + expect(rows).toHaveLength(expectedNumberOfRows); + }); + + describe('Forwards the refs to the input elements', () => { + type TestCase = { + render: (ref: ForwardedRef) => RenderResult; + getElement: () => Element; + }; + const testLabel = 'test'; + const testCases: { + checkbox: TestCase; + textfield: TestCase; + textarea: TestCase; + button: TestCase; + } = { + checkbox: { + render: (ref) => + render( + + + , + ), + getElement: () => getCheckbox(testLabel), + }, + textfield: { + render: (ref) => + render( + + + , + ), + getElement: () => getTextbox(testLabel) as HTMLInputElement, + }, + textarea: { + render: (ref) => + render( + + + , + ), + getElement: () => getTextbox(testLabel) as HTMLTextAreaElement, + }, + button: { + render: (ref) => + render( + + {testLabel} + , + ), + getElement: () => getButton(testLabel), + }, + }; + + it.each(Object.keys(testCases))('%s', (key) => { + const { render: renderComponent, getElement } = testCases[key]; + testRefForwarding(renderComponent, getElement); + }); + }); +}); + +const renderStudioInputTable = (props: StudioInputTableProps = {}) => + render(); + +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 ( + + + {children} + + + ); +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.tsx new file mode 100644 index 00000000000..fceeecc9309 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.tsx @@ -0,0 +1,20 @@ +import React, { forwardRef } from 'react'; +import type { StudioTableProps } from '../StudioTable'; +import { StudioTable } from '../StudioTable'; +import classes from './StudioInputTable.module.css'; +import cn from 'classnames'; + +export type StudioInputTableProps = StudioTableProps; + +export const StudioInputTable = forwardRef( + ({ className: givenClass, children, ...rest }, ref) => { + const className = cn(classes.inputTable, givenClass); + return ( + + {children} + + ); + }, +); + +StudioInputTable.displayName = 'StudioInputTable'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/index.ts b/frontend/libs/studio-components/src/components/StudioInputTable/index.ts new file mode 100644 index 00000000000..042198e8796 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/index.ts @@ -0,0 +1,26 @@ +import { StudioInputTable as StudioInputTableRoot } from './StudioInputTable'; +import { StudioTable } from '../StudioTable'; +import { StudioInputTableCell } from './Cell'; +import { StudioInputTableHeaderCell } from './HeaderCell'; +import { StudioInputTableRow } from './Row'; + +type StudioInputTableComponent = typeof StudioInputTableRoot & { + Head: typeof StudioTable.Head; + Body: typeof StudioTable.Body; + Row: typeof StudioInputTableRow; + Cell: typeof StudioInputTableCell; + HeaderCell: typeof StudioInputTableHeaderCell; +}; + +export const StudioInputTable = StudioInputTableRoot as StudioInputTableComponent; + +StudioInputTable.Head = StudioTable.Head; +StudioInputTable.Body = StudioTable.Body; +StudioInputTable.Row = StudioInputTableRow; +StudioInputTable.Cell = StudioInputTableCell; +StudioInputTable.HeaderCell = StudioInputTableHeaderCell; + +StudioInputTable.displayName = 'StudioInputTable'; +StudioInputTable.Head.displayName = 'StudioInputTable.Head'; +StudioInputTable.Body.displayName = 'StudioInputTable.Body'; +StudioInputTable.Row.displayName = 'StudioInputTable.Row'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/test-data/TestTable.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/test-data/TestTable.tsx new file mode 100644 index 00000000000..f9555904705 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/test-data/TestTable.tsx @@ -0,0 +1,59 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { StudioInputTable } from '../index'; +import * as testData from './testTableData'; +import type { StudioInputTableProps } from '../StudioInputTable'; +import { + buttonHeader, + headerCheckboxLabel, + textareaHeader, + textfieldHeader, + textHeader, +} from './testTableData'; + +export function TestTable(props: StudioInputTableProps): ReactElement { + return ( + + + + + {textHeader} + {textfieldHeader} + {textareaHeader} + {buttonHeader} + + + + + + + + + ); +} + +type TestRowProps = { + rowNumber: number; +}; + +function TestRow({ rowNumber: rn }: TestRowProps): ReactElement { + return ( + + + {testData.cleanText(rn)} + + + {testData.buttonLabel(rn)} + + ); +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/test-data/testTableData.ts b/frontend/libs/studio-components/src/components/StudioInputTable/test-data/testTableData.ts new file mode 100644 index 00000000000..4597ada6041 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/test-data/testTableData.ts @@ -0,0 +1,14 @@ +export const headerCheckboxLabel = 'Select all'; +export const textHeader = 'Text'; +export const textfieldHeader = 'Textfield'; +export const textareaHeader = 'Textarea'; +export const buttonHeader = 'Button'; +export const checkboxValue = (rowNumber: number) => `checkboxValue${rowNumber}`; +export const checkboxName = (rowNumber: number) => `checkboxName${rowNumber}`; +export const checkboxLabel = (rowNumber: number) => `Checkbox ${rowNumber}`; +export const cleanText = (rowNumber: number) => `Text ${rowNumber}`; +export const textfieldName = (rowNumber: number) => `textfield${rowNumber}`; +export const textfieldLabel = (rowNumber: number) => `Textfield ${rowNumber}`; +export const textareaName = (rowNumber: number) => `textarea${rowNumber}`; +export const textareaLabel = (rowNumber: number) => `Textarea ${rowNumber}`; +export const buttonLabel = (rowNumber: number) => `Button ${rowNumber}`; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts b/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts new file mode 100644 index 00000000000..47be5824190 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts @@ -0,0 +1,4 @@ +export type CellCoords = { + row: number; + column: number; +}; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/types/HTMLCellInputElement.ts b/frontend/libs/studio-components/src/components/StudioInputTable/types/HTMLCellInputElement.ts new file mode 100644 index 00000000000..94534fe785c --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/types/HTMLCellInputElement.ts @@ -0,0 +1 @@ +export type HTMLCellInputElement = HTMLInputElement | HTMLTextAreaElement | HTMLButtonElement; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/types/InputCellComponent.ts b/frontend/libs/studio-components/src/components/StudioInputTable/types/InputCellComponent.ts new file mode 100644 index 00000000000..d890c3f9acb --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/types/InputCellComponent.ts @@ -0,0 +1,5 @@ +import type { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes } from 'react'; + +export type InputCellComponent = ForwardRefExoticComponent< + PropsWithoutRef & RefAttributes +>; diff --git a/frontend/libs/studio-components/src/components/index.ts b/frontend/libs/studio-components/src/components/index.ts index 1f46f2df7b5..4b954b136fc 100644 --- a/frontend/libs/studio-components/src/components/index.ts +++ b/frontend/libs/studio-components/src/components/index.ts @@ -20,6 +20,7 @@ export * from './StudioFileUploader'; export * from './StudioGridSelector'; export * from './StudioHeading'; export * from './StudioIconTextfield'; +export * from './StudioInputTable'; export * from './StudioLabelAsParagraph'; export * from './StudioLabelWrapper'; export * from './StudioModal'; From ccaf6225e88ae16c342be3b04329e3129be08a4c Mon Sep 17 00:00:00 2001 From: Tomas Date: Mon, 7 Oct 2024 11:41:52 +0200 Subject: [PATCH 2/3] Create basic input table component --- .../StudioInputTable/Cell/BaseInputCell.ts | 23 +++ .../StudioInputTable/Cell/Cell.module.css | 15 ++ .../components/StudioInputTable/Cell/Cell.tsx | 3 + .../StudioInputTable/Cell/CellButton.tsx | 19 +++ .../StudioInputTable/Cell/CellCheckbox.tsx | 18 +++ .../StudioInputTable/Cell/CellTextarea.tsx | 19 +++ .../StudioInputTable/Cell/CellTextfield.tsx | 19 +++ .../components/StudioInputTable/Cell/index.ts | 26 ++++ .../HeaderCell/HeaderCell.tsx | 3 + .../HeaderCell/HeaderCellCheckbox.tsx | 20 +++ .../StudioInputTable/HeaderCell/index.ts | 13 ++ .../components/StudioInputTable/Row/Row.tsx | 13 ++ .../components/StudioInputTable/Row/index.ts | 3 + .../StudioInputTable.module.css | 4 + .../StudioInputTable.stories.tsx | 18 +++ .../StudioInputTable.test.tsx | 133 ++++++++++++++++++ .../StudioInputTable/StudioInputTable.tsx | 20 +++ .../src/components/StudioInputTable/index.ts | 26 ++++ .../StudioInputTable/test-data/TestTable.tsx | 59 ++++++++ .../test-data/testTableData.ts | 14 ++ .../StudioInputTable/types/CellCoords.ts | 4 + .../types/HTMLCellInputElement.ts | 1 + .../types/InputCellComponent.ts | 5 + .../studio-components/src/components/index.ts | 1 + 24 files changed, 479 insertions(+) create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/BaseInputCell.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Cell/index.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCell.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/index.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Row/Row.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/Row/index.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.module.css create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.stories.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.test.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/index.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/test-data/TestTable.tsx create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/test-data/testTableData.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/types/HTMLCellInputElement.ts create mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/types/InputCellComponent.ts diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/BaseInputCell.ts b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/BaseInputCell.ts new file mode 100644 index 00000000000..84e88030456 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/BaseInputCell.ts @@ -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 { + displayName: string; + + constructor(displayName: string) { + this.displayName = displayName; + } + + component(): InputCellComponent { + const component = forwardRef(this.render); + component.displayName = this.displayName; + return component; + } + + protected abstract render( + props: PropsWithoutRef, + ref: ForwardedRef, + ): ReturnType>; +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css new file mode 100644 index 00000000000..b609bcf72f4 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css @@ -0,0 +1,15 @@ +.textfieldCell, +.textareaCell { + padding: var(--fds-spacing-1) 0; + font-size: var(--studio-input-table-font-size); +} + +.buttonCell button { + display: 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; +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.tsx new file mode 100644 index 00000000000..086831d094f --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.tsx @@ -0,0 +1,3 @@ +import { StudioTable } from '../../StudioTable'; + +export const Cell = StudioTable.Cell; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx new file mode 100644 index 00000000000..612e105dbbb --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx @@ -0,0 +1,19 @@ +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'; + +export type CellButtonProps = StudioButtonProps; + +export class CellButton extends BaseInputCell { + render(props: StudioButtonProps, ref: ForwardedRef): ReactElement { + return ( + + + + ); + } +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx new file mode 100644 index 00000000000..bda837f269a --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx @@ -0,0 +1,18 @@ +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 { + render(props: CellCheckboxProps, ref: ForwardedRef): ReactElement { + return ( + + + + ); + } +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx new file mode 100644 index 00000000000..a43ccc1ac7a --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx @@ -0,0 +1,19 @@ +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'; + +export type CellTextareaProps = StudioTextareaProps; + +export class CellTextarea extends BaseInputCell { + render(props: CellTextareaProps, ref: ForwardedRef): ReactElement { + return ( + + + + ); + } +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx new file mode 100644 index 00000000000..1d77fb00938 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx @@ -0,0 +1,19 @@ +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'; + +export type CellTextfieldProps = StudioTextfieldProps; + +export class CellTextfield extends BaseInputCell { + render(props: CellTextfieldProps, ref: ForwardedRef): ReactElement { + return ( + + + + ); + } +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/index.ts b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/index.ts new file mode 100644 index 00000000000..1af726ee49a --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/index.ts @@ -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; + Textarea: InputCellComponent; + Button: InputCellComponent; + Checkbox: InputCellComponent; +}; + +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'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCell.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCell.tsx new file mode 100644 index 00000000000..13e5a0b4c01 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCell.tsx @@ -0,0 +1,3 @@ +import { StudioTable } from '../../StudioTable'; + +export const HeaderCell = StudioTable.HeaderCell; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx new file mode 100644 index 00000000000..061469beab2 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx @@ -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 & { value?: string }; + +export const HeaderCellCheckbox = forwardRef( + ({ value: givenValue, ...rest }, ref) => { + const defaultValue = useId(); + const value = givenValue ?? defaultValue; + return ( + + + + ); + }, +); + +HeaderCellCheckbox.displayName = 'HeaderCell.Checkbox'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/index.ts b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/index.ts new file mode 100644 index 00000000000..ac99ed99dc9 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/index.ts @@ -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'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Row/Row.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Row/Row.tsx new file mode 100644 index 00000000000..b2ff72491be --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Row/Row.tsx @@ -0,0 +1,13 @@ +import type { ComponentProps } from 'react'; +import React, { forwardRef } from 'react'; +import { StudioTable } from '../../StudioTable'; + +type RowProps = ComponentProps; + +export const Row = forwardRef(({ children, ...rest }, ref) => ( + + {children} + +)); + +Row.displayName = 'StudioInputTable.Row'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Row/index.ts b/frontend/libs/studio-components/src/components/StudioInputTable/Row/index.ts new file mode 100644 index 00000000000..e253f9525bf --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Row/index.ts @@ -0,0 +1,3 @@ +import { Row } from './Row'; + +export const StudioInputTableRow = Row; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.module.css b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.module.css new file mode 100644 index 00000000000..556d0361317 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.module.css @@ -0,0 +1,4 @@ +.inputTable { + --studio-input-table-font-size: --fds-font-size-f0; + font-size: var(--studio-input-table-font-size); +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.stories.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.stories.tsx new file mode 100644 index 00000000000..5aad6ddac61 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.stories.tsx @@ -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; + +export function render(props: StudioInputTableProps): ReactElement { + return ; +} + +const meta: Meta = { + title: 'Components/StudioInputTable', + component: TestTable, + render, +}; +export default meta; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.test.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.test.tsx new file mode 100644 index 00000000000..90adccb1462 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.test.tsx @@ -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) => + render( + + + + + + + , + ); + testRefForwarding(renderTable, getTable); + }); + + it('Appends the given class to the table', () => { + testRootClassNameAppending((className) => renderStudioInputTable({ className })); + }); + + it('Applies the given props to the table', () => { + testCustomAttributes(renderStudioInputTable, getTable); + }); + + it('Renders all headers', () => { + render(); + const headers = screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(expectedNumberOfColumns); + }); + + it('Renders all rows', () => { + render(); + const rows = screen.getAllByRole('row'); + expect(rows).toHaveLength(expectedNumberOfRows); + }); + + describe('Forwards the refs to the input elements', () => { + type TestCase = { + render: (ref: ForwardedRef) => RenderResult; + getElement: () => Element; + }; + const testLabel = 'test'; + const testCases: { + checkbox: TestCase; + textfield: TestCase; + textarea: TestCase; + button: TestCase; + } = { + checkbox: { + render: (ref) => + render( + + + , + ), + getElement: () => getCheckbox(testLabel), + }, + textfield: { + render: (ref) => + render( + + + , + ), + getElement: () => getTextbox(testLabel) as HTMLInputElement, + }, + textarea: { + render: (ref) => + render( + + + , + ), + getElement: () => getTextbox(testLabel) as HTMLTextAreaElement, + }, + button: { + render: (ref) => + render( + + {testLabel} + , + ), + getElement: () => getButton(testLabel), + }, + }; + + it.each(Object.keys(testCases))('%s', (key) => { + const { render: renderComponent, getElement } = testCases[key]; + testRefForwarding(renderComponent, getElement); + }); + }); +}); + +const renderStudioInputTable = (props: StudioInputTableProps = {}) => + render(); + +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 ( + + + {children} + + + ); +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.tsx new file mode 100644 index 00000000000..fceeecc9309 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/StudioInputTable.tsx @@ -0,0 +1,20 @@ +import React, { forwardRef } from 'react'; +import type { StudioTableProps } from '../StudioTable'; +import { StudioTable } from '../StudioTable'; +import classes from './StudioInputTable.module.css'; +import cn from 'classnames'; + +export type StudioInputTableProps = StudioTableProps; + +export const StudioInputTable = forwardRef( + ({ className: givenClass, children, ...rest }, ref) => { + const className = cn(classes.inputTable, givenClass); + return ( + + {children} + + ); + }, +); + +StudioInputTable.displayName = 'StudioInputTable'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/index.ts b/frontend/libs/studio-components/src/components/StudioInputTable/index.ts new file mode 100644 index 00000000000..042198e8796 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/index.ts @@ -0,0 +1,26 @@ +import { StudioInputTable as StudioInputTableRoot } from './StudioInputTable'; +import { StudioTable } from '../StudioTable'; +import { StudioInputTableCell } from './Cell'; +import { StudioInputTableHeaderCell } from './HeaderCell'; +import { StudioInputTableRow } from './Row'; + +type StudioInputTableComponent = typeof StudioInputTableRoot & { + Head: typeof StudioTable.Head; + Body: typeof StudioTable.Body; + Row: typeof StudioInputTableRow; + Cell: typeof StudioInputTableCell; + HeaderCell: typeof StudioInputTableHeaderCell; +}; + +export const StudioInputTable = StudioInputTableRoot as StudioInputTableComponent; + +StudioInputTable.Head = StudioTable.Head; +StudioInputTable.Body = StudioTable.Body; +StudioInputTable.Row = StudioInputTableRow; +StudioInputTable.Cell = StudioInputTableCell; +StudioInputTable.HeaderCell = StudioInputTableHeaderCell; + +StudioInputTable.displayName = 'StudioInputTable'; +StudioInputTable.Head.displayName = 'StudioInputTable.Head'; +StudioInputTable.Body.displayName = 'StudioInputTable.Body'; +StudioInputTable.Row.displayName = 'StudioInputTable.Row'; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/test-data/TestTable.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/test-data/TestTable.tsx new file mode 100644 index 00000000000..f9555904705 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/test-data/TestTable.tsx @@ -0,0 +1,59 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { StudioInputTable } from '../index'; +import * as testData from './testTableData'; +import type { StudioInputTableProps } from '../StudioInputTable'; +import { + buttonHeader, + headerCheckboxLabel, + textareaHeader, + textfieldHeader, + textHeader, +} from './testTableData'; + +export function TestTable(props: StudioInputTableProps): ReactElement { + return ( + + + + + {textHeader} + {textfieldHeader} + {textareaHeader} + {buttonHeader} + + + + + + + + + ); +} + +type TestRowProps = { + rowNumber: number; +}; + +function TestRow({ rowNumber: rn }: TestRowProps): ReactElement { + return ( + + + {testData.cleanText(rn)} + + + {testData.buttonLabel(rn)} + + ); +} diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/test-data/testTableData.ts b/frontend/libs/studio-components/src/components/StudioInputTable/test-data/testTableData.ts new file mode 100644 index 00000000000..4597ada6041 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/test-data/testTableData.ts @@ -0,0 +1,14 @@ +export const headerCheckboxLabel = 'Select all'; +export const textHeader = 'Text'; +export const textfieldHeader = 'Textfield'; +export const textareaHeader = 'Textarea'; +export const buttonHeader = 'Button'; +export const checkboxValue = (rowNumber: number) => `checkboxValue${rowNumber}`; +export const checkboxName = (rowNumber: number) => `checkboxName${rowNumber}`; +export const checkboxLabel = (rowNumber: number) => `Checkbox ${rowNumber}`; +export const cleanText = (rowNumber: number) => `Text ${rowNumber}`; +export const textfieldName = (rowNumber: number) => `textfield${rowNumber}`; +export const textfieldLabel = (rowNumber: number) => `Textfield ${rowNumber}`; +export const textareaName = (rowNumber: number) => `textarea${rowNumber}`; +export const textareaLabel = (rowNumber: number) => `Textarea ${rowNumber}`; +export const buttonLabel = (rowNumber: number) => `Button ${rowNumber}`; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts b/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts new file mode 100644 index 00000000000..47be5824190 --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts @@ -0,0 +1,4 @@ +export type CellCoords = { + row: number; + column: number; +}; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/types/HTMLCellInputElement.ts b/frontend/libs/studio-components/src/components/StudioInputTable/types/HTMLCellInputElement.ts new file mode 100644 index 00000000000..94534fe785c --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/types/HTMLCellInputElement.ts @@ -0,0 +1 @@ +export type HTMLCellInputElement = HTMLInputElement | HTMLTextAreaElement | HTMLButtonElement; diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/types/InputCellComponent.ts b/frontend/libs/studio-components/src/components/StudioInputTable/types/InputCellComponent.ts new file mode 100644 index 00000000000..d890c3f9acb --- /dev/null +++ b/frontend/libs/studio-components/src/components/StudioInputTable/types/InputCellComponent.ts @@ -0,0 +1,5 @@ +import type { ForwardRefExoticComponent, PropsWithoutRef, RefAttributes } from 'react'; + +export type InputCellComponent = ForwardRefExoticComponent< + PropsWithoutRef & RefAttributes +>; diff --git a/frontend/libs/studio-components/src/components/index.ts b/frontend/libs/studio-components/src/components/index.ts index 1f46f2df7b5..4b954b136fc 100644 --- a/frontend/libs/studio-components/src/components/index.ts +++ b/frontend/libs/studio-components/src/components/index.ts @@ -20,6 +20,7 @@ export * from './StudioFileUploader'; export * from './StudioGridSelector'; export * from './StudioHeading'; export * from './StudioIconTextfield'; +export * from './StudioInputTable'; export * from './StudioLabelAsParagraph'; export * from './StudioLabelWrapper'; export * from './StudioModal'; From b720cedb315cf6c547cd101bbba6d0efa71ecf7e Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 8 Oct 2024 09:56:27 +0200 Subject: [PATCH 3/3] Remove unused file and fix CSS --- .../components/StudioInputTable/Cell/Cell.module.css | 6 +++++- .../components/StudioInputTable/Cell/CellButton.tsx | 11 ++++++++--- .../components/StudioInputTable/Cell/CellCheckbox.tsx | 9 ++++++--- .../components/StudioInputTable/Cell/CellTextarea.tsx | 11 ++++++++--- .../StudioInputTable/Cell/CellTextfield.tsx | 11 ++++++++--- .../HeaderCell/HeaderCellCheckbox.tsx | 4 ++-- .../components/StudioInputTable/types/CellCoords.ts | 4 ---- 7 files changed, 37 insertions(+), 19 deletions(-) delete mode 100644 frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css index b609bcf72f4..de30179c56d 100644 --- a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/Cell.module.css @@ -4,8 +4,12 @@ font-size: var(--studio-input-table-font-size); } +.buttonCell { + text-align: center; +} + .buttonCell button { - display: flex; + display: inline-flex; } .textfieldCell:not(:hover) input:not(:hover):not(:active):not(:focus), diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx index 612e105dbbb..ea319198782 100644 --- a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellButton.tsx @@ -5,14 +5,19 @@ 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 { - render(props: StudioButtonProps, ref: ForwardedRef): ReactElement { + render( + { className: givenClass, ...rest }: StudioButtonProps, + ref: ForwardedRef, + ): ReactElement { + const className = cn(classes.buttonCell, givenClass); return ( - - + + ); } diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx index bda837f269a..0185dc8ad68 100644 --- a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellCheckbox.tsx @@ -8,10 +8,13 @@ import { BaseInputCell } from './BaseInputCell'; export type CellCheckboxProps = StudioCheckboxProps; export class CellCheckbox extends BaseInputCell { - render(props: CellCheckboxProps, ref: ForwardedRef): ReactElement { + render( + { className, ...rest }: CellCheckboxProps, + ref: ForwardedRef, + ): ReactElement { return ( - - + + ); } diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx index a43ccc1ac7a..f3a79e6cce3 100644 --- a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextarea.tsx @@ -5,14 +5,19 @@ 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 { - render(props: CellTextareaProps, ref: ForwardedRef): ReactElement { + render( + { className: givenClass, ...rest }: CellTextareaProps, + ref: ForwardedRef, + ): ReactElement { + const className = cn(classes.textareaCell, givenClass); return ( - - + + ); } diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx index 1d77fb00938..7e22ecfd555 100644 --- a/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx +++ b/frontend/libs/studio-components/src/components/StudioInputTable/Cell/CellTextfield.tsx @@ -5,14 +5,19 @@ 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 { - render(props: CellTextfieldProps, ref: ForwardedRef): ReactElement { + render( + { className: givenClass, ...rest }: CellTextfieldProps, + ref: ForwardedRef, + ): ReactElement { + const className = cn(classes.textfieldCell, givenClass); return ( - - + + ); } diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx index 061469beab2..3c7890b80bf 100644 --- a/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx +++ b/frontend/libs/studio-components/src/components/StudioInputTable/HeaderCell/HeaderCellCheckbox.tsx @@ -6,11 +6,11 @@ import { StudioCheckbox } from '../../StudioCheckbox'; export type HeaderCellCheckboxProps = Omit & { value?: string }; export const HeaderCellCheckbox = forwardRef( - ({ value: givenValue, ...rest }, ref) => { + ({ value: givenValue, className, ...rest }, ref) => { const defaultValue = useId(); const value = givenValue ?? defaultValue; return ( - + ); diff --git a/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts b/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts deleted file mode 100644 index 47be5824190..00000000000 --- a/frontend/libs/studio-components/src/components/StudioInputTable/types/CellCoords.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type CellCoords = { - row: number; - column: number; -};