Skip to content

Commit

Permalink
feat(ui): input text repasse technique et tests manquant (#2110)
Browse files Browse the repository at this point in the history
* feat(UI): création d'un input

* doc: Ajoute une story

* feat: Ajoute du style sur l'Input

* feat(UI): ajout de usetouched

* feat(UI): au mounted check validation

* fix: Fix le type de validation dans la story

* refacto(input): retours review finale

---------

Co-authored-by: Gauthier Fiorentino <[email protected]>
Co-authored-by: Suxue LI <[email protected]>
  • Loading branch information
3 people authored Oct 30, 2023
1 parent a079e77 commit cbbd6fb
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 34 deletions.
37 changes: 19 additions & 18 deletions src/client/components/ui/Form/Combobox/Combobox.module.scss
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
@use "@styles/utilities-deprecated";
@use "@styles/utilities";
@use "../InputText/Input.module" as inputStyles;

.combobox {
// TODO (GAFI 27-06-2023): mettre en commun le style avec le composant d'input et de select

$color-text: utilities.$color-text-primary;
$color-border: utilities.$color-background-border;
$color-text-disabled: inherit;
$color-background-disabled: utilities.$color-background-disabled;
$color-error: utilities.$color-error;
$color-text: inputStyles.$color-text;
$color-border: inputStyles.$color-border;
$color-text-disabled: inputStyles.$color-text-disabled;
$error-border-width: inputStyles.$error-border-width;
$color-background-disabled: inputStyles.$color-background-disabled;
$color-error: inputStyles.$color-error;
$border-width-compensation: inputStyles.$border-width-compensation;
$color-list-border: $color-border;
$color-list-background: utilities.$color-background-primary;
$color-option-hover: utilities.$color-background-primary-alternative;
$color-category-separator: $color-border;
$border-radius: inputStyles.$border-radius;
$border-width: inputStyles.$border-width;

position: relative;
display: grid;
Expand All @@ -34,8 +38,7 @@
}
}

$border-radius: 1.25rem;
$border-width: 1px;

border-radius: $border-radius;
@extend %outlined;
& input,
Expand All @@ -62,7 +65,6 @@
cursor: not-allowed;
}

$error-border-width: 2px;
& input[role="combobox"][data-touched="true"]:invalid,
& input[role="combobox"][data-touched="true"]:invalid ~ button {
border-color: $color-error;
Expand All @@ -71,19 +73,18 @@
& input[role="combobox"]:valid,
& input[role="combobox"]:not([data-touched="true"]) {
// NOTE (GAFI 06-07-2023): Compense la bordure plus épaisse quand en erreur
$border-width-compensation: calc($error-border-width - $border-width);
margin:
$border-width-compensation
0
$border-width-compensation
$border-width-compensation;
$border-width-compensation
0
$border-width-compensation
$border-width-compensation;

& ~ button {
margin:
$border-width-compensation
$border-width-compensation
$border-width-compensation
0;
$border-width-compensation
$border-width-compensation
$border-width-compensation
0;
}
}

Expand Down
1 change: 0 additions & 1 deletion src/client/components/ui/Form/Combobox/Combobox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1437,7 +1437,6 @@ describe('<Combobox />', () => {

expect(onInvalid).not.toHaveBeenCalled();
});
it.todo('ne marque pas le champ si on quite sans écrire dedans ?');
});

describe('requireValidOption', () => {
Expand Down
16 changes: 1 addition & 15 deletions src/client/components/ui/Form/Combobox/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import React, {
import { KeyBoard } from '~/client/components/keyboard/keyboard.enum';
import { Icon } from '~/client/components/ui/Icon/Icon';
import { useSynchronizedRef } from '~/client/hooks/useSynchronizedRef';
import { useTouchedInput } from '~/client/hooks/useTouchedInput';

import { ChangeEvent } from './ChangeEvent';
import styles from './Combobox.module.scss';
Expand All @@ -41,22 +42,7 @@ type ComboboxProps = Omit<
'aria-labelledby': string,
});

function useTouchedInput() {
const [touched, setTouched] = useState(false);
const valueOnFocus = useRef<string | null>(null);

const saveValueOnFocus = useCallback(function saveCurrentValue(value: string) {
valueOnFocus.current = value;
}, []);

const setTouchedOnBlur = useCallback(function touch(currentValue: string) {
if (valueOnFocus.current !== currentValue) {
setTouched(true);
}
}, []);

return { saveValueOnFocus, setTouchedOnBlur, touched };
}

export const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(function Combobox({
children,
Expand Down
41 changes: 41 additions & 0 deletions src/client/components/ui/Form/InputText/Input.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@use "@styles/utilities";

$color-text: utilities.$color-text-primary;
$color-border: utilities.$color-background-border;
$color-text-disabled: inherit;
$color-background-disabled: utilities.$color-background-disabled;
$color-error: utilities.$color-error;
$error-border-width: 2px;
$border-radius: 1.25rem;
$border-width: 1px;
$border-width-compensation: calc($error-border-width - $border-width);

%input {
color: $color-text;
background-color: transparent;
padding: 0.5rem 1rem;
border-radius: $border-radius;
border: $border-width solid $color-border;

&:disabled,
&:read-only {
color: $color-text-disabled;
background-color: $color-background-disabled;
cursor: not-allowed;
}

&[data-touched="true"]:invalid {
border-color: $color-error;
border-width: $error-border-width;
}

&:valid,
&:not([data-touched="true"]) {
// NOTE (GAFI 06-07-2023): Compense la bordure plus épaisse quand en erreur
margin: $border-width-compensation 0 $border-width-compensation $border-width-compensation;
}
}

.input {
@extend %input;
}
46 changes: 46 additions & 0 deletions src/client/components/ui/Form/InputText/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentPropsWithoutRef } from 'react';

import { Input } from './Input';

const meta: Meta<typeof Input> = {
component: Input,
title: 'Components/Form/Input',
};

export default meta;
type Story = StoryObj<typeof Input>;
export const exemple: Story = {
args: {},
render: (args) => (
<>
<label htmlFor="pays">Pays</label>
<Input id="pays" {...args} />
</>
),
};

export const disabled: Story = {
args: {
disabled: true,
},
render: (args) => (
<>
<label htmlFor="pays">Pays</label>
<Input id="pays" {...args} />
</>
),
};

export const withValidation: Story = {
args: {
validation: (value: ComponentPropsWithoutRef<typeof Input>['value']) =>
(typeof value === 'string' && value === 'France') ? '' : 'Entrer "France"',
},
render: (args) => (
<>
<label htmlFor="pays">Pays (Entrer France)</label>
<Input id="pays" {...args}/>
</>
),
};
153 changes: 153 additions & 0 deletions src/client/components/ui/Form/InputText/Input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @jest-environment jsdom
*/

import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';

import { Input } from '~/client/components/ui/Form/InputText/Input';

describe('<Input/>', () => {
it('affiche un textbox', () => {
render(<Input/>);
expect(screen.getByRole('textbox')).toBeVisible();
});

it('accepte les props natives d‘un input', () => {
render(<Input disabled aria-label={'foo'}/>);

const input = screen.getByRole('textbox');
expect(input).toBeDisabled();
expect(input).toHaveAccessibleName('foo');
});

it('accepte une ref', () => {
const ref = jest.fn();
render(<Input ref={ref}/>);

expect(ref).toHaveBeenCalledTimes(1);
expect(ref).toHaveBeenCalledWith(expect.any(HTMLInputElement));
});

it('accepte une classe', () => {
render(<Input className={'className'}/>);

const input = screen.getByRole('textbox');
expect(input).toHaveAttribute('class', expect.stringContaining('className'));
});

it('accepte un onChange', async () => {
const onChange = jest.fn();
const user = userEvent.setup();
render(<Input onChange={onChange}/>);

const input = screen.getByRole('textbox');
await user.type(input, 'a');
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ target: input }));
});

it('accepte un onFocus', async () => {
const onFocus = jest.fn();
const user = userEvent.setup();
render(<Input onFocus={onFocus}/>);

const input = screen.getByRole('textbox');
await user.click(input);
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledWith(expect.objectContaining({ target: input }));
});

it('accepte un onBlur', async () => {
const onBlur = jest.fn();
const user = userEvent.setup();
render(<Input onBlur={onBlur}/>);

const input = screen.getByRole('textbox');
await user.type(input, 'a');
await user.tab();
expect(onBlur).toHaveBeenCalledTimes(1);
expect(onBlur).toHaveBeenCalledWith(expect.objectContaining({ target: input }));
});

describe('validation', () => {
it('lorsque la valeur initiale de l‘input est valide, l‘input est valide', async () => {
const validation = jest.fn().mockReturnValue('');
render(<Input validation={validation}/>);

const input = screen.getByRole('textbox');
expect(input).toBeValid();
});

it('lorsque la valeur initiale de l‘input n‘est pas valide, l‘input est invalide', async () => {
const validation = jest.fn().mockReturnValue('error message');
render(<Input validation={validation}/>);

const input = screen.getByRole('textbox');
expect(input).toBeInvalid();
});

it('lorsque je tape une valeur valide, l‘input est valide', async () => {
const validation = jest.fn().mockReturnValue('');
const user = userEvent.setup();
render(<Input validation={validation}/>);

const input = screen.getByRole('textbox');
await user.type(input, 'a');

expect(input).toBeValid();
});

it('lorsque je tape une valeur invalide, l‘input est en erreur', async () => {
const validation = jest.fn().mockReturnValue('error');
const user = userEvent.setup();
render(<Input validation={validation}/>);

const input = screen.getByRole('textbox');
await user.type(input, 'a');

expect(input).toBeInvalid();
});
});

describe('l’input est marqué comme touché ou non', () => {
it('n’est pas marqué comme touché par défaut', () => {
render(<Input/>);

const input = screen.getByRole('textbox');
expect(input).toHaveAttribute('data-touched', 'false');
});

it('marque le champ comme touché quand on quitte le champ après avoir écrit dedans', async () => {
const user = userEvent.setup();
render(<Input/>);

const input = screen.getByRole('textbox');
await user.type(input, 'a');
await user.tab();

expect(input).toHaveAttribute('data-touched', 'true');
});

it('ne marque pas le champ tant qu’on ne quitte pas le champ', async () => {
const user = userEvent.setup();
render(<Input/>);

const input = screen.getByRole('textbox');
await user.type(input, 'a');

expect(input).toHaveAttribute('data-touched', 'false');
});

it('ne marque pas le champ comme touché quand on quitte le champ sans avoir écrit dedans', async () => {
const user = userEvent.setup();
render(<Input/>);

const input = screen.getByRole('textbox');
await user.click(input);
await user.tab();

expect(input).toHaveAttribute('data-touched', 'false');
});
});
});
Loading

0 comments on commit cbbd6fb

Please sign in to comment.