|
@@ -1,5 +1,6 @@
|
1
|
1
|
// @flow
|
2
|
2
|
|
|
3
|
+import clsx from 'clsx';
|
3
|
4
|
import React from 'react';
|
4
|
5
|
|
5
|
6
|
import { translate } from '../../../base/i18n';
|
|
@@ -11,14 +12,12 @@ import AbstractChat, {
|
11
|
12
|
type Props
|
12
|
13
|
} from '../AbstractChat';
|
13
|
14
|
|
14
|
|
-import ChatDialog from './ChatDialog';
|
15
|
|
-import Header from './ChatDialogHeader';
|
|
15
|
+import ChatHeader from './ChatHeader';
|
16
|
16
|
import ChatInput from './ChatInput';
|
17
|
17
|
import DisplayNameForm from './DisplayNameForm';
|
18
|
18
|
import KeyboardAvoider from './KeyboardAvoider';
|
19
|
19
|
import MessageContainer from './MessageContainer';
|
20
|
20
|
import MessageRecipient from './MessageRecipient';
|
21
|
|
-import TouchmoveHack from './TouchmoveHack';
|
22
|
21
|
|
23
|
22
|
/**
|
24
|
23
|
* React Component for holding the chat feature in a side panel that slides in
|
|
@@ -26,12 +25,6 @@ import TouchmoveHack from './TouchmoveHack';
|
26
|
25
|
*/
|
27
|
26
|
class Chat extends AbstractChat<Props> {
|
28
|
27
|
|
29
|
|
- /**
|
30
|
|
- * Whether or not the {@code Chat} component is off-screen, having finished
|
31
|
|
- * its hiding animation.
|
32
|
|
- */
|
33
|
|
- _isExited: boolean;
|
34
|
|
-
|
35
|
28
|
/**
|
36
|
29
|
* Reference to the React Component for displaying chat messages. Used for
|
37
|
30
|
* scrolling to the end of the chat messages.
|
|
@@ -47,13 +40,13 @@ class Chat extends AbstractChat<Props> {
|
47
|
40
|
constructor(props: Props) {
|
48
|
41
|
super(props);
|
49
|
42
|
|
50
|
|
- this._isExited = true;
|
51
|
43
|
this._messageContainerRef = React.createRef();
|
52
|
44
|
|
53
|
45
|
// Bind event handlers so they are only bound once for every instance.
|
54
|
|
- this._renderPanelContent = this._renderPanelContent.bind(this);
|
|
46
|
+ this._onChatTabKeyDown = this._onChatTabKeyDown.bind(this);
|
55
|
47
|
this._onChatInputResize = this._onChatInputResize.bind(this);
|
56
|
48
|
this._onEscClick = this._onEscClick.bind(this);
|
|
49
|
+ this._onPollsTabKeyDown = this._onPollsTabKeyDown.bind(this);
|
57
|
50
|
this._onToggleChat = this._onToggleChat.bind(this);
|
58
|
51
|
}
|
59
|
52
|
|
|
@@ -78,21 +71,6 @@ class Chat extends AbstractChat<Props> {
|
78
|
71
|
this._scrollMessageContainerToBottom(false);
|
79
|
72
|
}
|
80
|
73
|
}
|
81
|
|
- _onEscClick: (KeyboardEvent) => void;
|
82
|
|
-
|
83
|
|
- /**
|
84
|
|
- * Click handler for the chat sidenav.
|
85
|
|
- *
|
86
|
|
- * @param {KeyboardEvent} event - Esc key click to close the popup.
|
87
|
|
- * @returns {void}
|
88
|
|
- */
|
89
|
|
- _onEscClick(event) {
|
90
|
|
- if (event.key === 'Escape' && this.props._isOpen) {
|
91
|
|
- event.preventDefault();
|
92
|
|
- event.stopPropagation();
|
93
|
|
- this._onToggleChat();
|
94
|
|
- }
|
95
|
|
- }
|
96
|
74
|
|
97
|
75
|
/**
|
98
|
76
|
* Implements React's {@link Component#render()}.
|
|
@@ -101,10 +79,22 @@ class Chat extends AbstractChat<Props> {
|
101
|
79
|
* @returns {ReactElement}
|
102
|
80
|
*/
|
103
|
81
|
render() {
|
|
82
|
+ const { _isOpen, _isPollsEnabled, _showNamePrompt } = this.props;
|
|
83
|
+
|
104
|
84
|
return (
|
105
|
|
- <>
|
106
|
|
- { this._renderPanelContent() }
|
107
|
|
- </>
|
|
85
|
+ _isOpen ? <div
|
|
86
|
+ className = 'sideToolbarContainer'
|
|
87
|
+ id = 'sideToolbarContainer'
|
|
88
|
+ onKeyDown = { this._onEscClick } >
|
|
89
|
+ <ChatHeader
|
|
90
|
+ className = 'chat-header'
|
|
91
|
+ id = 'chat-header'
|
|
92
|
+ isPollsEnabled = { _isPollsEnabled }
|
|
93
|
+ onCancel = { this._onToggleChat } />
|
|
94
|
+ { _showNamePrompt
|
|
95
|
+ ? <DisplayNameForm isPollsEnabled = { _isPollsEnabled } />
|
|
96
|
+ : this._renderChat() }
|
|
97
|
+ </div> : null
|
108
|
98
|
);
|
109
|
99
|
}
|
110
|
100
|
|
|
@@ -121,6 +111,54 @@ class Chat extends AbstractChat<Props> {
|
121
|
111
|
this._messageContainerRef.current.maybeUpdateBottomScroll();
|
122
|
112
|
}
|
123
|
113
|
|
|
114
|
+ _onChatTabKeyDown: (KeyboardEvent) => void;
|
|
115
|
+
|
|
116
|
+ /**
|
|
117
|
+ * Key press handler for the chat tab.
|
|
118
|
+ *
|
|
119
|
+ * @param {KeyboardEvent} event - The event.
|
|
120
|
+ * @returns {void}
|
|
121
|
+ */
|
|
122
|
+ _onChatTabKeyDown(event) {
|
|
123
|
+ if (event.key === 'Enter' || event.key === ' ') {
|
|
124
|
+ event.preventDefault();
|
|
125
|
+ event.stopPropagation();
|
|
126
|
+ this._onToggleChatTab();
|
|
127
|
+ }
|
|
128
|
+ }
|
|
129
|
+
|
|
130
|
+ _onEscClick: (KeyboardEvent) => void;
|
|
131
|
+
|
|
132
|
+ /**
|
|
133
|
+ * Click handler for the chat sidenav.
|
|
134
|
+ *
|
|
135
|
+ * @param {KeyboardEvent} event - Esc key click to close the popup.
|
|
136
|
+ * @returns {void}
|
|
137
|
+ */
|
|
138
|
+ _onEscClick(event) {
|
|
139
|
+ if (event.key === 'Escape' && this.props._isOpen) {
|
|
140
|
+ event.preventDefault();
|
|
141
|
+ event.stopPropagation();
|
|
142
|
+ this._onToggleChat();
|
|
143
|
+ }
|
|
144
|
+ }
|
|
145
|
+
|
|
146
|
+ _onPollsTabKeyDown: (KeyboardEvent) => void;
|
|
147
|
+
|
|
148
|
+ /**
|
|
149
|
+ * Key press handler for the polls tab.
|
|
150
|
+ *
|
|
151
|
+ * @param {KeyboardEvent} event - The event.
|
|
152
|
+ * @returns {void}
|
|
153
|
+ */
|
|
154
|
+ _onPollsTabKeyDown(event) {
|
|
155
|
+ if (event.key === 'Enter' || event.key === ' ') {
|
|
156
|
+ event.preventDefault();
|
|
157
|
+ event.stopPropagation();
|
|
158
|
+ this._onTogglePollsTab();
|
|
159
|
+ }
|
|
160
|
+ }
|
|
161
|
+
|
124
|
162
|
/**
|
125
|
163
|
* Returns a React Element for showing chat messages and a form to send new
|
126
|
164
|
* chat messages.
|
|
@@ -129,12 +167,18 @@ class Chat extends AbstractChat<Props> {
|
129
|
167
|
* @returns {ReactElement}
|
130
|
168
|
*/
|
131
|
169
|
_renderChat() {
|
|
170
|
+ const { _isPollsEnabled, _isPollsTabFocused } = this.props;
|
132
|
171
|
|
133
|
|
- if (this.props._isPollsTabFocused) {
|
|
172
|
+ if (_isPollsTabFocused) {
|
134
|
173
|
return (
|
135
|
174
|
<>
|
136
|
|
- { this.props._isPollsEnabled && this._renderTabs()}
|
137
|
|
- <TouchmoveHack isModal = { true }><PollsPane /></TouchmoveHack>
|
|
175
|
+ {_isPollsEnabled && this._renderTabs()}
|
|
176
|
+ <div
|
|
177
|
+ aria-labelledby = 'polls-tab'
|
|
178
|
+ id = 'polls-panel'
|
|
179
|
+ role = 'tabpanel'>
|
|
180
|
+ <PollsPane />
|
|
181
|
+ </div>
|
138
|
182
|
<KeyboardAvoider />
|
139
|
183
|
</>
|
140
|
184
|
);
|
|
@@ -142,19 +186,21 @@ class Chat extends AbstractChat<Props> {
|
142
|
186
|
|
143
|
187
|
return (
|
144
|
188
|
<>
|
145
|
|
- {this.props._isPollsEnabled && this._renderTabs()}
|
146
|
|
- <TouchmoveHack
|
147
|
|
- flex = { true }
|
148
|
|
- isModal = { this.props._isModal }>
|
|
189
|
+ {_isPollsEnabled && this._renderTabs()}
|
|
190
|
+ <div
|
|
191
|
+ aria-labelledby = 'chat-tab'
|
|
192
|
+ className = { clsx('chat-panel', !_isPollsEnabled && 'chat-panel-no-tabs') }
|
|
193
|
+ id = 'chat-panel'
|
|
194
|
+ role = 'tabpanel'>
|
149
|
195
|
<MessageContainer
|
150
|
196
|
messages = { this.props._messages }
|
151
|
197
|
ref = { this._messageContainerRef } />
|
152
|
|
- </TouchmoveHack>
|
153
|
|
- <MessageRecipient />
|
154
|
|
- <ChatInput
|
155
|
|
- onResize = { this._onChatInputResize }
|
156
|
|
- onSend = { this._onSendMessage } />
|
157
|
|
- <KeyboardAvoider />
|
|
198
|
+ <MessageRecipient />
|
|
199
|
+ <ChatInput
|
|
200
|
+ onResize = { this._onChatInputResize }
|
|
201
|
+ onSend = { this._onSendMessage } />
|
|
202
|
+ <KeyboardAvoider />
|
|
203
|
+ </div>
|
158
|
204
|
</>
|
159
|
205
|
);
|
160
|
206
|
}
|
|
@@ -166,36 +212,55 @@ class Chat extends AbstractChat<Props> {
|
166
|
212
|
* @returns {ReactElement}
|
167
|
213
|
*/
|
168
|
214
|
_renderTabs() {
|
|
215
|
+ const { _isPollsEnabled, _isPollsTabFocused, _nbUnreadMessages, _nbUnreadPolls, t } = this.props;
|
169
|
216
|
|
170
|
217
|
return (
|
171
|
|
- <div className = { 'chat-tabs-container' }>
|
|
218
|
+ <div
|
|
219
|
+ ariaLabel = { t(_isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
|
|
220
|
+ className = { 'chat-tabs-container' }
|
|
221
|
+ role = 'tablist'>
|
172
|
222
|
<div
|
|
223
|
+ aria-controls = 'chat-panel'
|
|
224
|
+ aria-label = { t('chat.tabs.chat') }
|
|
225
|
+ aria-selected = { !_isPollsTabFocused }
|
173
|
226
|
className = { `chat-tab ${
|
174
|
|
- this.props._isPollsTabFocused ? '' : 'chat-tab-focus'
|
|
227
|
+ _isPollsTabFocused ? '' : 'chat-tab-focus'
|
175
|
228
|
}` }
|
176
|
|
- onClick = { this._onToggleChatTab }>
|
177
|
|
- <span className = { 'chat-tab-title' }>
|
178
|
|
- {this.props.t('chat.tabs.chat')}
|
|
229
|
+ id = 'chat-tab'
|
|
230
|
+ onClick = { this._onToggleChatTab }
|
|
231
|
+ onKeyDown = { this._onChatTabKeyDown }
|
|
232
|
+ role = 'tab'
|
|
233
|
+ tabIndex = '0'>
|
|
234
|
+ <span
|
|
235
|
+ className = { 'chat-tab-title' }>
|
|
236
|
+ {t('chat.tabs.chat')}
|
179
|
237
|
</span>
|
180
|
238
|
{this.props._isPollsTabFocused
|
181
|
|
- && this.props._nbUnreadMessages > 0 && (
|
|
239
|
+ && _nbUnreadMessages > 0 && (
|
182
|
240
|
<span className = { 'chat-tab-badge' }>
|
183
|
|
- {this.props._nbUnreadMessages}
|
|
241
|
+ {_nbUnreadMessages}
|
184
|
242
|
</span>
|
185
|
243
|
)}
|
186
|
244
|
</div>
|
187
|
245
|
<div
|
|
246
|
+ aria-controls = 'polls-panel'
|
|
247
|
+ aria-label = { t('chat.tabs.polls') }
|
|
248
|
+ aria-selected = { _isPollsTabFocused }
|
188
|
249
|
className = { `chat-tab ${
|
189
|
|
- this.props._isPollsTabFocused ? 'chat-tab-focus' : ''
|
|
250
|
+ _isPollsTabFocused ? 'chat-tab-focus' : ''
|
190
|
251
|
}` }
|
191
|
|
- onClick = { this._onTogglePollsTab }>
|
|
252
|
+ id = 'polls-tab'
|
|
253
|
+ onClick = { this._onTogglePollsTab }
|
|
254
|
+ onKeyDown = { this._onPollsTabKeyDown }
|
|
255
|
+ role = 'tab'
|
|
256
|
+ tabIndex = '0'>
|
192
|
257
|
<span className = { 'chat-tab-title' }>
|
193
|
|
- {this.props.t('chat.tabs.polls')}
|
|
258
|
+ {t('chat.tabs.polls')}
|
194
|
259
|
</span>
|
195
|
|
- {!this.props._isPollsTabFocused
|
|
260
|
+ {!_isPollsTabFocused
|
196
|
261
|
&& this.props._nbUnreadPolls > 0 && (
|
197
|
262
|
<span className = { 'chat-tab-badge' }>
|
198
|
|
- {this.props._nbUnreadPolls}
|
|
263
|
+ {_nbUnreadPolls}
|
199
|
264
|
</span>
|
200
|
265
|
)}
|
201
|
266
|
</div>
|
|
@@ -203,74 +268,6 @@ class Chat extends AbstractChat<Props> {
|
203
|
268
|
);
|
204
|
269
|
}
|
205
|
270
|
|
206
|
|
- /**
|
207
|
|
- * Instantiates a React Element to display at the top of {@code Chat} to
|
208
|
|
- * close {@code Chat}.
|
209
|
|
- *
|
210
|
|
- * @private
|
211
|
|
- * @returns {ReactElement}
|
212
|
|
- */
|
213
|
|
- _renderChatHeader() {
|
214
|
|
- return (
|
215
|
|
- <Header
|
216
|
|
- className = 'chat-header'
|
217
|
|
- id = 'chat-header'
|
218
|
|
- isPollsEnabled = { this.props._isPollsEnabled }
|
219
|
|
- onCancel = { this._onToggleChat } />
|
220
|
|
- );
|
221
|
|
- }
|
222
|
|
-
|
223
|
|
- _renderPanelContent: () => React$Node | null;
|
224
|
|
-
|
225
|
|
- /**
|
226
|
|
- * Renders the contents of the chat panel.
|
227
|
|
- *
|
228
|
|
- * @private
|
229
|
|
- * @returns {ReactElement | null}
|
230
|
|
- */
|
231
|
|
- _renderPanelContent() {
|
232
|
|
- const { _isModal, _isOpen, _showNamePrompt } = this.props;
|
233
|
|
- let ComponentToRender = null;
|
234
|
|
-
|
235
|
|
- if (_isOpen) {
|
236
|
|
- if (_isModal) {
|
237
|
|
- ComponentToRender = (
|
238
|
|
- <ChatDialog isPollsEnabled = { this.props._isPollsEnabled }>
|
239
|
|
- { _showNamePrompt
|
240
|
|
- ? <DisplayNameForm isPollsEnabled = { this.props._isPollsEnabled } />
|
241
|
|
- : this._renderChat() }
|
242
|
|
- </ChatDialog>
|
243
|
|
- );
|
244
|
|
- } else {
|
245
|
|
- ComponentToRender = (
|
246
|
|
- <>
|
247
|
|
- { this._renderChatHeader() }
|
248
|
|
- { _showNamePrompt
|
249
|
|
- ? <DisplayNameForm isPollsEnabled = { this.props._isPollsEnabled } />
|
250
|
|
- : this._renderChat() }
|
251
|
|
- </>
|
252
|
|
- );
|
253
|
|
- }
|
254
|
|
- }
|
255
|
|
- let className = '';
|
256
|
|
-
|
257
|
|
- if (_isOpen) {
|
258
|
|
- className = 'slideInExt';
|
259
|
|
- } else if (this._isExited) {
|
260
|
|
- className = 'invisible';
|
261
|
|
- }
|
262
|
|
-
|
263
|
|
- return (
|
264
|
|
- <div
|
265
|
|
- aria-haspopup = 'true'
|
266
|
|
- className = { `sideToolbarContainer ${className}` }
|
267
|
|
- id = 'sideToolbarContainer'
|
268
|
|
- onKeyDown = { this._onEscClick } >
|
269
|
|
- { ComponentToRender }
|
270
|
|
- </div>
|
271
|
|
- );
|
272
|
|
- }
|
273
|
|
-
|
274
|
271
|
/**
|
275
|
272
|
* Scrolls the chat messages so the latest message is visible.
|
276
|
273
|
*
|