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: add CellNumberfield to StudioInputTable #14345

Merged
merged 14 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import type { Override } from '../../types/Override';
export type StudioDecimalInputProps = Override<
{
description?: string;
onChange: (value: number) => void;
onChange?: (value: number) => void;
value?: number;
validationErrorMessage: string;
validationErrorMessage?: string;
ErlingHauan marked this conversation as resolved.
Show resolved Hide resolved
},
StudioTextfieldProps
>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.textfieldCell,
.textareaCell,
.textResourceCell {
.textResourceCell,
.numberCell {
padding-block: var(--fds-spacing-1);
padding-inline-end: var(--fds-spacing-2);
padding-inline-start: 0;
Expand All @@ -17,6 +18,7 @@

:is(
.textfieldCell:not(:hover) input,
.numberfieldCell:not(:hover) input,
.textareaCell:not(:hover) textarea,
.textResourceCell:not(:hover) input
):not(:hover):not(:active):not(:focus),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { StudioTable } from '../../StudioTable';
import type { FocusEvent, ForwardedRef, ReactElement } from 'react';
import React, { useCallback } from 'react';
import classes from './Cell.module.css';
import { BaseInputCell } from './BaseInputCell';
import cn from 'classnames';
import { isCaretAtEnd, isCaretAtStart, isSomethingSelected } from '../dom-utils/caretUtils';
import type { StudioDecimalInputProps } from '../../StudioDecimalInput';
import { StudioDecimalInput } from '../../StudioDecimalInput';
import { useEventProps } from './useEventProps';

export type CellNumberfieldProps = StudioDecimalInputProps;

export class CellNumberfield extends BaseInputCell<HTMLInputElement, CellNumberfieldProps> {
render(
{ className: givenClass, onFocus, ...rest }: CellNumberfieldProps,
ref: ForwardedRef<HTMLInputElement>,
): ReactElement {
/* eslint-disable react-hooks/rules-of-hooks */
/* Eslint misinterprets this as a class component, while it's really just a functional component within a class */

const handleFocus = useCallback(
(event: FocusEvent<HTMLInputElement>): void => {
onFocus?.(event);
event.currentTarget.select();
},
[onFocus],
);

const eventProps = useEventProps<FocusEvent, FocusEvent, number>({
onFocus: handleFocus,
...rest,
});

const className = cn(classes.numberfieldCell, givenClass);

return (
<StudioTable.Cell className={className}>
<StudioDecimalInput hideLabel ref={ref} size='small' {...rest} {...eventProps} />
</StudioTable.Cell>
);
}

shouldMoveFocusOnArrowKey({ key, currentTarget }) {
if (isSomethingSelected(currentTarget)) return false;
switch (key) {
case 'ArrowUp':
return isCaretAtStart(currentTarget);
case 'ArrowDown':
return isCaretAtEnd(currentTarget);
case 'ArrowLeft':
return isCaretAtStart(currentTarget);
case 'ArrowRight':
return isCaretAtEnd(currentTarget);
}
}

shouldMoveFocusOnEnterKey = () => true;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { CellTextfieldProps } from './CellTextfield';
import { CellTextfield } from './CellTextfield';
import type { CellNumberfieldProps } from './CellNumberfield';
import { CellNumberfield } from './CellNumberfield';
import type { CellTextareaProps } from './CellTextarea';
import { CellTextarea } from './CellTextarea';
import type { CellButtonProps } from './CellButton';
Expand All @@ -13,6 +15,7 @@ import { CellTextResource } from './CellTextResource';

type CellComponent = typeof Cell & {
Textfield: InputCellComponent<CellTextfieldProps, HTMLInputElement>;
Numberfield: InputCellComponent<CellNumberfieldProps, HTMLInputElement>;
Textarea: InputCellComponent<CellTextareaProps, HTMLTextAreaElement>;
Button: InputCellComponent<CellButtonProps, HTMLButtonElement>;
Checkbox: InputCellComponent<CellCheckboxProps, HTMLInputElement>;
Expand All @@ -22,6 +25,9 @@ type CellComponent = typeof Cell & {
export const StudioInputTableCell = Cell as CellComponent;

StudioInputTableCell.Textfield = new CellTextfield('StudioInputTable.Cell.Textfield').component();
StudioInputTableCell.Numberfield = new CellNumberfield(
'StudioInputTable.Cell.Numberfield',
).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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,33 @@ import {
textResourceProps,
textResourceSearchLabel,
textResourceValueLabel,
numberfieldLabel,
} from './test-data/testTableData';
import type { UserEvent } from '@testing-library/user-event';
import userEvent from '@testing-library/user-event';
import type { CellTextfieldProps } from './Cell/CellTextfield';
import type { CellTextareaProps } from './Cell/CellTextarea';
import type { CellCheckboxProps } from './Cell/CellCheckbox';
import type { CellButtonProps } from './Cell/CellButton';
import type { CellNumberfieldProps } from './Cell/CellNumberfield';
import type { HTMLCellInputElement } from './types/HTMLCellInputElement';
import type { EventName } from './types/EventName';
import type { FormEventProps } from './types/FormEventProps';
import type { EventPropName } from './types/EventPropName';
import { StringUtils } from '@studio/pure-functions';
import type { CellTextResourceInputProps } from './Cell/CellTextResource';

type ElementName = 'checkbox' | 'textfield' | 'textarea' | 'button' | 'textResource';
type ElementName =
| 'checkbox'
| 'textfield'
| 'numberfield'
| 'textarea'
| 'button'
| 'textResource';
type NativeElement<Name extends ElementName> = {
checkbox: HTMLInputElement;
textfield: HTMLInputElement;
numberfield: HTMLInputElement;
textarea: HTMLTextAreaElement;
button: HTMLButtonElement;
textResource: HTMLInputElement;
Expand Down Expand Up @@ -112,6 +121,8 @@ describe('StudioInputTable', () => {
expect(getTextfieldInRow(1)).toHaveFocus();
await user.keyboard('{Enter}'); // Move down to textfield 2
expect(getTextfieldInRow(2)).toHaveFocus();
await user.keyboard('{ArrowRight}'); // Move right to numberfield 2
expect(getNumberfieldInRow(2)).toHaveFocus();
await user.keyboard('{ArrowRight}'); // Move right to textarea 2
expect(getTextareaInRow(2)).toHaveFocus();
await user.keyboard('{ArrowRight}'); // Move right to text resource 2
Expand All @@ -129,6 +140,7 @@ describe('StudioInputTable', () => {
type TextboxTestCase = () => HTMLInputElement | HTMLTextAreaElement;
const textboxTestCases: { [key: string]: TextboxTestCase } = {
textfield: () => getTextfieldInRow(2),
numberfield: () => getNumberfieldInRow(2),
textarea: () => getTextareaInRow(2),
textResource: () => getTextResourceValueInRow(2),
};
Expand Down Expand Up @@ -276,6 +288,10 @@ describe('StudioInputTable', () => {
render: (ref) => renderSingleTextResourceCell(textResourceProps(0), ref),
getElement: () => getTextbox(textResourceValueLabel(0)) as HTMLInputElement,
},
numberfield: {
render: (ref) => renderSingleNumberfieldCell({ label: testLabel }, ref),
getElement: () => getTextbox(testLabel) as HTMLInputElement,
},
};

test.each(Object.keys(testCases))('%s', (key) => {
Expand Down Expand Up @@ -312,6 +328,27 @@ describe('StudioInputTable', () => {
},
},
},
numberfield: {
change: {
render: (onChange) =>
renderSingleNumberfieldCell({
label: 'test',
onChange: (value: number) => onChange({ target: { value } } as any),
}),
action: (user) => user.type(screen.getByRole('textbox'), '1'),
},
focus: {
render: (onFocus) => renderSingleNumberfieldCell({ label: 'test', onFocus }),
action: (user) => user.click(screen.getByRole('textbox')),
},
blur: {
render: (onBlur) => renderSingleNumberfieldCell({ label: 'test', onBlur }),
action: async (user) => {
await user.click(screen.getByRole('textbox'));
await user.tab();
},
},
},
textarea: {
change: {
render: (onChange) => renderSingleTextareaCell({ label: 'test', onChange }),
Expand Down Expand Up @@ -432,6 +469,16 @@ const renderSingleTextfieldCell = (
</SingleRow>,
);

const renderSingleNumberfieldCell = (
props: CellNumberfieldProps,
ref?: ForwardedRef<HTMLInputElement>,
): RenderResult =>
render(
<SingleRow>
<StudioInputTable.Cell.Numberfield {...props} ref={ref} />
</SingleRow>,
);

const renderSingleTextareaCell = (
props: CellTextareaProps,
ref?: ForwardedRef<HTMLTextAreaElement>,
Expand Down Expand Up @@ -480,6 +527,8 @@ const getCheckboxInRow = (rowNumber: number): HTMLInputElement =>
const getTextbox = (name: string) => screen.getByRole('textbox', { name });
const getTextfieldInRow = (rowNumber: number): HTMLInputElement =>
getTextbox(textfieldLabel(rowNumber)) as HTMLInputElement;
const getNumberfieldInRow = (rowNumber: number): HTMLInputElement =>
getTextbox(numberfieldLabel(rowNumber)) as HTMLInputElement;
const getTextareaInRow = (rowNumber: number): HTMLTextAreaElement =>
getTextbox(textareaLabel(rowNumber)) as HTMLTextAreaElement;
const getButton = (name: string): HTMLButtonElement =>
Expand Down Expand Up @@ -515,7 +564,7 @@ const placeCaretAtPosition = (
position: number,
): void => element.setSelectionRange(position, position);

const expectedNumberOfColumns = 6;
const expectedNumberOfColumns = 7;
const expectedNumberOfHeaderRows = 1;
const expectedNumberOfBodyRows = 3;
const expectedNumberOfRows = expectedNumberOfBodyRows + expectedNumberOfHeaderRows;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import type { StudioInputTableProps } from '../StudioInputTable';
import {
buttonHeader,
headerCheckboxLabel,
textareaHeader,
textfieldHeader,
numberfieldHeader,
textareaHeader,
textHeader,
textResourceHeader,
textResourceTexts,
Expand All @@ -22,6 +23,7 @@ export function TestTable(props: StudioInputTableProps): ReactElement {
<StudioInputTable.HeaderCell.Checkbox aria-label={headerCheckboxLabel} />
<StudioInputTable.HeaderCell>{textHeader}</StudioInputTable.HeaderCell>
<StudioInputTable.HeaderCell>{textfieldHeader}</StudioInputTable.HeaderCell>
<StudioInputTable.HeaderCell>{numberfieldHeader}</StudioInputTable.HeaderCell>
<StudioInputTable.HeaderCell>{textareaHeader}</StudioInputTable.HeaderCell>
<StudioInputTable.HeaderCell>{textResourceHeader}</StudioInputTable.HeaderCell>
<StudioInputTable.HeaderCell>{buttonHeader}</StudioInputTable.HeaderCell>
Expand Down Expand Up @@ -53,6 +55,10 @@ function TestRow({ rowNumber: rn }: TestRowProps): ReactElement {
name={testData.textfieldName(rn)}
label={testData.textfieldLabel(rn)}
/>
<StudioInputTable.Cell.Numberfield
name={testData.numberfieldName(rn)}
label={testData.numberfieldLabel(rn)}
/>
<StudioInputTable.Cell.Textarea
name={testData.textareaName(rn)}
label={testData.textareaLabel(rn)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { textResourcesMock } from '../../../test-data/textResourcesMock';
export const headerCheckboxLabel = 'Select all';
export const textHeader = 'Text';
export const textfieldHeader = 'Textfield';
export const numberfieldHeader = 'Numberfield';
export const textareaHeader = 'Textarea';
export const buttonHeader = 'Button';
export const textResourceHeader = 'Text Resource';
Expand All @@ -14,6 +15,8 @@ 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 numberfieldName = (rowNumber: number) => `numberfield${rowNumber}`;
export const numberfieldLabel = (rowNumber: number) => `Numberfield ${rowNumber}`;
export const textareaName = (rowNumber: number) => `textarea${rowNumber}`;
export const textareaLabel = (rowNumber: number) => `Textarea ${rowNumber}`;
export const buttonLabel = (rowNumber: number) => `Button ${rowNumber}`;
Expand Down
Loading