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

create input component #61

Merged
merged 25 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eca8227
create input component
Magnusrm Jul 6, 2022
3caaa29
use figma tokens where aplicable
Magnusrm Jul 7, 2022
22c0729
cleanup to address PR comment
Magnusrm Jul 8, 2022
2b10381
select input in a consistent way
Magnusrm Jul 8, 2022
61904ed
Merge branch 'main' of github.com:Altinn/altinn-design-system into fe…
haakemon Jul 18, 2022
7fa0e78
chore: add note to readme about updating rollup config when adding ne…
haakemon Jul 18, 2022
eaaa0d7
refactor: fix circular dependency, remove unused variables, join same…
haakemon Jul 18, 2022
bccdf46
refactor: rename interfaces to follow TS naming convention
haakemon Jul 18, 2022
37bddb8
refactor: use object property/value shorthand where possible, avoid s…
haakemon Jul 18, 2022
4cd39fe
test: use act to avoid warnings during testrun
haakemon Jul 19, 2022
50d5f49
refactor: change from render function to react component, update tests
haakemon Jul 19, 2022
d58fdd0
refactor: fix some typos, extract Icon to a separate file, add unit t…
haakemon Jul 19, 2022
2987800
refactor: update css to latest tokens, and simplify structure
haakemon Jul 19, 2022
68beb55
refactor: simplify styles
haakemon Jul 19, 2022
c39f7e1
fix: dont trigger blur when readonly
haakemon Jul 19, 2022
5a407bd
Merge branch 'main' of github.com:Altinn/altinn-design-system into fe…
haakemon Jul 20, 2022
adff1a1
chore: add story for number input
haakemon Jul 20, 2022
1622e2c
refactor: rename function to make its meaning clearer, and change fro…
haakemon Jul 20, 2022
f793213
feat: support onPaste event
haakemon Jul 22, 2022
ebf91c0
remove code that blocks onBlur callback when field is readonly to ali…
haakemon Jul 22, 2022
4f7d046
refactor: change to outline instead of increasing border-width to avo…
haakemon Jul 22, 2022
1e4cd1c
refactor: extend types from InputHTMLAttributes to avoid having to ma…
haakemon Jul 22, 2022
566c21a
Merge branch 'main' of github.com:Altinn/altinn-design-system into fe…
haakemon Aug 1, 2022
2899302
fix: height of inputfield is was too tall, because borders are on out…
haakemon Aug 2, 2022
23d54e4
chore: update css property key
haakemon Aug 2, 2022
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
},
"dependencies": {
"@altinn/figma-design-tokens": "^0.2.0",
"@react-hookz/web": "^14.2.2"
"@react-hookz/web": "^14.2.2",
"react-number-format": "^4.9.3"
},
"resolutions": {
"@storybook/react/webpack": "^5"
Expand Down
91 changes: 91 additions & 0 deletions src/components/TextField/TextField.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
.input-wrapper__field {
box-sizing: border-box;
border: none;
outline: none;
padding: var(--component-input-space-padding-y)
var(--component-input-space-padding-x)
var(--component-input-space-padding-y);
width: 100%;
}

.input-wrapper {
background-color: var(--component-input-color-background-default);
border-width: var(--component-input-border-width-default);
border-style: solid;
box-sizing: border-box;
display: flex;
align-items: center;
}

.input-wrapper--default {
border-color: var(--component-input-color-border-default);
margin: var(--component-input-border-width-default);
}

.input-wrapper--default:hover {
border-color: var(--component-input-color-border-hover);
}

.input-wrapper--default:focus-within {
border-width: var(--component-input-border-width-focus);
border-color: var(--component-input-color-border-focus);
outline: none;
margin: 0px;
}

.input-wrapper--error {
border-color: #E23B53;
background-color: #E23B53;
/* var(--component-input-color-border-error); */
margin: var(--component-input-border-width-default);
}

.input-wrapper--error:focus-within {
border-width: var(--component-input-border-width-focus);
border-color: #E23B53;
/* var(--component-input-color-border-error); */
outline: none;
margin: 0px;
}

.input-wrapper--disabled {
border-color: #6A6A6A;
/* var(--component-input-color-border-disabled); */
margin: var(--component-input-border-width-default);
}

.input-wrapper--disabled .input-wrapper__field {
background: repeating-linear-gradient(
135deg,
#efefef,
#efefef 2px,
#fff 3px,
#fff 5px
);
}

.input-wrapper--readonly-info {
border-color: #FBF6BD;
/* var(--component-input-color-border-readonly-info); */
margin: var(--component-input-border-width-default);
}

.input-wrapper--readonly-info .input-wrapper__field {
background-color: #FBF6BD;
}

.input-wrapper--readonly-confirm {
border-color: #D4F9E4;
/* var(--component-input-color-border-readonly-confirm); */
margin: var(--component-input-border-width-default);
}

.input-wrapper--readonly-confirm .input-wrapper__field {
background-color: #D4F9E4;
}

.input-wrapper__icon--error {
padding-right: var(--component-input-border-width-default);
padding-left: var(--component-input-border-width-default);
min-width: 22px;
}
107 changes: 107 additions & 0 deletions src/components/TextField/TextField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';
import type { ComponentStory, ComponentMeta } from '@storybook/react';
import { config } from 'storybook-addon-designs';

import { StoryPage } from '@sb/StoryPage';

import { ReadOnlyVariant, TextField } from './TextField';

const figmaLink =
'https://www.figma.com/file/wnBveAG2ikUspFsQwM3GNE/Altinn-Studio-Apps?node-id=2090%3A6723';

export default {
title: `Components/TextField`,
component: TextField,
parameters: {
design: config([
{
type: 'figma',
url: figmaLink,
},
{
type: 'link',
url: figmaLink,
},
]),
docs: {
page: () => (
<StoryPage
description={`TODO: Add a description (supports markdown)`}
/>
),
},
},
args: {
id: 'textfield-story',
formatting: {
align: 'left',
},
},
} as ComponentMeta<typeof TextField>;

const Template: ComponentStory<typeof TextField> = (args) => (
<TextField {...args} />
);

export const Default = Template.bind({});
Default.args = {
isValid: true,
};
Default.parameters = {
docs: {
description: {
story: 'Regular input field.',
},
},
};

export const Error = Template.bind({});
Error.args = {
isValid: false,
};
Error.parameters = {
docs: {
description: {
story: 'Input field displaying error.',
},
},
};

export const ReadOnlyInfo = Template.bind({});
ReadOnlyInfo.args = {
readOnly: true,
value: 'Some text',
};
ReadOnlyInfo.parameters = {
docs: {
description: {
story: 'Input field as readonly, info.',
},
},
};

export const ReadOnlyConfirm = Template.bind({});
ReadOnlyConfirm.args = {
readOnly: ReadOnlyVariant.ReadonlyConfirm,
value: 'Some text',
};
ReadOnlyConfirm.parameters = {
docs: {
description: {
story: 'Input field as readonly, confirm.',
},
},
};

export const Disabled = Template.bind({});
Disabled.args = {
disabled: true,
value: 'Some text',
};
Disabled.parameters = {
docs: {
description: {
story: 'Input field as disabled.',
},
},
};
137 changes: 137 additions & 0 deletions src/components/TextField/TextField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { render as renderRtl, screen } from '@testing-library/react';
import React from 'react';
import { userEvent } from '@storybook/testing-library';

import type { ITextFieldProps } from './TextField';
import { ReadOnlyVariant, TextField } from './TextField';

describe('TextField', () => {
it('should trigger onBlur event', async () => {
const fn = jest.fn();
render({ onBlur: fn });
const element = screen.getByRole('textbox');
userEvent.click(element);
expect(element).toHaveFocus();
userEvent.tab();
expect(fn).toHaveBeenCalledTimes(1);
});

it('should trigger onChange event', async () => {
const fn = jest.fn();
render({ onChange: fn });
const element = screen.getByRole('textbox');
userEvent.click(element);
expect(element).toHaveFocus();
userEvent.keyboard('test');
expect(fn).toHaveBeenCalledTimes(4);
});

describe('error-icon', () => {
it('should not show error-icon when isValid is true', () => {
render({ isValid: true });
expect(screen.queryByTestId('input-icon-error')).not.toBeInTheDocument();
});

it('should not show error-icon when isValid is true and readOnly is true', () => {
render({ isValid: true, readOnly: true });
expect(screen.queryByTestId('input-icon-error')).not.toBeInTheDocument();
});

it('should not show error-icon when isValid is true and disabled is true', () => {
render({ isValid: true, disabled: true });
expect(screen.queryByTestId('input-icon-error')).not.toBeInTheDocument();
});

it('should show error-icon when isValid is false', () => {
render({ isValid: false });
expect(screen.queryByTestId('input-icon-error')).toBeInTheDocument();
});
});

describe('input-variant', () => {
it('should use the css class for error when isValid is false and readOnly or disabled is not specified', () => {
render({ isValid: false });
expect(screen.queryByTestId('id-error')).toBeInTheDocument();
});

it('should use the css class for default when isValid is true and readOnly or disabled is not specified', () => {
render({ isValid: true });
expect(screen.queryByTestId('id-default')).toBeInTheDocument();
});

it('should use the css class for readonly-info when readOnly is true and disabled is not specified', () => {
render({ readOnly: true });
expect(screen.queryByTestId('id-readonly-info')).toBeInTheDocument();
});

it('should use the css class for readonly-confirm when readOnly is <readonly-confirm> and disabled is not specified', () => {
render({ readOnly: ReadOnlyVariant.ReadonlyConfirm });
expect(screen.queryByTestId('id-readonly-confirm')).toBeInTheDocument();
});

it('should use the css class for readonly-info when readOnly is <readonly-info> and disabled is not specified', () => {
render({ readOnly: ReadOnlyVariant.ReadonlyInfo });
expect(screen.queryByTestId('id-readonly-info')).toBeInTheDocument();
});

it('should use the css class for disabled when disabled is true', () => {
render({ disabled: true });
expect(screen.queryByTestId('id-disabled')).toBeInTheDocument();
});
});

describe('number-format-input', () => {
it('should render as a NumberFormat element if format.number is specified', () => {
render({ isValid: true, formatting: { number: { prefix: '$' } } });
expect(
screen.getByTestId('id-formatted-number-default'),
).toBeInTheDocument();
});

it('should trigger onBlur event as a numberformat input', async () => {
const fn = jest.fn();
render({ onBlur: fn, formatting: { number: { prefix: '$' } } });
const element = screen.getByRole('textbox');
userEvent.click(element);
expect(element).toHaveFocus();
userEvent.tab();
expect(fn).toHaveBeenCalledTimes(1);
});

it('should trigger onChange event once per change as a numberformat input', async () => {
const fn = jest.fn();
render({ onChange: fn, formatting: { number: { prefix: '$' } } });
const element = screen.getByRole('textbox');
userEvent.click(element);
expect(element).toHaveFocus();
userEvent.keyboard('1234');
expect(fn).toHaveBeenCalledTimes(4);
});

it('should trigger onChange event and update with unformatted value', async () => {
let testValue;
const onChange = (obj: any) => {
testValue = obj.target.value;
};
render({
onChange: onChange,
formatting: { number: { prefix: '$', thousandSeparator: ' ' } },
});
const element = screen.getByRole('textbox');
userEvent.click(element);
expect(element).toHaveFocus();
userEvent.keyboard('1234');
expect(screen.getByDisplayValue('$1 234')).toBeInTheDocument();
expect(testValue).toBe('1234');
});
});
});

const render = (props: Partial<ITextFieldProps> = {}) => {
const allProps = {
id: 'id',
...props,
};

renderRtl(<TextField {...allProps} />);
};
Loading