@jetbrains/ring-ui 4.1.0-beta.3 → 4.1.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 (234) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +17 -15
  3. package/babel.config.js +3 -2
  4. package/components/alert/alert.js +9 -3
  5. package/components/alert/alert.test.js +21 -48
  6. package/components/alert/container.css +1 -1
  7. package/components/alert/container.test.js +3 -13
  8. package/components/alert-service/alert-service.examples.css +18 -0
  9. package/components/alert-service/alert-service.examples.js +21 -0
  10. package/components/alert-service/alert-service.js +10 -3
  11. package/components/analytics/analytics__fus-plugin.js +3 -3
  12. package/components/analytics/analytics__ga-plugin.js +2 -2
  13. package/components/auth/auth.test.js +14 -7
  14. package/components/auth/auth__core.js +64 -33
  15. package/components/auth-dialog/auth-dialog.css +2 -3
  16. package/components/auth-dialog/auth-dialog.js +4 -1
  17. package/components/auth-dialog/auth-dialog.test.js +3 -19
  18. package/components/avatar/avatar.css +4 -1
  19. package/components/avatar/avatar.examples.js +3 -2
  20. package/components/avatar/avatar.js +34 -6
  21. package/components/avatar/avatar.test.js +20 -17
  22. package/components/avatar/fallback-avatar.js +136 -0
  23. package/components/avatar-editor-ng/avatar-editor-ng.css +2 -2
  24. package/components/avatar-editor-ng/avatar-editor-ng.js +2 -1
  25. package/components/avatar-editor-ng/{avatar-editor-ng.html → avatar-editor-ng__template.js} +2 -2
  26. package/components/badge/badge.test.js +13 -20
  27. package/components/button/button.css +2 -2
  28. package/components/button/button.js +4 -1
  29. package/components/button/button.test.js +32 -33
  30. package/components/button-group/button-group.js +1 -1
  31. package/components/button-group/caption.js +1 -1
  32. package/components/button-ng/button-ng.js +1 -1
  33. package/components/button-set-ng/button-set-ng.js +3 -1
  34. package/components/checkbox/checkbox.css +1 -1
  35. package/components/code/code.js +2 -5
  36. package/components/confirm/confirm.js +1 -0
  37. package/components/confirm-service/confirm-service.js +5 -5
  38. package/components/content-layout/content-layout.css +1 -1
  39. package/components/data-list/data-list.css +1 -1
  40. package/components/date-picker/date-input.js +5 -4
  41. package/components/date-picker/date-picker.css +34 -22
  42. package/components/date-picker/date-picker.js +16 -14
  43. package/components/date-picker/date-popup.js +22 -7
  44. package/components/date-picker/month-names.js +8 -5
  45. package/components/date-picker/month.js +6 -2
  46. package/components/date-picker/weekdays.js +10 -2
  47. package/components/dialog/dialog.examples.js +3 -1
  48. package/components/dialog/dialog.js +10 -5
  49. package/components/dialog/dialog.test.js +1 -1
  50. package/components/dialog/dialog__body-scroll-preventer.js +51 -31
  51. package/components/dialog-ng/dialog-ng.js +10 -10
  52. package/components/dialog-ng/{dialog-ng.html → dialog-ng__template.js} +2 -2
  53. package/components/dropdown/dropdown.examples.js +36 -1
  54. package/components/dropdown/dropdown.test.js +2 -2
  55. package/components/dropdown-menu/dropdown-menu.examples.js +47 -0
  56. package/components/dropdown-menu/dropdown-menu.js +117 -0
  57. package/components/dropdown-menu/dropdown-menu.test.js +76 -0
  58. package/components/error-bubble/error-bubble-legacy.css +1 -1
  59. package/components/error-bubble/error-bubble.css +1 -1
  60. package/components/error-bubble/error-bubble.examples.js +1 -1
  61. package/components/error-page/error-page.css +2 -2
  62. package/components/footer-ng/footer-ng.js +13 -3
  63. package/components/form/form.css +2 -2
  64. package/components/form-ng/form-ng.js +3 -1
  65. package/components/global/global.css +1 -1
  66. package/components/global/theme.js +1 -1
  67. package/components/global/variables.css +8 -1
  68. package/components/grid/grid.css +10 -9
  69. package/components/header/header.css +1 -1
  70. package/components/header/header.examples.js +7 -8
  71. package/components/header/profile.js +10 -11
  72. package/components/http/http.js +1 -1
  73. package/components/icon/icon.css +5 -4
  74. package/components/input/input-legacy.css +7 -7
  75. package/components/island/header.js +2 -2
  76. package/components/island/island.css +8 -3
  77. package/components/island-legacy/island-legacy.css +3 -1
  78. package/components/list/list.js +6 -1
  79. package/components/list/list__custom.js +9 -3
  80. package/components/list/list__item.js +8 -2
  81. package/components/list/list__link.js +2 -1
  82. package/components/loader-inline/loader-inline.css +1 -1
  83. package/components/loader-screen/loader-screen.css +1 -1
  84. package/components/message/message.css +1 -1
  85. package/components/message/message.examples.js +8 -7
  86. package/components/pager/pager.js +5 -3
  87. package/components/permissions/permissions.js +1 -1
  88. package/components/popup/popup.js +1 -1
  89. package/components/popup/popup.test.js +15 -13
  90. package/components/progress-bar/progress-bar.css +1 -1
  91. package/components/progress-bar/progress-bar.examples.js +3 -3
  92. package/components/progress-bar/progress-bar.js +5 -2
  93. package/components/progress-bar/progress-bar.test.js +12 -13
  94. package/components/progress-bar-ng/progress-bar-ng.examples.js +3 -3
  95. package/components/query-assist/query-assist.css +13 -3
  96. package/components/query-assist/query-assist.examples.js +3 -4
  97. package/components/query-assist/query-assist.js +56 -12
  98. package/components/query-assist/query-assist.test.js +37 -5
  99. package/components/save-field-ng/save-field-ng.css +0 -3
  100. package/components/save-field-ng/save-field-ng.js +3 -1
  101. package/components/save-field-ng/{save-field-ng.html → save-field-ng__template.js} +2 -2
  102. package/components/select/select.css +12 -7
  103. package/components/select/select.examples.js +13 -0
  104. package/components/select/select.js +30 -43
  105. package/components/select/select.test.js +4 -5
  106. package/components/select/select__popup.js +1 -0
  107. package/components/shortcuts-hint-ng/shortcuts-hint-ng.css +1 -1
  108. package/components/shortcuts-hint-ng/shortcuts-hint-ng.js +1 -1
  109. package/components/shortcuts-hint-ng/{shortcuts-hint-ng.html → shortcuts-hint-ng__template.js} +2 -2
  110. package/components/sidebar/sidebar.css +1 -0
  111. package/components/sidebar-ng/sidebar-ng.js +6 -2
  112. package/components/sidebar-ng/{sidebar-ng__button.html → sidebar-ng__button-template.js} +2 -2
  113. package/components/sidebar-ng/{sidebar-ng.html → sidebar-ng__template.js} +2 -2
  114. package/components/table/header.js +9 -1
  115. package/components/table/row.js +2 -1
  116. package/components/table/table.css +2 -1
  117. package/components/table-legacy/table-legacy.css +2 -2
  118. package/components/table-legacy/table-legacy__toolbar.css +2 -2
  119. package/components/table-legacy-ng/table-legacy-ng.js +38 -5
  120. package/components/table-legacy-ng/table-legacy-ng__pager.js +7 -1
  121. package/components/tabs/collapsible-tab.js +2 -2
  122. package/components/tabs/collapsible-tabs.js +5 -9
  123. package/components/tabs/tab-link.js +4 -2
  124. package/components/tabs/tabs.css +32 -5
  125. package/components/tabs-ng/tabs-ng.js +4 -2
  126. package/components/tabs-ng/{tabs-ng.html → tabs-ng__template.js} +6 -2
  127. package/components/tag/tag.css +5 -2
  128. package/components/tag/tag.examples.js +3 -0
  129. package/components/tag/tag.js +19 -16
  130. package/components/tags-input/tag-input.examples.js +1 -1
  131. package/components/tags-input/tags-input.js +5 -2
  132. package/components/template-ng/template-ng.js +1 -1
  133. package/components/tooltip/tooltip.js +7 -2
  134. package/components/user-agreement/user-agreement.css +1 -5
  135. package/components/user-agreement/user-agreement.examples.js +7 -6
  136. package/components/user-agreement/user-agreement.js +11 -3
  137. package/package.json +85 -83
  138. package/webpack.config.js +14 -10
  139. package/components/button-set-ng/button-set-ng.html +0 -1
  140. package/components/footer-ng/footer-ng.html +0 -13
  141. package/components/form-ng/form-ng__error-bubble.html +0 -3
  142. package/components/table-legacy-ng/table-legacy-ng.html +0 -4
  143. package/components/table-legacy-ng/table-legacy-ng__column.html +0 -12
  144. package/components/table-legacy-ng/table-legacy-ng__header.html +0 -4
  145. package/components/table-legacy-ng/table-legacy-ng__pager.html +0 -7
  146. package/components/table-legacy-ng/table-legacy-ng__row.html +0 -12
  147. package/components/table-legacy-ng/table-legacy-ng__title.html +0 -9
  148. package/dist/_helpers/_rollupPluginBabelHelpers.js +0 -123
  149. package/dist/_helpers/background-flow.js +0 -232
  150. package/dist/_helpers/badge.js +0 -3
  151. package/dist/_helpers/button.js +0 -145
  152. package/dist/_helpers/clickableLink.js +0 -65
  153. package/dist/_helpers/data-tests.js +0 -15
  154. package/dist/_helpers/disable-hover-hoc.js +0 -407
  155. package/dist/_helpers/dom.js +0 -101
  156. package/dist/_helpers/get-uid.js +0 -15
  157. package/dist/_helpers/linear-function.js +0 -17
  158. package/dist/_helpers/list.js +0 -1327
  159. package/dist/_helpers/logo.js +0 -36
  160. package/dist/_helpers/memoize.js +0 -17
  161. package/dist/_helpers/popup.js +0 -691
  162. package/dist/_helpers/popup.target.js +0 -27
  163. package/dist/_helpers/rerender-hoc.js +0 -49
  164. package/dist/_helpers/schedule-raf.js +0 -31
  165. package/dist/_helpers/sniffer.js +0 -6
  166. package/dist/_helpers/theme.js +0 -40
  167. package/dist/_helpers/url.js +0 -125
  168. package/dist/alert-service.js +0 -149
  169. package/dist/alert.js +0 -284
  170. package/dist/analytics.js +0 -116
  171. package/dist/auth-dialog-service.js +0 -56
  172. package/dist/auth-dialog.js +0 -122
  173. package/dist/auth.js +0 -1744
  174. package/dist/avatar.js +0 -152
  175. package/dist/badge.js +0 -52
  176. package/dist/button-group.js +0 -48
  177. package/dist/button-set.js +0 -27
  178. package/dist/button-toolbar.js +0 -30
  179. package/dist/button.js +0 -12
  180. package/dist/caret.js +0 -262
  181. package/dist/checkbox.js +0 -108
  182. package/dist/confirm-service.js +0 -102
  183. package/dist/confirm.js +0 -113
  184. package/dist/content-layout.js +0 -184
  185. package/dist/contenteditable.js +0 -81
  186. package/dist/data-list.js +0 -466
  187. package/dist/date-picker.js +0 -1398
  188. package/dist/dialog.js +0 -223
  189. package/dist/dropdown.js +0 -250
  190. package/dist/error-bubble.js +0 -56
  191. package/dist/error-message.js +0 -53
  192. package/dist/footer.js +0 -124
  193. package/dist/grid.js +0 -148
  194. package/dist/group.js +0 -34
  195. package/dist/header.js +0 -658
  196. package/dist/heading.js +0 -76
  197. package/dist/http.js +0 -207
  198. package/dist/hub-source.js +0 -130
  199. package/dist/icon.js +0 -211
  200. package/dist/input.js +0 -228
  201. package/dist/island.js +0 -314
  202. package/dist/link.js +0 -117
  203. package/dist/list.js +0 -29
  204. package/dist/loader-inline.js +0 -165
  205. package/dist/loader-screen.js +0 -45
  206. package/dist/loader.js +0 -338
  207. package/dist/login-dialog.js +0 -173
  208. package/dist/logo.js +0 -8
  209. package/dist/message.js +0 -226
  210. package/dist/old-browsers-message.js +0 -129
  211. package/dist/pager.js +0 -325
  212. package/dist/panel.js +0 -34
  213. package/dist/permissions.js +0 -466
  214. package/dist/popup-menu.js +0 -93
  215. package/dist/popup.js +0 -16
  216. package/dist/progress-bar.js +0 -111
  217. package/dist/proxy-attrs.js +0 -19
  218. package/dist/query-assist.js +0 -1081
  219. package/dist/radio.js +0 -112
  220. package/dist/select.js +0 -1920
  221. package/dist/selection.js +0 -213
  222. package/dist/shortcuts.js +0 -307
  223. package/dist/storage.js +0 -373
  224. package/dist/style.css +0 -1
  225. package/dist/tab-trap.js +0 -174
  226. package/dist/table.js +0 -903
  227. package/dist/tabs.js +0 -721
  228. package/dist/tag.js +0 -187
  229. package/dist/tags-input.js +0 -440
  230. package/dist/tags-list.js +0 -91
  231. package/dist/text.js +0 -38
  232. package/dist/toggle.js +0 -80
  233. package/dist/tooltip.js +0 -202
  234. package/dist/user-card.js +0 -218
@@ -6,6 +6,8 @@ import {Content} from '../island/island';
6
6
  import Dialog from '../dialog/dialog';
7
7
  import Button from '../button/button';
8
8
 
9
+ import {H2} from '../heading/heading';
10
+
9
11
  import styles from './auth-dialog.css';
10
12
 
11
13
  /**
@@ -68,6 +70,7 @@ export default class AuthDialog extends Component {
68
70
 
69
71
  return (
70
72
  <Dialog
73
+ label={title}
71
74
  data-test="ring-auth-dialog"
72
75
  className={className}
73
76
  contentClassName={classNames(className, styles.dialog)}
@@ -84,7 +87,7 @@ export default class AuthDialog extends Component {
84
87
  src={serviceImage}
85
88
  />
86
89
  )}
87
- <div className={styles.title}>{title}</div>
90
+ <H2 className={styles.title}>{title}</H2>
88
91
  {errorMessage && (
89
92
  <div className={styles.error}>{errorMessage}</div>
90
93
  )}
@@ -1,27 +1,11 @@
1
1
  import React from 'react';
2
- import {mount} from 'enzyme';
3
-
4
- import styles from './auth-dialog.css';
2
+ import {render, screen} from '@testing-library/react';
5
3
 
6
4
  import AuthDialog from './auth-dialog';
7
5
 
8
6
  describe('AuthDialog', () => {
9
- const defaultProps = {show: true, text: 'Foo'};
10
- let wrapper;
11
- const mountAuthDialog = props => {
12
- wrapper = mount(<AuthDialog {...props}/>);
13
- return wrapper;
14
- };
15
- afterEach(() => wrapper.unmount());
16
-
17
- const getContainer = () => document.querySelector('*[data-test~="ring-auth-dialog"]');
18
-
19
- it('should create component', () => {
20
- mountAuthDialog(defaultProps).should.have.type(AuthDialog);
21
- });
22
-
23
7
  it('should render confirm', () => {
24
- mountAuthDialog(defaultProps);
25
- getContainer().should.contain(`.${styles.title}`);
8
+ render(<AuthDialog show title="Foo"/>);
9
+ screen.getByRole('heading', {text: 'Foo'}).should.exist;
26
10
  });
27
11
  });
@@ -1,10 +1,13 @@
1
1
  @import "../global/variables.css";
2
2
 
3
3
  .avatar {
4
+ display: inline-block;
4
5
  object-fit: cover;
5
6
  object-position: center;
6
7
 
7
- border-radius: var(--ring-border-radius); /* This is a "graceful degradation" fallback, while the real value is controlled by JS */
8
+ /* This is a "graceful degradation" fallback, while the real value is controlled by JS */
9
+
10
+ border-radius: var(--ring-border-radius);
8
11
  }
9
12
 
10
13
  .subavatar {
@@ -21,7 +21,8 @@ export const avatar = () => {
21
21
  {Object.keys(Size).map(size => (
22
22
  <div className="avatar-demo" key={size}>
23
23
  <Avatar size={Size[size]} url={avatarDataUri}/>
24
- <Avatar size={Size[size]} url={avatarDataUri} round/>
24
+ <Avatar size={Size[size]} username="Jet Brains"/>
25
+ <Avatar size={Size[size]} username="Jet Brains" round/>
25
26
  <Avatar size={Size[size]}/>
26
27
  </div>
27
28
  ))}
@@ -38,7 +39,7 @@ avatar.parameters = {
38
39
  .avatar-demo {
39
40
  display: flex;
40
41
  justify-content: space-between;
41
- width: 200px;
42
+ width: 240px;
42
43
  margin-bottom: 16px;
43
44
  }
44
45
  </style>`
@@ -6,6 +6,7 @@ import {encodeURL, isDataURI, parseQueryString} from '../global/url';
6
6
  import {getPixelRatio} from '../global/dom';
7
7
 
8
8
  import styles from './avatar.css';
9
+ import FallbackAvatar from './fallback-avatar';
9
10
 
10
11
  /**
11
12
  * @name Avatar
@@ -15,6 +16,7 @@ export const Size = {
15
16
  Size18: 18,
16
17
  Size20: 20,
17
18
  Size24: 24,
19
+ Size28: 28,
18
20
  Size32: 32,
19
21
  Size40: 40,
20
22
  Size48: 48,
@@ -30,7 +32,9 @@ export default class Avatar extends PureComponent {
30
32
  url: PropTypes.string,
31
33
  round: PropTypes.bool,
32
34
  subavatar: PropTypes.string,
33
- subavatarSize: PropTypes.number
35
+ subavatarSize: PropTypes.number,
36
+ username: PropTypes.string,
37
+ skipParams: PropTypes.bool
34
38
  };
35
39
 
36
40
  static defaultProps = {
@@ -53,7 +57,18 @@ export default class Avatar extends PureComponent {
53
57
  };
54
58
 
55
59
  render() {
56
- const {size, url, dpr, style, round, subavatar, subavatarSize, ...restProps} = this.props;
60
+ const {
61
+ size,
62
+ url,
63
+ dpr,
64
+ style,
65
+ round,
66
+ subavatar,
67
+ subavatarSize,
68
+ username,
69
+ skipParams,
70
+ ...restProps
71
+ } = this.props;
57
72
  const sizeString = `${size}px`;
58
73
  const subavatarSizeString = `${subavatarSize}px`;
59
74
  const borderRadius = size <= Size.Size18 ? 'var(--ring-border-radius-small)' : 'var(--ring-border-radius)';
@@ -75,14 +90,25 @@ export default class Avatar extends PureComponent {
75
90
  return (
76
91
  <span
77
92
  {...restProps}
78
- className={classNames(styles.avatar, styles.empty, this.props.className)}
93
+ data-test="avatar"
94
+ className={
95
+ classNames(styles.avatar, this.props.className, {[styles.empty]: username == null})
96
+ }
79
97
  style={styleObj}
80
- />
98
+ >{
99
+ username != null && (
100
+ <FallbackAvatar
101
+ size={size}
102
+ round={round}
103
+ username={username}
104
+ />
105
+ )
106
+ }</span>
81
107
  );
82
108
  }
83
109
 
84
110
  let src = url;
85
- if (!isDataURI(url)) {
111
+ if (!skipParams && !isDataURI(url)) {
86
112
  const [urlStart, query] = url.split('?');
87
113
  const queryParams = {
88
114
  ...parseQueryString(query),
@@ -101,7 +127,7 @@ export default class Avatar extends PureComponent {
101
127
  subavatarSizeString
102
128
  };
103
129
 
104
- subavatarSrc = encodeURL(urlStart, queryParams);
130
+ subavatarSrc = skipParams ? subavatar : encodeURL(urlStart, queryParams);
105
131
  return (
106
132
  <div>
107
133
  <img
@@ -115,6 +141,7 @@ export default class Avatar extends PureComponent {
115
141
  />
116
142
  <img
117
143
  {...restProps}
144
+ data-test="avatar"
118
145
  onError={this.handleError}
119
146
  onLoad={this.handleSuccess}
120
147
  className={classNames(styles.subavatar)}
@@ -128,6 +155,7 @@ export default class Avatar extends PureComponent {
128
155
  return (
129
156
  <img
130
157
  {...restProps}
158
+ data-test="avatar"
131
159
  onError={this.handleError}
132
160
  onLoad={this.handleSuccess}
133
161
  className={classNames(styles.avatar, this.props.className)}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import {shallow, mount} from 'enzyme';
2
+ import {render, screen} from '@testing-library/react';
3
3
 
4
4
  import {getPixelRatio} from '../global/dom';
5
5
 
@@ -12,45 +12,48 @@ const dataURI = `data:image/svg+xml,${encodeURIComponent(`
12
12
  )}`;
13
13
 
14
14
  describe('Avatar', () => {
15
- const shallowAvatar = props => shallow(<Avatar {...props}/>);
16
- const mountAvatar = props => mount(<Avatar {...props}/>);
17
-
18
15
  it('should create component', () => {
19
- mountAvatar().should.have.type(Avatar);
16
+ render(<Avatar/>);
17
+ screen.getByTestId('avatar').should.exist;
20
18
  });
21
19
 
22
20
  it('should use passed className', () => {
23
- shallowAvatar({className: 'test-class'}).should.have.className('test-class');
21
+ render(<Avatar className="test-class"/>);
22
+ screen.getByTestId('avatar').should.have.class('test-class');
24
23
  });
25
24
 
26
25
  it('should use passed className when url is passed', () => {
27
- shallowAvatar({className: 'test-class', url: dataURI}).should.
28
- have.className('test-class');
26
+ render(<Avatar className="test-class" url={dataURI}/>);
27
+ screen.getByAltText('User avatar').should.have.class('test-class');
29
28
  });
30
29
 
31
30
  it('should render span when no url is passed', () => {
32
- shallowAvatar().should.have.tagName('span');
31
+ render(<Avatar/>);
32
+ screen.getByTestId('avatar').should.have.tagName('span');
33
33
  });
34
34
 
35
35
  it('should render image when url is passed', () => {
36
- shallowAvatar({url: dataURI}).should.match('img[src]');
36
+ render(<Avatar url={dataURI}/>);
37
+ screen.getByAltText('User avatar').should.exist;
37
38
  });
38
39
 
39
40
  it('should not append params when data:uri is passed', () => {
40
- shallowAvatar({url: dataURI}).should.have.prop('src').
41
- not.match(/dpr=|size=/);
41
+ render(<Avatar url={dataURI}/>);
42
+ screen.getByAltText('User avatar').src.should.not.match(/dpr=|size=/);
42
43
  });
43
44
 
44
- it('should append params when data:uri is passed', () => {
45
- shallowAvatar({url: 'http://'}).should.have.prop('src').match(/dpr=|size=/);
45
+ it('should append params when http:uri is passed', () => {
46
+ render(<Avatar url="http://"/>);
47
+ screen.getByAltText('User avatar').src.should.match(/dpr=|size=/);
46
48
  });
47
49
 
48
50
  it('should set size 20 as default', () => {
49
- shallowAvatar({url: 'http://'}).should.have.prop('src').match(/size=20/);
51
+ render(<Avatar url="http://"/>);
52
+ screen.getByAltText('User avatar').src.should.match(/size=20/);
50
53
  });
51
54
 
52
55
  it('should set proper dpr', () => {
53
- shallowAvatar({url: 'http://'}).should.have.prop('src').
54
- match(new RegExp(`dpr=${getPixelRatio()}`));
56
+ render(<Avatar url="http://"/>);
57
+ screen.getByAltText('User avatar').src.should.match(new RegExp(`dpr=${getPixelRatio()}`));
55
58
  });
56
59
  });
@@ -0,0 +1,136 @@
1
+ import React, {useMemo} from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import getUID from '../global/get-uid';
5
+
6
+ const colorPairs = [
7
+ ['#60A800', '#D5CA00'],
8
+ ['#21D370', '#03E9E1'],
9
+ ['#3BA1FF', '#36E97D'],
10
+ ['#00C243', '#00FFFF'],
11
+ ['#4BE098', '#627FFF'],
12
+ ['#168BFA', '#26F7C7'],
13
+ ['#9D4CFF', '#39D3C3'],
14
+ ['#0A81F6', '#0ACFF6'],
15
+ ['#765AF8', '#5A91F8'],
16
+ ['#9E54FF', '#0ACFF6'],
17
+ ['#B345F1', '#669DFF'],
18
+ ['#765AF8', '#C059EE'],
19
+ ['#9039D0', '#C239D0'],
20
+ ['#9F2AFF', '#FD56FD'],
21
+ ['#AB3AF2', '#E40568'],
22
+ ['#9F2AFF', '#E9A80B'],
23
+ ['#D50F6B', '#E73AE8'],
24
+ ['#ED5502', '#E73AE8'],
25
+ ['#ED358C', '#DBED18'],
26
+ ['#ED358C', '#F9902E'],
27
+ ['#FF7500', '#FFCA00']
28
+ ];
29
+
30
+ const Sizes = {
31
+ 18: {
32
+ radius: 2,
33
+ text: {x: 9, y: 13},
34
+ fontSize: '11px',
35
+ textAnchor: 'middle'
36
+ },
37
+ 24: {
38
+ radius: 3,
39
+ text: {x: 2, y: 13},
40
+ fontSize: '11px',
41
+ underscore: {x: 3, y: 17}
42
+ },
43
+ 32: {
44
+ radius: 3,
45
+ text: {x: 3, y: 17},
46
+ fontSize: '13px',
47
+ letterSpacing: 1,
48
+ underscore: {x: 4, y: 22}
49
+ },
50
+ 40: {
51
+ radius: 3,
52
+ text: {x: 5, y: 19},
53
+ fontSize: '15px',
54
+ letterSpacing: 1,
55
+ underscore: {x: 6, y: 28}
56
+ }
57
+ };
58
+
59
+ const sizeKeys = Object.keys(Sizes).map(Number);
60
+
61
+ function extractLetters(name) {
62
+ const names = name.split(/[\s._]+/).filter(Boolean);
63
+ if (names.length >= 2) {
64
+ return names[0][0].toUpperCase() + names[1][0].toUpperCase();
65
+ } else if (names.length === 1) {
66
+ if (names[0].length >= 2) {
67
+ return names[0].slice(0, 2).toUpperCase();
68
+ } else {
69
+ return `${names[0][0].toUpperCase()}X`;
70
+ }
71
+ } else {
72
+ return 'XX';
73
+ }
74
+ }
75
+
76
+ // https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0#gistcomment-2775538
77
+ const BASE = 32;
78
+ function hashCode(s) {
79
+ let h = 0;
80
+ for (let i = 0; i < s.length; i++) {
81
+ h = Math.imul(BASE - 1, h) + s.charCodeAt(i) | 0;
82
+ }
83
+
84
+ return h;
85
+ }
86
+
87
+ export default function FallbackAvatar({username, size, round}) {
88
+ const hash = Math.abs(hashCode(username.toLowerCase()));
89
+ const [fromColor, toColor] = colorPairs[hash % colorPairs.length];
90
+ const possibleSizeKeys = sizeKeys.filter(key => key >= size);
91
+ const sizeKey = possibleSizeKeys.length > 0
92
+ ? Math.min(...possibleSizeKeys)
93
+ : Math.max(...sizeKeys);
94
+ const sizes = Sizes[sizeKey];
95
+ const radius = round ? '50%' : sizes.radius;
96
+ const gradientId = useMemo(() => getUID('gradient-'), []);
97
+ return (
98
+ <svg viewBox={`0 0 ${sizeKey} ${sizeKey}`} xmlns="http://www.w3.org/2000/svg">
99
+ <defs>
100
+ <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
101
+ <stop stopColor={fromColor} offset="0"/>
102
+ <stop stopColor={toColor} offset="1"/>
103
+ </linearGradient>
104
+ </defs>
105
+ <g>
106
+ <rect
107
+ fill={`url(#${gradientId})`}
108
+ x="0"
109
+ y="0"
110
+ width={sizeKey}
111
+ height={sizeKey}
112
+ rx={radius}
113
+ ry={radius}
114
+ />
115
+ <text
116
+ x={sizes.text.x}
117
+ y={sizes.text.y}
118
+ fontFamily="Arial, Helvetica, sans-serif"
119
+ fontSize={sizes.fontSize}
120
+ letterSpacing={sizes.letterSpacing}
121
+ fill="#FFFFFF"
122
+ textAnchor={sizes.textAnchor}
123
+ >
124
+ <tspan>{extractLetters(username)}</tspan>
125
+ {sizes.underscore && <tspan x={sizes.underscore.x} y={sizes.underscore.y}>{'_'}</tspan>}
126
+ </text>
127
+ </g>
128
+ </svg>
129
+ );
130
+ }
131
+
132
+ FallbackAvatar.propTypes = {
133
+ username: PropTypes.string.isRequired,
134
+ size: PropTypes.number.isRequired,
135
+ round: PropTypes.bool
136
+ };
@@ -19,7 +19,7 @@
19
19
  width: 0;
20
20
  height: calc(unit * 5);
21
21
 
22
- content: '';
22
+ content: "";
23
23
  vertical-align: middle;
24
24
  }
25
25
  }
@@ -49,7 +49,7 @@
49
49
  width: 100%;
50
50
  height: 100%;
51
51
 
52
- content: '';
52
+ content: "";
53
53
 
54
54
  background-color: rgba(0, 0, 0, 0.8);
55
55
  }
@@ -6,6 +6,7 @@ import messageBundleNg from '../message-bundle-ng/message-bundle-ng';
6
6
  import alertService from '../alert-service/alert-service';
7
7
  import IconNG from '../icon-ng/icon-ng';
8
8
 
9
+ import template from './avatar-editor-ng__template';
9
10
  import './avatar-editor-ng.css';
10
11
 
11
12
  const angularModule = angular.module('Ring.avatar-editor', [messageBundleNg, IconNG]);
@@ -26,7 +27,7 @@ function rgAvatarEditor() {
26
27
  deleteLabel: '@',
27
28
  addMessage: '@'
28
29
  },
29
- template: require('./avatar-editor-ng.html'),
30
+ template,
30
31
  transclude: true,
31
32
  controller: [
32
33
  '$scope', '$attrs', 'RingMessageBundle',
@@ -1,4 +1,4 @@
1
- <div>
1
+ export default `<div>
2
2
  <div ng-click="!ngDisabled && controls.select()" class="ring-avatar-editor__frame" ng-class="{
3
3
  'ring-avatar-editor_controlled': controlled,
4
4
  'ring-avatar-editor': !ngDisabled
@@ -25,4 +25,4 @@
25
25
  >
26
26
  <span translate>{{ getDeleteLabel() }}</span>
27
27
  </rg-button>
28
- </div>
28
+ </div>`;
@@ -1,41 +1,34 @@
1
1
  import React from 'react';
2
- import {shallow, mount, render} from 'enzyme';
2
+ import {render, screen} from '@testing-library/react';
3
3
 
4
4
  import Badge from './badge';
5
5
  import style from './badge.css';
6
6
 
7
7
  describe('Badge', () => {
8
- const shallowBadge = (params, content) => shallow(<Badge {...params}>{content}</Badge>);
9
- const mountBadge = (params, content) => mount(<Badge {...params}>{content}</Badge>);
10
- const renderBadge = (params, content) => render(<Badge {...params}>{content}</Badge>);
11
-
12
- it('should create component', () => {
13
- mountBadge().should.have.type(Badge);
14
- });
15
-
16
8
  it('should render span with badge class', () => {
17
- const wrapper = shallowBadge();
18
- wrapper.should.have.tagName('span');
19
- wrapper.should.have.className(style.badge);
9
+ render(<Badge/>);
10
+ const element = screen.getByTestId('ring-badge');
11
+ element.should.have.tagName('span');
12
+ element.should.have.class(style.badge);
20
13
  });
21
14
 
22
15
  it('should use passed className', () => {
23
- shallowBadge({className: 'test-class'}).should.have.className('test-class');
24
- });
25
-
26
- it('should have default data-test', () => {
27
- shallowBadge({}).should.have.attr('data-test', 'ring-badge');
16
+ render(<Badge className="test-class"/>);
17
+ screen.getByTestId('ring-badge').should.have.class('test-class');
28
18
  });
29
19
 
30
20
  it('should use passed data-test', () => {
31
- shallowBadge({['data-test']: 'foo'}).should.have.attr('data-test', 'ring-badge foo');
21
+ render(<Badge data-test="foo"/>);
22
+ screen.getByTestId('foo', {exact: false}).should.exist;
32
23
  });
33
24
 
34
25
  it('should render children', () => {
35
- renderBadge({}, 'foo').should.have.text('foo');
26
+ render(<Badge>{'foo'}</Badge>);
27
+ screen.getByText('foo').should.exist;
36
28
  });
37
29
 
38
30
  it('should render valid badge', () => {
39
- shallowBadge({valid: true}, 'foo').should.have.className(style.valid);
31
+ render(<Badge valid/>);
32
+ screen.getByTestId('ring-badge').should.have.class(style.valid);
40
33
  });
41
34
  });
@@ -451,7 +451,7 @@
451
451
  width: calc(100% + loaderWidth);
452
452
  height: 100%;
453
453
 
454
- content: '';
454
+ content: "";
455
455
  animation: progress 1s linear infinite;
456
456
 
457
457
  background-repeat: repeat;
@@ -460,7 +460,7 @@
460
460
  }
461
461
 
462
462
  .delayed .content::after {
463
- content: '';
463
+ content: "";
464
464
  }
465
465
 
466
466
  .short {
@@ -1,5 +1,5 @@
1
1
  import 'focus-visible';
2
- import React, {PureComponent} from 'react';
2
+ import React, {createRef, PureComponent} from 'react';
3
3
  import PropTypes from 'prop-types';
4
4
  import classNames from 'classnames';
5
5
  import chevronDown from '@jetbrains/icons/chevron-10px';
@@ -47,6 +47,8 @@ class Button extends PureComponent {
47
47
  static IconSize = Size;
48
48
  static Theme = Theme;
49
49
 
50
+ buttonRef = createRef();
51
+
50
52
  render() {
51
53
  const {
52
54
  // Modifiers
@@ -102,6 +104,7 @@ class Button extends PureComponent {
102
104
  const Tag = isLink ? ClickableLink : 'button';
103
105
  return (
104
106
  <Tag
107
+ ref={this.buttonRef}
105
108
  tabIndex={loader ? -1 : 0}
106
109
  type={isLink ? null : 'button'}
107
110
  {...props}
@@ -1,65 +1,64 @@
1
1
  import React from 'react';
2
- import {mount, render} from 'enzyme';
2
+ import {render, screen} from '@testing-library/react';
3
+
3
4
  import caretDownSVG from '@jetbrains/icons/caret-down-10px';
4
5
 
5
6
  import Button from './button';
6
7
  import styles from './button.css';
7
8
 
8
9
  describe('Button', () => {
9
- const mountButton = props => mount(<Button {...props}/>);
10
- const getButtonOutput = props => mountButton(props).find('button');
11
- const renderButton = props => render(<Button {...props}/>);
12
-
13
10
  it('should create component', () => {
14
- mountButton().type().should.equal(Button.type);
11
+ render(<Button/>);
12
+ screen.getByRole('button').should.exist;
15
13
  });
16
14
 
17
15
  it('should set _default modifier', () => {
18
- getButtonOutput().hasClass(styles.button).should.be.true;
16
+ render(<Button/>);
17
+ screen.getByRole('button').className.should.include(styles.button);
19
18
  });
20
19
 
21
20
  it('should set modifiers', () => {
22
- const wrapper = getButtonOutput({
23
- active: true,
24
- danger: true,
25
- delayed: true,
26
- loader: true,
27
- primary: true,
28
- short: true
29
- });
21
+ render(
22
+ <Button
23
+ active
24
+ danger
25
+ delayed
26
+ loader
27
+ primary
28
+ short
29
+ />
30
+ );
30
31
 
31
- wrapper.hasClass(styles.active).should.be.true;
32
- wrapper.hasClass(styles.danger).should.be.true;
33
- wrapper.hasClass(styles.delayed).should.be.true;
34
- wrapper.hasClass(styles.loader).should.be.true;
35
- wrapper.hasClass(styles.primary).should.be.true;
36
- wrapper.hasClass(styles.short).should.be.true;
32
+ const className = screen.getByRole('button').className;
33
+ className.should.include(styles.active);
34
+ className.should.include(styles.danger);
35
+ className.should.include(styles.delayed);
36
+ className.should.include(styles.loader);
37
+ className.should.include(styles.primary);
38
+ className.should.include(styles.short);
37
39
  });
38
40
 
39
41
  it('should add icon', () => {
40
- const wrapper = renderButton({
41
- icon: caretDownSVG
42
- });
42
+ render(<Button icon={caretDownSVG}/>);
43
43
 
44
- wrapper.hasClass(styles.withIcon).should.be.true;
44
+ const element = screen.getByRole('button');
45
+ element.should.have.class(styles.withIcon);
45
46
  caretDownSVG.
46
47
  replace('/>', '></path>').
47
- should.include(wrapper.find('svg').html());
48
+ should.include(element.querySelector('svg').innerHTML);
48
49
  });
49
50
 
50
51
  it('should set custom class', () => {
51
52
  const CUSTOM_CLASS = 'test';
52
53
 
53
- const wrapper = getButtonOutput({
54
- className: CUSTOM_CLASS
55
- });
54
+ render(<Button className={CUSTOM_CLASS}/>);
56
55
 
57
- wrapper.hasClass(CUSTOM_CLASS).should.be.true;
56
+ screen.getByRole('button').should.have.class(CUSTOM_CLASS);
58
57
  });
59
58
 
60
59
  it('should render link instead of button if href specified', () => {
61
- const linkButton = mountButton({href: 'http://www.jetbrains.com'}).find('a');
62
- linkButton.should.exist;
63
- linkButton.should.have.attr('href', 'http://www.jetbrains.com');
60
+ render(<Button href="http://www.jetbrains.com"/>);
61
+
62
+ screen.getByRole('link').should.have.attr('href', 'http://www.jetbrains.com');
64
63
  });
65
64
  });
@@ -12,7 +12,7 @@ export default class ButtonGroup extends PureComponent {
12
12
  static propTypes = {
13
13
  children: PropTypes.node,
14
14
  className: PropTypes.string
15
- }
15
+ };
16
16
 
17
17
  render() {
18
18
  const {className} = this.props;
@@ -7,7 +7,7 @@ import styles from './button-group.css';
7
7
  export default class Caption extends PureComponent {
8
8
  static propTypes = {
9
9
  className: PropTypes.node
10
- }
10
+ };
11
11
 
12
12
  render() {
13
13
  const {className} = this.props;