@transferwise/components 45.13.0 → 45.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/build/index.esm.js +152 -91
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +156 -94
  4. package/build/index.js.map +1 -1
  5. package/build/main.css +1 -1
  6. package/build/styles/inputs/InputGroup.css +1 -0
  7. package/build/styles/main.css +1 -1
  8. package/build/types/avatarWrapper/AvatarWrapper.d.ts +18 -28
  9. package/build/types/avatarWrapper/AvatarWrapper.d.ts.map +1 -1
  10. package/build/types/avatarWrapper/index.d.ts +1 -1
  11. package/build/types/avatarWrapper/index.d.ts.map +1 -1
  12. package/build/types/badge/Badge.d.ts +2 -2
  13. package/build/types/badge/Badge.d.ts.map +1 -1
  14. package/build/types/badge/index.d.ts +2 -1
  15. package/build/types/badge/index.d.ts.map +1 -1
  16. package/build/types/body/Body.d.ts +2 -2
  17. package/build/types/common/closeButton/CloseButton.d.ts +1 -1
  18. package/build/types/common/hooks/useEffectEvent.d.ts +2 -0
  19. package/build/types/common/hooks/useEffectEvent.d.ts.map +1 -0
  20. package/build/types/common/hooks/useResizeObserver.d.ts +3 -0
  21. package/build/types/common/hooks/useResizeObserver.d.ts.map +1 -0
  22. package/build/types/common/index.d.ts +1 -1
  23. package/build/types/common/panel/Panel.d.ts +1 -1
  24. package/build/types/common/propsValues/profileType.d.ts +2 -0
  25. package/build/types/common/propsValues/profileType.d.ts.map +1 -1
  26. package/build/types/common/propsValues/sentiment.d.ts +1 -0
  27. package/build/types/common/propsValues/sentiment.d.ts.map +1 -1
  28. package/build/types/common/responsivePanel/ResponsivePanel.d.ts +1 -1
  29. package/build/types/dateLookup/tableLink/TableLink.d.ts.map +1 -1
  30. package/build/types/dateLookup/yearCalendar/YearCalendar.d.ts.map +1 -1
  31. package/build/types/dimmer/Dimmer.d.ts +1 -1
  32. package/build/types/image/Image.d.ts +17 -21
  33. package/build/types/image/Image.d.ts.map +1 -1
  34. package/build/types/image/index.d.ts +1 -1
  35. package/build/types/image/index.d.ts.map +1 -1
  36. package/build/types/index.d.ts +2 -0
  37. package/build/types/index.d.ts.map +1 -1
  38. package/build/types/inlineAlert/InlineAlert.d.ts +12 -15
  39. package/build/types/inlineAlert/InlineAlert.d.ts.map +1 -1
  40. package/build/types/inlineAlert/index.d.ts +1 -1
  41. package/build/types/inlineAlert/index.d.ts.map +1 -1
  42. package/build/types/inputs/Input.d.ts +4 -8
  43. package/build/types/inputs/Input.d.ts.map +1 -1
  44. package/build/types/inputs/InputGroup.d.ts +21 -0
  45. package/build/types/inputs/InputGroup.d.ts.map +1 -0
  46. package/build/types/inputs/TextArea.d.ts +4 -7
  47. package/build/types/inputs/TextArea.d.ts.map +1 -1
  48. package/build/types/promoCard/PromoCard.d.ts.map +1 -1
  49. package/build/types/select/searchBox/SearchBox.d.ts +1 -1
  50. package/build/types/utilities/cssValueWithUnit.d.ts +2 -0
  51. package/build/types/utilities/cssValueWithUnit.d.ts.map +1 -0
  52. package/package.json +4 -4
  53. package/src/avatarWrapper/AvatarWrapper.spec.tsx +105 -0
  54. package/src/avatarWrapper/AvatarWrapper.story.tsx +1 -41
  55. package/src/avatarWrapper/{AvatarWrapper.js → AvatarWrapper.tsx} +25 -56
  56. package/src/avatarWrapper/__snapshots__/{AvatarWrapper.spec.js.snap → AvatarWrapper.spec.tsx.snap} +76 -76
  57. package/src/badge/Badge.tsx +2 -2
  58. package/src/badge/index.ts +2 -0
  59. package/src/common/hooks/useEffectEvent.ts +22 -0
  60. package/src/common/hooks/useResizeObserver.ts +22 -0
  61. package/src/common/index.js +1 -1
  62. package/src/common/propsValues/profileType.ts +3 -0
  63. package/src/common/propsValues/sentiment.ts +10 -0
  64. package/src/image/{Image.spec.js → Image.spec.tsx} +5 -5
  65. package/src/image/{Image.story.js → Image.story.tsx} +3 -3
  66. package/src/image/Image.tsx +65 -0
  67. package/src/index.ts +2 -0
  68. package/src/inlineAlert/{InlineAlert.story.js → InlineAlert.story.tsx} +3 -2
  69. package/src/inlineAlert/InlineAlert.tsx +59 -0
  70. package/src/inputs/Input.tsx +13 -7
  71. package/src/inputs/InputGroup.css +1 -0
  72. package/src/inputs/InputGroup.less +61 -0
  73. package/src/inputs/InputGroup.story.tsx +73 -0
  74. package/src/inputs/InputGroup.tsx +142 -0
  75. package/src/inputs/TextArea.tsx +7 -6
  76. package/src/main.css +1 -1
  77. package/src/main.less +1 -0
  78. package/src/promoCard/PromoCard.tsx +1 -6
  79. package/src/promoCard/__snapshots__/PromoCardGroup.spec.tsx.snap +2 -2
  80. package/src/utilities/cssValueWithUnit.ts +3 -0
  81. package/src/avatarWrapper/AvatarWrapper.spec.js +0 -81
  82. package/src/badge/index.js +0 -1
  83. package/src/image/Image.js +0 -78
  84. package/src/inlineAlert/InlineAlert.js +0 -62
  85. /package/src/avatarWrapper/{index.js → index.ts} +0 -0
  86. /package/src/image/{index.js → index.ts} +0 -0
  87. /package/src/inlineAlert/{InlineAlert.spec.js → InlineAlert.spec.tsx} +0 -0
  88. /package/src/inlineAlert/{index.js → index.ts} +0 -0
@@ -0,0 +1,105 @@
1
+ import { BadgeProps } from '../badge';
2
+ import { Sentiment } from '../common';
3
+ import { Size, ProfileType } from '../common';
4
+ import { render, screen } from '../test-utils';
5
+
6
+ import AvatarWrapper from '.';
7
+
8
+ const name = 'Elizabeth Alexandra Mary Windsor';
9
+
10
+ describe('FlowNavigationAvatar', () => {
11
+ describe('with a name', () => {
12
+ it('shows the initials for a long name', () => {
13
+ render(<AvatarWrapper name={name} />);
14
+
15
+ expect(screen.getByText('EW')).toBeInTheDocument();
16
+ });
17
+
18
+ it('shows the first letter for a mononym', () => {
19
+ render(<AvatarWrapper name="Zichaleangeol" />);
20
+
21
+ expect(screen.getByText('Z')).toBeInTheDocument();
22
+ });
23
+
24
+ describe('AND profileType', () => {
25
+ describe('FlowNavigationAvatar', () => {
26
+ describe('with a name', () => {
27
+ it('shows the initials for a long name', () => {
28
+ render(<AvatarWrapper name={name} />);
29
+
30
+ expect(screen.getByText('EW')).toBeInTheDocument();
31
+ });
32
+
33
+ it('shows the first letter for a mononym', () => {
34
+ render(<AvatarWrapper name="Michelangelo" />);
35
+
36
+ expect(screen.getByText('M')).toBeInTheDocument();
37
+ });
38
+
39
+ describe('AND profileType', () => {
40
+ it('renders as BUSINESS profile type with an icon', () => {
41
+ const { container } = render(
42
+ <AvatarWrapper name={name} profileType={ProfileType.BUSINESS} />,
43
+ );
44
+
45
+ expect(container.firstChild).toMatchSnapshot();
46
+ });
47
+
48
+ it('renders as PERSONAL profile type with an icon', () => {
49
+ const { container } = render(
50
+ <AvatarWrapper name={name} profileType={ProfileType.PERSONAL} />,
51
+ );
52
+
53
+ expect(container.firstChild).toMatchSnapshot();
54
+ });
55
+
56
+ describe('AND avatar url', () => {
57
+ it('renders the image', () => {
58
+ const { container } = render(
59
+ <AvatarWrapper
60
+ url="https://wise.com"
61
+ name={name}
62
+ profileType={ProfileType.BUSINESS}
63
+ avatarProps={{ theme: 'dark' }}
64
+ />,
65
+ );
66
+
67
+ expect(container.firstChild).toMatchSnapshot();
68
+ });
69
+ });
70
+ });
71
+ });
72
+ });
73
+
74
+ describe('with nothing passed', () => {
75
+ it('renders a personal icon', () => {
76
+ const { container } = render(<AvatarWrapper />);
77
+
78
+ expect(container.firstChild).toMatchSnapshot();
79
+ });
80
+ });
81
+
82
+ describe('with a badge url passed', () => {
83
+ it('renders the badge', () => {
84
+ const { container } = render(
85
+ <AvatarWrapper
86
+ badgeUrl="https://badge.com"
87
+ badgeAltText="badge alt text"
88
+ badgeProps={{ size: Size.LARGE } as BadgeProps}
89
+ />,
90
+ );
91
+
92
+ expect(container.firstChild).toMatchSnapshot();
93
+ });
94
+ });
95
+
96
+ describe('with a badge status icon passed', () => {
97
+ it('renders the badge', () => {
98
+ const { container } = render(<AvatarWrapper badgeStatusIcon={Sentiment.POSITIVE} />);
99
+
100
+ expect(container.firstChild).toMatchSnapshot();
101
+ });
102
+ });
103
+ });
104
+ });
105
+ });
@@ -9,50 +9,10 @@ import AvatarWrapper from './AvatarWrapper';
9
9
  export default {
10
10
  component: AvatarWrapper,
11
11
  title: 'Content/AvatarWrapper',
12
- argTypes: {
13
- profileType: {
14
- type: {
15
- name: 'enum',
16
- value: Object.keys(ProfileType),
17
- },
18
- },
19
- badgeStatusIcon: {
20
- type: {
21
- name: 'enum',
22
- value: [
23
- Sentiment.POSITIVE,
24
- Sentiment.NEGATIVE,
25
- Sentiment.NEUTRAL,
26
- Sentiment.WARNING,
27
- Sentiment.PENDING,
28
- ],
29
- },
30
- },
31
- },
32
- tags: ['autodocs'],
33
12
  } satisfies Meta<typeof AvatarWrapper>;
34
13
 
35
14
  type Story = StoryObj<typeof AvatarWrapper>;
36
15
 
37
- export const WithBadgeUrl: Story = {
38
- args: {
39
- badgeUrl: 'https://wise.com/public-resources/assets/brand/fast_flag_badge_personal.svg',
40
- avatarProps: {
41
- outlined: true,
42
- },
43
- },
44
- };
45
-
46
- export const WithBadgeStatus: Story = {
47
- args: {
48
- profileType: ProfileType.BUSINESS,
49
- badgeStatusIcon: Sentiment.PENDING,
50
- avatarProps: {
51
- outlined: false,
52
- },
53
- },
54
- };
55
-
56
16
  export const All: Story = {
57
17
  args: {
58
18
  avatarProps: { outlined: true },
@@ -85,7 +45,7 @@ export const All: Story = {
85
45
  </div>
86
46
  Initials
87
47
  <div>
88
- <AvatarWrapper name="A A" avatarProps={avatarProps} />
48
+ <AvatarWrapper name="Any Aname" avatarProps={avatarProps} />
89
49
  </div>
90
50
  Default
91
51
  <div>
@@ -1,13 +1,19 @@
1
1
  import { Person as ProfileIcon, Briefcase as BriefcaseIcon } from '@transferwise/icons';
2
- import PropTypes from 'prop-types';
3
2
  import { useState, useEffect } from 'react';
4
3
 
5
- import Avatar, { AvatarType } from '../avatar';
6
- import Badge from '../badge';
7
- import { ProfileType, Size, Sentiment } from '../common';
4
+ import Avatar, { AvatarProps, AvatarType } from '../avatar';
5
+ import Badge, { BadgeProps } from '../badge';
6
+ import { ProfileType, ProfileTypePersonal, ProfileTypeBusiness, Size, Sentiment } from '../common';
8
7
  import StatusIcon from '../statusIcon/StatusIcon';
9
8
 
10
- const OptionalBadge = ({ url, altText, statusIcon, children, ...rest }) => {
9
+ interface OptionalBadgeProps {
10
+ url?: string;
11
+ altText?: string;
12
+ statusIcon?: Sentiment;
13
+ children: React.ReactNode;
14
+ }
15
+
16
+ const OptionalBadge = ({ url, altText, statusIcon, children, ...rest }: OptionalBadgeProps) => {
11
17
  if (url) {
12
18
  return (
13
19
  <Badge badge={<img src={url} alt={altText} />} {...rest}>
@@ -22,27 +28,20 @@ const OptionalBadge = ({ url, altText, statusIcon, children, ...rest }) => {
22
28
  </Badge>
23
29
  );
24
30
  }
25
- return children;
31
+ return <>{children}</>;
26
32
  };
27
33
 
28
- OptionalBadge.propTypes = {
29
- url: PropTypes.string,
30
- altText: PropTypes.string,
31
- statusIcon: PropTypes.oneOf([
32
- Sentiment.POSITIVE,
33
- Sentiment.NEGATIVE,
34
- Sentiment.NEUTRAL,
35
- Sentiment.WARNING,
36
- Sentiment.PENDING,
37
- ]),
38
- children: PropTypes.node.isRequired,
39
- };
40
-
41
- OptionalBadge.defaultProps = {
42
- url: undefined,
43
- altText: undefined,
44
- statusIcon: undefined,
45
- };
34
+ export interface AvatarWrapperProps {
35
+ url?: string;
36
+ profileType?: ProfileTypeBusiness | ProfileTypePersonal;
37
+ profileId?: string;
38
+ badgeUrl?: string;
39
+ badgeAltText?: string;
40
+ badgeStatusIcon?: Sentiment;
41
+ name?: string;
42
+ avatarProps?: AvatarProps;
43
+ badgeProps?: BadgeProps;
44
+ }
46
45
 
47
46
  const AvatarWrapper = ({
48
47
  url,
@@ -54,7 +53,7 @@ const AvatarWrapper = ({
54
53
  name,
55
54
  avatarProps,
56
55
  badgeProps,
57
- }) => {
56
+ }: AvatarWrapperProps) => {
58
57
  const [hasImageLoadError, setImageLoadError] = useState(false);
59
58
  const isBusinessProfile = profileType === ProfileType.BUSINESS;
60
59
 
@@ -103,7 +102,7 @@ const AvatarWrapper = ({
103
102
  );
104
103
  };
105
104
 
106
- function getInitials(name) {
105
+ function getInitials(name: string) {
107
106
  const allInitials = name
108
107
  .split(' ')
109
108
  .map((part) => part[0])
@@ -117,34 +116,4 @@ function getInitials(name) {
117
116
  return allInitials[0] + allInitials.slice(-1);
118
117
  }
119
118
 
120
- AvatarWrapper.defaultProps = {
121
- url: undefined,
122
- profileType: undefined,
123
- profileId: undefined,
124
- badgeUrl: undefined,
125
- badgeAltText: undefined,
126
- badgeStatusIcon: undefined,
127
- name: undefined,
128
- avatarProps: {},
129
- badgeProps: {},
130
- };
131
-
132
- AvatarWrapper.propTypes = {
133
- url: PropTypes.string,
134
- profileType: PropTypes.oneOf([ProfileType.PERSONAL, ProfileType.BUSINESS]),
135
- profileId: PropTypes.number,
136
- badgeUrl: PropTypes.string,
137
- badgeAltText: PropTypes.string,
138
- badgeStatusIcon: PropTypes.oneOf([
139
- Sentiment.POSITIVE,
140
- Sentiment.NEGATIVE,
141
- Sentiment.NEUTRAL,
142
- Sentiment.WARNING,
143
- Sentiment.PENDING,
144
- ]),
145
- name: PropTypes.string,
146
- avatarProps: PropTypes.shape({}),
147
- badgeProps: PropTypes.shape({}),
148
- };
149
-
150
119
  export default AvatarWrapper;
@@ -1,6 +1,79 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
- exports[`FlowNavigationAvatar with a badge status icon passed renders the badge 1`] = `
3
+ exports[`FlowNavigationAvatar with a name AND profileType FlowNavigationAvatar with a name AND profileType AND avatar url renders the image 1`] = `
4
+ <div
5
+ class="tw-avatar tw-avatar--48 tw-avatar--thumbnail"
6
+ >
7
+ <div
8
+ class="tw-avatar__content"
9
+ >
10
+ <img
11
+ alt="avatar"
12
+ src="https://wise.com"
13
+ />
14
+ </div>
15
+ </div>
16
+ `;
17
+
18
+ exports[`FlowNavigationAvatar with a name AND profileType FlowNavigationAvatar with a name AND profileType renders as BUSINESS profile type with an icon 1`] = `
19
+ <div
20
+ class="tw-avatar tw-avatar--48 tw-avatar--icon"
21
+ >
22
+ <div
23
+ class="tw-avatar__content"
24
+ >
25
+ <span
26
+ aria-hidden="true"
27
+ class="tw-icon tw-icon-briefcase "
28
+ data-testid="briefcase-icon"
29
+ role="presentation"
30
+ >
31
+ <svg
32
+ fill="currentColor"
33
+ focusable="false"
34
+ height="24"
35
+ viewBox="0 0 24 24"
36
+ width="24"
37
+ >
38
+ <path
39
+ d="M21.428 6.429H18V3.857A.86.86 0 0 0 17.143 3H6.857A.86.86 0 0 0 6 3.857V6.43H2.571a.86.86 0 0 0-.857.857v12.857a.86.86 0 0 0 .857.857h18.857a.86.86 0 0 0 .858-.857V7.286a.86.86 0 0 0-.858-.857ZM7.714 4.714h8.572V6.43H7.714V4.714Zm8.572 3.429v11.143H7.714V8.143h8.572Zm-12.857 0H6v11.143H3.429V8.143ZM20.57 19.286H18V8.143h2.571v11.143Z"
40
+ />
41
+ </svg>
42
+ </span>
43
+ </div>
44
+ </div>
45
+ `;
46
+
47
+ exports[`FlowNavigationAvatar with a name AND profileType FlowNavigationAvatar with a name AND profileType renders as PERSONAL profile type with an icon 1`] = `
48
+ <div
49
+ class="tw-avatar tw-avatar--48 tw-avatar--icon"
50
+ >
51
+ <div
52
+ class="tw-avatar__content"
53
+ >
54
+ <span
55
+ aria-hidden="true"
56
+ class="tw-icon tw-icon-person "
57
+ data-testid="person-icon"
58
+ role="presentation"
59
+ >
60
+ <svg
61
+ fill="currentColor"
62
+ focusable="false"
63
+ height="24"
64
+ viewBox="0 0 24 24"
65
+ width="24"
66
+ >
67
+ <path
68
+ d="M15.257 14.014A5.922 5.922 0 0 0 18 9c0-3.3-2.7-6-6-6S6 5.7 6 9c0 2.1 1.071 3.943 2.743 5.014-2.614 1.243-4.457 3.9-4.457 6.986H6c0-3.3 2.7-6 6-6s6 2.7 6 6h1.714c0-3.086-1.842-5.743-4.457-6.986ZM7.714 9A4.298 4.298 0 0 1 12 4.714 4.298 4.298 0 0 1 16.286 9 4.298 4.298 0 0 1 12 13.286 4.298 4.298 0 0 1 7.714 9Z"
69
+ />
70
+ </svg>
71
+ </span>
72
+ </div>
73
+ </div>
74
+ `;
75
+
76
+ exports[`FlowNavigationAvatar with a name AND profileType with a badge status icon passed renders the badge 1`] = `
4
77
  <div
5
78
  class="tw-badge tw-badge-border-light tw-badge-sm"
6
79
  >
@@ -64,7 +137,7 @@ exports[`FlowNavigationAvatar with a badge status icon passed renders the badge
64
137
  </div>
65
138
  `;
66
139
 
67
- exports[`FlowNavigationAvatar with a badge url passed renders the badge 1`] = `
140
+ exports[`FlowNavigationAvatar with a name AND profileType with a badge url passed renders the badge 1`] = `
68
141
  <div
69
142
  class="tw-badge tw-badge-border-light tw-badge-lg"
70
143
  >
@@ -109,80 +182,7 @@ exports[`FlowNavigationAvatar with a badge url passed renders the badge 1`] = `
109
182
  </div>
110
183
  `;
111
184
 
112
- exports[`FlowNavigationAvatar with a name AND profileType AND avatar url renders the image 1`] = `
113
- <div
114
- class="tw-avatar tw-avatar--48 tw-avatar--thumbnail"
115
- >
116
- <div
117
- class="tw-avatar__content"
118
- >
119
- <img
120
- alt="avatar"
121
- src="https://wise.com"
122
- />
123
- </div>
124
- </div>
125
- `;
126
-
127
- exports[`FlowNavigationAvatar with a name AND profileType renders as BUSINESS profile type with an icon 1`] = `
128
- <div
129
- class="tw-avatar tw-avatar--48 tw-avatar--icon"
130
- >
131
- <div
132
- class="tw-avatar__content"
133
- >
134
- <span
135
- aria-hidden="true"
136
- class="tw-icon tw-icon-briefcase "
137
- data-testid="briefcase-icon"
138
- role="presentation"
139
- >
140
- <svg
141
- fill="currentColor"
142
- focusable="false"
143
- height="24"
144
- viewBox="0 0 24 24"
145
- width="24"
146
- >
147
- <path
148
- d="M21.428 6.429H18V3.857A.86.86 0 0 0 17.143 3H6.857A.86.86 0 0 0 6 3.857V6.43H2.571a.86.86 0 0 0-.857.857v12.857a.86.86 0 0 0 .857.857h18.857a.86.86 0 0 0 .858-.857V7.286a.86.86 0 0 0-.858-.857ZM7.714 4.714h8.572V6.43H7.714V4.714Zm8.572 3.429v11.143H7.714V8.143h8.572Zm-12.857 0H6v11.143H3.429V8.143ZM20.57 19.286H18V8.143h2.571v11.143Z"
149
- />
150
- </svg>
151
- </span>
152
- </div>
153
- </div>
154
- `;
155
-
156
- exports[`FlowNavigationAvatar with a name AND profileType renders as PERSONAL profile type with an icon 1`] = `
157
- <div
158
- class="tw-avatar tw-avatar--48 tw-avatar--icon"
159
- >
160
- <div
161
- class="tw-avatar__content"
162
- >
163
- <span
164
- aria-hidden="true"
165
- class="tw-icon tw-icon-person "
166
- data-testid="person-icon"
167
- role="presentation"
168
- >
169
- <svg
170
- fill="currentColor"
171
- focusable="false"
172
- height="24"
173
- viewBox="0 0 24 24"
174
- width="24"
175
- >
176
- <path
177
- d="M15.257 14.014A5.922 5.922 0 0 0 18 9c0-3.3-2.7-6-6-6S6 5.7 6 9c0 2.1 1.071 3.943 2.743 5.014-2.614 1.243-4.457 3.9-4.457 6.986H6c0-3.3 2.7-6 6-6s6 2.7 6 6h1.714c0-3.086-1.842-5.743-4.457-6.986ZM7.714 9A4.298 4.298 0 0 1 12 4.714 4.298 4.298 0 0 1 16.286 9 4.298 4.298 0 0 1 12 13.286 4.298 4.298 0 0 1 7.714 9Z"
178
- />
179
- </svg>
180
- </span>
181
- </div>
182
- </div>
183
- `;
184
-
185
- exports[`FlowNavigationAvatar with nothing passed renders a personal icon 1`] = `
185
+ exports[`FlowNavigationAvatar with a name AND profileType with nothing passed renders a personal icon 1`] = `
186
186
  <div
187
187
  class="tw-avatar tw-avatar--48 tw-avatar--icon"
188
188
  >
@@ -12,7 +12,7 @@ import {
12
12
  CommonProps,
13
13
  } from '../common';
14
14
 
15
- type Props = {
15
+ export type BadgeProps = {
16
16
  badge: ReactNode;
17
17
  children: ReactNode;
18
18
  size?: SizeSmall | SizeMedium | SizeLarge;
@@ -25,7 +25,7 @@ const Badge = ({
25
25
  size = Size.SMALL,
26
26
  border = Theme.LIGHT,
27
27
  children,
28
- }: Props): ReactElement => {
28
+ }: BadgeProps): ReactElement => {
29
29
  const classes: string = classNames(
30
30
  'tw-badge',
31
31
  {
@@ -0,0 +1,2 @@
1
+ export { default } from './Badge';
2
+ export type { BadgeProps } from './Badge';
@@ -0,0 +1,22 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+
3
+ /*
4
+ * Inspired by:
5
+ *
6
+ * - https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event
7
+ * - https://legacy.reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback
8
+ */
9
+
10
+ export function useEffectEvent<A extends unknown[], R>(
11
+ callback: (...args: A) => R,
12
+ ): typeof callback {
13
+ const ref = useRef<typeof callback>(() => {
14
+ throw new Error('Cannot call an event handler while rendering.');
15
+ });
16
+
17
+ useEffect(() => {
18
+ ref.current = callback;
19
+ });
20
+
21
+ return useCallback((...args) => ref.current(...args), []);
22
+ }
@@ -0,0 +1,22 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import { useEffectEvent } from './useEffectEvent';
4
+
5
+ export function useResizeObserver(
6
+ ref: React.RefObject<Element>,
7
+ callback: (entry: ResizeObserverEntry) => void,
8
+ ) {
9
+ const handleCallback = useEffectEvent(callback);
10
+ useEffect(() => {
11
+ if (ref.current != null) {
12
+ const resizeObserver = new ResizeObserver(([entry]) => {
13
+ handleCallback(entry);
14
+ });
15
+ resizeObserver.observe(ref.current, { box: 'border-box' });
16
+ return () => {
17
+ resizeObserver.disconnect();
18
+ };
19
+ }
20
+ return () => {};
21
+ }, [handleCallback, ref]);
22
+ }
@@ -16,7 +16,7 @@ export * from './propsValues/position';
16
16
  export { Layout } from './propsValues/layouts';
17
17
  export * from './propsValues/status';
18
18
  export { Sentiment } from './propsValues/sentiment';
19
- export { ProfileType } from './propsValues/profileType';
19
+ export * from './propsValues/profileType';
20
20
  export { Variant } from './propsValues/variant';
21
21
  export * from './propsValues/scroll';
22
22
  export { MarkdownNodeType } from './propsValues/markdownNodeType';
@@ -2,3 +2,6 @@ export enum ProfileType {
2
2
  BUSINESS = 'BUSINESS',
3
3
  PERSONAL = 'PERSONAL',
4
4
  }
5
+
6
+ export type ProfileTypeBusiness = 'BUSINESS';
7
+ export type ProfileTypePersonal = 'PERSONAL';
@@ -18,3 +18,13 @@ export enum Sentiment {
18
18
  */
19
19
  SUCCESS = 'success',
20
20
  }
21
+
22
+ export type SentimentString =
23
+ | 'negative'
24
+ | 'neutral'
25
+ | 'positive'
26
+ | 'warning'
27
+ | 'pending'
28
+ | 'info'
29
+ | 'error'
30
+ | 'success';
@@ -2,11 +2,11 @@ import '@testing-library/jest-dom';
2
2
  import * as useHasIntersectedUtils from '../common/hooks/useHasIntersected/useHasIntersected';
3
3
  import { render, fireEvent, screen } from '../test-utils';
4
4
 
5
- import { EmptyTransparentImage } from './Image';
5
+ import { EmptyTransparentImage, ImageProps } from './Image';
6
6
 
7
7
  import Image from '.';
8
8
 
9
- const props = {
9
+ const props: ImageProps = {
10
10
  id: 'id',
11
11
  src: 'https://www.a-valid-src.png/',
12
12
  alt: 'test',
@@ -23,7 +23,7 @@ describe('Image', () => {
23
23
  it(`renders image`, () => {
24
24
  render(<Image {...props} loading="eager" />);
25
25
 
26
- const image = screen.getByRole('img');
26
+ const image: HTMLImageElement = screen.getByRole('img');
27
27
 
28
28
  expect(image).toBeInTheDocument();
29
29
  expect(image.src).toStrictEqual(props.src);
@@ -37,7 +37,7 @@ describe('Image', () => {
37
37
  jest.spyOn(useHasIntersectedUtils, 'useHasIntersected').mockReturnValue([false]);
38
38
  render(<Image {...props} />);
39
39
 
40
- const image = screen.getByRole('img');
40
+ const image: HTMLImageElement = screen.getByRole('img');
41
41
 
42
42
  expect(image).toBeInTheDocument();
43
43
  expect(image.src).toStrictEqual(EmptyTransparentImage);
@@ -48,7 +48,7 @@ describe('Image', () => {
48
48
 
49
49
  render(<Image {...props} />);
50
50
 
51
- const image = screen.getByRole('img');
51
+ const image: HTMLImageElement = screen.getByRole('img');
52
52
 
53
53
  expect(image).toBeInTheDocument();
54
54
  expect(image.src).toStrictEqual(props.src);
@@ -33,7 +33,7 @@ export const Basic = () => {
33
33
  shrink={shrink}
34
34
  stretch={stretch}
35
35
  onLoad={action('load 1')}
36
- onError={(error) => action(error)}
36
+ onError={action('error')}
37
37
  />
38
38
  <Image
39
39
  alt="test"
@@ -44,7 +44,7 @@ export const Basic = () => {
44
44
  shrink={shrink}
45
45
  stretch={stretch}
46
46
  onLoad={action('load 2')}
47
- onError={(error) => action(error)}
47
+ onError={action('error')}
48
48
  />
49
49
  <Image
50
50
  alt="test"
@@ -55,7 +55,7 @@ export const Basic = () => {
55
55
  shrink={shrink}
56
56
  stretch={stretch}
57
57
  onLoad={action('load 3')}
58
- onError={(error) => action(error)}
58
+ onError={action('error')}
59
59
  />
60
60
  </>
61
61
  );
@@ -0,0 +1,65 @@
1
+ import classnames from 'classnames';
2
+ import { useRef } from 'react';
3
+
4
+ import { useHasIntersected } from '../common/hooks';
5
+
6
+ export interface ImageProps {
7
+ alt: string;
8
+ src: string;
9
+ id?: string;
10
+ onLoad?: () => void;
11
+ onError?: () => void;
12
+ className?: string;
13
+ loading?: 'lazy' | 'eager';
14
+ stretch?: boolean;
15
+ role?: string;
16
+ shrink?: boolean;
17
+ }
18
+
19
+ export const EmptyTransparentImage =
20
+ 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
21
+
22
+ const Image = ({
23
+ id,
24
+ src,
25
+ alt,
26
+ onLoad,
27
+ onError,
28
+ className,
29
+ loading,
30
+ stretch = true,
31
+ role,
32
+ shrink = true,
33
+ }: ImageProps) => {
34
+ const elementReference = useRef<HTMLImageElement>(null);
35
+ const [hasIntersected] = useHasIntersected({ elRef: elementReference, loading });
36
+ let imageSource = src;
37
+ let imageOnLoad = onLoad;
38
+
39
+ if (loading === 'lazy' && !hasIntersected) {
40
+ imageSource = EmptyTransparentImage;
41
+ imageOnLoad = undefined;
42
+ }
43
+
44
+ return (
45
+ <img
46
+ ref={elementReference}
47
+ id={id}
48
+ alt={alt}
49
+ src={imageSource}
50
+ className={classnames([
51
+ 'tw-image',
52
+ {
53
+ 'tw-image__stretch': stretch,
54
+ 'tw-image__shrink': shrink,
55
+ },
56
+ className,
57
+ ])}
58
+ role={role}
59
+ onLoad={imageOnLoad}
60
+ onError={onError}
61
+ />
62
+ );
63
+ };
64
+
65
+ export default Image;