@elliemae/ds-chat 2.0.2 → 2.0.6

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.
@@ -21,7 +21,8 @@ const ChatDataTestIds = {
21
21
  SYSTEM_MSG_TIME: 'chat-system-message-time',
22
22
  BUBBLE: 'ds-chat-bubble',
23
23
  BUBBLE_TITLE: 'chat-bubble-title',
24
- BUBBLE_BODY: 'chat-bubble-body'
24
+ BUBBLE_BODY: 'chat-bubble-body',
25
+ BANNER: 'ds-banner-container'
25
26
  };
26
27
 
27
28
  exports.ChatDataTestIds = ChatDataTestIds;
@@ -48,10 +48,12 @@ const ChatComposer = props => {
48
48
  placeholder,
49
49
  inputMaxLength,
50
50
  maxLengthMessage,
51
+ ariaMaxLengthMessage,
51
52
  buttonDisabled,
52
53
  dataTestid,
53
54
  actionRef
54
55
  } = props;
56
+ const COMPOSER_MIN_HEIGHT = 32;
55
57
  const input = React__default["default"].useRef();
56
58
  const inputSize = React__default["default"].useRef();
57
59
  const inputSend = React__default["default"].useRef();
@@ -62,10 +64,10 @@ const ChatComposer = props => {
62
64
  var _inputSize$current, _inputSize$current2, _inputSize$current3, _inputSize$current4;
63
65
 
64
66
  const newHeight = ((_inputSize$current = inputSize.current) === null || _inputSize$current === void 0 ? void 0 : _inputSize$current.scrollHeight) > internalMaxHeight ? internalMaxHeight : (_inputSize$current2 = inputSize.current) === null || _inputSize$current2 === void 0 ? void 0 : _inputSize$current2.scrollHeight;
65
- onResize(newHeight);
67
+ if (newHeight !== height && newHeight > COMPOSER_MIN_HEIGHT) onResize(newHeight);
66
68
  setHeight(newHeight);
67
69
  setShowScroll(((_inputSize$current3 = inputSize.current) === null || _inputSize$current3 === void 0 ? void 0 : _inputSize$current3.scrollHeight) - ((_inputSize$current4 = inputSize.current) === null || _inputSize$current4 === void 0 ? void 0 : _inputSize$current4.clientHeight) > 5);
68
- }, [inputValue, internalMaxHeight, onResize]);
70
+ }, [inputValue, internalMaxHeight, height, onResize]);
69
71
  React__default["default"].useEffect(() => {
70
72
  if (actionRef && actionRef.current) {
71
73
  actionRef.current.focusToComposer = () => {
@@ -92,6 +94,9 @@ const ChatComposer = props => {
92
94
  "data-testid": dataTestid
93
95
  }, void 0, /*#__PURE__*/_jsx__default["default"](dsGrid.Grid, {
94
96
  cols: ['1fr', '24px'],
97
+ style: {
98
+ height: heightProps.height
99
+ },
95
100
  justifyContent: "center",
96
101
  alignItems: "flex-end",
97
102
  gutter: "xxs"
@@ -105,7 +110,9 @@ const ChatComposer = props => {
105
110
  onKeyDown: onKeyDown,
106
111
  placeholder: placeholder,
107
112
  "aria-label": "Message to send",
108
- "data-testid": "chat-composer-textarea"
113
+ "data-testid": "chat-composer-textarea",
114
+ name: "chat-composer-textarea",
115
+ id: "chat-composer-textarea"
109
116
  }), /*#__PURE__*/_jsx__default["default"](dsGrid.Grid, {
110
117
  height: "32px",
111
118
  alignItems: "center"
@@ -132,8 +139,7 @@ const ChatComposer = props => {
132
139
  width: (_input$current = input.current) === null || _input$current === void 0 ? void 0 : _input$current.clientWidth
133
140
  },
134
141
  onChange: handleChange,
135
- value: inputValue // maxLength={inputMaxLength}
136
-
142
+ value: inputValue
137
143
  }), /*#__PURE__*/_jsx__default["default"](dsGrid.Grid, {
138
144
  cols: ['1fr', '24px'],
139
145
  justifyContent: "flex-end",
@@ -146,7 +152,7 @@ const ChatComposer = props => {
146
152
  }, void 0, inputMaxLength && hasError ? /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
147
153
  children: [/*#__PURE__*/_jsx__default["default"](styled.StyledCounterErrorA11y, {
148
154
  role: "alert"
149
- }, void 0, maxLengthMessage || "You have exceeded the character limit by ".concat(inputLength - inputMaxLength)), maxLengthMessage || "Please limit to less than ".concat(inputMaxLength, " characters. ")]
155
+ }, void 0, ariaMaxLengthMessage || "You have entered ".concat(inputLength, " characters and have exceeded the limit by ").concat(inputLength - inputMaxLength, ". Please limit to less than ").concat(inputMaxLength, " characters. ")), maxLengthMessage || "Please limit to less than ".concat(inputMaxLength, " characters. ")]
150
156
  }) : '', inputMaxLength ? " ".concat(inputLength, " / ").concat(inputMaxLength) : ''), _Grid || (_Grid = /*#__PURE__*/_jsx__default["default"](dsGrid.Grid, {}))));
151
157
  };
152
158
 
@@ -8,6 +8,7 @@ var lodash = require('lodash');
8
8
  const defaultComposerPros = {
9
9
  placeholder: 'Type your message',
10
10
  inputValue: '',
11
+ ariaMaxLengthMessage: '',
11
12
  onChange: lodash.noop,
12
13
  onResize: lodash.noop,
13
14
  onSend: lodash.noop,
@@ -24,6 +25,7 @@ const defaultComposerPros = {
24
25
  const composerProps = {
25
26
  placeholder: reactDesc.PropTypes.string.description('Input Placeholder').defaultValue(defaultComposerPros.placeholder),
26
27
  inputValue: reactDesc.PropTypes.string.description('Input value').defaultValue(defaultComposerPros.inputValue),
28
+ ariaMaxLengthMessage: reactDesc.PropTypes.string.description('Aria message for Max length message when exceeded').defaultValue(defaultComposerPros.maxLengthMessage),
27
29
  maxLengthMessage: reactDesc.PropTypes.string.description('Max length message').defaultValue(defaultComposerPros.maxLengthMessage),
28
30
  onChange: reactDesc.PropTypes.func.description('Called when the user types').defaultValue(defaultComposerPros.onChange),
29
31
  onResize: reactDesc.PropTypes.func.description('Called the input changes his height').defaultValue(defaultComposerPros.onResize),
@@ -29,14 +29,14 @@ var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
29
29
  var DSIndeterminateProgressIndicator__default = /*#__PURE__*/_interopDefaultLegacy(DSIndeterminateProgressIndicator);
30
30
  var DSBanner__default = /*#__PURE__*/_interopDefaultLegacy(DSBanner);
31
31
 
32
- var _DSIndeterminateProgr, _StyledIndicatorConta;
32
+ var _DSIndeterminateProgr;
33
33
 
34
34
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
35
35
 
36
36
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty__default["default"](target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
37
37
 
38
38
  const ChatContainer = props$1 => {
39
- var _containerRef$current;
39
+ var _messagesListProps$me2, _containerRef$current;
40
40
 
41
41
  useValidateProps.useValidateProps(props$1, props.containerProps);
42
42
  const {
@@ -67,18 +67,16 @@ const ChatContainer = props$1 => {
67
67
  });
68
68
  const onResize = React__default["default"].useCallback(height => {
69
69
  if (onComposerResize) onComposerResize(height);
70
-
71
- if (height > 30) {
72
- /*
70
+ /*
73
71
  setTimeout is used to equal the animation duration from the
74
72
  composer text area when increase its size
75
73
  */
76
- setTimeout(() => {
77
- actionRef.current.scrollToIndex(messagesListProps.messages.length - 1, {
78
- align: 'start'
79
- });
80
- }, 50);
81
- }
74
+
75
+ setTimeout(() => {
76
+ actionRef.current.scrollToIndex(messagesListProps.messages.length - 1, {
77
+ align: 'start'
78
+ });
79
+ }, 50);
82
80
  }, [onComposerResize, messagesListProps.messages, actionRef]);
83
81
  const Container = device === 'desktop' ? styled.StyledChatContainerContent : styled.MobileStyledChatContainerContent;
84
82
  const handleTabs = React__default["default"].useCallback(e => {
@@ -86,17 +84,23 @@ const ChatContainer = props$1 => {
86
84
  const ref = actionRef || listRef;
87
85
 
88
86
  if (e.shiftKey && e.key === 'Tab') {
89
- e.preventDefault();
87
+ var _messagesListProps$me;
88
+
89
+ if (((_messagesListProps$me = messagesListProps.messages) === null || _messagesListProps$me === void 0 ? void 0 : _messagesListProps$me.length) > 0) {
90
+ e.preventDefault();
91
+
92
+ if (['top', 'bottom'].includes(bannerPosition)) {
93
+ bannerRef.current.focusOnWrapper();
94
+ } else {
95
+ var _ref$current;
90
96
 
91
- if (['top', 'bottom'].includes(bannerPosition)) {
92
- bannerRef.current.focusOnWrapper();
93
- } else {
94
- const last = ref.current.getMessagesInView().pop();
95
- ref.current.focusToIndexWithoutScroll(last);
97
+ const last = ref.current.getMessagesInView().pop();
98
+ (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focusToIndexWithoutScroll(last);
99
+ }
96
100
  }
97
101
  }
98
102
  }
99
- }, [actionRef, onComposerKeyDown, bannerPosition]); // Map actions
103
+ }, [actionRef, onComposerKeyDown, bannerPosition, (_messagesListProps$me2 = messagesListProps.messages) === null || _messagesListProps$me2 === void 0 ? void 0 : _messagesListProps$me2.length]); // Map actions
100
104
 
101
105
  React__default["default"].useEffect(() => {
102
106
  if (actionRef && actionRef.current) {
@@ -146,15 +150,15 @@ const ChatContainer = props$1 => {
146
150
  isOpen: true,
147
151
  type: bannerProps.type || DSBanner.BANNER_TYPES.INFO,
148
152
  actionRef: bannerRef
149
- })), sendingMessages && sendingMessages.length > 0 ? _StyledIndicatorConta || (_StyledIndicatorConta = /*#__PURE__*/_jsx__default["default"](styled.StyledIndicatorContainer, {
153
+ })), sendingMessages && sendingMessages.length > 0 ? /*#__PURE__*/_jsx__default["default"](styled.StyledIndicatorContainer, {
150
154
  isLoading: "bottom",
151
155
  linear: true,
152
156
  "data-testid": "ds-chat-loading-indicator"
153
157
  }, void 0, /*#__PURE__*/_jsx__default["default"](DSIndeterminateProgressIndicator__default["default"], {
154
158
  processing: true,
155
- title: "Loading",
159
+ title: sendingText,
156
160
  lineOnly: true
157
- }))) : null), /*#__PURE__*/jsxRuntime.jsx(ChatContainerComposer.ChatComposer, _objectSpread(_objectSpread({}, composerProps), {}, {
161
+ })) : null), /*#__PURE__*/jsxRuntime.jsx(ChatContainerComposer.ChatComposer, _objectSpread(_objectSpread({}, composerProps), {}, {
158
162
  onKeyDown: handleTabs,
159
163
  onResize: onResize,
160
164
  maxHeight: (((_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.clientHeight) || 0) / 2,
@@ -18,7 +18,7 @@ const StyledChatContainerContent = /*#__PURE__*/styled__default["default"](dsGri
18
18
  })(["height:100%;min-width:", "px;overflow:hidden;background:", ";position:relative;"], CONTAINER_WIDTH, props => props.theme.colors.neutral['050']);
19
19
  const StyledIndicatorContainer = /*#__PURE__*/styled__default["default"].div.withConfig({
20
20
  componentId: "sc-d732wo-1"
21
- })(["transition:0.4s ease-in-out;position:absolute;visibility:hidden;pointer-events:none;width:100%;padding:0 ", ";left:0;right:0;background:", ";", " ", " & div,a{font-size:13px;}"], props => !props.linear ? props.theme.space.xxs : 0, props => props.theme.colors.neutral['000'], props => props.isLoading === 'top' ? "\n top: -41px;\n transform: translate(0, 41px);\n visibility: visible;\n pointer-events: auto\n " : '', props => props.isLoading === 'bottom' ? "\n bottom: -41px;\n transform: translate(0, -41px);\n visibility: visible;\n pointer-events: auto;\n " : '');
21
+ })(["transition:0.4s ease-in-out;position:absolute;visibility:hidden;pointer-events:none;width:100%;padding:0 ", ";left:0;right:0;background:", ";", " ", " & div,a{font-size:13px;}"], props => !props.linear ? props.theme.space.xxs : 0, props => props.theme.colors.neutral['000'], props => props.isLoading === 'top' ? "\n top: -41px;\n transform: translate(0, 41px);\n visibility: visible;\n pointer-events: auto;\n " : '', props => props.isLoading === 'bottom' ? "\n bottom: -41px;\n transform: translate(0, -41px);\n visibility: visible;\n pointer-events: auto;\n " : '');
22
22
  const MobileStyledChatContainerContent = /*#__PURE__*/styled__default["default"](StyledChatContainerContent).withConfig({
23
23
  componentId: "sc-d732wo-2"
24
24
  })(["", ",", "{font-size:16px;}", "{font-size:13px;}", "{background-color:#fff;box-shadow:-0 1px 3px rgba(53,60,70,0.8);}", ",", "{font-size:16px;text-align:left;}", "{font-size:13px;}", "{font-size:16px;}", "{font-size:16px;}"], styled$1.StyledTitleLeftBubble, styled$1.StyleBodyWrapper, styled$1.StyledTitleRightBubble, style.Wrapper, style.Title, style.Message, style.Time, styled$2.StyledComposerInput, StyledIndicatorContainer);
@@ -19,6 +19,7 @@ var React = require('react');
19
19
  var reactDesc = require('react-desc');
20
20
  var reactVirtual = require('react-virtual');
21
21
  var DSButton = require('@elliemae/ds-button');
22
+ var lodash = require('lodash');
22
23
  var ChatBubble = require('../bubble/ChatBubble.js');
23
24
  var props$1 = require('../bubble/props.js');
24
25
  var MessageDelimiter = require('../message-delimiter/MessageDelimiter.js');
@@ -26,6 +27,7 @@ var SystemMessage = require('../system-message/SystemMessage.js');
26
27
  var styled = require('./styled.js');
27
28
  var props = require('./props.js');
28
29
  var useValidateProps = require('./config/useValidateProps.js');
30
+ var useGetNewMessages = require('./useGetNewMessages.js');
29
31
  var jsxRuntime = require('react/jsx-runtime');
30
32
 
31
33
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
@@ -67,6 +69,7 @@ const ChatContainerMessagesList = props$2 => {
67
69
  isLoading,
68
70
  bannerRef
69
71
  } = props$2;
72
+ const pendingOnScroll = React__default["default"].useRef(false);
70
73
  const loadMoreRef = React__default["default"].useRef(null);
71
74
  const virtualListRef = React__default["default"].useRef(); // estimateSize should not be really required given what was stated on
72
75
  // https://github.com/tannerlinsley/react-virtual/issues/23
@@ -86,9 +89,17 @@ const ChatContainerMessagesList = props$2 => {
86
89
  })), {}, {
87
90
  estimateSize: React__default["default"].useCallback(() => 80, [])
88
91
  }));
89
- const onScrollStop = React__default["default"].useCallback(() => {
92
+ const newMessages = useGetNewMessages.useGetNewMessages(messages);
93
+ const debouncedScroll = React__default["default"].useMemo(() => lodash.debounce(() => {
90
94
  var _virtualListRef$curre;
91
95
 
96
+ if (document.visibilityState === 'hidden') {
97
+ pendingOnScroll.current = true;
98
+ return [];
99
+ }
100
+
101
+ if (!virtualListRef.current) return [];
102
+ pendingOnScroll.current = false;
92
103
  const {
93
104
  bottom,
94
105
  top
@@ -97,15 +108,15 @@ const ChatContainerMessagesList = props$2 => {
97
108
  const indexList = [];
98
109
  list.forEach(item => {
99
110
  const {
100
- top: itemTop,
101
- bottom: itemBottom
111
+ top: itemTop
102
112
  } = item.getBoundingClientRect();
103
113
  const itemPadding = 10;
104
- if (itemTop + itemPadding < bottom && itemBottom - itemPadding > top) indexList.push(item.dataset.index);
114
+ if (itemTop + itemPadding < bottom && itemTop + itemPadding > top) indexList.push(item.dataset.index);
105
115
  });
106
116
  if (onScrollEnds) onScrollEnds(indexList);
107
117
  return indexList;
108
- }, [onScrollEnds]);
118
+ }, 500), [onScrollEnds]);
119
+ const onScrollStop = React__default["default"].useCallback(debouncedScroll, [debouncedScroll]);
109
120
  const onlyFocus = React__default["default"].useCallback(index => {
110
121
  setTimeout(() => {
111
122
  virtualListRef.current.querySelector("[data-index=\"".concat(index, "\"]")).focus();
@@ -117,7 +128,8 @@ const ChatContainerMessagesList = props$2 => {
117
128
  };
118
129
  vScroll(index, b);
119
130
  onlyFocus(index);
120
- }, [onlyFocus, vScroll]);
131
+ onScrollStop();
132
+ }, [onScrollStop, onlyFocus, vScroll]);
121
133
  React__default["default"].useEffect(() => {
122
134
  if (actionRef && actionRef.current) {
123
135
  actionRef.current.scrollToIndex = function (a) {
@@ -125,9 +137,11 @@ const ChatContainerMessagesList = props$2 => {
125
137
  align: 'center'
126
138
  };
127
139
  vScroll(a, b);
140
+ onScrollStop();
128
141
  };
129
142
 
130
143
  actionRef.current.getMessagesInView = onScrollStop;
144
+ onScrollStop(); // init useMemo due to debounce use.
131
145
 
132
146
  actionRef.current.focusToIndex = function (a) {
133
147
  let b = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
@@ -138,7 +152,7 @@ const ChatContainerMessagesList = props$2 => {
138
152
 
139
153
  actionRef.current.focusToIndexWithoutScroll = onlyFocus;
140
154
 
141
- actionRef.current.focusToList = a => {
155
+ actionRef.current.focusToList = () => {
142
156
  var _virtualListRef$curre2;
143
157
 
144
158
  (_virtualListRef$curre2 = virtualListRef.current) === null || _virtualListRef$curre2 === void 0 ? void 0 : _virtualListRef$curre2.focus();
@@ -155,7 +169,17 @@ const ChatContainerMessagesList = props$2 => {
155
169
  switch (key) {
156
170
  case 'ArrowUp':
157
171
  e.preventDefault();
158
- if (index > 0) virtualListRef.current.querySelector("[data-index=\"".concat(index - 1, "\"]")).focus();
172
+
173
+ if (index > 0) {
174
+ var _virtualListRef$curre3;
175
+
176
+ ((_virtualListRef$curre3 = virtualListRef.current) === null || _virtualListRef$curre3 === void 0 ? void 0 : _virtualListRef$curre3.querySelector("[data-index=\"".concat(index - 1, "\"]"))).focus();
177
+ } else if (hasMoreItems === 'top') {
178
+ var _loadMoreRef$current;
179
+
180
+ (_loadMoreRef$current = loadMoreRef.current) === null || _loadMoreRef$current === void 0 ? void 0 : _loadMoreRef$current.focus();
181
+ }
182
+
159
183
  break;
160
184
 
161
185
  case 'ArrowDown':
@@ -178,31 +202,31 @@ const ChatContainerMessagesList = props$2 => {
178
202
  break;
179
203
 
180
204
  case 'Tab':
181
- e.preventDefault();
182
-
183
205
  if (hasMoreItems === 'bottom' && !e.shiftKey) {
184
- var _loadMoreRef$current;
185
-
186
- (_loadMoreRef$current = loadMoreRef.current) === null || _loadMoreRef$current === void 0 ? void 0 : _loadMoreRef$current.focus();
187
- } else if (hasMoreItems === 'top' && e.shiftKey) {
188
206
  var _loadMoreRef$current2;
189
207
 
208
+ e.preventDefault();
190
209
  (_loadMoreRef$current2 = loadMoreRef.current) === null || _loadMoreRef$current2 === void 0 ? void 0 : _loadMoreRef$current2.focus();
210
+ } else if (hasMoreItems === 'top' && e.shiftKey) {
211
+ var _loadMoreRef$current3;
212
+
213
+ e.preventDefault();
214
+ (_loadMoreRef$current3 = loadMoreRef.current) === null || _loadMoreRef$current3 === void 0 ? void 0 : _loadMoreRef$current3.focus();
191
215
  } else if (!e.shiftKey) {
192
216
  if (bannerPosition) {
217
+ e.preventDefault();
193
218
  bannerRef.current.focusOnWrapper();
194
- } else {
195
- actionRef.current.focusToComposer();
196
219
  }
197
220
  } else {
198
- var _virtualListRef$curre3;
221
+ var _virtualListRef$curre4;
199
222
 
200
- (_virtualListRef$curre3 = virtualListRef.current) === null || _virtualListRef$curre3 === void 0 ? void 0 : _virtualListRef$curre3.focus();
223
+ e.preventDefault();
224
+ (_virtualListRef$curre4 = virtualListRef.current) === null || _virtualListRef$curre4 === void 0 ? void 0 : _virtualListRef$curre4.focus();
201
225
  }
202
226
 
203
227
  break;
204
228
  }
205
- }, [actionRef, hasMoreItems, messages.length, bannerPosition, scrollAndFocus]);
229
+ }, [bannerRef, hasMoreItems, messages.length, bannerPosition, scrollAndFocus]);
206
230
  const handleListKey = React__default["default"].useCallback(e => {
207
231
  const {
208
232
  key
@@ -211,12 +235,22 @@ const ChatContainerMessagesList = props$2 => {
211
235
  switch (key) {
212
236
  case 'ArrowUp':
213
237
  e.preventDefault();
214
- onlyFocus(onScrollStop().shift());
238
+
239
+ if (document.activeElement !== loadMoreRef.current) {
240
+ onlyFocus(onScrollStop().shift());
241
+ }
242
+
215
243
  break;
216
244
 
217
245
  case 'ArrowDown':
218
246
  e.preventDefault();
219
- onlyFocus(onScrollStop().pop());
247
+
248
+ if (document.activeElement === loadMoreRef.current) {
249
+ virtualListRef.current.querySelector("[data-index=\"0\"]").focus();
250
+ } else {
251
+ onlyFocus(onScrollStop().pop());
252
+ }
253
+
220
254
  break;
221
255
 
222
256
  case 'Home':
@@ -232,11 +266,6 @@ const ChatContainerMessagesList = props$2 => {
232
266
  case 'Tab':
233
267
  if (document.activeElement !== loadMoreRef.current) {
234
268
  if (hasMoreItems === 'bottom' && !e.shiftKey) {
235
- var _loadMoreRef$current3;
236
-
237
- e.preventDefault();
238
- (_loadMoreRef$current3 = loadMoreRef.current) === null || _loadMoreRef$current3 === void 0 ? void 0 : _loadMoreRef$current3.focus();
239
- } else if (hasMoreItems === 'top' && e.shiftKey) {
240
269
  var _loadMoreRef$current4;
241
270
 
242
271
  e.preventDefault();
@@ -249,15 +278,18 @@ const ChatContainerMessagesList = props$2 => {
249
278
  e.preventDefault();
250
279
  actionRef.current.focusToComposer();
251
280
  } else {
252
- var _virtualListRef$curre4;
281
+ var _virtualListRef$curre5;
253
282
 
254
283
  e.preventDefault();
255
- (_virtualListRef$curre4 = virtualListRef.current) === null || _virtualListRef$curre4 === void 0 ? void 0 : _virtualListRef$curre4.focus();
284
+ (_virtualListRef$curre5 = virtualListRef.current) === null || _virtualListRef$curre5 === void 0 ? void 0 : _virtualListRef$curre5.focus();
256
285
  }
257
286
 
258
287
  break;
259
288
  }
260
289
  }, [actionRef, hasMoreItems, messages.length, onScrollStop, onlyFocus, scrollAndFocus]);
290
+ const checkPendingOnScroll = React__default["default"].useCallback(() => {
291
+ if (pendingOnScroll.current === true) onScrollStop();
292
+ }, [onScrollStop]);
261
293
  React__default["default"].useEffect(() => {
262
294
  let handlerCallback = null;
263
295
  const localVirtualRef = virtualListRef.current;
@@ -271,11 +303,13 @@ const ChatContainerMessagesList = props$2 => {
271
303
  virtualListRef.current.addEventListener('scroll', onScroll);
272
304
  }
273
305
 
306
+ document.addEventListener('visibilitychange', checkPendingOnScroll);
274
307
  return () => {
275
308
  if (handlerCallback) clearTimeout(handlerCallback);
276
309
  if (onScrollEnds) localVirtualRef.removeEventListener('scroll', onScroll);
310
+ document.removeEventListener('visibilitychange', checkPendingOnScroll);
277
311
  };
278
- }, [onScrollStop, onScrollEnds]);
312
+ }, [onScrollStop, onScrollEnds, checkPendingOnScroll]);
279
313
  const render = React__default["default"].useMemo(() => virtualItems.map(virtualItem => {
280
314
  const {
281
315
  index,
@@ -296,7 +330,7 @@ const ChatContainerMessagesList = props$2 => {
296
330
  "data-index": index,
297
331
  style: style,
298
332
  ref: measureRef,
299
- tabIndex: 0,
333
+ tabIndex: -1,
300
334
  onKeyDown: handleKey,
301
335
  role: "listitem",
302
336
  children: /*#__PURE__*/_jsx__default["default"](styled.StyledWrapperListItem, {
@@ -312,14 +346,12 @@ const ChatContainerMessagesList = props$2 => {
312
346
  return /*#__PURE__*/_jsx__default["default"](styled.StyledWrapper, {}, void 0, /*#__PURE__*/jsxRuntime.jsx(styled.StyledChatContainerContentList, {
313
347
  "data-testid": "chat-thread-container",
314
348
  tabIndex: 0,
315
- "aria-live": "polite",
316
- "aria-relevant": "additions",
317
349
  ref: virtualListRef,
318
350
  role: "list",
319
351
  onKeyDown: handleListKey,
320
352
  children: /*#__PURE__*/_jsx__default["default"](styled.StyledChatContainerContentListScroll, {
321
353
  height: "".concat(totalSize, "px")
322
- }, void 0, render, !isLoading && hasMoreItems ? /*#__PURE__*/_jsx__default["default"](styled.StyledLoadMoreContainer, {
354
+ }, void 0, !isLoading && hasMoreItems ? /*#__PURE__*/_jsx__default["default"](styled.StyledLoadMoreContainer, {
323
355
  hasMoreItems: hasMoreItems
324
356
  }, void 0, /*#__PURE__*/_jsx__default["default"](DSButton__default["default"], {
325
357
  labelText: "Load More",
@@ -327,8 +359,11 @@ const ChatContainerMessagesList = props$2 => {
327
359
  onClick: getMoreMessages,
328
360
  "data-testid": "ds-chat-load-more-btn",
329
361
  innerRef: loadMoreRef
330
- })) : null)
331
- }));
362
+ })) : null, render)
363
+ }), /*#__PURE__*/_jsx__default["default"](styled.StyledNewMessagesHiddenContainer, {
364
+ "aria-live": "polite",
365
+ "aria-hidden": "false"
366
+ }, void 0, newMessages.map(newMessage => /*#__PURE__*/_jsx__default["default"]("p", {}, void 0, newMessage.title, " ", newMessage.time, " ", newMessage.body, " ", newMessage.errorMessage, " ", newMessage.helpMessage))));
332
367
  };
333
368
 
334
369
  ChatContainerMessagesList.defaultProps = props.defaultProps;
@@ -20,19 +20,23 @@ const StyledWrapperListItem = /*#__PURE__*/styled__default["default"].div.withCo
20
20
  const StyledChatContainerContentList = /*#__PURE__*/styled__default["default"].div.withConfig({
21
21
  componentId: "sc-1jhprzr-2"
22
22
  })(["width:100%;height:100%;padding:", ";padding-bottom:0;overflow-y:auto;overflow-x:hidden;"], props => props.theme.space.xs);
23
- const StyledChatContainerContentListScroll = /*#__PURE__*/styled__default["default"].div.withConfig({
23
+ const StyledNewMessagesHiddenContainer = /*#__PURE__*/styled__default["default"].div.withConfig({
24
24
  componentId: "sc-1jhprzr-3"
25
+ })(["position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(100%);white-space:nowrap;"]);
26
+ const StyledChatContainerContentListScroll = /*#__PURE__*/styled__default["default"].div.withConfig({
27
+ componentId: "sc-1jhprzr-4"
25
28
  })(["width:100%;height:", ";position:relative;"], props => props.height);
26
29
  const StyledLoadMoreContainer = /*#__PURE__*/styled__default["default"].div.withConfig({
27
- componentId: "sc-1jhprzr-4"
30
+ componentId: "sc-1jhprzr-5"
28
31
  })(["position:absolute;width:calc(100% + ", ");padding:", ";left:-", ";right:-", ";display:grid;justify-content:center;align-items:center;", " ", ""], props => props.theme.space.xxs, props => props.theme.space.xxs, props => props.theme.space.xxs, props => props.theme.space.xxs, props => props.hasMoreItems === 'top' ? 'top: 0;' : '', props => props.hasMoreItems === 'bottom' ? 'bottom: 0;' : '');
29
32
  const ListItem = /*#__PURE__*/styled__default["default"].div.withConfig({
30
- componentId: "sc-1jhprzr-5"
33
+ componentId: "sc-1jhprzr-6"
31
34
  })(["&:focus,&:focus-visible{outline:none;&:after{content:'';display:block;position:absolute;top:0;left:-", ";pointer-events:none;border-radius:2px;background-color:#ebedf0;width:calc(100% + 24px);border:2px solid #1e79c2;height:100%;z-index:-1;}}"], props => props.theme.space.xxs);
32
35
 
33
36
  exports.ListItem = ListItem;
34
37
  exports.StyledChatContainerContentList = StyledChatContainerContentList;
35
38
  exports.StyledChatContainerContentListScroll = StyledChatContainerContentListScroll;
36
39
  exports.StyledLoadMoreContainer = StyledLoadMoreContainer;
40
+ exports.StyledNewMessagesHiddenContainer = StyledNewMessagesHiddenContainer;
37
41
  exports.StyledWrapper = StyledWrapper;
38
42
  exports.StyledWrapperListItem = StyledWrapperListItem;
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ require('core-js/modules/web.dom-collections.iterator.js');
6
+ require('core-js/modules/esnext.async-iterator.for-each.js');
7
+ require('core-js/modules/esnext.iterator.constructor.js');
8
+ require('core-js/modules/esnext.iterator.for-each.js');
9
+ require('core-js/modules/esnext.async-iterator.map.js');
10
+ require('core-js/modules/esnext.iterator.map.js');
11
+ var React = require('react');
12
+
13
+ const useGetNewMessages = messages => {
14
+ const [seenMessages, setSeenMessages] = React.useState({});
15
+ const newMessages = React.useMemo(() => {
16
+ const list = [];
17
+ messages.forEach(message => {
18
+ if (!(message.dsId in seenMessages)) {
19
+ list.push(message);
20
+ }
21
+ });
22
+ setSeenMessages(Object.fromEntries(messages.map(m => [m.dsId, true])));
23
+ return list; // eslint-disable-next-line react-hooks/exhaustive-deps
24
+ }, [messages]);
25
+ return newMessages;
26
+ };
27
+
28
+ exports.useGetNewMessages = useGetNewMessages;
@@ -17,7 +17,8 @@ const ChatDataTestIds = {
17
17
  SYSTEM_MSG_TIME: 'chat-system-message-time',
18
18
  BUBBLE: 'ds-chat-bubble',
19
19
  BUBBLE_TITLE: 'chat-bubble-title',
20
- BUBBLE_BODY: 'chat-bubble-body'
20
+ BUBBLE_BODY: 'chat-bubble-body',
21
+ BANNER: 'ds-banner-container'
21
22
  };
22
23
 
23
24
  export { ChatDataTestIds };
@@ -38,10 +38,12 @@ const ChatComposer = props => {
38
38
  placeholder,
39
39
  inputMaxLength,
40
40
  maxLengthMessage,
41
+ ariaMaxLengthMessage,
41
42
  buttonDisabled,
42
43
  dataTestid,
43
44
  actionRef
44
45
  } = props;
46
+ const COMPOSER_MIN_HEIGHT = 32;
45
47
  const input = React.useRef();
46
48
  const inputSize = React.useRef();
47
49
  const inputSend = React.useRef();
@@ -52,10 +54,10 @@ const ChatComposer = props => {
52
54
  var _inputSize$current, _inputSize$current2, _inputSize$current3, _inputSize$current4;
53
55
 
54
56
  const newHeight = ((_inputSize$current = inputSize.current) === null || _inputSize$current === void 0 ? void 0 : _inputSize$current.scrollHeight) > internalMaxHeight ? internalMaxHeight : (_inputSize$current2 = inputSize.current) === null || _inputSize$current2 === void 0 ? void 0 : _inputSize$current2.scrollHeight;
55
- onResize(newHeight);
57
+ if (newHeight !== height && newHeight > COMPOSER_MIN_HEIGHT) onResize(newHeight);
56
58
  setHeight(newHeight);
57
59
  setShowScroll(((_inputSize$current3 = inputSize.current) === null || _inputSize$current3 === void 0 ? void 0 : _inputSize$current3.scrollHeight) - ((_inputSize$current4 = inputSize.current) === null || _inputSize$current4 === void 0 ? void 0 : _inputSize$current4.clientHeight) > 5);
58
- }, [inputValue, internalMaxHeight, onResize]);
60
+ }, [inputValue, internalMaxHeight, height, onResize]);
59
61
  React.useEffect(() => {
60
62
  if (actionRef && actionRef.current) {
61
63
  actionRef.current.focusToComposer = () => {
@@ -82,6 +84,9 @@ const ChatComposer = props => {
82
84
  "data-testid": dataTestid
83
85
  }, void 0, /*#__PURE__*/_jsx(Grid, {
84
86
  cols: ['1fr', '24px'],
87
+ style: {
88
+ height: heightProps.height
89
+ },
85
90
  justifyContent: "center",
86
91
  alignItems: "flex-end",
87
92
  gutter: "xxs"
@@ -95,7 +100,9 @@ const ChatComposer = props => {
95
100
  onKeyDown: onKeyDown,
96
101
  placeholder: placeholder,
97
102
  "aria-label": "Message to send",
98
- "data-testid": "chat-composer-textarea"
103
+ "data-testid": "chat-composer-textarea",
104
+ name: "chat-composer-textarea",
105
+ id: "chat-composer-textarea"
99
106
  }), /*#__PURE__*/_jsx(Grid, {
100
107
  height: "32px",
101
108
  alignItems: "center"
@@ -122,8 +129,7 @@ const ChatComposer = props => {
122
129
  width: (_input$current = input.current) === null || _input$current === void 0 ? void 0 : _input$current.clientWidth
123
130
  },
124
131
  onChange: handleChange,
125
- value: inputValue // maxLength={inputMaxLength}
126
-
132
+ value: inputValue
127
133
  }), /*#__PURE__*/_jsx(Grid, {
128
134
  cols: ['1fr', '24px'],
129
135
  justifyContent: "flex-end",
@@ -136,7 +142,7 @@ const ChatComposer = props => {
136
142
  }, void 0, inputMaxLength && hasError ? /*#__PURE__*/jsxs(Fragment, {
137
143
  children: [/*#__PURE__*/_jsx(StyledCounterErrorA11y, {
138
144
  role: "alert"
139
- }, void 0, maxLengthMessage || "You have exceeded the character limit by ".concat(inputLength - inputMaxLength)), maxLengthMessage || "Please limit to less than ".concat(inputMaxLength, " characters. ")]
145
+ }, void 0, ariaMaxLengthMessage || "You have entered ".concat(inputLength, " characters and have exceeded the limit by ").concat(inputLength - inputMaxLength, ". Please limit to less than ").concat(inputMaxLength, " characters. ")), maxLengthMessage || "Please limit to less than ".concat(inputMaxLength, " characters. ")]
140
146
  }) : '', inputMaxLength ? " ".concat(inputLength, " / ").concat(inputMaxLength) : ''), _Grid || (_Grid = /*#__PURE__*/_jsx(Grid, {}))));
141
147
  };
142
148
 
@@ -4,6 +4,7 @@ import { noop } from 'lodash';
4
4
  const defaultComposerPros = {
5
5
  placeholder: 'Type your message',
6
6
  inputValue: '',
7
+ ariaMaxLengthMessage: '',
7
8
  onChange: noop,
8
9
  onResize: noop,
9
10
  onSend: noop,
@@ -20,6 +21,7 @@ const defaultComposerPros = {
20
21
  const composerProps = {
21
22
  placeholder: PropTypes.string.description('Input Placeholder').defaultValue(defaultComposerPros.placeholder),
22
23
  inputValue: PropTypes.string.description('Input value').defaultValue(defaultComposerPros.inputValue),
24
+ ariaMaxLengthMessage: PropTypes.string.description('Aria message for Max length message when exceeded').defaultValue(defaultComposerPros.maxLengthMessage),
23
25
  maxLengthMessage: PropTypes.string.description('Max length message').defaultValue(defaultComposerPros.maxLengthMessage),
24
26
  onChange: PropTypes.func.description('Called when the user types').defaultValue(defaultComposerPros.onChange),
25
27
  onResize: PropTypes.func.description('Called the input changes his height').defaultValue(defaultComposerPros.onResize),
@@ -17,14 +17,14 @@ import { StyledIndicatorContainer, StyledChatContainerContent, MobileStyledChatC
17
17
  import { useValidateProps } from './config/useValidateProps.js';
18
18
  import { jsxs, jsx } from 'react/jsx-runtime';
19
19
 
20
- var _DSIndeterminateProgr, _StyledIndicatorConta;
20
+ var _DSIndeterminateProgr;
21
21
 
22
22
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
23
23
 
24
24
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
25
25
 
26
26
  const ChatContainer = props => {
27
- var _containerRef$current;
27
+ var _messagesListProps$me2, _containerRef$current;
28
28
 
29
29
  useValidateProps(props, containerProps);
30
30
  const {
@@ -55,18 +55,16 @@ const ChatContainer = props => {
55
55
  });
56
56
  const onResize = React.useCallback(height => {
57
57
  if (onComposerResize) onComposerResize(height);
58
-
59
- if (height > 30) {
60
- /*
58
+ /*
61
59
  setTimeout is used to equal the animation duration from the
62
60
  composer text area when increase its size
63
61
  */
64
- setTimeout(() => {
65
- actionRef.current.scrollToIndex(messagesListProps.messages.length - 1, {
66
- align: 'start'
67
- });
68
- }, 50);
69
- }
62
+
63
+ setTimeout(() => {
64
+ actionRef.current.scrollToIndex(messagesListProps.messages.length - 1, {
65
+ align: 'start'
66
+ });
67
+ }, 50);
70
68
  }, [onComposerResize, messagesListProps.messages, actionRef]);
71
69
  const Container = device === 'desktop' ? StyledChatContainerContent : MobileStyledChatContainerContent;
72
70
  const handleTabs = React.useCallback(e => {
@@ -74,17 +72,23 @@ const ChatContainer = props => {
74
72
  const ref = actionRef || listRef;
75
73
 
76
74
  if (e.shiftKey && e.key === 'Tab') {
77
- e.preventDefault();
75
+ var _messagesListProps$me;
76
+
77
+ if (((_messagesListProps$me = messagesListProps.messages) === null || _messagesListProps$me === void 0 ? void 0 : _messagesListProps$me.length) > 0) {
78
+ e.preventDefault();
79
+
80
+ if (['top', 'bottom'].includes(bannerPosition)) {
81
+ bannerRef.current.focusOnWrapper();
82
+ } else {
83
+ var _ref$current;
78
84
 
79
- if (['top', 'bottom'].includes(bannerPosition)) {
80
- bannerRef.current.focusOnWrapper();
81
- } else {
82
- const last = ref.current.getMessagesInView().pop();
83
- ref.current.focusToIndexWithoutScroll(last);
85
+ const last = ref.current.getMessagesInView().pop();
86
+ (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focusToIndexWithoutScroll(last);
87
+ }
84
88
  }
85
89
  }
86
90
  }
87
- }, [actionRef, onComposerKeyDown, bannerPosition]); // Map actions
91
+ }, [actionRef, onComposerKeyDown, bannerPosition, (_messagesListProps$me2 = messagesListProps.messages) === null || _messagesListProps$me2 === void 0 ? void 0 : _messagesListProps$me2.length]); // Map actions
88
92
 
89
93
  React.useEffect(() => {
90
94
  if (actionRef && actionRef.current) {
@@ -134,15 +138,15 @@ const ChatContainer = props => {
134
138
  isOpen: true,
135
139
  type: bannerProps.type || BANNER_TYPES.INFO,
136
140
  actionRef: bannerRef
137
- })), sendingMessages && sendingMessages.length > 0 ? _StyledIndicatorConta || (_StyledIndicatorConta = /*#__PURE__*/_jsx(StyledIndicatorContainer, {
141
+ })), sendingMessages && sendingMessages.length > 0 ? /*#__PURE__*/_jsx(StyledIndicatorContainer, {
138
142
  isLoading: "bottom",
139
143
  linear: true,
140
144
  "data-testid": "ds-chat-loading-indicator"
141
145
  }, void 0, /*#__PURE__*/_jsx(DSIndeterminateProgressIndicator, {
142
146
  processing: true,
143
- title: "Loading",
147
+ title: sendingText,
144
148
  lineOnly: true
145
- }))) : null), /*#__PURE__*/jsx(ChatComposer, _objectSpread(_objectSpread({}, composerProps), {}, {
149
+ })) : null), /*#__PURE__*/jsx(ChatComposer, _objectSpread(_objectSpread({}, composerProps), {}, {
146
150
  onKeyDown: handleTabs,
147
151
  onResize: onResize,
148
152
  maxHeight: (((_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.clientHeight) || 0) / 2,
@@ -10,7 +10,7 @@ const StyledChatContainerContent = /*#__PURE__*/styled(Grid).withConfig({
10
10
  })(["height:100%;min-width:", "px;overflow:hidden;background:", ";position:relative;"], CONTAINER_WIDTH, props => props.theme.colors.neutral['050']);
11
11
  const StyledIndicatorContainer = /*#__PURE__*/styled.div.withConfig({
12
12
  componentId: "sc-d732wo-1"
13
- })(["transition:0.4s ease-in-out;position:absolute;visibility:hidden;pointer-events:none;width:100%;padding:0 ", ";left:0;right:0;background:", ";", " ", " & div,a{font-size:13px;}"], props => !props.linear ? props.theme.space.xxs : 0, props => props.theme.colors.neutral['000'], props => props.isLoading === 'top' ? "\n top: -41px;\n transform: translate(0, 41px);\n visibility: visible;\n pointer-events: auto\n " : '', props => props.isLoading === 'bottom' ? "\n bottom: -41px;\n transform: translate(0, -41px);\n visibility: visible;\n pointer-events: auto;\n " : '');
13
+ })(["transition:0.4s ease-in-out;position:absolute;visibility:hidden;pointer-events:none;width:100%;padding:0 ", ";left:0;right:0;background:", ";", " ", " & div,a{font-size:13px;}"], props => !props.linear ? props.theme.space.xxs : 0, props => props.theme.colors.neutral['000'], props => props.isLoading === 'top' ? "\n top: -41px;\n transform: translate(0, 41px);\n visibility: visible;\n pointer-events: auto;\n " : '', props => props.isLoading === 'bottom' ? "\n bottom: -41px;\n transform: translate(0, -41px);\n visibility: visible;\n pointer-events: auto;\n " : '');
14
14
  const MobileStyledChatContainerContent = /*#__PURE__*/styled(StyledChatContainerContent).withConfig({
15
15
  componentId: "sc-d732wo-2"
16
16
  })(["", ",", "{font-size:16px;}", "{font-size:13px;}", "{background-color:#fff;box-shadow:-0 1px 3px rgba(53,60,70,0.8);}", ",", "{font-size:16px;text-align:left;}", "{font-size:13px;}", "{font-size:16px;}", "{font-size:16px;}"], StyledTitleLeftBubble, StyleBodyWrapper, StyledTitleRightBubble, Wrapper, Title, Message, Time, StyledComposerInput, StyledIndicatorContainer);
@@ -15,13 +15,15 @@ import React from 'react';
15
15
  import { describe } from 'react-desc';
16
16
  import { useVirtual } from 'react-virtual';
17
17
  import DSButton from '@elliemae/ds-button';
18
+ import { debounce } from 'lodash';
18
19
  import { ChatBubble } from '../bubble/ChatBubble.js';
19
20
  import { BUBBLE_TYPES } from '../bubble/props.js';
20
21
  import { MessageDelimiter } from '../message-delimiter/MessageDelimiter.js';
21
22
  import { SystemMessage } from '../system-message/SystemMessage.js';
22
- import { ListItem, StyledWrapperListItem, StyledWrapper, StyledChatContainerContentList, StyledChatContainerContentListScroll, StyledLoadMoreContainer } from './styled.js';
23
+ import { ListItem, StyledWrapperListItem, StyledWrapper, StyledChatContainerContentList, StyledChatContainerContentListScroll, StyledLoadMoreContainer, StyledNewMessagesHiddenContainer } from './styled.js';
23
24
  import { listProps, defaultProps } from './props.js';
24
25
  import { useValidateProps } from './config/useValidateProps.js';
26
+ import { useGetNewMessages } from './useGetNewMessages.js';
25
27
  import { jsx } from 'react/jsx-runtime';
26
28
 
27
29
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
@@ -56,6 +58,7 @@ const ChatContainerMessagesList = props => {
56
58
  isLoading,
57
59
  bannerRef
58
60
  } = props;
61
+ const pendingOnScroll = React.useRef(false);
59
62
  const loadMoreRef = React.useRef(null);
60
63
  const virtualListRef = React.useRef(); // estimateSize should not be really required given what was stated on
61
64
  // https://github.com/tannerlinsley/react-virtual/issues/23
@@ -75,9 +78,17 @@ const ChatContainerMessagesList = props => {
75
78
  })), {}, {
76
79
  estimateSize: React.useCallback(() => 80, [])
77
80
  }));
78
- const onScrollStop = React.useCallback(() => {
81
+ const newMessages = useGetNewMessages(messages);
82
+ const debouncedScroll = React.useMemo(() => debounce(() => {
79
83
  var _virtualListRef$curre;
80
84
 
85
+ if (document.visibilityState === 'hidden') {
86
+ pendingOnScroll.current = true;
87
+ return [];
88
+ }
89
+
90
+ if (!virtualListRef.current) return [];
91
+ pendingOnScroll.current = false;
81
92
  const {
82
93
  bottom,
83
94
  top
@@ -86,15 +97,15 @@ const ChatContainerMessagesList = props => {
86
97
  const indexList = [];
87
98
  list.forEach(item => {
88
99
  const {
89
- top: itemTop,
90
- bottom: itemBottom
100
+ top: itemTop
91
101
  } = item.getBoundingClientRect();
92
102
  const itemPadding = 10;
93
- if (itemTop + itemPadding < bottom && itemBottom - itemPadding > top) indexList.push(item.dataset.index);
103
+ if (itemTop + itemPadding < bottom && itemTop + itemPadding > top) indexList.push(item.dataset.index);
94
104
  });
95
105
  if (onScrollEnds) onScrollEnds(indexList);
96
106
  return indexList;
97
- }, [onScrollEnds]);
107
+ }, 500), [onScrollEnds]);
108
+ const onScrollStop = React.useCallback(debouncedScroll, [debouncedScroll]);
98
109
  const onlyFocus = React.useCallback(index => {
99
110
  setTimeout(() => {
100
111
  virtualListRef.current.querySelector("[data-index=\"".concat(index, "\"]")).focus();
@@ -106,7 +117,8 @@ const ChatContainerMessagesList = props => {
106
117
  };
107
118
  vScroll(index, b);
108
119
  onlyFocus(index);
109
- }, [onlyFocus, vScroll]);
120
+ onScrollStop();
121
+ }, [onScrollStop, onlyFocus, vScroll]);
110
122
  React.useEffect(() => {
111
123
  if (actionRef && actionRef.current) {
112
124
  actionRef.current.scrollToIndex = function (a) {
@@ -114,9 +126,11 @@ const ChatContainerMessagesList = props => {
114
126
  align: 'center'
115
127
  };
116
128
  vScroll(a, b);
129
+ onScrollStop();
117
130
  };
118
131
 
119
132
  actionRef.current.getMessagesInView = onScrollStop;
133
+ onScrollStop(); // init useMemo due to debounce use.
120
134
 
121
135
  actionRef.current.focusToIndex = function (a) {
122
136
  let b = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
@@ -127,7 +141,7 @@ const ChatContainerMessagesList = props => {
127
141
 
128
142
  actionRef.current.focusToIndexWithoutScroll = onlyFocus;
129
143
 
130
- actionRef.current.focusToList = a => {
144
+ actionRef.current.focusToList = () => {
131
145
  var _virtualListRef$curre2;
132
146
 
133
147
  (_virtualListRef$curre2 = virtualListRef.current) === null || _virtualListRef$curre2 === void 0 ? void 0 : _virtualListRef$curre2.focus();
@@ -144,7 +158,17 @@ const ChatContainerMessagesList = props => {
144
158
  switch (key) {
145
159
  case 'ArrowUp':
146
160
  e.preventDefault();
147
- if (index > 0) virtualListRef.current.querySelector("[data-index=\"".concat(index - 1, "\"]")).focus();
161
+
162
+ if (index > 0) {
163
+ var _virtualListRef$curre3;
164
+
165
+ ((_virtualListRef$curre3 = virtualListRef.current) === null || _virtualListRef$curre3 === void 0 ? void 0 : _virtualListRef$curre3.querySelector("[data-index=\"".concat(index - 1, "\"]"))).focus();
166
+ } else if (hasMoreItems === 'top') {
167
+ var _loadMoreRef$current;
168
+
169
+ (_loadMoreRef$current = loadMoreRef.current) === null || _loadMoreRef$current === void 0 ? void 0 : _loadMoreRef$current.focus();
170
+ }
171
+
148
172
  break;
149
173
 
150
174
  case 'ArrowDown':
@@ -167,31 +191,31 @@ const ChatContainerMessagesList = props => {
167
191
  break;
168
192
 
169
193
  case 'Tab':
170
- e.preventDefault();
171
-
172
194
  if (hasMoreItems === 'bottom' && !e.shiftKey) {
173
- var _loadMoreRef$current;
174
-
175
- (_loadMoreRef$current = loadMoreRef.current) === null || _loadMoreRef$current === void 0 ? void 0 : _loadMoreRef$current.focus();
176
- } else if (hasMoreItems === 'top' && e.shiftKey) {
177
195
  var _loadMoreRef$current2;
178
196
 
197
+ e.preventDefault();
179
198
  (_loadMoreRef$current2 = loadMoreRef.current) === null || _loadMoreRef$current2 === void 0 ? void 0 : _loadMoreRef$current2.focus();
199
+ } else if (hasMoreItems === 'top' && e.shiftKey) {
200
+ var _loadMoreRef$current3;
201
+
202
+ e.preventDefault();
203
+ (_loadMoreRef$current3 = loadMoreRef.current) === null || _loadMoreRef$current3 === void 0 ? void 0 : _loadMoreRef$current3.focus();
180
204
  } else if (!e.shiftKey) {
181
205
  if (bannerPosition) {
206
+ e.preventDefault();
182
207
  bannerRef.current.focusOnWrapper();
183
- } else {
184
- actionRef.current.focusToComposer();
185
208
  }
186
209
  } else {
187
- var _virtualListRef$curre3;
210
+ var _virtualListRef$curre4;
188
211
 
189
- (_virtualListRef$curre3 = virtualListRef.current) === null || _virtualListRef$curre3 === void 0 ? void 0 : _virtualListRef$curre3.focus();
212
+ e.preventDefault();
213
+ (_virtualListRef$curre4 = virtualListRef.current) === null || _virtualListRef$curre4 === void 0 ? void 0 : _virtualListRef$curre4.focus();
190
214
  }
191
215
 
192
216
  break;
193
217
  }
194
- }, [actionRef, hasMoreItems, messages.length, bannerPosition, scrollAndFocus]);
218
+ }, [bannerRef, hasMoreItems, messages.length, bannerPosition, scrollAndFocus]);
195
219
  const handleListKey = React.useCallback(e => {
196
220
  const {
197
221
  key
@@ -200,12 +224,22 @@ const ChatContainerMessagesList = props => {
200
224
  switch (key) {
201
225
  case 'ArrowUp':
202
226
  e.preventDefault();
203
- onlyFocus(onScrollStop().shift());
227
+
228
+ if (document.activeElement !== loadMoreRef.current) {
229
+ onlyFocus(onScrollStop().shift());
230
+ }
231
+
204
232
  break;
205
233
 
206
234
  case 'ArrowDown':
207
235
  e.preventDefault();
208
- onlyFocus(onScrollStop().pop());
236
+
237
+ if (document.activeElement === loadMoreRef.current) {
238
+ virtualListRef.current.querySelector("[data-index=\"0\"]").focus();
239
+ } else {
240
+ onlyFocus(onScrollStop().pop());
241
+ }
242
+
209
243
  break;
210
244
 
211
245
  case 'Home':
@@ -221,11 +255,6 @@ const ChatContainerMessagesList = props => {
221
255
  case 'Tab':
222
256
  if (document.activeElement !== loadMoreRef.current) {
223
257
  if (hasMoreItems === 'bottom' && !e.shiftKey) {
224
- var _loadMoreRef$current3;
225
-
226
- e.preventDefault();
227
- (_loadMoreRef$current3 = loadMoreRef.current) === null || _loadMoreRef$current3 === void 0 ? void 0 : _loadMoreRef$current3.focus();
228
- } else if (hasMoreItems === 'top' && e.shiftKey) {
229
258
  var _loadMoreRef$current4;
230
259
 
231
260
  e.preventDefault();
@@ -238,15 +267,18 @@ const ChatContainerMessagesList = props => {
238
267
  e.preventDefault();
239
268
  actionRef.current.focusToComposer();
240
269
  } else {
241
- var _virtualListRef$curre4;
270
+ var _virtualListRef$curre5;
242
271
 
243
272
  e.preventDefault();
244
- (_virtualListRef$curre4 = virtualListRef.current) === null || _virtualListRef$curre4 === void 0 ? void 0 : _virtualListRef$curre4.focus();
273
+ (_virtualListRef$curre5 = virtualListRef.current) === null || _virtualListRef$curre5 === void 0 ? void 0 : _virtualListRef$curre5.focus();
245
274
  }
246
275
 
247
276
  break;
248
277
  }
249
278
  }, [actionRef, hasMoreItems, messages.length, onScrollStop, onlyFocus, scrollAndFocus]);
279
+ const checkPendingOnScroll = React.useCallback(() => {
280
+ if (pendingOnScroll.current === true) onScrollStop();
281
+ }, [onScrollStop]);
250
282
  React.useEffect(() => {
251
283
  let handlerCallback = null;
252
284
  const localVirtualRef = virtualListRef.current;
@@ -260,11 +292,13 @@ const ChatContainerMessagesList = props => {
260
292
  virtualListRef.current.addEventListener('scroll', onScroll);
261
293
  }
262
294
 
295
+ document.addEventListener('visibilitychange', checkPendingOnScroll);
263
296
  return () => {
264
297
  if (handlerCallback) clearTimeout(handlerCallback);
265
298
  if (onScrollEnds) localVirtualRef.removeEventListener('scroll', onScroll);
299
+ document.removeEventListener('visibilitychange', checkPendingOnScroll);
266
300
  };
267
- }, [onScrollStop, onScrollEnds]);
301
+ }, [onScrollStop, onScrollEnds, checkPendingOnScroll]);
268
302
  const render = React.useMemo(() => virtualItems.map(virtualItem => {
269
303
  const {
270
304
  index,
@@ -285,7 +319,7 @@ const ChatContainerMessagesList = props => {
285
319
  "data-index": index,
286
320
  style: style,
287
321
  ref: measureRef,
288
- tabIndex: 0,
322
+ tabIndex: -1,
289
323
  onKeyDown: handleKey,
290
324
  role: "listitem",
291
325
  children: /*#__PURE__*/_jsx(StyledWrapperListItem, {
@@ -301,14 +335,12 @@ const ChatContainerMessagesList = props => {
301
335
  return /*#__PURE__*/_jsx(StyledWrapper, {}, void 0, /*#__PURE__*/jsx(StyledChatContainerContentList, {
302
336
  "data-testid": "chat-thread-container",
303
337
  tabIndex: 0,
304
- "aria-live": "polite",
305
- "aria-relevant": "additions",
306
338
  ref: virtualListRef,
307
339
  role: "list",
308
340
  onKeyDown: handleListKey,
309
341
  children: /*#__PURE__*/_jsx(StyledChatContainerContentListScroll, {
310
342
  height: "".concat(totalSize, "px")
311
- }, void 0, render, !isLoading && hasMoreItems ? /*#__PURE__*/_jsx(StyledLoadMoreContainer, {
343
+ }, void 0, !isLoading && hasMoreItems ? /*#__PURE__*/_jsx(StyledLoadMoreContainer, {
312
344
  hasMoreItems: hasMoreItems
313
345
  }, void 0, /*#__PURE__*/_jsx(DSButton, {
314
346
  labelText: "Load More",
@@ -316,8 +348,11 @@ const ChatContainerMessagesList = props => {
316
348
  onClick: getMoreMessages,
317
349
  "data-testid": "ds-chat-load-more-btn",
318
350
  innerRef: loadMoreRef
319
- })) : null)
320
- }));
351
+ })) : null, render)
352
+ }), /*#__PURE__*/_jsx(StyledNewMessagesHiddenContainer, {
353
+ "aria-live": "polite",
354
+ "aria-hidden": "false"
355
+ }, void 0, newMessages.map(newMessage => /*#__PURE__*/_jsx("p", {}, void 0, newMessage.title, " ", newMessage.time, " ", newMessage.body, " ", newMessage.errorMessage, " ", newMessage.helpMessage))));
321
356
  };
322
357
 
323
358
  ChatContainerMessagesList.defaultProps = defaultProps;
@@ -12,14 +12,17 @@ const StyledWrapperListItem = /*#__PURE__*/styled.div.withConfig({
12
12
  const StyledChatContainerContentList = /*#__PURE__*/styled.div.withConfig({
13
13
  componentId: "sc-1jhprzr-2"
14
14
  })(["width:100%;height:100%;padding:", ";padding-bottom:0;overflow-y:auto;overflow-x:hidden;"], props => props.theme.space.xs);
15
- const StyledChatContainerContentListScroll = /*#__PURE__*/styled.div.withConfig({
15
+ const StyledNewMessagesHiddenContainer = /*#__PURE__*/styled.div.withConfig({
16
16
  componentId: "sc-1jhprzr-3"
17
+ })(["position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(100%);white-space:nowrap;"]);
18
+ const StyledChatContainerContentListScroll = /*#__PURE__*/styled.div.withConfig({
19
+ componentId: "sc-1jhprzr-4"
17
20
  })(["width:100%;height:", ";position:relative;"], props => props.height);
18
21
  const StyledLoadMoreContainer = /*#__PURE__*/styled.div.withConfig({
19
- componentId: "sc-1jhprzr-4"
22
+ componentId: "sc-1jhprzr-5"
20
23
  })(["position:absolute;width:calc(100% + ", ");padding:", ";left:-", ";right:-", ";display:grid;justify-content:center;align-items:center;", " ", ""], props => props.theme.space.xxs, props => props.theme.space.xxs, props => props.theme.space.xxs, props => props.theme.space.xxs, props => props.hasMoreItems === 'top' ? 'top: 0;' : '', props => props.hasMoreItems === 'bottom' ? 'bottom: 0;' : '');
21
24
  const ListItem = /*#__PURE__*/styled.div.withConfig({
22
- componentId: "sc-1jhprzr-5"
25
+ componentId: "sc-1jhprzr-6"
23
26
  })(["&:focus,&:focus-visible{outline:none;&:after{content:'';display:block;position:absolute;top:0;left:-", ";pointer-events:none;border-radius:2px;background-color:#ebedf0;width:calc(100% + 24px);border:2px solid #1e79c2;height:100%;z-index:-1;}}"], props => props.theme.space.xxs);
24
27
 
25
- export { ListItem, StyledChatContainerContentList, StyledChatContainerContentListScroll, StyledLoadMoreContainer, StyledWrapper, StyledWrapperListItem };
28
+ export { ListItem, StyledChatContainerContentList, StyledChatContainerContentListScroll, StyledLoadMoreContainer, StyledNewMessagesHiddenContainer, StyledWrapper, StyledWrapperListItem };
@@ -0,0 +1,24 @@
1
+ import 'core-js/modules/web.dom-collections.iterator.js';
2
+ import 'core-js/modules/esnext.async-iterator.for-each.js';
3
+ import 'core-js/modules/esnext.iterator.constructor.js';
4
+ import 'core-js/modules/esnext.iterator.for-each.js';
5
+ import 'core-js/modules/esnext.async-iterator.map.js';
6
+ import 'core-js/modules/esnext.iterator.map.js';
7
+ import { useState, useMemo } from 'react';
8
+
9
+ const useGetNewMessages = messages => {
10
+ const [seenMessages, setSeenMessages] = useState({});
11
+ const newMessages = useMemo(() => {
12
+ const list = [];
13
+ messages.forEach(message => {
14
+ if (!(message.dsId in seenMessages)) {
15
+ list.push(message);
16
+ }
17
+ });
18
+ setSeenMessages(Object.fromEntries(messages.map(m => [m.dsId, true])));
19
+ return list; // eslint-disable-next-line react-hooks/exhaustive-deps
20
+ }, [messages]);
21
+ return newMessages;
22
+ };
23
+
24
+ export { useGetNewMessages };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elliemae/ds-chat",
3
- "version": "2.0.2",
3
+ "version": "2.0.6",
4
4
  "license": "MIT",
5
5
  "description": "ICE MT - Dimsum - Chat",
6
6
  "module": "./esm/index.js",
@@ -183,6 +183,10 @@
183
183
  "import": "./esm/parts/chat-container/ChatContainer.js",
184
184
  "require": "./cjs/parts/chat-container/ChatContainer.js"
185
185
  },
186
+ "./parts/chat-container-messages-list/useGetNewMessages": {
187
+ "import": "./esm/parts/chat-container-messages-list/useGetNewMessages.js",
188
+ "require": "./cjs/parts/chat-container-messages-list/useGetNewMessages.js"
189
+ },
186
190
  "./parts/chat-container-messages-list/styled": {
187
191
  "import": "./esm/parts/chat-container-messages-list/styled.js",
188
192
  "require": "./cjs/parts/chat-container-messages-list/styled.js"
@@ -305,15 +309,15 @@
305
309
  "generateSubmodules": true
306
310
  },
307
311
  "dependencies": {
308
- "@elliemae/ds-banner": "2.0.2",
309
- "@elliemae/ds-button": "2.0.2",
310
- "@elliemae/ds-grid": "2.0.2",
311
- "@elliemae/ds-icons": "2.0.2",
312
- "@elliemae/ds-indeterminate-progress-indicator": "2.0.2",
313
- "@elliemae/ds-props-helpers": "2.0.2",
314
- "@elliemae/ds-system": "2.0.2",
315
- "@elliemae/ds-tooltip": "2.0.2",
316
- "@elliemae/ds-truncated-tooltip-text": "2.0.2",
312
+ "@elliemae/ds-banner": "2.0.6",
313
+ "@elliemae/ds-button": "2.0.6",
314
+ "@elliemae/ds-grid": "2.0.6",
315
+ "@elliemae/ds-icons": "2.0.6",
316
+ "@elliemae/ds-indeterminate-progress-indicator": "2.0.6",
317
+ "@elliemae/ds-props-helpers": "2.0.6",
318
+ "@elliemae/ds-system": "2.0.6",
319
+ "@elliemae/ds-tooltip": "2.0.6",
320
+ "@elliemae/ds-truncated-tooltip-text": "2.0.6",
317
321
  "prop-types": "~15.7.2",
318
322
  "react-desc": "~4.1.2",
319
323
  "react-virtual": "~2.3.2"
@@ -18,4 +18,5 @@ export declare const ChatDataTestIds: {
18
18
  BUBBLE: string;
19
19
  BUBBLE_TITLE: string;
20
20
  BUBBLE_BODY: string;
21
+ BANNER: string;
21
22
  };
@@ -10,6 +10,9 @@ declare const ChatComposer: {
10
10
  inputValue: {
11
11
  deprecated: import("react-desc").PropTypesDescValidator;
12
12
  };
13
+ ariaMaxLengthMessage: {
14
+ deprecated: import("react-desc").PropTypesDescValidator;
15
+ };
13
16
  maxLengthMessage: {
14
17
  deprecated: import("react-desc").PropTypesDescValidator;
15
18
  };
@@ -2,6 +2,7 @@ import { BubbleProps } from '../bubble/props';
2
2
  export declare const StyledWrapper: import("styled-components").StyledComponent<"div", import("styled-components").DefaultTheme, {}, never>;
3
3
  export declare const StyledWrapperListItem: import("styled-components").StyledComponent<"div", import("styled-components").DefaultTheme, Partial<BubbleProps>, never>;
4
4
  export declare const StyledChatContainerContentList: import("styled-components").StyledComponent<"div", import("styled-components").DefaultTheme, {}, never>;
5
+ export declare const StyledNewMessagesHiddenContainer: import("styled-components").StyledComponent<"div", import("styled-components").DefaultTheme, {}, never>;
5
6
  export declare const StyledChatContainerContentListScroll: import("styled-components").StyledComponent<"div", import("styled-components").DefaultTheme, {
6
7
  height: string;
7
8
  }, never>;
@@ -0,0 +1,2 @@
1
+ import { BubbleProps } from '../bubble/props';
2
+ export declare const useGetNewMessages: (messages: BubbleProps[]) => BubbleProps[];
@@ -1,27 +1 @@
1
- export declare const preLoadMessages2: ({
2
- dsId: string;
3
- body: string;
4
- type: string;
5
- title: string;
6
- time: string;
7
- extraProps?: undefined;
8
- helpMessage?: undefined;
9
- } | {
10
- dsId: string;
11
- type: string;
12
- title: string;
13
- extraProps: {
14
- 'data-testid': string;
15
- };
16
- body?: undefined;
17
- time?: undefined;
18
- helpMessage?: undefined;
19
- } | {
20
- dsId: string;
21
- body: string;
22
- type: string;
23
- title: string;
24
- time: string;
25
- helpMessage: string | null;
26
- extraProps?: undefined;
27
- })[];
1
+ export declare const loadMsgs: (q: number) => unknown;