Skip to content

Commit d515820

Browse files
author
Hector Arce De Las Heras
committed
Enhance Input Component with Left and Right Icons and Update Related Elements
This commit enhances the Input component by introducing left and right icons, updates the InputIcon component, and revises the test cases. It also includes a few deprecations and minor changes to the stories and styles. Changes include: Input component in inputStandAlone.tsx now includes left and right icons, providing more flexibility in the design of the input fields. InputIcon component in inputIcon.tsx has been updated to support left and right icons. A deprecated version of the component has been introduced to support the old icon prop until it is removed. Test cases in input.test.tsx have been updated to reflect the changes made to the Input component, including testing for the new left and right icons. The icon and iconPosition props in the Input component in input.ts have been deprecated. They will be replaced with leftIcon and rightIcon in a future update. Stories in stories.tsx have been updated to include examples of the new left and right icons. Styles in input.styled.ts have been updated to accommodate the new left and right icons. These changes enhance the functionality and flexibility of the Input component.
1 parent b0510f7 commit d515820

File tree

23 files changed

+286
-36
lines changed

23 files changed

+286
-36
lines changed

src/components/input/__tests__/input.test.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,12 @@ describe('New Input Component', () => {
155155
expect(results).toHaveNoViolations();
156156
});
157157

158-
it('Should call onIconClick when user click on icon', async () => {
158+
it('Should call onLeftIconClick when user click on left icon', async () => {
159159
const onIconClick = jest.fn();
160160
const { container, getByRole } = renderProvider(
161161
<Input
162162
{...commonProps}
163-
icon={{ icon: 'UNICORN', altText: 'Open Info', onClick: onIconClick }}
163+
leftIcon={{ icon: 'UNICORN', altText: 'Open Info', onClick: onIconClick }}
164164
placeholder={'placeholder'}
165165
/>
166166
);
@@ -175,6 +175,26 @@ describe('New Input Component', () => {
175175
expect(results).toHaveNoViolations();
176176
});
177177

178+
it('Should call onRightIconClick when user click on right icon', async () => {
179+
const onIconClick = jest.fn();
180+
const { container, getByRole } = renderProvider(
181+
<Input
182+
{...commonProps}
183+
placeholder={'placeholder'}
184+
rightIcon={{ icon: 'UNICORN', altText: 'Open Info', onClick: onIconClick }}
185+
/>
186+
);
187+
188+
const triggerButton = getByRole('button', { name: 'Open Info' });
189+
fireEvent.click(triggerButton);
190+
expect(onIconClick).toHaveBeenCalled();
191+
192+
// A11Y and w3c validator
193+
const results = await axe(container);
194+
expect(container).toHTMLValidate();
195+
expect(results).toHaveNoViolations();
196+
});
197+
178198
it('Should be autoformated the text with mask', async () => {
179199
const onChange = jest.fn();
180200
const { container, getByRole } = renderProvider(
@@ -348,12 +368,36 @@ describe('New Input Component', () => {
348368
expect(results).toHaveNoViolations();
349369
});
350370

351-
it('Should show icon as img instead of as button', async () => {
371+
it('Should show left icon as img instead of as button', async () => {
372+
const iconClick = jest.fn();
373+
const { container, getByRole } = renderProvider(
374+
<Input
375+
{...commonProps}
376+
leftIcon={{ icon: 'UNICORN', altText: 'Open Info', onClick: iconClick }}
377+
placeholder={'placeholder'}
378+
/>
379+
);
380+
381+
const triggerButton = getByRole('button', { name: 'Open Info' });
382+
expect(triggerButton).toBeInTheDocument();
383+
384+
fireEvent.click(triggerButton);
385+
386+
expect(iconClick).toHaveBeenCalled();
387+
388+
// A11Y and w3c validator
389+
const results = await axe(container);
390+
expect(container).toHTMLValidate();
391+
expect(results).toHaveNoViolations();
392+
});
393+
394+
it('Should show right icon as img instead of as button', async () => {
395+
const iconClick = jest.fn();
352396
const { container, getByRole } = renderProvider(
353397
<Input
354398
{...commonProps}
355-
icon={{ icon: 'UNICORN', altText: 'Open Info' }}
356399
placeholder={'placeholder'}
400+
rightIcon={{ icon: 'UNICORN', altText: 'Open Info', onClick: iconClick }}
357401
/>
358402
);
359403

src/components/input/components/inputIcon.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,36 @@ import { IInputIcon } from '../types/input';
99
const InputIconStandAloneComponent = (
1010
props: IInputIcon,
1111
ref: React.ForwardedRef<HTMLDivElement>
12+
): JSX.Element | null => {
13+
const icon = props.leftIcon || props.rightIcon;
14+
if (!icon || props.loading) {
15+
return null;
16+
}
17+
18+
const onClick: React.MouseEventHandler<HTMLButtonElement> = event => {
19+
props.rightIcon?.onClick?.(event);
20+
props.leftIcon?.onClick?.(event);
21+
};
22+
23+
return (
24+
<InputIconStyled ref={ref} iconPosition={props.iconPosition} styles={props.styles}>
25+
<ElementOrIcon
26+
customIconStyles={props.styles?.inputIcon}
27+
disabled={props.disabled}
28+
{...props.rightIcon}
29+
{...props.leftIcon}
30+
onClick={onClick}
31+
/>
32+
</InputIconStyled>
33+
);
34+
};
35+
36+
export const InputIconStandAlone = React.forwardRef(InputIconStandAloneComponent);
37+
38+
// deprecated - remove this function when icon prop is removed
39+
const InputIconStandAloneDeprecatedComponent = (
40+
props: IInputIcon,
41+
ref: React.ForwardedRef<HTMLDivElement>
1242
): JSX.Element | null => {
1343
if (!props.icon || props.loading) {
1444
return null;
@@ -29,4 +59,6 @@ const InputIconStandAloneComponent = (
2959
);
3060
};
3161

32-
export const InputIconStandAlone = React.forwardRef(InputIconStandAloneComponent);
62+
export const InputIconStandAloneDeprecated = React.forwardRef(
63+
InputIconStandAloneDeprecatedComponent
64+
);

src/components/input/components/stories/stories.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
import * as React from 'react';
22

3+
import { ICONS } from '@/assets';
34
import { ReplaceContent } from '@/components/storybook/replaceContent/replaceContent';
45

6+
const themeWithAdditionalInfo = [
7+
'kubit',
8+
'flameLightAlt',
9+
'modelBankLightAlt',
10+
'horizonLightAlt',
11+
'novaLightAlt',
12+
'flameLightRegular',
13+
'modelBankLightRegular',
14+
'horizonLightRegular',
15+
'novaLightRegular',
16+
];
17+
18+
const themeWithoutIcon = themeWithAdditionalInfo.filter(theme => theme !== 'kubit');
19+
520
// LabelSecondary
621
const labelSecondaryStyles = {
722
display: 'inline-flex',
@@ -10,16 +25,14 @@ const labelSecondaryStyles = {
1025
fontWeight: '400',
1126
};
1227

13-
const themeWithAdditionalInfo = ['kubit'];
14-
1528
export const labelSecondary = (themeSelected: string): React.ReactNode =>
1629
themeWithAdditionalInfo.includes(themeSelected) && (
1730
<div style={labelSecondaryStyles}>
1831
<ReplaceContent width={'fit-content'} />
1932
</div>
2033
);
2134

22-
// AddiotionalInfo
35+
// AdditionalInfo
2336
const additionalInfoStyles = {
2437
marginLeft: '0.25rem',
2538
fontSize: '0.875rem',
@@ -32,3 +45,7 @@ export const additionalInfoAction = (themeSelected: string): React.ReactNode =>
3245
<ReplaceContent width={'fit-content'} />
3346
</div>
3447
);
48+
49+
// Icon
50+
export const inputIcon = (themeSelected: string): string | undefined =>
51+
themeWithoutIcon.includes(themeSelected) ? undefined : ICONS.ICON_PLACEHOLDER;

src/components/input/input.styled.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import styled, { css } from 'styled-components';
22

33
// mixins
44
import {
5-
getPaddingRight,
5+
getIconPadding,
6+
getIconPaddingDeprecated,
67
inputFocusWidthMixin,
78
mapBaseStyles,
89
mapLabelTypeStyles,
@@ -86,12 +87,13 @@ export const LoaderWrapperStyled = styled.div<LoaderStyledProps>`
8687
`;
8788

8889
export const InputIconStyled = styled.div<InputIconStyledProps>`
89-
${({ styles }) => getStyles(styles?.inputIconContainer)};
90-
${({ iconPosition, styles }) =>
91-
iconPosition === InputIconPosition.RIGHT &&
92-
css`
93-
${getStyles(styles?.inputIconContainerRight)};
94-
`};
90+
${({ iconPosition, styles }) => css`
91+
${getStyles(
92+
iconPosition === InputIconPosition.RIGHT
93+
? styles?.inputIconContainerRight
94+
: styles?.inputIconContainer
95+
)};
96+
`}
9597
`;
9698

9799
// Input Styles
@@ -135,14 +137,18 @@ export const InputStyled = styled.input<InputStyledProps>`
135137
136138
&:not(:placeholder-shown) {
137139
${({ styles }) => mapVariableStyles(styles?.[InputState.FILLED])}
138-
${({ icon, iconPosition, styles, state }) =>
139-
icon && iconPosition === InputIconPosition.RIGHT ? getPaddingRight(styles?.[state]) : ''}
140+
${({ icon, iconPosition, leftIcon, rightIcon, styles, state }) =>
141+
icon
142+
? getIconPaddingDeprecated(iconPosition, styles?.[state])
143+
: getIconPadding(!!leftIcon?.icon, !!rightIcon?.icon, styles?.[state])}
140144
}
141145
142146
&:placeholder-shown {
143147
${({ styles }) => mapVariableStyles(styles?.[InputState.EMPTY])}
144-
${({ icon, iconPosition, styles, state }) =>
145-
icon && iconPosition === InputIconPosition.RIGHT ? getPaddingRight(styles?.[state]) : ''}
148+
${({ icon, iconPosition, leftIcon, rightIcon, styles, state }) =>
149+
icon
150+
? getIconPaddingDeprecated(iconPosition, styles?.[state])
151+
: getIconPadding(!!leftIcon?.icon, !!rightIcon?.icon, styles?.[state])}
146152
}
147153
148154
&:disabled {
@@ -173,8 +179,8 @@ export const InputStyled = styled.input<InputStyledProps>`
173179
cursor: ${cursorPointer};
174180
`}
175181
176-
padding-left: ${({ styles, state, iconPosition }) =>
177-
iconPosition === InputIconPosition.LEFT && styles?.[state]?.inputContainer?.padding_left};
182+
padding-left: ${({ styles, state, leftIcon }) =>
183+
leftIcon && styles?.[state]?.inputContainer?.padding_left};
178184
box-shadow: ${({ styles, state }) => styles?.[state]?.inputContainer?.box_shadow};
179185
${({
180186
theme: {

src/components/input/inputStandAlone.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
HelpMessageStandAlone,
99
InformationAssociatedStandAlone,
1010
InputIconStandAlone,
11+
InputIconStandAloneDeprecated,
1112
LabelStandAlone,
1213
LoaderStandAlone,
1314
TextCountStandAlone,
@@ -26,6 +27,7 @@ import {
2627
AUTOCOMPLETE_TYPE,
2728
IInputStandAlone,
2829
InputHelpMessagePosition,
30+
InputIconPosition,
2931
LABEL_TYPE,
3032
MultipleRef,
3133
} from './types';
@@ -49,10 +51,23 @@ const InputStandAloneComponent = (
4951
const helpMessageId = `${props.inputId}HelpText`;
5052
const errorMessageId = `${props.inputId}Error`;
5153
const labelId = `${props.inputId}Label`;
52-
54+
// eslint-disable-next-line complexity
5355
const buildInput = () => (
5456
<InputWrapperStyled styles={styles?.[state]}>
5557
<LoaderStandAlone loader={props.loader} loading={props.loading} styles={styles?.[state]} />
58+
<InputIconStandAlone
59+
ref={
60+
ref && 'refLeftIcon' in ref
61+
? ((ref as unknown as MultipleRef)
62+
?.refLeftIcon as React.MutableRefObject<HTMLDivElement>)
63+
: undefined
64+
}
65+
disabled={isDisabled(state)}
66+
iconPosition={InputIconPosition.LEFT}
67+
leftIcon={props.leftIcon}
68+
state={state}
69+
styles={styles?.[state]}
70+
/>
5671
<InputStyled
5772
ref={
5873
ref && 'refInput' in ref
@@ -82,13 +97,15 @@ const InputStandAloneComponent = (
8297
id={props.inputId}
8398
inputMode={inputMode}
8499
labelType={styles?.[state]?.label?.type}
100+
leftIcon={props.leftIcon}
85101
max={props.max}
86102
maxLength={props.maxLength}
87103
min={props.min}
88104
minLength={props.minLength}
89105
name={props.name}
90106
placeholder={props.placeholder}
91107
required={props.required}
108+
rightIcon={props.rightIcon}
92109
role={props.role}
93110
state={state}
94111
styles={styles}
@@ -111,6 +128,20 @@ const InputStandAloneComponent = (
111128
}}
112129
/>
113130
<InputIconStandAlone
131+
ref={
132+
ref && 'refRightIcon' in ref
133+
? ((ref as unknown as MultipleRef)
134+
?.refRightIcon as React.MutableRefObject<HTMLDivElement>)
135+
: undefined
136+
}
137+
disabled={isDisabled(state)}
138+
iconPosition={InputIconPosition.RIGHT}
139+
rightIcon={props.rightIcon}
140+
state={state}
141+
styles={styles?.[state]}
142+
/>
143+
{/* remove this <InputIconStandalone> when prop `icon` is removed */}
144+
<InputIconStandAloneDeprecated
114145
ref={
115146
ref && 'refIcon' in ref
116147
? ((ref as unknown as MultipleRef)?.refIcon as React.MutableRefObject<HTMLDivElement>)

src/components/input/stories/argtypes.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ export const argtypes = (variants: IThemeObjectVariants, themeSelected: string):
2626
category: CATEGORY_CONTROL.MODIFIERS,
2727
},
2828
},
29+
errorMessageIcon: {
30+
type: { name: 'object' },
31+
description:
32+
'In the error state, the field is accompanied by an error icon and a description of the problem to help resolve it.',
33+
control: { type: 'object' },
34+
table: {
35+
type: {
36+
summary: 'IElementOrIcon',
37+
},
38+
category: CATEGORY_CONTROL.CONTENT,
39+
},
40+
},
41+
// deprecated - remove when `errorMessageIcon` prop is removed
2942
errorIcon: {
3043
description:
3144
'Object with error icon properties. In the error state, the field is accompanied by an error mbIcon and a description of the problem to help resolve it.',
@@ -63,6 +76,30 @@ export const argtypes = (variants: IThemeObjectVariants, themeSelected: string):
6376
detail: Object.keys(InputIconPosition).join(', '),
6477
},
6578
defaultValue: { summary: InputIconPosition.RIGHT },
79+
category: CATEGORY_CONTROL.CONTENT,
80+
},
81+
},
82+
leftIcon: {
83+
description: 'Icon to show on the left side of the input',
84+
type: { name: 'object' },
85+
control: { type: 'object' },
86+
table: {
87+
type: {
88+
summary: 'object',
89+
detail: 'IElementOrIcon',
90+
},
91+
category: CATEGORY_CONTROL.CONTENT,
92+
},
93+
},
94+
rightIcon: {
95+
description: 'Icon to show on the right side of the input',
96+
type: { name: 'object' },
97+
control: { type: 'object' },
98+
table: {
99+
type: {
100+
summary: 'object',
101+
detail: 'IElementOrIcon',
102+
},
66103
category: CATEGORY_CONTROL.MODIFIERS,
67104
},
68105
},

src/components/inputDropdown/hooks/__tests__/useInputDropdown.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ const mockProps = {
4242
},
4343
},
4444
type: InputTypeType.TEXT,
45+
rightIcon: {
46+
icon: 'UNICORN',
47+
altText: 'Open info',
48+
onClick: jest.fn(),
49+
},
4550
};
4651

4752
jest.useFakeTimers();
@@ -111,6 +116,7 @@ test('handleClickIconInputDropdown function', async () => {
111116
...mockProps,
112117
onOpenCloseOptions: mockOnOpenCloseOptions,
113118
onIconClick: mockOnIconClick,
119+
onRightIconClick: mockOnIconClick,
114120
value: 'label1',
115121
})
116122
);

0 commit comments

Comments
 (0)