Skip to content

Commit acf2972

Browse files
author
Hector Arce De Las Heras
committed
Add Badge component
1 parent 8f34efa commit acf2972

File tree

12 files changed

+906
-0
lines changed

12 files changed

+906
-0
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { act, fireEvent, screen } from '@testing-library/react';
2+
import * as React from 'react';
3+
4+
import { axe } from 'jest-axe';
5+
6+
import { renderProvider } from '@/tests/renderProvider/renderProvider.utility';
7+
import { windowMatchMedia } from '@/tests/windowMatchMedia';
8+
9+
import { BadgeUnControlled as Badge } from '../badgeUnControlled';
10+
import { IBadgeUnControlled } from '../types';
11+
12+
window.matchMedia = windowMatchMedia();
13+
14+
// Mocks
15+
const mockProps: IBadgeUnControlled = {
16+
dataTestId: 'badge-component',
17+
variant: 'PRIMARY',
18+
size: 'DEFAULT',
19+
dot: {
20+
variant: 'WITH_BORDER',
21+
size: 'MEDIUM',
22+
number: 23,
23+
maxNumber: 99,
24+
},
25+
popover: {
26+
variant: 'BADGE',
27+
content: (
28+
<div>
29+
<h1>Hello</h1>
30+
</div>
31+
),
32+
},
33+
icon: { icon: 'CONTACTS' },
34+
label: { content: 'Notifications' },
35+
labelIcon: { icon: 'CHEVRON_DOWN' },
36+
ariaLiveText: 'New notification',
37+
['aria-label']: 'Open menu',
38+
};
39+
40+
describe('Badge component', () => {
41+
it('Should be displayed correctly', async () => {
42+
const ref = jest.fn();
43+
const { container } = renderProvider(<Badge ref={ref} {...mockProps} />);
44+
45+
const badge = screen.getByTestId(mockProps.dataTestId + 'Dot');
46+
expect(badge).toBeInTheDocument();
47+
48+
const results = await axe(container);
49+
expect(container).toHTMLValidate();
50+
expect(results).toHaveNoViolations();
51+
});
52+
53+
it('When it has a dot, the dot position is custompizable using customDotTranslate', async () => {
54+
const ref = jest.fn();
55+
const { container } = renderProvider(
56+
<Badge ref={ref} {...mockProps} customDotTranslate="translate(2px, 2px)" />
57+
);
58+
59+
const badge = screen.getByTestId(mockProps.dataTestId + 'Dot');
60+
expect(badge).toBeInTheDocument();
61+
62+
const results = await axe(container);
63+
expect(container).toHTMLValidate();
64+
expect(results).toHaveNoViolations();
65+
});
66+
67+
it('Should be displayed correctly without label', async () => {
68+
const { container } = renderProvider(<Badge {...mockProps} label={undefined} />);
69+
70+
const badge = screen.getByTestId(mockProps.dataTestId + 'Dot');
71+
expect(badge).toBeInTheDocument();
72+
73+
const results = await axe(container);
74+
expect(container).toHTMLValidate();
75+
expect(results).toHaveNoViolations();
76+
});
77+
78+
it('Should be displayed correctly with simulate onClick', async () => {
79+
const { container } = renderProvider(<Badge {...mockProps} label={undefined} />);
80+
81+
const triggerButton = screen.getByLabelText('Open menu');
82+
fireEvent.click(triggerButton);
83+
84+
const badge = screen.getByTestId(mockProps.dataTestId + 'Dot');
85+
expect(badge).toBeInTheDocument();
86+
87+
const results = await axe(container);
88+
expect(container).toHTMLValidate();
89+
expect(results).toHaveNoViolations();
90+
});
91+
92+
it('Should display contentExpand when open', async () => {
93+
const { container, getByLabelText } = renderProvider(<Badge {...mockProps} />);
94+
95+
const triggerButton = getByLabelText('Open menu');
96+
fireEvent.click(triggerButton);
97+
98+
const contentExpand = screen.getByText('Hello');
99+
expect(contentExpand).toBeInTheDocument();
100+
101+
const results = await axe(container);
102+
expect(container).toHTMLValidate();
103+
expect(results).toHaveNoViolations();
104+
});
105+
106+
it('Should call onClick if defined when open', async () => {
107+
const onClick = jest.fn();
108+
const { container, getByLabelText } = renderProvider(
109+
<Badge {...mockProps} onClick={onClick} />
110+
);
111+
112+
const triggerButton = getByLabelText('Open menu');
113+
fireEvent.click(triggerButton);
114+
115+
expect(onClick).toHaveBeenCalled();
116+
117+
const results = await axe(container);
118+
expect(container).toHTMLValidate();
119+
expect(results).toHaveNoViolations();
120+
});
121+
122+
it('Popover can be closed automatically', async () => {
123+
const { getByLabelText } = renderProvider(<Badge {...mockProps} />);
124+
125+
const triggerButton = getByLabelText('Open menu');
126+
fireEvent.click(triggerButton);
127+
128+
const contentExpand = screen.getByText('Hello');
129+
expect(contentExpand).toBeInTheDocument();
130+
131+
await act(async () => {
132+
fireEvent.keyDown(window, {
133+
key: 'Escape',
134+
code: 'Escape',
135+
});
136+
});
137+
138+
expect(contentExpand).not.toBeInTheDocument();
139+
});
140+
141+
it('When blur badge, it should close', () => {
142+
const { getByLabelText } = renderProvider(
143+
<>
144+
<div data-testid="button-test">test</div>
145+
<Badge {...mockProps} />
146+
</>
147+
);
148+
149+
const triggerButton = getByLabelText('Open menu');
150+
fireEvent.click(triggerButton);
151+
152+
const contentExpand = screen.getByText('Hello');
153+
expect(contentExpand).toBeInTheDocument();
154+
155+
const badgeContainer = screen.getByTestId(mockProps.dataTestId + 'BadgeContainer');
156+
fireEvent.blur(badgeContainer);
157+
158+
expect(contentExpand).not.toBeInTheDocument();
159+
});
160+
});

src/components/badge/badge.styled.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import styled from 'styled-components';
2+
3+
import { getStyles } from '@/utils/getStyles/getStyles';
4+
5+
import { DotUseStateType } from './types';
6+
import { BadgeVariantStylesType } from './types/badgeTheme';
7+
8+
type BadgeStylesProps = {
9+
styles: BadgeVariantStylesType;
10+
};
11+
12+
type BadgeDotProps = {
13+
dotSize?: string;
14+
dotWidthHeight?: DotUseStateType | null;
15+
customDotTranslate?: string;
16+
};
17+
18+
const getTransformProp = (dotWidth: number, dotHeight: number) => {
19+
return `translate(${dotWidth * 0.3}px, -${dotHeight * 0.3}px)`;
20+
};
21+
22+
export const BadgeStyled = styled.button<BadgeStylesProps>`
23+
display: inline-flex;
24+
align-items: center;
25+
flex-direction: column;
26+
cursor: pointer;
27+
${props => getStyles(props.styles.container)}
28+
`;
29+
30+
export const SpanContainerIconAndDot = styled.span`
31+
position: relative;
32+
display: inline-flex;
33+
`;
34+
35+
export const BadgeDotStyled = styled.span<BadgeDotProps>`
36+
z-index: ${props => props.theme.Z_INDEX?.INTERN_1};
37+
position: absolute;
38+
top: 0;
39+
right: 0;
40+
line-height: 0;
41+
transform: ${({ customDotTranslate, dotWidthHeight }) => {
42+
if (customDotTranslate) {
43+
return customDotTranslate;
44+
}
45+
if (dotWidthHeight) {
46+
return getTransformProp(dotWidthHeight.dotWidth, dotWidthHeight.dotHeight);
47+
}
48+
return undefined;
49+
}};
50+
`;
51+
52+
export const BadgeLabelStyled = styled.span<BadgeStylesProps>`
53+
display: inline-flex;
54+
flex-direction: row;
55+
align-items: center;
56+
cursor: pointer;
57+
${props => getStyles(props.styles.labelContainer)}
58+
`;
59+
60+
export const BadgeContainerStyled = styled.div`
61+
position: relative;
62+
`;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as React from 'react';
2+
3+
import { STYLES_NAME } from '@/constants';
4+
import { useStyles } from '@/hooks/useStyles/useStyles';
5+
import { ErrorBoundary, FallbackComponent } from '@/provider/errorBoundary';
6+
7+
import { BadgeStandAlone } from './badgeStandAlone';
8+
import { BadgeStatus } from './types';
9+
import { IBadgeControlled, IBadgeStandAlone } from './types/badge';
10+
import { BadgeSizePropsType, BadgeVariantStylesType } from './types/badgeTheme';
11+
12+
const BadgeControlledComponent = React.forwardRef(
13+
<
14+
V = undefined extends string | unknown ? string | undefined : string | unknown,
15+
S = undefined extends string | unknown ? string | undefined : string | unknown,
16+
>(
17+
{ ctv, cts, ...props }: IBadgeControlled<V, S>,
18+
ref: React.ForwardedRef<HTMLDivElement> | undefined | null
19+
): JSX.Element => {
20+
const styles = useStyles<BadgeVariantStylesType, V>(STYLES_NAME.BADGE, props.variant, ctv);
21+
const iconStyles = styles[props.open ? BadgeStatus.OPEN : BadgeStatus.CLOSE];
22+
const sizeStyles = useStyles<BadgeSizePropsType, S>(STYLES_NAME.BADGE, props.size, cts);
23+
24+
return (
25+
<BadgeStandAlone
26+
{...props}
27+
ref={ref}
28+
iconStyles={iconStyles}
29+
sizeStyles={sizeStyles}
30+
styles={styles}
31+
/>
32+
);
33+
}
34+
);
35+
BadgeControlledComponent.displayName = 'BadgeControlledComponent';
36+
37+
const BagdeBoundary = <
38+
V = undefined extends string | unknown ? string | undefined : string | unknown,
39+
S = undefined extends string | unknown ? string | undefined : string | unknown,
40+
>(
41+
props: IBadgeControlled<V, S>,
42+
ref: React.ForwardedRef<HTMLDivElement> | undefined | null
43+
): JSX.Element => (
44+
<ErrorBoundary
45+
fallBackComponent={
46+
<FallbackComponent>
47+
<BadgeStandAlone {...(props as unknown as IBadgeStandAlone)} ref={ref} />
48+
</FallbackComponent>
49+
}
50+
>
51+
<BadgeControlledComponent {...props} ref={ref} />
52+
</ErrorBoundary>
53+
);
54+
55+
/**
56+
* @description
57+
* Badge component is a component that shows a badge with a number or a dot.
58+
* @param {React.PropsWithChildren<IBadgeControlled<V>>} props
59+
* @returns {JSX.Element}
60+
*/
61+
const BadgeControlled = React.forwardRef(BagdeBoundary) as <
62+
V = undefined extends string | unknown ? string | undefined : string | unknown,
63+
S = undefined extends string | unknown ? string | undefined : string | unknown,
64+
>(
65+
props: React.PropsWithChildren<IBadgeControlled<V, S>> & {
66+
ref?: React.ForwardedRef<HTMLDivElement> | undefined | null;
67+
}
68+
) => JSX.Element;
69+
70+
export { BadgeControlled };

0 commit comments

Comments
 (0)