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(StudioToggleableTextfield): Create components to handle toggleable textfields and validation #12214

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5141c02
update EditFormComponent --WIP
JamalAlabdullah Jan 30, 2024
eed3332
update use-cases yml
nkylstad Jan 30, 2024
999878d
added test for EditComponentId
JamalAlabdullah Jan 30, 2024
7e74d71
Merge remote-tracking branch 'origin' into 12181-update-editcomponent…
JamalAlabdullah Jan 30, 2024
c4e2a9d
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Jan 30, 2024
2e2a1c8
updated test
JamalAlabdullah Jan 30, 2024
2589610
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Jan 31, 2024
d9c906f
remove unised variable
JamalAlabdullah Jan 31, 2024
4c8d637
updated test in EditFormComponent
JamalAlabdullah Jan 31, 2024
0f9605f
added one more test to EditComponentId
JamalAlabdullah Jan 31, 2024
f9c45f0
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Feb 1, 2024
b8a9769
Added necessary components --WIP
JamalAlabdullah Feb 2, 2024
9dd4b22
added icon inside textfield
JamalAlabdullah Feb 2, 2024
0371eb2
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Feb 7, 2024
a45bf7f
added test --WIP
JamalAlabdullah Feb 7, 2024
612fc05
added custum error message
JamalAlabdullah Feb 8, 2024
fc6c353
fixed helpText bug and added schema validation --WIP
JamalAlabdullah Feb 12, 2024
56b55a8
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Feb 12, 2024
6da6999
fixed merge conflict
JamalAlabdullah Feb 12, 2024
7f47daa
fixed css and conflict merge
JamalAlabdullah Feb 13, 2024
6f0991f
fixed a prt of comments
JamalAlabdullah Feb 13, 2024
627da32
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Feb 14, 2024
b6214a6
added som tests and refactore StudioSchemutils --WIP
JamalAlabdullah Feb 14, 2024
9c020a0
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Feb 14, 2024
b967e77
added test --WIP
JamalAlabdullah Feb 14, 2024
9aaa263
added tests and fixed comments --WIP
JamalAlabdullah Feb 16, 2024
edff4db
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Feb 16, 2024
f8ba166
Added test
JamalAlabdullah Feb 16, 2024
6885d71
Merge remote-tracking branch 'origin/main' into 12181-update-editcomp…
JamalAlabdullah Feb 20, 2024
bf71b55
fixed comments --WIP
JamalAlabdullah Feb 21, 2024
4841d1c
Merge branch 'main' into 12181-update-editcomponentid-component-to-ma…
JamalAlabdullah Feb 26, 2024
6a77ba1
fixed merge conflict
JamalAlabdullah Feb 26, 2024
7c79a0b
refactor: move files, fix unit tests, a11y improvements, bugfixes
framitdavid Feb 27, 2024
84b6488
Install and move ajv into studio-components
framitdavid Feb 27, 2024
fc89625
merge main into feature
framitdavid Feb 28, 2024
9ae9d09
Clean up and passing schemas to StudioTextfieldSchema
framitdavid Feb 28, 2024
0147e79
UI improvements, moved CSS closer to the component that should own it
framitdavid Feb 29, 2024
82bf80f
added more tests
framitdavid Feb 29, 2024
5a41a83
removed auto-imported import
framitdavid Feb 29, 2024
03a0989
added one more test to ensure that edit field does not toggle to view…
framitdavid Feb 29, 2024
7178272
merge main into feature
framitdavid Feb 29, 2024
30335b1
revert
framitdavid Feb 29, 2024
8b035b6
renamed components for consistancy
framitdavid Feb 29, 2024
9b524b7
removed export from a variable that should not be exported
framitdavid Feb 29, 2024
853f7d8
PR feedback, naming, forwardRef and removed unused dependency
framitdavid Mar 1, 2024
4f23b03
updated yarn.lock
framitdavid Mar 1, 2024
f0d6f31
Merge branch 'main' into 12181-update-editcomponentid-component-to-ma…
framitdavid Mar 1, 2024
56a367a
improved label for component id
framitdavid Mar 1, 2024
0fb82d9
removed unused import
framitdavid Mar 1, 2024
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
5 changes: 3 additions & 2 deletions frontend/app-development/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ const isLatestFrontendVersion = (version: AppVersion): boolean =>

const UiEditor = () => {
const { org, app } = useStudioUrlParams();
const { data } = useAppVersionQuery(org, app);
return isLatestFrontendVersion(data) ? <UiEditorLatest /> : <UiEditorV3 />;
const { data: version } = useAppVersionQuery(org, app);
if (!version) return null;
return isLatestFrontendVersion(version) ? <UiEditorLatest /> : <UiEditorV3 />;
};

export const routerRoutes: RouterRoute[] = [
Expand Down
3 changes: 2 additions & 1 deletion frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1563,7 +1563,8 @@
"ux_editor.component_unknown": "Ukjent komponent",
"ux_editor.conditional_rendering_connection_header": "Betingede renderingstilkoblinger",
"ux_editor.container_empty": "Tomt, dra noe inn her...",
"ux_editor.container_not_editable_info": "Noen egenskaper for denne komponenten er ikke redigerbare for øyeblikket. Du kan legge til underkomponenter i kolonnen til venstre og redigere tekster.",
"ux_editor.container_not_editable_info": "Noen egenskaper for denne komponenten er ikke redigerbare for øyeblikket. Du kan legge til underkomponenter i kolonnen til venstre.",
"ux_editor.edit_component.id_help_text": "The component ID. Must be unique within all layouts/pages in a layout-set. Cannot end with <dash><number>.",
"ux_editor.edit_component.loading_schema": "Laster inn skjema",
"ux_editor.edit_component.show_beta_func": "Vis ny konfigurasjon (BETA)",
"ux_editor.edit_component.show_beta_func_help_text": "Vi jobber med å få på plass støtte for å redigere alle innstillinger. Ved å huke av her kan du ta i bruk den nye konfigurasjonsvisningen, som støtter flere innstillinger. Merk at denne visningen fortsatt er under utvikling, og vil kunne oppleves som noe ustabil.",
Expand Down
1 change: 1 addition & 0 deletions frontend/libs/studio-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"dependencies": {
"@studio/icons": "^0.1.0",
"ajv": "8.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { RefObject } from 'react';
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import React, {
forwardRef,
useCallback,
useEffect,
useMemo,
useState,
type RefObject,
} from 'react';
import { convertNumberToString, convertStringToNumber, isStringValidDecimalNumber } from './utils';
import type { StudioTextfieldProps } from '../StudioTextfield';
import { StudioTextfield } from '../StudioTextfield';
import { type StudioTextfieldProps, StudioTextfield } from '../StudioTextfield';

export interface StudioDecimalInputProps extends Omit<StudioTextfieldProps, 'onChange'> {
description: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.container {
display: flex;
gap: var(--fds-spacing-2);
}

.prefixIcon {
color: var(--fds-semantic-text-neutral-default);
margin-top: var(--fds-spacing-7);
font-size: var(--fds-sizing-6);
}

.textfield {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import { StudioIconTextfield } from './StudioIconTextfield';
import type { StudioIconTextfieldProps } from './StudioIconTextfield';
import { KeyVerticalIcon } from '@navikt/aksel-icons';
import userEvent from '@testing-library/user-event';

describe('StudioIconTextfield', () => {
it('render the icon', async () => {
renderStudioIconTextfield({
icon: <KeyVerticalIcon title='my key icon title' />,
});
expect(screen.getByTitle('my key icon title')).toBeInTheDocument();
});

it('should render label', () => {
renderStudioIconTextfield({
icon: <div />,
label: 'id',
});
expect(screen.getByLabelText('id')).toBeInTheDocument();
});

it('should execute onChange callback when input value changes', async () => {
const user = userEvent.setup();
const onChangeMock = jest.fn();

renderStudioIconTextfield({
icon: <div />,
label: 'Your ID',
onChange: onChangeMock,
});

const input = screen.getByLabelText('Your ID');

const inputValue = 'my id is 123';
await act(() => user.type(input, inputValue));
expect(onChangeMock).toHaveBeenCalledTimes(inputValue.length);
});

it('should forward the rest of the props to the input', () => {
renderStudioIconTextfield({
icon: <div />,
label: 'Your ID',
disabled: true,
});
expect(screen.getByLabelText('Your ID')).toBeDisabled();
});
});
const renderStudioIconTextfield = (props: StudioIconTextfieldProps) => {
return render(<StudioIconTextfield {...props} />);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { forwardRef } from 'react';
import { StudioTextfield, type StudioTextfieldProps } from '../StudioTextfield';
import cn from 'classnames';

import classes from './StudioIconTextfield.module.css';

export type StudioIconTextfieldProps = {
icon: React.ReactNode;
} & StudioTextfieldProps;

export const StudioIconTextfield = forwardRef<HTMLDivElement, StudioIconTextfieldProps>(
(
{ icon, className: givenClassName, ...rest }: StudioIconTextfieldProps,
ref,
): React.ReactElement => {
const className = cn(givenClassName, classes.textfield);
return (
<div className={classes.container} ref={ref}>
<div aria-hidden className={classes.prefixIcon}>
{icon}
</div>
<StudioTextfield {...rest} className={className} />
</div>
);
},
);

StudioIconTextfield.displayName = 'StudioIconTextfield';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { StudioIconTextfield, type StudioIconTextfieldProps } from './StudioIconTextfield';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Textfield } from '@digdir/design-system-react';
import React, { forwardRef } from 'react';
import type { SharedTextInputProps } from '../../types/SharedTextInputProps';
import { Textfield } from '@digdir/design-system-react';
import { type SharedTextInputProps } from '../../types/SharedTextInputProps';
import { useTextInputProps } from '../../hooks/useTextInputProps';

export type StudioTextfieldProps = SharedTextInputProps<HTMLInputElement>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { StudioTextfield } from './StudioTextfield';
export type { StudioTextfieldProps } from './StudioTextfield';
export { StudioTextfield, type StudioTextfieldProps } from './StudioTextfield';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.viewModeIconsContainer {
display: flex;
align-items: center;
color: var(--fds-semantic-text-neutral-default);
}

.editIcon {
margin-left: auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import { StudioTextfieldToggleView } from './StudioTextfieldToggleView';
import type { StudioTextfieldToggleViewProps } from './StudioTextfieldToggleView';
import userEvent from '@testing-library/user-event';

describe('StudioTextfieldToggleView', () => {
it('should render button text', () => {
renderStudioTextfieldToggleView({ children: 'My awesome button' });
expect(screen.getByRole('button', { name: 'My awesome button' })).toBeInTheDocument();
});

it('should execute the "onClick" method when button is clicked', async () => {
const user = userEvent.setup();
const onClickMock = jest.fn();

renderStudioTextfieldToggleView({ children: 'My awesome button text', onClick: onClickMock });

await act(() => user.click(screen.getByRole('button', { name: 'My awesome button text' })));
expect(onClickMock).toHaveBeenCalledTimes(1);
});

it('should render the KeyVerticalIcon', () => {
renderStudioTextfieldToggleView({ children: 'My awesome button text' });

// Uses testId to find the KeyVerticalIcon, since it's not available for screen reader.
expect(screen.getByTestId('keyIcon')).toBeInTheDocument();
});

it('should render the PencilIcon', () => {
renderStudioTextfieldToggleView({ children: 'My awesome button text' });

// Uses testId to find the EditIcon, since it's not available for screen reader.
expect(screen.getByTestId('editIcon')).toBeInTheDocument();
});

it('should forward the rest of the props to the button', () => {
renderStudioTextfieldToggleView({ children: 'My awesome button text', disabled: true });
expect(screen.getByRole('button', { name: 'My awesome button text' })).toBeDisabled();
});
});

const renderStudioTextfieldToggleView = (props: Partial<StudioTextfieldToggleViewProps>) => {
return render(<StudioTextfieldToggleView {...props} />);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { PencilIcon, KeyVerticalIcon } from '@studio/icons';
import { StudioButton, type StudioButtonProps } from '@studio/components';
import classes from './StudioTextfieldToggleView.module.css';

export type StudioTextfieldToggleViewProps = StudioButtonProps;

export const StudioTextfieldToggleView = ({
framitdavid marked this conversation as resolved.
Show resolved Hide resolved
onClick,
children,
...rest
}: StudioTextfieldToggleViewProps) => {
return (
<StudioButton {...rest} onClick={onClick}>
<span className={classes.viewModeIconsContainer}>
<KeyVerticalIcon data-testid='keyIcon' aria-hidden />
{children}
</span>
<PencilIcon className={classes.editIcon} data-testid='editIcon' aria-hidden />
</StudioButton>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
StudioTextfieldToggleView,
type StudioTextfieldToggleViewProps,
} from './StudioTextfieldToggleView';
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from 'react';
import { act, render, screen, fireEvent } from '@testing-library/react';
import {
StudioToggleableTextfield,
type StudioToggleableTextfieldProps,
} from './StudioToggleableTextfield';

import userEvent from '@testing-library/user-event';

describe('StudioToggleableTextfield', () => {
it('Renders the view mode by default', () => {
renderStudioTextField({
viewProps: { children: 'Edit binding' },
});
expect(screen.getByRole('button', { name: 'Edit binding' })).toBeInTheDocument();
});

it('should toggle to edit-mode when edit button is clicked', async () => {
const user = userEvent.setup();
renderStudioTextField({
viewProps: { children: 'Edit name' },
inputProps: { value: '', icon: <div />, label: 'Your name' },
});
await act(() => user.click(screen.getByRole('button', { name: 'Edit name' })));
expect(screen.getByLabelText('Your name')).toBeEnabled();
});

it('should run custom validation when value changes', async () => {
const customValidation = jest.fn();
const user = userEvent.setup();
renderStudioTextField({
viewProps: { children: 'Edit name' },
inputProps: { value: '', label: 'Your name', icon: <div /> },
customValidation,
});
await act(() => user.click(screen.getByRole('button', { name: 'Edit name' })));

const typedInputValue = 'John';
await act(() => user.type(screen.getByLabelText('Your name'), typedInputValue));

expect(customValidation).toHaveBeenCalledTimes(typedInputValue.length);
});

it('should be toggle back to view mode on blur', async () => {
const user = userEvent.setup();

renderStudioTextField({
viewProps: { children: 'edit' },
inputProps: { value: 'value', label: 'Your name', icon: <div /> },
});

await act(() => user.click(screen.getByRole('button', { name: 'edit' })));
expect(screen.getByLabelText('Your name')).toBeEnabled();
expect(screen.queryByRole('button', { name: 'edit' })).not.toBeInTheDocument();

fireEvent.blur(screen.getByLabelText('Your name'));
await screen.findByRole('button', { name: 'edit' });
});

it('should execute onBlur method when input is blurred', async () => {
const onBlurMock = jest.fn();
const user = userEvent.setup();
renderStudioTextField({
viewProps: { children: 'Edit name' },
inputProps: { onBlur: onBlurMock, label: 'Your name', icon: <div /> },
});

await act(() => user.click(screen.getByRole('button', { name: 'Edit name' })));
fireEvent.blur(screen.getByLabelText('Your name'));
expect(onBlurMock).toHaveBeenCalledTimes(1);
});

it('should not toggle view on blur when input field has error', async () => {
const user = userEvent.setup();

renderStudioTextField({
viewProps: { children: 'Edit your name' },
inputProps: { label: 'Your name', icon: <div />, error: 'Your name is a required field' },
});

await act(() => user.click(screen.getByRole('button', { name: 'Edit your name' })));

const inputField = screen.getByLabelText('Your name');
fireEvent.blur(inputField);

expect(inputField).toHaveAttribute('aria-invalid', 'true');
expect(screen.getByText('Your name is a required field')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Edit your name' })).not.toBeInTheDocument();
});

it('should execute onChange method when input value changes', async () => {
const onChangeMock = jest.fn();
const user = userEvent.setup();

renderStudioTextField({
viewProps: { children: 'edit' },
inputProps: { onChange: onChangeMock, label: 'Your name', icon: <div /> },
});

const inputValue = 'John';
await act(() => user.click(screen.getByRole('button', { name: 'edit' })));
await act(() => user.type(screen.getByLabelText('Your name'), inputValue));

expect(onChangeMock).toHaveBeenCalledTimes(inputValue.length);
});

it('should render error message if customValidation occured', async () => {
const user = userEvent.setup();

renderStudioTextField({
viewProps: { children: 'Edit name' },
inputProps: { label: 'Your name', icon: <div /> },
customValidation: (value: string) =>
value === 'test' ? 'Your name cannot be "test"' : undefined,
});

await act(() => user.click(screen.getByRole('button', { name: 'Edit name' })));
await act(() => user.type(screen.getByLabelText('Your name'), 'test'));
expect(screen.getByText('Your name cannot be "test"'));
});
});

const renderStudioTextField = (props: Partial<StudioToggleableTextfieldProps>) => {
const defaultProps: StudioToggleableTextfieldProps = {
inputProps: {
value: 'value',
icon: <div />,
},
viewProps: {
children: 'edit',
},
customValidation: jest.fn(),
};
return render(<StudioToggleableTextfield {...defaultProps} {...props} />);
};
Loading
Loading