Skip to content

Commit 3bf10c9

Browse files
author
Hector Arce De Las Heras
committed
Enhancements to SelectorBoxFile Component for Kubit Theme Compliance
This pull request introduces a series of updates to the SelectorBoxFile component, aligning it with the Kubit theme design specifications. The changes span across styling, functionality, and documentation, ensuring the component meets the latest design requirements
1 parent ed4ec90 commit 3bf10c9

18 files changed

+1461
-0
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { fireEvent, screen } from '@testing-library/react';
2+
import * as React from 'react';
3+
4+
import { axe } from 'jest-axe';
5+
6+
import { ICONS } from '@/assets';
7+
import { IconPositionType } from '@/components/button';
8+
import { TooltipAlignType } from '@/components/tooltip';
9+
import { renderProvider } from '@/tests/renderProvider/renderProvider.utility';
10+
import { ROLES } from '@/types';
11+
12+
import { SelectorBoxFile } from '../selectorBoxFile';
13+
import { SelectorBoxFileStateType } from '../types';
14+
15+
const mockProps = {
16+
variant: 'DEFAULT',
17+
title: { content: 'Title: Lorem Impsum' },
18+
subtitle: { content: 'Subtitle: Lorem ipsum dolor sit' },
19+
tooltipIcon: { icon: 'WARNING_IN_A_CIRCLE', altText: 'altTextTooltipIcon' },
20+
tooltip: {
21+
align: TooltipAlignType.RIGHT,
22+
title: { content: 'Tooltip title' },
23+
content: { content: 'Tooltip content' },
24+
closeIcon: { icon: 'CLOSE', altText: 'Close popover' },
25+
},
26+
containerBoxStateContent: {
27+
[SelectorBoxFileStateType.DEFAULT]: {
28+
icon: { icon: 'UPLOAD' },
29+
actionText: { content: 'Browse and select a file' },
30+
descriptionText: { content: 'and upload it here' },
31+
},
32+
[SelectorBoxFileStateType.LOADING]: {
33+
icon: { icon: 'UPLOAD' },
34+
actionText: { content: 'Cancel upload' },
35+
},
36+
[SelectorBoxFileStateType.SUCCESS]: {
37+
icon: { icon: 'CHECKMARK' },
38+
actionText: { content: 'Delete file' },
39+
},
40+
[SelectorBoxFileStateType.ERROR]: {
41+
icon: { icon: 'RENOVATION' },
42+
actionText: { content: 'Try again' },
43+
},
44+
[SelectorBoxFileStateType.DISABLED]: {
45+
icon: { icon: 'UPLOAD' },
46+
actionText: { content: 'Browse and select a file' },
47+
descriptionText: { content: 'and upload it here' },
48+
},
49+
},
50+
filename: '12345678asdfghj.pdf',
51+
errorMessageIcon: { icon: 'ERROR_BI_COLOR' },
52+
errorMessage: { content: 'Error uploading document' },
53+
description: { content: 'DescriptionNoLink' },
54+
button: {
55+
content: 'Link description',
56+
icon: { icon: ICONS.ICON_PLACEHOLDER, altText: 'altText' },
57+
iconPosition: IconPositionType.LEFT,
58+
},
59+
};
60+
61+
describe('SelectorBoxFile', () => {
62+
it('Title and subtitle may be present', async () => {
63+
const { container } = renderProvider(<SelectorBoxFile {...mockProps} />);
64+
const title = screen.getByText(mockProps.title.content);
65+
expect(title).toBeInTheDocument();
66+
const subtitle = screen.getByText(mockProps.subtitle.content);
67+
expect(subtitle).toBeInTheDocument();
68+
const tooltipIcon = screen.getByAltText(mockProps.tooltipIcon.altText);
69+
expect(tooltipIcon).toBeInTheDocument();
70+
const inputFile = screen.getByLabelText(
71+
new RegExp(mockProps.containerBoxStateContent.DEFAULT.actionText.content)
72+
);
73+
expect(inputFile).toBeInTheDocument();
74+
75+
const results = await axe(container);
76+
expect(container).toHTMLValidate({
77+
// Fix in the future: Currently the tooltip have internal div, so the tooltip can not be used next to text
78+
rules: {
79+
'element-permitted-content': 'off',
80+
},
81+
});
82+
expect(results).toHaveNoViolations();
83+
});
84+
85+
it('Title may not be present', () => {
86+
renderProvider(<SelectorBoxFile {...mockProps} title={undefined} />);
87+
const title = screen.queryByText(mockProps.title.content);
88+
expect(title).not.toBeInTheDocument();
89+
const subtitle = screen.getByText(mockProps.subtitle.content);
90+
expect(subtitle).toBeInTheDocument();
91+
});
92+
93+
it('Subtitle may not be present', () => {
94+
renderProvider(<SelectorBoxFile {...mockProps} subtitle={undefined} />);
95+
const title = screen.getByText(mockProps.title.content);
96+
expect(title).toBeInTheDocument();
97+
const subtitle = screen.queryByText(mockProps.subtitle.content);
98+
expect(subtitle).not.toBeInTheDocument();
99+
});
100+
101+
it('Title and subtitle may not be present', () => {
102+
renderProvider(<SelectorBoxFile {...mockProps} subtitle={undefined} title={undefined} />);
103+
const title = screen.queryByText(mockProps.title.content);
104+
expect(title).not.toBeInTheDocument();
105+
const subtitle = screen.queryByText(mockProps.subtitle.content);
106+
expect(subtitle).not.toBeInTheDocument();
107+
});
108+
109+
it('When errorMessage and error, error message is shown', () => {
110+
renderProvider(<SelectorBoxFile {...mockProps} error />);
111+
const errorMessage = screen.getByText(mockProps.errorMessage.content);
112+
expect(errorMessage).toBeInTheDocument();
113+
});
114+
115+
it('errorMessage is optional even for error state', async () => {
116+
const { container } = renderProvider(
117+
<SelectorBoxFile {...mockProps} error errorMessage={undefined} />
118+
);
119+
const inputFile = screen.getByLabelText(
120+
new RegExp(mockProps.containerBoxStateContent.ERROR.actionText.content)
121+
);
122+
expect(inputFile).toBeInTheDocument();
123+
124+
const results = await axe(container);
125+
expect(container).toHTMLValidate({
126+
// Fix in the future: Currently the tooltip have internal div, so the tooltip can not be used next to text
127+
rules: {
128+
'element-permitted-content': 'off',
129+
},
130+
});
131+
expect(results).toHaveNoViolations();
132+
});
133+
134+
it('Tooltip icon may not be present', () => {
135+
renderProvider(<SelectorBoxFile {...mockProps} tooltipIcon={undefined} />);
136+
const tooltipIcon = screen.queryByLabelText(mockProps.tooltipIcon.altText);
137+
expect(tooltipIcon).not.toBeInTheDocument();
138+
});
139+
140+
it('Tooltip may not be present', () => {
141+
renderProvider(<SelectorBoxFile {...mockProps} tooltip={undefined} />);
142+
const tooltip = screen.queryByRole(ROLES.DIALOG);
143+
expect(tooltip).not.toBeInTheDocument();
144+
const tooltipIcon = screen.getByAltText(mockProps.tooltipIcon.altText);
145+
expect(tooltipIcon).toBeInTheDocument();
146+
});
147+
148+
it('Description and descriptionLink may be present', () => {
149+
renderProvider(<SelectorBoxFile {...mockProps} />);
150+
const linkButtonName = mockProps.button.icon.altText + ' ' + mockProps.button.content;
151+
const linkButton = screen.getByRole(ROLES.BUTTON, { name: linkButtonName });
152+
expect(linkButton).toBeInTheDocument();
153+
});
154+
155+
it('DescriptionLink may be present without and description', () => {
156+
renderProvider(<SelectorBoxFile {...mockProps} description={undefined} />);
157+
const linkButtonName = mockProps.button.icon.altText + ' ' + mockProps.button.content;
158+
const linkButton = screen.queryByRole(ROLES.BUTTON, { name: linkButtonName });
159+
expect(linkButton).toBeInTheDocument();
160+
});
161+
162+
it('May not have actionText for ha given state', async () => {
163+
const { container } = renderProvider(
164+
<SelectorBoxFile
165+
{...mockProps}
166+
loading
167+
containerBoxStateContent={{
168+
...mockProps.containerBoxStateContent,
169+
LOADING: {
170+
actionText: undefined,
171+
description: { content: 'descriptionText' },
172+
icon: { ...mockProps.containerBoxStateContent.LOADING.icon, altText: 'iconAltText' },
173+
},
174+
}}
175+
/>
176+
);
177+
178+
const results = await axe(container);
179+
expect(container).toHTMLValidate({
180+
// Fix in the future: Currently the tooltip have internal div, so the tooltip can not be used next to text
181+
rules: {
182+
'element-permitted-content': 'off',
183+
},
184+
});
185+
expect(results).toHaveNoViolations();
186+
});
187+
188+
it('May have loading state', () => {
189+
renderProvider(<SelectorBoxFile {...mockProps} loading />);
190+
const inputFile = screen.getByLabelText(
191+
new RegExp(mockProps.containerBoxStateContent.LOADING.actionText.content)
192+
);
193+
expect(inputFile).toBeInTheDocument();
194+
});
195+
196+
it('May have success state', () => {
197+
renderProvider(<SelectorBoxFile {...mockProps} success />);
198+
const inputFile = screen.getByLabelText(
199+
new RegExp(mockProps.containerBoxStateContent.SUCCESS.actionText.content)
200+
);
201+
expect(inputFile).toBeInTheDocument();
202+
});
203+
204+
it('May have error state', () => {
205+
renderProvider(<SelectorBoxFile {...mockProps} error />);
206+
const inputFile = screen.getByLabelText(
207+
new RegExp(mockProps.containerBoxStateContent.ERROR.actionText.content)
208+
);
209+
expect(inputFile).toBeInTheDocument();
210+
});
211+
212+
it('May have disabled state', () => {
213+
renderProvider(<SelectorBoxFile {...mockProps} disabled />);
214+
const inputFile = screen.getByLabelText(
215+
new RegExp(mockProps.containerBoxStateContent.DISABLED.actionText.content)
216+
);
217+
expect(inputFile).toBeInTheDocument();
218+
});
219+
220+
it('May focus and on blur on the input', () => {
221+
// Test only ussed to increase test coverage
222+
// onFocus and onBlur only change styles
223+
renderProvider(<SelectorBoxFile {...mockProps} />);
224+
const inputFile = screen.getByLabelText(
225+
new RegExp(mockProps.containerBoxStateContent.DEFAULT.actionText.content)
226+
);
227+
fireEvent.focus(inputFile);
228+
fireEvent.blur(inputFile);
229+
expect(inputFile).toBeInTheDocument();
230+
});
231+
232+
it('When onChange and fileExtension is not valid, onFileError should be called', () => {
233+
const onFileError = jest.fn();
234+
renderProvider(
235+
<SelectorBoxFile {...mockProps} fileExtension={['pdf']} onFileError={onFileError} />
236+
);
237+
const inputFile = screen.getByLabelText(
238+
new RegExp(
239+
mockProps.containerBoxStateContent[SelectorBoxFileStateType.DEFAULT].actionText.content
240+
)
241+
);
242+
const file = new File([''], 'filename.jpg');
243+
fireEvent.change(inputFile, { target: { files: [file] } });
244+
expect(onFileError).toHaveBeenCalled();
245+
});
246+
247+
it('When onChange and maxSize is not valid, onSizeError should be called', () => {
248+
const onSizeError = jest.fn();
249+
renderProvider(<SelectorBoxFile {...mockProps} maxSize={0.000001} onSizeError={onSizeError} />);
250+
const inputFile = screen.getByLabelText(
251+
new RegExp(
252+
mockProps.containerBoxStateContent[SelectorBoxFileStateType.DEFAULT].actionText.content
253+
)
254+
);
255+
const file = new File(['0123456789'], 'filename.jpg');
256+
fireEvent.change(inputFile, { target: { files: [file] } });
257+
expect(onSizeError).toHaveBeenCalled();
258+
});
259+
260+
it('When onChange and no files, onSizeError nor onFileError should not be called', () => {
261+
const onSizeError = jest.fn();
262+
const onFileError = jest.fn();
263+
renderProvider(
264+
<SelectorBoxFile {...mockProps} onFileError={onFileError} onSizeError={onSizeError} />
265+
);
266+
const inputFile = screen.getByLabelText(
267+
new RegExp(
268+
mockProps.containerBoxStateContent[SelectorBoxFileStateType.DEFAULT].actionText.content
269+
)
270+
);
271+
272+
fireEvent.change(inputFile, { target: { files: [] } });
273+
expect(onSizeError).not.toHaveBeenCalled();
274+
expect(onFileError).not.toHaveBeenCalled();
275+
});
276+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './selectorBoxFileHeader';
2+
export * from './selectorBoxFileErrorMessage';
3+
export * from './selectorBoxFileContainerBox';
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/* eslint-disable complexity */
2+
import * as React from 'react';
3+
4+
import { ElementOrIcon } from '@/components/elementOrIcon';
5+
import { Loader } from '@/components/loader';
6+
import { Text, TextComponentType } from '@/components/text';
7+
8+
import {
9+
ActionIconAndActionTextContainerStyled,
10+
ContainerBoxActionDescriptionTextWrapper,
11+
ContainerBoxStyled,
12+
ContainerBoxTextWrapper,
13+
} from '../selectorBoxFile.styled';
14+
import {
15+
SelectorBoxFileContainerBoxStateContentType,
16+
SelectorBoxFilePropsStylesType,
17+
SelectorBoxFileStateType,
18+
} from '../types';
19+
20+
interface ISelectorBoxFileContainerBox {
21+
styles: SelectorBoxFilePropsStylesType;
22+
htmlFor: string;
23+
state: SelectorBoxFileStateType;
24+
containerBoxStateContent: SelectorBoxFileContainerBoxStateContentType;
25+
filename?: string;
26+
focus: boolean;
27+
}
28+
29+
export const SelectorBoxFileContainerBox = (props: ISelectorBoxFileContainerBox): JSX.Element => {
30+
return (
31+
<ContainerBoxStyled
32+
data-focus={props.focus}
33+
htmlFor={props.htmlFor}
34+
state={props.state}
35+
styles={props.styles}
36+
>
37+
<span>
38+
<Loader
39+
altText={props.containerBoxStateContent[props.state]?.icon?.altText}
40+
variant={
41+
props.styles?.states?.[SelectorBoxFileStateType.LOADING]?.containerBoxLoader?.variant
42+
}
43+
visible={props.state === SelectorBoxFileStateType.LOADING}
44+
width={
45+
props.styles?.states?.[SelectorBoxFileStateType.LOADING]?.containerBoxLoader?.width
46+
}
47+
/>
48+
{props.state !== SelectorBoxFileStateType.LOADING && (
49+
<ElementOrIcon
50+
customIconStyles={props.styles?.states?.[props.state]?.containerBoxIcon}
51+
{...props.containerBoxStateContent[props.state]?.icon}
52+
/>
53+
)}
54+
</span>
55+
<ContainerBoxTextWrapper state={props.state} styles={props.styles}>
56+
{props.filename &&
57+
[
58+
SelectorBoxFileStateType.LOADING,
59+
SelectorBoxFileStateType.SUCCESS,
60+
SelectorBoxFileStateType.ERROR,
61+
].includes(props.state) ? (
62+
<Text
63+
component={TextComponentType.SPAN}
64+
customTypography={props.styles?.states?.[props.state]?.containerBoxFilename}
65+
>
66+
{props.filename}
67+
</Text>
68+
) : null}
69+
<ContainerBoxActionDescriptionTextWrapper>
70+
{props.containerBoxStateContent[props.state]?.actionText?.content ? (
71+
<ActionIconAndActionTextContainerStyled state={props.state} styles={props.styles}>
72+
<ElementOrIcon
73+
altText={props.containerBoxStateContent[props.state]?.icon?.altText}
74+
customIconStyles={props.styles?.states?.[props.state]?.actionIcon}
75+
{...props.containerBoxStateContent[props.state]?.actionIcon}
76+
/>
77+
<Text
78+
component={TextComponentType.SPAN}
79+
customTypography={props.styles?.states?.[props.state]?.containerBoxActionText}
80+
{...props.containerBoxStateContent[props.state]?.actionText}
81+
>
82+
{props.containerBoxStateContent[props.state]?.actionText?.content}
83+
</Text>
84+
</ActionIconAndActionTextContainerStyled>
85+
) : null}
86+
{props.containerBoxStateContent[props.state]?.description ? (
87+
<Text
88+
component={TextComponentType.SPAN}
89+
customTypography={props.styles?.states?.[props.state]?.containerBoxDescription}
90+
{...props.containerBoxStateContent[props.state]?.description}
91+
>
92+
{props.containerBoxStateContent[props.state]?.description?.content}
93+
</Text>
94+
) : null}
95+
</ContainerBoxActionDescriptionTextWrapper>
96+
</ContainerBoxTextWrapper>
97+
</ContainerBoxStyled>
98+
);
99+
};

0 commit comments

Comments
 (0)