-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: APP-2612 - Implement InputContainer and InputText components (#48)
- Loading branch information
Showing
18 changed files
with
523 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './inputContainer'; | ||
export * from './inputText'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { InputContainer } from './inputContainer'; | ||
export type { IInputComponentProps, IInputContainerProps, InputVariant } from './inputContainer.api'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import type { InputHTMLAttributes, ReactNode } from 'react'; | ||
import type { AlertVariant } from '../../alerts/utils'; | ||
|
||
export type InputVariant = 'default' | 'warning' | 'critical'; | ||
|
||
export interface IInputContainerAlert { | ||
/** | ||
* Message to be displayed. | ||
*/ | ||
message: string; | ||
/** | ||
* Variant of the alert. | ||
*/ | ||
variant: Exclude<AlertVariant, 'info' | 'success'>; | ||
} | ||
|
||
export interface IInputContainerProps { | ||
/** | ||
* Label of the input. | ||
*/ | ||
label?: string; | ||
/** | ||
* Variant of the input. | ||
* @default default | ||
*/ | ||
variant?: InputVariant; | ||
/** | ||
* Help text displayed above the input. | ||
*/ | ||
helpText?: string; | ||
/** | ||
* Displays the optional tag when set to true. | ||
*/ | ||
isOptional?: boolean; | ||
/** | ||
* Displays the input as disabled when set to true. | ||
*/ | ||
isDisabled?: boolean; | ||
/** | ||
* Alert displayed below the input. | ||
*/ | ||
alert?: IInputContainerAlert; | ||
/** | ||
* Id of the input field. | ||
*/ | ||
id: string; | ||
/** | ||
* Displays an input length counter when set. | ||
*/ | ||
maxLength?: number; | ||
/** | ||
* Current input length displayed when maxLength property is set. | ||
*/ | ||
inputLength?: number; | ||
/** | ||
* Children of the component. | ||
*/ | ||
children?: ReactNode; | ||
/** | ||
* Classes for the component. | ||
*/ | ||
className?: string; | ||
} | ||
|
||
export interface IInputComponentProps | ||
extends Omit<IInputContainerProps, 'children' | 'id' | 'inputLength'>, | ||
Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> { | ||
/** | ||
* Classes for the input element. | ||
*/ | ||
inputClassName?: string; | ||
} |
25 changes: 25 additions & 0 deletions
25
src/components/input/inputContainer/inputContainer.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { InputContainer } from './inputContainer'; | ||
|
||
const meta: Meta<typeof InputContainer> = { | ||
title: 'components/Input/InputContainer', | ||
component: InputContainer, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=10055-28606&mode=design&t=dehPZplRn0YEdOuB-4', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof InputContainer>; | ||
|
||
/** | ||
* Default usage example of the InputContainer component. | ||
*/ | ||
export const Default: Story = { | ||
args: {}, | ||
}; | ||
|
||
export default meta; |
62 changes: 62 additions & 0 deletions
62
src/components/input/inputContainer/inputContainer.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import { InputContainer } from './inputContainer'; | ||
import type { IInputContainerProps } from './inputContainer.api'; | ||
|
||
describe('<InputContainer /> component', () => { | ||
const createTestComponent = (props?: Partial<IInputContainerProps>) => { | ||
const completeProps = { | ||
id: 'test', | ||
...props, | ||
}; | ||
|
||
return <InputContainer {...completeProps} />; | ||
}; | ||
|
||
it('renders the input label when specified', () => { | ||
const label = 'input-label'; | ||
|
||
// The getByLabelText requires a form control to be associated to the label | ||
const id = 'input-id'; | ||
const children = <input id={id} />; | ||
|
||
render(createTestComponent({ label, children, id })); | ||
expect(screen.getByLabelText(label)).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the optional tag when the label is set and isOptional prop is set to true', () => { | ||
const label = 'label-test'; | ||
const isOptional = true; | ||
render(createTestComponent({ label, isOptional })); | ||
expect(screen.getByText('Optional')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the help text when defined', () => { | ||
const helpText = 'help-text-test'; | ||
render(createTestComponent({ helpText })); | ||
expect(screen.getByText(helpText)).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the input value counter when maxLength is defined', () => { | ||
const maxLength = 100; | ||
const inputLength = 47; | ||
render(createTestComponent({ maxLength, inputLength })); | ||
expect(screen.getByText(`[${inputLength}/${maxLength}]`)).toBeInTheDocument(); | ||
}); | ||
|
||
it('adds a shake animation when the input length is equal to the max length property', () => { | ||
const maxLength = 10; | ||
const inputLength = 10; | ||
render(createTestComponent({ maxLength, inputLength })); | ||
expect(screen.getByText(`[${inputLength}/${maxLength}]`).className).toContain('shake'); | ||
}); | ||
|
||
it('renders the input alert when defined', () => { | ||
const alert = { | ||
message: 'input-alert-message', | ||
variant: 'critical' as const, | ||
}; | ||
render(createTestComponent({ alert })); | ||
expect(screen.getByRole('alert')).toBeInTheDocument(); | ||
expect(screen.getByText(alert.message)).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import classNames from 'classnames'; | ||
import { AlertInline } from '../../alerts'; | ||
import { Tag } from '../../tag'; | ||
import type { IInputContainerProps, InputVariant } from './inputContainer.api'; | ||
|
||
const variantToClassNames: Record<InputVariant | 'disabled', string[]> = { | ||
default: [ | ||
'border-neutral-100 bg-neutral-0', // Default state | ||
'hover:border-neutral-200 hover:shadow-neutral-md', // Hover state | ||
'focus-within:outline-primary-400 focus-within:border-primary-400 focus-within:shadow-primary-md', // Focus state | ||
'focus-within:hover:border-primary-400 focus-within:hover:shadow-primary-md', // Focus + Hover state | ||
], | ||
warning: [ | ||
'border-warning-500 bg-neutral-0', // Default state | ||
'hover:border-warning-600 hover:shadow-warning-md', // Hover state | ||
'focus-within:outline-warning-600 focus-within:border-warning-600 focus-within:shadow-warning-md', // Focus state | ||
'focus-within:hover:border-warning-600 focus-within:hover:shadow-warning-md', // Focus + Hover state | ||
], | ||
critical: [ | ||
'border-critical-500 bg-neutral-0', // Default state | ||
'hover:border-critical-600 hover:shadow-critical-md', // Hover state | ||
'focus-within:outline-critical-600 focus-within:border-critical-600 focus-within:shadow-critical-md', // Focus state | ||
'focus-within:hover:border-critical-600 focus-within:hover:shadow-critical-md', // Focus + Hover state | ||
], | ||
disabled: ['border-neutral-200 bg-neutral-100'], | ||
}; | ||
|
||
/** | ||
* The InputContainer component provides a consistent and shared styling foundation for various input components, such | ||
* as `InputText`, `InputNumber` and others. It also manages properties that are shared across all input components, | ||
* including `label`, `helpText` and more. | ||
*/ | ||
export const InputContainer: React.FC<IInputContainerProps> = (props) => { | ||
const { | ||
label, | ||
variant = 'default', | ||
helpText, | ||
isOptional, | ||
maxLength, | ||
inputLength = 0, | ||
alert, | ||
isDisabled, | ||
children, | ||
className, | ||
id, | ||
} = props; | ||
|
||
const processedVariant = isDisabled ? 'disabled' : variant; | ||
const containerClasses = classNames( | ||
'h-12 w-full rounded-xl border text-neutral-600 transition-all', // Default | ||
'outline-1 focus-within:outline', // Outline on focus | ||
'text-base font-normal leading-tight', // Typography | ||
variantToClassNames[processedVariant], | ||
); | ||
|
||
const counterClasses = classNames('text-sm font-normal leading-tight text-neutral-600', { | ||
'animate-shake': inputLength === maxLength, | ||
}); | ||
|
||
return ( | ||
<div className={classNames('flex grow flex-col gap-2 md:gap-3', className)}> | ||
{(label != null || helpText != null) && ( | ||
<label className="flex flex-col gap-0.5 md:gap-1" htmlFor={id}> | ||
{label && ( | ||
<div className="flex flex-row items-center gap-3"> | ||
<p className="text-base font-semibold leading-normal text-neutral-600 md:text-lg md:leading-tight"> | ||
{label} | ||
</p> | ||
{/* TODO: apply internationalisation to Optional label [APP-2627] */} | ||
{isOptional && <Tag variant="neutral" label="Optional" />} | ||
</div> | ||
)} | ||
{helpText && <p className="text-sm font-normal leading-normal text-neutral-800">{helpText}</p>} | ||
</label> | ||
)} | ||
<div className={containerClasses}>{children}</div> | ||
{maxLength != null && ( | ||
<p className={counterClasses}> | ||
[{inputLength}/{maxLength}] | ||
</p> | ||
)} | ||
{alert && <AlertInline variant={alert.variant} message={alert.message} />} | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { InputText, type IInputTextProps } from './inputText'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { useState, type ChangeEvent } from 'react'; | ||
import { InputText, type IInputTextProps } from './inputText'; | ||
|
||
const meta: Meta<typeof InputText> = { | ||
title: 'components/Input/InputText', | ||
component: InputText, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/jfKRr1V9evJUp1uBeyP3Zz/v1.0.0?type=design&node-id=17-292&mode=design&t=dehPZplRn0YEdOuB-4', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof InputText>; | ||
|
||
/** | ||
* Default uncontrolled usage example of the InputText component. | ||
*/ | ||
export const Default: Story = { | ||
args: { | ||
placeholder: 'Uncontrolled input', | ||
}, | ||
}; | ||
|
||
const ControlledComponent = (props: IInputTextProps) => { | ||
const [value, setValue] = useState<string>(''); | ||
|
||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => setValue(event.target.value); | ||
|
||
return <InputText value={value} onChange={handleChange} {...props} />; | ||
}; | ||
|
||
/** | ||
* Usage example of a controlled input. | ||
*/ | ||
export const Controlled: Story = { | ||
render: (props) => <ControlledComponent {...props} />, | ||
args: { | ||
placeholder: 'Controlled input', | ||
}, | ||
}; | ||
|
||
export default meta; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import { InputText, type IInputTextProps } from './inputText'; | ||
|
||
describe('<InputText /> component', () => { | ||
const createTestComponent = (props?: Partial<IInputTextProps>) => { | ||
const completeProps = { ...props }; | ||
|
||
return <InputText {...completeProps} />; | ||
}; | ||
|
||
it('renders a text input element', () => { | ||
render(createTestComponent()); | ||
expect(screen.getByRole('textbox')).toBeInTheDocument(); | ||
}); | ||
|
||
it('disables the text input when isDisabled is set to true', () => { | ||
const isDisabled = true; | ||
render(createTestComponent({ isDisabled })); | ||
expect(screen.getByRole<HTMLInputElement>('textbox').disabled).toBeTruthy(); | ||
}); | ||
|
||
it('applies the input class names when defined', () => { | ||
const inputClassName = 'class-test'; | ||
render(createTestComponent({ inputClassName })); | ||
expect(screen.getByRole('textbox').className).toContain(inputClassName); | ||
}); | ||
}); |
Oops, something went wrong.