@atlaskit/emoji 67.1.0 → 67.2.0

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 (131) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cjs/api/EmojiResource.js +29 -24
  3. package/dist/cjs/api/media/TokenManager.js +4 -4
  4. package/dist/cjs/components/common/CachingEmoji.js +14 -6
  5. package/dist/cjs/components/common/Emoji.js +48 -12
  6. package/dist/cjs/components/common/EmojiActions.js +60 -24
  7. package/dist/cjs/components/common/EmojiErrorMessage.js +12 -7
  8. package/dist/cjs/components/common/EmojiPlaceholder.js +1 -0
  9. package/dist/cjs/components/common/{EmojiButton.js → EmojiRadioButton.js} +28 -19
  10. package/dist/cjs/components/common/EmojiUploadPicker.js +80 -37
  11. package/dist/cjs/components/common/EmojiUploadPreview.js +11 -2
  12. package/dist/cjs/components/common/FileChooser.js +2 -2
  13. package/dist/cjs/components/common/ResourcedEmojiComponent.js +4 -0
  14. package/dist/cjs/components/common/RetryableButton.js +7 -3
  15. package/dist/cjs/components/common/TonePreviewButton.js +44 -0
  16. package/dist/cjs/components/common/ToneSelector.js +53 -25
  17. package/dist/cjs/components/common/styles.js +45 -16
  18. package/dist/cjs/components/i18n.js +44 -4
  19. package/dist/cjs/components/picker/CategorySelector.js +112 -90
  20. package/dist/cjs/components/picker/CategoryTracker.js +0 -28
  21. package/dist/cjs/components/picker/EmojiPickerCategoryHeading.js +2 -1
  22. package/dist/cjs/components/picker/EmojiPickerComponent.js +13 -7
  23. package/dist/cjs/components/picker/EmojiPickerEmojiRow.js +32 -4
  24. package/dist/cjs/components/picker/EmojiPickerList.js +140 -51
  25. package/dist/cjs/components/picker/EmojiPickerListSearch.js +16 -3
  26. package/dist/cjs/components/picker/EmojiPickerVirtualItems.js +5 -2
  27. package/dist/cjs/components/picker/VirtualList.js +196 -14
  28. package/dist/cjs/components/picker/styles.js +43 -51
  29. package/dist/cjs/context/EmojiPickerListContext.js +33 -0
  30. package/dist/cjs/hooks/useEmojiPickerListContext.js +12 -0
  31. package/dist/cjs/util/constants.js +40 -1
  32. package/dist/cjs/util/shared-styles.js +3 -4
  33. package/dist/cjs/version.json +1 -1
  34. package/dist/es2019/api/EmojiResource.js +29 -24
  35. package/dist/es2019/api/media/TokenManager.js +4 -4
  36. package/dist/es2019/components/common/CachingEmoji.js +10 -3
  37. package/dist/es2019/components/common/Emoji.js +44 -11
  38. package/dist/es2019/components/common/EmojiActions.js +54 -23
  39. package/dist/es2019/components/common/EmojiErrorMessage.js +7 -3
  40. package/dist/es2019/components/common/EmojiPlaceholder.js +1 -0
  41. package/dist/es2019/components/common/EmojiRadioButton.js +54 -0
  42. package/dist/es2019/components/common/EmojiUploadPicker.js +75 -36
  43. package/dist/es2019/components/common/EmojiUploadPreview.js +11 -2
  44. package/dist/es2019/components/common/FileChooser.js +1 -1
  45. package/dist/es2019/components/common/ResourcedEmojiComponent.js +4 -0
  46. package/dist/es2019/components/common/RetryableButton.js +7 -3
  47. package/dist/es2019/components/common/TonePreviewButton.js +34 -0
  48. package/dist/es2019/components/common/ToneSelector.js +55 -21
  49. package/dist/es2019/components/common/styles.js +41 -10
  50. package/dist/es2019/components/i18n.js +44 -4
  51. package/dist/es2019/components/picker/CategorySelector.js +114 -89
  52. package/dist/es2019/components/picker/CategoryTracker.js +0 -24
  53. package/dist/es2019/components/picker/EmojiPickerCategoryHeading.js +2 -2
  54. package/dist/es2019/components/picker/EmojiPickerComponent.js +14 -7
  55. package/dist/es2019/components/picker/EmojiPickerEmojiRow.js +51 -21
  56. package/dist/es2019/components/picker/EmojiPickerList.js +102 -21
  57. package/dist/es2019/components/picker/EmojiPickerListSearch.js +14 -4
  58. package/dist/es2019/components/picker/EmojiPickerVirtualItems.js +4 -1
  59. package/dist/es2019/components/picker/VirtualList.js +193 -12
  60. package/dist/es2019/components/picker/styles.js +20 -28
  61. package/dist/es2019/context/EmojiPickerListContext.js +17 -0
  62. package/dist/es2019/hooks/useEmojiPickerListContext.js +3 -0
  63. package/dist/es2019/util/constants.js +31 -0
  64. package/dist/es2019/util/shared-styles.js +1 -2
  65. package/dist/es2019/version.json +1 -1
  66. package/dist/esm/api/EmojiResource.js +29 -24
  67. package/dist/esm/api/media/TokenManager.js +4 -4
  68. package/dist/esm/components/common/CachingEmoji.js +14 -6
  69. package/dist/esm/components/common/Emoji.js +48 -12
  70. package/dist/esm/components/common/EmojiActions.js +61 -25
  71. package/dist/esm/components/common/EmojiErrorMessage.js +7 -3
  72. package/dist/esm/components/common/EmojiPlaceholder.js +1 -0
  73. package/dist/esm/components/common/EmojiRadioButton.js +52 -0
  74. package/dist/esm/components/common/EmojiUploadPicker.js +77 -36
  75. package/dist/esm/components/common/EmojiUploadPreview.js +11 -2
  76. package/dist/esm/components/common/FileChooser.js +1 -1
  77. package/dist/esm/components/common/ResourcedEmojiComponent.js +4 -0
  78. package/dist/esm/components/common/RetryableButton.js +7 -3
  79. package/dist/esm/components/common/TonePreviewButton.js +33 -0
  80. package/dist/esm/components/common/ToneSelector.js +49 -18
  81. package/dist/esm/components/common/styles.js +40 -12
  82. package/dist/esm/components/i18n.js +44 -4
  83. package/dist/esm/components/picker/CategorySelector.js +114 -95
  84. package/dist/esm/components/picker/CategoryTracker.js +0 -28
  85. package/dist/esm/components/picker/EmojiPickerCategoryHeading.js +2 -2
  86. package/dist/esm/components/picker/EmojiPickerComponent.js +13 -7
  87. package/dist/esm/components/picker/EmojiPickerEmojiRow.js +32 -4
  88. package/dist/esm/components/picker/EmojiPickerList.js +141 -52
  89. package/dist/esm/components/picker/EmojiPickerListSearch.js +17 -4
  90. package/dist/esm/components/picker/EmojiPickerVirtualItems.js +5 -2
  91. package/dist/esm/components/picker/VirtualList.js +195 -12
  92. package/dist/esm/components/picker/styles.js +37 -45
  93. package/dist/esm/context/EmojiPickerListContext.js +21 -0
  94. package/dist/esm/hooks/useEmojiPickerListContext.js +5 -0
  95. package/dist/esm/util/constants.js +31 -0
  96. package/dist/esm/util/shared-styles.js +1 -2
  97. package/dist/esm/version.json +1 -1
  98. package/dist/types/api/EmojiResource.d.ts +2 -0
  99. package/dist/types/components/common/Emoji.d.ts +7 -1
  100. package/dist/types/components/common/EmojiActions.d.ts +3 -2
  101. package/dist/types/components/common/{EmojiButton.d.ts → EmojiRadioButton.d.ts} +3 -4
  102. package/dist/types/components/common/EmojiUploadPicker.d.ts +6 -4
  103. package/dist/types/components/common/RetryableButton.d.ts +1 -0
  104. package/dist/types/components/common/TonePreviewButton.d.ts +14 -0
  105. package/dist/types/components/common/ToneSelector.d.ts +8 -5
  106. package/dist/types/components/common/internal-types.d.ts +9 -0
  107. package/dist/types/components/common/styles.d.ts +2 -1
  108. package/dist/types/components/i18n.d.ts +40 -0
  109. package/dist/types/components/picker/CategorySelector.d.ts +3 -10
  110. package/dist/types/components/picker/CategoryTracker.d.ts +0 -2
  111. package/dist/types/components/picker/EmojiPickerCategoryHeading.d.ts +2 -1
  112. package/dist/types/components/picker/EmojiPickerEmojiRow.d.ts +5 -0
  113. package/dist/types/components/picker/EmojiPickerList.d.ts +10 -5
  114. package/dist/types/components/picker/EmojiPickerListSearch.d.ts +1 -0
  115. package/dist/types/components/picker/EmojiPickerVirtualItems.d.ts +1 -1
  116. package/dist/types/components/picker/VirtualList.d.ts +2 -0
  117. package/dist/types/components/picker/styles.d.ts +1 -1
  118. package/dist/types/context/EmojiPickerListContext.d.ts +10 -0
  119. package/dist/types/hooks/useEmojiPickerListContext.d.ts +1 -0
  120. package/dist/types/util/constants.d.ts +27 -0
  121. package/dist/types/util/shared-styles.d.ts +1 -1
  122. package/dist/types/util/type-helpers.d.ts +1 -1
  123. package/package.json +9 -6
  124. package/report.api.md +52 -1
  125. package/README.md +0 -3
  126. package/dist/es2019/components/common/EmojiButton.js +0 -49
  127. package/dist/esm/components/common/EmojiButton.js +0 -43
  128. /package/dist/cjs/{components/hooks.js → hooks/useIsMounted.js} +0 -0
  129. /package/dist/es2019/{components/hooks.js → hooks/useIsMounted.js} +0 -0
  130. /package/dist/esm/{components/hooks.js → hooks/useIsMounted.js} +0 -0
  131. /package/dist/types/{components/hooks.d.ts → hooks/useIsMounted.d.ts} +0 -0
@@ -1,10 +1,11 @@
1
1
  /** @jsx jsx */
2
- import React, { Fragment, useEffect, useState } from 'react';
2
+ import { Fragment, useEffect, useState, useRef, memo, useCallback, useMemo } from 'react';
3
3
  import { jsx } from '@emotion/react';
4
4
  import { FormattedMessage, injectIntl } from 'react-intl-next';
5
5
  import TextField from '@atlaskit/textfield';
6
6
  import CrossIcon from '@atlaskit/icon/glyph/cross';
7
7
  import AkButton from '@atlaskit/button/standard-button';
8
+ import FocusLock from 'react-focus-lock';
8
9
  import * as ImageUtil from '../../util/image';
9
10
  import debug from '../../util/logger';
10
11
  import { messages } from '../i18n';
@@ -14,6 +15,8 @@ import FileChooser from './FileChooser';
14
15
  import { UploadStatus } from './internal-types';
15
16
  import { closeEmojiUploadButton, emojiChooseFileErrorMessage, emojiUpload, emojiUploadBottom, emojiUploadTop, uploadChooseFileBrowse, uploadChooseFileEmojiName, uploadChooseFileMessage, uploadChooseFileRow } from './styles';
16
17
  export const uploadEmojiNameInputTestId = 'upload-emoji-name-input';
18
+ export const uploadEmojiComponentTestId = 'upload-emoji-component';
19
+ export const cancelEmojiUploadPickerTestId = 'cancel-emoji-upload-picker';
17
20
  const disallowedReplacementsMap = new Map([[':', ''], ['!', ''], ['@', ''], ['#', ''], ['%', ''], ['^', ''], ['&', ''], ['*', ''], ['(', ''], [')', ''], [' ', '_']]);
18
21
  const sanitizeName = name => {
19
22
  // prevent / replace certain characters, allow others
@@ -27,7 +30,7 @@ const toEmojiName = uploadName => {
27
30
  const name = uploadName.split('_').join(' ');
28
31
  return `${name.substr(0, 1).toLocaleUpperCase()}${name.substr(1)}`;
29
32
  };
30
- const ChooseEmojiFile = props => {
33
+ const ChooseEmojiFile = /*#__PURE__*/memo(props => {
31
34
  const {
32
35
  name = '',
33
36
  onChooseFile,
@@ -42,13 +45,31 @@ const ChooseEmojiFile = props => {
42
45
  } = intl;
43
46
  const disableChooser = !name;
44
47
  const fileChooserButtonDescriptionId = 'choose.emoji.file.button.screen.reader.description.id';
45
- const onKeyDownHandler = event => {
48
+ const inputRef = useRef(null);
49
+ const onKeyDownHandler = useCallback(event => {
46
50
  if (event.key === 'Escape') {
47
51
  onUploadCancelled();
48
52
  }
49
- };
53
+ }, [onUploadCancelled]);
54
+ const setInputFocus = useCallback(() => {
55
+ var _inputRef$current, _document$activeEleme, _inputRef$current2;
56
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.focus();
57
+ if (((_document$activeEleme = document.activeElement) === null || _document$activeEleme === void 0 ? void 0 : _document$activeEleme.id) !== ((_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 ? void 0 : _inputRef$current2.id)) {
58
+ setInputFocus();
59
+ }
60
+ }, []);
61
+
62
+ // make sure input has focus after update
63
+ useEffect(() => {
64
+ window.requestAnimationFrame(setInputFocus);
65
+ }, [setInputFocus]);
66
+ const cancelLabel = formatMessage(messages.cancelLabel);
67
+ const emojiPlaceholder = formatMessage(messages.emojiPlaceholder);
68
+ const emojiNameAriaLabel = formatMessage(messages.emojiNameAriaLabel);
69
+ const emojiChooseFileTitle = formatMessage(messages.emojiChooseFileTitle);
50
70
  return jsx("div", {
51
- css: emojiUpload
71
+ css: emojiUpload,
72
+ "data-testid": uploadEmojiComponentTestId
52
73
  }, jsx("div", {
53
74
  css: emojiUploadTop
54
75
  }, jsx("span", {
@@ -57,34 +78,37 @@ const ChooseEmojiFile = props => {
57
78
  css: closeEmojiUploadButton
58
79
  }, jsx(AkButton, {
59
80
  onClick: onUploadCancelled,
60
- "aria-describedby": formatMessage(messages.cancelLabel),
81
+ "aria-label": cancelLabel,
61
82
  appearance: "subtle",
62
83
  spacing: "none",
63
- shouldFitContainer: true
84
+ shouldFitContainer: true,
85
+ testId: cancelEmojiUploadPickerTestId
64
86
  }, jsx(CrossIcon, {
65
87
  size: "small",
66
- label: formatMessage(messages.cancelLabel)
88
+ label: cancelLabel
67
89
  })))), jsx("div", {
68
90
  css: uploadChooseFileRow
69
91
  }, jsx("span", {
70
92
  css: uploadChooseFileEmojiName
71
93
  }, jsx(TextField, {
72
- placeholder: formatMessage(messages.emojiPlaceholder),
73
- "aria-label": formatMessage(messages.emojiNameAriaLabel),
94
+ placeholder: emojiPlaceholder,
95
+ "aria-label": emojiNameAriaLabel,
74
96
  maxLength: maxNameLength,
75
97
  onChange: onNameChange,
76
98
  onKeyDown: onKeyDownHandler,
77
99
  value: name,
78
100
  isCompact: true,
79
101
  autoFocus: true,
80
- testId: uploadEmojiNameInputTestId
102
+ testId: uploadEmojiNameInputTestId,
103
+ ref: inputRef,
104
+ id: "new-emoji-name-input"
81
105
  })), jsx("span", {
82
106
  css: uploadChooseFileBrowse
83
107
  }, jsx(FormattedMessage, messages.emojiChooseFileScreenReaderDescription, screenReaderDescription => jsx(Fragment, null, jsx("span", {
84
108
  hidden: true,
85
109
  id: fileChooserButtonDescriptionId
86
110
  }, screenReaderDescription), jsx(FileChooser, {
87
- label: formatMessage(messages.emojiChooseFileTitle),
111
+ label: emojiChooseFileTitle,
88
112
  onChange: onChooseFile,
89
113
  onClick: onClick,
90
114
  accept: "image/png,image/jpeg,image/gif",
@@ -96,8 +120,9 @@ const ChooseEmojiFile = props => {
96
120
  messageStyles: emojiChooseFileErrorMessage,
97
121
  message: errorMessage
98
122
  })));
99
- };
123
+ });
100
124
  const EmojiUploadPicker = props => {
125
+ var _document$activeEleme2;
101
126
  const {
102
127
  errorMessage,
103
128
  initialUploadName,
@@ -111,6 +136,8 @@ const EmojiUploadPicker = props => {
111
136
  const [name, setName] = useState(initialUploadName && sanitizeName(initialUploadName));
112
137
  const [filename, setFilename] = useState();
113
138
  const [previewImage, setPreviewImage] = useState();
139
+ // document is undefined during ssr rendering and throws an error
140
+ const lastFocusedElementId = useRef(typeof document !== 'undefined' ? (_document$activeEleme2 = document.activeElement) === null || _document$activeEleme2 === void 0 ? void 0 : _document$activeEleme2.id : '');
114
141
  useEffect(() => {
115
142
  if (errorMessage) {
116
143
  setUploadStatus(UploadStatus.Error);
@@ -126,13 +153,18 @@ const EmojiUploadPicker = props => {
126
153
  setName(sanitizeName(initialUploadName));
127
154
  }
128
155
  }, [initialUploadName]);
129
- const onNameChange = event => {
156
+ const clearUploadPicker = useCallback(() => {
157
+ setName(undefined);
158
+ setPreviewImage(undefined);
159
+ setUploadStatus(UploadStatus.Waiting);
160
+ }, []);
161
+ const onNameChange = useCallback(event => {
130
162
  let newName = sanitizeName(event.target.value);
131
163
  if (name !== newName) {
132
164
  setName(newName);
133
165
  }
134
- };
135
- const onAddEmoji = () => {
166
+ }, [name]);
167
+ const onAddEmoji = useCallback(() => {
136
168
  if (uploadStatus === UploadStatus.Uploading) {
137
169
  return;
138
170
  }
@@ -164,13 +196,16 @@ const EmojiUploadPicker = props => {
164
196
  });
165
197
  });
166
198
  }
167
- };
168
- const errorOnUpload = event => {
199
+ }, [clearUploadPicker, filename, name, onUploadEmoji, previewImage, uploadStatus]);
200
+ const cancelChooseFile = useCallback(() => {
201
+ setPreviewImage(undefined);
202
+ }, []);
203
+ const errorOnUpload = useCallback(event => {
169
204
  debug('File load error: ', event);
170
205
  setChooseEmojiErrorMessage(messages.emojiUploadFailed);
171
206
  cancelChooseFile();
172
- };
173
- const onFileLoad = file => async f => {
207
+ }, [cancelChooseFile]);
208
+ const onFileLoad = useCallback(file => async f => {
174
209
  try {
175
210
  setFilename(file.name);
176
211
  await ImageUtil.parseImage(f.target.result);
@@ -179,11 +214,8 @@ const EmojiUploadPicker = props => {
179
214
  setChooseEmojiErrorMessage(messages.emojiInvalidImage);
180
215
  cancelChooseFile();
181
216
  }
182
- };
183
- const cancelChooseFile = () => {
184
- setPreviewImage(undefined);
185
- };
186
- const onChooseFile = event => {
217
+ }, [cancelChooseFile]);
218
+ const onChooseFile = useCallback(event => {
187
219
  const files = event.target.files;
188
220
  if (files.length) {
189
221
  const reader = new FileReader();
@@ -200,17 +232,23 @@ const EmojiUploadPicker = props => {
200
232
  } else {
201
233
  cancelChooseFile();
202
234
  }
203
- };
204
- const clearUploadPicker = () => {
205
- setName(undefined);
206
- setPreviewImage(undefined);
207
- setUploadStatus(UploadStatus.Waiting);
208
- };
209
- const cancelUpload = () => {
235
+ }, [cancelChooseFile, errorOnUpload, onFileLoad]);
236
+ const cancelUpload = useCallback(() => {
210
237
  clearUploadPicker();
211
238
  onUploadCancelled();
212
- };
213
- return jsx(React.Fragment, null, name && previewImage ? jsx(EmojiUploadPreview, {
239
+
240
+ // using setTimeout here to allow the UI to update before setting focus
241
+ setTimeout(lastFocus => {
242
+ if (lastFocus) {
243
+ var _document$getElementB;
244
+ (_document$getElementB = document.getElementById(lastFocus)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.focus();
245
+ }
246
+ }, 0, lastFocusedElementId.current);
247
+ }, [clearUploadPicker, onUploadCancelled]);
248
+ const chooseErrorMessage = useMemo(() => chooseEmojiErrorMessage ? jsx(FormattedMessage, chooseEmojiErrorMessage) : undefined, [chooseEmojiErrorMessage]);
249
+ return jsx(FocusLock, {
250
+ noFocusGuards: true
251
+ }, name && previewImage ? jsx(EmojiUploadPreview, {
214
252
  errorMessage: errorMessage,
215
253
  name: name,
216
254
  onAddEmoji: onAddEmoji,
@@ -223,8 +261,9 @@ const EmojiUploadPicker = props => {
223
261
  onClick: onFileChooserClicked,
224
262
  onNameChange: onNameChange,
225
263
  onUploadCancelled: cancelUpload,
226
- errorMessage: chooseEmojiErrorMessage ? jsx(FormattedMessage, chooseEmojiErrorMessage) : undefined,
264
+ errorMessage: chooseErrorMessage,
227
265
  intl: intl
228
266
  }));
229
267
  };
230
- export default injectIntl(EmojiUploadPicker);
268
+ const EmojiUploadPickerComponent = injectIntl( /*#__PURE__*/memo(EmojiUploadPicker));
269
+ export default EmojiUploadPickerComponent;
@@ -11,8 +11,10 @@ import EmojiErrorMessage from './EmojiErrorMessage';
11
11
  import { UploadStatus } from './internal-types';
12
12
  import RetryableButton from './RetryableButton';
13
13
  import { bigEmojiPreview, cancelButton, emojiPreviewErrorMessage, uploadAddRow, uploadPreview, uploadPreviewFooter, uploadPreviewText } from './styles';
14
+ import VisuallyHidden from '@atlaskit/visually-hidden';
14
15
  export const uploadPreviewTestId = 'upload-preview';
15
16
  export const cancelUploadButtonTestId = 'cancel-upload-button';
17
+ const addEmojiButtonDescriptionId = 'add.emoji.button.screen.reader.description.id';
16
18
  class EmojiUploadPreview extends PureComponent {
17
19
  render() {
18
20
  const {
@@ -64,12 +66,19 @@ class EmojiUploadPreview extends PureComponent {
64
66
  messageStyles: emojiPreviewErrorMessage,
65
67
  message: errorMessage,
66
68
  tooltip: true
67
- }) : null, jsx(RetryableButton, {
69
+ }) : null, !errorMessage && jsx(VisuallyHidden, {
70
+ id: addEmojiButtonDescriptionId
71
+ }, jsx(FormattedMessage, _extends({}, messages.emojiPreview, {
72
+ values: {
73
+ emoji: name
74
+ }
75
+ }))), jsx(RetryableButton, {
68
76
  label: formatMessage(messages.addEmojiLabel),
69
77
  onSubmit: onAddEmoji,
70
78
  appearance: "primary",
71
79
  loading: uploading,
72
- error: !!errorMessage
80
+ error: !!errorMessage,
81
+ ariaDescribedby: addEmojiButtonDescriptionId
73
82
  }), jsx(AkButton, {
74
83
  onClick: onUploadCancelled,
75
84
  appearance: "subtle",
@@ -1,5 +1,5 @@
1
1
  import React, { useRef } from 'react';
2
- import AkButton from '@atlaskit/button/custom-theme-button';
2
+ import AkButton from '@atlaskit/button/standard-button';
3
3
  export const chooseFileButtonTestId = 'choose-file-button';
4
4
  export const fileUploadInputTestId = 'file-upload';
5
5
  const FileChooser = props => {
@@ -101,6 +101,10 @@ export const ResourcedEmojiComponent = props => {
101
101
  }
102
102
  fetchOrGetEmoji(resolvedEmojiProvider, emojiId, optimistic);
103
103
  }, [resolvedEmojiProvider, emojiId, optimistic, fetchOrGetEmoji]);
104
+
105
+ /**
106
+ * Setting resolved emoji provider for optimistic rendering
107
+ */
104
108
  useEffect(() => {
105
109
  Promise.resolve(emojiProvider).then(emojiProvider => {
106
110
  setResolvedEmojiProvider(emojiProvider);
@@ -21,20 +21,24 @@ const RetryButton = props => {
21
21
  css: uploadRetryButton,
22
22
  appearance: "warning",
23
23
  onClick: onSubmit,
24
- testId: retryUploadButtonTestId
24
+ testId: retryUploadButtonTestId,
25
+ autoFocus: true
25
26
  }, retryLabel));
26
27
  };
27
28
  const UploadButton = props => {
28
29
  const {
29
30
  appearance,
30
31
  onSubmit,
31
- label
32
+ label,
33
+ ariaDescribedby
32
34
  } = props;
33
35
  return jsx(AkButton, {
34
36
  css: uploadEmojiButton,
35
37
  appearance: appearance,
36
38
  onClick: onSubmit,
37
- testId: uploadEmojiButtonTestId
39
+ testId: uploadEmojiButtonTestId,
40
+ "aria-describedby": ariaDescribedby,
41
+ autoFocus: true
38
42
  }, label);
39
43
  };
40
44
  const RetryableButton = props => {
@@ -0,0 +1,34 @@
1
+ /** @jsx jsx */
2
+ import { forwardRef, memo } from 'react';
3
+ import { jsx } from '@emotion/react';
4
+ import { emojiButton, hidden } from './styles';
5
+ import Emoji from './Emoji';
6
+ export const tonePreviewTestId = 'tone-preview';
7
+ export const TonePreviewButton = /*#__PURE__*/forwardRef((props, ref) => {
8
+ const {
9
+ emoji,
10
+ selectOnHover,
11
+ ariaLabelText,
12
+ ariaExpanded,
13
+ onSelected,
14
+ isVisible = true
15
+ } = props;
16
+ return jsx("button", {
17
+ ref: ref,
18
+ css: [emojiButton, !isVisible && hidden],
19
+ onClick: onSelected,
20
+ "aria-label": ariaLabelText,
21
+ "aria-expanded": ariaExpanded,
22
+ "aria-controls": "emoji-picker-tone-selector",
23
+ style: {
24
+ overflow: 'hidden'
25
+ },
26
+ "data-testid": tonePreviewTestId
27
+ }, jsx(Emoji, {
28
+ emoji: emoji,
29
+ selectOnHover: selectOnHover,
30
+ shouldBeInteractive: false,
31
+ "aria-hidden": true
32
+ }));
33
+ });
34
+ export default /*#__PURE__*/memo(TonePreviewButton);
@@ -1,8 +1,14 @@
1
- import React, { useEffect, useMemo, useRef } from 'react';
2
- import EmojiButton from './EmojiButton';
1
+ /** @jsx jsx */
2
+ import { jsx } from '@emotion/react';
3
+ import { memo, useEffect, useMemo, useRef } from 'react';
3
4
  import { withAnalyticsEvents } from '@atlaskit/analytics-next';
4
5
  import { createAndFireEventInElementsChannel, toneSelectedEvent, toneSelectorOpenedEvent } from '../../util/analytics';
5
6
  import { setSkinToneAriaLabelText } from './setSkinToneAriaLabelText';
7
+ import EmojiRadioButton from './EmojiRadioButton';
8
+ import { useIntl } from 'react-intl-next';
9
+ import { messages } from '../i18n';
10
+ import { hidden } from './styles';
11
+ export const toneSelectorTestId = 'tone-selector';
6
12
  const extractAllTones = emoji => {
7
13
  if (emoji.skinVariations) {
8
14
  return [emoji, ...emoji.skinVariations];
@@ -13,30 +19,52 @@ export const ToneSelectorInternal = props => {
13
19
  const {
14
20
  createAnalyticsEvent,
15
21
  emoji,
16
- previewEmojiId,
17
- onToneSelected
22
+ onToneSelected,
23
+ onToneClose,
24
+ selectedTone,
25
+ isVisible
18
26
  } = props;
19
27
  const isMounted = useRef(false);
20
- const firstToneButtonRef = useRef(null);
28
+ const selectedToneRadioRef = useRef(null);
29
+ const {
30
+ formatMessage
31
+ } = useIntl();
21
32
  const emojiToneCollection = useMemo(() => {
22
- return extractAllTones(emoji).map((tone, index) => ({
23
- ...tone,
24
- focused: tone.id !== previewEmojiId,
25
- label: setSkinToneAriaLabelText(tone.name),
26
- toneId: index
27
- }));
28
- }, [emoji, previewEmojiId]);
33
+ var selectedToneIndex = -1;
34
+ const toneColletion = extractAllTones(emoji).map((tone, index) => {
35
+ const isSelected = index === selectedTone;
36
+ if (isSelected) {
37
+ selectedToneIndex = index;
38
+ }
39
+ return {
40
+ ...tone,
41
+ isSelected: isSelected,
42
+ label: setSkinToneAriaLabelText(tone.name),
43
+ toneIndex: index
44
+ };
45
+ });
46
+
47
+ // push description of selected tone to the end of the array
48
+ // so that it gets rendered last/rightmost
49
+ toneColletion.push(toneColletion.splice(selectedToneIndex, 1)[0]);
50
+ return toneColletion;
51
+ }, [emoji, selectedTone]);
29
52
  useEffect(() => {
30
- if (firstToneButtonRef.current) {
31
- firstToneButtonRef.current.focus();
53
+ if (isVisible) {
54
+ var _selectedToneRadioRef;
55
+ (_selectedToneRadioRef = selectedToneRadioRef.current) === null || _selectedToneRadioRef === void 0 ? void 0 : _selectedToneRadioRef.focus();
32
56
  }
33
- }, [firstToneButtonRef]);
57
+ }, [isVisible, selectedToneRadioRef]);
34
58
  const fireAnalyticsEvent = event => {
35
59
  if (createAnalyticsEvent) {
36
60
  createAndFireEventInElementsChannel(event)(createAnalyticsEvent);
37
61
  }
38
62
  };
39
63
  const onToneSelectedHandler = toneValue => () => {
64
+ if (selectedTone === toneValue && onToneClose) {
65
+ onToneClose();
66
+ return;
67
+ }
40
68
  onToneSelected(toneValue);
41
69
  const toneList = ['default', 'light', 'mediumLight', 'medium', 'mediumDark', 'dark'];
42
70
  fireAnalyticsEvent(toneSelectedEvent({
@@ -47,17 +75,23 @@ export const ToneSelectorInternal = props => {
47
75
  fireAnalyticsEvent(toneSelectorOpenedEvent({}));
48
76
  }
49
77
  isMounted.current = true;
50
- return /*#__PURE__*/React.createElement("div", null, emojiToneCollection.map(tone => {
51
- return /*#__PURE__*/React.createElement(EmojiButton, {
52
- ref: tone.focused ? firstToneButtonRef : null,
53
- shouldHideButton: tone.id === previewEmojiId,
78
+ return jsx("div", {
79
+ role: "radiogroup",
80
+ "data-testid": toneSelectorTestId,
81
+ id: "emoji-picker-tone-selector",
82
+ "aria-label": formatMessage(messages.emojiSelectSkinToneListAriaLabelText),
83
+ css: !isVisible && hidden
84
+ }, emojiToneCollection.map(tone => {
85
+ return jsx(EmojiRadioButton, {
86
+ ref: tone.isSelected ? selectedToneRadioRef : null,
87
+ defaultChecked: tone.isSelected,
54
88
  ariaLabelText: tone.label,
55
89
  key: `${tone.id}`,
56
- onSelected: onToneSelectedHandler(tone.toneId),
57
90
  emoji: tone,
91
+ onSelected: onToneSelectedHandler(tone.toneIndex),
58
92
  selectOnHover: true
59
93
  });
60
94
  }));
61
95
  };
62
96
  const ToneSelector = withAnalyticsEvents()(ToneSelectorInternal);
63
- export default ToneSelector;
97
+ export default /*#__PURE__*/memo(ToneSelector);
@@ -1,8 +1,7 @@
1
1
  import { css, keyframes } from '@emotion/react';
2
- import { borderRadius } from '@atlaskit/theme/constants';
3
2
  import { defaultEmojiHeight } from '../../util/constants';
4
3
  import { akEmojiSelectedBackgroundColor } from '../../util/shared-styles';
5
- import { N20, N200, N20A, N300, N900, R300, R400 } from '@atlaskit/theme/colors';
4
+ import { B100, N20, N200, N20A, N300, N900, R300, R400 } from '@atlaskit/theme/colors';
6
5
  export const commonSelectedStyles = 'emoji-common-selected';
7
6
  export const selectOnHoverStyles = 'emoji-common-select-on-hover';
8
7
  export const emojiSprite = 'emoji-common-emoji-sprite';
@@ -30,7 +29,7 @@ export const emojiToneSelectorContainer = css({
30
29
  padding: '10px 10px 11px 0'
31
30
  });
32
31
  export const emojiStyles = css({
33
- borderRadius: `${borderRadius()}px`,
32
+ borderRadius: "var(--ds-radius-100, 3px)",
34
33
  backgroundColor: 'transparent',
35
34
  display: 'inline-block',
36
35
  verticalAlign: 'middle',
@@ -47,6 +46,11 @@ export const emojiStyles = css({
47
46
  },
48
47
  img: {
49
48
  display: 'block'
49
+ },
50
+ '&:focus': {
51
+ boxShadow: `0 0 0 2px ${`var(--ds-border-focused, ${B100})`}`,
52
+ transitionDuration: '0s, 0.2s',
53
+ outline: 'none'
50
54
  }
51
55
  });
52
56
  export const emojiContainer = css({
@@ -62,6 +66,11 @@ export const emojiContainer = css({
62
66
  minHeight: `${defaultEmojiHeight}px`,
63
67
  minWidth: `${defaultEmojiHeight}px`,
64
68
  verticalAlign: 'middle'
69
+ },
70
+ '&:focus': {
71
+ boxShadow: `0 0 0 2px ${`var(--ds-border-focused, ${B100})`}`,
72
+ transitionDuration: '0s, 0.2s',
73
+ outline: 'none'
65
74
  }
66
75
  });
67
76
  export const placeholder = 'emoji-common-placeholder';
@@ -70,7 +79,7 @@ export const placeholderContainer = css({
70
79
  margin: '-1px 0',
71
80
  display: 'inline-block',
72
81
  background: "var(--ds-border, #f7f7f7)",
73
- borderRadius: `${borderRadius()}px`,
82
+ borderRadius: "var(--ds-radius-100, 3px)",
74
83
  overflow: 'hidden',
75
84
  verticalAlign: 'middle',
76
85
  whiteSpace: 'nowrap',
@@ -95,18 +104,25 @@ export const placeholderContainerAnimated = css({
95
104
  animation: `${easeSweep} 1s cubic-bezier(0.4, 0.0, 0.2, 1) infinite`
96
105
  }
97
106
  });
107
+ export const hidden = css({
108
+ opacity: 0,
109
+ visibility: 'hidden',
110
+ display: 'none'
111
+ });
98
112
  export const emojiButton = css({
99
113
  backgroundColor: 'transparent',
100
114
  border: '0',
115
+ borderRadius: "var(--ds-radius-100, 3px)",
101
116
  cursor: 'pointer',
102
117
  padding: 0,
118
+ position: 'relative',
119
+ display: 'inline-block',
103
120
  /* Firefox */
104
121
  ['&::-moz-focus-inner']: {
105
122
  border: '0 none',
106
123
  padding: 0
107
124
  },
108
125
  '&>span': {
109
- borderRadius: `${borderRadius()}px`,
110
126
  padding: '6px',
111
127
  // Scale sprite to fit regardless of default emoji size
112
128
  [`&>.${emojiSprite}`]: {
@@ -118,11 +134,26 @@ export const emojiButton = css({
118
134
  height: '24px',
119
135
  width: '24px'
120
136
  }
137
+ },
138
+ '&:focus': {
139
+ boxShadow: `0 0 0 2px ${`var(--ds-border-focused, ${B100})`}`,
140
+ transitionDuration: '0s, 0.2s',
141
+ outline: 'none'
121
142
  }
122
143
  });
123
- export const hiddenToneButton = css({
124
- // Hide currently selected tone that rendered in the ToneSelector to avoid duplication
125
- display: 'none'
144
+ export const emojiRadio = css({
145
+ opacity: 0,
146
+ position: 'absolute',
147
+ top: '-10px',
148
+ left: '-10px',
149
+ '+span': {
150
+ borderRadius: "var(--ds-radius-100, 3px)"
151
+ },
152
+ '&:focus + span': {
153
+ boxShadow: `0 0 0 2px ${`var(--ds-border-focused, ${B100})`}`,
154
+ transitionDuration: '0s, 0.2s',
155
+ outline: 'none'
156
+ }
126
157
  });
127
158
 
128
159
  // Emoji Preview
@@ -197,7 +228,7 @@ export const previewImg = css({
197
228
 
198
229
  export const emojiScrollable = css({
199
230
  border: `1px solid ${"var(--ds-border, #fff)"}`,
200
- borderRadius: `${borderRadius()}px`,
231
+ borderRadius: "var(--ds-radius-100, 3px)",
201
232
  display: 'block',
202
233
  margin: '0',
203
234
  overflowX: 'hidden',
@@ -264,7 +295,7 @@ export const uploadPreview = css({
264
295
  justifyContent: 'space-between',
265
296
  alignItems: 'center',
266
297
  background: `var(--ds-background-neutral, ${N20})`,
267
- borderRadius: `${borderRadius()}px`,
298
+ borderRadius: "var(--ds-radius-100, 3px)",
268
299
  padding: '10px'
269
300
  });
270
301
  export const uploadPreviewText = css({
@@ -22,8 +22,8 @@ export const messages = defineMessages({
22
22
  },
23
23
  emojiPlaceholder: {
24
24
  id: 'fabric.emoji.placeholder',
25
- defaultMessage: 'Emoji name',
26
- description: 'Placeholder for emoji'
25
+ defaultMessage: 'e.g. hello',
26
+ description: 'Placeholder for emoji that provides an example for emoji name'
27
27
  },
28
28
  emojiNameAriaLabel: {
29
29
  id: 'fabric.emoji.name.ariaLabel',
@@ -42,9 +42,14 @@ export const messages = defineMessages({
42
42
  },
43
43
  emojiSelectSkinToneButtonAriaLabelText: {
44
44
  id: 'fabric.emoji.select.skin.tone.ariaLabel',
45
- defaultMessage: 'Select skin tone, {selectedTone}',
45
+ defaultMessage: 'Choose your skin tone, {selectedTone} selected',
46
46
  description: 'Message indicating the purpose of the skin tone selection button and the selected tone'
47
47
  },
48
+ emojiSelectSkinToneListAriaLabelText: {
49
+ id: 'fabric.emoji.select.skin.list.ariaLabel',
50
+ defaultMessage: 'Skin tone selector',
51
+ description: 'Message indicating the purpose of the skin tone list and make user to choose one tone'
52
+ },
48
53
  emojiImageRequirements: {
49
54
  id: 'fabric.emoji.image.requirements',
50
55
  defaultMessage: 'JPG, PNG or GIF. Max size 1 MB.',
@@ -82,9 +87,24 @@ export const messages = defineMessages({
82
87
  },
83
88
  searchLabel: {
84
89
  id: 'fabric.emoji.search.label',
85
- defaultMessage: 'Search emoji',
90
+ defaultMessage: 'Emoji name',
86
91
  description: 'verb - button label to search'
87
92
  },
93
+ searchResultsStatus: {
94
+ id: 'fabric.emoji.search.status',
95
+ defaultMessage: 'Found {count} emojis',
96
+ description: 'search results status for screenreader to read out'
97
+ },
98
+ searchResultsStatusSeeAll: {
99
+ id: 'fabric.emoji.search.status',
100
+ defaultMessage: 'Seeing all emojis',
101
+ description: 'search results status when no search query for screenreader to read out'
102
+ },
103
+ categoriesSelectorLabel: {
104
+ id: 'fabric.emoji.categories.label',
105
+ defaultMessage: 'Choose a emoji category',
106
+ description: 'Aria label for Emoji categories selector at the top of emoji picker'
107
+ },
88
108
  categoriesSearchResults: {
89
109
  id: 'fabric.emoji.categories.search.results',
90
110
  defaultMessage: 'Search results',
@@ -169,5 +189,25 @@ export const messages = defineMessages({
169
189
  id: 'fabric.emoji.error.image.too.big',
170
190
  defaultMessage: 'Selected image is more than 1 MB',
171
191
  description: 'Error message for image too big, beyond the size limit'
192
+ },
193
+ emojiPickerTitle: {
194
+ id: 'fabric.emoji.picker',
195
+ defaultMessage: 'Emoji picker',
196
+ description: 'Aria label for emoji picker'
197
+ },
198
+ emojiPickerListPanel: {
199
+ id: 'fabric.emoji.pickerlist.tabpanel',
200
+ defaultMessage: 'Emojis actions and list panel',
201
+ description: 'Aria lable for tabpanel of emoji picker, which shows emojis actions and list'
202
+ },
203
+ emojiPickerGrid: {
204
+ id: 'fabric.emoji.pickerlist.grid',
205
+ defaultMessage: '{showSearchResults, select, true{Search results} other{Emojis}}',
206
+ description: `Aria label for emoji picker grid, showSearchResults is a boolean variable, message will be "Entering Emojis table", or "Leaving Emojis"`
207
+ },
208
+ emojiButtonRoleDescription: {
209
+ id: 'fabric.emoji.emojipicker.emoi.roledescription',
210
+ defaultMessage: 'emoji button',
211
+ description: `Aria roledescription for emoji button, used in emoji picker.`
172
212
  }
173
213
  });