Skip to content

Commit

Permalink
Add text resource picker to input table
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasEng committed Nov 29, 2024
1 parent 86fb29c commit 17f66c5
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 116 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.textfieldCell,
.textareaCell {
.textareaCell,
.textResourceCell {
padding: var(--fds-spacing-1) 0;
font-size: var(--studio-input-table-font-size);
}
Expand All @@ -12,8 +13,24 @@
display: inline-flex;
}

.textfieldCell:not(:hover) input:not(:hover):not(:active):not(:focus),
.textareaCell:not(:hover) textarea:not(:hover):not(:active):not(:focus) {
:is(
.textfieldCell:not(:hover) input,
.textareaCell:not(:hover) textarea,
.textResourceCell:not(:hover) input
):not(:hover):not(:active):not(:focus),
.textResourceCell:not(:hover) div:has(input:not(:hover):not(:active):not(:focus)) {
background-color: transparent;
border-color: transparent;
}

.textResourceCell:not(:hover) .textInput div:has(input:not(:hover):not(:active):not(:focus)) svg {
visibility: hidden;
}

.textResourceCell:not(:hover):not(:focus-within) .toggle {
visibility: hidden;
}

.textResourceCell .currentTextId {
padding-inline: var(--fds-spacing-2);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { StudioTable } from '../../StudioTable';
import { type FocusEvent, ForwardedRef, ReactElement, useCallback } from 'react';
import React from 'react';
import { BaseInputCell } from './BaseInputCell';
import {
StudioTextResourceInput,
StudioTextResourceInputProps,
} from '../../StudioTextResourceInput/StudioTextResourceInput';
import cn from 'classnames';
import classes from './Cell.module.css';
import { useEventProps } from './useEventProps';
import { isCaretAtEnd, isCaretAtStart, isSomethingSelected } from '../dom-utils/caretUtils';
import { isCombobox } from '../dom-utils/isCombobox';

export type CellTextResourceInputProps = StudioTextResourceInputProps & {
className?: string;
};

export class CellTextResource extends BaseInputCell<HTMLInputElement, CellTextResourceInputProps> {
render(
{ className: givenClass, onFocus, ...rest }: CellTextResourceInputProps,
ref: ForwardedRef<HTMLInputElement>,
): ReactElement {
const handleFocus = useCallback(
(event: FocusEvent<HTMLInputElement>): void => {
onFocus?.(event);
event.currentTarget.select();
},
[onFocus],
);

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

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

return (
<StudioTable.Cell className={className}>
<StudioTextResourceInput
currentIdClass={classes.currentTextId}
inputClass={classes.textInput}
toggleClass={classes.toggle}
{...rest}
{...eventProps}
ref={ref}
/>
</StudioTable.Cell>
);
}

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

shouldMoveFocusOnEnterKey = () => false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { Cell } from './Cell';
import type { CellCheckboxProps } from './CellCheckbox';
import { CellCheckbox } from './CellCheckbox';
import type { InputCellComponent } from '../types/InputCellComponent';
import { CellTextResource, CellTextResourceInputProps } from './CellTextResource';

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

export const StudioInputTableCell = Cell as CellComponent;
Expand All @@ -22,5 +24,8 @@ StudioInputTableCell.Textfield = new CellTextfield('StudioInputTable.Cell.Textfi
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.TextResource = new CellTextResource(
'StudioInputTable.Cell.TextResource',
).component();

StudioInputTableCell.displayName = 'StudioInputTable.Cell';
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
headerCheckboxLabel,
textareaLabel,
textfieldLabel,
textResourceProps,
textResourceValueLabel,
} from './test-data/testTableData';
import type { UserEvent } from '@testing-library/user-event';
import userEvent from '@testing-library/user-event';
Expand All @@ -27,13 +29,15 @@ import type { EventName } from './types/EventName';
import type { EventProps } from './types/EventProps';
import type { EventPropName } from './types/EventPropName';
import { StringUtils } from '@studio/pure-functions';
import { CellTextResourceInputProps } from './Cell/CellTextResource';

type ElementName = 'checkbox' | 'textfield' | 'textarea' | 'button';
type ElementName = 'checkbox' | 'textfield' | 'textarea' | 'button' | 'textResource';
type NativeElement<Name extends ElementName> = {
checkbox: HTMLInputElement;
textfield: HTMLInputElement;
textarea: HTMLTextAreaElement;
button: HTMLButtonElement;
textResource: HTMLInputElement;
}[Name];

// Test data:
Expand Down Expand Up @@ -108,18 +112,23 @@ describe('StudioInputTable', () => {
expect(getTextfieldInRow(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
expect(getTextResourceValueInRow(2)).toHaveFocus();
await user.keyboard('{ArrowRight}'); // Unselect text in text resource 2
expect(getTextResourceValueInRow(2)).toHaveFocus();
await user.keyboard('{ArrowRight}'); // Move right to button 2
expect(getButtonInRow(2)).toHaveFocus();
await user.keyboard('{ArrowUp}'); // Move up to button 1
expect(getButtonInRow(1)).toHaveFocus();
await user.keyboard('{ArrowLeft}'); // Move left to textarea 1
expect(getTextareaInRow(1)).toHaveFocus();
await user.keyboard('{ArrowLeft}'); // Move left to text resource 1
expect(getTextResourceValueInRow(1)).toHaveFocus();
});

type TextboxTestCase = () => HTMLInputElement | HTMLTextAreaElement;
const textboxTestCases: { [key: string]: TextboxTestCase } = {
textfield: () => getTextfieldInRow(2),
textarea: () => getTextareaInRow(2),
textResource: () => getTextResourceValueInRow(2),
};
type TextboxTestCaseName = keyof typeof textboxTestCases;
const textboxTestCaseNames: TextboxTestCaseName[] = Object.keys(textboxTestCases);
Expand Down Expand Up @@ -147,8 +156,8 @@ describe('StudioInputTable', () => {
render(<TestTable />);
const textbox = textboxTestCases[key]();
await user.type(textbox, 'test');
await user.keyboard('{ArrowRight}'); // Move focus out
await user.keyboard('{ArrowLeft}'); // Move focus back in - now the text should be selected
await user.click(document.body); // Move focus out
await user.click(textbox); // Move focus back in - now the text should be selected
expect(textbox.selectionStart).toBe(0);
expect(textbox.selectionEnd).toBe(4);
});
Expand Down Expand Up @@ -246,6 +255,10 @@ describe('StudioInputTable', () => {
render: (ref) => renderSingleButtonCell({ children: testLabel }, ref),
getElement: () => getButton(testLabel),
},
textResource: {
render: (ref) => renderSingleTextResourceCell(textResourceProps(0), ref),
getElement: () => getTextbox(textResourceValueLabel(0)) as HTMLInputElement,
},
};

test.each(Object.keys(testCases))('%s', (key) => {
Expand Down Expand Up @@ -332,6 +345,23 @@ describe('StudioInputTable', () => {
},
},
},
textResource: {
change: {
render: (onChange) => renderSingleTextResourceCell({ ...textResourceProps(0), onChange }),
action: (user) => user.type(screen.getByRole('textbox'), 'a'),
},
focus: {
render: (onFocus) => renderSingleTextResourceCell({ ...textResourceProps(0), onFocus }),
action: (user) => user.click(screen.getByRole('textbox')),
},
blur: {
render: (onBlur) => renderSingleTextResourceCell({ ...textResourceProps(0), onBlur }),
action: async (user) => {
await user.click(screen.getByRole('textbox'));
await user.tab();
},
},
},
};

describe.each(Object.keys(testCases))('%s', (key) => {
Expand Down Expand Up @@ -401,6 +431,16 @@ const renderSingleCheckboxCell = (
</SingleRow>,
);

const renderSingleTextResourceCell = (
props: CellTextResourceInputProps,
ref?: ForwardedRef<HTMLInputElement>,
): RenderResult =>
render(
<SingleRow>
<StudioInputTable.Cell.TextResource {...props} ref={ref} />
</SingleRow>,
);

const getTable = (): HTMLTableElement => screen.getByRole('table');
const getCheckbox = (name: string): HTMLInputElement =>
screen.getByRole('checkbox', { name }) as HTMLInputElement;
Expand All @@ -415,6 +455,8 @@ const getButton = (name: string): HTMLButtonElement =>
screen.getByRole('button', { name }) as HTMLButtonElement;
const getButtonInRow = (rowNumber: number): HTMLButtonElement =>
getButton(buttonLabel(rowNumber)) as HTMLButtonElement;
const getTextResourceValueInRow = (rowNumber: number): HTMLInputElement =>
getTextbox(textResourceValueLabel(rowNumber)) as HTMLInputElement;

const expectCaretPosition = (
element: HTMLInputElement | HTMLTextAreaElement,
Expand All @@ -438,7 +480,7 @@ const placeCaretAtPosition = (
position: number,
): void => element.setSelectionRange(position, position);

const expectedNumberOfColumns = 5;
const expectedNumberOfColumns = 6;
const expectedNumberOfHeaderRows = 1;
const expectedNumberOfBodyRows = 3;
const expectedNumberOfRows = expectedNumberOfBodyRows + expectedNumberOfHeaderRows;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { isCombobox } from './isCombobox';
import { render, screen } from '@testing-library/react';
import React from 'react';

describe('isCombobox', () => {
it('Returns true when the element is a combobox', () => {
render(<input type='text' role='combobox' />);
const element = screen.getByRole('combobox');
expect(isCombobox(element)).toBe(true);

Check failure on line 9 in frontend/libs/studio-components/src/components/StudioInputTable/dom-utils/isCombobox.test.tsx

View workflow job for this annotation

GitHub Actions / Typechecking and linting

Argument of type 'HTMLElement' is not assignable to parameter of type 'HTMLInputElement'.
});

it('Returns false when the element is not a combobox', () => {
render(<input type='text' />);
const element = screen.getByRole('textbox');
expect(isCombobox(element)).toBe(false);

Check failure on line 15 in frontend/libs/studio-components/src/components/StudioInputTable/dom-utils/isCombobox.test.tsx

View workflow job for this annotation

GitHub Actions / Typechecking and linting

Argument of type 'HTMLElement' is not assignable to parameter of type 'HTMLInputElement'.
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isCombobox(element: HTMLInputElement): boolean {
return element.getAttribute('role') === 'combobox';
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
textareaHeader,
textfieldHeader,
textHeader,
textResourceHeader,
} from './testTableData';
import { textResourcesMock } from '../../../test-data/textResourcesMock';

export function TestTable(props: StudioInputTableProps): ReactElement {
return (
Expand All @@ -21,6 +23,7 @@ export function TestTable(props: StudioInputTableProps): ReactElement {
<StudioInputTable.HeaderCell>{textfieldHeader}</StudioInputTable.HeaderCell>
<StudioInputTable.HeaderCell>{textareaHeader}</StudioInputTable.HeaderCell>
<StudioInputTable.HeaderCell>{buttonHeader}</StudioInputTable.HeaderCell>
<StudioInputTable.HeaderCell>{textResourceHeader}</StudioInputTable.HeaderCell>
</StudioInputTable.Row>
</StudioInputTable.Head>
<StudioInputTable.Body>
Expand Down Expand Up @@ -53,6 +56,20 @@ function TestRow({ rowNumber: rn }: TestRowProps): ReactElement {
name={testData.textareaName(rn)}
label={testData.textareaLabel(rn)}
/>
<StudioInputTable.Cell.TextResource
textResources={textResourcesMock}
currentId='land.NO'
onChangeCurrentId={() => {}}
onChangeTextResource={() => {}}
texts={{
editValue: 'Rediger verdi',
emptyResourceList: 'Fant ingen tekstressurser',
idLabel: 'ID:',
search: 'Søk',
textResourcePickerLabel: testData.textResourcePickerLabel(rn),
valueLabel: testData.textResourceValueLabel(rn),
}}
/>
<StudioInputTable.Cell.Button>{testData.buttonLabel(rn)}</StudioInputTable.Cell.Button>
</StudioInputTable.Row>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { CellTextResourceInputProps } from '../Cell/CellTextResource';
import { TextResourceInputTexts } from '../../StudioTextResourceInput/types/TextResourceInputTexts';
import { textResourcesMock } from '../../../test-data/textResourcesMock';

export const headerCheckboxLabel = 'Select all';
export const textHeader = 'Text';
export const textfieldHeader = 'Textfield';
export const textareaHeader = 'Textarea';
export const buttonHeader = 'Button';
export const textResourceHeader = 'Text Resource';
export const checkboxValue = (rowNumber: number) => `checkboxValue${rowNumber}`;
export const checkboxName = (rowNumber: number) => `checkboxName${rowNumber}`;
export const checkboxLabel = (rowNumber: number) => `Checkbox ${rowNumber}`;
Expand All @@ -12,3 +17,22 @@ 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}`;
export const textResourcePickerLabel = (rowNumber: number) => `Text resource ${rowNumber}`;
export const textResourceValueLabel = (rowNumber: number) => `Text value ${rowNumber}`;

export const textResourceProps = (rowNumber: number): CellTextResourceInputProps => ({
textResources: textResourcesMock,
texts: textResourceTexts(rowNumber),
currentId: 'land.NO',
onChangeCurrentId: jest.fn(),
onChangeTextResource: jest.fn(),
});

const textResourceTexts = (rowNumber: number): TextResourceInputTexts => ({
editValue: 'Rediger verdi',
emptyResourceList: 'Fant ingen tekstressurser',
idLabel: 'ID:',
search: 'Søk',
textResourcePickerLabel: textResourcePickerLabel(rowNumber),
valueLabel: textResourceValueLabel(rowNumber),
});
Loading

0 comments on commit 17f66c5

Please sign in to comment.