Skip to content

Commit

Permalink
improvement: HTML attributes on Input
Browse files Browse the repository at this point in the history
I want to set autocomplete on an input, but that's not an allowed
property of Input yet.

Added support for HTML attributes on Input element.
  • Loading branch information
Gido Manders committed Mar 18, 2024
1 parent 69ccaa9 commit 3847dad
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 81 deletions.
44 changes: 31 additions & 13 deletions src/form/Input/Input.test.tsx
Expand Up @@ -68,7 +68,15 @@ describe('Component: Input', () => {
<Input color="success" {...props} />
);

return { container, props, asFragment, rerender, onChangeSpy, onBlurSpy, onFocusSpy };
return {
container,
props,
asFragment,
rerender,
onChangeSpy,
onBlurSpy,
onFocusSpy
};
}

describe('ui', () => {
Expand All @@ -84,12 +92,16 @@ describe('Component: Input', () => {

test('with placeholder', () => {
setup({ hasPlaceholder: true });
expect(screen.queryByPlaceholderText('Please enter your first name')).toBeInTheDocument();
expect(
screen.queryByPlaceholderText('Please enter your first name')
).toBeInTheDocument();
});

test('without placeholder', () => {
setup({ hasPlaceholder: false });
expect(screen.queryByPlaceholderText('Please enter your first name')).not.toBeInTheDocument();
expect(
screen.queryByPlaceholderText('Please enter your first name')
).not.toBeInTheDocument();
});

test('visible label', () => {
Expand All @@ -106,18 +118,26 @@ describe('Component: Input', () => {
});

test('addon default', () => {
const { container } = setup({ addon: <InputGroupText>Default on the left</InputGroupText> });
const { container } = setup({
addon: <InputGroupText>Default on the left</InputGroupText>
});
expect(container).toMatchSnapshot();
});

test('addon left', () => {
setup({ addon: <InputGroupText>Left</InputGroupText>, addonPosition: 'left' });
setup({
addon: <InputGroupText>Left</InputGroupText>,
addonPosition: 'left'
});
const addon = screen.getByText('Left');
expect(addon.parentNode?.childNodes.item(0)).toBe(addon);
});

test('addon right', () => {
setup({ addon: <InputGroupText position="right">Right</InputGroupText>, addonPosition: 'right' });
setup({
addon: <InputGroupText position="right">Right</InputGroupText>,
addonPosition: 'right'
});
const addon = screen.getByText('Right');
expect(addon.parentNode?.childNodes.item(1)).toBe(addon);
});
Expand All @@ -142,7 +162,9 @@ describe('Component: Input', () => {
test('onChange', () => {
const { onChangeSpy } = setup({ value: undefined, type: 'text' });

fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Maarten' } });
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'Maarten' }
});

expect(onChangeSpy).toHaveBeenCalledTimes(1);
expect(onChangeSpy).toHaveBeenCalledWith('Maarten');
Expand Down Expand Up @@ -176,9 +198,7 @@ describe('Component: Input', () => {
value: ''
};

rerender(
<Input color="success" {...newProps} />
);
rerender(<Input color="success" {...newProps} />);

expect(screen.getByRole('textbox')).toHaveValue('');
});
Expand All @@ -193,9 +213,7 @@ describe('Component: Input', () => {
value: 'Maarten'
};

rerender(
<Input color="success" {...newProps} />
);
rerender(<Input color="success" {...newProps} />);

expect(screen.getByRole('textbox')).toHaveValue('Maarten');
});
Expand Down
126 changes: 62 additions & 64 deletions src/form/Input/Input.tsx
@@ -1,6 +1,12 @@
import React, { ChangeEvent } from 'react';
import RTMaskedInput from 'react-text-mask';
import { FormGroup, Input as RSInput, InputGroup, Label } from 'reactstrap';
import {
FormGroup,
Input as RSInput,
InputGroup,
InputProps as RSInputProps,
Label
} from 'reactstrap';
import { withJarb } from '../withJarb/withJarb';
import { FieldCompatible } from '../types';
import { uniqueId } from 'lodash';
Expand Down Expand Up @@ -36,80 +42,72 @@ export type InputType =
| 'time'
| 'color';

export type Props = FieldCompatible<string, string> & {
/**
* Optional type of the input, default to `text`.
*/
type?: InputType;

/**
* Optional mask of the input.
*
* @see https://text-mask.github.io/text-mask/
*/
mask?: InputMask;

/**
* Optional addon to display to the left or right of the input
* element. Provide either an Addon, AddonIcon or AddonButton to be
* rendered here.
*
* The `position` property of the addon determines were the addon
* is rendered.
*/
addon?: React.ReactElement;

/**
* The position of the Addon, is it to the right or left
* of the input.
*
* Defaults to 'left'
*/
addonPosition?: 'left' | 'right';

/**
* A ref to the actual input, can be used to focus the element.
*/
innerRef?: React.Ref<HTMLInputElement>;
};
export type Props = FieldCompatible<string, string> &
Omit<RSInputProps, 'value' | 'onChange' | 'onBlur' | 'onFocus' | 'type'> & {
/**
* Optional type of the input, default to `text`.
*/
type?: InputType;

/**
* Optional mask of the input.
*
* @see https://text-mask.github.io/text-mask/
*/
mask?: InputMask;

/**
* Optional addon to display to the left or right of the input
* element. Provide either an Addon, AddonIcon or AddonButton to be
* rendered here.
*
* The `position` property of the addon determines were the addon
* is rendered.
*/
addon?: React.ReactElement;

/**
* The position of the Addon, is it to the right or left
* of the input.
*
* Defaults to 'left'
*/
addonPosition?: 'left' | 'right';

/**
* A ref to the actual input, can be used to focus the element.
*/
innerRef?: React.Ref<HTMLInputElement>;
};

/**
* Input is a basic form element which allows the user to enter text.
*
* Supports addons and masks.
*/
export function Input(props: Props) {
const {
id = uniqueId(),
label,
hiddenLabel,
placeholder,
value,
onChange,
onBlur,
onFocus,
innerRef,
type = 'text',
error,
color,
valid,
mask,
addon,
addonPosition = 'left',
className = ''
} = props;

export function Input({
id = uniqueId(),
label,
hiddenLabel,
onChange,
type = 'text',
error,
color,
valid,
mask,
addon,
addonPosition = 'left',
className = '',
...htmlInputProps
}: Props) {
const inputProps = {
...htmlInputProps,
id,
type,
valid,
invalid: valid === false ? true : undefined,
value,
innerRef,
placeholder,
onFocus,
onChange: (event: ChangeEvent<HTMLInputElement>) => onChange(event.target.value),
onBlur,
onChange: (event: ChangeEvent<HTMLInputElement>) =>
onChange(event.target.value),
'aria-label': hiddenLabel && typeof label === 'string' ? label : undefined
};

Expand Down
4 changes: 4 additions & 0 deletions src/form/Input/__snapshots__/Input.test.tsx.snap
Expand Up @@ -10,6 +10,7 @@ exports[`Component: Input masked 1`] = `
aria-label="First name"
class="form-control"
id="11"
name="firstName"
type="text"
value=""
/>
Expand All @@ -36,6 +37,7 @@ exports[`Component: Input ui addon default 1`] = `
aria-label="First name"
class="form-control"
id="6"
name="firstName"
type="text"
value=""
/>
Expand All @@ -55,6 +57,7 @@ exports[`Component: Input ui default 1`] = `
aria-label="First name"
class="form-control"
id="1"
name="firstName"
type="text"
value=""
/>
Expand All @@ -78,6 +81,7 @@ exports[`Component: Input ui visible label 1`] = `
<input
class="form-control"
id="firstName"
name="firstName"
type="text"
value=""
/>
Expand Down
24 changes: 20 additions & 4 deletions src/form/NewPasswordInput/NewPasswordInput.tsx
Expand Up @@ -6,9 +6,17 @@ import { Translation } from '../../utilities/translation/translator';
import { Input, Props as InputProps } from '../Input/Input';
import { withJarb } from '../withJarb/withJarb';
import { PasswordStrength } from './PasswordStrength/PasswordStrength';
import { hasLowercase, hasMinimumLength, hasNoSpaces, hasNumber, hasSpecialChar, hasUppercase } from './PasswordStrength/rules';
import {
hasLowercase,
hasMinimumLength,
hasNoSpaces,
hasNumber,
hasSpecialChar,
hasUppercase
} from './PasswordStrength/rules';
import { NewPasswordInputRule } from './types';
import { withField } from '../withField/withField';
import { FieldCompatible } from '../types';

type PasswordProps = {
/**
Expand Down Expand Up @@ -82,7 +90,7 @@ export function NewPasswordInput(props: Props) {
'showMeter'
]) as InputProps;
inputProps.type = 'password';
const passwordStrengthProps = pick(props, [ 'minimumLength', 'showMeter' ]);
const passwordStrengthProps = pick(props, ['minimumLength', 'showMeter']);

return (
<div className="new-password-input">
Expand All @@ -99,12 +107,20 @@ export function NewPasswordInput(props: Props) {
/**
* Variant of the FileInput which can be used in a Jarb context.
*/
export const JarbNewPasswordInput = withJarb<string, string, Props>(NewPasswordInput);
export const JarbNewPasswordInput = withJarb<
string,
string,
FieldCompatible<string, string> & Props
>(NewPasswordInput);

/**
* Variant of the FileInput which can be used in a final form.
*/
export const FieldNewPasswordInput = withField<string, string, Props>(NewPasswordInput);
export const FieldNewPasswordInput = withField<
string,
string,
FieldCompatible<string, string> & Props
>(NewPasswordInput);

function getRulesFromProps(props: PasswordProps): NewPasswordInputRule[] {
const {
Expand Down
3 changes: 3 additions & 0 deletions src/form/withField/__snapshots__/withField.test.tsx.snap
Expand Up @@ -21,6 +21,7 @@ exports[`HoC: withField errorMode: tooltip 1`] = `
<input
class="form-control"
id="firstName"
name="firstName"
placeholder="Please enter your first name"
type="text"
value="yolo"
Expand Down Expand Up @@ -61,6 +62,7 @@ exports[`HoC: withField ui default 1`] = `
<input
class="form-control"
id="firstName"
name="firstName"
placeholder="Please enter your first name"
type="text"
value="yolo"
Expand All @@ -85,6 +87,7 @@ exports[`HoC: withField ui with error 1`] = `
aria-invalid="true"
class="is-invalid form-control"
id="firstName"
name="firstName"
placeholder="Please enter your first name"
type="text"
value="yolo"
Expand Down
3 changes: 3 additions & 0 deletions src/form/withJarb/__snapshots__/withJarb.test.tsx.snap
Expand Up @@ -21,6 +21,7 @@ exports[`HoC: withJarb errorMode: tooltip 1`] = `
<input
class="form-control"
id="firstName"
name="firstName"
placeholder="Please enter your first name"
type="text"
value="yolo"
Expand Down Expand Up @@ -61,6 +62,7 @@ exports[`HoC: withJarb ui default 1`] = `
<input
class="form-control"
id="firstName"
name="firstName"
placeholder="Please enter your first name"
type="text"
value="yolo"
Expand All @@ -85,6 +87,7 @@ exports[`HoC: withJarb ui with error 1`] = `
aria-invalid="true"
class="is-invalid form-control"
id="firstName"
name="firstName"
placeholder="Please enter your first name"
type="text"
value="yolo"
Expand Down
Expand Up @@ -20,6 +20,7 @@ exports[`Component: EpicForm ui 1`] = `
<input
class="form-control"
id="testField"
name="testField"
type="text"
value=""
/>
Expand Down

0 comments on commit 3847dad

Please sign in to comment.